Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,21 @@ Generated TypeScript types are compile-time types. The generated code does not
post-process rows or validate runtime values. Runtime parsing belongs to
postgres.js configuration.

PostgreSQL `json` and `jsonb` columns are emitted as `JsonValue` instead of
`any`:

```ts
export type JsonPrimitive = string | number | boolean | null;
export type JsonValue =
| JsonPrimitive
| readonly JsonValue[]
| { readonly [key: string]: JsonValue | undefined };
```

JSON parameters are serialized with `sql.json(...)` in generated queries. For
nullable JSON parameters, JavaScript `null` is sent as SQL `NULL`; use a
non-nullable JSON parameter if you need to write the JSON literal `null`.

## Vercel Notes

The generated code depends on the `postgres` npm package. postgres.js is pure
Expand Down
4 changes: 2 additions & 2 deletions examples/authors/postgresql/query.sql
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ ORDER BY name;

-- name: CreateAuthor :one
INSERT INTO authors (
name, bio, status
name, bio, status, profile, notes
) VALUES (
$1, $2, $3
$1, $2, $3, $4, $5
)
RETURNING *;

Expand Down
4 changes: 3 additions & 1 deletion examples/authors/postgresql/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ CREATE TABLE authors (
id BIGSERIAL PRIMARY KEY,
name text NOT NULL,
bio text,
status author_status NOT NULL DEFAULT 'active'
status author_status NOT NULL DEFAULT 'active',
profile jsonb NOT NULL DEFAULT '{}'::jsonb,
notes jsonb
);
28 changes: 22 additions & 6 deletions examples/bun-postgres/src/db/query_sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

import type { Sql } from "postgres";

export type JsonPrimitive = string | number | boolean | null;

export type JsonValue = JsonPrimitive | readonly JsonValue[] | {
readonly [key: string]: JsonValue | undefined;
};

export type AuthorStatus = "active" | "inactive" | "pending";

