Skip to content

Commit 04c9c32

Browse files
committed
feat: return undefineds instead of nulls from generated code
the reason is this way plays a lot more nicely with passing around optional parameters in typescript
1 parent f5bd794 commit 04c9c32

File tree

10 files changed

+194
-39
lines changed

10 files changed

+194
-39
lines changed

bun.lock

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
{
2+
"lockfileVersion": 1,
3+
"workspaces": {
4+
"": {
5+
"name": "experiment",
6+
"dependencies": {
7+
"@bufbuild/protobuf": "^1.4.2",
8+
"inflected": "^2.1.0",
9+
"javy": "^0.1.2",
10+
},
11+
"devDependencies": {
12+
"@types/inflected": "^2.1.3",
13+
"esbuild": "^0.19.5",
14+
"typescript": "^5.2.2",
15+
},
16+
},
17+
"test": {
18+
"name": "test",
19+
"dependencies": {
20+
"postgres": "^3.4.5",
21+
},
22+
},
23+
},
24+
"packages": {
25+
"@bufbuild/protobuf": ["@bufbuild/[email protected]", "", {}, "sha512-JyEH8Z+OD5Sc2opSg86qMHn1EM1Sa+zj/Tc0ovxdwk56ByVNONJSabuCUbLQp+eKN3rWNfrho0X+3SEqEPXIow=="],
26+
27+
"@esbuild/android-arm": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-bhvbzWFF3CwMs5tbjf3ObfGqbl/17ict2/uwOSfr3wmxDE6VdS2GqY/FuzIPe0q0bdhj65zQsvqfArI9MY6+AA=="],
28+
29+
"@esbuild/android-arm64": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-5d1OkoJxnYQfmC+Zd8NBFjkhyCNYwM4n9ODrycTFY6Jk1IGiZ+tjVJDDSwDt77nK+tfpGP4T50iMtVi4dEGzhQ=="],
30+
31+
"@esbuild/android-x64": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "x64" }, "sha512-9t+28jHGL7uBdkBjL90QFxe7DVA+KGqWlHCF8ChTKyaKO//VLuoBricQCgwhOjA1/qOczsw843Fy4cbs4H3DVA=="],
32+
33+
"@esbuild/darwin-arm64": ["@esbuild/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mvXGcKqqIqyKoxq26qEDPHJuBYUA5KizJncKOAf9eJQez+L9O+KfvNFu6nl7SCZ/gFb2QPaRqqmG0doSWlgkqw=="],
34+
35+
"@esbuild/darwin-x64": ["@esbuild/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ly8cn6fGLNet19s0X4unjcniX24I0RqjPv+kurpXabZYSXGM4Pwpmf85WHJN3lAgB8GSth7s5A0r856S+4DyiA=="],
36+
37+
"@esbuild/freebsd-arm64": ["@esbuild/[email protected]", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-GGDNnPWTmWE+DMchq1W8Sd0mUkL+APvJg3b11klSGUDvRXh70JqLAO56tubmq1s2cgpVCSKYywEiKBfju8JztQ=="],
38+
39+
"@esbuild/freebsd-x64": ["@esbuild/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1CCwDHnSSoA0HNwdfoNY0jLfJpd7ygaLAp5EHFos3VWJCRX9DMwWODf96s9TSse39Br7oOTLryRVmBoFwXbuuQ=="],
40+
41+
"@esbuild/linux-arm": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-lrWXLY/vJBzCPC51QN0HM71uWgIEpGSjSZZADQhq7DKhPcI6NH1IdzjfHkDQws2oNpJKpR13kv7/pFHBbDQDwQ=="],
42+
43+
"@esbuild/linux-arm64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-o3vYippBmSrjjQUCEEiTZ2l+4yC0pVJD/Dl57WfPwwlvFkrxoSO7rmBZFii6kQB3Wrn/6GwJUPLU5t52eq2meA=="],
44+
45+
"@esbuild/linux-ia32": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "ia32" }, "sha512-MkjHXS03AXAkNp1KKkhSKPOCYztRtK+KXDNkBa6P78F8Bw0ynknCSClO/ztGszILZtyO/lVKpa7MolbBZ6oJtQ=="],
46+
47+
"@esbuild/linux-loong64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-42GwZMm5oYOD/JHqHska3Jg0r+XFb/fdZRX+WjADm3nLWLcIsN27YKtqxzQmGNJgu0AyXg4HtcSK9HuOk3v1Dw=="],
48+
49+
"@esbuild/linux-mips64el": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-kcjndCSMitUuPJobWCnwQ9lLjiLZUR3QLQmlgaBfMX23UEa7ZOrtufnRds+6WZtIS9HdTXqND4yH8NLoVVIkcg=="],
50+
51+
"@esbuild/linux-ppc64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "ppc64" }, "sha512-yJAxJfHVm0ZbsiljbtFFP1BQKLc8kUF6+17tjQ78QjqjAQDnhULWiTA6u0FCDmYT1oOKS9PzZ2z0aBI+Mcyj7Q=="],
52+
53+
"@esbuild/linux-riscv64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-5u8cIR/t3gaD6ad3wNt1MNRstAZO+aNyBxu2We8X31bA8XUNyamTVQwLDA1SLoPCUehNCymhBhK3Qim1433Zag=="],
54+
55+
"@esbuild/linux-s390x": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "s390x" }, "sha512-Z6JrMyEw/EmZBD/OFEFpb+gao9xJ59ATsoTNlj39jVBbXqoZm4Xntu6wVmGPB/OATi1uk/DB+yeDPv2E8PqZGw=="],
56+
57+
"@esbuild/linux-x64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-psagl+2RlK1z8zWZOmVdImisMtrUxvwereIdyJTmtmHahJTKb64pAcqoPlx6CewPdvGvUKe2Jw+0Z/0qhSbG1A=="],
58+
59+
"@esbuild/netbsd-x64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "x64" }, "sha512-kL2l+xScnAy/E/3119OggX8SrWyBEcqAh8aOY1gr4gPvw76la2GlD4Ymf832UCVbmuWeTf2adkZDK+h0Z/fB4g=="],
60+
61+
"@esbuild/openbsd-x64": ["@esbuild/[email protected]", "", { "os": "openbsd", "cpu": "x64" }, "sha512-sPOfhtzFufQfTBgRnE1DIJjzsXukKSvZxloZbkJDG383q0awVAq600pc1nfqBcl0ice/WN9p4qLc39WhBShRTA=="],
62+
63+
"@esbuild/sunos-x64": ["@esbuild/[email protected]", "", { "os": "sunos", "cpu": "x64" }, "sha512-dGZkBXaafuKLpDSjKcB0ax0FL36YXCvJNnztjKV+6CO82tTYVDSH2lifitJ29jxRMoUhgkg9a+VA/B03WK5lcg=="],
64+
65+
"@esbuild/win32-arm64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-dWVjD9y03ilhdRQ6Xig1NWNgfLtf2o/STKTS+eZuF90fI2BhbwD6WlaiCGKptlqXlURVB5AUOxUj09LuwKGDTg=="],
66+
67+
"@esbuild/win32-ia32": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "ia32" }, "sha512-4liggWIA4oDgUxqpZwrDhmEfAH4d0iljanDOK7AnVU89T6CzHon/ony8C5LeOdfgx60x5cnQJFZwEydVlYx4iw=="],
68+
69+
"@esbuild/win32-x64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-czTrygUsB/jlM8qEW5MD8bgYU2Xg14lo6kBDXW6HdxKjh8M5PzETGiSHaz9MtbXBYDloHNUAUW2tMiKW4KM9Mw=="],
70+
71+
"@types/inflected": ["@types/[email protected]", "", {}, "sha512-qEllJ4fo4Cn8sPu/6+2Iw6ouxcFuQIfj3PDRO8cvzvUaJ5udD2IGwFm6xrzOQSJm4MzXRcvZkR3rqwWxvxP8eQ=="],
72+
73+
"esbuild": ["[email protected]", "", { "optionalDependencies": { "@esbuild/android-arm": "0.19.5", "@esbuild/android-arm64": "0.19.5", "@esbuild/android-x64": "0.19.5", "@esbuild/darwin-arm64": "0.19.5", "@esbuild/darwin-x64": "0.19.5", "@esbuild/freebsd-arm64": "0.19.5", "@esbuild/freebsd-x64": "0.19.5", "@esbuild/linux-arm": "0.19.5", "@esbuild/linux-arm64": "0.19.5", "@esbuild/linux-ia32": "0.19.5", "@esbuild/linux-loong64": "0.19.5", "@esbuild/linux-mips64el": "0.19.5", "@esbuild/linux-ppc64": "0.19.5", "@esbuild/linux-riscv64": "0.19.5", "@esbuild/linux-s390x": "0.19.5", "@esbuild/linux-x64": "0.19.5", "@esbuild/netbsd-x64": "0.19.5", "@esbuild/openbsd-x64": "0.19.5", "@esbuild/sunos-x64": "0.19.5", "@esbuild/win32-arm64": "0.19.5", "@esbuild/win32-ia32": "0.19.5", "@esbuild/win32-x64": "0.19.5" }, "bin": "bin/esbuild" }, "sha512-bUxalY7b1g8vNhQKdB24QDmHeY4V4tw/s6Ak5z+jJX9laP5MoQseTOMemAr0gxssjNcH0MCViG8ONI2kksvfFQ=="],
74+
75+
"inflected": ["[email protected]", "", {}, "sha512-hAEKNxvHf2Iq3H60oMBHkB4wl5jn3TPF3+fXek/sRwAB5gP9xWs4r7aweSF95f99HFoz69pnZTcu8f0SIHV18w=="],
76+
77+
"javy": ["[email protected]", "", {}, "sha512-z6Z+CV13SXBGxmY5UseWsYNVVR4SWPfOixKGluWi1Dpn594+M2JaGPBrFIIMnFFfi7kJzHCkcTn7CrhJ7JGKsA=="],
78+
79+
"postgres": ["[email protected]", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="],
80+
81+
"test": ["test@workspace:test"],
82+
83+
"typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w=="],
84+
}
85+
}

