feat: Stricter, more expressive type generation (enums, overrides, types-only) + correctness fixes - #73
Open
rtolsma wants to merge 7 commits into
Open
feat: Stricter, more expressive type generation (enums, overrides, types-only) + correctness fixes#73rtolsma wants to merge 7 commits into
rtolsma wants to merge 7 commits into
Conversation
Generate stricter, more expressive TypeScript types in two ways:
1. Enum types declared in the schema (previously ignored — the plugin
never read the request catalog) are now emitted as string literal
union types plus a runtime values array, and columns/params of that
type reference the union:
export type Status = "active" | "inactive" | "banned";
export const StatusValues: readonly Status[] = [...];
2. A new `overrides` plugin option (modeled on sqlc-gen-go's overrides)
maps SQL types — including domains and other custom types — or
individual `table.column`s to arbitrary TypeScript type expressions,
with optional type-only imports. This enables branded/nominal types
and concrete JSON payload types:
overrides:
- db_type: "email"
ts_type: "Email"
import: { path: "./brands", name: "Email" }
- db_type: "user_id"
ts_type: 'string & { readonly __brand: "user_id" }'
- column: "users.meta"
ts_type: "UserMeta"
import: { path: "./brands", name: "UserMeta" }
Resolution order is column override > db_type override > schema enum >
built-in driver mapping. Driver nullability (`| null`) and array (`T[]`)
wrapping still compose on top of resolved custom types. Emitted enum
declarations and imports are deduplicated per generated file and only
included when actually used by that file's queries.
Implemented as a CustomTypeRegistry (src/customTypes.ts) built from the
GenerateRequest catalog and plugin options; each driver accepts the
resolver and consults it before its built-in switch, so behavior is
identical across pg, postgres, mysql2, and better-sqlite3.
Output for schemas without enums or overrides is byte-for-byte
unchanged (verified against all seven example configs), and generated
code for the new features typechecks under `tsc --strict`.
Closes sqlc-dev#32. Addresses sqlc-dev#37.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GFyrFwpJC2eivaBRyNBJqv
Driver preambles imported types (QueryArrayConfig/QueryArrayResult from pg, Sql from postgres, Database from better-sqlite3, mysql/RowDataPacket/ ResultSetHeader from mysql2) as value imports, which fails to compile under `verbatimModuleSyntax` and forces the runtime driver package to be a hard dependency of the generated module. All preamble imports are now `import type`. The mysql2 default+named import is split into two declarations because a type-only import cannot combine a default import with named bindings. Verified the generated output for all drivers compiles under `tsc --strict` with `verbatimModuleSyntax: true`. Fixes sqlc-dev#55. Fixes sqlc-dev#61. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GFyrFwpJC2eivaBRyNBJqv
Columns like "invalid-name" produced interface properties, object literal keys, and args property accesses that were not valid TypeScript, so the generated file failed to parse. Property names are now emitted as quoted string keys and argument access falls back to element access (args["invalid-name"]) whenever the mapped name is not a valid identifier, via new propertyKey/ propertyAccess helpers shared by all four drivers. Verified with a schema containing a hyphenated column used in both a SELECT row and an INSERT parameter; output compiles under tsc --strict. Output for schemas with well-formed names is unchanged (all examples regenerate byte-for-byte identical). Fixes sqlc-dev#70. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GFyrFwpJC2eivaBRyNBJqv
Query text is emitted inside a template literal, so MySQL
backtick-quoted identifiers (and any backslash or "${" sequence in a
SQL string literal) terminated or corrupted the literal and produced
unparseable or wrong generated code.
The raw template text now escapes backslashes, backticks, and "${"
while the runtime string value stays byte-identical to the original
SQL. Verified by evaluating the emitted literal for a MySQL query using
backtick quoting and a literal containing "${" and backslashes, and
comparing against the source SQL. Existing example output is unchanged.
Fixes sqlc-dev#40.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GFyrFwpJC2eivaBRyNBJqv
With `types_only: true`, output contains only the Args/Row interfaces, enum literal union types, and override imports — no driver imports, query constants, or query functions. This lets a frontend share the database types generated for a backend without pulling in a database driver dependency. Verified the types-only output compiles standalone under tsc --strict with verbatimModuleSyntax, including enum unions and override imports. Default output is unchanged. Fixes sqlc-dev#71. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GFyrFwpJC2eivaBRyNBJqv
The bigint column mapping already honored support_big_numbers / big_number_strings, but :execlastid always returned Promise<number>, so AUTO_INCREMENT bigint keys silently lied about their type. The insert id now follows the same rules as bigint columns: big_number_strings returns string (converted with String(), since the driver returns big values as strings while the mysql2 typings declare insertId as number), support_big_numbers alone returns number | string, and the default stays number. Verified against the node-mysql2 example (both options enabled), which now returns Promise<string>, and the bun-mysql2 example (no options), which is unchanged; both outputs compile with the examples' tsconfig. Fixes sqlc-dev#22. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GFyrFwpJC2eivaBRyNBJqv
Parameters with no column name — e.g. the cast form
`WHERE uid = ANY($1::uuid[])` — generated an empty property name,
producing an unusable interface member and args[""] access.
Unnamed parameters now fall back to arg_N, numbered to match their
positional placeholder ($1 -> arg_1). Array parameter types themselves
already worked (string[] etc.); this makes them reachable.
Verified with a pg query using = ANY($1::uuid[]): args are now
{ arg_1: string[] } and the output compiles under tsc --strict.
Example outputs are unchanged.
Fixes sqlc-dev#29.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GFyrFwpJC2eivaBRyNBJqv
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
A series of atomic commits making generated types substantially stricter and more expressive, plus fixes for several open correctness issues. Each commit is independently revertable and states its own verification.
1. Strongly typed columns (
cd4421a) — closes #32, addresses #37Enums → string literal unions (automatic). The plugin previously ignored
GenerateRequest.catalog, soCREATE TYPE status AS ENUM (...)columns came out as plainstring. They now generate:and every column/parameter of that type references the union.
overridesplugin option (opt-in), modeled on sqlc-gen-go's. Match a SQL type by name (db_type— covers Postgres domains, which previously silently collapsed tostring) or a single column (column: "table.column"), and emit any TypeScript type expression, with an optional type-only import — enabling branded/nominal ID types and concrete JSON payload types:Resolution order: column override →
db_typeoverride → schema enum → built-in driver mapping. Nullability (| null) and array (T[]) wrapping compose on top. Implemented as aCustomTypeRegistry(src/customTypes.ts); each driver consults the resolver before its built-in switch, so behavior is identical across all four drivers.2. Type-only imports (
903881e) — fixes #55, fixes #61All driver preamble imports are now
import type(the mysql2 default+named import is split in two, since type-only imports cannot combine them). Generated output now compiles underverbatimModuleSyntax.3. Valid code for non-identifier column names (
74e1a5e) — fixes #70Columns like
"invalid-name"now emit quoted property keys andargs["invalid-name"]element access instead of unparseable code.4. Escape backticks/
${/backslashes in query literals (6d4de97) — fixes #40MySQL backtick-quoted queries (and SQL literals containing
${or backslashes) no longer corrupt the emitted template literal; the runtime string is byte-identical to the source SQL (verified by evaluating the emitted literal).5.
types_onlyoption (9198dca) — fixes #71types_only: trueemits only Args/Row interfaces, enum unions, and override imports — no driver imports or query functions — for sharing DB types with a frontend.6.
:execlastidrespects big number options (e71d7f0) — fixes #22The mysql2 insert id now follows the same rules as the bigint column mapping:
big_number_strings→string(viaString(...)),support_big_numbersalone →number | string, default unchanged.7. Positional fallback names for unnamed parameters (
77e343e) — fixes #29WHERE uid = ANY($1::uuid[])previously generated{ "": string[] }; unnamed parameters now fall back toarg_Nmatching their placeholder number. (Array parameter types themselves already worked; this makes them usable.)Testing
tsc --noEmitclean at every commit; wasm built via the documented esbuild + javy flow (javy 1.4.0, sqlc v1.31.1).Promise<string>), and those regenerated files are included in the respective commits. Thenode-pgandnode-mysql2example projects compile with their own tsconfigs.ANY($1::uuid[])parameter; MySQL schema with backtick quoting,${/backslash literals, and:execlastidunder both big-number option combinations. All generated output compiles undertsc --strict, and the pg output additionally underverbatimModuleSyntax.The README gains sections documenting enum generation,
overrides, andtypes_only.🤖 Generated with Claude Code
https://claude.ai/code/session_01GFyrFwpJC2eivaBRyNBJqv