@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.
Files changed (200) hide show
  1. package/.turbo/turbo-build.log +137 -13
  2. package/.turbo/turbo-test.log +36 -0
  3. package/CHANGELOG.md +7 -0
  4. package/dist/adapters/adapters.d.ts +18 -0
  5. package/dist/adapters/adapters.d.ts.map +1 -0
  6. package/dist/adapters/drizzle/drizzle-adapter.d.ts +21 -0
  7. package/dist/adapters/drizzle/drizzle-adapter.d.ts.map +1 -0
  8. package/dist/adapters/drizzle/drizzle-adapter.js +62 -0
  9. package/dist/adapters/drizzle/drizzle-adapter.js.map +1 -0
  10. package/dist/adapters/drizzle/drizzle-query.d.ts +17 -0
  11. package/dist/adapters/drizzle/drizzle-query.d.ts.map +1 -0
  12. package/dist/adapters/drizzle/drizzle-query.js +139 -0
  13. package/dist/adapters/drizzle/drizzle-query.js.map +1 -0
  14. package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts +9 -0
  15. package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts.map +1 -0
  16. package/dist/adapters/drizzle/drizzle-uow-compiler.js +300 -0
  17. package/dist/adapters/drizzle/drizzle-uow-compiler.js.map +1 -0
  18. package/dist/adapters/drizzle/drizzle-uow-decoder.js +82 -0
  19. package/dist/adapters/drizzle/drizzle-uow-decoder.js.map +1 -0
  20. package/dist/adapters/drizzle/drizzle-uow-executor.js +125 -0
  21. package/dist/adapters/drizzle/drizzle-uow-executor.js.map +1 -0
  22. package/dist/adapters/drizzle/generate.js +273 -0
  23. package/dist/adapters/drizzle/generate.js.map +1 -0
  24. package/dist/adapters/drizzle/join-column-utils.js +28 -0
  25. package/dist/adapters/drizzle/join-column-utils.js.map +1 -0
  26. package/dist/adapters/drizzle/shared.js +11 -0
  27. package/dist/adapters/drizzle/shared.js.map +1 -0
  28. package/dist/adapters/kysely/kysely-adapter.d.ts +23 -0
  29. package/dist/adapters/kysely/kysely-adapter.d.ts.map +1 -0
  30. package/dist/adapters/kysely/kysely-adapter.js +119 -0
  31. package/dist/adapters/kysely/kysely-adapter.js.map +1 -0
  32. package/dist/adapters/kysely/kysely-query-builder.js +306 -0
  33. package/dist/adapters/kysely/kysely-query-builder.js.map +1 -0
  34. package/dist/adapters/kysely/kysely-query-compiler.js +67 -0
  35. package/dist/adapters/kysely/kysely-query-compiler.js.map +1 -0
  36. package/dist/adapters/kysely/kysely-query.js +158 -0
  37. package/dist/adapters/kysely/kysely-query.js.map +1 -0
  38. package/dist/adapters/kysely/kysely-uow-compiler.js +139 -0
  39. package/dist/adapters/kysely/kysely-uow-compiler.js.map +1 -0
  40. package/dist/adapters/kysely/kysely-uow-executor.js +89 -0
  41. package/dist/adapters/kysely/kysely-uow-executor.js.map +1 -0
  42. package/dist/adapters/kysely/migration/execute.js +176 -0
  43. package/dist/adapters/kysely/migration/execute.js.map +1 -0
  44. package/dist/fragment.d.ts +54 -0
  45. package/dist/fragment.d.ts.map +1 -0
  46. package/dist/fragment.js +92 -0
  47. package/dist/fragment.js.map +1 -0
  48. package/dist/id.d.ts +2 -0
  49. package/dist/migration-engine/auto-from-schema.js +116 -0
  50. package/dist/migration-engine/auto-from-schema.js.map +1 -0
  51. package/dist/migration-engine/create.d.ts +41 -0
  52. package/dist/migration-engine/create.d.ts.map +1 -0
  53. package/dist/migration-engine/create.js +58 -0
  54. package/dist/migration-engine/create.js.map +1 -0
  55. package/dist/migration-engine/shared.d.ts +90 -0
  56. package/dist/migration-engine/shared.d.ts.map +1 -0
  57. package/dist/migration-engine/shared.js +8 -0
  58. package/dist/migration-engine/shared.js.map +1 -0
  59. package/dist/mod.d.ts +55 -2
  60. package/dist/mod.d.ts.map +1 -1
  61. package/dist/mod.js +111 -2
  62. package/dist/mod.js.map +1 -1
  63. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column-builder.js +108 -0
  64. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column-builder.js.map +1 -0
  65. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column.js +55 -0
  66. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column.js.map +1 -0
  67. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/entity.js +18 -0
  68. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/entity.js.map +1 -0
  69. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/common.js +183 -0
  70. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/common.js.map +1 -0
  71. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/enum.js +58 -0
  72. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/enum.js.map +1 -0
  73. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/foreign-keys.js +68 -0
  74. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/foreign-keys.js.map +1 -0
  75. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/unique-constraint.js +56 -0
  76. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/unique-constraint.js.map +1 -0
  77. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/utils/array.js +65 -0
  78. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/utils/array.js.map +1 -0
  79. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/conditions.js +81 -0
  80. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/conditions.js.map +1 -0
  81. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/select.js +13 -0
  82. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/select.js.map +1 -0
  83. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/functions/aggregate.js +10 -0
  84. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/functions/aggregate.js.map +1 -0
  85. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/sql.js +372 -0
  86. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/sql.js.map +1 -0
  87. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/subquery.js +23 -0
  88. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/subquery.js.map +1 -0
  89. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.js +62 -0
  90. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.js.map +1 -0
  91. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.utils.js +6 -0
  92. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.utils.js.map +1 -0
  93. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing-utils.js +8 -0
  94. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing-utils.js.map +1 -0
  95. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing.js +8 -0
  96. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing.js.map +1 -0
  97. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/view-common.js +6 -0
  98. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/view-common.js.map +1 -0
  99. package/dist/query/condition-builder.d.ts +41 -0
  100. package/dist/query/condition-builder.d.ts.map +1 -0
  101. package/dist/query/condition-builder.js +93 -0
  102. package/dist/query/condition-builder.js.map +1 -0
  103. package/dist/query/cursor.d.ts +88 -0
  104. package/dist/query/cursor.d.ts.map +1 -0
  105. package/dist/query/cursor.js +103 -0
  106. package/dist/query/cursor.js.map +1 -0
  107. package/dist/query/orm/orm.d.ts +18 -0
  108. package/dist/query/orm/orm.d.ts.map +1 -0
  109. package/dist/query/orm/orm.js +48 -0
  110. package/dist/query/orm/orm.js.map +1 -0
  111. package/dist/query/query.d.ts +79 -0
  112. package/dist/query/query.d.ts.map +1 -0
  113. package/dist/query/query.js +1 -0
  114. package/dist/query/result-transform.js +155 -0
  115. package/dist/query/result-transform.js.map +1 -0
  116. package/dist/query/unit-of-work.d.ts +435 -0
  117. package/dist/query/unit-of-work.d.ts.map +1 -0
  118. package/dist/query/unit-of-work.js +549 -0
  119. package/dist/query/unit-of-work.js.map +1 -0
  120. package/dist/schema/create.d.ts +273 -116
  121. package/dist/schema/create.d.ts.map +1 -1
  122. package/dist/schema/create.js +410 -222
  123. package/dist/schema/create.js.map +1 -1
  124. package/dist/schema/serialize.js +101 -0
  125. package/dist/schema/serialize.js.map +1 -0
  126. package/dist/schema-generator/schema-generator.d.ts +15 -0
  127. package/dist/schema-generator/schema-generator.d.ts.map +1 -0
  128. package/dist/shared/providers.d.ts +6 -0
  129. package/dist/shared/providers.d.ts.map +1 -0
  130. package/dist/util/import-generator.js +26 -0
  131. package/dist/util/import-generator.js.map +1 -0
  132. package/dist/util/parse.js +15 -0
  133. package/dist/util/parse.js.map +1 -0
  134. package/dist/util/types.d.ts +8 -0
  135. package/dist/util/types.d.ts.map +1 -0
  136. package/package.json +63 -2
  137. package/src/adapters/adapters.ts +22 -0
  138. package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +433 -0
  139. package/src/adapters/drizzle/drizzle-adapter.test.ts +122 -0
  140. package/src/adapters/drizzle/drizzle-adapter.ts +118 -0
  141. package/src/adapters/drizzle/drizzle-query.ts +234 -0
  142. package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +1084 -0
  143. package/src/adapters/drizzle/drizzle-uow-compiler.ts +546 -0
  144. package/src/adapters/drizzle/drizzle-uow-decoder.ts +165 -0
  145. package/src/adapters/drizzle/drizzle-uow-executor.ts +213 -0
  146. package/src/adapters/drizzle/generate.test.ts +643 -0
  147. package/src/adapters/drizzle/generate.ts +481 -0
  148. package/src/adapters/drizzle/join-column-utils.test.ts +79 -0
  149. package/src/adapters/drizzle/join-column-utils.ts +39 -0
  150. package/src/adapters/drizzle/migrate-drizzle.test.ts +226 -0
  151. package/src/adapters/drizzle/shared.ts +22 -0
  152. package/src/adapters/drizzle/test-utils.ts +56 -0
  153. package/src/adapters/kysely/kysely-adapter-pglite.test.ts +789 -0
  154. package/src/adapters/kysely/kysely-adapter.ts +196 -0
  155. package/src/adapters/kysely/kysely-query-builder.test.ts +1344 -0
  156. package/src/adapters/kysely/kysely-query-builder.ts +611 -0
  157. package/src/adapters/kysely/kysely-query-compiler.ts +124 -0
  158. package/src/adapters/kysely/kysely-query.ts +254 -0
  159. package/src/adapters/kysely/kysely-uow-compiler.test.ts +916 -0
  160. package/src/adapters/kysely/kysely-uow-compiler.ts +271 -0
  161. package/src/adapters/kysely/kysely-uow-executor.ts +149 -0
  162. package/src/adapters/kysely/kysely-uow-joins.test.ts +811 -0
  163. package/src/adapters/kysely/migration/execute-mysql.test.ts +1173 -0
  164. package/src/adapters/kysely/migration/execute-postgres.test.ts +2657 -0
  165. package/src/adapters/kysely/migration/execute.ts +382 -0
  166. package/src/adapters/kysely/migration/kysely-migrator.test.ts +197 -0
  167. package/src/fragment.test.ts +287 -0
  168. package/src/fragment.ts +198 -0
  169. package/src/migration-engine/auto-from-schema.test.ts +118 -58
  170. package/src/migration-engine/auto-from-schema.ts +103 -32
  171. package/src/migration-engine/create.test.ts +34 -46
  172. package/src/migration-engine/create.ts +41 -26
  173. package/src/migration-engine/shared.ts +26 -6
  174. package/src/mod.ts +197 -1
  175. package/src/query/condition-builder.test.ts +379 -0
  176. package/src/query/condition-builder.ts +294 -0
  177. package/src/query/cursor.test.ts +296 -0
  178. package/src/query/cursor.ts +147 -0
  179. package/src/query/orm/orm.ts +92 -0
  180. package/src/query/query-type.test.ts +429 -0
  181. package/src/query/query.ts +200 -0
  182. package/src/query/result-transform.test.ts +795 -0
  183. package/src/query/result-transform.ts +247 -0
  184. package/src/query/unit-of-work-types.test.ts +192 -0
  185. package/src/query/unit-of-work.test.ts +947 -0
  186. package/src/query/unit-of-work.ts +1199 -0
  187. package/src/schema/create.test.ts +653 -110
  188. package/src/schema/create.ts +708 -337
  189. package/src/schema/serialize.test.ts +559 -0
  190. package/src/schema/serialize.ts +359 -0
  191. package/src/schema-generator/schema-generator.ts +12 -0
  192. package/src/shared/config.ts +0 -8
  193. package/src/util/import-generator.ts +28 -0
  194. package/src/util/parse.ts +16 -0
  195. package/src/util/types.ts +4 -0
  196. package/tsconfig.json +1 -1
  197. package/tsdown.config.ts +11 -1
  198. package/vitest.config.ts +3 -0
  199. /package/dist/{cuid.js → id.js} +0 -0
  200. /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
+ }