Skip to content

Commit 0785139

Browse files
committed
fix(policy): run pg/sqlite tests, misc dual db compatibility fixes
1 parent dcfa6c3 commit 0785139

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+2757
-2786
lines changed

.github/workflows/build-test.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ jobs:
3434
strategy:
3535
matrix:
3636
node-version: [20.x]
37+
provider: [sqlite, postgresql]
3738

3839
steps:
3940
- name: Checkout
@@ -76,4 +77,6 @@ jobs:
7677
run: pnpm run lint
7778

7879
- name: Test
80+
env:
81+
TEST_DB_PROVIDER: ${{ matrix.provider }}
7982
run: pnpm run test

packages/common-helpers/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from './is-plain-object';
22
export * from './lower-case-first';
33
export * from './param-case';
4+
export * from './promise-utils';
45
export * from './sleep';
56
export * from './tiny-invariant';
67
export * from './upper-case-first';
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* Utility to run promises sequentially.
3+
*/
4+
export async function sequential<T>(tasks: Promise<T>[]): Promise<T[]> {
5+
const results: T[] = [];
6+
for (const task of tasks) {
7+
results.push(await task);
8+
}
9+
return Promise.resolve(results);
10+
}

packages/runtime/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
"watch": "tsup-node --watch",
99
"lint": "eslint src --ext ts",
1010
"test": "vitest run && pnpm test:typecheck",
11+
"test:sqlite": "TEST_DB_PROVIDER=sqlite vitest run",
12+
"test:postgresql": "TEST_DB_PROVIDER=postgresql vitest run",
1113
"test:generate": "tsx test/scripts/generate.ts",
1214
"test:typecheck": "tsc --project tsconfig.test.json",
1315
"pack": "pnpm pack"

