Skip to content

feat: sqlc v1.31.1 feature parity - #1

Merged
kvdomingo merged 31 commits into
developmentfrom
feat/sqlc-feature-parity
Oct 4, 2026
Merged

kvdomingo merged 31 commits into
developmentfrom
feat/sqlc-feature-parity

Conversation

@kvdomingo

@kvdomingo kvdomingo commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

Brings the plugin up to parity with sqlc v1.31.1 (OpenSpec change sqlc-feature-parity). PostgreSQL only; every new behaviour is opt-in except the correctness fixes and annotation-only typing changes.

New options

  • overrides (db_type/column with * globs, dotted or generic py_type or py_type + py_import, nullable), rename, both also read from sqlc's top-level options
  • omit_unused_structs, omit_sqlc_version
  • emit_modern_types (X | None, list[X], collections.abc)
  • emit_aware_datetime (pydantic.AwareDatetime for timestamptz; requires emit_pydantic_models)
  • emit_generic_querier (PEP 695 class Querier[_ConnT: Connection | Session])
  • emit_querier_protocol (QuerierProtocol / AsyncQuerierProtocol)
  • emit_query_errors (generated errors.py, same classes as alt-sqlc-gen-python plus constraint_name)

Query features

  • :copyfrom, :batchexec (single executemany), :batchone, :batchmany (lazy generators)
  • sqlc.embed(), query comments as method docstrings, multi-dimensional arrays
  • Queriers accept Session / AsyncSession; row values wrapped in typing.cast so output passes mypy --strict

