Skip to content

fix: expand sqlc.slice placeholders at runtime for the sqlite drivers#211

Merged
rayakame merged 9 commits into
mainfrom
fix/sqlc-slice-expansion
Jul 21, 2026
Merged

fix: expand sqlc.slice placeholders at runtime for the sqlite drivers#211
rayakame merged 9 commits into
mainfrom
fix/sqlc-slice-expansion

Conversation

@rayakame

@rayakame rayakame commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #210: queries using sqlc.slice failed at runtime on the SQLite drivers because the /*SLICE:name*/? placeholder sqlc leaves in the SQL was never expanded.

sqlc's contract puts that expansion on the code generator ("the /*SLICE:ages*/ placeholder is dynamically replaced on a per-query basis"), and its own Go codegen does it via strings.Replace in the generated code. The plugin only mapped the parameter to collections.abc.Sequence[...] and copied the SQL through verbatim, so binding the sequence raised sqlite3.ProgrammingError: type 'list' is not supported.

What generated code looks like now

def get_slice_rows(conn: sqlite3.Connection, *, ids: collections.abc.Sequence[int]) -> QueryResults[models.TestSlice]:
    def _decode_hook(row: sqlite3.Row) -> models.TestSlice:
        return models.TestSlice(id_=row[0], name=row[1])

    sql = GET_SLICE_ROWS.replace("/*SLICE:ids*/?", ",".join("?" * len(ids)) or "NULL", 1)
    return QueryResults(conn, sql, _decode_hook, *ids)
  • One ? per element; an empty sequence becomes NULL, so IN (NULL) matches no rows instead of raising (same semantics as sqlc's Go codegen).
  • Arguments are star-unpacked in SQL text order, so plain parameters around the slice keep binding correctly ((name, *ids, note)).
  • Overridden/converted slice parameters keep their element-wise conversion (*[conv(v) for v in ids]), and bundled query_parameter_limit params work (*params.ids).
  • The marker is built from the raw sqlc name, which can differ from the escaped Python parameter name (sqlc.slice('for') -> parameter for_, marker /*SLICE:for*/?).
  • Multiple slices in one query are replaced sequentially.

Why asyncpg is untouched

sqlc emits a bare IN ($1) on PostgreSQL - no marker, nothing to expand - and that SQL is broken at the sqlc level (verified against live postgres: DataError: invalid input for query argument $1). The macro exists "for drivers that do not support passing slices to the IN operator"; on PostgreSQL the idiomatic form is = ANY($1::type[]), which the plugin fully supports, converters included.

Implementation

  • model.PyType.SqlcSliceName carries the raw slice name; transform populates it in both the override and normal branches.
  • driver: sliceParams collects slice parameters (plain and bundled), expandParamsFlattenSlices star-unpacks them, and the sqlite driver emits the per-call .replace(...) chain. For :many the expansion sits after the decode hook so the output stays a ruff format no-op.

Validation

  • End-to-end against real databases: non-empty, empty, interleaved binding order, two slices in one query, :one, :exec, :execrows - all previously raising, all passing now.
  • New queries_slice.sql fixture module for both SQLite drivers with runtime pytest coverage of every generated function and both QueryResults consumption forms.
  • Go: 100.0% statement coverage on the touched packages, make pipelines clean, full test suite green (shuffled).
  • Python: full nox green (1453 pytest tests), pyright strict + ruff on the regenerated fixtures, and all three *_check sessions confirm the committed fixtures are in sync.

Summary by CodeRabbit

  • Bug Fixes
    • Corrected Python code generation for sqlc.slice on SQLite so slice placeholders expand at runtime into the right SQL placeholder order.
    • Repeated slice markers now interleave correctly with non-slice parameters; empty slices safely expand to NULL, while overrides/conversions remain element-wise.
    • PostgreSQL slice queries now use = ANY(...::type[]) syntax.
  • Tests
    • Expanded slice-based SQLite coverage for inserts, lookups, filtered queries, first-match queries, and deletes (async and sync variants).
  • Documentation
    • Updated the sqlc.slice macro documentation with SQLite runtime expansion details and corrected guidance.

rayakame added 2 commits July 21, 2026 20:53
sqlc leaves a /*SLICE:name*/? marker in the SQL and delegates the
expansion to the code generator, but the plugin only mapped the parameter
type and copied the SQL through verbatim, so every query using
sqlc.slice failed with "type 'list' is not supported" once the driver
was handed the sequence.

Generated functions now rewrite the constant per call, mirroring sqlc's
own Go codegen: the placeholder becomes one "?" per element, or NULL for
an empty sequence so IN (NULL) matches no rows, and the sequence is
star-unpacked into the positional arguments in SQL text order. Overridden
and converted slice parameters keep their element-wise conversion. The
marker is built from the raw sqlc name, which can differ from the escaped
Python parameter name.

asyncpg is deliberately untouched: sqlc emits a bare IN ($1) there with
no marker to expand, and the idiomatic form on PostgreSQL is
ANY($1::type[]), which the plugin already supports.

Closes #210
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ac5540f6-c5dd-4a4c-acec-4ca6e0ffe1f2

📥 Commits

Reviewing files that changed from the base of the PR and between 137c030 and a91fc80.

⛔ Files ignored due to path filters (5)
  • test/driver_aiosqlite/dataclass/functions/queries_slice.py is excluded by !test/driver_*/*/functions/**
  • test/driver_aiosqlite/sqlc-gen-better-python.wasm is excluded by !**/*.wasm, !**/*.wasm
  • test/driver_asyncpg/sqlc-gen-better-python.wasm is excluded by !**/*.wasm, !**/*.wasm
  • test/driver_sqlite3/dataclass/functions/queries_slice.py is excluded by !test/driver_*/*/functions/**
  • test/driver_sqlite3/sqlc-gen-better-python.wasm is excluded by !**/*.wasm, !**/*.wasm
📒 Files selected for processing (12)
  • docs/content/docs/reference/feature-support.md
  • internal/driver/common.go
  • internal/driver/common_test.go
  • internal/driver/sqlite_test.go
  • sqlc.yaml
  • test/driver_aiosqlite/dataclass/test_aiosqlite_dataclass_functions.py
  • test/driver_aiosqlite/queries_slice.sql
  • test/driver_aiosqlite/sqlc.yaml
  • test/driver_asyncpg/sqlc.yaml
  • test/driver_sqlite3/dataclass/test_sqlite3_dataclass_functions.py
  • test/driver_sqlite3/queries_slice.sql
  • test/driver_sqlite3/sqlc.yaml

📝 Walkthrough

Walkthrough

This change carries sqlc.slice metadata through generation, expands SQLite slice placeholders at call time, flattens slice arguments, and adds synchronous/asynchronous integration coverage for CRUD, filtering, empty slices, and converted parameters.

Changes

SQLite sqlc.slice support

Layer / File(s) Summary
Slice metadata and parameter flattening
internal/model/types.go, internal/transform/..., internal/driver/common.go, internal/*_test.go
PyType preserves slice names; shared helpers flatten direct and bundled slice parameters, including converted elements and repeated markers.
Runtime SQLite query generation
internal/driver/sqlite_base.go, internal/driver/sqlite_test.go
Generated functions replace slice markers per call, use NULL for empty sequences, and preserve SQL argument order.
SQLite integration coverage
test/driver_aiosqlite/..., test/driver_sqlite3/..., test/conftest.py
Schemas, queries, configurations, cleanup, and dataclass tests cover slice-based inserts, reads, filters, and deletes.
Release and generator metadata
.changes/unreleased/..., sqlc.yaml, test/driver_*/sqlc.yaml
Adds the unreleased fix entry and updates plugin integrity hashes and query-source configuration.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant GeneratedSQLiteFunction
  participant SQLiteConnection
  Caller->>GeneratedSQLiteFunction: pass slice parameters
  GeneratedSQLiteFunction->>GeneratedSQLiteFunction: replace slice marker and flatten arguments
  GeneratedSQLiteFunction->>SQLiteConnection: execute expanded SQL
  SQLiteConnection-->>GeneratedSQLiteFunction: return query result
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly states the main change: runtime expansion of sqlc.slice placeholders for SQLite drivers.
Linked Issues check ✅ Passed The generator and tests now expand sqlc.slice at call time for sqlite3/aiosqlite, flatten arguments, preserve order, and handle empty slices.
Out of Scope Changes check ✅ Passed The visible changes all support slice expansion, test coverage, or related docs/config updates; no unrelated edits stand out.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

The converters PR dropped both the query file and its TAGTYPE override from the dataclass/functions block while the generated module and its runtime tests stayed, so sqlc generate no longer regenerated it and sqlc diff no longer guarded it. Restore the wiring and the override; the regenerated output is byte-identical to the committed module.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/transform/type.go (1)

1-1: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject nullable sqlc.slice params in internal/transform/type.go. buildPyType still carries IsNullable through for slice params, which lets generated SQLite code star-unpack or call len() on None and crash at runtime. Force IsNullable: false when GetIsSqlcSlice() is true.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/transform/type.go` at line 1, Update buildPyType to override
IsNullable as false whenever the parameter’s GetIsSqlcSlice() is true,
preventing nullable slice parameters from reaching generated SQLite code.
Preserve the existing nullability behavior for non-slice parameters.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/driver/sqlite_base.go`:
- Around line 267-283: Update writeSliceExpansion to handle nullable slice
parameters before evaluating len(param.expr): either reject nullable slice
params during validation in sliceParams or guard the generated expansion so None
does not reach len(). Preserve normal expansion for non-null slice values and
ensure nullable parameters follow the intended NULL behavior.

---

Outside diff comments:
In `@internal/transform/type.go`:
- Line 1: Update buildPyType to override IsNullable as false whenever the
parameter’s GetIsSqlcSlice() is true, preventing nullable slice parameters from
reaching generated SQLite code. Preserve the existing nullability behavior for
non-slice parameters.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f3f1f710-af38-40da-975c-785f6ad274d6

📥 Commits

Reviewing files that changed from the base of the PR and between a3c7969 and 0020c2e.

⛔ Files ignored due to path filters (8)
  • test/driver_aiosqlite/dataclass/functions/models.py is excluded by !test/driver_*/*/functions/**
  • test/driver_aiosqlite/dataclass/functions/queries_slice.py is excluded by !test/driver_*/*/functions/**
  • test/driver_aiosqlite/sqlc-gen-better-python.wasm is excluded by !**/*.wasm, !**/*.wasm
  • test/driver_asyncpg/sqlc-gen-better-python.wasm is excluded by !**/*.wasm, !**/*.wasm
  • test/driver_sqlite3/attrs/classes/models.py is excluded by !test/driver_*/*/classes/**
  • test/driver_sqlite3/dataclass/functions/models.py is excluded by !test/driver_*/*/functions/**
  • test/driver_sqlite3/dataclass/functions/queries_slice.py is excluded by !test/driver_*/*/functions/**
  • test/driver_sqlite3/sqlc-gen-better-python.wasm is excluded by !**/*.wasm, !**/*.wasm
📒 Files selected for processing (19)
  • .changes/unreleased/Fixed-20260721-210000.yaml
  • internal/driver/common.go
  • internal/driver/common_test.go
  • internal/driver/sqlite_base.go
  • internal/driver/sqlite_test.go
  • internal/model/types.go
  • internal/transform/type.go
  • internal/transform/type_test.go
  • sqlc.yaml
  • test/conftest.py
  • test/driver_aiosqlite/dataclass/test_aiosqlite_dataclass_functions.py
  • test/driver_aiosqlite/queries_slice.sql
  • test/driver_aiosqlite/schema.sql
  • test/driver_aiosqlite/sqlc.yaml
  • test/driver_asyncpg/sqlc.yaml
  • test/driver_sqlite3/dataclass/test_sqlite3_dataclass_functions.py
  • test/driver_sqlite3/queries_slice.sql
  • test/driver_sqlite3/schema.sql
  • test/driver_sqlite3/sqlc.yaml

Comment thread internal/driver/sqlite_base.go
rayakame added 4 commits July 21, 2026 21:05
A sqlc.slice parameter against a nullable column arrives with notNull
false, which would have annotated it Sequence[T] | None and crashed the
generated len() call, failing pyright strict on the way. Force such
parameters non-nullable in the type layer: the expansion consumes the
sequence itself, "no values" is an empty sequence, and IN (NULL) never
matches NULL rows anyway, matching the plain slice parameters of sqlc's
own Go codegen.

Cover it end to end with a nullable note column on the slice fixture
table and a query slicing on it, for both sqlite drivers. Drop test_slice
in the session cleanup instead of deleting from it, so existing local db
files created before the column was added heal themselves.
count(*) always returns a row, so the generated None branch of the two-slice scalar query could never execute and capped patch coverage below 100%. Query the first matching name instead: same two-slice scalar shape, but an empty result is real, and the test asserts both the hit and the miss.
Two review findings on the slice expansion.

A parameter named sql was silently overwritten: the expansion assigns the
expanded query to a local named sql before any parameter is read, so the
query text itself got bound as the parameter value, type-correct and
invisible to pyright. Seed "sql" into the parameter dedup map for queries
with a slice parameter, exactly like the implicit conn/self argument, so
such a parameter becomes sql_2.

Reusing one slice name twice in a query failed on every call: sqlc merges
same-named sqlc.slice uses into one parameter but keeps a marker per use
site, and the count-1 replace left the second marker as a live unbound
placeholder. Replace all occurrences when a marker appears more than once
and bind one starred copy of the sequence per occurrence, which makes
such queries work; sqlc's own Go codegen still generates broken code for
them. Covered by a fixture query matching on name or note from the same
list, on both sqlite drivers.

Also name the remaining sqlite3 slice tests so the delete depends on
every reader, mirroring the aiosqlite file, and correct the cleanup
comment now that test_slice exists in both driver schemas.
@rayakame

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/driver/common.go`:
- Around line 68-72: Update the argument-flattening logic around
sliceMarkerCount so reused slice arguments are expanded at each SQL marker
occurrence rather than emitted consecutively; preserve the original
placeholder-occurrence order when scalar placeholders appear between slice
markers. Add a regression case covering an interleaved scalar and reused slice,
verifying the flattened arguments align with the expanded SQL placeholders.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 21eb50d1-6e9f-4b81-812e-963695b4b0a4

📥 Commits

Reviewing files that changed from the base of the PR and between 0020c2e and 137c030.

⛔ Files ignored due to path filters (8)
  • test/driver_aiosqlite/dataclass/functions/models.py is excluded by !test/driver_*/*/functions/**
  • test/driver_aiosqlite/dataclass/functions/queries_slice.py is excluded by !test/driver_*/*/functions/**
  • test/driver_aiosqlite/sqlc-gen-better-python.wasm is excluded by !**/*.wasm, !**/*.wasm
  • test/driver_asyncpg/sqlc-gen-better-python.wasm is excluded by !**/*.wasm, !**/*.wasm
  • test/driver_sqlite3/attrs/classes/models.py is excluded by !test/driver_*/*/classes/**
  • test/driver_sqlite3/dataclass/functions/models.py is excluded by !test/driver_*/*/functions/**
  • test/driver_sqlite3/dataclass/functions/queries_slice.py is excluded by !test/driver_*/*/functions/**
  • test/driver_sqlite3/sqlc-gen-better-python.wasm is excluded by !**/*.wasm, !**/*.wasm
📒 Files selected for processing (19)
  • internal/driver/common.go
  • internal/driver/common_test.go
  • internal/driver/sqlite_base.go
  • internal/driver/sqlite_test.go
  • internal/transform/queries.go
  • internal/transform/queries_test.go
  • internal/transform/type.go
  • internal/transform/type_test.go
  • sqlc.yaml
  • test/conftest.py
  • test/driver_aiosqlite/dataclass/test_aiosqlite_dataclass_functions.py
  • test/driver_aiosqlite/queries_slice.sql
  • test/driver_aiosqlite/schema.sql
  • test/driver_aiosqlite/sqlc.yaml
  • test/driver_asyncpg/sqlc.yaml
  • test/driver_sqlite3/dataclass/test_sqlite3_dataclass_functions.py
  • test/driver_sqlite3/queries_slice.sql
  • test/driver_sqlite3/schema.sql
  • test/driver_sqlite3/sqlc.yaml

Comment thread internal/driver/common.go Outdated
The consecutive starred copies broke as soon as another placeholder sat
between the two use sites of a reused slice: sqlc orders the parameter
array by first use, so the plain value was bound into the second IN list,
silently returning wrong rows with a matching argument count.

Order the flattened arguments by the SQL text instead. A small scanner
walks the query for markers and plain placeholders, skipping string
literals with doubled-quote escapes, quoted identifiers, and both comment
styles, so a question mark inside a literal never counts. Plain
expressions fill the "?" slots in parameter order and every marker
occurrence gets its slice's starred copy; when the SQL cannot account for
exactly the given arguments, emission falls back to the previous
consecutive form. Covered by a fixture query with a plain parameter
between the reuse sites, on both sqlite drivers.
@rayakame

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

The placeholder expansion ships with this branch, so the feature support page now explains how sqlc.slice works on the sqlite drivers, that it cannot be used with prepared statements, and that PostgreSQL uses ANY instead.
@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-07-21 21:10 UTC

@rayakame

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@rayakame
rayakame merged commit 0fae43b into main Jul 21, 2026
20 checks passed
@rayakame
rayakame deleted the fix/sqlc-slice-expansion branch July 21, 2026 21:08
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.

sqlc.slice is not expanded, so queries using it fail at runtime

1 participant