Adding a New Persistence
So you want to connect Vantage to a new database? This guide walks through the process in nine incremental steps — each one unlocks more framework features. You don’t have to implement all nine; stop whenever your persistence has enough capability for your use case.
- Overview
- Step 1: Type System
- Step 2: Expressions
- Step 3: Operators
- Step 4: Query Builder
- Step 5: Table & CRUD
- Step 6: Relationships
- Step 7: Multi-Backend Applications
- Step 8: Vista
- Step 9: Contained Relations
Overview
| Step | What you build | What it unlocks | Can skip? |
|---|---|---|---|
| 1. Type System | vantage_type_system! macro, AnyType, Record conversions | Type-safe values, struct ↔ record mapping | Required |
| 2. Expressions | Vendor macro, ExprDataSource | Execute raw queries, cross-database defer() | Skip for simple read-only sources (CSV) |
| 3. Operators | Vendor operation trait, typed .eq()/.gt()/.in_() | Ergonomic typed conditions instead of raw expression macros | Skip if you don’t expose typed columns |
| 4. Query Builder | Selectable, SelectableDataSource | Composable SELECT with conditions, ordering, limits | Skip if your persistence has no query language |
| 5. Table & CRUD | TableSource, entity tables, aggregates, writes | Table<DB, Entity>, full CRUD, ReadableDataSet, WritableDataSet | Required for table support |
| 6. Relationships | with_one, with_many, correlated subqueries | Reference traversal, expression fields | Skip if you don’t need cross-table queries |
| 7. Multi-Backend | model crate, vista_factory().from_table(), CLI example | Type-erased tables, generic UI/API code | Skip if you only use one persistence |
| 8. Vista | <Driver>VistaFactory, <Driver>TableShell, YAML extras | YAML-defined data handles consumed by UI / scripting / agents | Skip if you don’t need Vista support |
| 9. Contained Relations | embedded object/array columns as sub-tables | JSON/document sub-records surfaced as editable child sets | Skip if you have no embedded collections |
Step 1: Type System
Every database has its own idea of what types exist. The vantage_type_system! macro generates a
type trait, variant enum, and type-erased AnyType wrapper that prevents silent casting between
incompatible types.
You’ll implement the type trait for each Rust type your database supports, set up Record
conversions (free via serde for JSON-based backends, or via #[entity] macro for custom value
types), and add TryFrom<AnyType> for scalar extraction.
Step 2: Expressions
With types in place, build a vendor macro (sqlite_expr!, surreal_expr!) that produces
Expression<AnyType> with typed parameters. Implement ExprDataSource to execute expressions
against your database — handling parameter binding, deferred cross-database resolution, and result
parsing.
Skip this step if your persistence evaluates conditions in-memory (like CSV) — you can implement
TableSource directly without an expression engine.
Step 3: Operators
Build vendor-specific operation traits so typed columns get ergonomic .eq()/.gt()/.in_()
methods that produce your backend’s native condition type. Blanket-implemented over all
Expressive<T> so users don’t have to write raw expression macros for every condition.
Skip this step if you don’t expose typed columns to end users.
Step 4: Query Builder
Build a SELECT struct implementing the Selectable trait — fields, conditions, ordering, limits,
aggregates. Wire it up through SelectableDataSource so the rest of Vantage can create and execute
queries through a standard interface.
Skip this step if your persistence doesn’t have a query language. MongoDB, for instance, skips
Selectable and uses native BSON pipelines instead.
Step 5: Table & CRUD
Implement TableSource to give Vantage full table abstraction — columns, conditions, ordering,
pagination, entity CRUD, and aggregates. This is where Table<DB, Entity> comes alive and
auto-implements ReadableDataSet, WritableDataSet, and ActiveEntitySet.
Start with todo!() for every method and implement them incrementally, driven by tests.
Step 6: Relationships
Declare with_one and with_many relationships on tables and traverse them with get_ref_as.
Implement column_table_values_expr for subquery-based traversal and optionally
related_correlated_condition for correlated subqueries (expression fields like computed counts).
Skip this step if your persistence is flat (no foreign keys or cross-collection references).
Step 7: Multi-Backend Applications
Define your entities and table constructors once in a model crate, then erase the backend type with
db.vista_factory().from_table(table) so generic UI, CLI, and API code works identically across
SurrealDB, SQLite, CSV, MongoDB, or your new persistence. (Type erasure used to live in the
now-removed AnyTable; it moved up to Vista in
0.5.)
Step 8: Vista
Wrap your typed Table<Driver, E> as a Vista — the universal, schema-bearing handle that UI,
scripting, and agents consume through a CBOR / String boundary. Adds YAML schema loading and
condition delegation so callers don’t need a Rust entity struct.
Skip this step if your driver is only consumed from typed Rust code.
Step 9: Contained Relations
Surface an embedded object or array column (a JSON document, a Mongo sub-document) as an editable
child sub-table. Declare it with with_contained_one / with_contained_many; reads project the
embedded collection as records and writes patch it back into the host column. Native document
backends and JSON-blob columns share the same path.
Skip this step if your persistence has no embedded collections.