Skip to content

Commit 975cb7c

Browse files
committed
Convert bigint columns to number type with Number() conversion
Bun SQL returns bigint/int8/bigserial/serial8 as strings to avoid precision loss. For typical use cases (auto-incrementing IDs), JavaScript's number type (safe up to 2^53-1) is sufficient. Changes: - columnType() now returns 'number' for bigint types instead of 'string' - manyDecl() and oneDecl() wrap bigint column access with Number() - Nullable bigint columns use: row[i] === null ? null : Number(row[i]) - Added tests for Number() conversion behavior
1 parent a33a766 commit 975cb7c

File tree

2 files changed

+123
-13
lines changed

2 files changed

+123
-13
lines changed

src/drivers/bun-sql.codegen.test.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,68 @@ describe("bun-sql driver codegen", () => {
8282
expect(output).toContain("as GetThingRowValues[]");
8383
});
8484

85+
it("wraps bigint columns with Number() in :many", () => {
86+
const driver = new BunSqlDriver();
87+
88+
const params: Parameter[] = [];
89+
90+
const columns: Column[] = [
91+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
92+
({ name: "id", type: { name: "bigint" }, notNull: true } as unknown as Column),
93+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
94+
({ name: "name", type: { name: "text" }, notNull: true } as unknown as Column),
95+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
96+
({ name: "nullable_id", type: { name: "int8" }, notNull: false } as unknown as Column),
97+
];
98+
99+
const node = driver.manyDecl(
100+
"listThings",
101+
"listThingsQuery",
102+
undefined,
103+
"ListThingsRow",
104+
params,
105+
columns
106+
);
107+
108+
const output = print(node);
109+
110+
// Non-nullable bigint: Number(row[0])
111+
expect(output).toContain("id: Number(row[0])");
112+
// Regular text column: row[1] (no conversion)
113+
expect(output).toContain("name: row[1]");
114+
// Nullable bigint: row[2] === null ? null : Number(row[2])
115+
expect(output).toContain("nullableId: row[2] === null ? null : Number(row[2])");
116+
});
117+
118+
it("wraps bigint columns with Number() in :one", () => {
119+
const driver = new BunSqlDriver();
120+
121+
const params: Parameter[] = [];
122+
123+
const columns: Column[] = [
124+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
125+
({ name: "id", type: { name: "bigserial" }, notNull: true } as unknown as Column),
126+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
127+
({ name: "parent_id", type: { name: "serial8" }, notNull: false } as unknown as Column),
128+
];
129+
130+
const node = driver.oneDecl(
131+
"getThing",
132+
"getThingQuery",
133+
undefined,
134+
"GetThingRow",
135+
params,
136+
columns
137+
);
138+
139+
const output = print(node);
140+
141+
// Non-nullable bigserial: Number(row[0])
142+
expect(output).toContain("id: Number(row[0])");
143+
// Nullable serial8: row[1] === null ? null : Number(row[1])
144+
expect(output).toContain("parentId: row[1] === null ? null : Number(row[1])");
145+
});
146+
85147
it("maps common Postgres alias types", () => {
86148
const driver = new BunSqlDriver();
87149
const columns: Column[] = [
@@ -109,8 +171,9 @@ describe("bun-sql driver codegen", () => {
109171
const output = print(node);
110172

111173
expect(output).toContain("export type AliasRowValues");
174+
// bigint is now mapped to number (with runtime Number() conversion)
112175
expect(output).toMatch(
113-
/\[\s*number,\s*number,\s*string,\s*number,\s*number,\s*number,\s*Date,\s*Date,\s*string\s*\]/
176+
/\[\s*number,\s*number,\s*number,\s*number,\s*number,\s*number,\s*Date,\s*Date,\s*string\s*\]/
114177
);
115178
});
116179
});

src/drivers/bun-sql.ts

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,59 @@ import {
99
import { Parameter, Column } from "../gen/plugin/codegen_pb";
1010
import { argName, colName } from "./utlis";
1111

12+
// PostgreSQL types that Bun SQL returns as strings but we want as numbers.
13+
// These are 64-bit integers which Bun returns as strings to avoid precision loss,
14+
// but for typical use cases (auto-incrementing IDs), converting to JS number is safe.
15+
const BIGINT_TYPES = new Set(["int8", "bigint", "bigserial", "serial8"]);
16+
17+
function isBigIntType(column?: Column): boolean {
18+
if (!column?.type?.name) return false;
19+
let typeName = column.type.name;
20+
const pgCatalog = "pg_catalog.";
21+
if (typeName.startsWith(pgCatalog)) {
22+
typeName = typeName.slice(pgCatalog.length);
23+
}
24+
return BIGINT_TYPES.has(typeName.toLowerCase());
25+
}
26+
27+
// Creates an expression to access row[i], with Number() conversion for bigint types.
28+
// For nullable bigint columns: row[i] === null ? null : Number(row[i])
29+
// For non-nullable bigint columns: Number(row[i])
30+
// For other columns: row[i]
31+
function createRowAccessExpression(col: Column, index: number) {
32+
const rowAccess = factory.createElementAccessExpression(
33+
factory.createIdentifier("row"),
34+
factory.createNumericLiteral(`${index}`)
35+
);
36+
37+
if (!isBigIntType(col)) {
38+
return rowAccess;
39+
}
40+
41+
const numberCall = factory.createCallExpression(
42+
factory.createIdentifier("Number"),
43+
undefined,
44+
[rowAccess]
45+
);
46+
47+
if (col.notNull) {
48+
return numberCall;
49+
}
50+
51+
// row[i] === null ? null : Number(row[i])
52+
return factory.createConditionalExpression(
53+
factory.createBinaryExpression(
54+
rowAccess,
55+
factory.createToken(SyntaxKind.EqualsEqualsEqualsToken),
56+
factory.createNull()
57+
),
58+
factory.createToken(SyntaxKind.QuestionToken),
59+
factory.createNull(),
60+
factory.createToken(SyntaxKind.ColonToken),
61+
numberCall
62+
);
63+
}
64+
1265
function funcParamsDecl(iface: string | undefined, params: Parameter[]) {
1366
let funcParams = [
1467
factory.createParameterDeclaration(
@@ -64,7 +117,7 @@ export class Driver {
64117
break;
65118
}
66119
case "bigserial": {
67-
// string
120+
typ = factory.createKeywordTypeNode(SyntaxKind.NumberKeyword);
68121
break;
69122
}
70123
case "bit": {
@@ -159,11 +212,11 @@ export class Driver {
159212
break;
160213
}
161214
case "int8": {
162-
// string
215+
typ = factory.createKeywordTypeNode(SyntaxKind.NumberKeyword);
163216
break;
164217
}
165218
case "bigint": {
166-
// string
219+
typ = factory.createKeywordTypeNode(SyntaxKind.NumberKeyword);
167220
break;
168221
}
169222
case "interval": {
@@ -251,7 +304,7 @@ export class Driver {
251304
break;
252305
}
253306
case "serial8": {
254-
// string
307+
typ = factory.createKeywordTypeNode(SyntaxKind.NumberKeyword);
255308
break;
256309
}
257310
case "smallserial": {
@@ -526,10 +579,7 @@ export class Driver {
526579
columns.map((col, i) =>
527580
factory.createPropertyAssignment(
528581
factory.createIdentifier(colName(i, col)),
529-
factory.createElementAccessExpression(
530-
factory.createIdentifier("row"),
531-
factory.createNumericLiteral(`${i}`)
532-
)
582+
createRowAccessExpression(col, i)
533583
)
534584
),
535585
true
@@ -678,10 +728,7 @@ export class Driver {
678728
columns.map((col, i) =>
679729
factory.createPropertyAssignment(
680730
factory.createIdentifier(colName(i, col)),
681-
factory.createElementAccessExpression(
682-
factory.createIdentifier("row"),
683-
factory.createNumericLiteral(`${i}`)
684-
)
731+
createRowAccessExpression(col, i)
685732
)
686733
),
687734
true

0 commit comments

Comments
 (0)