Skip to content

Comparison & Synthesis

The capstone of the SQL & ORM abstraction survey. It reads the ~30 deep-dives against one another along the survey's fixed five-dimension spine, names the points the whole field agrees on, isolates the trade-offs that are still genuinely open, and closes with where an effects-first sparkles:sql fits.

Last reviewed: July 12, 2026

NOTE

The master catalog already gives the one-line-per-system summary; this page is the cross-cutting analysis. Every claim about a specific library is grounded in its deep-dive; follow the link for the verbatim source citation.


At-a-glance matrix

Each surveyed system by the two axes that most determine its character: how a query is made injection-safe, and what the library does with the effect of running it. The "Compile-time query check?" column is the sharpest differentiator — it separates libraries that catch a bad column or type before the program runs from those that discover it at runtime.

SystemRungInjection-safety mechanismCompile-time query check?Effect model
Go database/sqlDriverPositional placeholders (driver)NoBlocking (pool)
postgres.jsDriver + safe-SQLTagged template → $n paramsNoAsync (thenable)
DapperMicro-mapperAnonymous-object params → DbParameterNoBlocking / async
JDBIMicro-mapperNamed/positional bind (:id)NoBlocking
hasqlMicro-mapperPositional Encoders (out-of-band)No (opt-in via hasql-th)Blocking IO
Effect TSFunctional data mapperTagged template → auto ParameterNo (runtime compile)Effect value (typed SqlError)
doobieFunctional data mappersql"..."Put-bound ?No (opt-in .check vs live DB)ConnectionIO (cats-effect)
skunkFunctional data mapperTyped Codec fragments (never interpolates)No (runtime Describe)cats-effect + fs2
EctoFunctional data mapperQuery macros + ^ pin operatorPartial (macro-checked)Eager, blocking (BEAM)
QuillFunctional data mapperCompile-time quotation → liftsYes (static translation)Pluggable; ZIO
SlickFunctional-relationalLifted embedding (Rep[T])Yes (types)DBIO → effect-poly F
DieselTyped query builderFluent builder → bound paramsYes (types)Blocking
sqlxMacro-checked raw SQLReal bind paramsYes (query! vs live DB)Async
sqlcSQL-to-code generatorReal bind paramsYes (static SQL parse)Blocking (generated)
jOOQTyped query builderFluent builder → ? bindYes (types, from codegen)Blocking (+ R2DBC)
KyselyTyped query builderFluent builder → placeholdersYes (types, from a DB type)Async
DrizzleTyped query builderSQL-like builder → ParamYes (types, from schema)Async
ExposedTyped builder + DAODSL Op/QueryBuilder paramsYes (types)Blocking (+ coroutine)
SquealTyped query builderType-level schema + EncodeParamsYes (type-level)Indexed PQ monad
OpaleyeTyped query builderEscaped literals (postgresql-simple)Yes (types)Blocking IO
BeamTyped data mapperHKD query DSL → val_ paramsYes (types)Blocking IO
linq2dbLINQ providerLINQ closure values → paramsPartial (C#-typed, runtime SQL)Async / sync
EF CoreFull ORM (data mapper)LINQ → paramsPartial (C#-typed, runtime SQL)Async
HibernateFull ORM (data mapper)JPQL/Criteria named paramsPartial (Criteria typed; HQL not)Blocking (+ reactive)
SQLAlchemyFull ORM + CoreCore expression → bind paramsPartial (Core-typed; runtime)Blocking (+ asyncio)
Django ORMFull ORM (active-record)QuerySet values → paramsNoBlocking (+ async)
PrismaFull ORM (schema-first)Generated client → paramsYes (from .prisma schema)Async
TypeORMFull ORM (AR + DM)Repository/QueryBuilder :paramPartial (entity-typed; runtime)Async
SeaORMFull ORM (dynamic)sea-query Value bindPartial (entity-typed; runtime)Async
GORMFull ORM (active-record)clause.Expr ? bindNo (reflection)Blocking
entFull ORM (codegen)Generated typed predicates → paramsYes (from schema codegen)Blocking
persistent + esqueletoFull ORM + typed joinsTH entities + val paramsYes (types)Blocking SqlPersistT
ActiveRecordFull ORM (active record)sanitize_sql bound paramsNoBlocking

Two structural observations fall straight out of the matrix:

  1. Injection safety is a solved problem, mechanically. Every surveyed library — from the thinnest driver to the heaviest ORM — makes bound parameters the default and reserves a loudly-named escape hatch (sql.unsafe, $queryRawUnsafe, Arel.sql, sql.raw, FromSqlRaw, Fragment.const) for raw text. The interesting variation is how the safe default is expressed (a tagged template, a builder, a macro, a quotation) — dissected, with D's IES, in the safe-interpolation case study — not whether it exists.
  2. Compile-time query checking is the real fault line. It cleaves the field into three camps: statically-checked (Quill, Diesel, sqlx, sqlc, jOOQ, Kysely, Drizzle, Squeal, Opaleye, Beam, ent, Prisma, esqueleto — a wrong column or type won't compile), runtime-constructed but host-typed (the LINQ/Criteria ORMs — EF Core, Hibernate, SQLAlchemy Core, linq2db — where the host language types the expression but the SQL and its DB-validity are resolved at runtime), and stringly-dynamic (the driver/micro-mapper tier + the reflection ORMs — GORM, Django, ActiveRecord — where nothing checks the SQL against the schema until it executes).

The five-dimension spine

The same analytical cuts every deep-dive uses, read across the whole corpus.

1. Connection, pooling & resource lifetime

The universal substrate. Every non-trivial library ships or wraps a connection pool, because the connect handshake dominates per-query latency. The design variation is in how acquisition and release are scoped:

  • Manual / ambient — Go database/sql's *sql.DB is a pool you pass around; ActiveRecord and Django ORM bind a connection to the current thread/task implicitly; Exposed and Hibernate use a thread-local or context-bound session. Convenient, but a leaked or mis-scoped connection is a runtime bug.
  • Scoped as a value — the effect systems make lifetime a type. Effect TS's Acquirer is an Effect<Connection, SqlError, Scope>; skunk and doobie hand you a Resource[F, …]; Quill's ZIO contexts use ZLayer/Scope; Squeal wraps bracket. Here a leaked connection is a compile-time impossibility — the Scope/Resource guarantees release on every exit path, success or failure. This is the single most transferable idea for an effects-first design.

2. Query construction & injection safety

The survey's richest axis, with six recurring models (concepts). Ranked roughly by how much the compiler knows:

  • Raw string (Go database/sql, Dapper, JDBI SQL objects) — maximum control, zero static knowledge.
  • Tagged template (Effect TS, postgres.js, doobie) — reads like string interpolation, compiles to a parameterized statement. postgres.js is the most elegant point in this space: the same sql function is the query tag and the fragment builder (sql(obj) for inserts, sql(arr) for IN, sql('col') for identifiers).
  • Macro-checked raw SQL (sqlx, sqlc) — you still write SQL, but a build step validates it and infers result types. sqlx checks against a live DB (or a committed .sqlx cache); sqlc parses the SQL statically with an embedded real grammar (libpg_query), needing no DB connection.
  • Fluent typed builder (jOOQ, Kysely, Diesel, Drizzle, Exposed, SeaORM) — you never write SQL text; a method chain typed by a schema (generated, or a supplied type) produces it. Kysely is the purest "type-only" form (the schema is just a TypeScript type, erased at runtime).
  • Typed relational algebra (Slick, Squeal, Opaleye, Beam, esqueleto) — the query is a value in an embedded algebra whose types encode the schema; the Haskell trio pushes this furthest (Squeal reifies the entire schema as a type; Beam's higher-kinded data reuses one record for values and query expressions).
  • Quoted DSL / LINQ → AST (Quill, EF Core, linq2db) — a macro or expression-tree capture turns ordinary host-language code into an AST, then SQL. Quill is the compile-time extreme (SQL is generated during compilation where possible).

The escape hatch is universal and universally dangerous; the mark of a good design is how rarely you need it and how loudly it announces itself.

3. Schema, migrations & code generation

Three stances (concepts), and a clean split on who owns the truth:

A recurring, important finding: migrations are frequently not the query library's job.SQLAlchemy delegates to Alembic; Hibernate to Flyway/Liquibase; ent to Atlas; sqlc reads a schema but runs no migrations; the whole functional-mapper tier omits them by design. A migration runner is a separable concern.

4. Type mapping & result decoding

How a row becomes a typed value, and how much of that mapping is checked. The elegant designs make the codec composable and first-class: skunk's Codec and hasql's Encoders/Decoders compose bidirectionally like parser combinators; Quill derives GenericEncoder/GenericDecoder at compile time; the Haskell profunctor libraries (Opaleye) drive both binding and decoding from one Default instance. The ORMs hydrate whole object graphs (and pay for it with lazy-loading and N+1 hazards — see below). The micro-mappers (Dapper IL-emits a per-shape materializer; JDBI's RowMappers; sqlx/sqlc generate the struct) sit in between: typed row→object mapping without graph management.

5. Effect model, transactions & error handling

The dimension that most divides the field, and the one this survey weights most heavily.

  • Effect model. Three tiers: blocking (the JVM/Go/Ruby/Python mainstream + the Haskell IO libraries), async (the .NET/TS/Rust mainstream — futures/Task/Promises), and effect value (the description-as-a-value tier: Effect TS's Effect, Quill's ZIO, doobie/skunk's cats-effect F, Slick's effect-polymorphic DBIO, Squeal's indexed PQ). Only the last makes the required environment and the possible errors part of the query's type.
  • Transactions. Near-universal shape: a combinator wrapping a block (withTransaction(effect), transaction { … }, sql.begin(fn), inTransaction), committing on success and rolling back on failure. Nesting via savepoints is common but not universal — Effect TS, Quill, EF Core, Hibernate, jOOQ, SQLAlchemy, Prisma, postgres.js, Django ORM emit real SAVEPOINTs for nested transactions, while ent (returns ErrTxStarted), Slick, and linq2db (one flat transaction per connection) do not.
  • Errors. Three approaches, in ascending order of type-safety: thrown exceptions (the mainstream — SQLException, DbException, HibernateException, PersistException, Prisma's P#### codes), a narrowed exception channel (Quill's ZIO[…, SQLException, …], doobie's MonadError[F, Throwable]), and a structured typed error union — the design frontier, held essentially alone by Effect TS's SqlError over an 11-case SqlErrorReason discriminated union, each case carrying an isRetryable flag. No other surveyed library models "unique violation vs deadlock vs serialization failure" as distinct, matchable, retryable-aware types.

The consensus standard

Points on which the entire field — thin to heavy, across a dozen languages — has converged. A sparkles:sql that violated any of these would be objectively behind the state of the art:

  1. Parameter binding is the default; raw text is an opt-in escape hatch. Non-negotiable.
  2. Connections are pooled, and the pool is safe for concurrent use.
  3. Transactions are a block-scoped combinator that commits on success and rolls back on failure/exception.
  4. Result rows map to typed values — even the thinnest mappers (Dapper, sqlx, JDBI) refuse to leave you with untyped column bags.
  5. Dialect differences are abstracted behind a compiler/Idiom/Dialect, so one query targets many engines.
  6. Migrations are a separable concern — increasingly delegated to a dedicated tool (Alembic, Flyway, Atlas) rather than baked into the query layer.

Architectural trade-offs (still genuinely open)

Where the field has not converged — the real design decisions:

Trade-offThe two polesWhere the survey lands
Compile-time vs runtime query checkingStatic (Quill, sqlx, Diesel, the Haskell trio) vs runtime (EF Core, GORM)Static catches more bugs but constrains expressiveness and lengthens builds; the strongest static designs pay in type complexity.
Write-SQL vs build-SQLRaw SQL, checked (sqlx, sqlc, doobie) vs a typed builder/DSL (jOOQ, Diesel, Slick)No winner. Raw-SQL-checked keeps SQL legible and skips a DSL; builders compose and retarget dialects. Both beat stringly-dynamic.
Explicit persistence vs unit of workExplicit writes (Ecto, doobie, the functional tier) vs implicit flush (Hibernate, EF Core, SQLAlchemy)The unit of work is powerful but the source of ORMs' hardest bugs (flush timing, lazy-load N+1, LazyInitializationException). The functional mappers reject it deliberately.
Effect value vs async vs blockingEffect value (Effect TS, Quill, cats-effect) vs Promise/Task vs blockingEffect values make environment + errors typed and composition lawful, at the cost of a runtime and a learning curve.
Typed errors vs exceptionsA reason union (Effect TS) vs thrown SQLExceptionThe typed-union frontier is essentially unoccupied; it is where the most design headroom remains.
Schema ownershipCode-first vs db-first vs noneFollows the product: greenfield favours code-first migrations; integrating-an-existing-DB favours db-first codegen.

Where an effects-first sparkles:sql fits

The survey exists to inform an algebraic effects-first D library. Read as a design brief, the corpus points to a clear position: a functional data mapper that stops below the full-ORM rung — typed, composable queries and explicit, effect-typed persistence, without an identity map or an implicit unit of work. That is exactly where Effect TS, Quill, doobie, skunk, and Ecto sit, and it is the design centre the effects-first premise most naturally serves.

The most transferable ideas, by dimension:

  • The service seam = the query constructor. Effect TS's sharpest move is that the injected SqlClient service is the sql tagged-template function. A D analogue: the capability handed in by the effect layer is itself the query builder, so obtaining "the database" and "the way to write a query" are one act. Pairs naturally with D's Design-by-Introspection capability-detection style.
  • Scoped acquisition as the resource discipline. Model the connection/pool as a scoped acquire/release (Effect TS's Acquirer, skunk's Resource), so a leaked connection is unrepresentable. D's scope/@safe/-preview=dip1000 lifetime checking and the repo's during/event-horizon effect substrate are the natural fit.
  • A typed error reason union, not a bag of exceptions. Effect TS's SqlErrorReason (unique-violation / deadlock / serialization-failure / … each with isRetryable) is the single most under-adopted idea in the field and the best match for D's Expected!(T, E) error handling — a sum-typed error channel where a caller can match on "was this a retryable serialization failure?".
  • Transaction-as-combinator with a fiber/context-local connection. The universal withTransaction(effect) shape, with nesting lowered to real SAVEPOINTs and the active connection carried in the effect context (as Effect TS's TransactionConnection and Quill's FiberRef[Connection] do) — so nested calls and batched resolvers transparently join the same transaction.
  • Safe query construction: a compile-time-checked template, not a builder. Between Quill's compile-time quotation and Effect's runtime tagged-template, D can aim higher than either: D's mixin + CTFE + template machinery can make a compile-time-parameterized SQL template (values captured as binds, dialect rendered at compile time) that is injection-safe by construction without an ORM DSL — closer to Quill's static translation but using D string-interpolation/IES rather than a macro. Keep Quill's clean Idiom (dialect) / NamingStrategy separation.
  • Composable codecs. Adopt skunk/hasql's bidirectional, combinator-composed Codec for row↔type mapping, @nogc-friendly and derivable via D introspection — not a reflection-driven ORM materializer.
  • Batching without an ORM. Effect TS's SqlResolver (DataLoader-style, transaction-keyed) shows how to kill N+1 without lazy proxies — the functional answer to the ORM's hardest problem.

What to not borrow: the implicit unit of work, the identity map, lazy-loading proxies, and ambient/thread-local session state — the machinery that makes Hibernate/EF Core/ActiveRecord powerful but is also the source of their subtlest bugs, and which an effects-first, explicit-by-design library is precisely positioned to avoid.

NOTE

This synthesis is prior-art analysis, not a committed design. A concrete sparkles:sql proposal — milestones, D API sketches, the during/event-horizon integration — is deferred to a later spec effort, for which this survey is the evidence base.


Sources

  • Every claim about a specific library is grounded in its deep-dive (linked above), each of which carries its own primary-source Sources section and a claim-by-claim grounding ledger.
  • The shared vocabulary and the canonical pattern references (Codd; Fowler's Patterns of Enterprise Application Architecture; OWASP on SQL injection) are cited in concepts.