@fragno-dev/db 0.0.1 → 0.1.0
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 +137 -13
- package/.turbo/turbo-test.log +36 -0
- package/CHANGELOG.md +7 -0
- package/dist/adapters/adapters.d.ts +18 -0
- package/dist/adapters/adapters.d.ts.map +1 -0
- package/dist/adapters/drizzle/drizzle-adapter.d.ts +21 -0
- package/dist/adapters/drizzle/drizzle-adapter.d.ts.map +1 -0
- package/dist/adapters/drizzle/drizzle-adapter.js +62 -0
- package/dist/adapters/drizzle/drizzle-adapter.js.map +1 -0
- package/dist/adapters/drizzle/drizzle-query.d.ts +17 -0
- package/dist/adapters/drizzle/drizzle-query.d.ts.map +1 -0
- package/dist/adapters/drizzle/drizzle-query.js +139 -0
- package/dist/adapters/drizzle/drizzle-query.js.map +1 -0
- package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts +9 -0
- package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts.map +1 -0
- package/dist/adapters/drizzle/drizzle-uow-compiler.js +300 -0
- package/dist/adapters/drizzle/drizzle-uow-compiler.js.map +1 -0
- package/dist/adapters/drizzle/drizzle-uow-decoder.js +82 -0
- package/dist/adapters/drizzle/drizzle-uow-decoder.js.map +1 -0
- package/dist/adapters/drizzle/drizzle-uow-executor.js +125 -0
- package/dist/adapters/drizzle/drizzle-uow-executor.js.map +1 -0
- package/dist/adapters/drizzle/generate.js +273 -0
- package/dist/adapters/drizzle/generate.js.map +1 -0
- package/dist/adapters/drizzle/join-column-utils.js +28 -0
- package/dist/adapters/drizzle/join-column-utils.js.map +1 -0
- package/dist/adapters/drizzle/shared.js +11 -0
- package/dist/adapters/drizzle/shared.js.map +1 -0
- package/dist/adapters/kysely/kysely-adapter.d.ts +23 -0
- package/dist/adapters/kysely/kysely-adapter.d.ts.map +1 -0
- package/dist/adapters/kysely/kysely-adapter.js +119 -0
- package/dist/adapters/kysely/kysely-adapter.js.map +1 -0
- package/dist/adapters/kysely/kysely-query-builder.js +306 -0
- package/dist/adapters/kysely/kysely-query-builder.js.map +1 -0
- package/dist/adapters/kysely/kysely-query-compiler.js +67 -0
- package/dist/adapters/kysely/kysely-query-compiler.js.map +1 -0
- package/dist/adapters/kysely/kysely-query.js +158 -0
- package/dist/adapters/kysely/kysely-query.js.map +1 -0
- package/dist/adapters/kysely/kysely-uow-compiler.js +139 -0
- package/dist/adapters/kysely/kysely-uow-compiler.js.map +1 -0
- package/dist/adapters/kysely/kysely-uow-executor.js +89 -0
- package/dist/adapters/kysely/kysely-uow-executor.js.map +1 -0
- package/dist/adapters/kysely/migration/execute.js +176 -0
- package/dist/adapters/kysely/migration/execute.js.map +1 -0
- package/dist/fragment.d.ts +54 -0
- package/dist/fragment.d.ts.map +1 -0
- package/dist/fragment.js +92 -0
- package/dist/fragment.js.map +1 -0
- package/dist/id.d.ts +2 -0
- package/dist/migration-engine/auto-from-schema.js +116 -0
- package/dist/migration-engine/auto-from-schema.js.map +1 -0
- package/dist/migration-engine/create.d.ts +41 -0
- package/dist/migration-engine/create.d.ts.map +1 -0
- package/dist/migration-engine/create.js +58 -0
- package/dist/migration-engine/create.js.map +1 -0
- package/dist/migration-engine/shared.d.ts +90 -0
- package/dist/migration-engine/shared.d.ts.map +1 -0
- package/dist/migration-engine/shared.js +8 -0
- package/dist/migration-engine/shared.js.map +1 -0
- package/dist/mod.d.ts +55 -2
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +111 -2
- package/dist/mod.js.map +1 -1
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column-builder.js +108 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column-builder.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column.js +55 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/entity.js +18 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/entity.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/common.js +183 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/common.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/enum.js +58 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/enum.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/foreign-keys.js +68 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/foreign-keys.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/unique-constraint.js +56 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/unique-constraint.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/utils/array.js +65 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/utils/array.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/conditions.js +81 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/conditions.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/select.js +13 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/select.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/functions/aggregate.js +10 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/functions/aggregate.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/sql.js +372 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/sql.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/subquery.js +23 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/subquery.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.js +62 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.utils.js +6 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.utils.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing-utils.js +8 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing-utils.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing.js +8 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/view-common.js +6 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/view-common.js.map +1 -0
- package/dist/query/condition-builder.d.ts +41 -0
- package/dist/query/condition-builder.d.ts.map +1 -0
- package/dist/query/condition-builder.js +93 -0
- package/dist/query/condition-builder.js.map +1 -0
- package/dist/query/cursor.d.ts +88 -0
- package/dist/query/cursor.d.ts.map +1 -0
- package/dist/query/cursor.js +103 -0
- package/dist/query/cursor.js.map +1 -0
- package/dist/query/orm/orm.d.ts +18 -0
- package/dist/query/orm/orm.d.ts.map +1 -0
- package/dist/query/orm/orm.js +48 -0
- package/dist/query/orm/orm.js.map +1 -0
- package/dist/query/query.d.ts +79 -0
- package/dist/query/query.d.ts.map +1 -0
- package/dist/query/query.js +1 -0
- package/dist/query/result-transform.js +155 -0
- package/dist/query/result-transform.js.map +1 -0
- package/dist/query/unit-of-work.d.ts +435 -0
- package/dist/query/unit-of-work.d.ts.map +1 -0
- package/dist/query/unit-of-work.js +549 -0
- package/dist/query/unit-of-work.js.map +1 -0
- package/dist/schema/create.d.ts +273 -116
- package/dist/schema/create.d.ts.map +1 -1
- package/dist/schema/create.js +410 -222
- package/dist/schema/create.js.map +1 -1
- package/dist/schema/serialize.js +101 -0
- package/dist/schema/serialize.js.map +1 -0
- package/dist/schema-generator/schema-generator.d.ts +15 -0
- package/dist/schema-generator/schema-generator.d.ts.map +1 -0
- package/dist/shared/providers.d.ts +6 -0
- package/dist/shared/providers.d.ts.map +1 -0
- package/dist/util/import-generator.js +26 -0
- package/dist/util/import-generator.js.map +1 -0
- package/dist/util/parse.js +15 -0
- package/dist/util/parse.js.map +1 -0
- package/dist/util/types.d.ts +8 -0
- package/dist/util/types.d.ts.map +1 -0
- package/package.json +63 -2
- package/src/adapters/adapters.ts +22 -0
- package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +433 -0
- package/src/adapters/drizzle/drizzle-adapter.test.ts +122 -0
- package/src/adapters/drizzle/drizzle-adapter.ts +118 -0
- package/src/adapters/drizzle/drizzle-query.ts +234 -0
- package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +1084 -0
- package/src/adapters/drizzle/drizzle-uow-compiler.ts +546 -0
- package/src/adapters/drizzle/drizzle-uow-decoder.ts +165 -0
- package/src/adapters/drizzle/drizzle-uow-executor.ts +213 -0
- package/src/adapters/drizzle/generate.test.ts +643 -0
- package/src/adapters/drizzle/generate.ts +481 -0
- package/src/adapters/drizzle/join-column-utils.test.ts +79 -0
- package/src/adapters/drizzle/join-column-utils.ts +39 -0
- package/src/adapters/drizzle/migrate-drizzle.test.ts +226 -0
- package/src/adapters/drizzle/shared.ts +22 -0
- package/src/adapters/drizzle/test-utils.ts +56 -0
- package/src/adapters/kysely/kysely-adapter-pglite.test.ts +789 -0
- package/src/adapters/kysely/kysely-adapter.ts +196 -0
- package/src/adapters/kysely/kysely-query-builder.test.ts +1344 -0
- package/src/adapters/kysely/kysely-query-builder.ts +611 -0
- package/src/adapters/kysely/kysely-query-compiler.ts +124 -0
- package/src/adapters/kysely/kysely-query.ts +254 -0
- package/src/adapters/kysely/kysely-uow-compiler.test.ts +916 -0
- package/src/adapters/kysely/kysely-uow-compiler.ts +271 -0
- package/src/adapters/kysely/kysely-uow-executor.ts +149 -0
- package/src/adapters/kysely/kysely-uow-joins.test.ts +811 -0
- package/src/adapters/kysely/migration/execute-mysql.test.ts +1173 -0
- package/src/adapters/kysely/migration/execute-postgres.test.ts +2657 -0
- package/src/adapters/kysely/migration/execute.ts +382 -0
- package/src/adapters/kysely/migration/kysely-migrator.test.ts +197 -0
- package/src/fragment.test.ts +287 -0
- package/src/fragment.ts +198 -0
- package/src/migration-engine/auto-from-schema.test.ts +118 -58
- package/src/migration-engine/auto-from-schema.ts +103 -32
- package/src/migration-engine/create.test.ts +34 -46
- package/src/migration-engine/create.ts +41 -26
- package/src/migration-engine/shared.ts +26 -6
- package/src/mod.ts +197 -1
- package/src/query/condition-builder.test.ts +379 -0
- package/src/query/condition-builder.ts +294 -0
- package/src/query/cursor.test.ts +296 -0
- package/src/query/cursor.ts +147 -0
- package/src/query/orm/orm.ts +92 -0
- package/src/query/query-type.test.ts +429 -0
- package/src/query/query.ts +200 -0
- package/src/query/result-transform.test.ts +795 -0
- package/src/query/result-transform.ts +247 -0
- package/src/query/unit-of-work-types.test.ts +192 -0
- package/src/query/unit-of-work.test.ts +947 -0
- package/src/query/unit-of-work.ts +1199 -0
- package/src/schema/create.test.ts +653 -110
- package/src/schema/create.ts +708 -337
- package/src/schema/serialize.test.ts +559 -0
- package/src/schema/serialize.ts +359 -0
- package/src/schema-generator/schema-generator.ts +12 -0
- package/src/shared/config.ts +0 -8
- package/src/util/import-generator.ts +28 -0
- package/src/util/parse.ts +16 -0
- package/src/util/types.ts +4 -0
- package/tsconfig.json +1 -1
- package/tsdown.config.ts +11 -1
- package/vitest.config.ts +3 -0
- /package/dist/{cuid.js → id.js} +0 -0
- /package/src/{cuid.ts → id.ts} +0 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import type { CompiledQuery } from "kysely";
|
|
2
|
+
import type { AnyColumn, AnySchema, FragnoId } from "../../schema/create";
|
|
3
|
+
import type {
|
|
4
|
+
CompiledMutation,
|
|
5
|
+
MutationOperation,
|
|
6
|
+
RetrievalOperation,
|
|
7
|
+
UOWCompiler,
|
|
8
|
+
} from "../../query/unit-of-work";
|
|
9
|
+
import type { KyselyConfig } from "./kysely-adapter";
|
|
10
|
+
import { createKyselyQueryCompiler } from "./kysely-query-compiler";
|
|
11
|
+
import { createKyselyQueryBuilder } from "./kysely-query-builder";
|
|
12
|
+
import { buildCondition, type Condition } from "../../query/condition-builder";
|
|
13
|
+
import { decodeCursor, serializeCursorValues } from "../../query/cursor";
|
|
14
|
+
import type { AnySelectClause } from "../../query/query";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create a Kysely-specific Unit of Work compiler
|
|
18
|
+
*
|
|
19
|
+
* This compiler translates UOW operations into Kysely CompiledQuery objects
|
|
20
|
+
* that can be executed as a batch/transaction.
|
|
21
|
+
*
|
|
22
|
+
* @param schema - The database schema
|
|
23
|
+
* @param config - Kysely configuration
|
|
24
|
+
* @returns A UOWCompiler instance for Kysely
|
|
25
|
+
*/
|
|
26
|
+
export function createKyselyUOWCompiler<TSchema extends AnySchema>(
|
|
27
|
+
schema: TSchema,
|
|
28
|
+
config: KyselyConfig,
|
|
29
|
+
): UOWCompiler<TSchema, CompiledQuery> {
|
|
30
|
+
const queryCompiler = createKyselyQueryCompiler(schema, config);
|
|
31
|
+
const queryBuilder = createKyselyQueryBuilder(config.db, config.provider);
|
|
32
|
+
const { provider } = config;
|
|
33
|
+
|
|
34
|
+
function toTable(name: unknown) {
|
|
35
|
+
const table = schema.tables[name as string];
|
|
36
|
+
if (!table) {
|
|
37
|
+
throw new Error(`Invalid table name ${name}.`);
|
|
38
|
+
}
|
|
39
|
+
return table;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
compileRetrievalOperation(op: RetrievalOperation<TSchema>): CompiledQuery | null {
|
|
44
|
+
switch (op.type) {
|
|
45
|
+
case "count": {
|
|
46
|
+
return queryCompiler.count(op.table.name, {
|
|
47
|
+
where: op.options.where,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
case "find": {
|
|
52
|
+
// Map UOW FindOptions to query compiler's FindManyOptions
|
|
53
|
+
const {
|
|
54
|
+
useIndex: _useIndex,
|
|
55
|
+
orderByIndex,
|
|
56
|
+
joins: join,
|
|
57
|
+
after,
|
|
58
|
+
before,
|
|
59
|
+
pageSize,
|
|
60
|
+
...findManyOptions
|
|
61
|
+
} = op.options;
|
|
62
|
+
|
|
63
|
+
// Get index columns for ordering and cursor pagination
|
|
64
|
+
let indexColumns: AnyColumn[] = [];
|
|
65
|
+
let orderDirection: "asc" | "desc" = "asc";
|
|
66
|
+
|
|
67
|
+
if (orderByIndex) {
|
|
68
|
+
const index = op.table.indexes[orderByIndex.indexName];
|
|
69
|
+
orderDirection = orderByIndex.direction;
|
|
70
|
+
|
|
71
|
+
if (!index) {
|
|
72
|
+
// If _primary index doesn't exist, fall back to internal ID column
|
|
73
|
+
// (which is the actual primary key and maintains insertion order)
|
|
74
|
+
if (orderByIndex.indexName === "_primary") {
|
|
75
|
+
indexColumns = [op.table.getIdColumn()];
|
|
76
|
+
} else {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Index "${orderByIndex.indexName}" not found on table "${op.table.name}"`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
// Order by all columns in the index with the specified direction
|
|
83
|
+
indexColumns = index.columns;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Convert orderByIndex to orderBy format
|
|
88
|
+
let orderBy: [AnyColumn, "asc" | "desc"][] | undefined;
|
|
89
|
+
if (indexColumns.length > 0) {
|
|
90
|
+
orderBy = indexColumns.map((col) => [col, orderDirection]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Handle cursor pagination - build a cursor condition
|
|
94
|
+
let cursorCondition: Condition | undefined;
|
|
95
|
+
|
|
96
|
+
if ((after || before) && indexColumns.length > 0) {
|
|
97
|
+
const cursor = after || before;
|
|
98
|
+
const cursorData = decodeCursor(cursor!);
|
|
99
|
+
const serializedValues = serializeCursorValues(cursorData, indexColumns, provider);
|
|
100
|
+
|
|
101
|
+
// Build tuple comparison for cursor pagination
|
|
102
|
+
// For "after" with "asc": (col1, col2, ...) > (val1, val2, ...)
|
|
103
|
+
// For "before" with "desc": reverse the comparison
|
|
104
|
+
const isAfter = !!after;
|
|
105
|
+
const useGreaterThan =
|
|
106
|
+
(isAfter && orderDirection === "asc") || (!isAfter && orderDirection === "desc");
|
|
107
|
+
|
|
108
|
+
if (indexColumns.length === 1) {
|
|
109
|
+
// Simple single-column case
|
|
110
|
+
const col = indexColumns[0]!;
|
|
111
|
+
const val = serializedValues[col.ormName];
|
|
112
|
+
const operator = useGreaterThan ? ">" : "<";
|
|
113
|
+
cursorCondition = {
|
|
114
|
+
type: "compare",
|
|
115
|
+
a: col,
|
|
116
|
+
operator,
|
|
117
|
+
b: val,
|
|
118
|
+
};
|
|
119
|
+
} else {
|
|
120
|
+
// Multi-column tuple comparison - not yet supported for Kysely
|
|
121
|
+
throw new Error(
|
|
122
|
+
"Multi-column cursor pagination is not yet supported in Kysely Unit of Work implementation",
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Combine user where clause with cursor condition
|
|
128
|
+
let combinedWhere: Condition | undefined;
|
|
129
|
+
if (findManyOptions.where) {
|
|
130
|
+
const whereResult = buildCondition(op.table.columns, findManyOptions.where);
|
|
131
|
+
if (whereResult === true) {
|
|
132
|
+
combinedWhere = undefined;
|
|
133
|
+
} else if (whereResult === false) {
|
|
134
|
+
return null;
|
|
135
|
+
} else {
|
|
136
|
+
combinedWhere = whereResult;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (cursorCondition) {
|
|
141
|
+
if (combinedWhere) {
|
|
142
|
+
combinedWhere = {
|
|
143
|
+
type: "and",
|
|
144
|
+
items: [combinedWhere, cursorCondition],
|
|
145
|
+
};
|
|
146
|
+
} else {
|
|
147
|
+
combinedWhere = cursorCondition;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// When we have joins or need to bypass buildFindOptions, use queryBuilder directly
|
|
152
|
+
if (join && join.length > 0) {
|
|
153
|
+
return queryBuilder.findMany(op.table, {
|
|
154
|
+
// Safe cast: select from UOW matches SimplifyFindOptions requirement
|
|
155
|
+
select: (findManyOptions.select ?? true) as AnySelectClause,
|
|
156
|
+
where: combinedWhere,
|
|
157
|
+
orderBy,
|
|
158
|
+
limit: pageSize,
|
|
159
|
+
join,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return queryCompiler.findMany(op.table.name, {
|
|
164
|
+
...findManyOptions,
|
|
165
|
+
where: combinedWhere ? () => combinedWhere! : undefined,
|
|
166
|
+
orderBy: orderBy?.map(([col, dir]) => [col.ormName, dir]),
|
|
167
|
+
limit: pageSize,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
compileMutationOperation(
|
|
174
|
+
op: MutationOperation<TSchema>,
|
|
175
|
+
): CompiledMutation<CompiledQuery> | null {
|
|
176
|
+
switch (op.type) {
|
|
177
|
+
case "create":
|
|
178
|
+
// queryCompiler.create() calls encodeValues() which handles runtime defaults
|
|
179
|
+
return {
|
|
180
|
+
query: queryCompiler.create(op.table, op.values),
|
|
181
|
+
expectedAffectedRows: null, // creates don't need affected row checks
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
case "update": {
|
|
185
|
+
const table = toTable(op.table);
|
|
186
|
+
const idColumn = table.getIdColumn();
|
|
187
|
+
const versionColumn = table.getVersionColumn();
|
|
188
|
+
|
|
189
|
+
const externalId = typeof op.id === "string" ? op.id : op.id.externalId;
|
|
190
|
+
const versionToCheck = getVersionToCheck(op.id, op.checkVersion);
|
|
191
|
+
|
|
192
|
+
// Build WHERE clause that filters by ID and optionally by version
|
|
193
|
+
const whereClause =
|
|
194
|
+
versionToCheck !== undefined
|
|
195
|
+
? () =>
|
|
196
|
+
buildCondition(table.columns, (eb) =>
|
|
197
|
+
eb.and(
|
|
198
|
+
eb(idColumn.ormName, "=", externalId),
|
|
199
|
+
eb(versionColumn.ormName, "=", versionToCheck),
|
|
200
|
+
),
|
|
201
|
+
)
|
|
202
|
+
: () => buildCondition(table.columns, (eb) => eb(idColumn.ormName, "=", externalId));
|
|
203
|
+
|
|
204
|
+
const query = queryCompiler.updateMany(op.table, {
|
|
205
|
+
where: whereClause,
|
|
206
|
+
set: op.set,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return query
|
|
210
|
+
? {
|
|
211
|
+
query,
|
|
212
|
+
expectedAffectedRows: op.checkVersion ? 1 : null,
|
|
213
|
+
}
|
|
214
|
+
: null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
case "delete": {
|
|
218
|
+
const table = toTable(op.table);
|
|
219
|
+
const idColumn = table.getIdColumn();
|
|
220
|
+
const versionColumn = table.getVersionColumn();
|
|
221
|
+
|
|
222
|
+
// Extract external ID based on whether op.id is FragnoId or string
|
|
223
|
+
const externalId = typeof op.id === "string" ? op.id : op.id.externalId;
|
|
224
|
+
const versionToCheck = getVersionToCheck(op.id, op.checkVersion);
|
|
225
|
+
|
|
226
|
+
// Build WHERE clause that filters by ID and optionally by version
|
|
227
|
+
const whereClause =
|
|
228
|
+
versionToCheck !== undefined
|
|
229
|
+
? () =>
|
|
230
|
+
buildCondition(table.columns, (eb) =>
|
|
231
|
+
eb.and(
|
|
232
|
+
eb(idColumn.ormName, "=", externalId),
|
|
233
|
+
eb(versionColumn.ormName, "=", versionToCheck),
|
|
234
|
+
),
|
|
235
|
+
)
|
|
236
|
+
: () => buildCondition(table.columns, (eb) => eb(idColumn.ormName, "=", externalId));
|
|
237
|
+
|
|
238
|
+
const query = queryCompiler.deleteMany(op.table, {
|
|
239
|
+
where: whereClause,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
return query
|
|
243
|
+
? {
|
|
244
|
+
query,
|
|
245
|
+
expectedAffectedRows: op.checkVersion ? 1 : null,
|
|
246
|
+
}
|
|
247
|
+
: null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Get the version to check for a given ID and checkVersion flag.
|
|
256
|
+
* @returns The version to check or undefined if no check is required.
|
|
257
|
+
* @throws Error if the ID is a string and checkVersion is true.
|
|
258
|
+
*/
|
|
259
|
+
function getVersionToCheck(id: FragnoId | string, checkVersion: boolean): number | undefined {
|
|
260
|
+
if (!checkVersion) {
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (typeof id === "string") {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`Cannot use checkVersion with a string ID. Version checking requires a FragnoId with version information.`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return id.version;
|
|
271
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { Kysely, QueryResult } from "kysely";
|
|
2
|
+
import type { CompiledMutation, MutationResult } from "../../query/unit-of-work";
|
|
3
|
+
|
|
4
|
+
function getAffectedRows(result: QueryResult<unknown>): number {
|
|
5
|
+
const affectedRows =
|
|
6
|
+
result.numAffectedRows ??
|
|
7
|
+
result.numChangedRows ??
|
|
8
|
+
// PGLite returns `affectedRows` instead of `numAffectedRows` or `numChangedRows`
|
|
9
|
+
("affectedRows" in result &&
|
|
10
|
+
(typeof result["affectedRows"] === "number" || typeof result["affectedRows"] === "bigint")
|
|
11
|
+
? result["affectedRows"]
|
|
12
|
+
: undefined);
|
|
13
|
+
|
|
14
|
+
if (affectedRows === undefined) {
|
|
15
|
+
throw new Error("No affected rows found");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (affectedRows > Number.MAX_SAFE_INTEGER) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`affectedRows BigInt value ${affectedRows.toString()} exceeds JS safe integer range`,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return Number(affectedRows);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Execute the retrieval phase of a Unit of Work using Kysely
|
|
29
|
+
*
|
|
30
|
+
* All retrieval queries are executed inside a single transaction to ensure
|
|
31
|
+
* snapshot isolation - all reads see a consistent view of the database.
|
|
32
|
+
*
|
|
33
|
+
* @param kysely - The Kysely database instance
|
|
34
|
+
* @param retrievalBatch - Array of compiled retrieval queries
|
|
35
|
+
* @returns Array of query results matching the retrieval operations order
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* const retrievalResults = await executeKyselyRetrievalPhase(kysely, compiled.retrievalBatch);
|
|
40
|
+
* const [users, posts] = retrievalResults;
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export async function executeKyselyRetrievalPhase(
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
|
+
kysely: Kysely<any>,
|
|
46
|
+
retrievalBatch: (Kysely<unknown>["executeQuery"] extends (query: infer Q) => unknown
|
|
47
|
+
? Q
|
|
48
|
+
: never)[],
|
|
49
|
+
): Promise<unknown[]> {
|
|
50
|
+
// If no retrieval operations, return empty array immediately
|
|
51
|
+
if (retrievalBatch.length === 0) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const retrievalResults: unknown[] = [];
|
|
56
|
+
|
|
57
|
+
// Execute all retrieval queries inside a transaction for snapshot isolation
|
|
58
|
+
await kysely.transaction().execute(async (tx) => {
|
|
59
|
+
for (const query of retrievalBatch) {
|
|
60
|
+
const result = await tx.executeQuery(query);
|
|
61
|
+
retrievalResults.push(result.rows);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return retrievalResults;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Execute the mutation phase of a Unit of Work using Kysely
|
|
70
|
+
*
|
|
71
|
+
* All mutation queries are executed in a transaction with optimistic locking.
|
|
72
|
+
* If any version check fails, the entire transaction is rolled back and
|
|
73
|
+
* success=false is returned.
|
|
74
|
+
*
|
|
75
|
+
* @param kysely - The Kysely database instance
|
|
76
|
+
* @param mutationBatch - Array of compiled mutation queries with expected affected rows
|
|
77
|
+
* @returns Object with success flag and internal IDs from create operations
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```ts
|
|
81
|
+
* const { success, createdInternalIds } = await executeKyselyMutationPhase(kysely, compiled.mutationBatch);
|
|
82
|
+
* if (!success) {
|
|
83
|
+
* console.log("Version conflict detected, retrying...");
|
|
84
|
+
* }
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export async function executeKyselyMutationPhase(
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
89
|
+
kysely: Kysely<any>,
|
|
90
|
+
mutationBatch: CompiledMutation<
|
|
91
|
+
Kysely<unknown>["executeQuery"] extends (query: infer Q) => unknown ? Q : never
|
|
92
|
+
>[],
|
|
93
|
+
): Promise<MutationResult> {
|
|
94
|
+
// If there are no mutations, return success immediately
|
|
95
|
+
if (mutationBatch.length === 0) {
|
|
96
|
+
return { success: true, createdInternalIds: [] };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const createdInternalIds: (bigint | null)[] = [];
|
|
100
|
+
|
|
101
|
+
// Execute mutation batch in a transaction
|
|
102
|
+
try {
|
|
103
|
+
await kysely.transaction().execute(async (tx) => {
|
|
104
|
+
for (const compiledMutation of mutationBatch) {
|
|
105
|
+
const result = await tx.executeQuery(compiledMutation.query);
|
|
106
|
+
|
|
107
|
+
// For creates (expectedAffectedRows === null), try to extract internal ID
|
|
108
|
+
if (compiledMutation.expectedAffectedRows === null) {
|
|
109
|
+
// Check if result has rows (RETURNING clause supported)
|
|
110
|
+
if (Array.isArray(result.rows) && result.rows.length > 0) {
|
|
111
|
+
const row = result.rows[0] as Record<string, unknown>;
|
|
112
|
+
if ("_internalId" in row || "_internal_id" in row) {
|
|
113
|
+
const internalId = (row["_internalId"] ?? row["_internal_id"]) as bigint;
|
|
114
|
+
createdInternalIds.push(internalId);
|
|
115
|
+
} else {
|
|
116
|
+
// RETURNING supported but _internalId not found
|
|
117
|
+
createdInternalIds.push(null);
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
// No RETURNING support (e.g., MySQL)
|
|
121
|
+
createdInternalIds.push(null);
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
// Check affected rows for updates/deletes
|
|
125
|
+
const affectedRows = getAffectedRows(result);
|
|
126
|
+
|
|
127
|
+
if (affectedRows !== compiledMutation.expectedAffectedRows) {
|
|
128
|
+
// Version conflict detected - the UPDATE/DELETE didn't affect the expected number of rows
|
|
129
|
+
// This means either the row doesn't exist or the version has changed
|
|
130
|
+
throw new Error(
|
|
131
|
+
`Version conflict: expected ${compiledMutation.expectedAffectedRows} rows affected, but got ${affectedRows}`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return { success: true, createdInternalIds };
|
|
139
|
+
} catch (error) {
|
|
140
|
+
// Transaction failed - could be version conflict or other constraint violation
|
|
141
|
+
// Return success=false to indicate the UOW should be retried
|
|
142
|
+
if (error instanceof Error && error.message.includes("Version conflict")) {
|
|
143
|
+
return { success: false };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Other database errors should be thrown
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
}
|