Skip to content

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.

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 implement mabat::GenericColumn.
    • #[derive(View)] on structs: columns, Option columns, 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.
  • Enums with data (M2).
    • The tag strategy: variant columns in the row.
    • The table_per_variant strategy: 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 with serde.
  • Filters and counting. mabat::filter::col(..) conditions (eq … ilike, is_in, groups and !) on the root query, and Load::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 with Mabat::reload, and mabat::scaffold.
  • Collections and recursion (M4).
    • Ordered lists placed by an index column.
    • BTreeMap/HashMap collections keyed by a column.
    • Many-to-many collections through a link table.
    • Recursive views loaded level by level (depth = n) or with one WITH RECURSIVE query (recursive = "cte"), with cycle detection.
    • Chains of parents: a to_one reference back to its own view, held in an Option<Box<T>> or Option<Arc<T>>, with depth = n, or recursive = "cte" for every chain to its end in one query. Box<T> is accepted for any to-one reference.
  • Shared values and graphs (M5).
    • Arc<T> fields shared per entity.
    • Ref<T> fields loaded with Load::graph into a Graph of arenas, with generated navigation methods.
    • Cycles without Rc, Weak or RefCell. Each entity is fetched once and each relationship is loaded once.
  • DBA tooling.
    • Builder::manifest writes the views’ queries, aliases and accepted types as JSON.
    • The mabat command line tool (crate mabat-cli) runs check (against a database or a schema file, in a transaction that is rolled back), explain and scaffold, 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), mysql and sqlite features. 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.
    • :keys in override SQL for the keys of a batched query on any database.
    • Checks, manifests and mabat check on 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 and ENUM tags.
  • JSON and selections (M7). Load::json loads views as JSON, and Load::select with a Selection (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_json writes 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.json indexes its capabilities, attributes, functions, errors and diagnostics, and llms.txt points 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 with llms-full.txt (README, guides and specification in one file) by the documentation site; the mabat crate carries llms.txt, MPA.md and mpa.json.
  • Benchmarks. benches/orm-comparison loads 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::sql runs SQL as the root query, for aggregates, joins and other rows that are not a table, with named parameters (:name) bound by Load::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::Params reports a parameter without a value, a value without a parameter, or a computed field without SQL.
  • Tracing. tracing spans at the debug level: mabat.load and the other operations with their view, mabat.query for each query of a load (view, query name, path, override, keys, rows), and mabat.statement for each statement (database, SQL, rows, elapsed_ms). Bound values are never traced.
  • Streaming. Load::stream loads values a batch at a time as a futures::Stream, holding one batch in memory: one query reads the keys of every match, with the filter, order and page, then each batch of batch_size keys (1,000 by default) is loaded with its collections and references, yielding the values in the order of all. On a PostgreSQL connection the keys are fetched a batch at a time from a WITH HOLD cursor, so the server holds them rather than the stream. A value deleted between batches is skipped outside a snapshot; in a REPEATABLE READ transaction or with Pooled::snapshot, every batch sees one snapshot. Load::json_stream streams JSON, of a selection or of every field. Graph views cannot be streamed.
  • Schema snapshots. mabat schema writes 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 --check lists 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 new mabat-check crate holds the snapshot, with no database driver, for build scripts. mabat check --snapshot mabat/schema.json (or Manifest::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 not Option, 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 that cargo build fails 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 to mabat-check, with no database driver, and are re-exported where they were; Manifest::check(conn, dirs) is now mabat::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-row UPDATE (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 a Graph, 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::insert and Graph::add_root build graphs in code, and delete removes an entity’s links with it. mabat::save_graph_changes saves only the entities inserted or handed out by get_mut since 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 an Option integer key field: save inserts a value without a key and reads the key the database generated (RETURNING, or LAST_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 by save and save_changes alike.
  • 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 with Error::Conflict, and new versions are written back into the value. mabat::save now takes the value by &mut.
  • Saving aggregates (M8). mabat::save creates or replaces a value and makes its owned collections, links and variant tables match it, in one transaction; mabat::delete deletes 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::nested with a Nested filters, 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 take where, orderBy, limit and offset.
  • GraphQL (M7). The mabat-graphql crate generates an async-graphql schema from views: object types, unions for enums with data, GraphQL enums, map entries, and root fields with where, orderBy, limit, offset and 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) and Pooled::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 a Pooled where 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.