Fixes

  • Keyword escaping (from → from_), pass for empty classes, valid unique enum member names, escaped string literals, multi-line comments, missing PostgreSQL type aliases (varchar, timestamp, …)
  • Keyword parameters that would shadow a module-level name the method uses (errors, models, cast, imported modules) get a trailing _; repeated parameter names get a numeric suffix (id, id_2)
  • db_type overrides accept SQL spellings (bigint, timestamp with time zone)
  • Parameters named after builtins or py_type names used in the body (list, int, dict, …) are escaped too; dedup suffixes skip names already taken (id, id_2, id_2_2)
  • Method names that would shadow a type in later class-scope annotations (list, int, models, …) get a trailing _
  • Generation fails instead of emitting broken code when two queries in one file map to the same method or constant name (after Python's NFKC identifier normalisation), or when a query file would overwrite models.py/errors.py

Notable decisions

  • Imports are recorded while each file is built instead of predicted by the importer; existing fixtures are byte-identical.
  • Codegen-block overrides are checked before global ones, but a global rename replaces a codegen one (as in sqlc's Go codegen). Both documented in the README.
  • rename also applies to enum values (one flat map); documented.
  • examples/ builds against the local WASM so sqlc diff in CI tests this tree.
  • rowcount and :execresult use string annotations so SQLAlchemy 1.4 still imports the code.

Also in this PR

  • Go 1.27 (go.mod, mise.toml; CI reads go.mod)
  • Release workflow: on merge to main, the PR title picks the bump (feat → minor, fix/hotfix → patch, !/BREAKING CHANGE → major); first release is v1.0.0. Publishes alt-sqlc-gen-python.wasm + .sha256.
  • zizmor and pre-commit (prek) hooks; README rewritten for the fork

Testing

  • make test, go vet ./..., sqlc diff in examples/
  • New e2e fixtures for every feature; two feature-matrix fixtures type-checked with mypy --strict in CI (Python 3.14)
  • examples/ pytest against Postgres 18 (psycopg 3 + asyncpg)

🤖 Generated with Claude Code

kvdomingo and others added 17 commits September 25, 2026 00:18
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Example deps move to SQLAlchemy 2, psycopg 3, pydantic 2 and mypy for
the upcoming fixture checks.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Examples now build against the local plugin (file://../bin) so that
sqlc diff checks this tree rather than the released 1.2.0 WASM.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…params

Needed for executemany param lists, error wrapping, X | None
annotations, Union[...] slices, protocol stubs and PEP 695 queriers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
String constants escape backslashes and quotes, so SQL text is now
emitted as runtime text and the printer doubles its backslashes. Class
and function docstrings share one triple-quoted helper (multi-line
docstrings used to print five quotes), empty class bodies print pass,
multi-line field comments stay inside comments, and If prints else.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Field, parameter, method and class names go through one pyIdent helper
(rename, then a trailing underscore for hard keywords). Enum members
that sanitize to an empty, digit-leading or duplicate name get
VALUE_<n>, a VALUE_ prefix, or a _<k> suffix.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…uilder

Each generated file now records the imports its nodes use (typing and
collections.abc names, dotted type modules, py_import names), replacing
the importer's parallel prediction. Sync and async querier methods come
from one builder with an async flag. pyType carries ArrayDims for nested
List wrappers, and the PostgreSQL map gains the varchar/bpchar/name/time/
timestamp spellings sqlc's Go codegen knows. Existing fixtures are
unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ersion

overrides map a db_type or column (with * globs) to a py_type, given as
a dotted path or as py_type plus py_import. Overrides and rename are
also read from sqlc's global options for the plugin; codegen entries
are checked first, while global rename values win. Invalid entries fail
generation and name the entry.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Querier and AsyncQuerier take Connection | Session and AsyncConnection |
AsyncSession and declare a typed _conn. Row values are wrapped in
typing.cast so mypy --strict accepts the generated code. rowcount reads
and the :execresult return use string annotations, so no subscripted
SQLAlchemy type is evaluated at runtime. Annotation-only: behaviour is
unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
emit_generic_querier makes Querier/AsyncQuerier PEP 695 generics bounded
by the connection-or-session union, so Querier(session)._conn is typed
Session. Query comments become method docstrings. sqlc.embed() fields
are built as nested models from their run of row columns.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
All four take one positional Sequence of params objects. :copyfrom and
:batchexec run a single executemany (returning rowcount and None);
:batchone and :batchmany are generators that run the statement per item
and yield lazily. Generation used to fail on :copyfrom and panic on the
batch commands.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Behind emit_querier_protocol. Protocols come from the same method list
as the queriers; async generator methods are declared with plain def so
the implementation matches structurally.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Generates errors.py with alt-sqlc-gen-python's QueryError hierarchy plus
constraint_name, and wraps each querier method body in
errors._wrap_errors(), which re-raises IntegrityError/OperationalError
as the class for the SQLSTATE. The with block encloses generator loops,
so errors while iterating are wrapped too. A query file mapping to
errors.py fails generation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Two kitchen-sink fixtures combine every new feature with the classic and
the modern option sets. The db job moves to Python 3.12 (PEP 695
fixtures), compiles every fixture, and type-checks both matrices.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A library example is generated with the classic and the modern option
sets. pytest covers lazy batch results, single executemany calls, typed
errors (also while iterating) through psycopg and asyncpg, Session and
generic queriers, a protocol fake, and aware datetimes. The harness
moves to SQLAlchemy 2, psycopg 3 and pytest-asyncio 1.x.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
README gains sections for every new option and command plus a note for
alt-sqlc-gen-python users; design.md records where the implementation
differs from the original plan.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@kvdomingo kvdomingo self-assigned this Sep 24, 2026
@kvdomingo
kvdomingo marked this pull request as ready for review October 4, 2026 07:19
kvdomingo and others added 4 commits October 4, 2026 15:22
go.mod moves from go 1.19 to 1.27 and mise pins 1.27.1; CI reads the
version from go.mod so the two cannot drift. Fixture sha256s follow the
rebuilt WASM.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@kvdomingo
kvdomingo force-pushed the feat/sqlc-feature-parity branch from 90e3ba9 to 14313ce Compare October 4, 2026 07:46
kvdomingo and others added 3 commits October 4, 2026 15:50
The README described the original fork's always-on modern output,
try/except error wrapping and a keyword-only :batchexec. Document the
opt-in options as implemented, the batch commands, the published
alt-sqlc-gen-python.wasm asset and the PR-title-driven release workflow.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The fork drops upstream's tags. With none present, the first releasable
merge is tagged v1.0.0 instead of counting up from v0.0.0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@kvdomingo
kvdomingo force-pushed the feat/sqlc-feature-parity branch from f6977d1 to 7dba9d6 Compare October 4, 2026 07:56
…edence and embed nulls

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
kvdomingo and others added 5 commits October 4, 2026 16:09
…ry py_type module, accept db_type aliases

A keyword parameter named errors, models, cast or an imported module
shadowed the module-level name the method body reads. Repeated parameter
names produced a SyntaxError. A generic py_type such as
dict[str, decimal.Decimal] was cut at its last dot into an invalid import.
db_type: bigint never matched because sqlc reports pg_catalog.int8.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Params named after a builtin or py_type name used inside cast() (list,
int, str, dict, ...) shadowed it in the method body; escape them like
errors/cast. Dedup suffixes for params and struct fields now skip names
already taken, so id, id, id_2 no longer yields two id_2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iles

Method names are now escaped against the same shadowed-name set as params,
so a query named List/Int/Models no longer hides those types in later
annotations of the class body. A query file named models.sql now errors
instead of silently overwriting models.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…meter

Two queries that map to the same Python method (e.g. List and List_) now
fail generation instead of emitting duplicate defs and constants.

The generic querier type parameter is now _ConnT, so it no longer shadows
the SQL constant of a query named T.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ant and NFKC collisions

Each query file is its own module, so same-named methods across files are
valid again. Also reject duplicate constants and methods that collide after
Python's NFKC identifier normalisation. Update docs to the _ConnT type
parameter name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kvdomingo added a commit that referenced this pull request Oct 4, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kvdomingo
kvdomingo force-pushed the feat/sqlc-feature-parity branch from c4f8768 to 4ca45c8 Compare October 4, 2026 09:01
@kvdomingo
kvdomingo merged commit 9fc9d2d into development Oct 4, 2026
4 checks passed
@kvdomingo
kvdomingo deleted the feat/sqlc-feature-parity branch October 4, 2026 09:05
kvdomingo added a commit that referenced this pull request Oct 4, 2026
* chore: mise & openspec tooling setup

* feat: sqlc v1.31.1 feature parity (#1)

* docs(openspec): propose sqlc-feature-parity change

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore(tooling): move CI to sqlc 1.31.1 and examples to SQLAlchemy 2

Example deps move to SQLAlchemy 2, psycopg 3, pydantic 2 and mypy for
the upcoming fixture checks.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore(testdata): regenerate fixtures and examples with sqlc 1.31.1

Examples now build against the local plugin (file://../bin) so that
sqlc diff checks this tree rather than the released 1.2.0 WASM.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(ast): add ListComp, With, BinOp, Tuple, ellipsis and class type params

Needed for executemany param lists, error wrapping, X | None
annotations, Union[...] slices, protocol stubs and PEP 695 queriers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(printer): escape string literals, print docstrings, pass and else

String constants escape backslashes and quotes, so SQL text is now
emitted as runtime text and the printer doubles its backslashes. Class
and function docstrings share one triple-quoted helper (multi-line
docstrings used to print five quotes), empty class bodies print pass,
multi-line field comments stay inside comments, and If prints else.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(codegen): escape Python keywords and sanitize enum member names

Field, parameter, method and class names go through one pyIdent helper
(rename, then a trailing underscore for hard keywords). Enum members
that sanitize to an empty, digit-leading or duplicate name get
VALUE_<n>, a VALUE_ prefix, or a _<k> suffix.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor(codegen): record imports while building, share one querier builder

Each generated file now records the imports its nodes use (typing and
collections.abc names, dotted type modules, py_import names), replacing
the importer's parallel prediction. Sync and async querier methods come
from one builder with an async flag. pyType carries ArrayDims for nested
List wrappers, and the PostgreSQL map gains the varchar/bpchar/name/time/
timestamp spellings sqlc's Go codegen knows. Existing fixtures are
unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(testdata): cover emit_modern_types and emit_aware_datetime

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(config): add overrides, rename, omit_unused_structs, omit_sqlc_version

overrides map a db_type or column (with * globs) to a py_type, given as
a dotted path or as py_type plus py_import. Overrides and rename are
also read from sqlc's global options for the plugin; codegen entries
are checked first, while global rename values win. Invalid entries fail
generation and name the entry.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(querier): accept sessions and cast row values

Querier and AsyncQuerier take Connection | Session and AsyncConnection |
AsyncSession and declare a typed _conn. Row values are wrapped in
typing.cast so mypy --strict accepts the generated code. rowcount reads
and the :execresult return use string annotations, so no subscripted
SQLAlchemy type is evaluated at runtime. Annotation-only: behaviour is
unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(querier): generic queriers, query docstrings and sqlc.embed()

emit_generic_querier makes Querier/AsyncQuerier PEP 695 generics bounded
by the connection-or-session union, so Querier(session)._conn is typed
Session. Query comments become method docstrings. sqlc.embed() fields
are built as nested models from their run of row columns.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(querier): support :copyfrom, :batchexec, :batchone and :batchmany

All four take one positional Sequence of params objects. :copyfrom and
:batchexec run a single executemany (returning rowcount and None);
:batchone and :batchmany are generators that run the statement per item
and yield lazily. Generation used to fail on :copyfrom and panic on the
batch commands.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(querier): emit QuerierProtocol and AsyncQuerierProtocol

Behind emit_querier_protocol. Protocols come from the same method list
as the queriers; async generator methods are declared with plain def so
the implementation matches structurally.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(querier): typed query errors behind emit_query_errors

Generates errors.py with alt-sqlc-gen-python's QueryError hierarchy plus
constraint_name, and wraps each querier method body in
errors._wrap_errors(), which re-raises IntegrityError/OperationalError
as the class for the SQLSTATE. The with block encloses generator loops,
so errors while iterating are wrapped too. A query file mapping to
errors.py fails generation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(ci): feature matrix fixtures, compile and mypy --strict in CI

Two kitchen-sink fixtures combine every new feature with the classic and
the modern option sets. The db job moves to Python 3.12 (PEP 695
fixtures), compiles every fixture, and type-checks both matrices.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(examples): exercise new features against Postgres

A library example is generated with the classic and the modern option
sets. pytest covers lazy batch results, single executemany calls, typed
errors (also while iterating) through psycopg and asyncpg, Session and
generic queriers, a protocol fake, and aware datetimes. The harness
moves to SQLAlchemy 2, psycopg 3 and pytest-asyncio 1.x.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: document new options, commands and fork migration

README gains sections for every new option and command plus a note for
alt-sqlc-gen-python users; design.md records where the implementation
differs from the original plan.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ci: update postgres

* ci: install sqlalchemy asyncio extras

* chore(go): bump to Go 1.27

go.mod moves from go 1.19 to 1.27 and mise pins 1.27.1; CI reads the
version from go.mod so the two cannot drift. Fixture sha256s follow the
rebuilt WASM.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ci: add zizmor, pre-commit hooks

* ci: autogenerate release version

* docs(readme): match the implementation and the automated release

The README described the original fork's always-on modern output,
try/except error wrapping and a keyword-only :batchexec. Document the
opt-in options as implemented, the batch commands, the published
alt-sqlc-gen-python.wasm asset and the PR-title-driven release workflow.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ci(release): start versioning at v1.0.0

The fork drops upstream's tags. With none present, the first releasable
merge is tagged v1.0.0 instead of counting up from v0.0.0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: update readme

* docs(readme): document override aliases, generic py_type, rename precedence and embed nulls

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(codegen): escape shadowing params, dedupe param names, import every py_type module, accept db_type aliases

A keyword parameter named errors, models, cast or an imported module
shadowed the module-level name the method body reads. Repeated parameter
names produced a SyntaxError. A generic py_type such as
dict[str, decimal.Decimal] was cut at its last dot into an invalid import.
db_type: bigint never matched because sqlc reports pg_catalog.int8.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(codegen): escape builtin-named params, keep deduped names unique

Params named after a builtin or py_type name used inside cast() (list,
int, str, dict, ...) shadowed it in the method body; escape them like
errors/cast. Dedup suffixes for params and struct fields now skip names
already taken, so id, id, id_2 no longer yields two id_2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(codegen): escape builtin-named methods, reject models.sql query files

Method names are now escaped against the same shadowed-name set as params,
so a query named List/Int/Models no longer hides those types in later
annotations of the class body. A query file named models.sql now errors
instead of silently overwriting models.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(codegen): reject duplicate method names, rename generic type parameter

Two queries that map to the same Python method (e.g. List and List_) now
fail generation instead of emitting duplicate defs and constants.

The generic querier type parameter is now _ConnT, so it no longer shadows
the SQL constant of a query named T.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(codegen): scope duplicate-name checks per query file, catch constant and NFKC collisions

Each query file is its own module, so same-named methods across files are
valid again. Also reject duplicate constants and methods that collide after
Python's NFKC identifier normalisation. Update docs to the _ConnT type
parameter name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.

1 participant