@fragno-dev/db 0.1.12 → 0.1.14
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 +51 -44
- package/CHANGELOG.md +12 -0
- package/dist/adapters/adapters.d.ts +13 -1
- package/dist/adapters/adapters.d.ts.map +1 -1
- package/dist/adapters/adapters.js.map +1 -1
- package/dist/adapters/drizzle/drizzle-adapter.d.ts +2 -0
- package/dist/adapters/drizzle/drizzle-adapter.d.ts.map +1 -1
- package/dist/adapters/drizzle/drizzle-adapter.js +7 -2
- package/dist/adapters/drizzle/drizzle-adapter.js.map +1 -1
- package/dist/adapters/drizzle/drizzle-query.d.ts.map +1 -1
- package/dist/adapters/drizzle/drizzle-query.js +10 -4
- package/dist/adapters/drizzle/drizzle-query.js.map +1 -1
- package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts +0 -1
- package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts.map +1 -1
- package/dist/adapters/drizzle/drizzle-uow-compiler.js +51 -37
- package/dist/adapters/drizzle/drizzle-uow-compiler.js.map +1 -1
- package/dist/adapters/drizzle/drizzle-uow-decoder.js +26 -2
- package/dist/adapters/drizzle/drizzle-uow-decoder.js.map +1 -1
- package/dist/adapters/drizzle/generate.js +1 -1
- package/dist/adapters/drizzle/shared.d.ts +14 -1
- package/dist/adapters/drizzle/shared.d.ts.map +1 -0
- package/dist/adapters/kysely/kysely-adapter.d.ts +3 -1
- package/dist/adapters/kysely/kysely-adapter.d.ts.map +1 -1
- package/dist/adapters/kysely/kysely-adapter.js +7 -2
- package/dist/adapters/kysely/kysely-adapter.js.map +1 -1
- package/dist/adapters/kysely/kysely-query.d.ts.map +1 -1
- package/dist/adapters/kysely/kysely-query.js +34 -4
- package/dist/adapters/kysely/kysely-query.js.map +1 -1
- package/dist/adapters/kysely/kysely-shared.d.ts +11 -0
- package/dist/adapters/kysely/kysely-shared.d.ts.map +1 -0
- package/dist/adapters/kysely/kysely-uow-compiler.js +40 -10
- package/dist/adapters/kysely/kysely-uow-compiler.js.map +1 -1
- package/dist/adapters/kysely/migration/execute-base.js +1 -1
- package/dist/bind-services.d.ts +7 -0
- package/dist/bind-services.d.ts.map +1 -0
- package/dist/bind-services.js +14 -0
- package/dist/bind-services.js.map +1 -0
- package/dist/fragment.d.ts +131 -12
- package/dist/fragment.d.ts.map +1 -1
- package/dist/fragment.js +107 -8
- package/dist/fragment.js.map +1 -1
- package/dist/mod.d.ts +5 -2
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +4 -2
- package/dist/mod.js.map +1 -1
- package/dist/query/cursor.d.ts +67 -32
- package/dist/query/cursor.d.ts.map +1 -1
- package/dist/query/cursor.js +84 -31
- package/dist/query/cursor.js.map +1 -1
- package/dist/query/query.d.ts +7 -2
- package/dist/query/query.d.ts.map +1 -1
- package/dist/query/unit-of-work.d.ts +113 -18
- package/dist/query/unit-of-work.d.ts.map +1 -1
- package/dist/query/unit-of-work.js +266 -16
- package/dist/query/unit-of-work.js.map +1 -1
- package/package.json +2 -2
- package/src/adapters/adapters.ts +14 -0
- package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +78 -6
- package/src/adapters/drizzle/drizzle-adapter-sqlite.test.ts +139 -9
- package/src/adapters/drizzle/drizzle-adapter.ts +16 -1
- package/src/adapters/drizzle/drizzle-query.ts +35 -15
- package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +76 -60
- package/src/adapters/drizzle/drizzle-uow-compiler.ts +82 -41
- package/src/adapters/drizzle/drizzle-uow-decoder.ts +42 -6
- package/src/adapters/kysely/kysely-adapter-pglite.test.ts +104 -6
- package/src/adapters/kysely/kysely-adapter.ts +16 -1
- package/src/adapters/kysely/kysely-query.ts +76 -16
- package/src/adapters/kysely/kysely-uow-compiler.test.ts +62 -46
- package/src/adapters/kysely/kysely-uow-compiler.ts +53 -16
- package/src/adapters/kysely/kysely-uow-joins.test.ts +30 -30
- package/src/bind-services.test.ts +214 -0
- package/src/bind-services.ts +37 -0
- package/src/db-fragment.test.ts +800 -0
- package/src/fragment.ts +557 -28
- package/src/mod.ts +25 -1
- package/src/query/cursor.test.ts +113 -68
- package/src/query/cursor.ts +127 -36
- package/src/query/query.ts +21 -2
- package/src/query/unit-of-work-multi-schema.test.ts +64 -0
- package/src/query/unit-of-work-types.test.ts +13 -0
- package/src/query/unit-of-work.test.ts +5 -9
- package/src/query/unit-of-work.ts +629 -62
- package/src/uow-context-integration.test.ts +102 -0
- package/src/uow-context.test.ts +182 -0
- package/src/fragment.test.ts +0 -341
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type TableNameMapper,
|
|
15
15
|
parseDrizzle,
|
|
16
16
|
type DBType,
|
|
17
|
+
createTableNameMapper,
|
|
17
18
|
} from "./shared";
|
|
18
19
|
import { encodeValues, ReferenceSubquery } from "../../query/result-transform";
|
|
19
20
|
import { serialize } from "../../schema/serialize";
|
|
@@ -33,30 +34,42 @@ export type DrizzleCompiledQuery = {
|
|
|
33
34
|
* This compiler translates UOW operations into Drizzle query functions
|
|
34
35
|
* that can be executed as a batch/transaction.
|
|
35
36
|
*
|
|
36
|
-
* @param schema - The database schema
|
|
37
37
|
* @param pool - Connection pool for acquiring database connections
|
|
38
38
|
* @param provider - SQL provider (sqlite, mysql, postgresql)
|
|
39
|
-
* @param mapper - Optional table name mapper for namespace prefixing
|
|
39
|
+
* @param mapper - Optional table name mapper for namespace prefixing (fallback for operations without explicit namespace)
|
|
40
40
|
* @returns A UOWCompiler instance for Drizzle
|
|
41
41
|
*/
|
|
42
|
-
export function createDrizzleUOWCompiler
|
|
43
|
-
schema: TSchema,
|
|
42
|
+
export function createDrizzleUOWCompiler(
|
|
44
43
|
pool: ConnectionPool<DBType>,
|
|
45
44
|
provider: "sqlite" | "mysql" | "postgresql",
|
|
46
45
|
mapper?: TableNameMapper,
|
|
47
|
-
): UOWCompiler<
|
|
46
|
+
): UOWCompiler<DrizzleCompiledQuery> {
|
|
48
47
|
// Get db synchronously for compilation (doesn't execute, just builds SQL)
|
|
49
48
|
// TODO: We don't even need a Drizzle instance with a db client attached here. `drizzle({ schema })` is enough.
|
|
50
49
|
const dbRaw = pool.getDatabaseSync();
|
|
51
50
|
const [db, drizzleTables] = parseDrizzle(dbRaw);
|
|
52
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Get the mapper for a specific operation
|
|
54
|
+
* Uses operation's namespace if provided, otherwise falls back to the default mapper
|
|
55
|
+
*/
|
|
56
|
+
function getMapperForOperation(namespace: string | undefined): TableNameMapper | undefined {
|
|
57
|
+
if (namespace) {
|
|
58
|
+
return createTableNameMapper(namespace);
|
|
59
|
+
}
|
|
60
|
+
return mapper;
|
|
61
|
+
}
|
|
62
|
+
|
|
53
63
|
/**
|
|
54
64
|
* Convert a Fragno table to a Drizzle table
|
|
55
65
|
* @throws Error if table is not found in Drizzle schema
|
|
56
66
|
*/
|
|
57
|
-
function toDrizzleTable(table: AnyTable): TableType {
|
|
58
|
-
//
|
|
59
|
-
const
|
|
67
|
+
function toDrizzleTable(table: AnyTable, namespace: string | undefined): TableType {
|
|
68
|
+
// Get the mapper for this operation's namespace
|
|
69
|
+
const opMapper = getMapperForOperation(namespace);
|
|
70
|
+
|
|
71
|
+
// Map logical table name to physical table name using the operation-specific mapper
|
|
72
|
+
const physicalTableName = opMapper ? opMapper.toPhysical(table.ormName) : table.ormName;
|
|
60
73
|
const out = drizzleTables[physicalTableName];
|
|
61
74
|
if (out) {
|
|
62
75
|
return out;
|
|
@@ -71,13 +84,17 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
|
|
|
71
84
|
* Convert a Fragno column to a Drizzle column
|
|
72
85
|
* @throws Error if column is not found in Drizzle table
|
|
73
86
|
*/
|
|
74
|
-
function toDrizzleColumn(
|
|
87
|
+
function toDrizzleColumn(
|
|
88
|
+
schema: AnySchema,
|
|
89
|
+
namespace: string | undefined,
|
|
90
|
+
col: AnyColumn,
|
|
91
|
+
): ColumnType {
|
|
75
92
|
const fragnoTable = schema.tables[col.tableName];
|
|
76
93
|
if (!fragnoTable) {
|
|
77
94
|
throw new Error(`[Drizzle] Unknown table ${col.tableName} for column ${col.ormName}.`);
|
|
78
95
|
}
|
|
79
96
|
|
|
80
|
-
const table = toDrizzleTable(fragnoTable);
|
|
97
|
+
const table = toDrizzleTable(fragnoTable, namespace);
|
|
81
98
|
const out = table[col.ormName];
|
|
82
99
|
if (out) {
|
|
83
100
|
return out;
|
|
@@ -89,13 +106,17 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
|
|
|
89
106
|
/**
|
|
90
107
|
* Build a WHERE clause from a condition using Drizzle's query builder
|
|
91
108
|
*/
|
|
92
|
-
function buildWhere(
|
|
109
|
+
function buildWhere(
|
|
110
|
+
schema: AnySchema,
|
|
111
|
+
namespace: string | undefined,
|
|
112
|
+
condition: Condition,
|
|
113
|
+
): Drizzle.SQL | undefined {
|
|
93
114
|
if (condition.type === "compare") {
|
|
94
|
-
const left = toDrizzleColumn(condition.a);
|
|
115
|
+
const left = toDrizzleColumn(schema, namespace, condition.a);
|
|
95
116
|
const op = condition.operator;
|
|
96
117
|
let right = condition.b;
|
|
97
118
|
if (right instanceof Column) {
|
|
98
|
-
right = toDrizzleColumn(right);
|
|
119
|
+
right = toDrizzleColumn(schema, namespace, right);
|
|
99
120
|
} else {
|
|
100
121
|
// Handle string references - convert external ID to internal ID via subquery
|
|
101
122
|
if (condition.a.role === "reference" && typeof right === "string") {
|
|
@@ -181,11 +202,11 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
|
|
|
181
202
|
}
|
|
182
203
|
|
|
183
204
|
if (condition.type === "and") {
|
|
184
|
-
return Drizzle.and(...condition.items.map((item) => buildWhere(item)));
|
|
205
|
+
return Drizzle.and(...condition.items.map((item) => buildWhere(schema, namespace, item)));
|
|
185
206
|
}
|
|
186
207
|
|
|
187
208
|
if (condition.type === "not") {
|
|
188
|
-
const result = buildWhere(condition.item);
|
|
209
|
+
const result = buildWhere(schema, namespace, condition.item);
|
|
189
210
|
if (!result) {
|
|
190
211
|
return;
|
|
191
212
|
}
|
|
@@ -193,7 +214,7 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
|
|
|
193
214
|
return Drizzle.not(result);
|
|
194
215
|
}
|
|
195
216
|
|
|
196
|
-
return Drizzle.or(...condition.items.map((item) => buildWhere(item)));
|
|
217
|
+
return Drizzle.or(...condition.items.map((item) => buildWhere(schema, namespace, item)));
|
|
197
218
|
}
|
|
198
219
|
|
|
199
220
|
/**
|
|
@@ -229,7 +250,7 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
|
|
|
229
250
|
* Get table from schema by name
|
|
230
251
|
* @throws Error if table is not found in schema
|
|
231
252
|
*/
|
|
232
|
-
function getTable(name: unknown): AnyTable {
|
|
253
|
+
function getTable(schema: AnySchema, name: unknown): AnyTable {
|
|
233
254
|
const table = schema.tables[name as string];
|
|
234
255
|
if (!table) {
|
|
235
256
|
throw new Error(`Invalid table name ${name}.`);
|
|
@@ -260,6 +281,8 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
|
|
|
260
281
|
* Process joins recursively to support nested joins with orderBy and limit
|
|
261
282
|
*/
|
|
262
283
|
function processJoins(
|
|
284
|
+
schema: AnySchema,
|
|
285
|
+
namespace: string | undefined,
|
|
263
286
|
joins: CompiledJoin[],
|
|
264
287
|
): Record<string, Drizzle.DBQueryConfig<"many", boolean>> {
|
|
265
288
|
const result: Record<string, Drizzle.DBQueryConfig<"many", boolean>> = {};
|
|
@@ -286,7 +309,7 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
|
|
|
286
309
|
let joinOrderBy: Drizzle.SQL[] | undefined;
|
|
287
310
|
if (options.orderBy && options.orderBy.length > 0) {
|
|
288
311
|
joinOrderBy = options.orderBy.map(([col, direction]) => {
|
|
289
|
-
const drizzleCol = toDrizzleColumn(col);
|
|
312
|
+
const drizzleCol = toDrizzleColumn(schema, namespace, col);
|
|
290
313
|
return direction === "asc" ? Drizzle.asc(drizzleCol) : Drizzle.desc(drizzleCol);
|
|
291
314
|
});
|
|
292
315
|
}
|
|
@@ -294,7 +317,7 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
|
|
|
294
317
|
// Build WHERE clause for this join if provided
|
|
295
318
|
let joinWhere: Drizzle.SQL | undefined;
|
|
296
319
|
if (options.where) {
|
|
297
|
-
joinWhere = buildWhere(options.where);
|
|
320
|
+
joinWhere = buildWhere(schema, namespace, options.where);
|
|
298
321
|
}
|
|
299
322
|
|
|
300
323
|
// Build the join config
|
|
@@ -307,7 +330,7 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
|
|
|
307
330
|
|
|
308
331
|
// Recursively process nested joins
|
|
309
332
|
if (options.join && options.join.length > 0) {
|
|
310
|
-
joinConfig.with = processJoins(options.join);
|
|
333
|
+
joinConfig.with = processJoins(schema, namespace, options.join);
|
|
311
334
|
}
|
|
312
335
|
|
|
313
336
|
result[joinName] = joinConfig;
|
|
@@ -317,7 +340,8 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
|
|
|
317
340
|
}
|
|
318
341
|
|
|
319
342
|
return {
|
|
320
|
-
compileRetrievalOperation(op: RetrievalOperation<
|
|
343
|
+
compileRetrievalOperation(op: RetrievalOperation<AnySchema>): DrizzleCompiledQuery | null {
|
|
344
|
+
const schema = op.schema;
|
|
321
345
|
switch (op.type) {
|
|
322
346
|
case "count": {
|
|
323
347
|
// Build WHERE clause
|
|
@@ -329,11 +353,11 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
|
|
|
329
353
|
return null;
|
|
330
354
|
}
|
|
331
355
|
if (condition !== true) {
|
|
332
|
-
whereClause = buildWhere(condition);
|
|
356
|
+
whereClause = buildWhere(schema, op.namespace, condition);
|
|
333
357
|
}
|
|
334
358
|
}
|
|
335
359
|
|
|
336
|
-
const drizzleTable = toDrizzleTable(op.table);
|
|
360
|
+
const drizzleTable = toDrizzleTable(op.table, op.namespace);
|
|
337
361
|
const query = db.select({ count: Drizzle.count() }).from(drizzleTable);
|
|
338
362
|
|
|
339
363
|
const compiledQuery = whereClause ? query.where(whereClause).toSQL() : query.toSQL();
|
|
@@ -377,7 +401,7 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
|
|
|
377
401
|
let orderBy: Drizzle.SQL[] | undefined;
|
|
378
402
|
if (indexColumns.length > 0) {
|
|
379
403
|
orderBy = indexColumns.map((col) => {
|
|
380
|
-
const drizzleCol = toDrizzleColumn(col);
|
|
404
|
+
const drizzleCol = toDrizzleColumn(schema, op.namespace, col);
|
|
381
405
|
return orderDirection === "asc" ? Drizzle.asc(drizzleCol) : Drizzle.desc(drizzleCol);
|
|
382
406
|
});
|
|
383
407
|
}
|
|
@@ -413,7 +437,7 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
|
|
|
413
437
|
return null;
|
|
414
438
|
}
|
|
415
439
|
if (condition !== true) {
|
|
416
|
-
const clause = buildWhere(condition);
|
|
440
|
+
const clause = buildWhere(schema, op.namespace, condition);
|
|
417
441
|
if (clause) {
|
|
418
442
|
whereClauses.push(clause);
|
|
419
443
|
}
|
|
@@ -423,8 +447,9 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
|
|
|
423
447
|
// Add cursor-based pagination conditions
|
|
424
448
|
if ((after || before) && indexColumns.length > 0) {
|
|
425
449
|
const cursor = after || before;
|
|
426
|
-
|
|
427
|
-
const
|
|
450
|
+
// Decode cursor if it's a string, otherwise use it as-is
|
|
451
|
+
const cursorObj = typeof cursor === "string" ? decodeCursor(cursor!) : cursor!;
|
|
452
|
+
const serializedValues = serializeCursorValues(cursorObj, indexColumns, provider);
|
|
428
453
|
|
|
429
454
|
// Build tuple comparison for cursor pagination
|
|
430
455
|
// For "after" with "asc": (col1, col2, ...) > (val1, val2, ...)
|
|
@@ -435,12 +460,12 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
|
|
|
435
460
|
|
|
436
461
|
if (indexColumns.length === 1) {
|
|
437
462
|
// Simple single-column case
|
|
438
|
-
const col = toDrizzleColumn(indexColumns[0]!);
|
|
463
|
+
const col = toDrizzleColumn(schema, op.namespace, indexColumns[0]!);
|
|
439
464
|
const val = serializedValues[indexColumns[0]!.ormName];
|
|
440
465
|
whereClauses.push(useGreaterThan ? Drizzle.gt(col, val) : Drizzle.lt(col, val));
|
|
441
466
|
} else {
|
|
442
467
|
// Multi-column tuple comparison using SQL
|
|
443
|
-
const drizzleCols = indexColumns.map((c) => toDrizzleColumn(c));
|
|
468
|
+
const drizzleCols = indexColumns.map((c) => toDrizzleColumn(schema, op.namespace, c));
|
|
444
469
|
const vals = indexColumns.map((c) => serializedValues[c.ormName]);
|
|
445
470
|
const operator = useGreaterThan ? ">" : "<";
|
|
446
471
|
// Safe cast: building a SQL comparison expression for cursor pagination
|
|
@@ -468,23 +493,37 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
|
|
|
468
493
|
|
|
469
494
|
// Process joins recursively to support nested joins
|
|
470
495
|
if (joins) {
|
|
471
|
-
queryConfig.with = processJoins(joins);
|
|
496
|
+
queryConfig.with = processJoins(schema, op.namespace, joins);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// For multi-schema support: get the mapper for the operation's namespace
|
|
500
|
+
const opMapper = getMapperForOperation(op.namespace);
|
|
501
|
+
const physicalTableName = opMapper
|
|
502
|
+
? opMapper.toPhysical(op.table.ormName)
|
|
503
|
+
: op.table.ormName;
|
|
504
|
+
const tableQuery = db.query[physicalTableName];
|
|
505
|
+
|
|
506
|
+
if (!tableQuery) {
|
|
507
|
+
throw new Error(
|
|
508
|
+
`[Drizzle] Table ${op.table.ormName} (physical: ${physicalTableName}) not found in db.query. ` +
|
|
509
|
+
`Available tables: ${Object.keys(db.query).join(", ")}`,
|
|
510
|
+
);
|
|
472
511
|
}
|
|
473
512
|
|
|
474
|
-
const
|
|
475
|
-
const compiledQuery = db.query[physicalTableName].findMany(queryConfig).toSQL();
|
|
513
|
+
const compiledQuery = tableQuery.findMany(queryConfig).toSQL();
|
|
476
514
|
return compiledQuery;
|
|
477
515
|
}
|
|
478
516
|
}
|
|
479
517
|
},
|
|
480
518
|
|
|
481
519
|
compileMutationOperation(
|
|
482
|
-
op: MutationOperation<
|
|
520
|
+
op: MutationOperation<AnySchema>,
|
|
483
521
|
): CompiledMutation<DrizzleCompiledQuery> | null {
|
|
522
|
+
const schema = op.schema;
|
|
484
523
|
switch (op.type) {
|
|
485
524
|
case "create": {
|
|
486
|
-
const table = getTable(op.table);
|
|
487
|
-
const drizzleTable = toDrizzleTable(table);
|
|
525
|
+
const table = getTable(schema, op.table);
|
|
526
|
+
const drizzleTable = toDrizzleTable(table, op.namespace);
|
|
488
527
|
// encodeValues now handles runtime defaults automatically
|
|
489
528
|
const encodedValues = encodeValues(op.values, table, true, provider);
|
|
490
529
|
const values = processReferenceSubqueries(encodedValues);
|
|
@@ -497,10 +536,10 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
|
|
|
497
536
|
}
|
|
498
537
|
|
|
499
538
|
case "update": {
|
|
500
|
-
const table = getTable(op.table);
|
|
539
|
+
const table = getTable(schema, op.table);
|
|
501
540
|
const idColumn = table.getIdColumn();
|
|
502
541
|
const versionColumn = table.getVersionColumn();
|
|
503
|
-
const drizzleTable = toDrizzleTable(table);
|
|
542
|
+
const drizzleTable = toDrizzleTable(table, op.namespace);
|
|
504
543
|
|
|
505
544
|
const externalId = typeof op.id === "string" ? op.id : op.id.externalId;
|
|
506
545
|
const versionToCheck = getVersionToCheck(op.id, op.checkVersion);
|
|
@@ -522,7 +561,8 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
|
|
|
522
561
|
return null;
|
|
523
562
|
}
|
|
524
563
|
|
|
525
|
-
const whereClause =
|
|
564
|
+
const whereClause =
|
|
565
|
+
condition === true ? undefined : buildWhere(schema, op.namespace, condition);
|
|
526
566
|
const encodedSetValues = encodeValues(op.set, table, false, provider);
|
|
527
567
|
const setValues = processReferenceSubqueries(encodedSetValues);
|
|
528
568
|
|
|
@@ -540,10 +580,10 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
|
|
|
540
580
|
}
|
|
541
581
|
|
|
542
582
|
case "delete": {
|
|
543
|
-
const table = getTable(op.table);
|
|
583
|
+
const table = getTable(schema, op.table);
|
|
544
584
|
const idColumn = table.getIdColumn();
|
|
545
585
|
const versionColumn = table.getVersionColumn();
|
|
546
|
-
const drizzleTable = toDrizzleTable(table);
|
|
586
|
+
const drizzleTable = toDrizzleTable(table, op.namespace);
|
|
547
587
|
|
|
548
588
|
if (!op.id) {
|
|
549
589
|
throw new Error(
|
|
@@ -581,7 +621,8 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
|
|
|
581
621
|
return null;
|
|
582
622
|
}
|
|
583
623
|
|
|
584
|
-
const whereClause =
|
|
624
|
+
const whereClause =
|
|
625
|
+
condition === true ? undefined : buildWhere(schema, op.namespace, condition);
|
|
585
626
|
|
|
586
627
|
const compiledQuery = db.delete(drizzleTable).where(whereClause).toSQL();
|
|
587
628
|
return {
|
|
@@ -4,6 +4,7 @@ import type { RetrievalOperation, UOWDecoder } from "../../query/unit-of-work";
|
|
|
4
4
|
import { decodeResult } from "../../query/result-transform";
|
|
5
5
|
import { getOrderedJoinColumns } from "./join-column-utils";
|
|
6
6
|
import type { DrizzleResult } from "./shared";
|
|
7
|
+
import { createCursorFromRecord, Cursor, type CursorResult } from "../../query/cursor";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Join information with nested join support
|
|
@@ -142,17 +143,14 @@ function transformJoinArraysToObjects(
|
|
|
142
143
|
return transformedRow;
|
|
143
144
|
}
|
|
144
145
|
|
|
145
|
-
export function createDrizzleUOWDecoder
|
|
146
|
-
_schema: TSchema,
|
|
147
|
-
provider: SQLProvider,
|
|
148
|
-
): UOWDecoder<TSchema, DrizzleResult> {
|
|
146
|
+
export function createDrizzleUOWDecoder(provider: SQLProvider): UOWDecoder<DrizzleResult> {
|
|
149
147
|
return (rawResults, ops) => {
|
|
150
148
|
if (rawResults.length !== ops.length) {
|
|
151
149
|
throw new Error("rawResults and ops must have the same length");
|
|
152
150
|
}
|
|
153
151
|
|
|
154
152
|
return rawResults.map((result, index) => {
|
|
155
|
-
const op = ops[index] as RetrievalOperation<
|
|
153
|
+
const op = ops[index] as RetrievalOperation<AnySchema>;
|
|
156
154
|
if (!op) {
|
|
157
155
|
throw new Error("op must be defined");
|
|
158
156
|
}
|
|
@@ -173,10 +171,48 @@ export function createDrizzleUOWDecoder<TSchema extends AnySchema>(
|
|
|
173
171
|
}
|
|
174
172
|
|
|
175
173
|
// Handle find operations - decode each row
|
|
176
|
-
|
|
174
|
+
const decodedRows = result.rows.map((row) => {
|
|
177
175
|
const transformedRow = transformJoinArraysToObjects(row, op, provider);
|
|
178
176
|
return decodeResult(transformedRow, op.table, provider);
|
|
179
177
|
});
|
|
178
|
+
|
|
179
|
+
// If cursor generation is requested, wrap in CursorResult
|
|
180
|
+
if (op.withCursor) {
|
|
181
|
+
let cursor: Cursor | undefined;
|
|
182
|
+
|
|
183
|
+
// Generate cursor from last item if results exist
|
|
184
|
+
if (decodedRows.length > 0 && op.options.orderByIndex && op.options.pageSize) {
|
|
185
|
+
const lastItem = decodedRows[decodedRows.length - 1];
|
|
186
|
+
const indexName = op.options.orderByIndex.indexName;
|
|
187
|
+
|
|
188
|
+
// Get index columns
|
|
189
|
+
let indexColumns;
|
|
190
|
+
if (indexName === "_primary") {
|
|
191
|
+
indexColumns = [op.table.getIdColumn()];
|
|
192
|
+
} else {
|
|
193
|
+
const index = op.table.indexes[indexName];
|
|
194
|
+
if (index) {
|
|
195
|
+
indexColumns = index.columns;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (indexColumns && lastItem) {
|
|
200
|
+
cursor = createCursorFromRecord(lastItem as Record<string, unknown>, indexColumns, {
|
|
201
|
+
indexName: op.options.orderByIndex.indexName,
|
|
202
|
+
orderDirection: op.options.orderByIndex.direction,
|
|
203
|
+
pageSize: op.options.pageSize,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const cursorResult: CursorResult<unknown> = {
|
|
209
|
+
items: decodedRows,
|
|
210
|
+
cursor,
|
|
211
|
+
};
|
|
212
|
+
return cursorResult;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return decodedRows;
|
|
180
216
|
});
|
|
181
217
|
};
|
|
182
218
|
}
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
type FragnoId,
|
|
11
11
|
type FragnoReference,
|
|
12
12
|
} from "../../schema/create";
|
|
13
|
-
import {
|
|
13
|
+
import { Cursor } from "../../query/cursor";
|
|
14
14
|
|
|
15
15
|
describe("KyselyAdapter PGLite", () => {
|
|
16
16
|
const testSchema = schema((s) => {
|
|
@@ -396,10 +396,12 @@ describe("KyselyAdapter PGLite", () => {
|
|
|
396
396
|
|
|
397
397
|
// Get cursor for pagination (using the last item from page 1)
|
|
398
398
|
const lastItem = page1Results[page1Results.length - 1]!;
|
|
399
|
-
const cursor =
|
|
399
|
+
const cursor = new Cursor({
|
|
400
|
+
indexName: "name_idx",
|
|
401
|
+
orderDirection: "asc",
|
|
402
|
+
pageSize: 2,
|
|
400
403
|
indexValues: { name: lastItem.name },
|
|
401
|
-
|
|
402
|
-
});
|
|
404
|
+
}).encode();
|
|
403
405
|
|
|
404
406
|
// Get page 2 using the cursor
|
|
405
407
|
const page2 = queryEngine
|
|
@@ -785,7 +787,7 @@ describe("KyselyAdapter PGLite", () => {
|
|
|
785
787
|
});
|
|
786
788
|
});
|
|
787
789
|
|
|
788
|
-
it("should handle timestamps and
|
|
790
|
+
it("should handle timestamps and time zones correctly", async () => {
|
|
789
791
|
const queryEngine = adapter.createQueryEngine(testSchema, "test");
|
|
790
792
|
|
|
791
793
|
// Create a user
|
|
@@ -824,7 +826,7 @@ describe("KyselyAdapter PGLite", () => {
|
|
|
824
826
|
const specificDate = new Date("2024-06-15T14:30:00Z");
|
|
825
827
|
expect(specificDate.toISOString()).toBe("2024-06-15T14:30:00.000Z");
|
|
826
828
|
|
|
827
|
-
// Verify that dates from different
|
|
829
|
+
// Verify that dates from different time zones are handled correctly
|
|
828
830
|
const localDate = new Date("2024-06-15T14:30:00");
|
|
829
831
|
expect(localDate).toBeInstanceOf(Date);
|
|
830
832
|
expect(typeof localDate.getTimezoneOffset()).toBe("number");
|
|
@@ -872,4 +874,100 @@ describe("KyselyAdapter PGLite", () => {
|
|
|
872
874
|
// Verify the foreign key relationship is correct
|
|
873
875
|
expect(post?.user_id.internalId).toBe(user?.id.internalId);
|
|
874
876
|
});
|
|
877
|
+
|
|
878
|
+
it("should support cursor-based pagination with findWithCursor()", async () => {
|
|
879
|
+
const queryEngine = adapter.createQueryEngine(testSchema, "test");
|
|
880
|
+
|
|
881
|
+
// Create multiple users for pagination testing with unique prefix
|
|
882
|
+
const prefix = "CursorPagTest";
|
|
883
|
+
const userIds: FragnoId[] = [];
|
|
884
|
+
for (let i = 1; i <= 25; i++) {
|
|
885
|
+
const userId = await queryEngine.create("users", {
|
|
886
|
+
name: `${prefix} ${i.toString().padStart(2, "0")}`,
|
|
887
|
+
age: 20 + i,
|
|
888
|
+
});
|
|
889
|
+
userIds.push(userId);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Fetch first page with cursor (filter by prefix to avoid other test data)
|
|
893
|
+
const firstPage = await queryEngine.findWithCursor("users", (b) =>
|
|
894
|
+
b.whereIndex("name_idx").orderByIndex("name_idx", "asc").pageSize(10),
|
|
895
|
+
);
|
|
896
|
+
|
|
897
|
+
// Check structure
|
|
898
|
+
expect(firstPage).toHaveProperty("items");
|
|
899
|
+
expect(firstPage).toHaveProperty("cursor");
|
|
900
|
+
expect(Array.isArray(firstPage.items)).toBe(true);
|
|
901
|
+
expect(firstPage.items.length).toBeGreaterThan(0);
|
|
902
|
+
|
|
903
|
+
assert(firstPage.cursor instanceof Cursor);
|
|
904
|
+
|
|
905
|
+
// Fetch second page using cursor
|
|
906
|
+
const secondPage = await queryEngine.findWithCursor("users", (b) =>
|
|
907
|
+
b
|
|
908
|
+
.whereIndex("name_idx")
|
|
909
|
+
.after(firstPage.cursor!)
|
|
910
|
+
.orderByIndex("name_idx", "asc")
|
|
911
|
+
.pageSize(10),
|
|
912
|
+
);
|
|
913
|
+
|
|
914
|
+
expect(secondPage.items.length).toBeGreaterThan(0);
|
|
915
|
+
|
|
916
|
+
// Verify no overlap - all names in second page should be different from first page
|
|
917
|
+
const firstPageNames = new Set(firstPage.items.map((u) => u.name));
|
|
918
|
+
const secondPageNames = secondPage.items.map((u) => u.name);
|
|
919
|
+
|
|
920
|
+
for (const name of secondPageNames) {
|
|
921
|
+
expect(firstPageNames.has(name)).toBe(false);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Verify ordering - last item of first page should come before first item of second page
|
|
925
|
+
const firstPageLast = firstPage.items[firstPage.items.length - 1].name;
|
|
926
|
+
const secondPageFirst = secondPage.items[0].name;
|
|
927
|
+
expect(firstPageLast < secondPageFirst).toBe(true);
|
|
928
|
+
|
|
929
|
+
// Verify our test data is present
|
|
930
|
+
const testUsers = await queryEngine.find("users", (b) =>
|
|
931
|
+
b.whereIndex("name_idx").pageSize(100),
|
|
932
|
+
);
|
|
933
|
+
const testUserNames = testUsers.filter((u) => u.name.startsWith(prefix)).map((u) => u.name);
|
|
934
|
+
expect(testUserNames).toHaveLength(25);
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
it("should support findWithCursor() in Unit of Work", async () => {
|
|
938
|
+
const queryEngine = adapter.createQueryEngine(testSchema, "test");
|
|
939
|
+
|
|
940
|
+
// Create test users if not already present
|
|
941
|
+
const existingUsers = await queryEngine.find("users", (b) =>
|
|
942
|
+
b.whereIndex("name_idx").pageSize(1),
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
if (existingUsers.length === 0) {
|
|
946
|
+
for (let i = 1; i <= 5; i++) {
|
|
947
|
+
await queryEngine.create("users", {
|
|
948
|
+
name: `UOW Cursor User ${i}`,
|
|
949
|
+
age: 30 + i,
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Use findWithCursor in UOW
|
|
955
|
+
const uow = queryEngine
|
|
956
|
+
.createUnitOfWork("cursor-test")
|
|
957
|
+
.findWithCursor("users", (b) =>
|
|
958
|
+
b.whereIndex("name_idx").orderByIndex("name_idx", "asc").pageSize(3),
|
|
959
|
+
);
|
|
960
|
+
|
|
961
|
+
const [result] = await uow.executeRetrieve();
|
|
962
|
+
|
|
963
|
+
// Verify result structure
|
|
964
|
+
expect(result).toHaveProperty("items");
|
|
965
|
+
expect(result).toHaveProperty("cursor");
|
|
966
|
+
expect(Array.isArray(result.items)).toBe(true);
|
|
967
|
+
expect(result.items.length).toBeGreaterThan(0);
|
|
968
|
+
|
|
969
|
+
if (result.items.length === 3) {
|
|
970
|
+
expect(result.cursor).toBeInstanceOf(Cursor);
|
|
971
|
+
}
|
|
972
|
+
});
|
|
875
973
|
});
|
|
@@ -28,6 +28,7 @@ export interface KyselyConfig {
|
|
|
28
28
|
export class KyselyAdapter implements DatabaseAdapter<KyselyUOWConfig> {
|
|
29
29
|
#connectionPool: ConnectionPool<KyselyAny>;
|
|
30
30
|
#provider: SQLProvider;
|
|
31
|
+
#schemaNamespaceMap = new WeakMap<AnySchema, string>();
|
|
31
32
|
|
|
32
33
|
constructor(config: KyselyConfig) {
|
|
33
34
|
this.#connectionPool = createKyselyConnectionPool(config.db);
|
|
@@ -46,13 +47,27 @@ export class KyselyAdapter implements DatabaseAdapter<KyselyUOWConfig> {
|
|
|
46
47
|
await this.#connectionPool.close();
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
createTableNameMapper(namespace: string) {
|
|
51
|
+
return createTableNameMapper(namespace);
|
|
52
|
+
}
|
|
53
|
+
|
|
49
54
|
createQueryEngine<T extends AnySchema>(
|
|
50
55
|
schema: T,
|
|
51
56
|
namespace: string,
|
|
52
57
|
): AbstractQuery<T, KyselyUOWConfig> {
|
|
58
|
+
// Register schema-namespace mapping
|
|
59
|
+
this.#schemaNamespaceMap.set(schema, namespace);
|
|
60
|
+
|
|
53
61
|
// Only create mapper if namespace is non-empty
|
|
54
62
|
const mapper = namespace ? createTableNameMapper(namespace) : undefined;
|
|
55
|
-
return fromKysely(
|
|
63
|
+
return fromKysely(
|
|
64
|
+
schema,
|
|
65
|
+
this.#connectionPool,
|
|
66
|
+
this.#provider,
|
|
67
|
+
mapper,
|
|
68
|
+
undefined,
|
|
69
|
+
this.#schemaNamespaceMap,
|
|
70
|
+
);
|
|
56
71
|
}
|
|
57
72
|
|
|
58
73
|
async isConnectionHealthy(): Promise<boolean> {
|