Skip to content

fix(duckdb): rewrite sqlc.arg and @name to native placeholders - #4664

Open
xen0l wants to merge 1 commit into
sqlc-dev:mainfrom
xen0l:duckdb-named-params
Open

xen0l wants to merge 1 commit into
sqlc-dev:mainfrom
xen0l:duckdb-named-params

Conversation

@xen0l

@xen0l xen0l commented Oct 24, 2026

Copy link
Copy Markdown

Fixes #4658

Problem

The preprocess dialect table (internal/sql/preprocess/dialect.go) had no entry for DuckDB, and an engine with no entry gets its source back unchanged. sqlc.arg(), sqlc.narg() and @name then reached the analyzer as function calls and column names (unknown column "want_id"), or failed in the parser (@).

Change

  • Add a config.EngineDuckDB entry: $1 placeholders, @name as sqlc syntax, $$ strings, $1 as a bind parameter. A bare ? is left to the engine, as before.
  • Add a NoSlice dialect flag and set it for DuckDB. sqlc.slice is rejected with sqlc.slice is not supported by this engine; bind a list parameter instead.
  • Add a DuckDB row to the dialect table in internal/sql/preprocess/CLAUDE.md.

Why sqlc.slice is rejected: with $1 placeholders the generated Go still calls strings.Replace on a ? marker that the SQL does not contain. The call changes nothing, so the query binds only the first element of the slice and returns wrong rows without an error. I ran id IN (sqlc.slice(ids)) with []int32{1, 3} on DuckDB and got one row. A DuckDB user binds a list with x = ANY($1::INTEGER[]) or list_contains($1::INTEGER[], x); I ran both with []int32{1, 3} and each returned the right two rows.
The PostgreSQL engine generates the same IN ($1) and the same no-op Replace today. I did not change that here.

Tests

New end-to-end cases:

  • duckdb_named_params/duckdb (sqlc generate, output committed): sqlc.arg, sqlc.narg, @name, and two sqlc.arg in one query. It fails without the dialect entry (Parser Error: syntax error at or near "@") and passes with it.
  • analyze_slice_rejected/duckdb (sqlc analyze, stderr.txt committed): sqlc.slice must fail. It fails without the NoSlice flag and passes with it.

go test ./internal/endtoend -run TestReplay/base: 971 pass, 58 skip, 0 fail before; 973 pass, 58 skip, 0 fail after. The only differences are the two new cases.
go test ./internal/engine/... ./internal/core/... ./internal/compiler/... ./internal/codegen/... ./internal/cmd/... ./internal/metadata/... ./internal/sql/...: all ok.

I ran the generated Go of the first case on DuckDB v1.5.6 (duckdb-go/v2 v2.10506.0): ByID(2), ByAt(3), ByNullableID(4) and Range(1,4) each returned the expected rows.

Not run: the managed-db context, --tags=examples, and the internal/goldeneye checks.

Notes

  • A query that mixes sqlc.arg(a) with an explicit $2 fails with could not determine data type of parameter $1. The PostgreSQL and SQLite engines give the same error for the same mix, so I left it.
  • I set the lexical flags from DuckDB's documented syntax. I did not set NestedBlockComment, because I could not confirm whether the DuckDB parser nests block comments. Please check it.

The preprocess dialect table had no entry for DuckDB, and an engine with
no entry gets its source back unchanged. sqlc.arg(), sqlc.narg() and
@name then reached the analyzer as function calls and column names.

Add a DuckDB entry that uses $1 placeholders.

Reject sqlc.slice for this dialect. With $1 placeholders the generated Go
still calls strings.Replace on a "?" marker that the SQL does not
contain, so it binds only the first element of the list and returns wrong
rows without an error. A list parameter (list_contains or ANY) is the
DuckDB way to do this.

Adds the duckdb_named_params and analyze_slice_rejected cases.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

duckdb: sqlc.arg(), sqlc.narg() and @name are not rewritten

1 participant