feat: sqlc.switch — compile-time query branches for dynamic ORDER BY/WHERE (discussion #364)#4485
Open
celso-alexandre wants to merge 7 commits into
Open
feat: sqlc.switch — compile-time query branches for dynamic ORDER BY/WHERE (discussion #364)#4485celso-alexandre wants to merge 7 commits into
celso-alexandre wants to merge 7 commits into
Conversation
Add a sqlc.switch(@selector, sqlc.when('key', 'sql'), sqlc.else('sql')) macro that expands at compile time into one static query per branch, named <QueryName><BranchKey>. This implements the sqlc.switch() idea floated in discussions/364 ("generate multiple optimized queries at compile time rather than runtime CASE"). Each branch fragment is spliced into the query in place of the macro call and re-parsed as an ordinary query, so: - the generated SQL is fully static (no runtime CASE, planner uses indexes); - branch fragments are author-written constants, never runtime input, so there is no SQL injection surface; - a bad column reference in a fragment is a normal compile error; - a generated name colliding with another query is caught by the existing duplicate-query-name check. Recognition is AST-based, identical to sqlc.arg/sqlc.slice, so it works wherever those macros parse: WHERE on all engines, ORDER BY on PostgreSQL and MySQL. SQLite drops function calls in ORDER BY (see sqlc-dev#4429), so it errors there instead of emitting the unexpanded call. The macro is rejected in the SELECT projection, where branches could change the result shape. The only change to existing code is a thin wrapper in parseQueries; all macro logic lives in the new internal/compiler/expand_switch.go. Includes unit tests and golden end-to-end tests for PostgreSQL (stdlib + pgx), MySQL, and SQLite, plus a design note in docs/proposals/sqlc-switch.md.
Allow several sqlc.switch() calls in one query as long as they declare the same keys (e.g. the same sort applied in a CTE pre-sort and the final ORDER BY). Expansion stays linear in the number of keys — one function per key, each call contributing its own fragment — not the cross product. Branches expanded from a sqlc.switch() now share a single Params and Row struct named after the original query, instead of an identical copy per branch. All branches have the same parameters and result columns (only the spliced fragment differs), so the per-branch structs were byte-identical. A new SwitchGroup field links the branches from compiler metadata through to the Go generator, which emits the shared struct once and points every branch at it.
VariableForField used EmitStruct() to decide between arg.Field and an inlined bare name. A value whose struct is DefinedElsewhere (a shared sqlc.switch Params struct) still takes a single struct arg, so it must use arg.Field. Switch to v.Emit to match Pairs().
SQLite ORDER BY clauses were parsed but not propagated into SelectStmt.SortClause during AST conversion. This caused ORDER BY expressions, including sqlc.switch(...) and function calls such as upper(name), to be invisible to later AST visitors and macro recognition passes. Co-authored-by: OpenAI Codex <codex@openai.com>
…support fix(sqlite): populate SelectStmt.SortClause during AST conversion
… validation Populating SelectStmt.SortClause for SQLite (006f4e8) made the strict_order_by validation apply to SQLite queries for the first time. That broke queries ordering by rowid, _rowid_, or oid, which exist on most SQLite tables but never appear in the declared schema. Skip validation for those implicit names on the sqlite engine only; typos in regular column names still fail. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…bumps) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements
sqlc.switch— compile-time expansion of dynamicORDER BY/WHEREbranches into one static, fully-validated query per branch.Follows up on the proposal posted in #364 (comment), opened after @StevenACoffman indicated a PR would be welcome at this point.
expands at
sqlc generatetime intoListAuthorsNameAsc,ListAuthorsRecent, andListAuthorsElse— each a plain static query. No runtimeCASE(planner/index friendly), no string interpolation (no injection surface), and every branch is re-parsed and validated against the catalog like any ordinary query.Design
WHERE,ORDER BY); rejected in theSELECTlist.sqlc.arg/sqlc.slice; macro logic lives in a dedicated compiler file, with a thin wrapper inparseQueries. The core expansion has zero codegen changes, so all language backends benefit.sqlc.switchper query are allowed when they declare the same key set (e.g. the same choice driving a CTE pre-sort and the finalORDER BY); expansion stays linear, one function per key, with sharedParams/Rowstructs in the Go backend.Full design note:
docs/proposals/sqlc-switch.md.Commits
feat(compiler): sqlc.switch for compile-time query branches— core expansion + validationfeat(golang): multiple sqlc.switch per query + shared Params/Row structsfix(golang): reference shared struct fields as arg.Field in query bodyfix(sqlite): populate SelectStmt.SortClause during AST conversion— contributed by @fahmifan; SQLite ORDER BY terms were parsed but never propagated, which also blockedsqlc.switch(andsqlc.arg) recognition therefix(compiler): allow sqlite implicit rowid columns in strict ORDER BY validation— populatingSortClausemadestrict_order_byapply to SQLite for the first time; without this, existing queries usingORDER BY rowidwould start failingTesting
internal/endtoend/testdata/sqlc_switch/,order_by_rowid/)go test --tags=examples -timeout 20m ./...) green against live PostgreSQL 16 and MySQL 9replacedirective and regenerated: the only diffs were in queries that actually usesqlc.switch— everything else came out byte-identicalDisclosure
The initial implementation was AI-generated as a proof of concept to explore the design space; it has since been manually reviewed and tested (unit tests, golden tests across all three engines, and real use in a production project). Details in the discussion post.
Open questions
Carrying over from the discussion, happy to adjust any of these:
sqlc.switch/when/else— or something else?sqlc.elsebe mandatory, or is "no match falls through" acceptable?ORDER BYonly, orWHEREtoo?🤖 Generated with Claude Code