examples/bun-postgres/src/db/query1_sql.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ WHERE id = $1 LIMIT 1`;
1212
export interface GetAuthorRow {
1313
id: number;
1414
name: string;
15-
bio: string | null;
15+
bio: string | undefined;
1616
}
1717
export async function getAuthor(sql: Sql, args: GetAuthorArgs): Promise<GetAuthorRow | null> {
1818
const rows = await sql.unsafe(getAuthorQuery, [args.id]).values();
@@ -26,7 +26,7 @@ WHERE id = $1 LIMIT 1`;
2626
return {
2727
id: Number(row[0]),
2828
name: row[1],
29-
bio: row[2]
29+
bio: row[2] ?? undefined
3030
};
3131
}
3232
const listAuthorsQuery = `-- name: Read_ListAuthors :many
@@ -35,13 +35,13 @@ ORDER BY name`;
3535
export interface ListAuthorsRow {
3636
id: number;
3737
name: string;
38-
bio: string | null;
38+
bio: string | undefined;
3939
}
4040
export async function listAuthors(sql: Sql): Promise<ListAuthorsRow[]> {
4141
return (await sql.unsafe(listAuthorsQuery, []).values()).map(row => ({
4242
id: Number(row[0]),
4343
name: row[1],
44-
bio: row[2]
44+
bio: row[2] ?? undefined
4545
}));
4646
}
4747
const getNameByIdQuery = `-- name: GetNameById :one
@@ -81,13 +81,13 @@ ORDER BY name`;
8181
export interface ListRow {
8282
id: number;
8383
name: string;
84-
bio: string | null;
84+
bio: string | undefined;
8585
}
8686
export async function list(sql: Sql): Promise<ListRow[]> {
8787
return (await sql.unsafe(listQuery, []).values()).map(row => ({
8888
id: Number(row[0]),
8989
name: row[1],
90-
bio: row[2]
90+
bio: row[2] ?? undefined
9191
}));
9292
}
9393
}

