Skip to main content

What Does a Governed Data Runtime Cost? TeaQL vs Diesel and SeaORM on MusicBrainz

· 10 min read
Philip Z
Architect

TeaQL does more during a query than map database rows into Rust structs.

It preserves loaded, null, and not-loaded state. It builds an identity-consistent object graph. It carries query purpose and diagnostic comments. The same runtime also supports checker/fix behavior, mutation ledgers, audit boundaries, and cross-data-source relations.

That raises a fair question: what does this additional runtime model cost on a real relational graph?

We tested the current TeaQL Rust runtime against Diesel and SeaORM using a PostgreSQL copy of the public MusicBrainz database. The result is more useful than a simple winner: across three relationship shapes, TeaQL consistently landed between Diesel and SeaORM—and came within 3.4% of Diesel on one five-query graph.

The result first

These are medians from the current local rerun on August 25, 2026, after the TeaQL Rust 4.2.25 relation-state changes:

WorkloadDieselTeaQLSeaORMTeaQL relative position
MB01: seven-level release graph2.325 ms2.925 ms3.898 ms25.8% slower than Diesel; 25.0% faster than SeaORM
MB02: wide artist-credit graph49.949 ms57.587 ms64.560 ms15.3% slower than Diesel; 10.8% faster than SeaORM
MB03: generic recording/work graph2.769 ms2.864 ms3.932 ms3.4% slower than Diesel; 27.2% faster than SeaORM

Every framework passed the same cardinality and checksum gate before its timing was accepted. A fast result with a missing relation, projection, active-version predicate, or duplicate path was a failed run, not a benchmark result.

This is not evidence that TeaQL is universally faster than SeaORM, or that Diesel is universally faster than TeaQL. It is evidence that TeaQL's generated, identity-preserving relationship loading is competitive on these three graph shapes while retaining a broader runtime contract.

Three graph shapes, not one convenient query

A single benchmark can accidentally select the exact shape one implementation optimizes best. We therefore retained three materially different reads.

MB01: a deep release graph

MB01 follows seven entity types:

Release
-> Medium
-> Track
-> Recording
-> ArtistCredit
-> ArtistCreditName
-> Artist

Each operation returns:

  • 10 releases;
  • 14 media;
  • 133 tracks;
  • 176 artist-credit-name path traversals;
  • 176 artist traversals;
  • checksum 13,877,887,930.

All three implementations execute seven parameterized statements and project the same IDs, versions, names, and foreign keys. This workload tests depth and repeated identities more than raw row volume.

TeaQL's application query is assembled from generated, typed request pieces. For example, the reusable lower half of the graph can be named once:

fn artist_credit_graph() -> ArtistCreditRequest {
Q::artist_credits_minimal()
.select_name()
.select_artist_credit_name_list_with(
Q::artist_credit_names_minimal()
.select_name()
.select_artist_with(Q::artists_minimal().select_name()),
)
}

fn recording_graph() -> RecordingRequest {
Q::recordings_minimal()
.select_name()
.select_artist_credit_with(artist_credit_graph())
}

fn medium_graph() -> MediumRequest {
Q::mediums_minimal()
.select_name()
.select_track_list_with(
Q::tracks_minimal()
.select_name()
.select_recording_with(recording_graph()),
)
}

Q::releases_minimal()
.select_name()
.select_medium_list_with(medium_graph())
.order_by_id_desc()
.limit(10)
.comment("what: repeatedly load the MB01 release graph")
.purpose("why: compare mainstream ORM graph loading")
.execute_for_list(&context)
.await?;

The helper functions are ordinary typed request builders: they can be reused, extended, or nested in another request without falling back to SQL strings or an untyped include specification. This makes a large query read more like a set of Lego pieces than one monolithic expression. comment and purpose remain on the final execution boundary because they describe this concrete operation, not a reusable projection fragment.

The composed request is expanded into one query per graph level. The runtime installs the results into a flat identity graph and exposes typed relationships without requiring application-owned lookup maps.

