9.2 KiB
Repository Reference
Overview
Repositories implement the persistence contract defined in ADR-007 (Repository Pattern). Each repository targets a single aggregate root and follows the session-factory pattern: public methods obtain their own session from the factory, flush (but do not commit), and delegate transaction control to the caller or a UnitOfWork wrapper.
All public methods are decorated with @database_retry (ADR-033) to handle transient database errors.
LifecyclePlanRepository
Module: src/cleveragents/infrastructure/database/repositories.py
Provides CRUD operations for v3 lifecycle plans (v3_plans table and its child tables: plan_projects, plan_arguments, plan_invariants).
Constructor
LifecyclePlanRepository(session_factory: Callable[[], Session])
Methods
| Method | Signature | Description |
|---|---|---|
create |
create(plan) -> Plan |
Persist a new plan via LifecyclePlanModel.from_domain(). Handles child tables. Raises DuplicatePlanError on duplicate plan_id. |
get |
get(plan_id: str) -> Plan | None |
Retrieve by ULID primary key. Returns domain model or None. |
get_by_name |
get_by_name(namespaced_name: str) -> Plan | None |
Retrieve by full namespaced name. Returns domain model or None. |
update |
update(plan) -> Plan |
Update phase, processing_state, timestamps, actor refs, error fields, and replace all child rows. Raises PlanNotFoundError if the plan does not exist. |
delete |
delete(plan_id: str) -> bool |
Delete plan and cascade to child tables. Returns True if deleted, False if not found. |
list_plans |
list_plans(phase?, processing_state?, action_name?, project_name?, namespace?, limit=100, offset=0) -> list[Plan] |
Filter and paginate plans. Deterministic ordering by created_at DESC. |
count |
count(phase?, processing_state?) -> int |
Count matching plans with optional filters. |
Filters
The list_plans method supports the following filters, matching CLI flags:
- phase: Filter by lifecycle phase (
action,strategize,execute,apply). - processing_state: Filter by processing state (
queued,processing,errored,complete,cancelled,applied,constrained). - action_name: Filter by the action template name (FK to
actionstable). - project_name: Filter by project name (joins
plan_projectschild table). - namespace: Filter by plan namespace.
Results are always ordered by created_at DESC for deterministic pagination.
Custom Exceptions
| Exception | Parent | Description |
|---|---|---|
DuplicatePlanError |
DatabaseError |
Raised when creating a plan with a plan_id that already exists. |
PlanNotFoundError |
DatabaseError |
Raised when updating or deleting a plan that does not exist. |
Session and Transaction Pattern
# Session-factory pattern: each method gets its own session
repo = LifecyclePlanRepository(session_factory=lambda: session)
# All mutations flush but do NOT commit
plan = repo.create(plan_domain_object)
# Caller must commit:
session.commit()
Child Table Handling
- plan_projects: Stores plan-to-project links with alias and read_only flags.
- plan_arguments: Stores resolved argument values with stable ordering via
position. - plan_invariants: Stores invariant constraints with source provenance (
plan,action,project,global).
On create, child rows are populated from the domain model. On update, all child rows are replaced (clear + re-add) to ensure consistency.
ActionRepository
Module: src/cleveragents/infrastructure/database/repositories.py
Provides CRUD operations for v3 lifecycle actions (actions table and its child tables: action_arguments, action_invariants).
See ActionRepository class documentation in repositories.py for full method signatures.
Custom Exceptions
| Exception | Parent | Description |
|---|---|---|
DuplicateActionError |
DatabaseError |
Raised when creating an action with a name that already exists. |
ActionInUseError |
BusinessRuleViolation |
Raised when deleting an action still referenced by plans. |
Resource Registry Repositories
The resource registry persistence layer is implemented by two repositories in
src/cleveragents/infrastructure/database/repositories.py:
ResourceTypeRepository
Persists and retrieves ResourceTypeSpec domain objects backed by the
resource_types table.
| Method | Description |
|---|---|
create(resource_type) |
Persist a new resource type. Raises DuplicateResourceTypeError on conflict. |
get(name) |
Retrieve by primary key (namespaced name string). Returns None if missing. |
list_types(namespace?, user_addable?) |
List with optional namespace and user_addable filters, ordered by name. |
update(resource_type) |
Update mutable fields. Raises ResourceTypeNotFoundError if missing. |
delete(name) |
Delete by name. Raises ResourceTypeHasResourcesError if resources reference the type. Returns False if not found. |
Session pattern: Each method obtains its own session from the factory. Mutating methods flush but do not commit.
ResourceRepository
Persists and retrieves Resource domain objects backed by the
resources table.
| Method | Description |
|---|---|
create(resource) |
Persist a new resource. Validates type existence. Raises ResourceTypeNotFoundError or DuplicateResourceError. |
get(resource_id) |
Retrieve by ULID primary key. Returns None if missing. |
get_by_name(namespaced_name) |
Retrieve by unique namespaced name. Returns None if missing. |
list_resources(type_name?, namespace?, limit?, offset?) |
List with filters and pagination. |
update(resource) |
Update metadata. Raises ResourceNotFoundRepoError if missing. |
delete(resource_id) |
Delete by ULID. Raises ResourceHasEdgesError if DAG edges exist. |
resolve_namespaced_name(name_or_id) |
Try name lookup first, then fall back to ULID. Returns None if not found. |
Resource Registry Custom Exceptions
| Exception | Base | Purpose |
|---|---|---|
ResourceTypeNotFoundError |
DatabaseError |
Resource type lookup failed |
ResourceNotFoundRepoError |
DatabaseError |
Resource lookup failed |
ResourceTypeHasResourcesError |
BusinessRuleViolation |
Delete blocked by referencing resources |
ResourceHasEdgesError |
BusinessRuleViolation |
Delete blocked by DAG edges |
DuplicateResourceTypeError |
DatabaseError |
Name uniqueness violation |
DuplicateResourceError |
DatabaseError |
Name uniqueness violation |
Usage Example
from sqlalchemy.orm import sessionmaker
from cleveragents.infrastructure.database.repositories import (
ResourceTypeRepository,
ResourceRepository,
)
factory = sessionmaker(bind=engine)
rt_repo = ResourceTypeRepository(factory)
res_repo = ResourceRepository(factory)
# Create a resource type
rt_repo.create(my_resource_type_spec)
# Create a resource
res_repo.create(my_resource)
# Resolve by name or ULID
result = res_repo.resolve_namespaced_name("myorg/my-resource")
UnitOfWork Lifecycle Usage
Module: src/cleveragents/infrastructure/database/unit_of_work.py
The UnitOfWork class provides transactional access to all repositories. The UnitOfWorkContext exposes both legacy and v3 lifecycle repositories within a single transaction boundary.
Available Repositories on UnitOfWorkContext
| Property | Type | Description |
|---|---|---|
actions |
ActionRepository |
V3 lifecycle action persistence. |
lifecycle_plans |
LifecyclePlanRepository |
V3 lifecycle plan persistence. |
projects |
ProjectRepository |
Legacy project persistence. |
plans |
PlanRepository |
Legacy plan persistence (deprecated; use lifecycle_plans). |
contexts |
ContextRepository |
Context item persistence. |
changes |
ChangeRepository |
Change persistence. |
debug_attempts |
DebugAttemptRepository |
Debug attempt persistence. |
actors |
ActorRepository |
Actor persistence. |
Transaction Pattern
from cleveragents.infrastructure.database.unit_of_work import UnitOfWork
uow = UnitOfWork(database_url="sqlite:///app.db")
# All operations within the context manager are committed atomically
with uow.transaction() as ctx:
ctx.actions.create(action)
ctx.lifecycle_plans.create(plan)
# Both persisted on successful exit; rolled back on exception
Session-Factory Bridging
The v3 repositories (ActionRepository, LifecyclePlanRepository) use a session-factory pattern (Callable[[], Session]), while legacy repositories take a Session directly. UnitOfWorkContext bridges both patterns by providing a _session_factory() method that returns the transaction's session, ensuring all repositories operate within the same transaction boundary.
Cross-Repository Atomicity
Creating an action and a plan in the same transaction guarantees that either both persist or neither does:
with uow.transaction() as ctx:
ctx.actions.create(action)
ctx.lifecycle_plans.create(plan)
# If plan creation fails, the action is also rolled back