examples/bun-postgres/src/db/query2_sql.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ INSERT INTO authors (
2121
RETURNING id, name, bio`;
2222
export interface CreateAuthorArgs {
2323
name: string;
24-
bio: string | null;
24+
bio: string | undefined;
2525
}
2626
export interface CreateAuthorRow {
2727
id: number;
2828
name: string;
29-
bio: string | null;
29+
bio: string | undefined;
3030
}
3131
export async function createAuthor(sql: Sql, args: CreateAuthorArgs): Promise<CreateAuthorRow | null> {
3232
const rows = await sql.unsafe(createAuthorQuery, [args.name, args.bio]).values();
@@ -40,7 +40,7 @@ RETURNING id, name, bio`;
4040
return {
4141
id: Number(row[0]),
4242
name: row[1],
43-
bio: row[2]
43+
bio: row[2] ?? undefined
4444
};
4545
}
4646
}

examples/node-postgres/src/db/query1_sql.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ WHERE id = $1 LIMIT 1`;
1212
export interface GetAuthorRow {
1313
id: number;
1414
name: string;
15-
bio: string | null;
15+
bio: string | undefined;
1616
}
1717
export async function getAuthor(sql: Sql, args: GetAuthorArgs): Promise<GetAuthorRow | null> {
1818
const rows = await sql.unsafe(getAuthorQuery, [args.id]).values();
@@ -26,7 +26,7 @@ WHERE id = $1 LIMIT 1`;
2626
return {
2727
id: Number(row[0]),
2828
name: row[1],
29-
bio: row[2]
29+
bio: row[2] ?? undefined
3030
};
3131
}
3232
const listAuthorsQuery = `-- name: Read_ListAuthors :many
@@ -35,13 +35,13 @@ ORDER BY name`;
3535
export interface ListAuthorsRow {
3636
id: number;
3737
name: string;
38-
bio: string | null;
38+
bio: string | undefined;
3939
}
4040
export async function listAuthors(sql: Sql): Promise<ListAuthorsRow[]> {
4141
return (await sql.unsafe(listAuthorsQuery, []).values()).map(row => ({
4242
id: Number(row[0]),
4343
name: row[1],
44-
bio: row[2]
44+
bio: row[2] ?? undefined
4545
}));
4646
}
4747
const getNameByIdQuery = `-- name: GetNameById :one
@@ -81,13 +81,13 @@ ORDER BY name`;
8181
export interface ListRow {
8282
id: number;
8383
name: string;
84-
bio: string | null;
84+
bio: string | undefined;
8585
}
8686
export async function list(sql: Sql): Promise<ListRow[]> {
8787
return (await sql.unsafe(listQuery, []).values()).map(row => ({
8888
id: Number(row[0]),
8989
name: row[1],
90-
bio: row[2]
90+
bio: row[2] ?? undefined
9191
}));
9292
}
9393
}

