Changelog
All notable changes to Mabat are listed here. The format follows Keep a Changelog, and versions follow Semantic Versioning. Until 1.0, minor versions may change the API.
[Unreleased]
Section titled “[Unreleased]”The first release, with milestones 1 to 5 of the design, the DBA tooling, MySQL and SQLite.
- Views (M1).
- Embedded structs may be generic over types (
Range<T>), each instantiation a shape of its own; fields typed by a parameter implementmabat::GenericColumn. #[derive(View)]on structs: columns,Optioncolumns, embedded structs with column prefixes, to-many collections and to-one references.- Loading with one root query plus one batched
WHERE fk = ANY($1)query per relationship. - Results decoded by column alias, each alias found once per query result and then read by position. Child rows are grouped by parent key in one list, with a fast hasher. Loading 1,000 tasks of 10 subtasks takes about 3% longer than hand-written SQLx running the same queries.
- Errors that name the view and the field path.
- Embedded structs may be generic over types (
- Enums with data (M2).
- The
tagstrategy: variant columns in the row. - The
table_per_variantstrategy: a batched query per variant table, sent only the keys of rows of that variant. - Nested enums, tuple variants and PostgreSQL enum tags.
- Strict decoding of unknown and NULL tags, columns of other variants and missing variant rows, with
lenient. #[view(json)]fields decoded withserde.
- The
- Filters and counting.
mabat::filter::col(..)conditions (eq…ilike,is_in, groups and!) on the root query, andLoad::count. - Overrides (M3).
- Override files (TOML or
.sql) replace any query by name, with no code changes. - Every generated and overridden query is checked against the views and the database at startup:
aliases, types, keys and parameters, reported like compiler errors (
M0100–M0105). - Schema drift in generated queries is reported too.
OnInvalid::UseGenerated, shadow mode with mismatch and timing statistics, runtime reloading withMabat::reload, andmabat::scaffold.
- Override files (TOML or
- Collections and recursion (M4).
- Ordered lists placed by an
indexcolumn. BTreeMap/HashMapcollections keyed by a column.- Many-to-many collections
througha link table. - Recursive views loaded level by level (
depth = n) or with oneWITH RECURSIVEquery (recursive = "cte"), with cycle detection. - Chains of parents: a
to_onereference back to its own view, held in anOption<Box<T>>orOption<Arc<T>>, withdepth = n, orrecursive = "cte"for every chain to its end in one query.Box<T>is accepted for any to-one reference.
- Ordered lists placed by an
- Shared values and graphs (M5).
Arc<T>fields shared per entity.Ref<T>fields loaded withLoad::graphinto aGraphof arenas, with generated navigation methods.- Cycles without
Rc,WeakorRefCell. Each entity is fetched once and each relationship is loaded once.
- DBA tooling.
Builder::manifestwrites the views’ queries, aliases and accepted types as JSON.- The
mabatcommand line tool (cratemabat-cli) runscheck(against a database or a schema file, in a transaction that is rolled back),explainandscaffold, with no Rust toolchain. mabat scaffold --query <name>scaffolds only the queries being tuned, and--out <dir>writes them to the view’s override file, adding them to an existing file without replacing a query it overrides.
- MySQL and SQLite (M6).
- The
postgres(default),mysqlandsqlitefeatures. A view is decoded on each enabled database, and a load runs on the database of its connection;#[view(databases = "...")]limits a view to some of them. - Keys bound as
IN (?, …)lists, padded to a power of two so statements are reused, and split into statements of at most 1,000 keys for child queries. :keysin override SQL for the keys of a batched query on any database.- Checks, manifests and
mabat checkon both. With--schema, MySQL checks in a temporary database that is dropped afterwards, and SQLite in an in-memory database. - On MySQL: unsigned integer keys,
BINARY(16)UUID keys andENUMtags.
- The
- JSON and selections (M7).
Load::jsonloads views as JSON, andLoad::selectwith aSelection(built in code or parsed from GraphQL-like text) loads only the selected fields: only their columns are selected and only their child queries run. Recursive and graph views load as trees as deep as the selection. The key column keeps the key field’s name even when the field is not selected, so overridden queries load selections too.Load::graph_jsonwrites a whole graph as JSON that keeps identity: each entity once with an$id("Employee:2"), and{"$ref": id}everywhere else. - An example application.
crates/mabat-example-chinook: a web service on the Chinook music store with REST and GraphQL from the same views, NDJSON streaming, playlists saved with generated keys, a DBA’s override, and a build script that checks the views against a schema snapshot. - The MPA specification.
docs/mpa.md, the Mabat Persistence Architecture, states Mabat’s contract as numbered rules;docs/mpa.jsonindexes its capabilities, attributes, functions, errors and diagnostics, andllms.txtpoints AI tools to both. A test keeps the index in step with the derive and the errors. - For AI tools.
llms.txt, whose capabilities and unsupported features are generated from the specification’s index, served withllms-full.txt(README, guides and specification in one file) by the documentation site; themabatcrate carriesllms.txt,MPA.mdandmpa.json. - Benchmarks.
benches/orm-comparisonloads the same nested data with Mabat, hand-written SQLx, SeaORM and diesel-async at 1, 100 and 10,000 roots with criterion; the results are in the performance guide. - Reports.
Load::sqlruns SQL as the root query, for aggregates, joins and other rows that are not a table, with named parameters (:name) bound byLoad::bind; a root override may take named parameters too.#[view(computed)]fields hold values the SQL computes; the generated query does not select them, override checks require them, and loads may order and filter by them. The view’s collections and references load as usual.Error::Paramsreports a parameter without a value, a value without a parameter, or a computed field without SQL. - Tracing.
tracingspans at thedebuglevel:mabat.loadand the other operations with their view,mabat.queryfor each query of a load (view, query name, path, override, keys, rows), andmabat.statementfor each statement (database, SQL, rows,elapsed_ms). Bound values are never traced. - Streaming.
Load::streamloads values a batch at a time as afutures::Stream, holding one batch in memory: one query reads the keys of every match, with the filter, order and page, then each batch ofbatch_sizekeys (1,000 by default) is loaded with its collections and references, yielding the values in the order ofall. On a PostgreSQL connection the keys are fetched a batch at a time from aWITH HOLDcursor, so the server holds them rather than the stream. A value deleted between batches is skipped outside a snapshot; in aREPEATABLE READtransaction or withPooled::snapshot, every batch sees one snapshot.Load::json_streamstreams JSON, of a selection or of every field. Graph views cannot be streamed. - Schema snapshots.
mabat schemawrites a snapshot of a database’s schema as JSON (mabat/schema.json): tables and views, columns with their types, nullability and generated values, and primary and foreign keys, sorted so that diffs are readable.mabat schema --checklists how a database differs from a snapshot and exits with status 1, for CI. The manifest of the views (format 2) records the tables and columns behind each query. The newmabat-checkcrate holds the snapshot, with no database driver, for build scripts.mabat check --snapshot mabat/schema.json(orManifest::check_snapshot) checks the views against a snapshot with no database: missing tables and columns, column types the fields cannot be decoded from, nullable columns under fields that are notOption, mismatched link keys, keys that are not primary keys, and generated keys the database does not generate (M0201–M0207). The manifest records which views have generated keys. A build script can run the same check,mabat_check::build("mabat/views.json", "mabat/schema.json").run(), so thatcargo buildfails when the views no longer match the schema (crates/mabat-example-build). The manifest types,explain,scaffold, the override file parser and the report moved tomabat-check, with no database driver, and are re-exported where they were;Manifest::check(conn, dirs)is nowmabat::manifest::check(&manifest, conn, dirs). - Saving many values (M8).
mabat::save_all(&mut values, conn)saves many aggregates in one transaction with statements per table and level rather than per row: one multi-rowUPDATE(from a table of values, cast to the column types on PostgreSQL) and one multi-row insert for the rows of each table, then their collections, variant rows and links the same way. Saving 1,000 tasks of 10 subtasks takes about 80 ms instead of 1.6 s on a local PostgreSQL (examples/save_all.rs), and far less time waiting on the network for a remote one. - Saving graphs (M8).
mabat::save_graph(&mut graph, conn)saves every entity of aGraph, each after the entities it references, so that generated keys exist and foreign keys hold; in a cycle, optional references are written NULL and set afterwards.Vec<Ref<T>>collections replace their links, write their elements’ foreign key, or, as the inverse of the elements’ reference, are checked to agree with it.Graph::new,Graph::insertandGraph::add_rootbuild graphs in code, anddeleteremoves an entity’s links with it.mabat::save_graph_changessaves only the entities inserted or handed out byget_mutsince the graph was loaded or saved (Graph::is_changed), with the foreign keys their collections write to other entities’ rows. - Keys generated by the database (M8).
#[view(generated)]on anOptioninteger key field:saveinserts a value without a key and reads the key the database generated (RETURNING, orLAST_INSERT_ID()on MySQL), writes what it owns under it, and writes the keys back into the value and its elements. New elements without a key are inserted bysaveandsave_changesalike. - Saving changes and optimistic locking (M8).
mabat::save_changes(&before, &mut after, conn)writes only what changed: the columns that differ, new and removed elements, and links that differ.#[view(version)]versions rows; a stale version fails withError::Conflict, and new versions are written back into the value.mabat::savenow takes the value by&mut. - Saving aggregates (M8).
mabat::savecreates or replaces a value and makes its owned collections, links and variant tables match it, in one transaction;mabat::deletedeletes an aggregate with all it owns. On every database. A row is updated first and inserted only when no row has its key, so a view of some of a table’s columns saves them in an existing row. - Arguments of nested collections.
Load::nestedwith aNestedfilters, orders and pages the elements of a to-many collection for each parent, in the collection’s one query, also through overrides. Nested GraphQL lists takewhere,orderBy,limitandoffset. - GraphQL (M7). The
mabat-graphqlcrate generates an async-graphql schema from views: object types, unions for enums with data, GraphQL enums, map entries, and root fields withwhere,orderBy,limit,offsetand lookup by key. Each root field is one load of the selected fields. An example server serves Chinook with GraphiQL. - Concurrent loads (M6).
Pooled::snapshot(PostgreSQL) andPooled::read_committed(any database) run the queries of each level of a load at the same time on connections of a pool, in a snapshot that the connections share or reading what is committed. Loads, counts, checks and registries take aPooledwhere they take a connection. - Platform. PostgreSQL, MySQL 8 or later, or SQLite, with SQLx 0.9, and Rust 1.94 or later.
- End-to-end tests against the Pagila and Chinook sample databases, and Chinook on MySQL and SQLite.