@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.
Files changed (51) hide show
  1. package/.turbo/turbo-build.log +30 -28
  2. package/CHANGELOG.md +13 -0
  3. package/dist/adapters/drizzle/drizzle-query.d.ts.map +1 -1
  4. package/dist/adapters/drizzle/drizzle-query.js +38 -34
  5. package/dist/adapters/drizzle/drizzle-query.js.map +1 -1
  6. package/dist/adapters/kysely/kysely-adapter.d.ts +3 -2
  7. package/dist/adapters/kysely/kysely-adapter.d.ts.map +1 -1
  8. package/dist/adapters/kysely/kysely-adapter.js.map +1 -1
  9. package/dist/adapters/kysely/kysely-query.d.ts +22 -0
  10. package/dist/adapters/kysely/kysely-query.d.ts.map +1 -0
  11. package/dist/adapters/kysely/kysely-query.js +72 -50
  12. package/dist/adapters/kysely/kysely-query.js.map +1 -1
  13. package/dist/adapters/kysely/kysely-uow-executor.js +2 -2
  14. package/dist/adapters/kysely/kysely-uow-executor.js.map +1 -1
  15. package/dist/migration-engine/generation-engine.d.ts +1 -1
  16. package/dist/migration-engine/generation-engine.d.ts.map +1 -1
  17. package/dist/migration-engine/generation-engine.js.map +1 -1
  18. package/dist/mod.d.ts +5 -5
  19. package/dist/mod.d.ts.map +1 -1
  20. package/dist/mod.js.map +1 -1
  21. package/dist/query/query.d.ts +24 -8
  22. package/dist/query/query.d.ts.map +1 -1
  23. package/dist/query/result-transform.js +17 -5
  24. package/dist/query/result-transform.js.map +1 -1
  25. package/dist/query/unit-of-work.d.ts +5 -4
  26. package/dist/query/unit-of-work.d.ts.map +1 -1
  27. package/dist/query/unit-of-work.js +2 -3
  28. package/dist/query/unit-of-work.js.map +1 -1
  29. package/dist/schema/serialize.js +2 -0
  30. package/dist/schema/serialize.js.map +1 -1
  31. package/package.json +2 -2
  32. package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +170 -50
  33. package/src/adapters/drizzle/drizzle-adapter-sqlite.test.ts +89 -35
  34. package/src/adapters/drizzle/drizzle-query.test.ts +54 -4
  35. package/src/adapters/drizzle/drizzle-query.ts +65 -60
  36. package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +63 -3
  37. package/src/adapters/kysely/kysely-adapter-pglite.test.ts +88 -0
  38. package/src/adapters/kysely/kysely-adapter.ts +6 -3
  39. package/src/adapters/kysely/kysely-query.test.ts +498 -0
  40. package/src/adapters/kysely/kysely-query.ts +137 -82
  41. package/src/adapters/kysely/kysely-uow-compiler.test.ts +66 -0
  42. package/src/adapters/kysely/kysely-uow-executor.ts +5 -9
  43. package/src/migration-engine/generation-engine.ts +2 -1
  44. package/src/mod.ts +6 -6
  45. package/src/query/query-type.test.ts +34 -14
  46. package/src/query/query.ts +77 -36
  47. package/src/query/result-transform.test.ts +5 -5
  48. package/src/query/result-transform.ts +29 -11
  49. package/src/query/unit-of-work.ts +8 -11
  50. package/src/schema/serialize.test.ts +223 -0
  51. package/src/schema/serialize.ts +16 -0
@@ -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 extends AnyTable,
56
- JoinOut,
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
- Out & {
72
- [$K in K]: MapRelationType<SelectResult<Target, JoinOut, Select>>[Type];
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
- TableName extends keyof TSchema["tables"] & string,
114
- Select extends SelectClause<TSchema["tables"][TableName]> = true,
115
- JoinOut = {},
116
- >(
117
- table: TableName,
118
- builderFn?: (
119
- builder: Omit<FindBuilder<TSchema["tables"][TableName]>, "build">,
120
- ) => Omit<FindBuilder<TSchema["tables"][TableName], Select, JoinOut>, "build">,
121
- ) => Promise<SelectResult<TSchema["tables"][TableName], JoinOut, Select>[]>;
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
- TableName extends keyof TSchema["tables"] & string,
129
- Select extends SelectClause<TSchema["tables"][TableName]> = true,
130
- JoinOut = {},
131
- >(
132
- table: TableName,
133
- builderFn?: (
134
- builder: Omit<FindBuilder<TSchema["tables"][TableName]>, "build">,
135
- ) => Omit<FindBuilder<TSchema["tables"][TableName], Select, JoinOut>, "build">,
136
- ) => Promise<SelectResult<TSchema["tables"][TableName], JoinOut, Select> | null>;
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" | "check">,
165
- ) => Omit<UpdateBuilder<TSchema["tables"][TableName]>, "build" | "check">,
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 fallback to external ID for reference when internal ID unavailable", () => {
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
- expect(result).toEqual({
242
- title: "Test Post",
243
- userId: "user123",
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 - convert external ID to internal ID via subquery
127
- if (col.role === "reference" && typeof value === "string") {
128
- // Find relation that uses this column
129
- const relation = Object.values(table.relations).find((rel) =>
130
- rel.on.some(([localCol]) => localCol === k),
131
- );
132
- if (relation) {
133
- result[col.name] = new ReferenceSubquery(relation.table, value);
134
- continue;
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
- ): this {
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 this;
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
- ): this {
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
- ): this {
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", () => {
@@ -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
  }