Skip to main content

Why TeaQL Rust Removed SQLx

· 5 min read
TeaQL Code Gen
Core Contributor

TeaQL Rust originally treated SQLx as the obvious database foundation. It was mature, async, familiar to Rust backend developers, and already supported PostgreSQL, MySQL, and SQLite.

Then our AI coding workflow started complaining about the dependency graph.

At first, that sounded like a tooling annoyance. The agents were not saying anything profound: too many crates, too many features, too much compile-time surface, too many database-specific details leaking into the workspace. For a human developer, that may be acceptable. For an AI coding agent that needs to inspect, modify, compile, and explain a business runtime repeatedly, the cost is more visible.

But dependency weight was only the first signal. The stronger reason came later: SQLx did not fit the execution model we needed for TeaQL's execute_for_stream direction.

The First Signal: AI Felt the Dependency Cost

TeaQL is designed to be worked on by coding agents as well as humans. That changes how we evaluate a dependency.

A dependency is not only runtime code. It is also:

  • compile time
  • feature flags
  • generated type surface
  • error shapes
  • examples an agent may copy incorrectly
  • places where database concerns leak through the business API

SQLx is a good Rust library. The problem was not quality. The problem was fit.

TeaQL wants generated business APIs to stay small and explainable. The agent should mostly see domain queries, audit intent, relation paths, and provider capabilities. When the provider layer pulls in a large async SQL abstraction, the agent has more irrelevant surface to reason about before it can make the correct change.

That was annoying, but not yet decisive.

The Real Requirement: Streaming Is a Runtime Contract

TeaQL queries are not just SQL strings. A read can include selected fields, relation enhancement, grouped data, aggregate projections, trace comments, purpose metadata, and future provider capabilities.

For large result sets, TeaQL needs a streaming execution path. The high-level contract is closer to:

Q::orders()
.purpose("export orders")
.comment("stream order rows for report generation")
.stream(1000)
.execute_for_stream(&ctx)
.await?;

The exact generated terminal API can evolve, but the runtime requirement is stable: TeaQL must be able to fetch rows in chunks, enhance each chunk, preserve query intent metadata, and keep provider behavior explicit.

That is not the same as exposing a database driver's row stream directly.

Why SQLx Was the Wrong Boundary

SQLx is very good when the application code owns the SQL shape. TeaQL is different: the generated domain query owns the request shape, and the provider is an implementation detail below the runtime boundary.

For execute_for_stream, TeaQL needs the provider contract to say:

  • I can execute this compiled TeaQL query.
  • I can return chunks with clear boundaries.
  • I can preserve query comments and purpose metadata.
  • I can cooperate with relation enhancement after each chunk.
  • I can expose capabilities without forcing application code to depend on one driver model.

SQLx's streaming APIs are driver-oriented. TeaQL's streaming API is domain-runtime-oriented. The mismatch showed up in the abstraction: if we kept SQLx at the center, the stream would be shaped by SQLx first and TeaQL second.

That was backwards.

The Current Direction

TeaQL Rust now uses narrower provider crates:

  • teaql-provider-postgres for PostgreSQL through deadpool-postgres / tokio-postgres
  • teaql-provider-mysql for MySQL through mysql_async
  • teaql-provider-sqlite for SQLite through rusqlite

The important part is not the specific driver names. The important part is that each provider implements TeaQL's execution traits instead of making TeaQL inherit a general SQL abstraction.

The runtime can now model capabilities such as normal query execution, mutation execution, transaction execution, and streaming execution as TeaQL contracts. For SQLite, the provider already has chunked fetch behavior. Other providers can implement the same contract in the way that is natural for their driver.

What We Learned

The AI complaint was useful because it noticed friction early. It did not prove SQLx was wrong by itself. It pushed us to ask whether the dependency was helping the core model or making every agent and maintainer carry unrelated complexity.

The streaming requirement answered that question.

TeaQL is not trying to be a thin wrapper around a database driver. It is a generated business runtime. Reads carry intent. Writes carry audit descriptions. Relation loading, aggregation, tracing, and provider capabilities belong to the TeaQL layer.

Once execute_for_stream became a real requirement, the database layer had to follow TeaQL's runtime contract. Removing SQLx made that boundary clearer.

When Direct SQLx Still Makes Sense

This is not an argument against SQLx.

If an application is mostly hand-written SQL, if compile-time checked SQL queries are the main value, or if the project does not need generated domain APIs, direct SQLx can still be the right choice.

TeaQL is for a different shape of system: model-driven business software where the generated API, runtime audit trail, and execution contract matter more than exposing the database driver's native API.

That is why removing SQLx was not just dependency cleanup. It was a correction of the architecture boundary.