Skip to main content

ModelRepository

Trait ModelRepository 

Source
pub trait ModelRepository: Send + Sync {
    // Required methods
    fn list<'life0, 'async_trait>(
        &'life0 self,
    ) -> Pin<Box<dyn Future<Output = Result<Vec<Model>, RepositoryError>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait;
    fn get_by_id<'life0, 'async_trait>(
        &'life0 self,
        id: i64,
    ) -> Pin<Box<dyn Future<Output = Result<Model, RepositoryError>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait;
    fn get_by_name<'life0, 'life1, 'async_trait>(
        &'life0 self,
        name: &'life1 str,
    ) -> Pin<Box<dyn Future<Output = Result<Model, RepositoryError>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait,
             'life1: 'async_trait;
    fn insert<'life0, 'life1, 'async_trait>(
        &'life0 self,
        model: &'life1 NewModel,
    ) -> Pin<Box<dyn Future<Output = Result<Model, RepositoryError>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait,
             'life1: 'async_trait;
    fn find_by_path<'life0, 'life1, 'async_trait>(
        &'life0 self,
        path: &'life1 Path,
    ) -> Pin<Box<dyn Future<Output = Result<Option<Model>, RepositoryError>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait,
             'life1: 'async_trait;
    fn update<'life0, 'life1, 'async_trait>(
        &'life0 self,
        model: &'life1 Model,
    ) -> Pin<Box<dyn Future<Output = Result<(), RepositoryError>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait,
             'life1: 'async_trait;
    fn delete<'life0, 'async_trait>(
        &'life0 self,
        id: i64,
    ) -> Pin<Box<dyn Future<Output = Result<(), RepositoryError>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait;

    // Provided method
    fn get_by_identifier<'life0, 'life1, 'async_trait>(
        &'life0 self,
        identifier: &'life1 str,
    ) -> Pin<Box<dyn Future<Output = Result<Option<Model>, RepositoryError>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait,
             'life1: 'async_trait { ... }
}
Expand description

Repository for model persistence operations.

This trait defines CRUD operations for models. Implementations are responsible for all storage details (SQL, filesystem, etc.).

§Design Rules

  • No sqlx types in signatures
  • CRUD-only: list, get, insert, update, delete
  • Tags and search logic belong in ModelService, not here
  • The one exception is ModelRepository::get_by_identifier, a provided method: identifier resolution is a lookup-key policy, and it lives here precisely so that every facade over the repository shares one copy of it.

Required Methods§

Source

fn list<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = Result<Vec<Model>, RepositoryError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

List all models in the repository.

Source

fn get_by_id<'life0, 'async_trait>( &'life0 self, id: i64, ) -> Pin<Box<dyn Future<Output = Result<Model, RepositoryError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

Get a model by its database ID.

Returns Err(RepositoryError::NotFound) if the model doesn’t exist.

Source

fn get_by_name<'life0, 'life1, 'async_trait>( &'life0 self, name: &'life1 str, ) -> Pin<Box<dyn Future<Output = Result<Model, RepositoryError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Get a model by its name.

Returns Err(RepositoryError::NotFound) if no model with that name exists.

Source

fn insert<'life0, 'life1, 'async_trait>( &'life0 self, model: &'life1 NewModel, ) -> Pin<Box<dyn Future<Output = Result<Model, RepositoryError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Insert a new model into the repository, or update the existing row for the same model.

This upserts. Registering the same model twice is not an error: it overwrites the mutable columns and returns the existing row, id included. That is deliberate — post-download registration has to be safe to retry, and a re-scan must not fail on what it already knows.

It also means this method never reports a duplicate. A caller for whom “already there” is an error — an explicit “add this file to my library” rather than a registration — must ask Self::find_by_path first. ModelService::import_from_file does.

This doc used to promise Err(RepositoryError::AlreadyExists) on a duplicate file path. No implementation ever did that, and the promise is what made the silent overwrite hard to see.

Source

fn find_by_path<'life0, 'life1, 'async_trait>( &'life0 self, path: &'life1 Path, ) -> Pin<Box<dyn Future<Output = Result<Option<Model>, RepositoryError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Find the model registered under path, if there is one.

path is matched against the form the implementation stores.

An implementation that also stores a model’s sibling shard paths must match those too: adding shard 2 of a group already in the library is the same duplicate as adding shard 1, and only shard 1’s path lives in the primary path field. SqliteModelRepository in gglib-db does this; the test doubles in this workspace store no siblings and so match on the single path alone, which means a sharded duplicate is only observable in tests that use the real repository.

Callers pass a path already resolved by canonical_model_path; this method does not resolve it for them. That is the whole point of the split: resolution can fail, and a failure has to reach the caller as an error rather than decay into Ok(None), which reads as “no duplicate” and silently reinstates the overwrite this lookup exists to prevent.

Required rather than provided. A default body here would have to touch the filesystem, which ports/ does not do (see the design rules in this module’s README), and would scan the entire table per call in a trait that every test double inherits.

Source

fn update<'life0, 'life1, 'async_trait>( &'life0 self, model: &'life1 Model, ) -> Pin<Box<dyn Future<Output = Result<(), RepositoryError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Update an existing model.

Returns Err(RepositoryError::NotFound) if the model doesn’t exist.

Source

fn delete<'life0, 'async_trait>( &'life0 self, id: i64, ) -> Pin<Box<dyn Future<Output = Result<(), RepositoryError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

Delete a model by its database ID.

Returns Err(RepositoryError::NotFound) if the model doesn’t exist.

Provided Methods§

Source

fn get_by_identifier<'life0, 'life1, 'async_trait>( &'life0 self, identifier: &'life1 str, ) -> Pin<Box<dyn Future<Output = Result<Option<Model>, RepositoryError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Resolve a model by user-facing identifier: numeric database id first, then exact name.

This is the single lookup-key policy for the workspace — every facade over a repository (ModelService, the ModelCatalogPort adapter) delegates here rather than choosing its own key. Before this existed the two disagreed: the service resolved ids, the catalog port did not, so the same string resolved differently depending on which pipeline a request travelled down.

Provided rather than required so implementors and test doubles inherit it automatically.

Returns Ok(None) when nothing matches. A storage failure on the id lookup propagates rather than silently falling through to the name lookup — only a genuine NotFound continues.

Implementors§