sqlc — Patterns#
Concurrency patterns#
Fan-out with errgroup + GOMAXPROCS bound#
- Usage: The single concurrency spine of the entire codebase.
processQuerySetsfans out over all (queryset × generator) pairs in parallel, bounded toruntime.GOMAXPROCS(0)workers. - Example:
internal/cmd/process.go:61—grp, gctx := errgroup.WithContext(ctx)followed bygrp.SetLimit(runtime.GOMAXPROCS(0)) - Assessment: Idiomatic and well-controlled. The bound prevents CPU over-subscription without manual pool management. Each worker is fully independent (no shared mutable state beyond the final output map), so the concurrency is safe by design. Note: goroutine spawning with
go func(...)counts 0 — all concurrency flows througherrgroup.Go, which is a deliberate discipline.
Mutex-guarded output accumulation#
- Usage: The
generatorstruct uses async.Mutexto protect writes to theoutput map[string]stringwhile fan-out workers run concurrently. - Example:
internal/cmd/generate.go:172—m sync.Mutexfield on the generator; locked onProcessResult. - Assessment: Minimal locking scope — the mutex is only held during map writes, not during the heavy parsing/codegen work. This is the right granularity.
sync.Once for lazy initialization#
- Usage: Used in two places:
cmd/vet.go:390(clientOnce sync.Once) for lazy gRPC client initialization;internal/sqltest/local/mysql.go:20for one-time database setup. - Assessment: Standard Go idiom for expensive, idempotent initialization. Prevents redundant setup under concurrent test runs.
sync.Map for concurrent analyzer caches#
- Usage:
internal/engine/postgresql/analyzer/analyze.go:31-33uses threesync.Mapfields (formats,columns,tables) to cache per-query analysis results across concurrent queries. - Assessment: Appropriate choice for write-once/read-many caching under goroutine concurrency from the errgroup fan-out.
sync.RWMutex for pattern matching cache#
- Usage:
internal/pattern/match.go:17—matchCacheLock sync.RWMutexguards a compiled-regex cache used by the type-override glob matching in the Go code generator. - Assessment: Correct read-favored locking for a cache that is warm after the first run.
Select for polling / readiness waiting#
- Usage: 6
select {}occurrences, all in test infrastructure:internal/sqltest/native/postgres.go:199,internal/sqltest/docker/postgres.go:86, and equivalent MySQL paths. Used to poll for database readiness with atime.Aftertimeout case. - Assessment: Confined to test harness code; production paths do not use select-based polling.
Categories check#
- Worker pools: Implicit via
errgroup.SetLimit(GOMAXPROCS)— bounded concurrency, not a named pool. - Fan-out/fan-in: Yes — processQuerySets fans out, errgroup.Wait() fans in.
- Pipeline processing: Compiler pipeline is sequential within each goroutine (ParseCatalog → ParseQueries → codegen), not a channel-based pipeline.
- Context cancellation: Thorough — context.Context appears 5272 times across all packages;
errgroup.WithContextpropagates cancellation to all workers. - Graceful shutdown: Not applicable — sqlc is a CLI tool that exits when done. No long-running server to shut down gracefully.
- Rate limiting: None beyond the GOMAXPROCS-bounded errgroup.
Error handling#
- Style: Mixed: sentinel errors for domain conditions, structured error types for source-location errors,
fmt.Errorf("%w", ...)for wrapping at call boundaries. - Error types defined:
sqlerr.Error(internal/sql/sqlerr/errors.go) — carries PostgreSQL error code (e.g.42703), human message, source location (line/column), and a wrapped sentinel. Constructor functions likesqlerr.ColumnNotFound(rel, col)produce pre-built instances.- Sentinel values:
sqlerr.Exists,sqlerr.NotFound,sqlerr.NotUnique— used witherrors.Isfor conditional logic (e.g., “drop if not exists” DDL handling). multierr.FileError(internal/multierr/error.go) — wraps an error with filename + line + column. ImplementsUnwrap(). Aggregated intomultierr.Errorwhich collects all parse errors from a single SQL file.ErrFailedChecks(internal/cmd/vet.go) — sentinel for distinguishing user-visible vet failures from unexpected tool errors.
- Wrapping approach:
fmt.Errorf("%w", err)is the universal approach; nopkg/errors.errors.Isanderrors.Asare used for unwrapping in call sites (internal/ext/process/gen.go:78,internal/sql/catalog/func.go:103, etc.). - Examples:
internal/sql/sqlerr/errors.go:42—sqlerr.ColumnNotFound("users", "id")returns&Error{Err: NotFound, Code: "42703", Message: "column \"id\" of relation \"users\""}.internal/multierr/error.go:26—e.Add(filename, in, loc, err)computes source line/column and appends aFileError; the compiler collects these and returns the aggregate.cmd/sqlc-test-setup/main.go:140—fmt.Errorf("configuring apt proxy: %w", err)— the prevalent wrapping style.
Configuration pattern#
- Approach: Config struct passed as value/pointer. No functional options at the application level (only
internal/quickdb/rpc.gousesOption func(*options)for a minor RPC utility). The dominant pattern is aconfig.Configtree parsed from YAML/JSON, combined intoconfig.CombinedSettingsper queryset viaconfig.Combine(). - Example:
internal/cmd/process.go:71—combo := config.Combine(*conf, sql.SQL)merges global defaults with per-queryset settings;combois threaded through the entire pipeline as a plain struct value. - Assessment: The config struct approach is straightforward and testable. The
config.Combinestep makes the “effective config” explicit rather than having scattered defaults.
Dependency injection#
- Approach: Manual wiring — no DI framework (no Wire, dig, or fx).
- Evidence:
internal/compiler/compiler.go—NewCompiler(sql config.SQL, combo config.CombinedSettings, parserOpts opts.Parser)selects and constructs the dialect engine based onsql.Engine, wires in the optionalCachedAnalyzer, and returns the complete*Compiler.internal/cmd/generate.go—codegen()selects one of threeHandlerimplementations (built-in, WASM, process) based on whichsql.Gen.*orsql.Plugin.*field is populated.- No global state aside from
debug.Debug(set from theSQLCDEBUGenv var).
- Assessment: The manual wiring is clean and easy to follow precisely because the constructor site is explicit. The absence of a DI framework is appropriate for a CLI tool with a fixed startup sequence.
Other notable patterns#
Adapter pattern (HandleFunc)#
internal/ext/handler.go:50 — HandleFunc(fn) wraps a plain func(context.Context, *plugin.GenerateRequest) (*plugin.GenerateResponse, error) into the Handler interface, which extends grpc.ClientConnInterface. This means built-in Go generators (which are just functions) participate in the same dispatch path as WASM and subprocess plugins without any special casing in the caller. A concise, idiomatic adapter.
Interface embedding for unified dispatch#
internal/ext/handler.go:14-19 — Handler embeds the two methods of grpc.ClientConnInterface (Invoke, NewStream) alongside the domain-specific Generate method. This lets any Handler be passed directly to plugin.NewCodegenServiceClient, which expects a grpc.ClientConnInterface. The embedding makes the type relationship explicit and eliminates a conversion step.
Visitor pattern via recursive type switches (AST traversal)#
The SQL dialect parsers (internal/engine/postgresql/parse.go, convert.go) walk the pg_query_go protobuf parse tree using deep recursive type switches on union node.Node fields. There is no formal Visitor interface — Go’s type switch serves as the dispatch mechanism. Example: internal/engine/postgresql/parse.go:99 — switch n := in.Node.(type) dispatches to 40+ case branches.
Decorator pattern (CachedAnalyzer)#
internal/analyzer — CachedAnalyzer wraps an Analyzer and adds disk-based caching of analysis results, keyed by a hash of (query + schema). Transparent to callers via the Analyzer interface. This is the classic decorator: c.inner.Analyze(ctx, q) is called on a cache miss and the result is persisted; on a cache hit the inner is bypassed.
Factory functions with PostgreSQL error codes#
internal/sql/sqlerr/errors.go — constructor functions (ColumnNotFound, RelationExists, etc.) serve as a factory for *sqlerr.Error values, embedding PostgreSQL SQLSTATE codes. Callers test the semantic condition via errors.Is(err, sqlerr.NotFound) rather than string matching. This is the idiomatic Go “errors as values” pattern applied to a domain-specific error vocabulary.
Struct embedding for config composition#
internal/cmd/process.go:20-25 — OutputPair embeds config.SQL directly (anonymous field), giving it all SQL fields while also carrying Gen and Plugin overlays. Used to represent one (queryset × generator) work item without copying all SQL fields.
Minimal generics usage#
One generic function exists: filterHunks[T gonp.Elem] at internal/cmd/cmd.go:249, used in the sqlc diff command to filter diff hunks. Generics adoption is minimal and purposeful — introduced where the type parameter genuinely eliminates a duplicate function, not as a broad refactor.
Table-driven dispatch (switch over config fields)#
internal/cmd/process.go:94-103 — rather than a registry map, a switch { case sql.Gen.Go != nil: ... case sql.Plugin != nil: ... } pattern dispatches to the correct lang/name for each work item. This table-like switch recurs throughout the codegen path. Simple and explicit, appropriate for a fixed set of dispatch cases.