Skip to content

sqlc (Go)

A command-line SQL compiler: you write a schema and a set of raw SQL queries each tagged with a -- name: GetAuthor :one comment, and sqlc generate parses and type-checks that SQL against its own embedded engine grammars, then emits fully-typed Go — a struct per table, a Queries type, and one method per query running on database/sql or pgx — with no runtime library, no ORM, and no query DSL.

FieldValue
LanguageGo (the generator); emits Go, and — via plugins — Kotlin, Python, TypeScript, and more
LicenseMIT (Copyright (c) 2024 Riza, Inc.)
Repositorysqlc-dev/sqlc
Documentationdocs.sqlc.dev · playground · README.md
CategorySafe-SQL / micro-mapper — a SQL-to-code generator (db-first codegen); not an ORM, not a query builder
Abstraction levelBetween the safe-SQL / micro-mapper and functional data-mapper rungs; the generated code is a micro-mapper
Query modelMacro-checked raw SQL — but statically parsed by sqlc's own grammar, needing no database connection at build time
Effect/async modelBlocking — generated methods are synchronous database/sql/pgx calls taking a context.Context; errors are Go error returns
BackendsPostgreSQL, MySQL, SQLite (primary); ClickHouse and GoogleSQL engines also present
First release≈2019 (kyleconroy/sqlc, web-attested via the introductory blog post)
Latest versionv1.31.1 (the pinned tree's internal/info constant)

NOTE

sqlc is this survey's data point for build-time code generation from raw SQL. Like sqlx (Rust) and cornucopia it belongs to the macro-checked raw SQL family — but where sqlx's query! macro PREPAREs against a live development database, sqlc parses the SQL statically with its own embedded grammar and infers every type from a schema it reads from .sql files, needing no database connection. It sits on the db-first codegen axis: the schema .sql is the source of truth, and the output is ordinary Go you check into your repo. See concepts for shared vocabulary.


Overview

What it solves

sqlc removes the trade every Go developer using database/sql faces: hand-write the SQL and then hand-write, and keep in sync, the rows.Scan(&a, &b, …) boilerplate and the parameter structs — or adopt an ORM and lose SQL. sqlc keeps the SQL and generates the boilerplate. The README states the loop in three steps (README.md):

"sqlc generates type-safe code from SQL. Here's how it works:1. You write queries in SQL.1. You run sqlc to generate code with type-safe interfaces to those queries.1. You write application code that calls the generated code."

The documentation index makes the output language concrete and the promise blunt (docs/index.rst):

"sqlc generates fully type-safe idiomatic Go code from SQL. … Seriously, it's that easy. You don't have to write any boilerplate SQL querying code ever again."

The framing is a compiler, not a library — the repository's own title is "sqlc: A SQL Compiler" (README.md), and the CLI exposes a compile subcommand whose entire job is to "Statically check SQL for syntax and type errors" (internal/cmd/cmd.go) without emitting anything. That is the load-bearing distinction from every other tool in this survey: sqlc has a genuine front end. It parses your DDL into a catalog, parses each query against that catalog, resolves the type of every parameter and every result column, and only then hands a fully-typed intermediate representation to a language back end. A misspelled column, a type mismatch, or a query against a non-existent table fails sqlc generate — the SQL bug is caught at code-generation time, before the Go even compiles.

Two design negatives define the shape. sqlc is not a driver (it generates no wire code — it targets database/sql/pgx), and it is not an ORM or builder: there is no runtime query object, no method chain, no change tracking. The generated code is a thin, readable mapper you own.

Design philosophy

SQL is the source language; Go is the object language. sqlc treats a directory of .sql files the way a C compiler treats .c files — as input to be parsed, checked, and lowered. The project's tongue-in-cheek epigraph captures the stance (docs/index.rst): "And lo, the Great One looked down upon the people and proclaimed: 'SQL is actually pretty great'." The consequence is that everything expressible in your database's SQL dialect is available to you, because sqlc embeds a real parser for that dialect (see Schema, migrations & code generation) — you are not restricted to what a builder DSL can model.

Pure build-time, zero runtime. There is no import "sqlc" in the generated code. The tutorial is explicit that the tool itself is self-contained (docs/tutorials/getting-started-postgresql.md): "sqlc itself has no dependencies." The emitted package imports only context, database/sql (or pgx), and whatever type packages your columns need. No reflection, no metadata is consulted at runtime; the "mapping" is fixed Go source produced once at generation time.

The query annotation drives the method shape. sqlc's central convention is a one-line comment above each query (docs/reference/query-annotations.md):

"sqlc requires each query to have a small comment indicating the name and command. The format of this comment is as follows: -- name: <name> <command>"

The <name> becomes the Go method name (and must be a valid Go identifier — validated in internal/metadata/meta.go); the <command> (:one, :many, :exec, …) selects the method's return shape. This is the whole user-facing surface: annotated SQL in, typed Go out.


Connection, pooling & resource lifetime

sqlc generates no connection, pool, or lifetime management code — a deliberate absence, and a finding for this survey. The generated Queries type wraps a single narrow interface, DBTX, that any driver handle satisfies (internal/codegen/golang/templates/stdlib/dbCode.tmpl):

go
type DBTX interface {
    ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
    PrepareContext(context.Context, string) (*sql.Stmt, error)
    QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
    QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}

func New(db DBTX) *Queries {
    return &Queries{db: db}
}

Because *sql.DB, *sql.Tx, and *sql.Conn all implement these four methods, New(db) accepts any of them — and connection pooling is entirely database/sql's job (or pgxpool's for the pgx back end, whose generated DBTX uses Exec/Query/QueryRow instead). sqlc owns none of the resource story from concepts: no pool sizing, no acquire/release, no scoped lifetime. It hands you a stateless method set over whatever handle you pass. That is the same minimalism as Go's stdlib database/sql — sqlc adds typing, not plumbing — and it is the sharp contrast with hasql's Pool, sqlx's (Rust) bounded fair Pool, or the effect systems' scoped Acquirer. Prepared statements are optional and opt-in: with emit_prepared_queries, Prepare(ctx, db) calls PrepareContext for every query up front and routes calls through the cached *sql.Stmt (dbCode.tmpl); otherwise each call is an unprepared …Context call.