MB02: a wide collaboration graph

MB02 selects the 100 ArtistCredit rows with the largest artist_count above one, loads their ordered ArtistCreditName members, and resolves every typed Artist.

Each accepted operation returns:

  • 100 artist credits;
  • 2,090 memberships;
  • 2,090 resolved artists;
  • zero artist_count mismatches;
  • checksum 2,584,441,647.

This graph executes only three queries, but materializes 2,290 identity-bearing entities across 2,090 relation paths. Here TeaQL was 15.3% behind Diesel and 10.8% ahead of SeaORM. The result suggests that TeaQL's graph assembly scales reasonably as decoding and relationship volume dominate fixed runtime costs.

MusicBrainz represents recording-to-work relationships through a generic link model. MB03 selects 100 recent recordings that have work relations, loads up to ten association rows per recording, and resolves both sides:

Recording
-> LRecordingWork
-> Link -> LinkType
-> Work

The accepted result contains 103 association rows, 103 links, 103 link types, and 103 works. No parent exceeds the per-parent limit, and the checksum is 6,601,766,716.

This workload performs five database queries for only 512 total entities. It is therefore sensitive to fixed query, repository, and graph-assembly costs. In the current rerun, TeaQL was only 3.4% behind Diesel and 27.2% ahead of SeaORM.

What “mainstream usage” means here

We did not ask every framework to imitate TeaQL's internal strategy or race on hand-written SQL.

  • Diesel uses derived Associations, belonging_to, and grouped_by, plus typed eq_any parent lookups where the traversal direction requires them.
  • SeaORM uses LoaderTrait::load_many, LoaderTrait::load_one, and its typed query API.
  • TeaQL uses its generated Q API and nested select_*_with relation selection.

No implementation uses raw SQL, stored procedures, custom PostgreSQL functions, or a workload-specific result cache. A single join across the whole graph was deliberately excluded because multiple to-many edges multiply rows and do not represent a robust default loading strategy.

The database sees different parameterization details where each framework has its own normal policy. TeaQL uses scalar equality for one ID, scalar IN for small sets, and PostgreSQL = ANY($1) for larger sets. That stable array-bound shape avoids expanded-parameter limits; it is not presented as proof of a universal speed advantage.

Measurement protocol

The current comparison used:

  • local PostgreSQL with the same MusicBrainz dataset for all frameworks;
  • Rust --release builds;
  • sequential operations with no client-side concurrency;
  • connection and runtime construction outside the measured interval;
  • three complete warm-up operations per framework process;
  • five framework-order-rotated rounds;
  • 100 complete operations per MB01 result;
  • 20 complete MB02 and MB03 operations per result;
  • SQL and process logging disabled for all measured frameworks;
  • full graph traversal and checksum validation in every process.

We report medians because the machine showed material round-to-round variance. Publishing extra decimal places would imply accuracy the experiment does not have.

Two more TeaQL workloads, without an ORM ranking

We also reran two exploratory TeaQL-only scenarios after moving reverse relations out of generated entity storage:

WorkloadTeaQL medianCorrectness gate
MB04: 1,000 releases and release-country geography8.381 ms1,035 availability rows, 42 distinct areas, zero missing targets
MB05: concurrent alias search across three entity types45.068 ms204 resolved parents, zero missing targets

We have not yet implemented equivalent mainstream Diesel and SeaORM versions of MB04 and MB05. These numbers are useful runtime regression evidence, but they are not a cross-framework comparison.

That distinction matters. A benchmark becomes less credible every time an author quietly converts “not measured” into “probably faster.”

Why relations moved out of Rust entity structs

TeaQL 4.2.25 changes how generated reverse relations are represented. A parent entity no longer embeds every reverse SmartList<Child> in its structure. Instead, the runtime owns the loaded identity graph, and generated accessors return a RelationHandle with three explicit states:

Loaded the selected relationship contains values
Empty the relationship was selected and contains no values
NotLoaded the relationship was not selected

