@fragno-dev/db 0.1.11 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +30 -28
- package/CHANGELOG.md +13 -0
- package/dist/adapters/drizzle/drizzle-query.d.ts.map +1 -1
- package/dist/adapters/drizzle/drizzle-query.js +38 -34
- package/dist/adapters/drizzle/drizzle-query.js.map +1 -1
- package/dist/adapters/kysely/kysely-adapter.d.ts +3 -2
- package/dist/adapters/kysely/kysely-adapter.d.ts.map +1 -1
- package/dist/adapters/kysely/kysely-adapter.js.map +1 -1
- package/dist/adapters/kysely/kysely-query.d.ts +22 -0
- package/dist/adapters/kysely/kysely-query.d.ts.map +1 -0
- package/dist/adapters/kysely/kysely-query.js +72 -50
- package/dist/adapters/kysely/kysely-query.js.map +1 -1
- package/dist/adapters/kysely/kysely-uow-executor.js +2 -2
- package/dist/adapters/kysely/kysely-uow-executor.js.map +1 -1
- package/dist/migration-engine/generation-engine.d.ts +1 -1
- package/dist/migration-engine/generation-engine.d.ts.map +1 -1
- package/dist/migration-engine/generation-engine.js.map +1 -1
- package/dist/mod.d.ts +5 -5
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js.map +1 -1
- package/dist/query/query.d.ts +24 -8
- package/dist/query/query.d.ts.map +1 -1
- package/dist/query/result-transform.js +17 -5
- package/dist/query/result-transform.js.map +1 -1
- package/dist/query/unit-of-work.d.ts +5 -4
- package/dist/query/unit-of-work.d.ts.map +1 -1
- package/dist/query/unit-of-work.js +2 -3
- package/dist/query/unit-of-work.js.map +1 -1
- package/dist/schema/serialize.js +2 -0
- package/dist/schema/serialize.js.map +1 -1
- package/package.json +2 -2
- package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +170 -50
- package/src/adapters/drizzle/drizzle-adapter-sqlite.test.ts +89 -35
- package/src/adapters/drizzle/drizzle-query.test.ts +54 -4
- package/src/adapters/drizzle/drizzle-query.ts +65 -60
- package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +63 -3
- package/src/adapters/kysely/kysely-adapter-pglite.test.ts +88 -0
- package/src/adapters/kysely/kysely-adapter.ts +6 -3
- package/src/adapters/kysely/kysely-query.test.ts +498 -0
- package/src/adapters/kysely/kysely-query.ts +137 -82
- package/src/adapters/kysely/kysely-uow-compiler.test.ts +66 -0
- package/src/adapters/kysely/kysely-uow-executor.ts +5 -9
- package/src/migration-engine/generation-engine.ts +2 -1
- package/src/mod.ts +6 -6
- package/src/query/query-type.test.ts +34 -14
- package/src/query/query.ts +77 -36
- package/src/query/result-transform.test.ts +5 -5
- package/src/query/result-transform.ts +29 -11
- package/src/query/unit-of-work.ts +8 -11
- package/src/schema/serialize.test.ts +223 -0
- package/src/schema/serialize.ts +16 -0
package/src/query/query.ts
CHANGED
|
@@ -17,7 +17,7 @@ export type RawColumnValues<T extends AnyTable> = {
|
|
|
17
17
|
[K in keyof T["columns"] as string extends K ? never : K]: T["columns"][K]["$out"];
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
export type TableToColumnValues<T extends AnyTable> = RawColumnValues<T
|
|
20
|
+
export type TableToColumnValues<T extends AnyTable> = Prettify<RawColumnValues<T>>;
|
|
21
21
|
|
|
22
22
|
type PickNullable<T> = {
|
|
23
23
|
[P in keyof T as null extends T[P] ? P : never]: T[P];
|
|
@@ -44,18 +44,16 @@ export type TableToUpdateValues<T extends AnyTable> = {
|
|
|
44
44
|
type MainSelectResult<S extends SelectClause<T>, T extends AnyTable> = S extends true
|
|
45
45
|
? TableToColumnValues<T>
|
|
46
46
|
: S extends (keyof T["columns"])[]
|
|
47
|
-
? {
|
|
47
|
+
? Prettify<{
|
|
48
48
|
[K in S[number] as string extends K ? never : K]: K extends keyof T["columns"]
|
|
49
49
|
? T["columns"][K]["$out"]
|
|
50
50
|
: never;
|
|
51
|
-
}
|
|
51
|
+
}>
|
|
52
52
|
: never;
|
|
53
53
|
|
|
54
|
-
export type SelectResult<
|
|
55
|
-
T
|
|
56
|
-
|
|
57
|
-
Select extends SelectClause<T>,
|
|
58
|
-
> = MainSelectResult<Select, T> & JoinOut;
|
|
54
|
+
export type SelectResult<T extends AnyTable, JoinOut, Select extends SelectClause<T>> = Prettify<
|
|
55
|
+
MainSelectResult<Select, T> & JoinOut
|
|
56
|
+
>;
|
|
59
57
|
|
|
60
58
|
interface MapRelationType<Type> {
|
|
61
59
|
one: Type | null;
|
|
@@ -68,15 +66,43 @@ export type JoinBuilder<T extends AnyTable, Out = {}> = {
|
|
|
68
66
|
options?: FindManyOptions<Target, Select, JoinOut, false>,
|
|
69
67
|
) => JoinBuilder<
|
|
70
68
|
T,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
Prettify<
|
|
70
|
+
Out & {
|
|
71
|
+
[$K in K]: MapRelationType<SelectResult<Target, JoinOut, Select>>[Type];
|
|
72
|
+
}
|
|
73
|
+
>
|
|
74
74
|
>
|
|
75
75
|
: never;
|
|
76
76
|
};
|
|
77
77
|
|
|
78
78
|
export type OrderBy<Column = string> = [columnName: Column, "asc" | "desc"];
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Extract Select type parameter from a FindBuilder type (handles Omit wrapper)
|
|
82
|
+
* @internal
|
|
83
|
+
*/
|
|
84
|
+
type ExtractSelect<T> =
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
86
|
+
T extends FindBuilder<any, infer TSelect, any>
|
|
87
|
+
? TSelect
|
|
88
|
+
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
89
|
+
T extends Omit<FindBuilder<any, infer TSelect, any>, any>
|
|
90
|
+
? TSelect
|
|
91
|
+
: true;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Extract JoinOut type parameter from a FindBuilder type (handles Omit wrapper)
|
|
95
|
+
* @internal
|
|
96
|
+
*/
|
|
97
|
+
type ExtractJoinOut<T> =
|
|
98
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
99
|
+
T extends FindBuilder<any, any, infer TJoinOut>
|
|
100
|
+
? TJoinOut
|
|
101
|
+
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
102
|
+
T extends Omit<FindBuilder<any, any, infer TJoinOut>, any>
|
|
103
|
+
? TJoinOut
|
|
104
|
+
: {};
|
|
105
|
+
|
|
80
106
|
export type FindFirstOptions<
|
|
81
107
|
T extends AnyTable = AnyTable,
|
|
82
108
|
Select extends SelectClause<T> = SelectClause<T>,
|
|
@@ -109,31 +135,48 @@ export interface AbstractQuery<TSchema extends AnySchema, TUOWConfig = void> {
|
|
|
109
135
|
/**
|
|
110
136
|
* Find multiple records using a builder pattern
|
|
111
137
|
*/
|
|
112
|
-
find:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
138
|
+
find: {
|
|
139
|
+
// Overload when builder function is provided - infer Select and JoinOut from builder
|
|
140
|
+
<TableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
|
|
141
|
+
table: TableName,
|
|
142
|
+
builderFn: (
|
|
143
|
+
builder: Omit<FindBuilder<TSchema["tables"][TableName]>, "build">,
|
|
144
|
+
) => TBuilderResult,
|
|
145
|
+
): Promise<
|
|
146
|
+
SelectResult<
|
|
147
|
+
TSchema["tables"][TableName],
|
|
148
|
+
ExtractJoinOut<TBuilderResult>,
|
|
149
|
+
Extract<ExtractSelect<TBuilderResult>, SelectClause<TSchema["tables"][TableName]>>
|
|
150
|
+
>[]
|
|
151
|
+
>;
|
|
152
|
+
// Overload when no builder function - return all columns
|
|
153
|
+
<TableName extends keyof TSchema["tables"] & string>(
|
|
154
|
+
table: TableName,
|
|
155
|
+
): Promise<SelectResult<TSchema["tables"][TableName], {}, true>[]>;
|
|
156
|
+
};
|
|
122
157
|
|
|
123
158
|
/**
|
|
124
159
|
* Find the first record matching the criteria
|
|
125
160
|
* Implemented as a wrapper around find() with pageSize(1)
|
|
126
161
|
*/
|
|
127
|
-
findFirst:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
)
|
|
136
|
-
|
|
162
|
+
findFirst: {
|
|
163
|
+
// Overload when builder function is provided - infer Select and JoinOut from builder
|
|
164
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
165
|
+
<TableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
|
|
166
|
+
table: TableName,
|
|
167
|
+
builderFn: (
|
|
168
|
+
builder: Omit<FindBuilder<TSchema["tables"][TableName]>, "build">,
|
|
169
|
+
) => TBuilderResult,
|
|
170
|
+
): Promise<SelectResult<
|
|
171
|
+
TSchema["tables"][TableName],
|
|
172
|
+
ExtractJoinOut<TBuilderResult>,
|
|
173
|
+
Extract<ExtractSelect<TBuilderResult>, SelectClause<TSchema["tables"][TableName]>>
|
|
174
|
+
> | null>;
|
|
175
|
+
// Overload when no builder function - return all columns
|
|
176
|
+
<TableName extends keyof TSchema["tables"] & string>(
|
|
177
|
+
table: TableName,
|
|
178
|
+
): Promise<SelectResult<TSchema["tables"][TableName], {}, true> | null>;
|
|
179
|
+
};
|
|
137
180
|
|
|
138
181
|
/**
|
|
139
182
|
* Create a single record
|
|
@@ -161,8 +204,8 @@ export interface AbstractQuery<TSchema extends AnySchema, TUOWConfig = void> {
|
|
|
161
204
|
table: TableName,
|
|
162
205
|
id: FragnoId | string,
|
|
163
206
|
builderFn: (
|
|
164
|
-
builder: Omit<UpdateBuilder<TSchema["tables"][TableName]>, "build"
|
|
165
|
-
) => Omit<UpdateBuilder<TSchema["tables"][TableName]>, "build"
|
|
207
|
+
builder: Omit<UpdateBuilder<TSchema["tables"][TableName]>, "build">,
|
|
208
|
+
) => Omit<UpdateBuilder<TSchema["tables"][TableName]>, "build">,
|
|
166
209
|
) => Promise<void>;
|
|
167
210
|
|
|
168
211
|
/**
|
|
@@ -180,9 +223,7 @@ export interface AbstractQuery<TSchema extends AnySchema, TUOWConfig = void> {
|
|
|
180
223
|
delete: <TableName extends keyof TSchema["tables"] & string>(
|
|
181
224
|
table: TableName,
|
|
182
225
|
id: FragnoId | string,
|
|
183
|
-
builderFn?: (
|
|
184
|
-
builder: Omit<DeleteBuilder, "build" | "check">,
|
|
185
|
-
) => Omit<DeleteBuilder, "build" | "check">,
|
|
226
|
+
builderFn?: (builder: Omit<DeleteBuilder, "build">) => Omit<DeleteBuilder, "build">,
|
|
186
227
|
) => Promise<void>;
|
|
187
228
|
|
|
188
229
|
/**
|
|
@@ -226,7 +226,7 @@ describe("encodeValues", () => {
|
|
|
226
226
|
});
|
|
227
227
|
});
|
|
228
228
|
|
|
229
|
-
it("should
|
|
229
|
+
it("should convert FragnoId without internalId to ReferenceSubquery for reference columns", () => {
|
|
230
230
|
const fragnoId = new FragnoId({
|
|
231
231
|
externalId: "user123",
|
|
232
232
|
version: 1,
|
|
@@ -238,10 +238,10 @@ describe("encodeValues", () => {
|
|
|
238
238
|
"postgresql",
|
|
239
239
|
);
|
|
240
240
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
241
|
+
// FragnoId without internalId should be converted to ReferenceSubquery for database lookup
|
|
242
|
+
expect(result["title"]).toBe("Test Post");
|
|
243
|
+
expect(result["userId"]).toBeInstanceOf(ReferenceSubquery);
|
|
244
|
+
expect((result["userId"] as ReferenceSubquery).externalIdValue).toBe("user123");
|
|
245
245
|
});
|
|
246
246
|
|
|
247
247
|
it("should handle FragnoId across different providers", () => {
|
|
@@ -123,18 +123,36 @@ export function encodeValues(
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
if (value !== undefined) {
|
|
126
|
-
// Handle string references
|
|
127
|
-
if (col.role === "reference"
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
126
|
+
// Handle string references and FragnoId objects
|
|
127
|
+
if (col.role === "reference") {
|
|
128
|
+
if (typeof value === "string") {
|
|
129
|
+
// String external ID - generate subquery
|
|
130
|
+
const relation = Object.values(table.relations).find((rel) =>
|
|
131
|
+
rel.on.some(([localCol]) => localCol === k),
|
|
132
|
+
);
|
|
133
|
+
if (relation) {
|
|
134
|
+
result[col.name] = new ReferenceSubquery(relation.table, value);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
throw new Error(`Reference column ${k} not found in table ${table.name}`);
|
|
138
|
+
} else if (value instanceof FragnoId) {
|
|
139
|
+
// FragnoId object
|
|
140
|
+
if (value.internalId !== undefined) {
|
|
141
|
+
// If internal ID is populated, use it directly (no subquery needed)
|
|
142
|
+
result[col.name] = value.internalId;
|
|
143
|
+
continue;
|
|
144
|
+
} else {
|
|
145
|
+
// If internal ID is not populated, use external ID via subquery
|
|
146
|
+
const relation = Object.values(table.relations).find((rel) =>
|
|
147
|
+
rel.on.some(([localCol]) => localCol === k),
|
|
148
|
+
);
|
|
149
|
+
if (relation) {
|
|
150
|
+
result[col.name] = new ReferenceSubquery(relation.table, value.externalId);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
throw new Error(`Reference column ${k} not found in table ${table.name}`);
|
|
154
|
+
}
|
|
135
155
|
}
|
|
136
|
-
|
|
137
|
-
throw new Error(`Reference column ${k} not found in table ${table.name}`);
|
|
138
156
|
}
|
|
139
157
|
|
|
140
158
|
result[col.name] = serialize(value, col, provider);
|
|
@@ -939,7 +939,7 @@ export class UnitOfWork<
|
|
|
939
939
|
builderFn?: (
|
|
940
940
|
// We omit "build" because we don't want to expose it to the user
|
|
941
941
|
builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
|
|
942
|
-
) => Omit<FindBuilder<TSchema["tables"][TTableName], TSelect, TJoinOut>, "build"
|
|
942
|
+
) => Omit<FindBuilder<TSchema["tables"][TTableName], TSelect, TJoinOut>, "build"> | void,
|
|
943
943
|
): UnitOfWork<
|
|
944
944
|
TSchema,
|
|
945
945
|
[...TRetrievalResults, SelectResult<TSchema["tables"][TTableName], TJoinOut, TSelect>[]],
|
|
@@ -985,11 +985,12 @@ export class UnitOfWork<
|
|
|
985
985
|
|
|
986
986
|
/**
|
|
987
987
|
* Add a create operation (mutation phase only)
|
|
988
|
+
* Returns a FragnoId with the external ID that can be used immediately in subsequent operations
|
|
988
989
|
*/
|
|
989
990
|
create<TableName extends keyof TSchema["tables"] & string>(
|
|
990
991
|
table: TableName,
|
|
991
992
|
values: TableToInsertValues<TSchema["tables"][TableName]>,
|
|
992
|
-
):
|
|
993
|
+
): FragnoId {
|
|
993
994
|
if (this.#state === "executed") {
|
|
994
995
|
throw new Error(`create() can only be called during mutation phase.`);
|
|
995
996
|
}
|
|
@@ -1041,7 +1042,7 @@ export class UnitOfWork<
|
|
|
1041
1042
|
generatedExternalId: externalId,
|
|
1042
1043
|
});
|
|
1043
1044
|
|
|
1044
|
-
return
|
|
1045
|
+
return FragnoId.fromExternal(externalId, 0);
|
|
1045
1046
|
}
|
|
1046
1047
|
|
|
1047
1048
|
/**
|
|
@@ -1053,8 +1054,8 @@ export class UnitOfWork<
|
|
|
1053
1054
|
builderFn: (
|
|
1054
1055
|
// We omit "build" because we don't want to expose it to the user
|
|
1055
1056
|
builder: Omit<UpdateBuilder<TSchema["tables"][TableName]>, "build">,
|
|
1056
|
-
) => Omit<UpdateBuilder<TSchema["tables"][TableName]>, "build"
|
|
1057
|
-
):
|
|
1057
|
+
) => Omit<UpdateBuilder<TSchema["tables"][TableName]>, "build"> | void,
|
|
1058
|
+
): void {
|
|
1058
1059
|
if (this.#state === "executed") {
|
|
1059
1060
|
throw new Error(`update() can only be called during mutation phase.`);
|
|
1060
1061
|
}
|
|
@@ -1071,8 +1072,6 @@ export class UnitOfWork<
|
|
|
1071
1072
|
checkVersion,
|
|
1072
1073
|
set,
|
|
1073
1074
|
});
|
|
1074
|
-
|
|
1075
|
-
return this;
|
|
1076
1075
|
}
|
|
1077
1076
|
|
|
1078
1077
|
/**
|
|
@@ -1084,8 +1083,8 @@ export class UnitOfWork<
|
|
|
1084
1083
|
builderFn?: (
|
|
1085
1084
|
// We omit "build" because we don't want to expose it to the user
|
|
1086
1085
|
builder: Omit<DeleteBuilder, "build">,
|
|
1087
|
-
) => Omit<DeleteBuilder, "build"
|
|
1088
|
-
):
|
|
1086
|
+
) => Omit<DeleteBuilder, "build"> | void,
|
|
1087
|
+
): void {
|
|
1089
1088
|
if (this.#state === "executed") {
|
|
1090
1089
|
throw new Error(`delete() can only be called during mutation phase.`);
|
|
1091
1090
|
}
|
|
@@ -1101,8 +1100,6 @@ export class UnitOfWork<
|
|
|
1101
1100
|
id: opId,
|
|
1102
1101
|
checkVersion,
|
|
1103
1102
|
});
|
|
1104
|
-
|
|
1105
|
-
return this;
|
|
1106
1103
|
}
|
|
1107
1104
|
|
|
1108
1105
|
/**
|
|
@@ -342,6 +342,229 @@ describe("serialize", () => {
|
|
|
342
342
|
const time = Date.now();
|
|
343
343
|
expect(deserialize(time, dateCol, "sqlite")).toEqual(new Date(time));
|
|
344
344
|
});
|
|
345
|
+
|
|
346
|
+
it("should handle ISO string timestamps with positive timezone offset", () => {
|
|
347
|
+
const timestampCol = column("timestamp");
|
|
348
|
+
const time = "2024-06-15T14:30:00+05:30"; // India Standard Time
|
|
349
|
+
const result = deserialize(time, timestampCol, "sqlite");
|
|
350
|
+
expect(result).toBeInstanceOf(Date);
|
|
351
|
+
expect(result.toISOString()).toBe("2024-06-15T09:00:00.000Z");
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("should handle ISO string timestamps with negative timezone offset", () => {
|
|
355
|
+
const timestampCol = column("timestamp");
|
|
356
|
+
const time = "2024-06-15T14:30:00-08:00"; // Pacific Time
|
|
357
|
+
const result = deserialize(time, timestampCol, "sqlite");
|
|
358
|
+
expect(result).toBeInstanceOf(Date);
|
|
359
|
+
expect(result.toISOString()).toBe("2024-06-15T22:30:00.000Z");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("should preserve absolute time when deserializing numeric timestamps", () => {
|
|
363
|
+
const timestampCol = column("timestamp");
|
|
364
|
+
// Create a specific date and get its numeric representation
|
|
365
|
+
const specificDate = new Date("2024-06-15T12:00:00Z");
|
|
366
|
+
const numericTimestamp = specificDate.getTime();
|
|
367
|
+
|
|
368
|
+
const result = deserialize(numericTimestamp, timestampCol, "sqlite");
|
|
369
|
+
expect(result).toBeInstanceOf(Date);
|
|
370
|
+
expect(result.getTime()).toBe(numericTimestamp);
|
|
371
|
+
expect(result.toISOString()).toBe("2024-06-15T12:00:00.000Z");
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("should handle round-trip serialization/deserialization with timezones", () => {
|
|
375
|
+
const timestampCol = column("timestamp");
|
|
376
|
+
// Start with a date with timezone info
|
|
377
|
+
const originalTime = "2024-06-15T14:30:00+02:00";
|
|
378
|
+
const deserialized = deserialize(originalTime, timestampCol, "sqlite");
|
|
379
|
+
|
|
380
|
+
// SQLite would store this as a number
|
|
381
|
+
const numericValue = deserialized.getTime();
|
|
382
|
+
|
|
383
|
+
// Deserialize the numeric value back
|
|
384
|
+
const roundTrip = deserialize(numericValue, timestampCol, "sqlite");
|
|
385
|
+
|
|
386
|
+
expect(roundTrip).toBeInstanceOf(Date);
|
|
387
|
+
expect(roundTrip.getTime()).toBe(deserialized.getTime());
|
|
388
|
+
expect(roundTrip.toISOString()).toBe(deserialized.toISOString());
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
describe("postgresql date handling", () => {
|
|
393
|
+
it("should convert string timestamps to Date", () => {
|
|
394
|
+
const timestampCol = column("timestamp");
|
|
395
|
+
const time = "2024-01-01 12:30:45.123";
|
|
396
|
+
expect(deserialize(time, timestampCol, "postgresql")).toEqual(new Date(time));
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it("should convert ISO string timestamps to Date", () => {
|
|
400
|
+
const timestampCol = column("timestamp");
|
|
401
|
+
const time = "2024-01-01T00:00:00.000Z";
|
|
402
|
+
expect(deserialize(time, timestampCol, "postgresql")).toEqual(new Date(time));
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("should convert date strings to Date", () => {
|
|
406
|
+
const dateCol = column("date");
|
|
407
|
+
const time = "2024-01-01";
|
|
408
|
+
expect(deserialize(time, dateCol, "postgresql")).toEqual(new Date(time));
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("should handle timestamps with positive timezone offset", () => {
|
|
412
|
+
const timestampCol = column("timestamp");
|
|
413
|
+
const time = "2024-06-15T14:30:00+05:30"; // India Standard Time
|
|
414
|
+
const result = deserialize(time, timestampCol, "postgresql");
|
|
415
|
+
expect(result).toBeInstanceOf(Date);
|
|
416
|
+
expect(result.toISOString()).toBe("2024-06-15T09:00:00.000Z");
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it("should handle timestamps with negative timezone offset", () => {
|
|
420
|
+
const timestampCol = column("timestamp");
|
|
421
|
+
const time = "2024-06-15T14:30:00-08:00"; // Pacific Time
|
|
422
|
+
const result = deserialize(time, timestampCol, "postgresql");
|
|
423
|
+
expect(result).toBeInstanceOf(Date);
|
|
424
|
+
expect(result.toISOString()).toBe("2024-06-15T22:30:00.000Z");
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it("should handle timestamps with fractional seconds and timezone", () => {
|
|
428
|
+
const timestampCol = column("timestamp");
|
|
429
|
+
const time = "2024-06-15T14:30:45.123+01:00"; // Central European Time
|
|
430
|
+
const result = deserialize(time, timestampCol, "postgresql");
|
|
431
|
+
expect(result).toBeInstanceOf(Date);
|
|
432
|
+
expect(result.toISOString()).toBe("2024-06-15T13:30:45.123Z");
|
|
433
|
+
expect(result.getTime()).toBe(new Date("2024-06-15T13:30:45.123Z").getTime());
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("should preserve absolute time across timezone conversions", () => {
|
|
437
|
+
const timestampCol = column("timestamp");
|
|
438
|
+
// Same absolute time in different timezones
|
|
439
|
+
const utcTime = "2024-06-15T12:00:00Z";
|
|
440
|
+
const estTime = "2024-06-15T08:00:00-04:00";
|
|
441
|
+
const jstTime = "2024-06-15T21:00:00+09:00";
|
|
442
|
+
|
|
443
|
+
const utcResult = deserialize(utcTime, timestampCol, "postgresql");
|
|
444
|
+
const estResult = deserialize(estTime, timestampCol, "postgresql");
|
|
445
|
+
const jstResult = deserialize(jstTime, timestampCol, "postgresql");
|
|
446
|
+
|
|
447
|
+
// All should represent the same absolute time
|
|
448
|
+
expect(utcResult.getTime()).toBe(estResult.getTime());
|
|
449
|
+
expect(utcResult.getTime()).toBe(jstResult.getTime());
|
|
450
|
+
expect(estResult.getTime()).toBe(jstResult.getTime());
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
describe("mysql date handling", () => {
|
|
455
|
+
it("should convert string timestamps to Date", () => {
|
|
456
|
+
const timestampCol = column("timestamp");
|
|
457
|
+
const time = "2024-01-01 12:30:45";
|
|
458
|
+
expect(deserialize(time, timestampCol, "mysql")).toEqual(new Date(time));
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("should convert ISO string timestamps to Date", () => {
|
|
462
|
+
const timestampCol = column("timestamp");
|
|
463
|
+
const time = "2024-01-01T00:00:00.000Z";
|
|
464
|
+
expect(deserialize(time, timestampCol, "mysql")).toEqual(new Date(time));
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it("should convert date strings to Date", () => {
|
|
468
|
+
const dateCol = column("date");
|
|
469
|
+
const time = "2024-01-01";
|
|
470
|
+
expect(deserialize(time, dateCol, "mysql")).toEqual(new Date(time));
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it("should handle timestamps with positive timezone offset", () => {
|
|
474
|
+
const timestampCol = column("timestamp");
|
|
475
|
+
const time = "2024-06-15T14:30:00+05:30"; // India Standard Time
|
|
476
|
+
const result = deserialize(time, timestampCol, "mysql");
|
|
477
|
+
expect(result).toBeInstanceOf(Date);
|
|
478
|
+
expect(result.toISOString()).toBe("2024-06-15T09:00:00.000Z");
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it("should handle timestamps with negative timezone offset", () => {
|
|
482
|
+
const timestampCol = column("timestamp");
|
|
483
|
+
const time = "2024-06-15T14:30:00-08:00"; // Pacific Time
|
|
484
|
+
const result = deserialize(time, timestampCol, "mysql");
|
|
485
|
+
expect(result).toBeInstanceOf(Date);
|
|
486
|
+
expect(result.toISOString()).toBe("2024-06-15T22:30:00.000Z");
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it("should handle timestamps with fractional seconds and timezone", () => {
|
|
490
|
+
const timestampCol = column("timestamp");
|
|
491
|
+
const time = "2024-06-15T14:30:45.123+01:00"; // Central European Time
|
|
492
|
+
const result = deserialize(time, timestampCol, "mysql");
|
|
493
|
+
expect(result).toBeInstanceOf(Date);
|
|
494
|
+
expect(result.toISOString()).toBe("2024-06-15T13:30:45.123Z");
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it("should preserve absolute time across timezone conversions", () => {
|
|
498
|
+
const timestampCol = column("timestamp");
|
|
499
|
+
// Same absolute time in different timezones
|
|
500
|
+
const utcTime = "2024-06-15T12:00:00Z";
|
|
501
|
+
const cstTime = "2024-06-15T20:00:00+08:00"; // China Standard Time
|
|
502
|
+
const pstTime = "2024-06-15T04:00:00-08:00"; // Pacific Time
|
|
503
|
+
|
|
504
|
+
const utcResult = deserialize(utcTime, timestampCol, "mysql");
|
|
505
|
+
const cstResult = deserialize(cstTime, timestampCol, "mysql");
|
|
506
|
+
const pstResult = deserialize(pstTime, timestampCol, "mysql");
|
|
507
|
+
|
|
508
|
+
// All should represent the same absolute time
|
|
509
|
+
expect(utcResult.getTime()).toBe(cstResult.getTime());
|
|
510
|
+
expect(utcResult.getTime()).toBe(pstResult.getTime());
|
|
511
|
+
expect(cstResult.getTime()).toBe(pstResult.getTime());
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
describe("cockroachdb date handling", () => {
|
|
516
|
+
it("should convert string timestamps to Date", () => {
|
|
517
|
+
const timestampCol = column("timestamp");
|
|
518
|
+
const time = "2024-01-01 12:30:45.123";
|
|
519
|
+
expect(deserialize(time, timestampCol, "cockroachdb")).toEqual(new Date(time));
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it("should convert date strings to Date", () => {
|
|
523
|
+
const dateCol = column("date");
|
|
524
|
+
const time = "2024-01-01";
|
|
525
|
+
expect(deserialize(time, dateCol, "cockroachdb")).toEqual(new Date(time));
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it("should handle timestamps with positive timezone offset", () => {
|
|
529
|
+
const timestampCol = column("timestamp");
|
|
530
|
+
const time = "2024-06-15T14:30:00+05:30"; // India Standard Time
|
|
531
|
+
const result = deserialize(time, timestampCol, "cockroachdb");
|
|
532
|
+
expect(result).toBeInstanceOf(Date);
|
|
533
|
+
expect(result.toISOString()).toBe("2024-06-15T09:00:00.000Z");
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it("should handle timestamps with negative timezone offset", () => {
|
|
537
|
+
const timestampCol = column("timestamp");
|
|
538
|
+
const time = "2024-06-15T14:30:00-08:00"; // Pacific Time
|
|
539
|
+
const result = deserialize(time, timestampCol, "cockroachdb");
|
|
540
|
+
expect(result).toBeInstanceOf(Date);
|
|
541
|
+
expect(result.toISOString()).toBe("2024-06-15T22:30:00.000Z");
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it("should handle timestamps with fractional seconds and timezone", () => {
|
|
545
|
+
const timestampCol = column("timestamp");
|
|
546
|
+
const time = "2024-06-15T14:30:45.123+01:00"; // Central European Time
|
|
547
|
+
const result = deserialize(time, timestampCol, "cockroachdb");
|
|
548
|
+
expect(result).toBeInstanceOf(Date);
|
|
549
|
+
expect(result.toISOString()).toBe("2024-06-15T13:30:45.123Z");
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it("should preserve absolute time across timezone conversions", () => {
|
|
553
|
+
const timestampCol = column("timestamp");
|
|
554
|
+
// Same absolute time in different timezones
|
|
555
|
+
const utcTime = "2024-06-15T12:00:00Z";
|
|
556
|
+
const aestTime = "2024-06-15T22:00:00+10:00"; // Australian Eastern Standard Time
|
|
557
|
+
const brtTime = "2024-06-15T09:00:00-03:00"; // Brasilia Time
|
|
558
|
+
|
|
559
|
+
const utcResult = deserialize(utcTime, timestampCol, "cockroachdb");
|
|
560
|
+
const aestResult = deserialize(aestTime, timestampCol, "cockroachdb");
|
|
561
|
+
const brtResult = deserialize(brtTime, timestampCol, "cockroachdb");
|
|
562
|
+
|
|
563
|
+
// All should represent the same absolute time
|
|
564
|
+
expect(utcResult.getTime()).toBe(aestResult.getTime());
|
|
565
|
+
expect(utcResult.getTime()).toBe(brtResult.getTime());
|
|
566
|
+
expect(aestResult.getTime()).toBe(brtResult.getTime());
|
|
567
|
+
});
|
|
345
568
|
});
|
|
346
569
|
|
|
347
570
|
describe("boolean handling", () => {
|
package/src/schema/serialize.ts
CHANGED
|
@@ -300,6 +300,22 @@ export function deserialize(value: unknown, col: AnyColumn, provider: SQLProvide
|
|
|
300
300
|
return new Date(value);
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
+
if (
|
|
304
|
+
(provider === "postgresql" || provider === "cockroachdb") &&
|
|
305
|
+
(col.type === "timestamp" || col.type === "date") &&
|
|
306
|
+
typeof value === "string"
|
|
307
|
+
) {
|
|
308
|
+
return new Date(value);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (
|
|
312
|
+
provider === "mysql" &&
|
|
313
|
+
(col.type === "timestamp" || col.type === "date") &&
|
|
314
|
+
typeof value === "string"
|
|
315
|
+
) {
|
|
316
|
+
return new Date(value);
|
|
317
|
+
}
|
|
318
|
+
|
|
303
319
|
if (col.type === "bool" && typeof value === "number") {
|
|
304
320
|
return value === 1;
|
|
305
321
|
}
|