export interface GetAuthorArgs {
Expand All @@ -13,10 +19,12 @@ export interface GetAuthorRow {
name: string;
bio: string | null;
status: AuthorStatus;
profile: JsonValue;
notes: JsonValue | null;
}

export async function getAuthor(sql: Sql, args: GetAuthorArgs): Promise<GetAuthorRow | null> {
const rows = await sql<GetAuthorRow[]> `SELECT id, name, bio, status FROM authors
const rows = await sql<GetAuthorRow[]> `SELECT id, name, bio, status, profile, notes FROM authors
WHERE id = ${args.id} LIMIT 1`;
return rows[0] ?? null;
}
Expand All @@ -26,33 +34,39 @@ export interface ListAuthorsRow {
name: string;
bio: string | null;
status: AuthorStatus;
profile: JsonValue;
notes: JsonValue | null;
}

export async function listAuthors(sql: Sql): Promise<ListAuthorsRow[]> {
return await sql<ListAuthorsRow[]> `SELECT id, name, bio, status FROM authors
return await sql<ListAuthorsRow[]> `SELECT id, name, bio, status, profile, notes FROM authors
ORDER BY name`;
}

export interface CreateAuthorArgs {
name: string;
bio: string | null;
status: AuthorStatus;
profile: JsonValue;
notes: JsonValue | null;
}

export interface CreateAuthorRow {
id: number;
name: string;
bio: string | null;
status: AuthorStatus;
profile: JsonValue;
notes: JsonValue | null;
}

export async function createAuthor(sql: Sql, args: CreateAuthorArgs): Promise<CreateAuthorRow | null> {
const rows = await sql<CreateAuthorRow[]> `INSERT INTO authors (
name, bio, status
name, bio, status, profile, notes
) VALUES (
${args.name}, ${args.bio}, ${args.status}
${args.name}, ${args.bio}, ${args.status}, ${sql.json(args.profile)}, ${args.notes === null ? null : sql.json(args.notes)}
)
RETURNING id, name, bio, status`;
RETURNING id, name, bio, status, profile, notes`;
return rows[0] ?? null;
}

Expand All @@ -65,10 +79,12 @@ export interface ListAuthorsByStatusRow {
name: string;
bio: string | null;
status: AuthorStatus;
profile: JsonValue;
notes: JsonValue | null;
}

export async function listAuthorsByStatus(sql: Sql, args: ListAuthorsByStatusArgs): Promise<ListAuthorsByStatusRow[]> {
return await sql<ListAuthorsByStatusRow[]> `SELECT id, name, bio, status FROM authors
return await sql<ListAuthorsByStatusRow[]> `SELECT id, name, bio, status, profile, notes FROM authors
WHERE status = ${args.status}
ORDER BY name`;
}
Expand Down
2 changes: 2 additions & 0 deletions examples/bun-postgres/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ async function main() {
name: "Seal",
bio: "Kissed from a rose",
status: "active",
profile: { website: "https://example.com", verified: true },
notes: null,
});
if (author === null) {
throw new Error("author not created");
Expand Down
28 changes: 22 additions & 6 deletions examples/node-postgres/src/db/query_sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

import type { Sql } from "postgres";

export type JsonPrimitive = string | number | boolean | null;

export type JsonValue = JsonPrimitive | readonly JsonValue[] | {
readonly [key: string]: JsonValue | undefined;
};

export type AuthorStatus = "active" | "inactive" | "pending";

export interface GetAuthorArgs {
Expand All @@ -13,10 +19,12 @@ export interface GetAuthorRow {
name: string;
bio: string | null;
status: AuthorStatus;
profile: JsonValue;
notes: JsonValue | null;
}

export async function getAuthor(sql: Sql, args: GetAuthorArgs): Promise<GetAuthorRow | null> {
const rows = await sql<GetAuthorRow[]> `SELECT id, name, bio, status FROM authors
const rows = await sql<GetAuthorRow[]> `SELECT id, name, bio, status, profile, notes FROM authors
WHERE id = ${args.id} LIMIT 1`;
return rows[0] ?? null;
}
Expand All @@ -26,33 +34,39 @@ export interface ListAuthorsRow {
name: string;
bio: string | null;
status: AuthorStatus;
profile: JsonValue;
notes: JsonValue | null;
}

export async function listAuthors(sql: Sql): Promise<ListAuthorsRow[]> {
return await sql<ListAuthorsRow[]> `SELECT id, name, bio, status FROM authors
return await sql<ListAuthorsRow[]> `SELECT id, name, bio, status, profile, notes FROM authors
ORDER BY name`;
}

export interface CreateAuthorArgs {
name: string;
bio: string | null;
status: AuthorStatus;
profile: JsonValue;
notes: JsonValue | null;
}

export interface CreateAuthorRow {
id: number;
name: string;
bio: string | null;
status: AuthorStatus;
profile: JsonValue;
notes: JsonValue | null;
}

export async function createAuthor(sql: Sql, args: CreateAuthorArgs): Promise<CreateAuthorRow | null> {
const rows = await sql<CreateAuthorRow[]> `INSERT INTO authors (
name, bio, status
name, bio, status, profile, notes
) VALUES (
${args.name}, ${args.bio}, ${args.status}
${args.name}, ${args.bio}, ${args.status}, ${sql.json(args.profile)}, ${args.notes === null ? null : sql.json(args.notes)}
)
RETURNING id, name, bio, status`;
RETURNING id, name, bio, status, profile, notes`;
return rows[0] ?? null;
}

Expand All @@ -65,10 +79,12 @@ export interface ListAuthorsByStatusRow {
name: string;
bio: string | null;
status: AuthorStatus;
profile: JsonValue;
notes: JsonValue | null;
}

export async function listAuthorsByStatus(sql: Sql, args: ListAuthorsByStatusArgs): Promise<ListAuthorsByStatusRow[]> {
return await sql<ListAuthorsByStatusRow[]> `SELECT id, name, bio, status FROM authors
return await sql<ListAuthorsByStatusRow[]> `SELECT id, name, bio, status, profile, notes FROM authors
WHERE status = ${args.status}
ORDER BY name`;
}
Expand Down
2 changes: 2 additions & 0 deletions examples/node-postgres/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ async function main() {
name: "Seal",
bio: "Kissed from a rose",
status: "active",
profile: { website: "https://example.com", verified: true },
notes: null,
});
if (author === null) {
throw new Error("author not created");
Expand Down
4 changes: 4 additions & 0 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,9 @@ postgres.js. Current bigint policy emits PostgreSQL `int8`/`bigint`/`bigserial`
as TypeScript `number`, so configure OID 20 parsing as shown when selecting
bigint values. This is intended for values below `Number.MAX_SAFE_INTEGER`.

PostgreSQL `json` and `jsonb` are emitted as generated `JsonValue` aliases
instead of `any`. Scalar JSON parameters are wrapped with `sql.json(...)`; for
nullable JSON parameters, JavaScript `null` is sent as SQL `NULL`.

Current supported surface: PostgreSQL, postgres.js, Node.js/Vercel. Do not rely
on old Bun SQL, MySQL, SQLite, or upstream-sync README instructions.
65 changes: 63 additions & 2 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,55 @@ function enumTypeDecl(name: string, enumDef: Enum): Node {
);
}

function jsonTypeDecls(): Node[] {
return [
factory.createTypeAliasDeclaration(
[factory.createToken(SyntaxKind.ExportKeyword)],
factory.createIdentifier("JsonPrimitive"),
undefined,
factory.createUnionTypeNode([
factory.createKeywordTypeNode(SyntaxKind.StringKeyword),
factory.createKeywordTypeNode(SyntaxKind.NumberKeyword),
factory.createKeywordTypeNode(SyntaxKind.BooleanKeyword),
factory.createLiteralTypeNode(factory.createNull()),
]),
),
factory.createTypeAliasDeclaration(
[factory.createToken(SyntaxKind.ExportKeyword)],
factory.createIdentifier("JsonValue"),
undefined,
factory.createUnionTypeNode([
factory.createTypeReferenceNode(factory.createIdentifier("JsonPrimitive"), undefined),
factory.createTypeOperatorNode(
SyntaxKind.ReadonlyKeyword,
factory.createArrayTypeNode(
factory.createTypeReferenceNode(factory.createIdentifier("JsonValue"), undefined),
),
),
factory.createTypeLiteralNode([
factory.createIndexSignature(
[factory.createToken(SyntaxKind.ReadonlyKeyword)],
[
factory.createParameterDeclaration(
undefined,
undefined,
factory.createIdentifier("key"),
undefined,
factory.createKeywordTypeNode(SyntaxKind.StringKeyword),
undefined,
),
],
factory.createUnionTypeNode([
factory.createTypeReferenceNode(factory.createIdentifier("JsonValue"), undefined),
factory.createKeywordTypeNode(SyntaxKind.UndefinedKeyword),
]),
),
]),
]),
),
];
}

/**
* Convert snake_case to PascalCase
*/
Expand Down Expand Up @@ -101,6 +150,7 @@ function codegen(input: GenerateRequest): GenerateResponse {

// Track enums used in this file
const fileEnums = new Set<string>();
let fileUsesJson = false;

for (const query of queries) {
const lowerName = query.name[0].toLowerCase() + query.name.slice(1);
Expand All @@ -125,6 +175,9 @@ function codegen(input: GenerateRequest): GenerateResponse {
fileEnums.add(enumName);
usedEnums.add(enumName);
}
if (postgres.isJsonColumn(param.column)) {
fileUsesJson = true;
}
}

try {
Expand Down Expand Up @@ -168,6 +221,9 @@ function codegen(input: GenerateRequest): GenerateResponse {
fileEnums.add(enumName);
usedEnums.add(enumName);
}
if (postgres.isJsonColumn(col)) {
fileUsesJson = true;
}
}

try {
Expand Down Expand Up @@ -232,7 +288,12 @@ function codegen(input: GenerateRequest): GenerateResponse {
}
}

// Add enum type declarations at the beginning of the file (after imports)
// Add shared JSON and enum type declarations at the beginning of the file (after imports)
const sharedTypeNodes: Node[] = [];
if (fileUsesJson) {
sharedTypeNodes.push(...jsonTypeDecls());
}

const enumNodes: Node[] = [];
for (const enumName of fileEnums) {
const enumDef = enumMap.get(enumName);
Expand All @@ -243,7 +304,7 @@ function codegen(input: GenerateRequest): GenerateResponse {

// Insert enum declarations after the preamble (imports)
const preambleLength = postgres.preamble().length;
nodes.splice(preambleLength, 0, ...enumNodes);
nodes.splice(preambleLength, 0, ...sharedTypeNodes, ...enumNodes);

files.push(
new File({
Expand Down
Loading
Loading