Query construction & injection safety

This is one of sqlc's two centres of gravity. The mechanism has three parts: you write raw SQL, you annotate it, and sqlc statically parses it to synthesize an injection-safe, typed method.

You write SQL; sqlc parses and types it

A query file is ordinary SQL with a leading annotation. From the PostgreSQL tutorial (docs/tutorials/getting-started-postgresql.md):

sql
-- name: GetAuthor :one
SELECT * FROM authors
WHERE id = $1 LIMIT 1;

-- name: CreateAuthor :one
INSERT INTO authors (name, bio) VALUES ($1, $2)
RETURNING *;

The compiler reads each query file, parses every statement with the per-engine parser, and for each statement extracts the name/command from the comment, then resolves parameters and columns against the catalog (internal/compiler/parse.go, internal/compiler/compile.go). SELECT * is expanded at compile time into the concrete column list (so a * that cannot be resolved is an error), and $1/$2 are matched to catalog column types to infer the Go parameter types. The result is a typed Query (SQL, Metadata, []*Column, []Parameter) in internal/compiler/query.go.

The generated method: a const string plus bind parameters

The emitter turns that typed Query into a package-level SQL constant and a method that passes the parameters as bind arguments to the driver (internal/codegen/golang/templates/stdlib/queryCode.tmpl):

go
const getAuthor = `-- name: GetAuthor :one
SELECT id, name, bio FROM authors
WHERE id = $1 LIMIT 1
`

func (q *Queries) GetAuthor(ctx context.Context, id int64) (Author, error) {
    row := q.db.QueryRowContext(ctx, getAuthor, id)
    var i Author
    err := row.Scan(&i.ID, &i.Name, &i.Bio)
    return i, err
}

