sqlc — Patterns#

Concurrency patterns#

Fan-out with errgroup + GOMAXPROCS bound#

  • Usage: The single concurrency spine of the entire codebase. processQuerySets fans out over all (queryset × generator) pairs in parallel, bounded to runtime.GOMAXPROCS(0) workers.
  • Example: internal/cmd/process.go:61grp, gctx := errgroup.WithContext(ctx) followed by grp.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 through errgroup.Go, which is a deliberate discipline.

Mutex-guarded output accumulation#

  • Usage: The generator struct uses a sync.Mutex to protect writes to the output map[string]string while fan-out workers run concurrently.
  • Example: internal/cmd/generate.go:172m sync.Mutex field on the generator; locked on ProcessResult.
  • 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:20 for 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-33 uses three sync.Map fields (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:17matchCacheLock sync.RWMutex guards 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 a time.After timeout 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.WithContext propagates 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 like sqlerr.ColumnNotFound(rel, col) produce pre-built instances.
    • Sentinel values: sqlerr.Exists, sqlerr.NotFound, sqlerr.NotUnique — used with errors.Is for conditional logic (e.g., “drop if not exists” DDL handling).
    • multierr.FileError (internal/multierr/error.go) — wraps an error with filename + line + column. Implements Unwrap(). Aggregated into multierr.Error which 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; no pkg/errors. errors.Is and errors.As are 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:42sqlerr.ColumnNotFound("users", "id") returns &Error{Err: NotFound, Code: "42703", Message: "column \"id\" of relation \"users\""}.
    • internal/multierr/error.go:26e.Add(filename, in, loc, err) computes source line/column and appends a FileError; the compiler collects these and returns the aggregate.
    • cmd/sqlc-test-setup/main.go:140fmt.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.go uses Option func(*options) for a minor RPC utility). The dominant pattern is a config.Config tree parsed from YAML/JSON, combined into config.CombinedSettings per queryset via config.Combine().
  • Example: internal/cmd/process.go:71combo := config.Combine(*conf, sql.SQL) merges global defaults with per-queryset settings; combo is threaded through the entire pipeline as a plain struct value.
  • Assessment: The config struct approach is straightforward and testable. The config.Combine step 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.goNewCompiler(sql config.SQL, combo config.CombinedSettings, parserOpts opts.Parser) selects and constructs the dialect engine based on sql.Engine, wires in the optional CachedAnalyzer, and returns the complete *Compiler.
    • internal/cmd/generate.gocodegen() selects one of three Handler implementations (built-in, WASM, process) based on which sql.Gen.* or sql.Plugin.* field is populated.
    • No global state aside from debug.Debug (set from the SQLCDEBUG env 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:50HandleFunc(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-19Handler 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:99switch n := in.Node.(type) dispatches to 40+ case branches.

Decorator pattern (CachedAnalyzer)#

internal/analyzerCachedAnalyzer 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-25OutputPair 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.