There is no implicit database access when an accessor is called.

This reduces generated type-graph growth and makes cyclic models easier for Rust to compile and represent. It also keeps the public API compact: business code still asks the entity for its typed relation, while the data lives in the runtime identity graph.

The trade-off is measurable. Direct traversal through a relation handle is slightly more expensive than reading an embedded list. In MB01, traversal was roughly 0.144 ms in the current run versus about 0.062 ms in an earlier retained baseline. The absolute difference is small compared with database and hydration time, but we report it because removing an embedded field is not literally free.

Overall MB01–MB05 query performance did not regress as a group, and all correctness gates remained stable. We do not attribute every improvement to RelationHandle; the current runtime also contains compact-row decoding, prepared-statement caching, reduced metadata allocation, and flatter identity hydration introduced during the same optimization period.

Performance is not the whole runtime contract

Diesel is an excellent reference point for efficient, strongly typed Rust database access. SeaORM provides a productive async ORM model with generated entities and relationship loaders. TeaQL targets a different application boundary: one governed domain model generates consistent APIs across Java, Rust, TypeScript, Swift, Python, .NET, and Go.

The measured TeaQL query path additionally preserves:

  • typed loaded/null/not-loaded semantics;
  • identity-consistent relationship graphs;
  • mandatory query purpose and diagnostic comments;
  • active-version filtering;
  • a shared contract with checker/fix, audit, and mutation APIs;
  • runtime-managed cross-data-source relationship capability.

A query log that records intent, not only SQL

The two strings at the end of the benchmark request are not decorative:

.comment("what: repeatedly load the MB01 release graph")
.purpose("why: compare mainstream ORM graph loading")

TeaQL carries both into query diagnostics. The SQL tells an operator what the database executed; comment identifies the concrete operation, while purpose records why the application asked for it. That distinction is useful when a slow query, an unexpected access path, or AI-generated application code must be reviewed after the fact. It also gives observability systems a stable piece of business intent instead of asking an operator to reconstruct intent from a prepared statement and a stack trace.

There is a small discipline cost: application queries must declare their intent. We consider that a feature of a governed runtime, especially when more code is produced or modified by AI agents.

The benchmark is part of a harness, not a one-off race

TeaQL is developed with the TeaQL Agent Kit, a Harness Engineering workflow that gives coding agents model rules, generated Assist material, executable checks, and retained evidence. The goal is not merely to make an agent produce a query that compiles once. The harness repeatedly checks whether model evaluation, generation, runtime behavior, and documentation still agree as the system evolves.

The same domain model and API concepts are maintained across seven generated runtime families: Java, Rust, TypeScript, Swift, Python, .NET, and Go. Each language keeps its native naming and type-system conventions, while TeaQL's Q, E, mutation, checker/fix, audit, loaded-state, and Assist vocabulary provide a shared conceptual contract. Conformance evidence matters here: “seven languages” is an engineering obligation to detect and close gaps, not a claim that every implementation already has identical maturity or performance.

Not every one of those capabilities is exercised by these read-only queries, so the benchmark cannot assign a precise cost to each feature. They explain why our goal is not to turn TeaQL into a thin SQL tuple decoder. The engineering target is to keep the governed runtime close enough to the fastest mainstream typed approach that teams do not have to abandon the higher-level contract for ordinary application queries.

On this MusicBrainz sample, that target is credible: TeaQL is not the fastest in the table, but it is consistently competitive—and the gap is small enough to measure rather than hand-wave. The value proposition is therefore not “the fastest Rust query at any cost.” It is competitive query performance together with explainable intent, a continuously exercised AI coding harness, and one domain contract that can travel across seven languages.

TeaQL Rust is available at github.com/teaql/teaql-rs. The benchmark article and its follow-up evidence are tracked publicly in teaql-io-site issue #13.


Authorship disclosure: This article was written by Philip Zhang with LLM-assisted drafting and editing. The benchmark design, execution, validation, and conclusions were reviewed by the author.