@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,1084 @@
1
+ import { assert, beforeAll, describe, expect, it } from "vitest";
2
+ import {
3
+ column,
4
+ FragnoId,
5
+ idColumn,
6
+ referenceColumn,
7
+ schema,
8
+ type AnySchema,
9
+ } from "../../schema/create";
10
+ import { createDrizzleUOWCompiler } from "./drizzle-uow-compiler";
11
+ import type { DrizzleConfig } from "./drizzle-adapter";
12
+ import { drizzle } from "drizzle-orm/pglite";
13
+ import type { DBType } from "./shared";
14
+ import { UnitOfWork, type UOWDecoder } from "../../query/unit-of-work";
15
+ import { writeAndLoadSchema } from "./test-utils";
16
+
17
+ /**
18
+ * Integration tests for Drizzle UOW compiler and executor.
19
+ * These tests generate a real Drizzle schema and verify compilation works correctly.
20
+ */
21
+ describe("drizzle-uow-compiler", () => {
22
+ const testSchema = schema((s) => {
23
+ return s
24
+ .addTable("users", (t) => {
25
+ return t
26
+ .addColumn("id", idColumn())
27
+ .addColumn("name", column("string"))
28
+ .addColumn("email", column("string"))
29
+ .addColumn("age", column("integer").nullable())
30
+ .createIndex("idx_email", ["email"], { unique: true })
31
+ .createIndex("idx_name", ["name"]);
32
+ })
33
+ .addTable("posts", (t) => {
34
+ return t
35
+ .addColumn("id", idColumn())
36
+ .addColumn("title", column("string"))
37
+ .addColumn("content", column("string"))
38
+ .addColumn("userId", referenceColumn())
39
+ .addColumn("viewCount", column("integer").defaultTo(0))
40
+ .createIndex("idx_user", ["userId"])
41
+ .createIndex("idx_title", ["title"]);
42
+ })
43
+ .addReference("author", {
44
+ type: "one",
45
+ from: { table: "posts", column: "userId" },
46
+ to: { table: "users", column: "id" },
47
+ });
48
+ });
49
+
50
+ let db: DBType;
51
+ let config: DrizzleConfig;
52
+
53
+ beforeAll(async () => {
54
+ // Write schema to file and dynamically import it
55
+ const { schemaModule, cleanup } = await writeAndLoadSchema(
56
+ "drizzle-uow-compiler",
57
+ testSchema,
58
+ "postgresql",
59
+ );
60
+
61
+ // Create Drizzle instance with PGLite (in-memory Postgres)
62
+ db = drizzle({
63
+ schema: schemaModule,
64
+ }) as unknown as DBType;
65
+
66
+ config = {
67
+ db,
68
+ provider: "postgresql",
69
+ };
70
+
71
+ return async () => {
72
+ await cleanup();
73
+ };
74
+ });
75
+
76
+ function createTestUOWWithSchema<const T extends AnySchema>(schema: T) {
77
+ const compiler = createDrizzleUOWCompiler(schema, config);
78
+ const mockExecutor = {
79
+ executeRetrievalPhase: async () => [],
80
+ executeMutationPhase: async () => ({ success: true, createdInternalIds: [] }),
81
+ };
82
+ const mockDecoder: UOWDecoder<T> = (rawResults, operations) => {
83
+ if (rawResults.length !== operations.length) {
84
+ throw new Error("rawResults and ops must have the same length");
85
+ }
86
+ return rawResults;
87
+ };
88
+ return new UnitOfWork(schema, compiler, mockExecutor, mockDecoder);
89
+ }
90
+
91
+ function createTestUOW(name?: string) {
92
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
93
+ const mockExecutor = {
94
+ executeRetrievalPhase: async () => [],
95
+ executeMutationPhase: async () => ({ success: true, createdInternalIds: [] }),
96
+ };
97
+ const mockDecoder: UOWDecoder<typeof testSchema> = (rawResults, operations) => {
98
+ if (rawResults.length !== operations.length) {
99
+ throw new Error("rawResults and ops must have the same length");
100
+ }
101
+ return rawResults;
102
+ };
103
+ return new UnitOfWork(testSchema, compiler, mockExecutor, mockDecoder, name);
104
+ }
105
+
106
+ it("should create a compiler with the correct structure", () => {
107
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
108
+
109
+ expect(compiler).toBeDefined();
110
+ expect(compiler.compileRetrievalOperation).toBeInstanceOf(Function);
111
+ expect(compiler.compileMutationOperation).toBeInstanceOf(Function);
112
+ });
113
+
114
+ describe("compileRetrievalOperation", () => {
115
+ it("should compile find operation with where clause", () => {
116
+ const uow = createTestUOW();
117
+ uow.find("users", (b) =>
118
+ b.whereIndex("idx_email", (eb) => eb("email", "=", "test@example.com")),
119
+ );
120
+
121
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
122
+ const compiled = uow.compile(compiler);
123
+
124
+ expect(compiled.retrievalBatch).toHaveLength(1);
125
+ expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
126
+ `"select "id", "name", "email", "age", "_internalId", "_version" from "users" "users" where "users"."email" = $1"`,
127
+ );
128
+ expect(compiled.retrievalBatch[0].params).toEqual(["test@example.com"]);
129
+ });
130
+
131
+ it("should compile find operation with select clause", () => {
132
+ const uow = createTestUOW();
133
+ uow.find("users", (b) =>
134
+ b.whereIndex("idx_name", (eb) => eb("name", "=", "Alice")).select(["id", "name"]),
135
+ );
136
+
137
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
138
+ const compiled = uow.compile(compiler);
139
+
140
+ expect(compiled.retrievalBatch).toHaveLength(1);
141
+ expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
142
+ `"select "id", "name", "_internalId", "_version" from "users" "users" where "users"."name" = $1"`,
143
+ );
144
+ expect(compiled.retrievalBatch[0].params).toEqual(["Alice"]);
145
+ });
146
+
147
+ it("should compile find operation with pageSize", () => {
148
+ const uow = createTestUOW();
149
+ uow.find("users", (b) => b.whereIndex("primary").pageSize(10));
150
+
151
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
152
+ const compiled = uow.compile(compiler);
153
+
154
+ expect(compiled.retrievalBatch).toHaveLength(1);
155
+ expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
156
+ `"select "id", "name", "email", "age", "_internalId", "_version" from "users" "users" limit $1"`,
157
+ );
158
+ expect(compiled.retrievalBatch[0].params).toEqual([10]);
159
+ });
160
+
161
+ it("should compile find operation with orderByIndex on primary index", () => {
162
+ const uow = createTestUOW();
163
+ uow.find("users", (b) => b.whereIndex("primary").orderByIndex("primary", "desc"));
164
+
165
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
166
+ const compiled = uow.compile(compiler);
167
+
168
+ expect(compiled.retrievalBatch).toHaveLength(1);
169
+ expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
170
+ `"select "id", "name", "email", "age", "_internalId", "_version" from "users" "users" order by "users"."id" desc"`,
171
+ );
172
+ });
173
+
174
+ it("should compile find operation with orderByIndex", () => {
175
+ const uow = createTestUOW();
176
+ uow.find("users", (b) => b.whereIndex("idx_name").orderByIndex("idx_name", "desc"));
177
+
178
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
179
+ const compiled = uow.compile(compiler);
180
+
181
+ expect(compiled.retrievalBatch).toHaveLength(1);
182
+ expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
183
+ `"select "id", "name", "email", "age", "_internalId", "_version" from "users" "users" order by "users"."name" desc"`,
184
+ );
185
+ });
186
+
187
+ it("should compile multiple find operations", () => {
188
+ const uow = createTestUOW();
189
+ uow.find("users", (b) =>
190
+ b.whereIndex("idx_email", (eb) => eb("email", "=", "user1@example.com")),
191
+ );
192
+ uow.find("posts", (b) => b.whereIndex("idx_title", (eb) => eb("title", "contains", "test")));
193
+
194
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
195
+ const compiled = uow.compile(compiler);
196
+
197
+ expect(compiled.retrievalBatch).toHaveLength(2);
198
+ expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
199
+ `"select "id", "name", "email", "age", "_internalId", "_version" from "users" "users" where "users"."email" = $1"`,
200
+ );
201
+ expect(compiled.retrievalBatch[1].sql).toMatchInlineSnapshot(
202
+ `"select "id", "title", "content", "userId", "viewCount", "_internalId", "_version" from "posts" "posts" where "posts"."title" like $1"`,
203
+ );
204
+ });
205
+
206
+ it("should compile find operation with selectCount", () => {
207
+ const uow = createTestUOW();
208
+ uow.find("users", (b) => b.whereIndex("primary").selectCount());
209
+
210
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
211
+ const compiled = uow.compile(compiler);
212
+
213
+ expect(compiled.retrievalBatch).toHaveLength(1);
214
+ expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
215
+ `"select count(*) from "users""`,
216
+ );
217
+ });
218
+
219
+ it("should compile find operation with selectCount and where clause", () => {
220
+ const uow = createTestUOW();
221
+ uow.find("users", (b) =>
222
+ b.whereIndex("idx_name", (eb) => eb("name", "starts with", "John")).selectCount(),
223
+ );
224
+
225
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
226
+ const compiled = uow.compile(compiler);
227
+
228
+ expect(compiled.retrievalBatch).toHaveLength(1);
229
+ expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
230
+ `"select count(*) from "users" where "users"."name" like $1"`,
231
+ );
232
+ expect(compiled.retrievalBatch[0].params).toEqual(["John%"]);
233
+ });
234
+
235
+ it("should compile find operation with cursor pagination using after", () => {
236
+ const uow = createTestUOW();
237
+ const cursor = "eyJpbmRleFZhbHVlcyI6eyJuYW1lIjoiQWxpY2UifSwiZGlyZWN0aW9uIjoiZm9yd2FyZCJ9"; // {"indexValues":{"name":"Alice"},"direction":"forward"}
238
+ uow.find("users", (b) =>
239
+ b.whereIndex("idx_name").orderByIndex("idx_name", "asc").after(cursor).pageSize(10),
240
+ );
241
+
242
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
243
+ const compiled = uow.compile(compiler);
244
+
245
+ expect(compiled.retrievalBatch).toHaveLength(1);
246
+ expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
247
+ `"select "id", "name", "email", "age", "_internalId", "_version" from "users" "users" where "users"."name" > $1 order by "users"."name" asc limit $2"`,
248
+ );
249
+ expect(compiled.retrievalBatch[0].params).toEqual(["Alice", 10]);
250
+ });
251
+
252
+ it("should compile find operation with cursor pagination using before", () => {
253
+ const uow = createTestUOW();
254
+ const cursor = "eyJpbmRleFZhbHVlcyI6eyJuYW1lIjoiQm9iIn0sImRpcmVjdGlvbiI6ImJhY2t3YXJkIn0="; // {"indexValues":{"name":"Bob"},"direction":"backward"}
255
+ uow.find("users", (b) =>
256
+ b.whereIndex("idx_name").orderByIndex("idx_name", "desc").before(cursor).pageSize(10),
257
+ );
258
+
259
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
260
+ const compiled = uow.compile(compiler);
261
+
262
+ expect(compiled.retrievalBatch).toHaveLength(1);
263
+ expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
264
+ `"select "id", "name", "email", "age", "_internalId", "_version" from "users" "users" where "users"."name" > $1 order by "users"."name" desc limit $2"`,
265
+ );
266
+ expect(compiled.retrievalBatch[0].params).toEqual(["Bob", 10]);
267
+ });
268
+
269
+ it("should compile find operation with cursor pagination and additional where conditions", () => {
270
+ const uow = createTestUOW();
271
+ const cursor = "eyJpbmRleFZhbHVlcyI6eyJuYW1lIjoiQWxpY2UifSwiZGlyZWN0aW9uIjoiZm9yd2FyZCJ9";
272
+ uow.find("users", (b) =>
273
+ b
274
+ .whereIndex("idx_name", (eb) => eb("name", "starts with", "John"))
275
+ .orderByIndex("idx_name", "asc")
276
+ .after(cursor)
277
+ .pageSize(5),
278
+ );
279
+
280
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
281
+ const compiled = uow.compile(compiler);
282
+
283
+ expect(compiled.retrievalBatch).toHaveLength(1);
284
+ expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
285
+ `"select "id", "name", "email", "age", "_internalId", "_version" from "users" "users" where ("users"."name" like $1 and "users"."name" > $2) order by "users"."name" asc limit $3"`,
286
+ );
287
+ expect(compiled.retrievalBatch[0].params).toEqual(["John%", "Alice", 5]);
288
+ });
289
+
290
+ it("should compile find operation with join", () => {
291
+ const uow = createTestUOW();
292
+ uow.find("posts", (b) =>
293
+ b
294
+ .whereIndex("idx_title", (eb) => eb("title", "contains", "test"))
295
+ .join((jb) => jb.author()),
296
+ );
297
+
298
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
299
+ const compiled = uow.compile(compiler);
300
+
301
+ expect(compiled.retrievalBatch).toHaveLength(1);
302
+ // This should generate SQL that joins posts with users and selects author name and email
303
+ expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
304
+ `"select "posts"."id", "posts"."title", "posts"."content", "posts"."userId", "posts"."viewCount", "posts"."_internalId", "posts"."_version", "posts_author"."data" as "author" from "posts" "posts" left join lateral (select json_build_array("posts_author"."id", "posts_author"."name", "posts_author"."email", "posts_author"."age", "posts_author"."_internalId", "posts_author"."_version") as "data" from (select * from "users" "posts_author" where "posts_author"."_internalId" = "posts"."userId" limit $1) "posts_author") "posts_author" on true where "posts"."title" like $2"`,
305
+ );
306
+ expect(compiled.retrievalBatch[0].params).toEqual([1, "%test%"]);
307
+ });
308
+
309
+ it("should compile find operation with join filtering", () => {
310
+ const uow = createTestUOW();
311
+ uow.find("posts", (b) =>
312
+ b
313
+ .whereIndex("primary")
314
+ .join((jb) =>
315
+ jb.author((builder) =>
316
+ builder.whereIndex("idx_name", (eb) => eb("name", "=", "Alice")).select(["name"]),
317
+ ),
318
+ ),
319
+ );
320
+
321
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
322
+ const compiled = uow.compile(compiler);
323
+
324
+ expect(compiled.retrievalBatch).toHaveLength(1);
325
+ const sql = compiled.retrievalBatch[0].sql;
326
+ expect(sql).toMatchInlineSnapshot(
327
+ `"select "posts"."id", "posts"."title", "posts"."content", "posts"."userId", "posts"."viewCount", "posts"."_internalId", "posts"."_version", "posts_author"."data" as "author" from "posts" "posts" left join lateral (select json_build_array("posts_author"."name", "posts_author"."_internalId", "posts_author"."_version") as "data" from (select * from "users" "posts_author" where ("posts_author"."_internalId" = "posts"."userId" and "posts_author"."name" = $1) limit $2) "posts_author") "posts_author" on true"`,
328
+ );
329
+ });
330
+
331
+ it("should compile find operation with join ordering", () => {
332
+ const uow = createTestUOW();
333
+ uow.find("posts", (b) =>
334
+ b
335
+ .whereIndex("primary")
336
+ .join((jb) => jb.author((builder) => builder.orderByIndex("idx_name", "desc"))),
337
+ );
338
+
339
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
340
+ const compiled = uow.compile(compiler);
341
+
342
+ expect(compiled.retrievalBatch).toHaveLength(1);
343
+ const sql = compiled.retrievalBatch[0].sql;
344
+ expect(sql).toMatchInlineSnapshot(
345
+ `"select "posts"."id", "posts"."title", "posts"."content", "posts"."userId", "posts"."viewCount", "posts"."_internalId", "posts"."_version", "posts_author"."data" as "author" from "posts" "posts" left join lateral (select json_build_array("posts_author"."id", "posts_author"."name", "posts_author"."email", "posts_author"."age", "posts_author"."_internalId", "posts_author"."_version") as "data" from (select * from "users" "posts_author" where "posts_author"."_internalId" = "posts"."userId" order by "posts_author"."name" desc limit $1) "posts_author") "posts_author" on true"`,
346
+ );
347
+ });
348
+
349
+ it("should compile find operation with join pageSize", () => {
350
+ const uow = createTestUOW();
351
+ uow.find("posts", (b) =>
352
+ b.whereIndex("primary").join((jb) => jb.author((builder) => builder.pageSize(5))),
353
+ );
354
+
355
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
356
+ const compiled = uow.compile(compiler);
357
+
358
+ expect(compiled.retrievalBatch).toHaveLength(1);
359
+ const sql = compiled.retrievalBatch[0].sql;
360
+ // Should have limit in the joined query
361
+ expect(sql).toMatchInlineSnapshot(
362
+ `"select "posts"."id", "posts"."title", "posts"."content", "posts"."userId", "posts"."viewCount", "posts"."_internalId", "posts"."_version", "posts_author"."data" as "author" from "posts" "posts" left join lateral (select json_build_array("posts_author"."id", "posts_author"."name", "posts_author"."email", "posts_author"."age", "posts_author"."_internalId", "posts_author"."_version") as "data" from (select * from "users" "posts_author" where "posts_author"."_internalId" = "posts"."userId" limit $1) "posts_author") "posts_author" on true"`,
363
+ );
364
+ });
365
+ });
366
+
367
+ describe("compileMutationOperation", () => {
368
+ it("should compile create operation", () => {
369
+ const uow = createTestUOW();
370
+ uow.create("users", {
371
+ name: "John Doe",
372
+ email: "john@example.com",
373
+ age: 30,
374
+ });
375
+
376
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
377
+ const compiled = uow.compile(compiler);
378
+ const [batch] = compiled.mutationBatch;
379
+ assert(batch);
380
+ expect(batch.expectedAffectedRows).toBeNull();
381
+ expect(batch.query.sql).toMatchInlineSnapshot(
382
+ `"insert into "users" ("id", "name", "email", "age", "_internalId", "_version") values ($1, $2, $3, $4, default, default)"`,
383
+ );
384
+ expect(batch.query.params).toMatchObject([
385
+ expect.any(String),
386
+ "John Doe",
387
+ "john@example.com",
388
+ 30,
389
+ ]);
390
+ });
391
+
392
+ it("should compile update operation with ID", () => {
393
+ const uow = createTestUOW();
394
+ const userId = FragnoId.fromExternal("user123", 0);
395
+ uow.update("users", userId, (b) =>
396
+ b.set({
397
+ name: "Jane Doe",
398
+ age: 25,
399
+ }),
400
+ );
401
+
402
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
403
+ const compiled = uow.compile(compiler);
404
+ const [batch] = compiled.mutationBatch;
405
+ assert(batch);
406
+ expect(batch.expectedAffectedRows).toBeNull();
407
+ expect(batch.query.sql).toMatchInlineSnapshot(
408
+ `"update "users" set "name" = $1, "age" = $2, "_version" = COALESCE(_version, 0) + 1 where "users"."id" = $3"`,
409
+ );
410
+ expect(batch.query.params).toMatchObject(["Jane Doe", 25, "user123"]);
411
+ });
412
+
413
+ it("should compile update operation with version check", () => {
414
+ const uow = createTestUOW();
415
+ const userId = FragnoId.fromExternal("user123", 5);
416
+ uow.update("users", userId, (b) => b.set({ age: 18 }).check());
417
+
418
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
419
+ const compiled = uow.compile(compiler);
420
+ const [batch] = compiled.mutationBatch;
421
+ assert(batch);
422
+ expect(batch.expectedAffectedRows).toBe(1);
423
+ expect(batch.query.sql).toMatchInlineSnapshot(
424
+ `"update "users" set "age" = $1, "_version" = COALESCE(_version, 0) + 1 where ("users"."id" = $2 and "users"."_version" = $3)"`,
425
+ );
426
+ expect(batch.query.params).toMatchObject([18, "user123", 5]);
427
+ });
428
+
429
+ it("should compile delete operation with ID", () => {
430
+ const uow = createTestUOW();
431
+ const userId = FragnoId.fromExternal("user123", 0);
432
+ uow.delete("users", userId);
433
+
434
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
435
+ const compiled = uow.compile(compiler);
436
+ const [batch] = compiled.mutationBatch;
437
+ assert(batch);
438
+ expect(batch.expectedAffectedRows).toBeNull();
439
+ expect(batch.query.sql).toMatchInlineSnapshot(
440
+ `"delete from "users" where "users"."id" = $1"`,
441
+ );
442
+ expect(batch.query.params).toMatchObject(["user123"]);
443
+ });
444
+
445
+ it("should compile delete operation with version check", () => {
446
+ const uow = createTestUOW();
447
+ const userId = FragnoId.fromExternal("user123", 3);
448
+ uow.delete("users", userId, (b) => b.check());
449
+
450
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
451
+ const compiled = uow.compile(compiler);
452
+ const [batch] = compiled.mutationBatch;
453
+ assert(batch);
454
+ expect(batch.expectedAffectedRows).toBe(1);
455
+ expect(batch.query.sql).toMatchInlineSnapshot(
456
+ `"delete from "users" where ("users"."id" = $1 and "users"."_version" = $2)"`,
457
+ );
458
+ expect(batch.query.params).toMatchObject(["user123", 3]);
459
+ });
460
+
461
+ it("should compile update operation with string ID", () => {
462
+ const uow = createTestUOW();
463
+ uow.update("users", "user123", (b) =>
464
+ b.set({
465
+ name: "Jane Doe",
466
+ age: 25,
467
+ }),
468
+ );
469
+
470
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
471
+ const compiled = uow.compile(compiler);
472
+ const [batch] = compiled.mutationBatch;
473
+ assert(batch);
474
+ expect(batch.expectedAffectedRows).toBeNull();
475
+ expect(batch.query.sql).toMatchInlineSnapshot(
476
+ `"update "users" set "name" = $1, "age" = $2, "_version" = COALESCE(_version, 0) + 1 where "users"."id" = $3"`,
477
+ );
478
+ expect(batch.query.params).toMatchObject(["Jane Doe", 25, "user123"]);
479
+ });
480
+
481
+ it("should compile delete operation with string ID", () => {
482
+ const uow = createTestUOW();
483
+ uow.delete("users", "user123");
484
+
485
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
486
+ const compiled = uow.compile(compiler);
487
+ const [batch] = compiled.mutationBatch;
488
+ assert(batch);
489
+ expect(batch.expectedAffectedRows).toBeNull();
490
+ expect(batch.query.sql).toMatchInlineSnapshot(
491
+ `"delete from "users" where "users"."id" = $1"`,
492
+ );
493
+ expect(batch.query.params).toMatchObject(["user123"]);
494
+ });
495
+
496
+ it("should throw when trying to check() with string ID on update", () => {
497
+ const uow = createTestUOW();
498
+ expect(() => {
499
+ uow.update("users", "user123", (b) => b.set({ name: "Jane" }).check());
500
+ }).toThrow(
501
+ 'Cannot use check() with a string ID on table "users". Version checking requires a FragnoId with version information.',
502
+ );
503
+ });
504
+
505
+ it("should throw when trying to check() with string ID on delete", () => {
506
+ const uow = createTestUOW();
507
+ expect(() => {
508
+ uow.delete("users", "user123", (b) => b.check());
509
+ }).toThrow(
510
+ 'Cannot use check() with a string ID on table "users". Version checking requires a FragnoId with version information.',
511
+ );
512
+ });
513
+
514
+ it("should compile multiple mutation operations", () => {
515
+ const uow = createTestUOW();
516
+ uow.create("users", {
517
+ name: "Alice",
518
+ email: "alice@example.com",
519
+ });
520
+ const postId = FragnoId.fromExternal("post123", 0);
521
+ uow.update("posts", postId, (b) => b.set({ viewCount: 10 }));
522
+ const userId = FragnoId.fromExternal("user456", 0);
523
+ uow.delete("posts", userId);
524
+
525
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
526
+ const compiled = uow.compile(compiler);
527
+ const [createBatch, updateBatch, deleteBatch] = compiled.mutationBatch;
528
+
529
+ expect(compiled.mutationBatch).toHaveLength(3);
530
+
531
+ assert(createBatch);
532
+ expect(createBatch.query.sql).toMatchInlineSnapshot(
533
+ `"insert into "users" ("id", "name", "email", "age", "_internalId", "_version") values ($1, $2, $3, default, default, default)"`,
534
+ );
535
+ expect(createBatch.query.params).toMatchObject([
536
+ expect.any(String), // auto-generated ID
537
+ "Alice",
538
+ "alice@example.com",
539
+ ]);
540
+
541
+ assert(updateBatch);
542
+ expect(updateBatch.query.sql).toMatchInlineSnapshot(
543
+ `"update "posts" set "viewCount" = $1, "_version" = COALESCE(_version, 0) + 1 where "posts"."id" = $2"`,
544
+ );
545
+
546
+ assert(deleteBatch);
547
+ expect(deleteBatch.query.sql).toMatchInlineSnapshot(
548
+ `"delete from "posts" where "posts"."id" = $1"`,
549
+ );
550
+ });
551
+ });
552
+
553
+ describe("complete UOW workflow", () => {
554
+ it("should compile retrieval and mutation phases together", () => {
555
+ const uow = createTestUOW("update-user-balance");
556
+
557
+ // Retrieval phase
558
+ uow.find("users", (b) => b.whereIndex("primary", (eb) => eb("id", "=", "user123")));
559
+
560
+ // Mutation phase
561
+ const userId = FragnoId.fromExternal("user123", 3);
562
+ uow.update("users", userId, (b) => b.set({ age: 31 }).check());
563
+
564
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
565
+ const compiled = uow.compile(compiler);
566
+
567
+ expect(compiled.name).toBe("update-user-balance");
568
+ expect(compiled.retrievalBatch).toHaveLength(1);
569
+ expect(compiled.mutationBatch).toHaveLength(1);
570
+
571
+ expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
572
+ `"select "id", "name", "email", "age", "_internalId", "_version" from "users" "users" where "users"."id" = $1"`,
573
+ );
574
+
575
+ // Update should include version check in WHERE clause
576
+ const [batch] = compiled.mutationBatch;
577
+ assert(batch);
578
+ expect(batch.expectedAffectedRows).toBe(1);
579
+ expect(batch.query.sql).toMatchInlineSnapshot(
580
+ `"update "users" set "age" = $1, "_version" = COALESCE(_version, 0) + 1 where ("users"."id" = $2 and "users"."_version" = $3)"`,
581
+ );
582
+ });
583
+
584
+ it("should handle complex where conditions", () => {
585
+ const uow = createTestUOW();
586
+ uow.find("users", (b) =>
587
+ b.whereIndex("idx_email", (eb) =>
588
+ eb.and(
589
+ eb("email", "contains", "@example.com"),
590
+ // @ts-expect-error - name is not indexed
591
+ eb.or(eb("name", "=", "Alice"), eb("name", "=", "Bob")),
592
+ ),
593
+ ),
594
+ );
595
+
596
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
597
+ const compiled = uow.compile(compiler);
598
+
599
+ expect(compiled.retrievalBatch).toHaveLength(1);
600
+ expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
601
+ `"select "id", "name", "email", "age", "_internalId", "_version" from "users" "users" where ("users"."email" like $1 and ("users"."name" = $2 or "users"."name" = $3))"`,
602
+ );
603
+ expect(compiled.retrievalBatch[0].params).toEqual(["%@example.com%", "Alice", "Bob"]);
604
+ });
605
+
606
+ it("should return null for operations with always-false conditions", () => {
607
+ const uow = createTestUOW();
608
+ uow.find("users", (b) => b.whereIndex("primary", () => false));
609
+
610
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
611
+ const compiled = uow.compile(compiler);
612
+
613
+ // When condition is false, the operation should return null and not be added to batch
614
+ expect(compiled.retrievalBatch).toHaveLength(0);
615
+ });
616
+
617
+ it("should handle always-true conditions", () => {
618
+ const uow = createTestUOW();
619
+ uow.find("users", (b) => b.whereIndex("primary", () => true));
620
+
621
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
622
+ const compiled = uow.compile(compiler);
623
+
624
+ expect(compiled.retrievalBatch).toHaveLength(1);
625
+ expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
626
+ `"select "id", "name", "email", "age", "_internalId", "_version" from "users" "users""`,
627
+ );
628
+ });
629
+ });
630
+
631
+ describe("version checking", () => {
632
+ it("should embed version check in update WHERE clause", () => {
633
+ const uow = createTestUOW();
634
+
635
+ const userId = FragnoId.fromExternal("user123", 5);
636
+ uow.update("users", userId, (b) => b.set({ age: 31 }).check());
637
+
638
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
639
+ const compiled = uow.compile(compiler);
640
+ const [batch] = compiled.mutationBatch;
641
+ assert(batch);
642
+ expect(batch.expectedAffectedRows).toBe(1);
643
+ expect(batch.query.sql).toMatchInlineSnapshot(
644
+ `"update "users" set "age" = $1, "_version" = COALESCE(_version, 0) + 1 where ("users"."id" = $2 and "users"."_version" = $3)"`,
645
+ );
646
+ expect(batch.query.params).toMatchObject([31, "user123", 5]);
647
+ });
648
+
649
+ it("should embed version check in delete WHERE clause", () => {
650
+ const uow = createTestUOW();
651
+
652
+ const userId = FragnoId.fromExternal("user456", 3);
653
+ uow.delete("users", userId, (b) => b.check());
654
+
655
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
656
+ const compiled = uow.compile(compiler);
657
+ const [batch] = compiled.mutationBatch;
658
+ assert(batch);
659
+ expect(batch.expectedAffectedRows).toBe(1);
660
+ expect(batch.query.sql).toMatchInlineSnapshot(
661
+ `"delete from "users" where ("users"."id" = $1 and "users"."_version" = $2)"`,
662
+ );
663
+ expect(batch.query.params).toMatchObject(["user456", 3]);
664
+ });
665
+
666
+ it("should handle version checks on different tables", () => {
667
+ const uow = createTestUOW();
668
+
669
+ const userId = FragnoId.fromExternal("user1", 2);
670
+ const postId = FragnoId.fromExternal("post1", 1);
671
+
672
+ uow.update("users", userId, (b) => b.set({ age: 30 }).check());
673
+ uow.update("posts", postId, (b) => b.set({ viewCount: 100 }).check());
674
+
675
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
676
+ const compiled = uow.compile(compiler);
677
+ const [userBatch, postBatch] = compiled.mutationBatch;
678
+
679
+ expect(compiled.mutationBatch).toHaveLength(2);
680
+
681
+ assert(userBatch);
682
+ expect(userBatch.expectedAffectedRows).toBe(1);
683
+ expect(userBatch.query.sql).toMatchInlineSnapshot(
684
+ `"update "users" set "age" = $1, "_version" = COALESCE(_version, 0) + 1 where ("users"."id" = $2 and "users"."_version" = $3)"`,
685
+ );
686
+ expect(userBatch.query.params).toMatchObject([30, "user1", 2]);
687
+
688
+ assert(postBatch);
689
+ expect(postBatch.expectedAffectedRows).toBe(1);
690
+ expect(postBatch.query.sql).toMatchInlineSnapshot(
691
+ `"update "posts" set "viewCount" = $1, "_version" = COALESCE(_version, 0) + 1 where ("posts"."id" = $2 and "posts"."_version" = $3)"`,
692
+ );
693
+ expect(postBatch.query.params).toMatchObject([100, "post1", 1]);
694
+ });
695
+
696
+ it("should not affect updates without version checks", () => {
697
+ const uow = createTestUOW();
698
+
699
+ const userId = FragnoId.fromExternal("user1", 0);
700
+ uow.update("users", userId, (b) => b.set({ age: 25 }));
701
+
702
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
703
+ const compiled = uow.compile(compiler);
704
+ const [batch] = compiled.mutationBatch;
705
+ assert(batch);
706
+ expect(batch.expectedAffectedRows).toBeNull();
707
+ // Should be normal update without version check
708
+ expect(batch.query.sql).toMatchInlineSnapshot(
709
+ `"update "users" set "age" = $1, "_version" = COALESCE(_version, 0) + 1 where "users"."id" = $2"`,
710
+ );
711
+ });
712
+ });
713
+
714
+ describe("edge cases", () => {
715
+ it("should handle UOW with no operations", () => {
716
+ const uow = createTestUOW();
717
+
718
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
719
+ const compiled = uow.compile(compiler);
720
+
721
+ expect(compiled.retrievalBatch).toHaveLength(0);
722
+ expect(compiled.mutationBatch).toHaveLength(0);
723
+ });
724
+
725
+ it("should handle UOW with only retrieval operations", () => {
726
+ const uow = createTestUOW();
727
+ uow.find("users");
728
+
729
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
730
+ const compiled = uow.compile(compiler);
731
+
732
+ expect(compiled.retrievalBatch).toHaveLength(1);
733
+ expect(compiled.mutationBatch).toHaveLength(0);
734
+ });
735
+
736
+ it("should handle UOW with only mutation operations", () => {
737
+ const uow = createTestUOW();
738
+ uow.create("users", {
739
+ name: "Test User",
740
+ email: "test@example.com",
741
+ });
742
+
743
+ const compiler = createDrizzleUOWCompiler(testSchema, config);
744
+ const compiled = uow.compile(compiler);
745
+
746
+ expect(compiled.retrievalBatch).toHaveLength(0);
747
+ expect(compiled.mutationBatch).toHaveLength(1);
748
+ });
749
+ });
750
+
751
+ describe("default value generation", () => {
752
+ // Create a schema with columns that have different types of defaults
753
+ const defaultsSchema = schema((s) => {
754
+ return s.addTable("logs", (t) => {
755
+ return t
756
+ .addColumn("id", idColumn())
757
+ .addColumn("message", column("string"))
758
+ .addColumn("sessionId", column("string").defaultTo$("auto")) // runtime auto
759
+ .addColumn("timestamp", column("timestamp").defaultTo$("now")) // runtime now
760
+ .addColumn("counter", column("integer").defaultTo$((() => 42) as () => number)) // runtime function
761
+ .addColumn("status", column("string").defaultTo("pending")) // static default
762
+ .createIndex("idx_session", ["sessionId"]);
763
+ });
764
+ });
765
+
766
+ let defaultsDb: DBType;
767
+ let defaultsConfig: DrizzleConfig;
768
+
769
+ beforeAll(async () => {
770
+ // Write schema to file and dynamically import it
771
+ const { schemaModule, cleanup } = await writeAndLoadSchema(
772
+ "drizzle-uow-compiler-defaults",
773
+ defaultsSchema,
774
+ "postgresql",
775
+ );
776
+
777
+ // Create Drizzle instance with PGLite (in-memory Postgres)
778
+ defaultsDb = drizzle({
779
+ schema: schemaModule,
780
+ }) as unknown as DBType;
781
+
782
+ defaultsConfig = {
783
+ db: defaultsDb,
784
+ provider: "postgresql",
785
+ };
786
+
787
+ return async () => {
788
+ await cleanup();
789
+ };
790
+ });
791
+
792
+ it("should generate runtime defaults for missing columns", () => {
793
+ const uow = createTestUOWWithSchema(defaultsSchema);
794
+ // Only provide message, all other columns should get defaults
795
+ uow.create("logs", {
796
+ message: "Test log",
797
+ });
798
+
799
+ const compiler = createDrizzleUOWCompiler(defaultsSchema, defaultsConfig);
800
+ const compiled = uow.compile(compiler);
801
+ const [batch] = compiled.mutationBatch;
802
+ assert(batch);
803
+
804
+ expect(batch.query.sql).toMatchInlineSnapshot(
805
+ `"insert into "logs" ("id", "message", "sessionId", "timestamp", "counter", "status", "_internalId", "_version") values ($1, $2, $3, $4, $5, default, default, default)"`,
806
+ );
807
+
808
+ expect(batch.query.params).toMatchObject([
809
+ expect.any(String), // auto-generated ID
810
+ "Test log",
811
+ expect.any(String), // auto-generated sessionId
812
+ expect.any(String), // auto-generated timestamp (serialized)
813
+ 42, // function-generated counter
814
+ ]);
815
+ });
816
+
817
+ it("should not override user-provided values with defaults", () => {
818
+ const uow = createTestUOWWithSchema(defaultsSchema);
819
+ const customTimestamp = new Date("2024-01-01");
820
+ // Provide all values explicitly
821
+ uow.create("logs", {
822
+ message: "Test log",
823
+ sessionId: "custom-session-id",
824
+ timestamp: customTimestamp,
825
+ counter: 100,
826
+ status: "active",
827
+ });
828
+
829
+ const compiler = createDrizzleUOWCompiler(defaultsSchema, defaultsConfig);
830
+ const compiled = uow.compile(compiler);
831
+ const [batch] = compiled.mutationBatch;
832
+ assert(batch);
833
+
834
+ // All user-provided values should be used, not defaults
835
+ const params = batch.query.params;
836
+ expect(params[1]).toBe("Test log");
837
+ expect(params[2]).toBe("custom-session-id");
838
+ expect(params[3]).toEqual(customTimestamp.toISOString()); // serialized to ISO string
839
+ expect(params[4]).toBe(100);
840
+ expect(params[5]).toBe("active");
841
+ });
842
+
843
+ it("should handle mix of provided and default values", () => {
844
+ const uow = createTestUOWWithSchema(defaultsSchema);
845
+ // Provide some values, let others use defaults
846
+ uow.create("logs", {
847
+ message: "Partial log",
848
+ counter: 999, // override the function default
849
+ });
850
+
851
+ const compiler = createDrizzleUOWCompiler(defaultsSchema, defaultsConfig);
852
+ const compiled = uow.compile(compiler);
853
+ const [batch] = compiled.mutationBatch;
854
+ assert(batch);
855
+
856
+ expect(batch.query.params).toMatchObject([
857
+ expect.any(String), // auto-generated ID
858
+ "Partial log",
859
+ expect.any(String), // auto-generated sessionId
860
+ expect.any(String), // auto-generated timestamp (serialized)
861
+ 999, // user-provided counter
862
+ ]);
863
+ // status should use DB default (omitted from INSERT)
864
+ });
865
+
866
+ it("should generate unique values for auto defaults across multiple creates", () => {
867
+ const uow = createTestUOWWithSchema(defaultsSchema);
868
+ uow.create("logs", { message: "Log 1" });
869
+ uow.create("logs", { message: "Log 2" });
870
+ uow.create("logs", { message: "Log 3" });
871
+
872
+ const compiler = createDrizzleUOWCompiler(defaultsSchema, defaultsConfig);
873
+ const compiled = uow.compile(compiler);
874
+
875
+ expect(compiled.mutationBatch).toHaveLength(3);
876
+
877
+ // Extract sessionId from each create
878
+ const sessionIds = compiled.mutationBatch.map((batch) => {
879
+ assert(batch);
880
+ return batch.query.params[2]; // sessionId is 3rd param (after id, message)
881
+ });
882
+
883
+ // All sessionIds should be unique
884
+ const uniqueSessionIds = new Set(sessionIds);
885
+ expect(uniqueSessionIds.size).toBe(3);
886
+ });
887
+ });
888
+
889
+ describe("nested joins", () => {
890
+ // Create a schema that supports nested joins
891
+ const nestedSchema = schema((s) => {
892
+ return s
893
+ .addTable("users", (t) => {
894
+ return t
895
+ .addColumn("id", idColumn())
896
+ .addColumn("name", column("string"))
897
+ .addColumn("email", column("string"))
898
+ .createIndex("idx_name", ["name"]);
899
+ })
900
+ .addTable("posts", (t) => {
901
+ return t
902
+ .addColumn("id", idColumn())
903
+ .addColumn("title", column("string"))
904
+ .addColumn("userId", referenceColumn())
905
+ .createIndex("idx_user", ["userId"]);
906
+ })
907
+ .addTable("comments", (t) => {
908
+ return t
909
+ .addColumn("id", idColumn())
910
+ .addColumn("text", column("string"))
911
+ .addColumn("postId", referenceColumn())
912
+ .createIndex("idx_post", ["postId"]);
913
+ })
914
+ .addReference("author", {
915
+ type: "one",
916
+ from: { table: "posts", column: "userId" },
917
+ to: { table: "users", column: "id" },
918
+ })
919
+ .addReference("post", {
920
+ type: "one",
921
+ from: { table: "comments", column: "postId" },
922
+ to: { table: "posts", column: "id" },
923
+ });
924
+ });
925
+
926
+ let nestedDb: DBType;
927
+ let nestedConfig: DrizzleConfig;
928
+
929
+ beforeAll(async () => {
930
+ // Write schema to file and dynamically import it
931
+ const { schemaModule, cleanup } = await writeAndLoadSchema(
932
+ "drizzle-uow-compiler-nested",
933
+ nestedSchema,
934
+ "postgresql",
935
+ );
936
+
937
+ // Create Drizzle instance with PGLite (in-memory Postgres)
938
+ nestedDb = drizzle({
939
+ schema: schemaModule,
940
+ }) as unknown as DBType;
941
+
942
+ nestedConfig = {
943
+ db: nestedDb,
944
+ provider: "postgresql",
945
+ };
946
+
947
+ return async () => {
948
+ await cleanup();
949
+ };
950
+ }, 15000);
951
+
952
+ function createNestedUOW(name?: string) {
953
+ const compiler = createDrizzleUOWCompiler(nestedSchema, nestedConfig);
954
+ const mockExecutor = {
955
+ executeRetrievalPhase: async () => [],
956
+ executeMutationPhase: async () => ({ success: true, createdInternalIds: [] }),
957
+ };
958
+ const mockDecoder: UOWDecoder<typeof nestedSchema> = (rawResults, operations) => {
959
+ if (rawResults.length !== operations.length) {
960
+ throw new Error("rawResults and ops must have the same length");
961
+ }
962
+ return rawResults;
963
+ };
964
+ return new UnitOfWork(nestedSchema, compiler, mockExecutor, mockDecoder, name);
965
+ }
966
+
967
+ it("should compile nested joins (comments -> post -> author)", () => {
968
+ const uow = createNestedUOW();
969
+ uow.find("comments", (b) =>
970
+ b
971
+ .whereIndex("primary")
972
+ .join((jb) =>
973
+ jb.post((postBuilder) =>
974
+ postBuilder
975
+ .select(["title"])
976
+ .join((jb2) => jb2.author((authorBuilder) => authorBuilder.select(["name"]))),
977
+ ),
978
+ ),
979
+ );
980
+
981
+ const compiler = createDrizzleUOWCompiler(nestedSchema, nestedConfig);
982
+ const compiled = uow.compile(compiler);
983
+
984
+ expect(compiled.retrievalBatch).toHaveLength(1);
985
+ const sql = compiled.retrievalBatch[0].sql;
986
+
987
+ // Should contain nested lateral joins
988
+ expect(sql).toContain("left join lateral");
989
+ // Should join comments -> post
990
+ expect(sql).toContain('"comments_post"');
991
+ // Should join post -> author within the first join
992
+ expect(sql).toContain('"comments_post_author"');
993
+ // Should have the nested structure with proper lateral joins
994
+ expect(sql).toMatchInlineSnapshot(
995
+ `"select "comments"."id", "comments"."text", "comments"."postId", "comments"."_internalId", "comments"."_version", "comments_post"."data" as "post" from "comments" "comments" left join lateral (select json_build_array("comments_post"."title", "comments_post"."_internalId", "comments_post"."_version", "comments_post_author"."data") as "data" from (select * from "posts" "comments_post" where "comments_post"."_internalId" = "comments"."postId" limit $1) "comments_post" left join lateral (select json_build_array("comments_post_author"."name", "comments_post_author"."_internalId", "comments_post_author"."_version") as "data" from (select * from "users" "comments_post_author" where "comments_post_author"."_internalId" = "comments_post"."userId" limit $2) "comments_post_author") "comments_post_author" on true) "comments_post" on true"`,
996
+ );
997
+ });
998
+
999
+ it("should compile nested joins with filtering at each level", () => {
1000
+ const uow = createNestedUOW();
1001
+ uow.find("comments", (b) =>
1002
+ b
1003
+ .whereIndex("primary")
1004
+ .join((jb) =>
1005
+ jb.post((postBuilder) =>
1006
+ postBuilder
1007
+ .select(["title"])
1008
+ .join((jb2) =>
1009
+ jb2.author((authorBuilder) =>
1010
+ authorBuilder
1011
+ .whereIndex("idx_name", (eb) => eb("name", "=", "Alice"))
1012
+ .select(["name"]),
1013
+ ),
1014
+ ),
1015
+ ),
1016
+ ),
1017
+ );
1018
+
1019
+ const compiler = createDrizzleUOWCompiler(nestedSchema, nestedConfig);
1020
+ const compiled = uow.compile(compiler);
1021
+
1022
+ expect(compiled.retrievalBatch).toHaveLength(1);
1023
+ const sql = compiled.retrievalBatch[0].sql;
1024
+
1025
+ // Should have WHERE clause in the nested author join
1026
+ expect(sql).toContain('"comments_post_author"."name" = $');
1027
+ expect(sql).toMatchInlineSnapshot(
1028
+ `"select "comments"."id", "comments"."text", "comments"."postId", "comments"."_internalId", "comments"."_version", "comments_post"."data" as "post" from "comments" "comments" left join lateral (select json_build_array("comments_post"."title", "comments_post"."_internalId", "comments_post"."_version", "comments_post_author"."data") as "data" from (select * from "posts" "comments_post" where "comments_post"."_internalId" = "comments"."postId" limit $1) "comments_post" left join lateral (select json_build_array("comments_post_author"."name", "comments_post_author"."_internalId", "comments_post_author"."_version") as "data" from (select * from "users" "comments_post_author" where ("comments_post_author"."_internalId" = "comments_post"."userId" and "comments_post_author"."name" = $2) limit $3) "comments_post_author") "comments_post_author" on true) "comments_post" on true"`,
1029
+ );
1030
+ });
1031
+
1032
+ it("should compile nested joins with ordering and limits at each level", () => {
1033
+ const uow = createNestedUOW();
1034
+ uow.find("comments", (b) =>
1035
+ b
1036
+ .whereIndex("primary")
1037
+ .pageSize(10)
1038
+ .join((jb) =>
1039
+ jb.post((postBuilder) =>
1040
+ postBuilder
1041
+ .select(["title"])
1042
+ .pageSize(1)
1043
+ .join((jb2) =>
1044
+ jb2.author((authorBuilder) =>
1045
+ authorBuilder.orderByIndex("idx_name", "asc").pageSize(1),
1046
+ ),
1047
+ ),
1048
+ ),
1049
+ ),
1050
+ );
1051
+
1052
+ const compiler = createDrizzleUOWCompiler(nestedSchema, nestedConfig);
1053
+ const compiled = uow.compile(compiler);
1054
+
1055
+ expect(compiled.retrievalBatch).toHaveLength(1);
1056
+ const sql = compiled.retrievalBatch[0].sql;
1057
+
1058
+ // Should have limits at all levels and ordering in nested author join
1059
+ expect(sql).toContain('order by "comments_post_author"."name" asc');
1060
+ expect(sql).toMatchInlineSnapshot(
1061
+ `"select "comments"."id", "comments"."text", "comments"."postId", "comments"."_internalId", "comments"."_version", "comments_post"."data" as "post" from "comments" "comments" left join lateral (select json_build_array("comments_post"."title", "comments_post"."_internalId", "comments_post"."_version", "comments_post_author"."data") as "data" from (select * from "posts" "comments_post" where "comments_post"."_internalId" = "comments"."postId" limit $1) "comments_post" left join lateral (select json_build_array("comments_post_author"."id", "comments_post_author"."name", "comments_post_author"."email", "comments_post_author"."_internalId", "comments_post_author"."_version") as "data" from (select * from "users" "comments_post_author" where "comments_post_author"."_internalId" = "comments_post"."userId" order by "comments_post_author"."name" asc limit $2) "comments_post_author") "comments_post_author" on true) "comments_post" on true limit $3"`,
1062
+ );
1063
+ });
1064
+
1065
+ it("should compile multiple nested joins from same table", () => {
1066
+ const uow = createNestedUOW();
1067
+ uow.find("posts", (b) =>
1068
+ b.whereIndex("primary").join((jb) =>
1069
+ // Join to author with nested structure
1070
+ jb.author((authorBuilder) => authorBuilder.select(["name", "email"])),
1071
+ ),
1072
+ );
1073
+
1074
+ const compiler = createDrizzleUOWCompiler(nestedSchema, nestedConfig);
1075
+ const compiled = uow.compile(compiler);
1076
+
1077
+ expect(compiled.retrievalBatch).toHaveLength(1);
1078
+ const sql = compiled.retrievalBatch[0].sql;
1079
+ expect(sql).toMatchInlineSnapshot(
1080
+ `"select "posts"."id", "posts"."title", "posts"."userId", "posts"."_internalId", "posts"."_version", "posts_author"."data" as "author" from "posts" "posts" left join lateral (select json_build_array("posts_author"."name", "posts_author"."email", "posts_author"."_internalId", "posts_author"."_version") as "data" from (select * from "users" "posts_author" where "posts_author"."_internalId" = "posts"."userId" limit $1) "posts_author") "posts_author" on true"`,
1081
+ );
1082
+ });
1083
+ });
1084
+ });