SQL & ORM Abstraction
A breadth-first survey of database access across languages — from the thinnest layer that keeps SQL injection-proof (safe string templates, raw drivers), through typed query builders and functional data mappers, up to full object-relational mappers with change tracking and unit-of-work. The goal is a grounded map of how ~30 production systems in a dozen ecosystems structure the four hard problems — safe query construction, connection & transaction lifetime, schema & migration, and result mapping — to inform the design of an algebraic effects-first D data-access library (working name sparkles:sql). Because the design centre is effects-first, the survey weights the effect-system and functional data-mapper families most heavily: Effect TS (TypeScript), Quill, doobie, skunk, Slick (Scala), and Ecto (Elixir).
This survey answers seven questions:
- What are the levels of abstraction, and what does each rung trade? The driver → safe-SQL → typed-builder → functional-data-mapper → full-ORM ladder. See concepts: the abstraction ladder and the master catalog.
- How is SQL made injection-safe, and how do the mechanisms differ? Runtime tagged-templates vs typed builders vs compile-time quotation vs macro-checked raw SQL. See concepts: statements, parameters & injection, the by-query-model taxonomy, and the safe-interpolation case study (which features D's IES).
- Compile-time or runtime query construction — who checks the query, and when? Type-level schemas and macros (Quill,
sqlx,Squeal,jOOQ) vs runtime AST builders. See the comparison. - How is database access modelled as an effect, and how do transactions compose? Blocking vs async vs effect-value (
IO/ZIO/Effect/ConnectionIO);withTransactioncombinators, nesting, and savepoints. See the by-effect-model taxonomy. - How is error handling typed? A single typed error channel with a reason union (Effect TS) vs a narrowed exception channel (Quill) vs thrown
SQLException/DbException. See concepts: effects, transactions & errors. - What are the schema & migration strategies? Code-first, schema-first, and db-first (introspection + codegen). See the by-schema-stance taxonomy.
- What should an effects-first
sparkles:sqlborrow, across the thin→ORM spectrum? The synthesis and delta. See the comparison.
NOTE
Scope: complete — all five waves + synthesis published. This survey was built in waves. Wave 1 establishes the shared vocabulary (concepts) and the effect-system / functional-data-mapper core: Effect TS, Quill, doobie, skunk, Slick, Ecto. Wave 2 adds the Haskell typed cluster (hasql, Squeal, Opaleye, Beam, persistent + esqueleto). Wave 3 adds the typed query builders & thin safe-SQL (Diesel, sqlx, SeaORM, Kysely, Drizzle, jOOQ, sqlc, linq2db, Dapper, Exposed). Wave 4 adds the full ORMs (EF Core, Hibernate, SQLAlchemy, Django ORM, Prisma, TypeORM, GORM, ent, ActiveRecord). Wave 5 adds the raw / tagged-template baseline (Go database/sql, postgres.js, JDBI), and the capstone comparison & synthesis reads the whole corpus against itself and distils the design brief for an effects-first sparkles:sql.
Last reviewed: July 12, 2026
Master catalog
One row per surveyed system. Category places it on the abstraction ladder. Query model is how a query is expressed (concepts). Effect/async model is how a result is returned. Schema stance is the library's relationship to the schema (concepts). The Link column points at the deep-dive (or the wave that publishes it).
| System | Language | Category | Query model | Effect / async model | Schema stance | Link |
|---|---|---|---|---|---|---|
Effect TS sql | TypeScript | Functional data mapper | Tagged template | Effect value (typed SqlError) | Code-agnostic | effect-ts |
| Quill | Scala | Functional data mapper | Quoted DSL → AST (compile-time) | Pluggable; ZIO in JDBC-ZIO | Code-agnostic | quill |
| doobie | Scala | Functional data mapper | Tagged template + Fragment | ConnectionIO (cats-effect) | None (SQL only) | doobie |
| skunk | Scala | Functional data mapper | Typed statement + Codec | cats-effect + fs2 | None (SQL only) | skunk |
| Slick | Scala | Functional-relational | Typed relational algebra | DBIO → effect-poly F (IO/Future/ZIO) | Schema + codegen | slick |
| Ecto | Elixir | Functional data mapper | Composable query macros | Immutable / blocking | Code-first + migrations | ecto |
| hasql | Haskell | Safe-SQL / micro-mapper | Raw SQL + Encoders/Decoders | Session over IO (blocking) | None (SQL only) | hasql |
| Squeal | Haskell | Typed query builder | Typed relational algebra | Indexed monad PQ over IO | Code-first (type-level) | squeal |
| Opaleye | Haskell | Typed query builder | Typed relational algebra (arrows) | IO (postgresql-simple) | Db-first | opaleye |
| Beam | Haskell | Functional data mapper | Typed relational algebra (Q monad) | MonadBeam over IO | Code-first / db-first | beam |
| persistent + esqueleto | Haskell | Full ORM + typed joins | TH entities + type-safe EDSL | SqlPersistT over IO (blocking) | Code-first | persistent-esqueleto |
| Diesel | Rust | Typed query builder | Fluent typed builder | Blocking (+ async fork) | Db-first (schema.rs) | diesel |
| sqlx | Rust | Safe-SQL / micro-mapper | Macro-checked raw SQL | Async | Db-first (compile check) | sqlx |
| SeaORM | Rust | Full ORM (data mapper) | Fluent builder over sqlx | Async | Code/db-first (entities) | sea-orm |
| Kysely | TypeScript | Typed query builder | Fluent typed builder | Async | Db-first (types) | kysely |
| Drizzle | TypeScript | Typed query builder | Fluent SQL-like builder | Async | Code-first (schema) | drizzle |
| jOOQ | Java | Typed query builder | Fluent typed builder | Blocking (+ R2DBC) | Db-first codegen | jooq |
| sqlc | Go | Safe-SQL / micro-mapper | Raw SQL → generated code | Blocking (database/sql) | Db-first (codegen) | sqlc |
| linq2db | .NET | Typed query builder | LINQ → SQL | Async | Db-first / POCO | linq2db |
| Dapper | .NET | Safe-SQL / micro-mapper | Raw SQL + auto-map | Blocking / async | None | dapper |
| Exposed | Kotlin | Typed query builder + DAO | Fluent DSL / DAO | Blocking (+ suspend) | Code-first | exposed |
| EF Core | .NET | Full ORM (data mapper) | LINQ | Async | Code-first + migrations | ef-core |
| Hibernate / JPA | Java | Full ORM (data mapper) | JPQL / Criteria | Blocking (+ reactive) | Code-first / db-first | hibernate |
| SQLAlchemy | Python | Full ORM (data mapper) + Core | Core expression + ORM | Blocking (+ asyncio) | Code-first | sqlalchemy |
| Django ORM | Python | Full ORM (active-record-ish) | QuerySet method chains | Blocking (+ async) | Code-first + migrations | django-orm |
| Prisma | TypeScript | Full ORM (data mapper) | Schema-first + generated client | Async | Schema-first (.prisma) | prisma |
| TypeORM | TypeScript | Full ORM (AR + data mapper) | Decorators + query builder | Async | Code-first | typeorm |
| GORM | Go | Full ORM (active-record-ish) | Chainable methods + struct tags | Blocking | Code-first (automigrate) | gorm |
| ent | Go | Full ORM (data mapper) | Schema-as-code + generated builders | Blocking | Code-first (codegen) | ent |
| ActiveRecord | Ruby | Full ORM (active record) | Model + query methods | Blocking | Db-first + migrations | activerecord |
Go database/sql | Go | Driver (+ sqlx) | Raw string | Blocking | None | go-database-sql |
| postgres.js | JS / TS | Driver + safe-SQL | Tagged template | Async | None | postgres-js |
| JDBI | Java | Safe-SQL / micro-mapper | Raw SQL (fluent + SQL objects) | Blocking | None | jdbi |
Taxonomy
Each table re-cuts the same set by one axis. Forward-dated systems are named but not yet linked; their deep-dives arrive in the marked wave.
By abstraction level
The primary axis (see concepts: the abstraction ladder).
| Rung | Systems |
|---|---|
| Driver | Go database/sql, postgres.js, JDBC, ADO.NET |
| Safe-SQL / micro-mapper | doobie-adjacent, hasql, Dapper, JDBI, sqlc, sqlx, postgres.js |
| Typed query builder | Slick, Squeal, Opaleye, Diesel, Kysely, Drizzle, jOOQ, linq2db, Exposed |
| Functional data mapper | Effect TS, Quill, doobie, skunk, Ecto, Beam |
| Full ORM (data mapper) | EF Core, Hibernate, SQLAlchemy, Prisma, ent, SeaORM, persistent |
| Full ORM (active record) | ActiveRecord, Django ORM, GORM, TypeORM |
By query construction model
How the query is expressed (concepts: query construction models).
| Model | Checked | Systems |
|---|---|---|
| Raw string | never | Go database/sql, Dapper, JDBI (SQL objects) |
| Tagged template | runtime | Effect TS, doobie (sql"..."), postgres.js |
| Fluent typed builder | compile | jOOQ, Kysely, Diesel, Drizzle, Exposed, SeaORM |
| Quoted DSL → AST | compile | Quill, EF Core / linq2db (LINQ) |
| Typed relational algebra | compile (types) | Slick, Squeal, Opaleye, Beam, esqueleto |
| Macro-checked raw SQL | compile | sqlx, sqlc, cornucopia |
| Criteria / method chains | runtime/partial | Hibernate, SQLAlchemy, Django ORM, GORM, ActiveRecord, Ecto macros |
By effect / async model
How a result is returned (concepts: effects, transactions & errors).
| Model | Systems |
|---|---|
| Blocking | Diesel, jOOQ, Hibernate, JDBI, Go database/sql, sqlc, GORM, ent, ActiveRecord, Django ORM |
Async (future / promise / Task) | sqlx, EF Core, Prisma, Kysely, Drizzle, TypeORM, postgres.js, SeaORM, linq2db, SQLAlchemy (asyncio) |
Effect value (IO/ZIO/Effect) | Effect TS, Quill (ZIO), doobie, skunk (cats-effect), Slick (DBIO run to an effect-poly F), Squeal (indexed PQ), Beam |
Blocking IO (Haskell) | hasql (Session), Opaleye (postgresql-simple), persistent + esqueleto (SqlPersistT) |
| Functional/immutable, blocking | Ecto (eager tagged-tuple Repo calls) |
By schema stance
The library's relationship to the schema (concepts: schema, migrations & codegen).
| Stance | Systems |
|---|---|
| Code-first (models → schema) | EF Core, Django ORM, TypeORM, Beam, Drizzle, Exposed, Ecto, GORM, ent, SeaORM |
| Schema-first (declaration) | Prisma (.prisma), Slick (codegen input), Squeal (type-level) |
| Db-first (introspect + gen) | jOOQ, sqlc, sqlx, Diesel, Kysely, Opaleye, ActiveRecord (schema.rb), linq2db |
| None (raw SQL, no schema) | doobie, skunk, hasql, Dapper, JDBI, Go database/sql, postgres.js |
Milestones
A high-confidence timeline of when the field's ideas and tools landed. Per-library provenance (and exact release dates) live in each deep-dive's Ecosystem & maturity and Sources; forward-dated entries are marked.
| Year | Milestone |
|---|---|
| 1970 | Codd — the relational model (CACM) |
| 1986–1992 | SQL standardized (SQL-86 … SQL-92); JDBC-style call-level interfaces emerge |
| 2001 | Hibernate (Gavin King) — the archetypal JVM ORM; JDBI's ancestor era |
| 2003–2004 | SQLAlchemy begins (Bayer); Ruby on Rails / ActiveRecord (DHH) popularizes active record |
| 2005 | Django ORM ships with Django; Hibernate informs the JPA standard (JSR 220, 2006) |
| 2007–2008 | LINQ + LINQ to SQL / Entity Framework (Microsoft) — language-integrated query on .NET |
| 2009 | jOOQ (Lukas Eder) — typed SQL DSL generated from the schema |
| 2012–2013 | Slick (Typesafe) — functional-relational mapping for Scala; Ecto work begins for Elixir |
| 2015 | Ecto 1.0 (Elixir); doobie matures (Rob Norris); HugSQL/Yesod persistent era |
| 2016 | Quill (Li Haoyi/Flavio Brasil) — compile-time QDSL; Diesel 1.0 (Rust); Opaleye/Beam (Haskell typed SQL) |
| 2017–2019 | skunk (Norris) — pure-FP Postgres, no JDBC; hasql matures; ent (Facebook, Go); sqlc (Go) |
| 2019 | Prisma 2 rewrite (schema-first + Rust query engine); EF Core matures as the .NET default |
| 2020 | sqlx (Rust) — compile-time-checked raw SQL; SeaORM begins; GORM v2 |
| 2022 | Kysely + Drizzle (TypeScript) — type-safe query builders challenge the ORM default |
| 2023–2026* | Effect TS sql — DB-access-as-effect (typed SqlError, scoped acquirer); ProtoQuill (Scala 3); Drizzle/Kysely adoption surge |
* 2023–2026 entries are current-as-of-review; exact dates are in the per-library deep-dives (some forward-dated pending their wave).
Quick navigation
Suggested reading paths
- "I'm designing
sparkles:sql(effects-first)." concepts (the ladder + the effect/transaction vocabulary) → Effect TS (DB-as-effect, typed error union, scoped acquirer, transaction-as-combinator) → Quill (compile-time safe query DSL,Idiom/NamingStrategy) → doobie + skunk (the cats-effect free-monad and pure-FP-Postgres alternatives) → the comparison. - "I want the safe-SQL-without-an-ORM story." concepts: injection safety → the safe-interpolation case study (the technique + D's IES) → Effect TS / doobie (tagged templates) → sqlx / sqlc (macro-checked / codegen'd raw SQL) → hasql / Dapper (micro-mappers).
- "I want the typed-query-builder lineage." Slick → Squeal / Opaleye / Beam (Haskell type-level schemas) → jOOQ / Kysely / Diesel / Drizzle (fluent typed builders).
- "I want to understand full ORMs." concepts: ORM patterns → Hibernate / EF Core / SQLAlchemy (data mapper + unit of work) → ActiveRecord / Django ORM / GORM (active record) → Prisma / ent (schema-first / codegen).
Library deep-dives
Grouped by category; see the master catalog for the one-line summary.
- Effect systems & functional access: Effect TS · Quill · doobie · skunk · Slick · Ecto.
- Typed functional SQL (Haskell): hasql · Squeal · Opaleye · Beam · persistent + esqueleto.
- Typed builders & thin safe-SQL: Diesel · sqlx · SeaORM · Kysely · Drizzle · jOOQ · sqlc · linq2db · Dapper · Exposed.
- Full ORMs: EF Core · Hibernate · SQLAlchemy · Django ORM · Prisma · TypeORM · GORM · ent · ActiveRecord.
- Raw / tagged-template baseline: Go
database/sql· postgres.js · JDBI. - Synthesis: Comparison & Synthesis — the cross-cutting analysis + the effects-first
sparkles:sqldesign brief. - Case study: Safe SQL Interpolation — how each ecosystem makes the
${value}syntax injection-safe, featuring D's interpolated expression sequences (IES).
Sources
- Each deep-dive's
Sourcessection carries its primary sources (repository files + official docs), pinned in the survey's grounding ledger. - Shared vocabulary and the canonical pattern references (Codd; Fowler, Patterns of Enterprise Application Architecture; OWASP on SQL injection) are cited in concepts.