Two properties matter. First, the SQL text is fixed at generation time — a const, never assembled at runtime. sqlc does not build SQL from strings the way a query builder does; you wrote the query, sqlc merely embedded it. Second, dynamic data travels only as bind parameters: id is passed as the trailing variadic argument to QueryRowContext, so the value is transferred out-of-band and SQL injection is structurally impossible for a bound value — exactly the parameter-binding safety model concepts describes, inherited directly from database/sql. There is no interpolation channel to misuse.

Naming, nullability, and embedding macros

For anything positional binding cannot express, sqlc adds a small set of sqlc.* macros that it rewrites away before generating (docs/reference/macros.md):

  • sqlc.arg(name) attaches a name to a parameter: "This macro expands to an engine-specific parameter placeholder. The name of the parameter is noted and used during code generation." So WHERE lower(name) = sqlc.arg(name) gives a named Go argument instead of a positional one.
  • sqlc.narg(name) is "The same as sqlc.arg, but always marks the parameter as nullable" — forcing an Option-shaped (sql.NullString, *string) Go type where inference would otherwise pick a non-null one.
  • sqlc.embed(table) lets you "reuse existing model structs in more queries": a join selecting sqlc.embed(students), sqlc.embed(test_scores) produces a row struct with nested Student and TestScore fields instead of a flat column list.
  • sqlc.slice("ages") handles IN (…) for drivers that cannot bind a slice: it "generates a dynamic query at runtime with the correct number of parameters."

sqlc.slice is the single place sqlc's generated code touches the query text at runtime — it strings.Replaces a /*SLICE:ages*/? marker with the right number of ? placeholders (queryCode.tmpl) — but note the safety invariant holds: only the count of placeholders changes, and every actual value still travels as a bind parameter, never as interpolated text. The macro's own doc flags the one cost: "this macro can't be used with prepared statements" (macros.md).

The escape hatch, and its absence

There is no queryRaw in sqlc, because every query is already raw SQL — the "escape hatch" of other tools is sqlc's normal mode. The corresponding limitation is the mirror image: sqlc cannot express a query whose structure varies at runtime (a conditional WHERE, a dynamic column list, a runtime-chosen ORDER BY). Every query must be a fixed, named SQL statement known at generation time. Where a builder like Kysely or jOOQ shines — assembling query shape from data — sqlc simply does not play; you drop to database/sql by hand for that one query.


Schema, migrations & code generation

The other centre of gravity, and where the "compiler" claim is earned: sqlc ships its own SQL parser per engine and its own Go emitter.

The engines: real, embedded grammars

sqlc generate dispatches on the configured engine to a per-dialect parser and catalog (internal/compiler/engine.go):

EngineParserHow it parses SQL
postgresqlpganalyze/pg_query_go/v6 (postgresql/parse.go)Wraps libpg_query — the actual PostgreSQL server grammar extracted from PG's C source
mysqlsqlc-dev/marino/parser (dolphin/parse.go)sqlc's fork of the TiDB/PingCAP MySQL parser (the struct field is literally pingcap)
sqliteANTLR-generated (sqlite/parse.go, antlr4-go/antlr/v4)A grammar compiled to an ANTLR recursive-descent parser

The PostgreSQL choice is the notable one: rather than reimplement PG's grammar, sqlc embeds libpg_query, the exact parser Postgres itself uses, so any syntax the server accepts, sqlc accepts. (When cgo is unavailable — Windows, or a pure-Go build — it falls back to wasilibs/go-pgquery, a WASM build of the same library; parser/parser_wasi.go.) Each parser translates the dialect AST into sqlc's shared internal/sql/ast node types, so the rest of the compiler is engine-agnostic.

The catalog: schema is the source of truth

sqlc reads your schema DDL and folds it into an in-memory catalog — the type environment for every subsequent query (internal/compiler/compile.go). It parses CREATE/ALTER statements (docs/howto/ddl.md): "sqlc parses CREATE TABLE and ALTER TABLE statements in order to generate the necessary code." Column nullability, primary keys, array dimensions, and enum values are all captured here (e.g. a column is NotNull if it carries NOT NULL or is part of the primary key — postgresql/parse.go). By default there is no database connection: the catalog is built entirely from the .sql files, and type inference runs against it statically. An optional analyzer.database mode can connect to a real database and use EXPLAIN/describe output to refine inference, but it is off unless configured (internal/compiler/engine.go) — the static path is the norm.

