sqlc — Architecture#
Architectural style#
Compiler Pipeline with a Language-Agnostic Plugin System
sqlc is architecturally a compiler: it takes SQL files and a schema, produces a typed intermediate representation (IR), and then hands that IR off to code generators. The pipeline stages — parse, catalog-build, type-resolve, IR-emit, codegen — map almost 1:1 to the named packages under internal/. The compiler’s output boundary with code generators is a protobuf-defined GenerateRequest/Response that enables both in-process Go generators and out-of-process plugins (WASM or subprocess) without changing the orchestration layer.
This makes sqlc a layered compiler with an extensible codegen plugin surface, not a typical service or library.
Component diagram (textual)#
┌────────────────────────────────────────────────────────────────┐
│ cmd/sqlc → pkg/cli → internal/cmd/Do() │
│ (thin shell: cobra commands; reads sqlc.yaml config) │
└─────────────────────────────┬──────────────────────────────────┘
│
┌───────────────────▼──────────────────────┐
│ internal/cmd/processQuerySets() │
│ (errgroup; GOMAXPROCS parallelism) │
└───┬───────────────────────────────────────┘
│ │
┌─────────▼──────────┐ ┌──────────▼──────────┐
│ parse() pipeline │ │ codegen() dispatch │
│ │ │ │
│ compiler.New() │ │ ┌──────────────┐ │
│ .ParseCatalog() │ │ │ Built-in Go │ │
│ .ParseQueries() │ │ │ (golang.Gen) │ │
│ .Result() │ │ ├──────────────┤ │
│ │ │ │ WASM plugin │ │
│ ┌──────────────┐ │ │ │ (wasm.Runner)│ │
│ │ Engine layer │ │ │ ├──────────────┤ │
│ │ (dialect- │ │ │ │ Process plug │ │
│ │ specific │ │ │ │(process.Run.)│ │
│ │ parser + │ │ │ └──────┬───────┘ │
│ │ catalog) │ │ │ │ │
│ └──────┬───────┘ │ │ plugin.Codegen │
│ │ sql/ast │ │ ServiceClient │
│ ┌──────▼───────┐ │ │ (grpc.ClientConn │
│ │ sql/catalog │ │ │ interface) │
│ └──────────────┘ │ └─────────────────────┘
│ ┌──────────────┐ │
│ │ analyzer │ │ (optional: live DB type analysis)
│ │ (optional) │ │
│ └──────────────┘ │
└────────────────────┘
│
┌─────────▼─────────────────────────────────────┐
│ compiler.Result → plugin.GenerateRequest │
│ (typed query IR, catalog snapshot) │
└───────────────────────────────────────────────┘There is also an alternate execution path for sqlc.cloud:
internal/cmd/Generate()
→ remoteGenerate()
→ remote.NewClient() + gRPC call to sqlc.cloud
→ returns generated files directlyCore components#
CLI layer#
- Package:
cmd/sqlc,pkg/cli,internal/cmd - Responsibility: Entry point. Cobra command registration, flag parsing, I/O wiring. The
Do(args, stdin, stdout, stderr)function ininternal/cmdis the single top-level dispatcher. - Key types:
cobra.Command,Env(debug/experiment flags),Options - Dependencies: cobra, config, tracer, opts
Config#
- Package:
internal/config,internal/config/convert - Responsibility: Parses
sqlc.yaml/sqlc.jsoninto typed Go structs. Handles v1/v2 config version migration. Validates config combinations. - Key types:
config.Config,config.SQL,config.CombinedSettings,config.Plugin - Dependencies: gopkg.in/yaml.v3, stdlib encoding/json
Compiler#
- Package:
internal/compiler - Responsibility: Orchestrates the parse/type-resolve pipeline for a single SQL queryset. Creates the engine (parser + catalog) for the configured dialect, runs
parseCatalogto build the in-memory schema, thenparseQueriesto type-check each query against the catalog. - Key types:
Compilerstruct,Parserinterface (dialect-agnostic parse contract),Result(typed query IR + catalog snapshot) - Dependencies: engine/* (dialect-specific parsers), sql/ast, sql/catalog, analyzer, opts
Engine layer (SQL dialects)#
- Package:
internal/engine/postgresql,internal/engine/dolphin(MySQL),internal/engine/sqlite,internal/engine/clickhouse - Responsibility: Each dialect sub-package provides a
Parserimplementation and aNewCatalog()factory. The parser converts the native parse tree (pg_query_go for PostgreSQL, TiDB parser for MySQL, an internal parser for SQLite) into the sharedsql/astnode types. - Key types: dialect-specific
Parserstructs;sql/ast.Statement,sql/catalog.Catalog - Dependencies: pg_query_go (PostgreSQL), TiDB parser (MySQL); no external dep for SQLite
SQL AST and Catalog#
- Package:
internal/sql/ast,internal/sql/catalog - Responsibility: Language-agnostic AST node types that all engines produce. The catalog is an in-memory representation of the database schema (tables, columns, types, functions) built from DDL statements.
- Key types:
ast.Statement,ast.Node,catalog.Catalog,catalog.Schema,catalog.Table - Dependencies: none beyond stdlib
Analyzer (optional live-DB type analysis)#
- Package:
internal/analyzer,internal/engine/postgresql/analyzer,internal/engine/sqlite/analyzer - Responsibility: When a live database URI is configured, the analyzer sends queries to the actual database and retrieves column type information. Results are disk-cached. Used to supplement or replace static catalog analysis.
- Key types:
Analyzerinterface (Analyze,Close,EnsureConn,GetColumnNames),CachedAnalyzer - Dependencies: pgx, database/sql, internal/cache
Plugin dispatch (ext layer)#
- Package:
internal/ext,internal/ext/wasm,internal/ext/process - Responsibility: Provides three interchangeable backends for code generation, all presenting the same
grpc.ClientConnInterface:- Built-in Go generators are wrapped with
ext.HandleFunc()(in-process function call) - WASM plugins are executed via
wasm.Runnerusing the Wazero runtime - Process plugins are executed via
process.Runner(subprocess with protobuf over stdin/stdout)
- Built-in Go generators are wrapped with
- Key types:
ext.Handlerinterface,wasm.Runner,process.Runner - Dependencies: wazero (WASM runtime), google.golang.org/grpc (interface only), internal/plugin
Plugin IR (protobuf)#
- Package:
internal/plugin - Responsibility: Protobuf-generated types that form the formal contract between the compiler and all code generators.
GenerateRequestcarries the full typed query IR;GenerateResponsecarries the output files. - Key types:
plugin.GenerateRequest,plugin.GenerateResponse,plugin.File,plugin.CodegenServiceClient - Dependencies: google.golang.org/protobuf
Built-in code generators#
- Package:
internal/codegen/golang,internal/codegen/json - Responsibility: Consume a
GenerateRequestand produce aGenerateResponse. The Go generator usestext/templatewith type-mapping tables; the JSON generator emits the IR as JSON. Both are wrapped viaext.HandleFuncto use the same dispatch path as external plugins. - Key types:
golang.Generate(ctx, *plugin.GenerateRequest),json.Generate(ctx, *plugin.GenerateRequest) - Dependencies: text/template, encoding/json, internal/plugin
Vet#
- Package:
internal/vet - Responsibility: Implements
sqlc vet: loads CEL rule expressions from the config, evaluates them against each query’s metadata (execution plan, row counts, etc.), and reports violations. Uses the analyzer to execute queries against a live database. - Key types:
VetRule, CEL environment - Dependencies: cel-go, internal/analyzer
Data flow#
Typical sqlc generate run:
1. main() → cmd.Do() → cobra.Execute() → genCmd.RunE()
2. Generate(ctx, dir, filename, opts)
a. readConfig() → config.ParseConfig() → config.Config
b. config.Validate() + env.Validate()
c. processQuerySets() [errgroup, GOMAXPROCS workers]
For each (SQL queryset × generator) pair in parallel:
i. parse(ctx, name, dir, sql, combo, parserOpts, stderr)
→ compiler.NewCompiler(sql, combo, parserOpts)
→ selects engine parser + catalog for dialect
→ optionally wraps with CachedAnalyzer
→ c.ParseCatalog(schema files)
→ reads DDL files, calls parser.Parse()
→ calls catalog.Update(stmt) for each DDL node
→ c.ParseQueries(query files, opts)
→ reads .sql query files, calls parser.Parse()
→ for each stmt: parseQuery() → type-resolution
→ looks up table/column types in catalog
→ optionally calls analyzer.Analyze() for live DB types
→ returns compiler.Result{Catalog, Queries}
ii. codegen(ctx, combo, sql, result)
→ codeGenRequest(result, combo) → plugin.GenerateRequest
→ selects handler: ext.HandleFunc / wasm.Runner / process.Runner
→ plugin.NewCodegenServiceClient(handler).Generate(ctx, req)
→ returns plugin.GenerateResponse{Files}
iii. g.ProcessResult() → writes filename → source to output map
3. For each filename in output map → os.WriteFile()Remote path (when cloud.project + --remote flag):
Generate() → remoteGenerate()
→ bundles config + SQL files into remote.GenerateRequest
→ gRPC call to sqlc.cloud
→ receives generated files in response
→ writes to diskInitialization / Bootstrap#
main()callscmd.Do(os.Args[1:], stdin, stdout, stderr).Do()builds a freshcobra.Commandtree on every invocation — there is no global state aside fromdebug.Debug(set fromSQLCDEBUGenv var).- If
SQLCDEBUG=trace=<path>is set, a runtime trace file is opened before Cobra executes. - Each command reads the config file, constructs a
Compilerper query-set, runs the pipeline, and then exits.
There is no dependency injection framework. All wiring is explicit: NewCompiler receives config.SQL + config.CombinedSettings and constructs the correct engine and analyzer based on the engine: field. The codegen() function constructs the correct handler based on which sql.Gen.* field is set.
The processQuerySets function uses errgroup.WithContext with grp.SetLimit(runtime.GOMAXPROCS(0)) to bound parallelism — each (queryset, generator) pair runs in its own goroutine.
Configuration#
- Format: YAML (
sqlc.yaml,sqlc.yml) or JSON (sqlc.json); both supported. YAML takes precedence if both exist. - Config versions: v1 and v2 schemas;
internal/config/converthandles migration. - Key config sections:
sql[]: one entry per queryset — specifiesengine,schema,queries,gen.go/gen.json,codegen[](plugins)plugins[]: named plugin definitions withwasmorprocessbackendcloud.project: enables remote execution via sqlc.clouddatabase.uri/database.managed: enables live-DB analysis
- No Viper: config is parsed directly with
gopkg.in/yaml.v3andencoding/json. Environment variables are used only for debug/experiment flags (SQLCDEBUG,SQLCEXPERIMENT) read ininternal/opts.
Key design decisions#
Protobuf as the plugin contract — The
GenerateRequest/Responseprotobuf IR is the single most important architectural decision. It makes the code-generator boundary language-agnostic: any program that can read a protobuf from stdin and write one to stdout can be a sqlc plugin. WASM plugins add hermetic sandboxing; the same proto is used.gRPC interface as the universal dispatch abstraction — All three plugin backends (built-in function, WASM, subprocess) implement
grpc.ClientConnInterface, so the codegen dispatch loop is a singleplugin.NewCodegenServiceClient(handler).Generate()call regardless of plugin type. In-process Go generators are wrapped withext.HandleFuncto adapt a plain function into this interface — an elegant use of adapter pattern over an interface standard.Engine isolation via a shared AST — Each SQL dialect has its own parser that converts to
internal/sql/ast, a language-agnostic node tree. The compiler and all analysis code operates only on this shared AST, so adding a new dialect never touches the compiler or code generators. ClickHouse is already partially present as evidence of this extensibility.Optional live-DB analysis with filesystem caching — The
Analyzerinterface allows sqlc to fall back to a live database connection for type information when static analysis is insufficient. Results are cached on disk (hashed by query + schema) to avoid redundant database round-trips, so iterative development is fast even with database-backed analysis enabled.pkg/clias the only public surface — Everything exceptDo()/Run()isinternal/, which makes sqlc safely embeddable as a library while protecting every internal API. Thepkg/clipackage was originallycmd/sqlc/main.go-equivalent, extracted to allow embedding sqlc in tools likesqlc-gen-*plugins.Parallel codegen across query-sets —
processQuerySetsfans out over all (queryset × generator) pairs using an errgroup bounded byGOMAXPROCS. Each pair independently parses its schema and queries, so large configs with many SQL files benefit from multi-core parallelism without any shared mutable state between workers (thegeneratorstruct uses a mutex only for the final output-map write).