Skip to content
97 changes: 97 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,103 @@ 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.

### 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:
Expand Down
4 changes: 3 additions & 1 deletion examples/bun-mysql2/src/db/query_sql.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
2 changes: 1 addition & 1 deletion examples/bun-pg/src/db/query_sql.ts
Original file line number Diff line number Diff line change
@@ -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<QueryArrayResult>;
Expand Down
2 changes: 1 addition & 1 deletion examples/bun-postgres/src/db/query_sql.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion examples/node-better-sqlite3/src/db/query_sql.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
8 changes: 5 additions & 3 deletions examples/node-mysql2/src/db/query_sql.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -91,12 +93,12 @@ export interface CreateAuthorReturnIdArgs {
bio: string | null;
}

export async function createAuthorReturnId(client: Client, args: CreateAuthorReturnIdArgs): Promise<number> {
export async function createAuthorReturnId(client: Client, args: CreateAuthorReturnIdArgs): Promise<string> {
const [result] = await client.query<ResultSetHeader>({
sql: createAuthorReturnIdQuery,
values: [args.name, args.bio]
});
return result?.insertId ?? 0;
return String(result?.insertId ?? 0);
}

export const deleteAuthorQuery = `-- name: DeleteAuthor :exec
Expand Down
2 changes: 1 addition & 1 deletion examples/node-pg/src/db/query_sql.ts
Original file line number Diff line number Diff line change
@@ -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<QueryArrayResult>;
Expand Down
2 changes: 1 addition & 1 deletion examples/node-postgres/src/db/query_sql.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
66 changes: 48 additions & 18 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
createPrinter,
createSourceFile,
factory,
isImportDeclaration,
} from "typescript";

import {
Expand All @@ -27,7 +28,12 @@ import {
Query,
} from "./gen/plugin/codegen_pb";

import { argName, colName } from "./drivers/utlis";
import { argName, colName, propertyKey } 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";
Expand All @@ -44,6 +50,8 @@ interface Options {
runtime?: string;
driver?: string;
mysql2?: Mysql2Options
overrides?: TypeOverride[];
types_only?: boolean;
}

interface Driver {
Expand Down Expand Up @@ -79,19 +87,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}`);
Expand All @@ -106,7 +117,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

Expand All @@ -121,7 +133,16 @@ 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;
while (insertAt < nodes.length && isImportDeclaration(nodes[insertAt])) {
insertAt++;
}
nodes.splice(insertAt, 0, ...registry.fileDecls(queries));

for (const query of queries) {
const colmap = new Map<string, number>();
Expand All @@ -139,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;
Expand All @@ -158,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)
Expand Down Expand Up @@ -221,6 +244,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(
Expand All @@ -229,7 +259,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
Expand All @@ -250,7 +280,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)
)
Expand All @@ -271,7 +301,7 @@ function rowDecl(
columns.map((column, i) =>
factory.createPropertySignature(
undefined,
factory.createIdentifier(colName(i, column)),
propertyKey(colName(i, column)),
undefined,
driver.columnType(column)
)
Expand Down
Loading