sqlc does not run migrations

A pointed absence: sqlc is not a migration runner. It reads schema, but never applies it (docs/howto/ddl.md):

"sqlc does not perform database migrations for you. However, sqlc is able to differentiate between up and down migrations. sqlc ignores down migrations when parsing SQL files."

You point sqlc's schema at either a single DDL file or a directory of migration files produced by an external tool — atlas, dbmate, golang-migrate, goose, sql-migrate, tern are all supported (ddl.md) — and sqlc strips the down/rollback halves (RemoveRollbackStatements in internal/migrations/migrations.go) to reconstruct the current schema. Applying migrations, versioning them, and recording what ran is left to that external tool. This is the opposite of sqlx (Rust) and the effect systems, which ship a first-party checksum-verified migration runner.

The emitter, and the plugin architecture

The built-in Go back end is a text/template emitter (internal/codegen/golang/gen.go): it builds []Enum, []Struct, and []Query from the compiler's typed result, then renders db.go (the DBTX/Queries scaffolding), models.go (a struct per table/enum), and <queries>.sql.go (a const + method per query). The generated code is run through go/format, so it is gofmt-clean — the tutorial notes sqlc "generates readable, idiomatic Go code that you otherwise would've had to write yourself" (tut).

Crucially, the Go generator is itself a codegen plugin. Every back end — built-in or external — is invoked over the same CodegenService gRPC interface with a GenerateRequest protobuf; the built-in Go and JSON emitters are wired in-process via ext.HandleFunc(golang.Generate), exactly parallel to how a WASM or subprocess plugin is called (internal/cmd/generate.go). External languages plug in two ways (docs/guides/plugins.md):

  • WASM plugins — a .wasm module fetched by URL and pinned by sha256, run in the wazero sandbox: "WASM plugins are fully sandboxed; they do not have access to the network, filesystem, or environment variables."
  • Process plugins — an external binary reading the request on stdin: "Process-based plugins offer minimal security. Only use plugins that you trust."

This is how sqlc targets Kotlin, Python, and TypeScript (sqlc-gen-kotlin et al.) — and how the community adds C#, F#, Ruby, Zig, and more, all consuming the same schema-and-query-analysis front end.


Type mapping & result decoding

Type mapping is a compile-time table lookup, not a runtime codec. Each engine has a <engine>_type.go mapping catalog types to Go types; goType in internal/codegen/golang/go_type.go resolves a column to its Go type, honoring user overrides first. The doc states the default posture (docs/reference/datatypes.md): "sqlc attempts to make reasonable default choices when mapping internal database types to Go types."

Nullability is driven by the schema, and it flows into the Go type. A NOT NULL timestamp becomes time.Time; a nullable one becomes sql.NullTime (datatypes.md) — or, under the pgx/v5 package, the corresponding pgtype. PostgreSQL arrays materialize as Go slices (text[][]string). Enums become a defined Go type with typed constants. Because inference reads the catalog's NotNull bit, adding NOT NULL in the schema changes the generated Go type — nullability is a first-class, statically-derived property, the same headline that sqlx and Kysely sell, reached here through DDL parsing rather than a live-DB describe.

Row hydration is generated Scan code, not reflection. For :one the method does row.Scan(&i.ID, &i.Name, …); for :many it loops rows.Next()/rows.Scan(…) into a slice (queryCode.tmpl). The field order and pointers are fixed at generation time from the resolved column list, so decoding costs nothing beyond the driver's own Scan. Custom Go types plug in via overrides (map a column or a DB type to your own type, provided it implements the driver's Scanner/Valuer), and sqlc.embed (above) composes existing model structs into a nested row type — a limited, explicit form of object composition, never an automatic object graph.


Effect model, transactions & error handling

This is the dimension the survey weights most heavily, and sqlc lands at the blocking point — the generated code is ordinary synchronous Go.

Blocking calls, no effect value

