From cd4421a0df687df336725279a1563d588713b844 Mon Sep 17 00:00:00 2001 From: Ryan Tolsma <1tolsmar@gmail.com> Date: Sat, 22 Aug 2026 01:44:53 -0400 Subject: [PATCH 1/7] feat: Strongly typed columns via enum literal unions and type overrides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 #32. Addresses #37. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GFyrFwpJC2eivaBRyNBJqv --- README.md | 80 +++++++++++ src/app.ts | 30 +++- src/customTypes.ts | 248 ++++++++++++++++++++++++++++++++++ src/drivers/better-sqlite3.ts | 12 +- src/drivers/mysql2.ts | 10 +- src/drivers/pg.ts | 12 +- src/drivers/postgres.ts | 12 +- 7 files changed, 393 insertions(+), 11 deletions(-) create mode 100644 src/customTypes.ts diff --git a/README.md b/README.md index 5798b73..3ec1a6c 100644 --- a/README.md +++ b/README.md @@ -334,6 +334,86 @@ sql: driver: better-sqlite3 # npm package name ``` +## Strongly typed columns + +### Enums + +Enum types declared in your schema are generated as string literal union +types, along with a runtime array of their values. No configuration is +required. + +```sql +CREATE TYPE status AS ENUM ('active', 'inactive', 'banned'); + +CREATE TABLE users ( + id BIGSERIAL PRIMARY KEY, + st status NOT NULL +); +``` + +```ts +export type Status = "active" | "inactive" | "banned"; + +export const StatusValues: readonly Status[] = ["active", "inactive", "banned"]; + +export interface GetUserRow { + id: string; + st: Status; +} +``` + +### Type overrides + +Use the `overrides` option to map SQL types (including domains and other +custom types) or individual columns to your own TypeScript types. Each entry +matches either a SQL type name (`db_type`) or a single column +(`column: "table.column"`), and emits `ts_type` verbatim. An optional +`import` adds a type-only import to files that use the override. + +```yaml +codegen: +- out: db + plugin: ts + options: + runtime: node + driver: pg + overrides: + # Map a domain (or any SQL type) to an imported type + - db_type: "email" + ts_type: "Email" + import: + path: "./brands" + name: "Email" + # Inline branded/nominal types work too + - db_type: "user_id" + ts_type: 'string & { readonly __brand: "user_id" }' + # Give a JSON column a concrete payload type + - column: "users.meta" + ts_type: "UserMeta" + import: + path: "./brands" + name: "UserMeta" +``` + +```ts +import type { Email, UserMeta } from "./brands"; + +export interface GetUserRow { + id: string & { readonly __brand: "user_id" }; + email: Email; + meta: UserMeta | null; +} +``` + +Column overrides take precedence over `db_type` overrides, which take +precedence over generated enum types and the built-in type mappings. +Nullability and array handling still apply: a nullable overridden column is +emitted as `T | null`, and an array column as `T[]`. + +Note that overrides only change the generated *types* — converting values at +runtime (for example, parsing a `numeric` string into a `Big`) is still up to +your driver configuration. + ## Development If you want to build and test sqlc-gen-typescript locally, follow these steps: diff --git a/src/app.ts b/src/app.ts index e3a077c..d016892 100644 --- a/src/app.ts +++ b/src/app.ts @@ -16,6 +16,7 @@ import { createPrinter, createSourceFile, factory, + isImportDeclaration, } from "typescript"; import { @@ -28,6 +29,11 @@ import { } from "./gen/plugin/codegen_pb"; import { argName, colName } from "./drivers/utlis"; +import { + CustomTypeFn, + CustomTypeRegistry, + TypeOverride, +} from "./customTypes"; import { Driver as Sqlite3Driver } from "./drivers/better-sqlite3"; import { Driver as PgDriver } from "./drivers/pg"; import { Driver as PostgresDriver } from "./drivers/postgres"; @@ -44,6 +50,7 @@ interface Options { runtime?: string; driver?: string; mysql2?: Mysql2Options + overrides?: TypeOverride[]; } interface Driver { @@ -79,19 +86,22 @@ interface Driver { ) => Node; } -function createNodeGenerator(options: Options): Driver { +function createNodeGenerator( + options: Options, + customType: CustomTypeFn +): Driver { switch (options.driver) { case "mysql2": { - return new MysqlDriver(options.mysql2); + return new MysqlDriver(options.mysql2, customType); } case "pg": { - return new PgDriver(); + return new PgDriver(customType); } case "postgres": { - return new PostgresDriver(); + return new PostgresDriver(customType); } case "better-sqlite3": { - return new Sqlite3Driver(); + return new Sqlite3Driver(customType); } } throw new Error(`unknown driver: ${options.driver}`); @@ -106,7 +116,8 @@ function codegen(input: GenerateRequest): GenerateResponse { options = JSON.parse(text) as Options; } - const driver = createNodeGenerator(options); + const registry = new CustomTypeRegistry(input, options.overrides ?? []); + const driver = createNodeGenerator(options, registry.resolver()); // TODO: Verify options, parse them from protobuf honestly @@ -122,6 +133,13 @@ function codegen(input: GenerateRequest): GenerateResponse { for (const [filename, queries] of querymap.entries()) { const nodes = driver.preamble(queries); + // Insert custom type imports and enum declarations after the driver's + // own imports, before any other declarations + let insertAt = 0; + while (insertAt < nodes.length && isImportDeclaration(nodes[insertAt])) { + insertAt++; + } + nodes.splice(insertAt, 0, ...registry.fileDecls(queries)); for (const query of queries) { const colmap = new Map(); diff --git a/src/customTypes.ts b/src/customTypes.ts new file mode 100644 index 0000000..f3cd857 --- /dev/null +++ b/src/customTypes.ts @@ -0,0 +1,248 @@ +import { NodeFlags, SyntaxKind, TypeNode, Node, factory } from "typescript"; + +import { Column, GenerateRequest, Query } from "./gen/plugin/codegen_pb"; + +// A function that maps a column to a custom TypeScript type, or undefined to +// fall back to the driver's built-in type mapping. +export type CustomTypeFn = (column: Column) => TypeNode | undefined; + +export interface TypeImport { + // Module specifier to import from, e.g. "./brands" or "zod" + path: string; + // Named export to import. Imports are always emitted as type-only. + name: string; +} + +export interface TypeOverride { + // Match a SQL type by name, e.g. "uuid", "email" (a domain), or + // "pg_catalog.numeric". Either db_type or column must be set. + db_type?: string; + // Match a single column as "table.column", e.g. "authors.bio" + column?: string; + // TypeScript type expression to emit, e.g. "UserId" or + // `string & { readonly __brand: "email" }` + ts_type: string; + // Optional type-only import to add to files that use this override + import?: TypeImport; +} + +interface EnumEntry { + typeName: string; + valuesName: string; + vals: string[]; +} + +function pascalCase(name: string): string { + return name + .split(/[^a-zA-Z0-9]+/) + .filter((part) => part.length > 0) + .map((part) => part[0].toUpperCase() + part.slice(1)) + .join(""); +} + +function stripCatalog(name: string): string { + const pgCatalog = "pg_catalog."; + return name.startsWith(pgCatalog) ? name.slice(pgCatalog.length) : name; +} + +// Prints an arbitrary type expression verbatim, e.g. `Branded` +function verbatimType(text: string): TypeNode { + return factory.createTypeReferenceNode( + factory.createIdentifier(text), + undefined + ); +} + +// Resolves custom column types from two sources, in priority order: +// +// 1. User-configured overrides (by column, then by SQL type name), which +// cover domains, branded/nominal types, and JSON payload types +// 2. Enum types declared in the schema, emitted as string literal unions +export class CustomTypeRegistry { + private enums: Map = new Map(); + private byColumn: Map = new Map(); + private byDbType: Map = new Map(); + + constructor(input: GenerateRequest, overrides: TypeOverride[]) { + const defaultSchema = input.catalog?.defaultSchema ?? ""; + for (const schema of input.catalog?.schemas ?? []) { + for (const enumType of schema.enums) { + const entry: EnumEntry = { + typeName: pascalCase(enumType.name), + valuesName: `${pascalCase(enumType.name)}Values`, + vals: enumType.vals, + }; + if (schema.name === defaultSchema || schema.name === "") { + this.enums.set(enumType.name, entry); + } + if (schema.name !== "") { + this.enums.set(`${schema.name}.${enumType.name}`, entry); + } + } + } + for (const override of overrides) { + if (!override.ts_type) { + throw new Error(`override is missing ts_type`); + } + if (override.column) { + this.byColumn.set(override.column, override); + } else if (override.db_type) { + this.byDbType.set(override.db_type, override); + } else { + throw new Error( + `override for ts_type "${override.ts_type}" must set db_type or column` + ); + } + } + } + + resolver(): CustomTypeFn { + return (column) => { + const override = this.overrideFor(column); + if (override) { + return verbatimType(override.ts_type); + } + const enumEntry = this.enumFor(column); + if (enumEntry) { + return factory.createTypeReferenceNode( + factory.createIdentifier(enumEntry.typeName), + undefined + ); + } + return undefined; + }; + } + + private overrideFor(column: Column): TypeOverride | undefined { + if (column.table?.name) { + const byColumn = this.byColumn.get( + `${column.table.name}.${column.name}` + ); + if (byColumn) { + return byColumn; + } + } + if (column.type?.name) { + return ( + this.byDbType.get(column.type.name) ?? + this.byDbType.get(stripCatalog(column.type.name)) + ); + } + return undefined; + } + + private enumFor(column: Column): EnumEntry | undefined { + if (!column.type?.name) { + return undefined; + } + return this.enums.get(stripCatalog(column.type.name)); + } + + // Declarations needed at the top of a generated file: type-only imports for + // overrides and literal union declarations for enums, limited to the types + // actually used by the file's queries. + fileDecls(queries: Query[]): Node[] { + const usedEnums: Map = new Map(); + const usedImports: Map> = new Map(); + + const visit = (column?: Column) => { + if (!column) { + return; + } + const override = this.overrideFor(column); + if (override?.import) { + if (!usedImports.has(override.import.path)) { + usedImports.set(override.import.path, new Set()); + } + usedImports.get(override.import.path)?.add(override.import.name); + return; + } + if (override) { + return; + } + const enumEntry = this.enumFor(column); + if (enumEntry) { + usedEnums.set(enumEntry.typeName, enumEntry); + } + }; + + for (const query of queries) { + query.columns.forEach(visit); + query.params.forEach((param) => visit(param.column)); + } + + const nodes: Node[] = []; + for (const path of [...usedImports.keys()].sort()) { + const names = [...(usedImports.get(path) ?? [])].sort(); + nodes.push( + factory.createImportDeclaration( + undefined, + factory.createImportClause( + true, + undefined, + factory.createNamedImports( + names.map((name) => + factory.createImportSpecifier( + false, + undefined, + factory.createIdentifier(name) + ) + ) + ) + ), + factory.createStringLiteral(path), + undefined + ) + ); + } + for (const typeName of [...usedEnums.keys()].sort()) { + const entry = usedEnums.get(typeName); + if (entry) { + nodes.push(...enumDecls(entry)); + } + } + return nodes; + } +} + +// export type Status = "active" | "inactive"; +// export const StatusValues: readonly Status[] = ["active", "inactive"]; +function enumDecls(entry: EnumEntry): Node[] { + return [ + factory.createTypeAliasDeclaration( + [factory.createToken(SyntaxKind.ExportKeyword)], + factory.createIdentifier(entry.typeName), + undefined, + factory.createUnionTypeNode( + entry.vals.map((val) => + factory.createLiteralTypeNode(factory.createStringLiteral(val)) + ) + ) + ), + factory.createVariableStatement( + [factory.createToken(SyntaxKind.ExportKeyword)], + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier(entry.valuesName), + undefined, + factory.createTypeOperatorNode( + SyntaxKind.ReadonlyKeyword, + factory.createArrayTypeNode( + factory.createTypeReferenceNode( + factory.createIdentifier(entry.typeName), + undefined + ) + ) + ), + factory.createArrayLiteralExpression( + entry.vals.map((val) => factory.createStringLiteral(val)), + false + ) + ), + ], + NodeFlags.Const + ) + ), + ]; +} diff --git a/src/drivers/better-sqlite3.ts b/src/drivers/better-sqlite3.ts index c6210e8..6ec24d8 100644 --- a/src/drivers/better-sqlite3.ts +++ b/src/drivers/better-sqlite3.ts @@ -8,6 +8,7 @@ import { } from "typescript"; import { Parameter, Column, Query } from "../gen/plugin/codegen_pb"; +import { CustomTypeFn } from "../customTypes"; import { argName } from "./utlis"; function funcParamsDecl(iface: string | undefined, params: Parameter[]) { @@ -45,6 +46,12 @@ function funcParamsDecl(iface: string | undefined, params: Parameter[]) { } export class Driver { + private readonly customType: CustomTypeFn; + + constructor(customType?: CustomTypeFn) { + this.customType = customType ?? (() => undefined); + } + /** * {@link https://github.com/WiseLibs/better-sqlite3/blob/v9.4.1/docs/api.md#binding-parameters} * {@link https://github.com/sqlc-dev/sqlc/blob/v1.25.0/internal/codegen/golang/sqlite_type.go} @@ -55,7 +62,10 @@ export class Driver { } let typ: TypeNode = factory.createKeywordTypeNode(SyntaxKind.AnyKeyword); - switch (column.type.name) { + const custom = this.customType(column); + if (custom !== undefined) { + typ = custom; + } else switch (column.type.name) { case "int": case "integer": case "tinyint": diff --git a/src/drivers/mysql2.ts b/src/drivers/mysql2.ts index 7859ade..5efb8de 100644 --- a/src/drivers/mysql2.ts +++ b/src/drivers/mysql2.ts @@ -3,6 +3,7 @@ import { SyntaxKind, NodeFlags, TypeNode, factory } from "typescript"; // import { writeFileSync, STDIO } from "javy/fs"; import { Parameter, Column, Query } from "../gen/plugin/codegen_pb"; +import { CustomTypeFn } from "../customTypes"; import { argName, colName } from "./utlis"; export interface Mysql2Options { @@ -46,9 +47,11 @@ function funcParamsDecl(iface: string | undefined, params: Parameter[]) { export class Driver { private readonly options: Mysql2Options + private readonly customType: CustomTypeFn; - constructor(options?: Mysql2Options) { + constructor(options?: Mysql2Options, customType?: CustomTypeFn) { this.options = options ?? {} + this.customType = customType ?? (() => undefined); } columnType(column?: Column): TypeNode { @@ -57,7 +60,10 @@ export class Driver { } let typ: TypeNode = factory.createKeywordTypeNode(SyntaxKind.StringKeyword); - switch (column.type.name) { + const custom = this.customType(column); + if (custom !== undefined) { + typ = custom; + } else switch (column.type.name) { case "bigint": { typ = factory.createKeywordTypeNode(SyntaxKind.NumberKeyword); diff --git a/src/drivers/pg.ts b/src/drivers/pg.ts index 95828cf..c4f6a34 100644 --- a/src/drivers/pg.ts +++ b/src/drivers/pg.ts @@ -8,6 +8,7 @@ import { } from "typescript"; import { Parameter, Column, Query } from "../gen/plugin/codegen_pb"; +import { CustomTypeFn } from "../customTypes"; import { argName, colName } from "./utlis"; function funcParamsDecl(iface: string | undefined, params: Parameter[]) { @@ -45,6 +46,12 @@ function funcParamsDecl(iface: string | undefined, params: Parameter[]) { } export class Driver { + private readonly customType: CustomTypeFn; + + constructor(customType?: CustomTypeFn) { + this.customType = customType ?? (() => undefined); + } + columnType(column?: Column): TypeNode { if (column === undefined || column.type === undefined) { return factory.createKeywordTypeNode(SyntaxKind.AnyKeyword); @@ -56,7 +63,10 @@ export class Driver { typeName = typeName.slice(pgCatalog.length); } let typ: TypeNode = factory.createKeywordTypeNode(SyntaxKind.StringKeyword); - switch (typeName) { + const custom = this.customType(column); + if (custom !== undefined) { + typ = custom; + } else switch (typeName) { case "aclitem": { // string break; diff --git a/src/drivers/postgres.ts b/src/drivers/postgres.ts index 68d23f2..228f4c4 100644 --- a/src/drivers/postgres.ts +++ b/src/drivers/postgres.ts @@ -7,6 +7,7 @@ import { } from "typescript"; import { Parameter, Column } from "../gen/plugin/codegen_pb"; +import { CustomTypeFn } from "../customTypes"; import { argName, colName } from "./utlis"; import { log } from "../logger"; @@ -45,6 +46,12 @@ function funcParamsDecl(iface: string | undefined, params: Parameter[]) { } export class Driver { + private readonly customType: CustomTypeFn; + + constructor(customType?: CustomTypeFn) { + this.customType = customType ?? (() => undefined); + } + columnType(column?: Column): TypeNode { if (column === undefined || column.type === undefined) { return factory.createKeywordTypeNode(SyntaxKind.AnyKeyword); @@ -56,7 +63,10 @@ export class Driver { typeName = typeName.slice(pgCatalog.length); } let typ: TypeNode = factory.createKeywordTypeNode(SyntaxKind.StringKeyword); - switch (typeName) { + const custom = this.customType(column); + if (custom !== undefined) { + typ = custom; + } else switch (typeName) { case "aclitem": { // string break; From 903881e7d411adee4b63d82cf681dffd22cf636e Mon Sep 17 00:00:00 2001 From: Ryan Tolsma <1tolsmar@gmail.com> Date: Sat, 22 Aug 2026 01:52:23 -0400 Subject: [PATCH 2/7] fix: Emit type-only imports in all driver preambles 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 #55. Fixes #61. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GFyrFwpJC2eivaBRyNBJqv --- examples/bun-mysql2/src/db/query_sql.ts | 4 +++- examples/bun-pg/src/db/query_sql.ts | 2 +- examples/bun-postgres/src/db/query_sql.ts | 2 +- examples/node-better-sqlite3/src/db/query_sql.ts | 2 +- examples/node-mysql2/src/db/query_sql.ts | 4 +++- examples/node-pg/src/db/query_sql.ts | 2 +- examples/node-postgres/src/db/query_sql.ts | 2 +- src/drivers/better-sqlite3.ts | 2 +- src/drivers/mysql2.ts | 12 +++++++++++- src/drivers/pg.ts | 4 ++-- src/drivers/postgres.ts | 2 +- 11 files changed, 26 insertions(+), 12 deletions(-) diff --git a/examples/bun-mysql2/src/db/query_sql.ts b/examples/bun-mysql2/src/db/query_sql.ts index 1673007..9b3efeb 100644 --- a/examples/bun-mysql2/src/db/query_sql.ts +++ b/examples/bun-mysql2/src/db/query_sql.ts @@ -1,6 +1,8 @@ // Code generated by sqlc. DO NOT EDIT. -import mysql, { RowDataPacket, ResultSetHeader } from "mysql2/promise"; +import type mysql from "mysql2/promise"; + +import type { RowDataPacket, ResultSetHeader } from "mysql2/promise"; type Client = mysql.Connection | mysql.Pool; diff --git a/examples/bun-pg/src/db/query_sql.ts b/examples/bun-pg/src/db/query_sql.ts index 2e8db52..525d4a4 100644 --- a/examples/bun-pg/src/db/query_sql.ts +++ b/examples/bun-pg/src/db/query_sql.ts @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. -import { QueryArrayConfig, QueryArrayResult } from "pg"; +import type { QueryArrayConfig, QueryArrayResult } from "pg"; interface Client { query: (config: QueryArrayConfig) => Promise; diff --git a/examples/bun-postgres/src/db/query_sql.ts b/examples/bun-postgres/src/db/query_sql.ts index 8e15f3b..1121031 100644 --- a/examples/bun-postgres/src/db/query_sql.ts +++ b/examples/bun-postgres/src/db/query_sql.ts @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. -import { Sql } from "postgres"; +import type { Sql } from "postgres"; export const getAuthorQuery = `-- name: GetAuthor :one SELECT id, name, bio FROM authors diff --git a/examples/node-better-sqlite3/src/db/query_sql.ts b/examples/node-better-sqlite3/src/db/query_sql.ts index 8996855..6628e69 100644 --- a/examples/node-better-sqlite3/src/db/query_sql.ts +++ b/examples/node-better-sqlite3/src/db/query_sql.ts @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. -import { Database } from "better-sqlite3"; +import type { Database } from "better-sqlite3"; export const getAuthorQuery = `-- name: GetAuthor :one SELECT id, name, bio FROM authors diff --git a/examples/node-mysql2/src/db/query_sql.ts b/examples/node-mysql2/src/db/query_sql.ts index c79988f..6a8b548 100644 --- a/examples/node-mysql2/src/db/query_sql.ts +++ b/examples/node-mysql2/src/db/query_sql.ts @@ -1,6 +1,8 @@ // Code generated by sqlc. DO NOT EDIT. -import mysql, { RowDataPacket, ResultSetHeader } from "mysql2/promise"; +import type mysql from "mysql2/promise"; + +import type { RowDataPacket, ResultSetHeader } from "mysql2/promise"; type Client = mysql.Connection | mysql.Pool; diff --git a/examples/node-pg/src/db/query_sql.ts b/examples/node-pg/src/db/query_sql.ts index 2e8db52..525d4a4 100644 --- a/examples/node-pg/src/db/query_sql.ts +++ b/examples/node-pg/src/db/query_sql.ts @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. -import { QueryArrayConfig, QueryArrayResult } from "pg"; +import type { QueryArrayConfig, QueryArrayResult } from "pg"; interface Client { query: (config: QueryArrayConfig) => Promise; diff --git a/examples/node-postgres/src/db/query_sql.ts b/examples/node-postgres/src/db/query_sql.ts index 8e15f3b..1121031 100644 --- a/examples/node-postgres/src/db/query_sql.ts +++ b/examples/node-postgres/src/db/query_sql.ts @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. -import { Sql } from "postgres"; +import type { Sql } from "postgres"; export const getAuthorQuery = `-- name: GetAuthor :one SELECT id, name, bio FROM authors diff --git a/src/drivers/better-sqlite3.ts b/src/drivers/better-sqlite3.ts index 6ec24d8..5afffd0 100644 --- a/src/drivers/better-sqlite3.ts +++ b/src/drivers/better-sqlite3.ts @@ -125,7 +125,7 @@ export class Driver { factory.createImportDeclaration( undefined, factory.createImportClause( - false, + true, undefined, factory.createNamedImports([ factory.createImportSpecifier( diff --git a/src/drivers/mysql2.ts b/src/drivers/mysql2.ts index 5efb8de..128258d 100644 --- a/src/drivers/mysql2.ts +++ b/src/drivers/mysql2.ts @@ -237,8 +237,18 @@ export class Driver { factory.createImportDeclaration( undefined, factory.createImportClause( - false, + true, factory.createIdentifier("mysql"), + undefined + ), + factory.createStringLiteral("mysql2/promise"), + undefined + ), + factory.createImportDeclaration( + undefined, + factory.createImportClause( + true, + undefined, factory.createNamedImports([ factory.createImportSpecifier( false, diff --git a/src/drivers/pg.ts b/src/drivers/pg.ts index c4f6a34..0387fa9 100644 --- a/src/drivers/pg.ts +++ b/src/drivers/pg.ts @@ -350,7 +350,7 @@ export class Driver { factory.createImportDeclaration( undefined, factory.createImportClause( - false, + true, undefined, factory.createNamedImports([ factory.createImportSpecifier( @@ -381,7 +381,7 @@ export class Driver { factory.createImportDeclaration( undefined, factory.createImportClause( - false, + true, undefined, factory.createNamedImports([ factory.createImportSpecifier( diff --git a/src/drivers/postgres.ts b/src/drivers/postgres.ts index 228f4c4..42389fd 100644 --- a/src/drivers/postgres.ts +++ b/src/drivers/postgres.ts @@ -313,7 +313,7 @@ export class Driver { factory.createImportDeclaration( undefined, factory.createImportClause( - false, + true, undefined, factory.createNamedImports([ factory.createImportSpecifier( From 74e1a5e6cf4553a09f783a90b42bf9c04c8f6ae0 Mon Sep 17 00:00:00 2001 From: Ryan Tolsma <1tolsmar@gmail.com> Date: Sat, 22 Aug 2026 01:53:38 -0400 Subject: [PATCH 3/7] fix: Generate valid code for column names that are not TS identifiers 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 #70. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GFyrFwpJC2eivaBRyNBJqv --- src/app.ts | 6 +++--- src/drivers/better-sqlite3.ts | 17 ++++------------- src/drivers/mysql2.ts | 32 +++++++------------------------- src/drivers/pg.ts | 25 ++++++------------------- src/drivers/postgres.ts | 25 ++++++------------------- src/drivers/utlis.ts | 27 +++++++++++++++++++++++++++ 6 files changed, 53 insertions(+), 79 deletions(-) diff --git a/src/app.ts b/src/app.ts index d016892..5dd0e08 100644 --- a/src/app.ts +++ b/src/app.ts @@ -28,7 +28,7 @@ import { Query, } from "./gen/plugin/codegen_pb"; -import { argName, colName } from "./drivers/utlis"; +import { argName, colName, propertyKey } from "./drivers/utlis"; import { CustomTypeFn, CustomTypeRegistry, @@ -268,7 +268,7 @@ function argsDecl( params.map((param, i) => factory.createPropertySignature( undefined, - factory.createIdentifier(argName(i, param.column)), + propertyKey(argName(i, param.column)), undefined, driver.columnType(param.column) ) @@ -289,7 +289,7 @@ function rowDecl( columns.map((column, i) => factory.createPropertySignature( undefined, - factory.createIdentifier(colName(i, column)), + propertyKey(colName(i, column)), undefined, driver.columnType(column) ) diff --git a/src/drivers/better-sqlite3.ts b/src/drivers/better-sqlite3.ts index 5afffd0..820fedd 100644 --- a/src/drivers/better-sqlite3.ts +++ b/src/drivers/better-sqlite3.ts @@ -9,7 +9,7 @@ import { import { Parameter, Column, Query } from "../gen/plugin/codegen_pb"; import { CustomTypeFn } from "../customTypes"; -import { argName } from "./utlis"; +import { argName, propertyAccess } from "./utlis"; function funcParamsDecl(iface: string | undefined, params: Parameter[]) { let funcParams = [ @@ -200,10 +200,7 @@ export class Driver { ), undefined, params.map((param, i) => - factory.createPropertyAccessExpression( - factory.createIdentifier("args"), - factory.createIdentifier(argName(i, param.column)) - ) + propertyAccess("args", argName(i, param.column)) ) ) ) @@ -286,10 +283,7 @@ export class Driver { ), undefined, params.map((param, i) => - factory.createPropertyAccessExpression( - factory.createIdentifier("args"), - factory.createIdentifier(argName(i, param.column)) - ) + propertyAccess("args", argName(i, param.column)) ) ) ) @@ -401,10 +395,7 @@ export class Driver { ), undefined, params.map((param, i) => - factory.createPropertyAccessExpression( - factory.createIdentifier("args"), - factory.createIdentifier(argName(i, param.column)) - ) + propertyAccess("args", argName(i, param.column)) ) ) ) diff --git a/src/drivers/mysql2.ts b/src/drivers/mysql2.ts index 128258d..734d99a 100644 --- a/src/drivers/mysql2.ts +++ b/src/drivers/mysql2.ts @@ -4,7 +4,7 @@ import { SyntaxKind, NodeFlags, TypeNode, factory } from "typescript"; import { Parameter, Column, Query } from "../gen/plugin/codegen_pb"; import { CustomTypeFn } from "../customTypes"; -import { argName, colName } from "./utlis"; +import { argName, colName, propertyAccess, propertyKey } from "./utlis"; export interface Mysql2Options { support_big_numbers?: boolean; @@ -334,10 +334,7 @@ export class Driver { factory.createIdentifier("values"), factory.createArrayLiteralExpression( params.map((param, i) => - factory.createPropertyAccessExpression( - factory.createIdentifier("args"), - factory.createIdentifier(argName(i, param.column)) - ) + propertyAccess("args", argName(i, param.column)) ), false ) @@ -424,12 +421,7 @@ export class Driver { factory.createIdentifier("values"), factory.createArrayLiteralExpression( params.map((param, i) => - factory.createPropertyAccessExpression( - factory.createIdentifier("args"), - factory.createIdentifier( - argName(i, param.column) - ) - ) + propertyAccess("args", argName(i, param.column)) ), false ) @@ -483,7 +475,7 @@ export class Driver { factory.createObjectLiteralExpression( columns.map((col, i) => factory.createPropertyAssignment( - factory.createIdentifier(colName(i, col)), + propertyKey(colName(i, col)), factory.createElementAccessExpression( factory.createIdentifier("row"), factory.createNumericLiteral(`${i}`) @@ -576,12 +568,7 @@ export class Driver { factory.createIdentifier("values"), factory.createArrayLiteralExpression( params.map((param, i) => - factory.createPropertyAccessExpression( - factory.createIdentifier("args"), - factory.createIdentifier( - argName(i, param.column) - ) - ) + propertyAccess("args", argName(i, param.column)) ), false ) @@ -647,7 +634,7 @@ export class Driver { factory.createObjectLiteralExpression( columns.map((col, i) => factory.createPropertyAssignment( - factory.createIdentifier(colName(i, col)), + propertyKey(colName(i, col)), factory.createElementAccessExpression( factory.createIdentifier("row"), factory.createNumericLiteral(`${i}`) @@ -723,12 +710,7 @@ export class Driver { factory.createIdentifier("values"), factory.createArrayLiteralExpression( params.map((param, i) => - factory.createPropertyAccessExpression( - factory.createIdentifier("args"), - factory.createIdentifier( - argName(i, param.column) - ) - ) + propertyAccess("args", argName(i, param.column)) ), false ) diff --git a/src/drivers/pg.ts b/src/drivers/pg.ts index 0387fa9..0996972 100644 --- a/src/drivers/pg.ts +++ b/src/drivers/pg.ts @@ -9,7 +9,7 @@ import { import { Parameter, Column, Query } from "../gen/plugin/codegen_pb"; import { CustomTypeFn } from "../customTypes"; -import { argName, colName } from "./utlis"; +import { argName, colName, propertyAccess, propertyKey } from "./utlis"; function funcParamsDecl(iface: string | undefined, params: Parameter[]) { let funcParams = [ @@ -482,10 +482,7 @@ export class Driver { factory.createIdentifier("values"), factory.createArrayLiteralExpression( params.map((param, i) => - factory.createPropertyAccessExpression( - factory.createIdentifier("args"), - factory.createIdentifier(argName(i, param.column)) - ) + propertyAccess("args", argName(i, param.column)) ), false ) @@ -563,12 +560,7 @@ export class Driver { factory.createIdentifier("values"), factory.createArrayLiteralExpression( params.map((param, i) => - factory.createPropertyAccessExpression( - factory.createIdentifier("args"), - factory.createIdentifier( - argName(i, param.column) - ) - ) + propertyAccess("args", argName(i, param.column)) ), false ) @@ -640,7 +632,7 @@ export class Driver { factory.createObjectLiteralExpression( columns.map((col, i) => factory.createPropertyAssignment( - factory.createIdentifier(colName(i, col)), + propertyKey(colName(i, col)), factory.createElementAccessExpression( factory.createIdentifier("row"), factory.createNumericLiteral(`${i}`) @@ -711,12 +703,7 @@ export class Driver { factory.createIdentifier("values"), factory.createArrayLiteralExpression( params.map((param, i) => - factory.createPropertyAccessExpression( - factory.createIdentifier("args"), - factory.createIdentifier( - argName(i, param.column) - ) - ) + propertyAccess("args", argName(i, param.column)) ), false ) @@ -773,7 +760,7 @@ export class Driver { factory.createObjectLiteralExpression( columns.map((col, i) => factory.createPropertyAssignment( - factory.createIdentifier(colName(i, col)), + propertyKey(colName(i, col)), factory.createElementAccessExpression( factory.createIdentifier("row"), factory.createNumericLiteral(`${i}`) diff --git a/src/drivers/postgres.ts b/src/drivers/postgres.ts index 42389fd..0a7fa0e 100644 --- a/src/drivers/postgres.ts +++ b/src/drivers/postgres.ts @@ -8,7 +8,7 @@ import { import { Parameter, Column } from "../gen/plugin/codegen_pb"; import { CustomTypeFn } from "../customTypes"; -import { argName, colName } from "./utlis"; +import { argName, colName, propertyAccess, propertyKey } from "./utlis"; import { log } from "../logger"; function funcParamsDecl(iface: string | undefined, params: Parameter[]) { @@ -363,10 +363,7 @@ export class Driver { factory.createIdentifier(queryName), factory.createArrayLiteralExpression( params.map((param, i) => - factory.createPropertyAccessExpression( - factory.createIdentifier("args"), - factory.createIdentifier(argName(i, param.column)) - ) + propertyAccess("args", argName(i, param.column)) ), false ), @@ -425,12 +422,7 @@ export class Driver { factory.createIdentifier(queryName), factory.createArrayLiteralExpression( params.map((param, i) => - factory.createPropertyAccessExpression( - factory.createIdentifier("args"), - factory.createIdentifier( - argName(i, param.column) - ) - ) + propertyAccess("args", argName(i, param.column)) ), false ), @@ -461,7 +453,7 @@ export class Driver { factory.createObjectLiteralExpression( columns.map((col, i) => factory.createPropertyAssignment( - factory.createIdentifier(colName(i, col)), + propertyKey(colName(i, col)), factory.createElementAccessExpression( factory.createIdentifier("row"), factory.createNumericLiteral(`${i}`) @@ -531,12 +523,7 @@ export class Driver { factory.createIdentifier(queryName), factory.createArrayLiteralExpression( params.map((param, i) => - factory.createPropertyAccessExpression( - factory.createIdentifier("args"), - factory.createIdentifier( - argName(i, param.column) - ) - ) + propertyAccess("args", argName(i, param.column)) ), false ), @@ -605,7 +592,7 @@ export class Driver { factory.createObjectLiteralExpression( columns.map((col, i) => factory.createPropertyAssignment( - factory.createIdentifier(colName(i, col)), + propertyKey(colName(i, col)), factory.createElementAccessExpression( factory.createIdentifier("row"), factory.createNumericLiteral(`${i}`) diff --git a/src/drivers/utlis.ts b/src/drivers/utlis.ts index 870b421..515c3b1 100644 --- a/src/drivers/utlis.ts +++ b/src/drivers/utlis.ts @@ -1,3 +1,5 @@ +import { Expression, PropertyName, factory } from "typescript"; + import { Column } from "../gen/plugin/codegen_pb"; // https://stackoverflow.com/questions/40710628/how-to-convert-snake-case-to-camelcase @@ -22,3 +24,28 @@ export function argName(index: number, column?: Column): string { export function colName(index: number, column?: Column): string { return fieldName("col", index, column); } + +const validIdentifier = /^[A-Za-z_$][A-Za-z0-9_$]*$/; + +// Property name for an interface or object literal: a plain identifier when +// possible, otherwise a quoted string ("invalid-name": ...) +export function propertyKey(name: string): PropertyName { + if (validIdentifier.test(name)) { + return factory.createIdentifier(name); + } + return factory.createStringLiteral(name); +} + +// target.name when name is a valid identifier, otherwise target["name"] +export function propertyAccess(target: string, name: string): Expression { + if (validIdentifier.test(name)) { + return factory.createPropertyAccessExpression( + factory.createIdentifier(target), + factory.createIdentifier(name) + ); + } + return factory.createElementAccessExpression( + factory.createIdentifier(target), + factory.createStringLiteral(name) + ); +} From 6d4de9735b9c0ef486467c4c2a8f46bbff537212 Mon Sep 17 00:00:00 2001 From: Ryan Tolsma <1tolsmar@gmail.com> Date: Sat, 22 Aug 2026 01:54:39 -0400 Subject: [PATCH 4/7] fix: Escape backticks, \${ and backslashes in generated query literals 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 #40. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GFyrFwpJC2eivaBRyNBJqv --- src/app.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index 5dd0e08..a5c40c9 100644 --- a/src/app.ts +++ b/src/app.ts @@ -239,6 +239,13 @@ function readInput(): GenerateRequest { } function queryDecl(name: string, sql: string) { + // Escape characters that have special meaning inside a template literal so + // queries containing backticks (MySQL quoting), "${" or backslashes still + // produce the original SQL text at runtime + const raw = sql + .replace(/\\/g, "\\\\") + .replace(/`/g, "\\`") + .replace(/\$\{/g, "\\${"); return factory.createVariableStatement( [factory.createToken(SyntaxKind.ExportKeyword)], factory.createVariableDeclarationList( @@ -247,7 +254,7 @@ function queryDecl(name: string, sql: string) { factory.createIdentifier(name), undefined, undefined, - factory.createNoSubstitutionTemplateLiteral(sql, sql) + factory.createNoSubstitutionTemplateLiteral(sql, raw) ), ], NodeFlags.Const //| NodeFlags.Constant | NodeFlags.Constant From 9198dcac99acae4f9b163b93b0b0bc8ec97925fe Mon Sep 17 00:00:00 2001 From: Ryan Tolsma <1tolsmar@gmail.com> Date: Sat, 22 Aug 2026 01:55:49 -0400 Subject: [PATCH 5/7] feat: Add types_only option to generate interfaces without queries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 #71. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GFyrFwpJC2eivaBRyNBJqv --- README.md | 17 +++++++++++++++++ src/app.ts | 21 +++++++++++++-------- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 3ec1a6c..11d600e 100644 --- a/README.md +++ b/README.md @@ -414,6 +414,23 @@ Note that overrides only change the generated *types* — converting values at runtime (for example, parsing a `numeric` string into a `Big`) is still up to your driver configuration. +### Types-only generation + +Set `types_only: true` to generate only the `Args`/`Row` interfaces, enum +union types, and override imports — no driver imports, query constants, or +query functions. This is useful for sharing database types with a frontend +that never runs the queries itself. + +```yaml +codegen: +- out: src/types/db + plugin: ts + options: + runtime: node + driver: pg + types_only: true +``` + ## Development If you want to build and test sqlc-gen-typescript locally, follow these steps: diff --git a/src/app.ts b/src/app.ts index a5c40c9..0ba4abe 100644 --- a/src/app.ts +++ b/src/app.ts @@ -51,6 +51,7 @@ interface Options { driver?: string; mysql2?: Mysql2Options overrides?: TypeOverride[]; + types_only?: boolean; } interface Driver { @@ -132,7 +133,9 @@ function codegen(input: GenerateRequest): GenerateResponse { } for (const [filename, queries] of querymap.entries()) { - const nodes = driver.preamble(queries); + // With types_only, emit only interfaces, enums, and override imports — + // no driver imports, query constants, or query functions + const nodes = options.types_only ? [] : driver.preamble(queries); // Insert custom type imports and enum declarations after the driver's // own imports, before any other declarations let insertAt = 0; @@ -157,13 +160,15 @@ function codegen(input: GenerateRequest): GenerateResponse { const lowerName = query.name[0].toLowerCase() + query.name.slice(1); const textName = `${lowerName}Query`; - nodes.push( - queryDecl( - textName, - `-- name: ${query.name} ${query.cmd} + if (!options.types_only) { + nodes.push( + queryDecl( + textName, + `-- name: ${query.name} ${query.cmd} ${query.text}` - ) - ); + ) + ); + } let argIface = undefined; let returnIface = undefined; @@ -176,7 +181,7 @@ ${query.text}` nodes.push(rowDecl(returnIface, driver, query.columns)); } - switch (query.cmd) { + switch (options.types_only ? "" : query.cmd) { case ":exec": { nodes.push( driver.execDecl(lowerName, textName, argIface, query.params) From e71d7f05b7f08df959f78bc5d2caf2217b064c98 Mon Sep 17 00:00:00 2001 From: Ryan Tolsma <1tolsmar@gmail.com> Date: Sat, 22 Aug 2026 01:57:53 -0400 Subject: [PATCH 6/7] fix(mysql): Make :execlastid respect big number options The bigint column mapping already honored support_big_numbers / big_number_strings, but :execlastid always returned Promise, 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, and the bun-mysql2 example (no options), which is unchanged; both outputs compile with the examples' tsconfig. Fixes #22. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GFyrFwpJC2eivaBRyNBJqv --- examples/node-mysql2/src/db/query_sql.ts | 4 +- src/drivers/mysql2.ts | 50 +++++++++++++++++++----- 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/examples/node-mysql2/src/db/query_sql.ts b/examples/node-mysql2/src/db/query_sql.ts index 6a8b548..9d10881 100644 --- a/examples/node-mysql2/src/db/query_sql.ts +++ b/examples/node-mysql2/src/db/query_sql.ts @@ -93,12 +93,12 @@ export interface CreateAuthorReturnIdArgs { bio: string | null; } -export async function createAuthorReturnId(client: Client, args: CreateAuthorReturnIdArgs): Promise { +export async function createAuthorReturnId(client: Client, args: CreateAuthorReturnIdArgs): Promise { const [result] = await client.query({ sql: createAuthorReturnIdQuery, values: [args.name, args.bio] }); - return result?.insertId ?? 0; + return String(result?.insertId ?? 0); } export const deleteAuthorQuery = `-- name: DeleteAuthor :exec diff --git a/src/drivers/mysql2.ts b/src/drivers/mysql2.ts index 734d99a..cac0f84 100644 --- a/src/drivers/mysql2.ts +++ b/src/drivers/mysql2.ts @@ -1,4 +1,4 @@ -import { SyntaxKind, NodeFlags, TypeNode, factory } from "typescript"; +import { Expression, SyntaxKind, NodeFlags, TypeNode, factory } from "typescript"; // import { writeFileSync, STDIO } from "javy/fs"; @@ -45,6 +45,20 @@ function funcParamsDecl(iface: string | undefined, params: Parameter[]) { return funcParams; } +// The mysql2 types declare insertId as a number, but with +// big_number_strings enabled the driver returns big values as strings, so +// convert explicitly to match the declared return type +function insertIdExpr(value: Expression, options: Mysql2Options): Expression { + if (options.support_big_numbers && options.big_number_strings) { + return factory.createCallExpression( + factory.createIdentifier("String"), + undefined, + [value] + ); + } + return value; +} + export class Driver { private readonly options: Mysql2Options private readonly customType: CustomTypeFn; @@ -658,6 +672,21 @@ export class Driver { ) { const funcParams = funcParamsDecl(argIface, params); + // Mirror the bigint column mapping: with support_big_numbers the driver + // may return the last insert id as a string, and with big_number_strings + // it always does + let insertIdType: TypeNode = factory.createKeywordTypeNode( + SyntaxKind.NumberKeyword + ); + if (this.options.support_big_numbers) { + insertIdType = this.options.big_number_strings + ? factory.createKeywordTypeNode(SyntaxKind.StringKeyword) + : factory.createUnionTypeNode([ + factory.createKeywordTypeNode(SyntaxKind.NumberKeyword), + factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + ]); + } + return factory.createFunctionDeclaration( [ factory.createToken(SyntaxKind.ExportKeyword), @@ -668,7 +697,7 @@ export class Driver { undefined, funcParams, factory.createTypeReferenceNode(factory.createIdentifier("Promise"), [ - factory.createTypeReferenceNode("number", undefined), + insertIdType, ]), factory.createBlock( [ @@ -732,14 +761,17 @@ export class Driver { ) ), factory.createReturnStatement( - factory.createBinaryExpression( - factory.createPropertyAccessChain( - factory.createIdentifier("result"), - factory.createToken(SyntaxKind.QuestionDotToken), - factory.createIdentifier("insertId") + insertIdExpr( + factory.createBinaryExpression( + factory.createPropertyAccessChain( + factory.createIdentifier("result"), + factory.createToken(SyntaxKind.QuestionDotToken), + factory.createIdentifier("insertId") + ), + factory.createToken(SyntaxKind.QuestionQuestionToken), + factory.createNumericLiteral(0) ), - factory.createToken(SyntaxKind.QuestionQuestionToken), - factory.createNumericLiteral(0) + this.options ) ), ], From 77e343e933ae0dd3d6a3a01a827dbbb7bc749db8 Mon Sep 17 00:00:00 2001 From: Ryan Tolsma <1tolsmar@gmail.com> Date: Sat, 22 Aug 2026 01:58:57 -0400 Subject: [PATCH 7/7] fix: Fall back to positional arg names for unnamed parameters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 #29. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GFyrFwpJC2eivaBRyNBJqv --- src/drivers/utlis.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/drivers/utlis.ts b/src/drivers/utlis.ts index 515c3b1..41ccb6a 100644 --- a/src/drivers/utlis.ts +++ b/src/drivers/utlis.ts @@ -8,8 +8,8 @@ export function fieldName( index: number, column?: Column ): string { - let name = `${prefix}_${index}`; - if (column) { + let name = `${prefix}_${index + 1}`; + if (column && column.name) { name = column.name; } return name