@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,546 @@
1
+ import * as Drizzle from "drizzle-orm";
2
+ import type { AnyColumn, AnySchema, AnyTable, FragnoId } from "../../schema/create";
3
+ import { Column } from "../../schema/create";
4
+ import type {
5
+ CompiledMutation,
6
+ MutationOperation,
7
+ RetrievalOperation,
8
+ UOWCompiler,
9
+ } from "../../query/unit-of-work";
10
+ import { buildCondition, type Condition } from "../../query/condition-builder";
11
+ import type { DrizzleConfig } from "./drizzle-adapter";
12
+ import { type ColumnType, type TableType, parseDrizzle } from "./shared";
13
+ import { encodeValues, ReferenceSubquery } from "../../query/result-transform";
14
+ import { serialize } from "../../schema/serialize";
15
+ import { decodeCursor, serializeCursorValues } from "../../query/cursor";
16
+ import type { CompiledJoin } from "../../query/orm/orm";
17
+ import { getOrderedJoinColumns } from "./join-column-utils";
18
+
19
+ export type DrizzleCompiledQuery = {
20
+ sql: string;
21
+ params: unknown[];
22
+ };
23
+
24
+ /**
25
+ * Create a Drizzle-specific Unit of Work compiler
26
+ *
27
+ * This compiler translates UOW operations into Drizzle query functions
28
+ * that can be executed as a batch/transaction.
29
+ *
30
+ * @param schema - The database schema
31
+ * @param config - Drizzle configuration
32
+ * @param onQuery - Optional callback to receive compiled queries for logging/debugging
33
+ * @returns A UOWCompiler instance for Drizzle
34
+ */
35
+ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
36
+ schema: TSchema,
37
+ config: DrizzleConfig,
38
+ onQuery?: (query: DrizzleCompiledQuery) => void,
39
+ ): UOWCompiler<TSchema, DrizzleCompiledQuery> {
40
+ const [db, drizzleTables] = parseDrizzle(config.db);
41
+ const { provider } = config;
42
+
43
+ /**
44
+ * Convert a Fragno table to a Drizzle table
45
+ * @throws Error if table is not found in Drizzle schema
46
+ */
47
+ function toDrizzleTable(table: AnyTable): TableType {
48
+ const tableName = table.ormName;
49
+ const out = drizzleTables[tableName];
50
+ if (out) {
51
+ return out;
52
+ }
53
+
54
+ throw new Error(
55
+ `[Drizzle] Unknown table name ${tableName}, is it included in your Drizzle schema?`,
56
+ );
57
+ }
58
+
59
+ /**
60
+ * Convert a Fragno column to a Drizzle column
61
+ * @throws Error if column is not found in Drizzle table
62
+ */
63
+ function toDrizzleColumn(col: AnyColumn): ColumnType {
64
+ const fragnoTable = schema.tables[col.tableName];
65
+ if (!fragnoTable) {
66
+ throw new Error(`[Drizzle] Unknown table ${col.tableName} for column ${col.ormName}.`);
67
+ }
68
+
69
+ const table = toDrizzleTable(fragnoTable);
70
+ const out = table[col.ormName];
71
+ if (out) {
72
+ return out;
73
+ }
74
+
75
+ throw new Error(`[Drizzle] Unknown column name ${col.ormName} in ${fragnoTable.ormName}.`);
76
+ }
77
+
78
+ /**
79
+ * Build a WHERE clause from a condition using Drizzle's query builder
80
+ */
81
+ function buildWhere(condition: Condition): Drizzle.SQL | undefined {
82
+ if (condition.type === "compare") {
83
+ const left = toDrizzleColumn(condition.a);
84
+ const op = condition.operator;
85
+ let right = condition.b;
86
+ if (right instanceof Column) {
87
+ right = toDrizzleColumn(right);
88
+ } else {
89
+ // Serialize non-Column values (e.g., FragnoId -> string, Date -> number for SQLite)
90
+ right = serialize(right, condition.a, provider);
91
+ }
92
+
93
+ switch (op) {
94
+ case "=":
95
+ return Drizzle.eq(left, right);
96
+ case "!=":
97
+ return Drizzle.ne(left, right);
98
+ case ">":
99
+ return Drizzle.gt(left, right);
100
+ case ">=":
101
+ return Drizzle.gte(left, right);
102
+ case "<":
103
+ return Drizzle.lt(left, right);
104
+ case "<=":
105
+ return Drizzle.lte(left, right);
106
+ case "in": {
107
+ return Drizzle.inArray(left, right as never[]);
108
+ }
109
+ case "not in":
110
+ return Drizzle.notInArray(left, right as never[]);
111
+ case "is":
112
+ return right === null ? Drizzle.isNull(left) : Drizzle.eq(left, right);
113
+ case "is not":
114
+ return right === null ? Drizzle.isNotNull(left) : Drizzle.ne(left, right);
115
+ case "contains": {
116
+ right =
117
+ typeof right === "string" ? `%${right}%` : Drizzle.sql`concat('%', ${right}, '%')`;
118
+ return Drizzle.like(left, right as string);
119
+ }
120
+ case "not contains": {
121
+ right =
122
+ typeof right === "string" ? `%${right}%` : Drizzle.sql`concat('%', ${right}, '%')`;
123
+ return Drizzle.notLike(left, right as string);
124
+ }
125
+ case "ends with": {
126
+ right = typeof right === "string" ? `%${right}` : Drizzle.sql`concat('%', ${right})`;
127
+ return Drizzle.like(left, right as string);
128
+ }
129
+ case "not ends with": {
130
+ right = typeof right === "string" ? `%${right}` : Drizzle.sql`concat('%', ${right})`;
131
+ return Drizzle.notLike(left, right as string);
132
+ }
133
+ case "starts with": {
134
+ right = typeof right === "string" ? `${right}%` : Drizzle.sql`concat(${right}, '%')`;
135
+ return Drizzle.like(left, right as string);
136
+ }
137
+ case "not starts with": {
138
+ right = typeof right === "string" ? `${right}%` : Drizzle.sql`concat(${right}, '%')`;
139
+ return Drizzle.notLike(left, right as string);
140
+ }
141
+
142
+ default:
143
+ throw new Error(`Unsupported operator: ${op}`);
144
+ }
145
+ }
146
+
147
+ if (condition.type === "and") {
148
+ return Drizzle.and(...condition.items.map((item) => buildWhere(item)));
149
+ }
150
+
151
+ if (condition.type === "not") {
152
+ const result = buildWhere(condition.item);
153
+ if (!result) return;
154
+
155
+ return Drizzle.not(result);
156
+ }
157
+
158
+ return Drizzle.or(...condition.items.map((item) => buildWhere(item)));
159
+ }
160
+
161
+ /**
162
+ * Process reference subqueries in encoded values, converting them to Drizzle SQL subqueries
163
+ */
164
+ function processReferenceSubqueries(values: Record<string, unknown>): Record<string, unknown> {
165
+ const processed: Record<string, unknown> = {};
166
+
167
+ for (const [key, value] of Object.entries(values)) {
168
+ if (value instanceof ReferenceSubquery) {
169
+ const refTable = value.referencedTable;
170
+ const externalId = value.externalIdValue;
171
+ const internalIdCol = refTable.getInternalIdColumn();
172
+ const idCol = refTable.getIdColumn();
173
+ const drizzleRefTable = toDrizzleTable(refTable);
174
+ const drizzleIdCol = toDrizzleColumn(idCol);
175
+
176
+ // Create a parameterized SQL subquery using Drizzle's query builder
177
+ // Safe cast: we're building a SQL subquery that returns a single bigint value
178
+ processed[key] = db
179
+ .select({ value: drizzleRefTable[internalIdCol.ormName] })
180
+ .from(drizzleRefTable)
181
+ .where(Drizzle.eq(drizzleIdCol, externalId))
182
+ .limit(1) as unknown;
183
+ } else {
184
+ processed[key] = value;
185
+ }
186
+ }
187
+
188
+ return processed;
189
+ }
190
+
191
+ /**
192
+ * Get table from schema by name
193
+ * @throws Error if table is not found in schema
194
+ */
195
+ function getTable(name: unknown): AnyTable {
196
+ const table = schema.tables[name as string];
197
+ if (!table) {
198
+ throw new Error(`Invalid table name ${name}.`);
199
+ }
200
+ return table;
201
+ }
202
+
203
+ /**
204
+ * Get the version to check for a given ID and checkVersion flag.
205
+ * @returns The version to check or undefined if no check is required.
206
+ * @throws Error if the ID is a string and checkVersion is true.
207
+ */
208
+ function getVersionToCheck(id: FragnoId | string, checkVersion: boolean): number | undefined {
209
+ if (!checkVersion) {
210
+ return undefined;
211
+ }
212
+
213
+ if (typeof id === "string") {
214
+ throw new Error(
215
+ `Cannot use checkVersion with a string ID. Version checking requires a FragnoId with version information.`,
216
+ );
217
+ }
218
+
219
+ return id.version;
220
+ }
221
+
222
+ /**
223
+ * Process joins recursively to support nested joins with orderBy and limit
224
+ */
225
+ function processJoins(
226
+ joins: CompiledJoin[],
227
+ ): Record<string, Drizzle.DBQueryConfig<"many", boolean>> {
228
+ const result: Record<string, Drizzle.DBQueryConfig<"many", boolean>> = {};
229
+
230
+ for (const join of joins) {
231
+ const { options, relation } = join;
232
+
233
+ if (!options) {
234
+ continue;
235
+ }
236
+
237
+ const targetTable = relation.table;
238
+ const joinName = relation.name;
239
+
240
+ // Build columns for this join using shared utility
241
+ const selectOption = options.select === undefined ? true : options.select;
242
+ const orderedColumns = getOrderedJoinColumns(targetTable, selectOption);
243
+ const joinColumns: Record<string, boolean> = {};
244
+ for (const colName of orderedColumns) {
245
+ joinColumns[colName] = true;
246
+ }
247
+
248
+ // Build orderBy for this join
249
+ let joinOrderBy: Drizzle.SQL[] | undefined;
250
+ if (options.orderBy && options.orderBy.length > 0) {
251
+ joinOrderBy = options.orderBy.map(([col, direction]) => {
252
+ const drizzleCol = toDrizzleColumn(col);
253
+ return direction === "asc" ? Drizzle.asc(drizzleCol) : Drizzle.desc(drizzleCol);
254
+ });
255
+ }
256
+
257
+ // Build WHERE clause for this join if provided
258
+ let joinWhere: Drizzle.SQL | undefined;
259
+ if (options.where) {
260
+ joinWhere = buildWhere(options.where);
261
+ }
262
+
263
+ // Build the join config
264
+ const joinConfig: Drizzle.DBQueryConfig<"many", boolean> = {
265
+ columns: joinColumns,
266
+ orderBy: joinOrderBy,
267
+ limit: options.limit,
268
+ where: joinWhere,
269
+ };
270
+
271
+ // Recursively process nested joins
272
+ if (options.join && options.join.length > 0) {
273
+ joinConfig.with = processJoins(options.join);
274
+ }
275
+
276
+ result[joinName] = joinConfig;
277
+ }
278
+
279
+ return result;
280
+ }
281
+
282
+ return {
283
+ compileRetrievalOperation(op: RetrievalOperation<TSchema>): DrizzleCompiledQuery | null {
284
+ switch (op.type) {
285
+ case "count": {
286
+ // Build WHERE clause
287
+ let whereClause: Drizzle.SQL | undefined;
288
+ if (op.options.where) {
289
+ const condition = buildCondition(op.table.columns, op.options.where);
290
+ if (condition === false) {
291
+ // Never matches - return null
292
+ return null;
293
+ }
294
+ if (condition !== true) {
295
+ whereClause = buildWhere(condition);
296
+ }
297
+ }
298
+
299
+ const drizzleTable = toDrizzleTable(op.table);
300
+ const query = db.select({ count: Drizzle.count() }).from(drizzleTable);
301
+
302
+ const compiledQuery = whereClause ? query.where(whereClause).toSQL() : query.toSQL();
303
+ onQuery?.(compiledQuery);
304
+ return compiledQuery;
305
+ }
306
+
307
+ case "find": {
308
+ const {
309
+ useIndex: _useIndex,
310
+ orderByIndex,
311
+ joins,
312
+ after,
313
+ before,
314
+ pageSize,
315
+ ...findOptions
316
+ } = op.options;
317
+
318
+ // Get index columns for ordering and cursor pagination
319
+ let indexColumns: AnyColumn[] = [];
320
+ let orderDirection: "asc" | "desc" = "asc";
321
+
322
+ if (orderByIndex) {
323
+ const index = op.table.indexes[orderByIndex.indexName];
324
+ orderDirection = orderByIndex.direction;
325
+
326
+ if (!index) {
327
+ // If _primary index doesn't exist, fall back to ID column
328
+ if (orderByIndex.indexName === "_primary") {
329
+ indexColumns = [op.table.getIdColumn()];
330
+ } else {
331
+ throw new Error(
332
+ `Index "${orderByIndex.indexName}" not found on table "${op.table.name}"`,
333
+ );
334
+ }
335
+ } else {
336
+ indexColumns = index.columns;
337
+ }
338
+ }
339
+
340
+ // Convert orderByIndex to orderBy format
341
+ let orderBy: Drizzle.SQL[] | undefined;
342
+ if (indexColumns.length > 0) {
343
+ orderBy = indexColumns.map((col) => {
344
+ const drizzleCol = toDrizzleColumn(col);
345
+ return orderDirection === "asc" ? Drizzle.asc(drizzleCol) : Drizzle.desc(drizzleCol);
346
+ });
347
+ }
348
+
349
+ // Build query configuration
350
+ const columns: Record<string, boolean> = {};
351
+ const select = findOptions.select;
352
+
353
+ if (select === true || select === undefined) {
354
+ for (const col of Object.values(op.table.columns)) {
355
+ columns[col.ormName] = true;
356
+ }
357
+ } else {
358
+ for (const k of select) {
359
+ columns[op.table.columns[k].ormName] = true;
360
+ }
361
+ // Always include hidden columns (for FragnoId construction with internal ID and version)
362
+ for (const col of Object.values(op.table.columns)) {
363
+ if (col.isHidden && !columns[col.ormName]) {
364
+ columns[col.ormName] = true;
365
+ }
366
+ }
367
+ }
368
+
369
+ // Build WHERE clause with cursor conditions
370
+ const whereClauses: Drizzle.SQL[] = [];
371
+
372
+ // Add user-defined where clause
373
+ if (findOptions.where) {
374
+ const condition = buildCondition(op.table.columns, findOptions.where);
375
+ if (condition === false) {
376
+ // Never matches - return null to indicate this query should be skipped
377
+ return null;
378
+ }
379
+ if (condition !== true) {
380
+ const clause = buildWhere(condition);
381
+ if (clause) {
382
+ whereClauses.push(clause);
383
+ }
384
+ }
385
+ }
386
+
387
+ // Add cursor-based pagination conditions
388
+ if ((after || before) && indexColumns.length > 0) {
389
+ const cursor = after || before;
390
+ const cursorData = decodeCursor(cursor!);
391
+ const serializedValues = serializeCursorValues(cursorData, indexColumns, provider);
392
+
393
+ // Build tuple comparison for cursor pagination
394
+ // For "after" with "asc": (col1, col2, ...) > (val1, val2, ...)
395
+ // For "before" with "desc": reverse the comparison
396
+ const isAfter = !!after;
397
+ const useGreaterThan =
398
+ (isAfter && orderDirection === "asc") || (!isAfter && orderDirection === "desc");
399
+
400
+ if (indexColumns.length === 1) {
401
+ // Simple single-column case
402
+ const col = toDrizzleColumn(indexColumns[0]!);
403
+ const val = serializedValues[indexColumns[0]!.ormName];
404
+ whereClauses.push(useGreaterThan ? Drizzle.gt(col, val) : Drizzle.lt(col, val));
405
+ } else {
406
+ // Multi-column tuple comparison using SQL
407
+ const drizzleCols = indexColumns.map((c) => toDrizzleColumn(c));
408
+ const vals = indexColumns.map((c) => serializedValues[c.ormName]);
409
+ const operator = useGreaterThan ? ">" : "<";
410
+ // Safe cast: building a SQL comparison expression for cursor pagination
411
+ // Build the tuple comparison: (col1, col2) > (val1, val2)
412
+ const colsSQL = Drizzle.sql.join(drizzleCols, Drizzle.sql.raw(", "));
413
+ const valsSQL = Drizzle.sql.join(
414
+ vals.map((v) => Drizzle.sql`${v}`),
415
+ Drizzle.sql.raw(", "),
416
+ );
417
+ whereClauses.push(
418
+ Drizzle.sql`(${colsSQL}) ${Drizzle.sql.raw(operator)} (${valsSQL})`,
419
+ );
420
+ }
421
+ }
422
+
423
+ const whereClause = whereClauses.length > 0 ? Drizzle.and(...whereClauses) : undefined;
424
+
425
+ const queryConfig: Drizzle.DBQueryConfig<"many", boolean> = {
426
+ columns,
427
+ limit: pageSize,
428
+ where: whereClause,
429
+ orderBy,
430
+ with: {},
431
+ };
432
+
433
+ // Process joins recursively to support nested joins
434
+ if (joins) {
435
+ queryConfig.with = processJoins(joins);
436
+ }
437
+
438
+ const compiledQuery = db.query[op.table.ormName].findMany(queryConfig).toSQL();
439
+ onQuery?.(compiledQuery);
440
+ return compiledQuery;
441
+ }
442
+ }
443
+ },
444
+
445
+ compileMutationOperation(
446
+ op: MutationOperation<TSchema>,
447
+ ): CompiledMutation<DrizzleCompiledQuery> | null {
448
+ switch (op.type) {
449
+ case "create": {
450
+ const table = getTable(op.table);
451
+ const drizzleTable = toDrizzleTable(table);
452
+ // encodeValues now handles runtime defaults automatically
453
+ const encodedValues = encodeValues(op.values, table, true, provider);
454
+ const values = processReferenceSubqueries(encodedValues);
455
+
456
+ const compiledQuery = db.insert(drizzleTable).values(values).toSQL();
457
+ onQuery?.(compiledQuery);
458
+ return {
459
+ query: compiledQuery,
460
+ expectedAffectedRows: null, // creates don't need affected row checks
461
+ };
462
+ }
463
+
464
+ case "update": {
465
+ const table = getTable(op.table);
466
+ const idColumn = table.getIdColumn();
467
+ const versionColumn = table.getVersionColumn();
468
+ const drizzleTable = toDrizzleTable(table);
469
+
470
+ const externalId = typeof op.id === "string" ? op.id : op.id.externalId;
471
+ const versionToCheck = getVersionToCheck(op.id, op.checkVersion);
472
+
473
+ // Build WHERE clause that filters by ID and optionally by version
474
+ const condition =
475
+ versionToCheck !== undefined
476
+ ? buildCondition(table.columns, (eb) =>
477
+ eb.and(
478
+ eb(idColumn.ormName, "=", externalId),
479
+ eb(versionColumn.ormName, "=", versionToCheck),
480
+ ),
481
+ )
482
+ : buildCondition(table.columns, (eb) => eb(idColumn.ormName, "=", externalId));
483
+
484
+ // Handle boolean cases
485
+ if (condition === false) {
486
+ // Never matches - skip this operation
487
+ return null;
488
+ }
489
+
490
+ const whereClause = condition === true ? undefined : buildWhere(condition);
491
+ const encodedSetValues = encodeValues(op.set, table, false, provider);
492
+ const setValues = processReferenceSubqueries(encodedSetValues);
493
+
494
+ // Automatically increment _version for optimistic concurrency control
495
+ // Safe cast: we're building a SQL expression for incrementing the version
496
+ setValues[versionColumn.ormName] = Drizzle.sql.raw(
497
+ `COALESCE(${versionColumn.ormName}, 0) + 1`,
498
+ ) as unknown;
499
+
500
+ const compiledQuery = db.update(drizzleTable).set(setValues).where(whereClause).toSQL();
501
+ onQuery?.(compiledQuery);
502
+ return {
503
+ query: compiledQuery,
504
+ expectedAffectedRows: op.checkVersion ? 1 : null,
505
+ };
506
+ }
507
+
508
+ case "delete": {
509
+ const table = getTable(op.table);
510
+ const idColumn = table.getIdColumn();
511
+ const versionColumn = table.getVersionColumn();
512
+ const drizzleTable = toDrizzleTable(table);
513
+
514
+ const externalId = typeof op.id === "string" ? op.id : op.id.externalId;
515
+ const versionToCheck = getVersionToCheck(op.id, op.checkVersion);
516
+
517
+ // Build WHERE clause that filters by ID and optionally by version
518
+ const condition =
519
+ versionToCheck !== undefined
520
+ ? buildCondition(table.columns, (eb) =>
521
+ eb.and(
522
+ eb(idColumn.ormName, "=", externalId),
523
+ eb(versionColumn.ormName, "=", versionToCheck),
524
+ ),
525
+ )
526
+ : buildCondition(table.columns, (eb) => eb(idColumn.ormName, "=", externalId));
527
+
528
+ // Handle boolean cases
529
+ if (condition === false) {
530
+ // Never matches - skip this operation
531
+ return null;
532
+ }
533
+
534
+ const whereClause = condition === true ? undefined : buildWhere(condition);
535
+
536
+ const compiledQuery = db.delete(drizzleTable).where(whereClause).toSQL();
537
+ onQuery?.(compiledQuery);
538
+ return {
539
+ query: compiledQuery,
540
+ expectedAffectedRows: op.checkVersion ? 1 : null,
541
+ };
542
+ }
543
+ }
544
+ },
545
+ };
546
+ }
@@ -0,0 +1,165 @@
1
+ import type { AnySchema, AnyTable } from "../../schema/create";
2
+ import type { SQLProvider } from "../../shared/providers";
3
+ import type { RetrievalOperation, UOWDecoder } from "../../query/unit-of-work";
4
+ import { decodeResult } from "../../query/result-transform";
5
+ import type { DrizzleResult } from "./drizzle-query";
6
+ import { getOrderedJoinColumns } from "./join-column-utils";
7
+
8
+ /**
9
+ * Join information with nested join support
10
+ */
11
+ interface JoinInfo {
12
+ relation: { name: string; table: AnyTable };
13
+ options:
14
+ | {
15
+ select: true | string[];
16
+ join?: JoinInfo[];
17
+ }
18
+ | false;
19
+ }
20
+
21
+ /**
22
+ * Recursively transform join arrays to objects, handling nested joins.
23
+ *
24
+ * Drizzle joins use `json_build_array` where nested join data is appended after the parent's columns.
25
+ * For example, if post has columns [id, title, content, _internalId, _version] and a nested author join,
26
+ * the array will be: [id, title, content, _internalId, _version, authorArray]
27
+ *
28
+ * @param value - The join array from Drizzle
29
+ * @param joinInfo - Join metadata including nested joins
30
+ * @param relationName - Name of the current relation (for prefixing column names)
31
+ * @returns Object with flattened keys (relationName:columnName) for all levels
32
+ */
33
+ function transformJoinArray(
34
+ value: unknown[],
35
+ joinInfo: JoinInfo,
36
+ relationName: string,
37
+ ): Record<string, unknown> {
38
+ const result: Record<string, unknown> = {};
39
+
40
+ if (joinInfo.options === false) {
41
+ return result;
42
+ }
43
+
44
+ const targetTable = joinInfo.relation.table;
45
+
46
+ // Get ordered columns using shared utility (must match compiler's column order)
47
+ const orderedSelectedColumns = getOrderedJoinColumns(targetTable, joinInfo.options.select);
48
+
49
+ // Map column values to flattened format: relationName:columnName
50
+ for (let i = 0; i < orderedSelectedColumns.length && i < value.length; i++) {
51
+ const columnName = orderedSelectedColumns[i];
52
+ if (columnName) {
53
+ result[`${relationName}:${columnName}`] = value[i];
54
+ }
55
+ }
56
+
57
+ // Handle nested joins - they appear after all columns in the array
58
+ if (joinInfo.options.join && joinInfo.options.join.length > 0) {
59
+ let nestedArrayIndex = orderedSelectedColumns.length;
60
+
61
+ for (const nestedJoin of joinInfo.options.join) {
62
+ const nestedRelationName = `${relationName}:${nestedJoin.relation.name}`;
63
+ const nestedValue = value[nestedArrayIndex];
64
+
65
+ if (Array.isArray(nestedValue)) {
66
+ // Recursively transform nested join
67
+ const nestedResult = transformJoinArray(nestedValue, nestedJoin, nestedRelationName);
68
+ Object.assign(result, nestedResult);
69
+ }
70
+
71
+ nestedArrayIndex++;
72
+ }
73
+ }
74
+
75
+ return result;
76
+ }
77
+
78
+ /**
79
+ * Drizzle joins using `json_build_array` so the result is a tuple of values that we need to map to
80
+ * the correct columns. This function handles nested joins recursively.
81
+ *
82
+ * @param row - Raw database result row that may contain join arrays
83
+ * @param op - The retrieval operation containing join information
84
+ * @returns Transformed row with join arrays converted to objects
85
+ */
86
+ function transformJoinArraysToObjects(
87
+ row: Record<string, unknown>,
88
+ op: {
89
+ type: string;
90
+ table: AnyTable;
91
+ options?: {
92
+ joins?: JoinInfo[];
93
+ };
94
+ },
95
+ ): Record<string, unknown> {
96
+ // Only process find operations with joins
97
+ if (op.type !== "find" || !op.options?.joins) {
98
+ return row;
99
+ }
100
+
101
+ const transformedRow = { ...row };
102
+
103
+ for (const join of op.options.joins) {
104
+ const relationName = join.relation.name;
105
+ const value = row[relationName];
106
+
107
+ // Skip if not an array (join didn't return data)
108
+ if (!Array.isArray(value)) {
109
+ continue;
110
+ }
111
+
112
+ // Skip if join options are false (join was disabled)
113
+ if (join.options === false) {
114
+ continue;
115
+ }
116
+
117
+ // Get the target table for this relation
118
+ const relation = op.table.relations[relationName];
119
+ if (!relation) {
120
+ continue;
121
+ }
122
+
123
+ // Recursively transform this join and its nested joins
124
+ const joinResult = transformJoinArray(value, join, relationName);
125
+ Object.assign(transformedRow, joinResult);
126
+
127
+ // Remove the original array property
128
+ delete transformedRow[relationName];
129
+ }
130
+
131
+ return transformedRow;
132
+ }
133
+
134
+ export function createDrizzleUOWDecoder<TSchema extends AnySchema>(
135
+ _schema: TSchema,
136
+ provider: SQLProvider,
137
+ ): UOWDecoder<TSchema, DrizzleResult> {
138
+ return (rawResults, ops) => {
139
+ if (rawResults.length !== ops.length) {
140
+ throw new Error("rawResults and ops must have the same length");
141
+ }
142
+
143
+ return rawResults.map((result, index) => {
144
+ const op = ops[index] as RetrievalOperation<TSchema>;
145
+ if (!op) {
146
+ throw new Error("op must be defined");
147
+ }
148
+
149
+ // Handle count operations - return the count value directly
150
+ if (op.type === "count") {
151
+ if (result.rows.length > 0 && result.rows[0]) {
152
+ const row = result.rows[0];
153
+ return (row as Record<string, unknown>)["count"] as number;
154
+ }
155
+ return 0;
156
+ }
157
+
158
+ // Handle find operations - decode each row
159
+ return result.rows.map((row) => {
160
+ const transformedRow = transformJoinArraysToObjects(row, op);
161
+ return decodeResult(transformedRow, op.table, provider);
162
+ });
163
+ });
164
+ };
165
+ }