Every generated method is a direct, synchronous driver call taking a context.Context as its first argument and returning (value, error) (queryCode.tmpl). There is no future, no Task, no IO/effect value: authors, err := queries.ListAuthors(ctx) runs the query and blocks until the rows arrive (tut). This is the Go idiom — concurrency is the caller's job (a goroutine per unit of work), not a property of the query value. It is the antithesis of doobie/skunk/Quill's interpretable ConnectionIO/ZIO descriptions, and of sqlx's (Rust) async futures: a sqlc method is neither reifiable nor deferred; it is the effect, executed on call. For this survey's algebraic-effects lens, sqlc is the reference point for "typed shape, untyped effect" — the result is exhaustively typed, but the effect is a bare, eager, side-effecting function.

Transactions: the driver's, wrapped by WithTx

sqlc generates no transaction combinator. Instead the Queries type carries a WithTx method (docs/howto/transactions.md): "the WithTx method allows a Queries instance to be associated with a transaction." You manage the transaction lifecycle with the driver's API and re-bind the query set onto it:

go
tx, err := db.Begin()
if err != nil {
    return err
}
defer tx.Rollback()
qtx := queries.WithTx(tx)
r, err := qtx.GetRecord(ctx, id)
// … more qtx calls …
return tx.Commit()

WithTx(tx) simply returns a new Queries{db: tx} — because *sql.Tx satisfies DBTX, the same generated methods now run inside the transaction (dbCode.tmpl). Begin/commit/rollback, savepoints, isolation levels, and nesting are all whatever the underlying driver offers; sqlc neither models nor constrains them. There is no withTransaction(effect) block, no automatic rollback-on-error beyond the idiomatic defer tx.Rollback(), and no savepoint machinery like the effect systems' nested withTransaction.

Errors: plain Go error, no typed channel