packages/runtime/src/client/client-impl.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export class ClientImpl<Schema extends SchemaDef> {
6161
executor?: QueryExecutor,
6262
) {
6363
this.$schema = schema;
64-
this.$options = options ?? ({} as ClientOptions<Schema>);
64+
this.$options = options;
6565

6666
this.$options.functions = {
6767
...BuiltinFunctions,
@@ -326,7 +326,7 @@ export class ClientImpl<Schema extends SchemaDef> {
326326

327327
function createClientProxy<Schema extends SchemaDef>(client: ClientImpl<Schema>): ClientImpl<Schema> {
328328
const inputValidator = new InputValidator(client.$schema);
329-
const resultProcessor = new ResultProcessor(client.$schema);
329+
const resultProcessor = new ResultProcessor(client.$schema, client.$options);
330330

331331
return new Proxy(client, {
332332
get: (target, prop, receiver) => {

packages/runtime/src/client/crud/dialects/base-dialect.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
4545
return value;
4646
}
4747

48+
transformOutput(value: unknown, _type: BuiltinType) {
49+
return value;
50+
}
51+
4852
// #region common query builders
4953

5054
buildSelectModel(eb: ExpressionBuilder<any, any>, model: string, modelAlias: string) {
@@ -1255,5 +1259,10 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
12551259
*/
12561260
abstract get supportInsertWithDefault(): boolean;
12571261

1262+
/**
1263+
* Gets the SQL column type for the given field definition.
1264+
*/
1265+
abstract getFieldSqlType(fieldDef: FieldDef): string;
1266+
12581267
// #endregion
12591268
}

packages/runtime/src/client/crud/dialects/postgresql.ts

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { invariant } from '@zenstackhq/common-helpers';
2+
import Decimal from 'decimal.js';
23
import {
34
sql,
45
type Expression,
@@ -11,6 +12,8 @@ import { match } from 'ts-pattern';
1112
import type { BuiltinType, FieldDef, GetModels, SchemaDef } from '../../../schema';
1213
import { DELEGATE_JOINED_FIELD_PREFIX } from '../../constants';
1314
import type { FindArgs } from '../../crud-types';
15+
import { QueryError } from '../../errors';
16+
import type { ClientOptions } from '../../options';
1417
import {
1518
buildJoinPairs,
1619
getDelegateDescendantModels,
@@ -23,6 +26,10 @@ import {
2326
import { BaseCrudDialect } from './base-dialect';
2427

2528
export class PostgresCrudDialect<Schema extends SchemaDef> extends BaseCrudDialect<Schema> {
29+
constructor(schema: Schema, options: ClientOptions<Schema>) {
30+
super(schema, options);
31+
}
32+
2633
override get provider() {
2734
return 'postgresql' as const;
2835
}
@@ -44,13 +51,63 @@ export class PostgresCrudDialect<Schema extends SchemaDef> extends BaseCrudDiale
4451
} else {
4552
return match(type)
4653
.with('DateTime', () =>
47-
value instanceof Date ? value : typeof value === 'string' ? new Date(value) : value,
54+
value instanceof Date
55+
? value.toISOString()
56+
: typeof value === 'string'
57+
? new Date(value).toISOString()
58+
: value,
4859
)
4960
.with('Decimal', () => (value !== null ? value.toString() : value))
5061
.otherwise(() => value);
5162
}
5263
}
5364

65+
override transformOutput(value: unknown, type: BuiltinType) {
66+
return match(type)
67+
.with('DateTime', () => this.transformOutputDate(value))
68+
.with('Bytes', () => this.transformOutputBytes(value))
69+
.with('BigInt', () => this.transformOutputBigInt(value))
70+
.with('Decimal', () => this.transformDecimal(value))
71+
.otherwise(() => super.transformOutput(value, type));
72+
}
73+
74+
private transformOutputBigInt(value: unknown) {
75+
if (typeof value === 'bigint') {
76+
return value;
77+
}
78+
invariant(
79+
typeof value === 'string' || typeof value === 'number',
80+
`Expected string or number, got ${typeof value}`,
81+
);
82+
return BigInt(value);
83+
}
84+
85+
private transformDecimal(value: unknown) {
86+
if (value instanceof Decimal) {
87+
return value;
88+
}
89+
invariant(
90+
typeof value === 'string' || typeof value === 'number' || value instanceof Decimal,
91+
`Expected string, number or Decimal, got ${typeof value}`,
92+
);
93+
return new Decimal(value);
94+
}
95+
96+
private transformOutputDate(value: unknown) {
97+
if (typeof value === 'string') {
98+
return new Date(value);
99+
} else if (value instanceof Date) {
100+
value.setTime(value.getTime() - value.getTimezoneOffset() * 60 * 1000);
101+
return value;
102+
} else {
103+
return value;
104+
}
105+
}
106+
107+
private transformOutputBytes(value: unknown) {
108+
return Buffer.isBuffer(value) ? Uint8Array.from(value) : value;
109+
}
110+
54111
override buildRelationSelection(
55112
query: SelectQueryBuilder<any, any, any>,
56113
model: string,
@@ -370,4 +427,37 @@ export class PostgresCrudDialect<Schema extends SchemaDef> extends BaseCrudDiale
370427
override get supportInsertWithDefault() {
371428
return true;
372429
}
430+
431+
override getFieldSqlType(fieldDef: FieldDef) {
432+
// TODO: respect `@db.x` attributes
433+
if (fieldDef.relation) {
434+
throw new QueryError('Cannot get SQL type of a relation field');
435+
}
436+
437+
let result: string;
438+
439+
if (this.schema.enums?.[fieldDef.type]) {
440+
// enums are treated as text
441+
result = 'text';
442+
} else {
443+
result = match(fieldDef.type)
444+
.with('String', () => 'text')
445+
.with('Boolean', () => 'boolean')
446+
.with('Int', () => 'integer')
447+
.with('BigInt', () => 'bigint')
448+
.with('Float', () => 'double precision')
449+
.with('Decimal', () => 'decimal')
450+
.with('DateTime', () => 'timestamp')
451+
.with('Bytes', () => 'bytea')
452+
.with('Json', () => 'jsonb')
453+
// fallback to text
454+
.otherwise(() => 'text');
455+
}
456+
457+
if (fieldDef.array) {
458+
result += '[]';
459+
}
460+
461+
return result;
462+
}
373463
}

packages/runtime/src/client/crud/dialects/sqlite.ts

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { invariant } from '@zenstackhq/common-helpers';
2-
import type Decimal from 'decimal.js';
2+
import Decimal from 'decimal.js';
33
import {
44
ExpressionWrapper,
55
sql,
@@ -9,9 +9,10 @@ import {
99
type SelectQueryBuilder,
1010
} from 'kysely';
1111
import { match } from 'ts-pattern';
12-
import type { BuiltinType, GetModels, SchemaDef } from '../../../schema';
12+
import type { BuiltinType, FieldDef, GetModels, SchemaDef } from '../../../schema';
1313
import { DELEGATE_JOINED_FIELD_PREFIX } from '../../constants';
1414
import type { FindArgs } from '../../crud-types';
15+
import { QueryError } from '../../errors';
1516
import {
1617
getDelegateDescendantModels,
1718
getManyToManyRelation,
@@ -41,7 +42,13 @@ export class SqliteCrudDialect<Schema extends SchemaDef> extends BaseCrudDialect
4142
} else {
4243
return match(type)
4344
.with('Boolean', () => (value ? 1 : 0))
44-
.with('DateTime', () => (value instanceof Date ? value.toISOString() : value))
45+
.with('DateTime', () =>
46+
value instanceof Date
47+
? value.toISOString()
48+
: typeof value === 'string'
49+
? new Date(value).toISOString()
50+
: value,
51+
)
4552
.with('Decimal', () => (value as Decimal).toString())
4653
.with('Bytes', () => Buffer.from(value as Uint8Array))
4754
.with('Json', () => JSON.stringify(value))
@@ -50,6 +57,68 @@ export class SqliteCrudDialect<Schema extends SchemaDef> extends BaseCrudDialect
5057
}
5158
}
5259

60+
override transformOutput(value: unknown, type: BuiltinType) {
61+
if (this.schema.typeDefs && type in this.schema.typeDefs) {
62+
// typed JSON field
63+
return this.transformOutputJson(value);
64+
} else {
65+
return match(type)
66+
.with('Boolean', () => this.transformOutputBoolean(value))
67+
.with('DateTime', () => this.transformOutputDate(value))
68+
.with('Bytes', () => this.transformOutputBytes(value))
69+
.with('Decimal', () => this.transformOutputDecimal(value))
70+
.with('BigInt', () => this.transformOutputBigInt(value))
71+
.with('Json', () => this.transformOutputJson(value))
72+
.otherwise(() => super.transformOutput(value, type));
73+
}
74+
}
75+
76+
private transformOutputDecimal(value: unknown) {
77+
if (value instanceof Decimal) {
78+
return value;
79+
}
80+
invariant(
81+
typeof value === 'string' || typeof value === 'number' || value instanceof Decimal,
82+
`Expected string, number or Decimal, got ${typeof value}`,
83+
);
84+
return new Decimal(value);
85+
}
86+
87+
private transformOutputBigInt(value: unknown) {
88+
if (typeof value === 'bigint') {
89+
return value;
90+
}
91+
invariant(
92+
typeof value === 'string' || typeof value === 'number',
93+
`Expected string or number, got ${typeof value}`,
94+
);
95+
return BigInt(value);
96+
}
97+
98+
private transformOutputBoolean(value: unknown) {
99+
return !!value;
100+
}
101+
102+
private transformOutputDate(value: unknown) {
103+
if (typeof value === 'number') {
104+
return new Date(value);
105+
} else if (typeof value === 'string') {
106+
return new Date(value);
107+
} else {
108+
return value;
109+
}
110+
}
111+
112+
private transformOutputBytes(value: unknown) {
113+
return Buffer.isBuffer(value) ? Uint8Array.from(value) : value;
114+
}
115+
116+
private transformOutputJson(value: unknown) {
117+
// better-sqlite3 returns JSON as string
118+
invariant(typeof value === 'string', 'Expected string, got ' + typeof value);
119+
return JSON.parse(value as string);
120+
}
121+
53122
override buildRelationSelection(
54123
query: SelectQueryBuilder<any, any, any>,
55124
model: string,
@@ -301,4 +370,34 @@ export class SqliteCrudDialect<Schema extends SchemaDef> extends BaseCrudDialect
301370
override get supportInsertWithDefault() {
302371
return false;
303372
}
373+
374+
override getFieldSqlType(fieldDef: FieldDef) {
375+
// TODO: respect `@db.x` attributes
376+
if (fieldDef.relation) {
377+
throw new QueryError('Cannot get SQL type of a relation field');
378+
}
379+
if (fieldDef.array) {
380+
throw new QueryError('SQLite does not support scalar list type');
381+
}
382+
383+
if (this.schema.enums?.[fieldDef.type]) {
384+
// enums are stored as text
385+
return 'text';
386+
}
387+
388+
return (
389+
match(fieldDef.type)
390+
.with('String', () => 'text')
391+
.with('Boolean', () => 'integer')
392+
.with('Int', () => 'integer')
393+
.with('BigInt', () => 'integer')
394+
.with('Float', () => 'real')
395+
.with('Decimal', () => 'decimal')
396+
.with('DateTime', () => 'numeric')
397+
.with('Bytes', () => 'blob')
398+
.with('Json', () => 'jsonb')
399+
// fallback to text
400+
.otherwise(() => 'text')
401+
);
402+
}
304403
}

0 commit comments

Comments
 (0)