Models & collections¶
Audience: developers using the model-first API in Python or Rust.
This is the core ergonomics story for ModelVault: your class or struct is the schema. This guide covers how dataclasses, Pydantic models, and Rust types map to collections—naming, registration, compatibility checks, and subset models for read projections.
New here? Start with Why ModelVault or Quickstart. Full apps: Examples.
What ships in 1.0¶
| Surface | Capability |
|---|---|
| Rust | register_model::<T>(), collection::<T>(), subset models as compatible catalog subsets |
| Python | modelvault.models.collection(db, Model) — auto-register on first use; reopen validates against catalog |
| Python (asyncio) | modelvault.models.async_collection(db, Model) with AsyncDatabase — same schema markers; use with await (FastAPI guide) |
| Projections | ModelQuery.select([...]) / all(fields=[...]) (Python); subset QueryBuilder::all() (Rust) |
| Naming | Rust #[db(collection = "...")]; Python __modelvault_collection__ or pluralized snake_case |
Coming in 1.1
Projection-aware decode (skip unused columns at record layer); DbModel derive for nested paths and constraints. See roadmap.
Related: Python guide · Async policy (use async_collection for concurrent reads in asyncio apps)
Collection identity vs name¶
| Concept | Behavior |
|---|---|
| Collection ID | Stable internal identity — never changes |
| Collection name | Human-facing handle in APIs and CLI |
Rename a Python class or Rust struct without touching stored data by keeping the same collection name override.
Default collection names¶
Pluralized snake_case of the class name:
| Class | Default collection |
|---|---|
User |
users |
OrderLine |
order_lines |
Override: __modelvault_collection__ = "users"
Default is the Rust type name (e.g. User).
Override: #[db(collection = "users")] or DbModel::collection_name()
Registering models¶
| Language | Pattern |
|---|---|
| Rust | db.register_model::<Book>() then db.collection::<Book>() |
| Python | modelvault.models.collection(db, Book) |
| Python (asyncio) | await-able: books = modelvault.models.async_collection(db, Book) |
Compatibility on reopen¶
- Collection missing → create with model schema
- Collection exists → model fields must be a compatible subset of the catalog (same PK; each path/type must match). Full-schema models must match index definitions too
Schema version changes use plan_schema_version / register_schema_version (Python: modelvault.models.plan / apply) — not silent re-registration.
Subset models¶
Define a type with fewer fields than the stored collection to reduce materialization at the API layer.
Semantics¶
- Read projections only — do not alter storage
- Every declared path must exist in the catalog with matching type
- Undeclared catalog fields are omitted from results
- Writes through a subset model validate only the fields you provide — use the full model for complete rows
Python example¶
@dataclass
class Book:
__modelvault_primary_key__ = "id"
id: int
title: str
year: int
@dataclass
class BookTitle:
__modelvault_primary_key__ = "id"
__modelvault_collection__ = "books" # same collection as Book
id: int
title: str
books = modelvault.models.collection(db, BookTitle)
rows = books.where("id", 1).all()
Rust example¶
See subset_models.rs on GitHub.
Performance (1.0)¶
Full rows are decoded internally, then projected in memory. Skipping decode for unused fields is a 1.1 optimization.
Common use cases¶
- UI list views (
UserSummary) - Partial nested reads
- Low-latency endpoints that do not need full records
Naming + subsets together¶
Subset models target the same collection name as the full model. Compatibility checks run against the catalog entry for that name.