Every method returns a bare error. A :one query that finds no row surfaces the driver's sql.ErrNoRows (or pgx.ErrNoRows); a constraint violation surfaces the driver's error type, which the caller inspects with errors.Is/errors.As and driver-specific predicates (e.g. *pq.Error/*pgconn.PgError for a unique-violation SQLSTATE). sqlc adds no error taxonomy of its own — there is no SqlError reason union like Effect TS, no per-query typed error slot, no Either hierarchy like hasql. An optional wrap_errors codegen flag wraps returns in fmt.Errorf("query X: %w", err) for context (gen.go), but the channel remains Go's single untyped error. This is the same coarseness as sqlx's (Rust) monolithic Error, taken further: sqlc delegates the error type to the driver entirely.

Build-time verification: vet, verify, diff

sqlc pushes some checks that other tools do at runtime into the toolchain. sqlc vet runs queries through CEL lint rules (docs/howto/vet.md) — with an optional database connection it exposes EXPLAIN (… FORMAT JSON) output to the rules, catching, e.g., a query that would do a sequential scan. sqlc verify ("Verify schema, queries, and configuration", internal/cmd/verify.go) checks queries against an updated schema via sqlc Cloud, and sqlc diff compares freshly-generated output to the committed files so CI fails if someone edited SQL without regenerating. These are the compiler's answer to the effect systems' runtime typed errors: shift the failure left, to generate/CI time.


Ecosystem & maturity

sqlc is a mature, widely-adopted tool under the permissive MIT license, copyright "Riza, Inc." (LICENSE), created by Kyle Conroy (the sponsor link points at github.com/sponsors/kyleconroy, and the first release was ≈2019 — web-attested via the introductory blog post). The pinned tree reports v1.31.1 (internal/info/facts.go).

Backends. PostgreSQL and MySQL are the most complete; SQLite is Beta for the built-in Go generator (docs/reference/language-support.rst). ClickHouse and GoogleSQL engines also exist in internal/engine/ (the pinned commit — 22d878a — is "feat: add GoogleSQL support to sqlc parse"), though narrower than the big three.

Languages. Beyond built-in Go, first-party plugins cover Kotlin, Python, and TypeScript (README.md); the community language table lists C#, F#, Java, PHP, Ruby, Zig, Rust, and Gleam plugins (langsupport) — all riding the same WASM/process plugin protocol. This plugin split is what lets a single SQL-analysis front end fan out across ecosystems, and is the strongest architectural bet in the project.

sqlc Cloud adds hosted query verification, managed test databases, and push/verify against published schema versions (tut) — an optional commercial layer over the open-source generator.


Strengths

  • Real SQL front end. sqlc genuinely parses your SQL (Postgres via libpg_query — the server's own grammar) and type-checks it against a catalog, so column/type/table bugs fail generate, before any Go compiles — and without a database connection.
  • Injection-safe by construction, zero runtime. Queries are const strings; data binds out-of-band through database/sql. No ORM, no reflection, no runtime dependency — the emitted code is plain, gofmt-clean Go you own.
  • Nullability and types in the generated signatures. NOT NULL in the schema changes the Go type; arrays, enums, and composite/embedded rows all map through statically.
  • Idiomatic, driver-agnostic output. One method per query on a four-method DBTX interface; works with database/sql, lib/pq, or pgx, and drops straight into transactions via WithTx.
  • Language-agnostic via plugins. A WASM/process codegen protocol (the built-in Go generator uses it too) targets Kotlin, Python, TypeScript, and a long community tail from one front end.
  • Shift-left tooling. vet (CEL + EXPLAIN), verify, and diff push checks into CI that other stacks meet only at runtime.

Weaknesses

  • No dynamic queries. Every query is a fixed, named statement decided at generation time; runtime-variable structure (conditional WHERE, dynamic columns/ORDER BY) has no expression — you fall back to hand-written database/sql there.
  • Not a migration tool. sqlc reads schema but never applies it; you need a separate migration runner, and sqlc's view of the schema can drift from what's actually deployed.
  • Static inference has blind spots. Without the optional live-DB analyzer, complex expressions, some function return types, and tricky nullability need sqlc.narg/overrides or column annotations to steer; the analyzer that fixes this needs a database and is off by default.
  • Blocking, untyped-effect model. Generated methods are eager synchronous calls returning a bare error — no effect value to inspect/interpret, no typed error channel, transactions and savepoints entirely delegated to the driver.
  • A regeneration step in the loop. The generated code is an artifact to re-run and re-commit on every SQL change; diff in CI is needed to keep it honest.
  • Backend/language maturity is uneven. PostgreSQL/MySQL lead; SQLite is Beta; ClickHouse and GoogleSQL are narrower; non-Go languages are Beta plugins.

Key design decisions and trade-offs

DecisionRationaleTrade-off
Embed a real per-engine parser (Postgres = libpg_query)Accept anything the dialect does; type-check statically without a DB connectionThree parser back ends to maintain; static inference has gaps a live DB would resolve
Static catalog from schema .sql, DB analyzer optionalNo database needed at build time; deterministic, CI-friendly generateSchema files can drift from the deployed DB; hard cases need macros/overrides or the opt-in analyzer
-- name: … :cmd annotation drives codegenMinimal surface — annotated SQL is the whole input; command picks the method shapeEvery query must be a fixed, named statement; no runtime-dynamic query structure
Emit const SQL + bind params on database/sql/pgxInjection-safe by construction; zero runtime dependency; idiomatic, readable GoCannot vary query shape at runtime; sqlc.slice is the one (still param-safe) exception
Generate no connection/pool/transaction code (DBTX + WithTx)Delegate resource lifetime to the driver; work with any handleNo pooling/lifetime/savepoint model of its own; caller owns all of it
Blocking methods, bare error returnsMatch Go idiom; concurrency and error taxonomy are the caller'sNo effect value, no typed error channel; transactions are driver-managed
Everything is a codegen plugin (WASM/process, built-ins too)One SQL front end targets many languages; sandboxed, pinned WASM pluginsNon-Go targets are separate plugins at varying maturity; a plugin protocol to keep stable
Read migrations, never run themStay a compiler; interoperate with atlas/goose/dbmate/…No first-party migration runner or checksum verification (unlike sqlx, the effect systems)

Sources