ADR-003: SQL Code Generation and Data Access Pattern
Date: 2025-04-30
Status: Accepted
Context
Section titled “Context”We need a robust, maintainable, and type-safe way to interact with our PostgreSQL database (managed by Supabase) from our Go application. The approach should handle mapping database rows to Go structs, executing queries, and managing connections.
Key considerations include:
- Type safety between Go code and the database schema.
- Performance overhead of the data access layer.
- Avoiding boilerplate code for CRUD operations.
- Testability of the application logic that depends on data access.
- Developer control over the exact SQL being executed.
Decision
Section titled “Decision”-
SQL Code Generation: We will use
sqlc(https://github.com/sqlc-dev/sqlc) to generate type-safe Go code directly from SQL queries.- SQL queries (
.sqlfiles containingCREATE,SELECT,INSERT,UPDATE,DELETEstatements with specialsqlccomments) will be stored in thesql/queries/directory. sqlcwill generate Go code (structs matching table schemas, functions for each query, and aQuerierinterface) into theinternal/database/package.- A
task db:generatecommand will be added toTaskfile.ymlto runsqlc generate.
- SQL queries (
-
Data Access Pattern: We will implement the Repository Pattern on top of the
sqlc-generated code.- Interfaces (e.g.,
UserRepository) will be defined in theinternal/repository/package. These interfaces will define the data access methods required by the application’s business logic (e.g.,GetUserByID,CreateUser). - Concrete implementations of these repository interfaces will be created, embedding the
sqlc-generated*Queriesstruct (which implements theQuerierinterface) and the*pgxpool.Poolfor database connections. - Application logic (handlers, services) will depend on the repository interfaces, not the concrete implementations or the
sqlc-generated code directly.
- Interfaces (e.g.,
Consequences
Section titled “Consequences”Positive:
- Type Safety:
sqlcgenerates Go code that reflects the SQL query parameters and return types, catching mismatches at compile time. - Reduced Boilerplate: Eliminates manual
sql.Rowsscanning and struct mapping. - Performance: Minimal overhead compared to full ORMs, as it generates straightforward code using the standard
database/sqlorpgxinterfaces. - SQL Control: Developers write and control the exact SQL queries.
- Testability: The Repository Pattern allows business logic to be tested easily by mocking the repository interfaces, decoupling it from the actual database.
- Clear Separation: Enforces a clean separation between data access logic (repositories) and business logic.
Negative:
- Requires Writing SQL: Developers need to write SQL queries manually in
.sqlfiles. - Extra Layer: The Repository Pattern adds a layer of interfaces and structs compared to directly using the
sqlc-generatedQuerier. - Generation Step: Requires running
sqlc generateafter changing SQL queries or schema.
Alternatives Considered
Section titled “Alternatives Considered”- Full ORM (e.g., GORM, Ent): Rejected due to potential performance overhead, “magic” behavior obscuring underlying SQL, and sometimes complex APIs. We prefer more explicit SQL control.
- Lightweight SQL Wrappers (e.g., sqlx): Considered, but
sqlcprovides stronger compile-time type safety guarantees derived directly from the SQL queries. - Manual
database/sqlorpgx: Rejected as it involves significant boilerplate for scanning rows and mapping to structs, whichsqlcautomates. - Direct use of
sqlcQuerier: Considered, but using the Repository Pattern provides better decoupling and testability for application logic.
References
Section titled “References”sqlcDocumentation: https://docs.sqlc.dev/- Repository Pattern discussions.
Enforcement
Section titled “Enforcement”- Testable consequences:
- TC-1: No hand-written SQL string literals in
internal/handler. - TC-2: Handlers do not import
pgxordatabase/sql— data access goes through repository interfaces. (Importinginternal/databasefor sqlc-generated param/row types is legitimate.)
- TC-1: No hand-written SQL string literals in
- Checks:
- TC-1, TC-2 →
adr003-no-sql-in-handlersinscripts/adrcheck(status: block, promoted 2026-09-30)
- TC-1, TC-2 →
- Not machine-checkable: Repository-interface granularity and naming judgment.
- Amendment 2026-08-19 (#90): TC-3 — committed
internal/database/equals whatsqlc generateproduces fromsql/→task check:generated(scripts/checkgenerated, regenerates into a scratch copy and diffs) intask ci(status: block since 2026-09-30 via-mode=blockinTaskfile.yml). Closes the ADR-033 TODO. - Graduation log:
- 2026-08-19 — the one standing
adr003-no-sql-in-handlersfinding (health handler importedpgxpoolto ping the pool, ADR-033 launch note) was resolved by refactoring the ping behind a consumer-sidehandler.Pingerseam (#92, #99); the check has reported clean since. Status stays warn — promotion per the 7-clean-days rule. - 2026-09-30 — promoted warn → block (ADR-033 §4): clean since 2026-07-12 (80 days, zero false positives; the rule asks for 7).
checks/enforcement.config.jsoncarriesgraduated: 2026-09-30. Demotion back to warn is allowed with the same trail. - 2026-09-30 —
task check:generatedpromoted warn → block (-mode=blockinTaskfile.yml, ADR-033 §4): clean since 2026-08-19 (42 days). It caught the real templ-CLI drift class the Docker builder had (CHANGELOG 0.9.0) before it was wired, which is the other promotion trigger.
- 2026-08-19 — the one standing