examples/node-postgres/src/db/query2_sql.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ INSERT INTO authors (
2121
RETURNING id, name, bio`;
2222
export interface CreateAuthorArgs {
2323
name: string;
24-
bio: string | null;
24+
bio: string | undefined;
2525
}
2626
export interface CreateAuthorRow {
2727
id: number;
2828
name: string;
29-
bio: string | null;
29+
bio: string | undefined;
3030
}
3131
export async function createAuthor(sql: Sql, args: CreateAuthorArgs): Promise<CreateAuthorRow | null> {
3232
const rows = await sql.unsafe(createAuthorQuery, [args.name, args.bio]).values();
@@ -40,7 +40,7 @@ RETURNING id, name, bio`;
4040
return {
4141
id: Number(row[0]),
4242
name: row[1],
43-
bio: row[2]
43+
bio: row[2] ?? undefined
4444
};
4545
}
4646
}

src/app.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import ts, {
1616
createPrinter,
1717
createSourceFile,
1818
factory,
19-
addSyntheticLeadingComment,
2019
} from "typescript";
2120

2221
import {
@@ -190,12 +189,6 @@ ${query.text.trim()}`,
190189
if (query.columns.length === 1) {
191190
const col = query.columns[0];
192191
returnIface = driver.parseDatabaseType(col.type?.name ?? "");
193-
addSyntheticLeadingComment(
194-
nodesToPush.slice(-1)[0],
195-
SyntaxKind.MultiLineCommentTrivia,
196-
`${col.type.name}, ${col.arrayDims}: ${returnIface}`,
197-
true,
198-
);
199192
if (col.isArray || col.type?.name === "anyarray") {
200193
returnIface += "[]";
201194
}

src/drivers/postgres.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ export class Driver {
177177
}
178178
return factory.createUnionTypeNode([
179179
keyword,
180-
factory.createLiteralTypeNode(factory.createNull()),
180+
factory.createKeywordTypeNode(SyntaxKind.UndefinedKeyword),
181181
]);
182182
}
183183

@@ -487,14 +487,18 @@ export class Driver {
487487
: colName(i, col),
488488
),
489489
!embeds.has(col.name)
490-
? this.buildColumnAccessExpression(col, "row", i)
490+
? col.notNull
491+
? this.buildColumnAccessExpression(col, "row", i)
492+
: this.buildNullableColumnExpression(col, "row", i)
491493
: factory.createObjectLiteralExpression(
492494
embeds
493495
.get(col.name)
494496
?.map((embed, j) =>
495497
factory.createPropertyAssignment(
496498
factory.createIdentifier(colName(j, embed)),
497-
this.buildColumnAccessExpression(embed, "row", i + j),
499+
embed.notNull
500+
? this.buildColumnAccessExpression(embed, "row", i + j)
501+
: this.buildNullableColumnExpression(embed, "row", i + j),
498502
),
499503
),
500504
),
@@ -536,6 +540,41 @@ export class Driver {
536540
return expr?.expression ?? access;
537541
}
538542

543+
buildNullableColumnExpression(
544+
column: Column,
545+
name: string,
546+
i: number,
547+
): Expression {
548+
const access = factory.createElementAccessExpression(
549+
factory.createIdentifier(name),
550+
factory.createNumericLiteral(i),
551+
);
552+
553+
const type = this.parseDatabaseType(column.type?.name ?? "");
554+
555+
// For Date types, we need to check if the value exists before creating a Date object
556+
if (type === "Date") {
557+
return factory.createConditionalExpression(
558+
access,
559+
factory.createToken(SyntaxKind.QuestionToken),
560+
factory.createNewExpression(
561+
factory.createIdentifier("Date"),
562+
undefined,
563+
[access],
564+
),
565+
factory.createToken(SyntaxKind.ColonToken),
566+
factory.createIdentifier("undefined"),
567+
);
568+
}
569+
570+
// For other types, use the original logic with nullish coalescing
571+
return factory.createBinaryExpression(
572+
this.buildColumnAccessExpression(column, name, i),
573+
factory.createToken(SyntaxKind.QuestionQuestionToken),
574+
factory.createIdentifier("undefined"),
575+
);
576+
}
577+
539578
execlastidDecl(
540579
funcName: string,
541580
queryName: string,

test/cases/aggregate.test.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,15 @@ await prepare(sql`
1212
1313
-- name: AggEmailsWithoutCastMany :many
1414
SELECT 1 as foo, array_agg(e.id) AS emails FROM emails e;
15+
16+
-- name: AggEmailsGroup :many
17+
SELECT e.*, agg_distinct(o.id) AS email_ids FROM emails e
18+
JOIN orgs o ON e.id = o.email_id
19+
GROUP BY e.id;
1520
`);
1621

1722
describe("aggregation", () => {
18-
it("returns an array type in One mode", async () => {
23+
it("returns an array type for :one", async () => {
1924
const result = await (await import("./aggregate_sql")).aggEmailsOne(db);
2025

2126
result as string[];
@@ -24,7 +29,7 @@ describe("aggregation", () => {
2429
expect(Array.isArray(result)).toBe(true);
2530
});
2631

27-
it("returns an array type in Many mode", async () => {
32+
it("returns an array type for :many", async () => {
2833
const result = await (await import("./aggregate_sql")).aggEmailsMany(db);
2934

3035
result[0].emails as string[];
@@ -33,7 +38,7 @@ describe("aggregation", () => {
3338
expect(Array.isArray(result)).toBe(true);
3439
});
3540

36-
it("returns an array type in a query without type cast in One mode", async () => {
41+
it("returns an array type in a query without type cast for :one", async () => {
3742
const result = await (
3843
await import("./aggregate_sql")
3944
).aggEmailsWithoutCastOne(db);
@@ -44,14 +49,9 @@ describe("aggregation", () => {
4449
expect(Array.isArray(result)).toBe(true);
4550
});
4651

47-
it("returns an array type in a query without type cast in Many mode", async () => {
48-
const result = await (
49-
await import("./aggregate_sql")
50-
).aggEmailsWithoutCastMany(db);
52+
it("returns an array type in a query without type cast for :many", async () => {
53+
const result = await (await import("./aggregate_sql")).aggEmailsGroup(db);
5154

52-
result[0].emails as string[];
53-
54-
expect(result[0].emails).toHaveLength(3);
55-
expect(Array.isArray(result)).toBe(true);
55+
result[0].emailIds as string[];
5656
});
5757
});

0 commit comments

Comments
 (0)