@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,811 @@
1
+ import { Kysely, PostgresDialect } from "kysely";
2
+ import { describe, it, beforeAll, assert, expect } from "vitest";
3
+ import { column, idColumn, referenceColumn, schema } from "../../schema/create";
4
+ import type { Kysely as KyselyType, PostgresDialectConfig } from "kysely";
5
+ import { UnitOfWork, type UOWDecoder } from "../../query/unit-of-work";
6
+ import { createKyselyUOWCompiler } from "./kysely-uow-compiler";
7
+ import type { KyselyConfig } from "./kysely-adapter";
8
+
9
+ describe("kysely-uow-joins", () => {
10
+ const userSchema = schema((s) => {
11
+ return (
12
+ s
13
+ .addTable("users", (t) => {
14
+ return t
15
+ .addColumn("id", idColumn())
16
+ .addColumn("name", column("string"))
17
+ .addColumn("email", column("string"))
18
+ .addColumn("invitedBy", referenceColumn())
19
+ .createIndex("idx_name", ["name"])
20
+ .createIndex("idx_id", ["id"]);
21
+ })
22
+ .addTable("posts", (t) => {
23
+ return t
24
+ .addColumn("id", idColumn())
25
+ .addColumn("title", column("string"))
26
+ .addColumn("content", column("string"))
27
+ .addColumn("userId", referenceColumn())
28
+ .addColumn("publishedAt", column("timestamp"))
29
+ .createIndex("idx_title", ["title"])
30
+ .createIndex("idx_id", ["id"]);
31
+ })
32
+ .addTable("tags", (t) => {
33
+ return t
34
+ .addColumn("id", idColumn())
35
+ .addColumn("name", column("string"))
36
+ .createIndex("idx_id", ["id"]);
37
+ })
38
+ .addTable("post_tags", (t) => {
39
+ return t
40
+ .addColumn("id", idColumn())
41
+ .addColumn("postId", referenceColumn())
42
+ .addColumn("tagId", referenceColumn())
43
+ .createIndex("idx_post", ["postId"])
44
+ .createIndex("idx_tag", ["tagId"]);
45
+ })
46
+ .addTable("comments", (t) => {
47
+ return t
48
+ .addColumn("id", idColumn())
49
+ .addColumn("content", column("string"))
50
+ .addColumn("postId", referenceColumn())
51
+ .addColumn("authorId", referenceColumn())
52
+ .createIndex("idx_post", ["postId"])
53
+ .createIndex("idx_author", ["authorId"]);
54
+ })
55
+ // Basic one-to-many relationships
56
+ .addReference("author", {
57
+ type: "one",
58
+ from: { table: "posts", column: "userId" },
59
+ to: { table: "users", column: "id" },
60
+ })
61
+ .addReference("inviter", {
62
+ type: "one",
63
+ from: { table: "users", column: "invitedBy" },
64
+ to: { table: "users", column: "id" },
65
+ })
66
+ .addReference("post", {
67
+ type: "one",
68
+ from: { table: "comments", column: "postId" },
69
+ to: { table: "posts", column: "id" },
70
+ })
71
+ .addReference("author", {
72
+ type: "one",
73
+ from: { table: "comments", column: "authorId" },
74
+ to: { table: "users", column: "id" },
75
+ })
76
+ // Many-to-many relationships
77
+ .addReference("post", {
78
+ type: "one",
79
+ from: { table: "post_tags", column: "postId" },
80
+ to: { table: "posts", column: "id" },
81
+ })
82
+ .addReference("tag", {
83
+ type: "one",
84
+ from: { table: "post_tags", column: "tagId" },
85
+ to: { table: "tags", column: "id" },
86
+ })
87
+ );
88
+ });
89
+
90
+ let kysely: KyselyType<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
91
+ let config: KyselyConfig;
92
+
93
+ beforeAll(async () => {
94
+ kysely = new Kysely({
95
+ dialect: new PostgresDialect({} as PostgresDialectConfig),
96
+ });
97
+
98
+ config = {
99
+ db: kysely,
100
+ provider: "postgresql",
101
+ };
102
+ });
103
+
104
+ // Helper to create UnitOfWork for testing
105
+ function createTestUOW(name?: string) {
106
+ const mockCompiler = createKyselyUOWCompiler(userSchema, config);
107
+ const mockExecutor = {
108
+ executeRetrievalPhase: async () => [],
109
+ executeMutationPhase: async () => ({ success: true, createdInternalIds: [] }),
110
+ };
111
+ const mockDecoder: UOWDecoder<typeof userSchema> = (rawResults, operations) => {
112
+ if (rawResults.length !== operations.length) {
113
+ throw new Error("rawResults and ops must have the same length");
114
+ }
115
+ return rawResults;
116
+ };
117
+ return new UnitOfWork(userSchema, mockCompiler, mockExecutor, mockDecoder, name);
118
+ }
119
+
120
+ describe("postgresql", () => {
121
+ it("should compile select with join condition comparing columns", () => {
122
+ const uow = createTestUOW();
123
+ uow.find("posts", (b) =>
124
+ b
125
+ .whereIndex("primary")
126
+ .select(["id", "userId"])
127
+ .join((jb) => jb.author()),
128
+ );
129
+
130
+ const compiler = createKyselyUOWCompiler(userSchema, config);
131
+ const compiled = uow.compile(compiler);
132
+
133
+ expect(compiled.retrievalBatch).toHaveLength(1);
134
+ const query = compiled.retrievalBatch[0];
135
+ assert(query);
136
+ expect(query.sql).toMatchInlineSnapshot(
137
+ `"select "author"."id" as "author:id", "author"."name" as "author:name", "author"."email" as "author:email", "author"."invitedBy" as "author:invitedBy", "author"."_internalId" as "author:_internalId", "author"."_version" as "author:_version", "posts"."id" as "id", "posts"."userId" as "userId", "posts"."_internalId" as "_internalId", "posts"."_version" as "_version" from "posts" left join "users" as "author" on "posts"."userId" = "author"."_internalId""`,
138
+ );
139
+ });
140
+
141
+ it("should compile join with specific column selection", () => {
142
+ const uow = createTestUOW();
143
+ uow.find("posts", (b) =>
144
+ b
145
+ .whereIndex("primary")
146
+ .select(["id", "title"])
147
+ .join((jb) => jb.author((ab) => ab.select(["name", "email"]))),
148
+ );
149
+
150
+ const compiler = createKyselyUOWCompiler(userSchema, config);
151
+ const compiled = uow.compile(compiler);
152
+
153
+ expect(compiled.retrievalBatch).toHaveLength(1);
154
+ const query = compiled.retrievalBatch[0];
155
+ assert(query);
156
+ expect(query.sql).toMatchInlineSnapshot(
157
+ `"select "author"."name" as "author:name", "author"."email" as "author:email", "author"."_internalId" as "author:_internalId", "author"."_version" as "author:_version", "posts"."id" as "id", "posts"."title" as "title", "posts"."_internalId" as "_internalId", "posts"."_version" as "_version" from "posts" left join "users" as "author" on "posts"."userId" = "author"."_internalId""`,
158
+ );
159
+ });
160
+
161
+ it("should compile join with WHERE conditions on joined table", () => {
162
+ const uow = createTestUOW();
163
+ uow.find("posts", (b) =>
164
+ b
165
+ .whereIndex("primary")
166
+ .select(["id", "title"])
167
+ .join((jb) =>
168
+ jb.author((ab) =>
169
+ ab.select(["name"]).whereIndex("idx_name", (eb) => eb("name", "contains", "john")),
170
+ ),
171
+ ),
172
+ );
173
+
174
+ const compiler = createKyselyUOWCompiler(userSchema, config);
175
+ const compiled = uow.compile(compiler);
176
+
177
+ expect(compiled.retrievalBatch).toHaveLength(1);
178
+ const query = compiled.retrievalBatch[0];
179
+ assert(query);
180
+ expect(query.sql).toMatchInlineSnapshot(
181
+ `"select "author"."name" as "author:name", "author"."_internalId" as "author:_internalId", "author"."_version" as "author:_version", "posts"."id" as "id", "posts"."title" as "title", "posts"."_internalId" as "_internalId", "posts"."_version" as "_version" from "posts" left join "users" as "author" on ("posts"."userId" = "author"."_internalId" and "users"."name" like $1)"`,
182
+ );
183
+ });
184
+
185
+ it("should compile self-referencing join", () => {
186
+ const uow = createTestUOW();
187
+ uow.find("users", (b) =>
188
+ b
189
+ .whereIndex("primary")
190
+ .select(["id", "name"])
191
+ .join((jb) => jb.inviter((ib) => ib.select(["name"]))),
192
+ );
193
+
194
+ const compiler = createKyselyUOWCompiler(userSchema, config);
195
+ const compiled = uow.compile(compiler);
196
+
197
+ expect(compiled.retrievalBatch).toHaveLength(1);
198
+ const query = compiled.retrievalBatch[0];
199
+ assert(query);
200
+ expect(query.sql).toMatchInlineSnapshot(
201
+ `"select "inviter"."name" as "inviter:name", "inviter"."_internalId" as "inviter:_internalId", "inviter"."_version" as "inviter:_version", "users"."id" as "id", "users"."name" as "name", "users"."_internalId" as "_internalId", "users"."_version" as "_version" from "users" left join "users" as "inviter" on "users"."invitedBy" = "inviter"."_internalId""`,
202
+ );
203
+ });
204
+
205
+ it("should compile multiple joins in single query", () => {
206
+ const uow = createTestUOW();
207
+ uow.find("comments", (b) =>
208
+ b
209
+ .whereIndex("primary")
210
+ .select(["id", "content"])
211
+ .join((jb) => jb.post((pb) => pb.select(["title"])).author((ab) => ab.select(["name"]))),
212
+ );
213
+
214
+ const compiler = createKyselyUOWCompiler(userSchema, config);
215
+ const compiled = uow.compile(compiler);
216
+
217
+ expect(compiled.retrievalBatch).toHaveLength(1);
218
+ const query = compiled.retrievalBatch[0];
219
+ assert(query);
220
+ expect(query.sql).toMatchInlineSnapshot(
221
+ `"select "post"."title" as "post:title", "post"."_internalId" as "post:_internalId", "post"."_version" as "post:_version", "author"."name" as "author:name", "author"."_internalId" as "author:_internalId", "author"."_version" as "author:_version", "comments"."id" as "id", "comments"."content" as "content", "comments"."_internalId" as "_internalId", "comments"."_version" as "_version" from "comments" left join "posts" as "post" on "comments"."postId" = "post"."_internalId" left join "users" as "author" on "comments"."authorId" = "author"."_internalId""`,
222
+ );
223
+ });
224
+
225
+ it("should compile many-to-many join through junction table", () => {
226
+ const uow = createTestUOW();
227
+ uow.find("post_tags", (b) =>
228
+ b
229
+ .whereIndex("primary")
230
+ .select(["id"])
231
+ .join((jb) => jb.post((pb) => pb.select(["title"])).tag((tb) => tb.select(["name"]))),
232
+ );
233
+
234
+ const compiler = createKyselyUOWCompiler(userSchema, config);
235
+ const compiled = uow.compile(compiler);
236
+
237
+ expect(compiled.retrievalBatch).toHaveLength(1);
238
+ const query = compiled.retrievalBatch[0];
239
+ assert(query);
240
+ expect(query.sql).toMatchInlineSnapshot(
241
+ `"select "post"."title" as "post:title", "post"."_internalId" as "post:_internalId", "post"."_version" as "post:_version", "tag"."name" as "tag:name", "tag"."_internalId" as "tag:_internalId", "tag"."_version" as "tag:_version", "post_tags"."id" as "id", "post_tags"."_internalId" as "_internalId", "post_tags"."_version" as "_version" from "post_tags" left join "posts" as "post" on "post_tags"."postId" = "post"."_internalId" left join "tags" as "tag" on "post_tags"."tagId" = "tag"."_internalId""`,
242
+ );
243
+ });
244
+
245
+ it("should compile complex join with multiple WHERE conditions", () => {
246
+ const uow = createTestUOW();
247
+ uow.find("posts", (b) =>
248
+ b
249
+ .whereIndex("primary")
250
+ .select(["id", "title"])
251
+ .join((jb) =>
252
+ jb.author((ab) =>
253
+ ab.select(["name"]).whereIndex("idx_name", (eb) =>
254
+ eb.and(
255
+ eb("name", "contains", "john"),
256
+ // @ts-expect-error - email is not indexed
257
+ eb("email", "ends with", "@example.com"),
258
+ ),
259
+ ),
260
+ ),
261
+ ),
262
+ );
263
+
264
+ const compiler = createKyselyUOWCompiler(userSchema, config);
265
+ const compiled = uow.compile(compiler);
266
+
267
+ expect(compiled.retrievalBatch).toHaveLength(1);
268
+ const query = compiled.retrievalBatch[0];
269
+ assert(query);
270
+ expect(query.sql).toMatchInlineSnapshot(
271
+ `"select "author"."name" as "author:name", "author"."_internalId" as "author:_internalId", "author"."_version" as "author:_version", "posts"."id" as "id", "posts"."title" as "title", "posts"."_internalId" as "_internalId", "posts"."_version" as "_version" from "posts" left join "users" as "author" on ("posts"."userId" = "author"."_internalId" and ("users"."name" like $1 and "users"."email" like $2))"`,
272
+ );
273
+ });
274
+
275
+ it("should compile join with ordering on joined table - LIMITATION: orderBy applied but may have limited effect", () => {
276
+ const uow = createTestUOW();
277
+ uow.find("posts", (b) =>
278
+ b
279
+ .whereIndex("primary")
280
+ .select(["id", "title"])
281
+ .join((jb) => jb.author((ab) => ab.select(["name"]).orderByIndex("idx_name", "asc"))),
282
+ );
283
+
284
+ const compiler = createKyselyUOWCompiler(userSchema, config);
285
+ const compiled = uow.compile(compiler);
286
+
287
+ expect(compiled.retrievalBatch).toHaveLength(1);
288
+ const query = compiled.retrievalBatch[0];
289
+ assert(query);
290
+ // LIMITATION: orderBy on join options is applied but has limited effect in SQL JOINs
291
+ expect(query.sql).toMatchInlineSnapshot(
292
+ `"select "author"."name" as "author:name", "author"."_internalId" as "author:_internalId", "author"."_version" as "author:_version", "posts"."id" as "id", "posts"."title" as "title", "posts"."_internalId" as "_internalId", "posts"."_version" as "_version" from "posts" left join "users" as "author" on "posts"."userId" = "author"."_internalId""`,
293
+ );
294
+ });
295
+
296
+ it("should compile join with pageSize on joined table - LIMITATION: pageSize applied but may have limited effect", () => {
297
+ const uow = createTestUOW();
298
+ uow.find("posts", (b) =>
299
+ b
300
+ .whereIndex("primary")
301
+ .select(["id", "title"])
302
+ .join((jb) => jb.author((ab) => ab.select(["name"]).pageSize(1))),
303
+ );
304
+
305
+ const compiler = createKyselyUOWCompiler(userSchema, config);
306
+ const compiled = uow.compile(compiler);
307
+
308
+ expect(compiled.retrievalBatch).toHaveLength(1);
309
+ const query = compiled.retrievalBatch[0];
310
+ assert(query);
311
+ // LIMITATION: pageSize on join options is applied but has limited effect in SQL JOINs
312
+ expect(query.sql).toMatchInlineSnapshot(
313
+ `"select "author"."name" as "author:name", "author"."_internalId" as "author:_internalId", "author"."_version" as "author:_version", "posts"."id" as "id", "posts"."title" as "title", "posts"."_internalId" as "_internalId", "posts"."_version" as "_version" from "posts" left join "users" as "author" on "posts"."userId" = "author"."_internalId""`,
314
+ );
315
+ });
316
+
317
+ // Tests for nested joins - SUPPORTED in UOW API
318
+ it("should support nested joins (join on joined table) - IMPLEMENTED in UOW API", () => {
319
+ const uow = createTestUOW();
320
+ uow.find("posts", (b) =>
321
+ b
322
+ .whereIndex("primary")
323
+ .select(["id", "title"])
324
+ .join((jb) =>
325
+ jb.author((ab) =>
326
+ ab
327
+ .select(["name"])
328
+ .join((authorJoin) => authorJoin["inviter"]((ib) => ib.select(["name"]))),
329
+ ),
330
+ ),
331
+ );
332
+
333
+ const compiler = createKyselyUOWCompiler(userSchema, config);
334
+ const compiled = uow.compile(compiler);
335
+
336
+ expect(compiled.retrievalBatch).toHaveLength(1);
337
+ const query = compiled.retrievalBatch[0];
338
+ assert(query);
339
+ // EXPECTED SQL: Should include both the author join AND the inviter join
340
+ expect(query.sql).toMatchInlineSnapshot(
341
+ `"select "author"."name" as "author:name", "author"."_internalId" as "author:_internalId", "author"."_version" as "author:_version", "author_inviter"."name" as "author:inviter:name", "author_inviter"."_internalId" as "author:inviter:_internalId", "author_inviter"."_version" as "author:inviter:_version", "posts"."id" as "id", "posts"."title" as "title", "posts"."_internalId" as "_internalId", "posts"."_version" as "_version" from "posts" left join "users" as "author" on "posts"."userId" = "author"."_internalId" left join "users" as "author_inviter" on "author"."invitedBy" = "author_inviter"."_internalId""`,
342
+ );
343
+ });
344
+ });
345
+
346
+ describe("id column selection in joins", () => {
347
+ it("should properly transform id columns in joined tables to FragnoId objects", () => {
348
+ const uow = createTestUOW();
349
+ uow.find("posts", (b) =>
350
+ b
351
+ .whereIndex("primary")
352
+ .select(["id", "title"])
353
+ .join((jb) => jb.author((ab) => ab.select(["id", "name"]))),
354
+ );
355
+
356
+ const compiler = createKyselyUOWCompiler(userSchema, config);
357
+ const compiled = uow.compile(compiler);
358
+
359
+ expect(compiled.retrievalBatch).toHaveLength(1);
360
+ const query = compiled.retrievalBatch[0];
361
+ assert(query);
362
+ expect(query.sql).toContain('"author"."id"');
363
+ expect(query.sql).toContain('"author"."_internalId" as "author:_internalId"');
364
+ expect(query.sql).toContain('"author"."_version" as "author:_version"');
365
+ expect(query.sql).toMatchInlineSnapshot(
366
+ `"select "author"."id" as "author:id", "author"."name" as "author:name", "author"."_internalId" as "author:_internalId", "author"."_version" as "author:_version", "posts"."id" as "id", "posts"."title" as "title", "posts"."_internalId" as "_internalId", "posts"."_version" as "_version" from "posts" left join "users" as "author" on "posts"."userId" = "author"."_internalId""`,
367
+ );
368
+ });
369
+
370
+ it("should select id columns from main table and joined table", () => {
371
+ const uow = createTestUOW();
372
+ uow.find("posts", (b) =>
373
+ b
374
+ .whereIndex("primary")
375
+ .select(["id", "title"])
376
+ .join((jb) => jb.author((ab) => ab.select(["id", "name"]))),
377
+ );
378
+
379
+ const compiler = createKyselyUOWCompiler(userSchema, config);
380
+ const compiled = uow.compile(compiler);
381
+
382
+ expect(compiled.retrievalBatch).toHaveLength(1);
383
+ const query = compiled.retrievalBatch[0];
384
+ assert(query);
385
+ // Both main table id and joined table id should be in the query
386
+ expect(query.sql).toContain('"posts"."id"');
387
+ expect(query.sql).toContain('"author"."id"');
388
+ // When id is selected from joined table, its _internalId is also included
389
+ expect(query.sql).toContain('"author"."_internalId" as "author:_internalId"');
390
+ expect(query.sql).toMatchInlineSnapshot(
391
+ `"select "author"."id" as "author:id", "author"."name" as "author:name", "author"."_internalId" as "author:_internalId", "author"."_version" as "author:_version", "posts"."id" as "id", "posts"."title" as "title", "posts"."_internalId" as "_internalId", "posts"."_version" as "_version" from "posts" left join "users" as "author" on "posts"."userId" = "author"."_internalId""`,
392
+ );
393
+ });
394
+
395
+ it("should handle id-only selection in join", () => {
396
+ const uow = createTestUOW();
397
+ uow.find("posts", (b) =>
398
+ b
399
+ .whereIndex("primary")
400
+ .select(["id"])
401
+ .join((jb) => jb.author((ab) => ab.select(["id"]))),
402
+ );
403
+
404
+ const compiler = createKyselyUOWCompiler(userSchema, config);
405
+ const compiled = uow.compile(compiler);
406
+
407
+ expect(compiled.retrievalBatch).toHaveLength(1);
408
+ const query = compiled.retrievalBatch[0];
409
+ assert(query);
410
+ // When only id is selected, _internalId is still included for joined table
411
+ expect(query.sql).toContain('"author"."_internalId" as "author:_internalId"');
412
+ expect(query.sql).toMatchInlineSnapshot(
413
+ `"select "author"."id" as "author:id", "author"."_internalId" as "author:_internalId", "author"."_version" as "author:_version", "posts"."id" as "id", "posts"."_internalId" as "_internalId", "posts"."_version" as "_version" from "posts" left join "users" as "author" on "posts"."userId" = "author"."_internalId""`,
414
+ );
415
+ });
416
+
417
+ it("should include _internalId for both main and joined table when id is selected", () => {
418
+ const uow = createTestUOW();
419
+ uow.find("posts", (b) =>
420
+ b
421
+ .whereIndex("primary")
422
+ .select(["id", "title"])
423
+ .join((jb) => jb.author((ab) => ab.select(["id", "name"]))),
424
+ );
425
+
426
+ const compiler = createKyselyUOWCompiler(userSchema, config);
427
+ const compiled = uow.compile(compiler);
428
+
429
+ expect(compiled.retrievalBatch).toHaveLength(1);
430
+ const query = compiled.retrievalBatch[0];
431
+ assert(query);
432
+ // Both tables should have their _internalId included when id is selected
433
+ expect(query.sql).toContain('"posts"."_internalId"');
434
+ expect(query.sql).toContain('"author"."_internalId" as "author:_internalId"');
435
+ expect(query.sql).toMatchInlineSnapshot(
436
+ `"select "author"."id" as "author:id", "author"."name" as "author:name", "author"."_internalId" as "author:_internalId", "author"."_version" as "author:_version", "posts"."id" as "id", "posts"."title" as "title", "posts"."_internalId" as "_internalId", "posts"."_version" as "_version" from "posts" left join "users" as "author" on "posts"."userId" = "author"."_internalId""`,
437
+ );
438
+ });
439
+
440
+ it("should not include _internalId when id is not selected", () => {
441
+ const uow = createTestUOW();
442
+ uow.find("posts", (b) =>
443
+ b
444
+ .whereIndex("primary")
445
+ .select(["title"])
446
+ .join((jb) => jb.author((ab) => ab.select(["name"]))),
447
+ );
448
+
449
+ const compiler = createKyselyUOWCompiler(userSchema, config);
450
+ const compiled = uow.compile(compiler);
451
+
452
+ expect(compiled.retrievalBatch).toHaveLength(1);
453
+ const query = compiled.retrievalBatch[0];
454
+ assert(query);
455
+ // Hidden columns (_internalId, _version) are always included for internal use
456
+ expect(query.sql).toContain('"posts"."_internalId" as "_internalId"');
457
+ expect(query.sql).toContain('"author"."_internalId" as "author:_internalId"');
458
+ expect(query.sql).toContain('"posts"."_version" as "_version"');
459
+ expect(query.sql).toContain('"author"."_version" as "author:_version"');
460
+ expect(query.sql).toMatchInlineSnapshot(
461
+ `"select "author"."name" as "author:name", "author"."_internalId" as "author:_internalId", "author"."_version" as "author:_version", "posts"."title" as "title", "posts"."_internalId" as "_internalId", "posts"."_version" as "_version" from "posts" left join "users" as "author" on "posts"."userId" = "author"."_internalId""`,
462
+ );
463
+ });
464
+
465
+ it("should handle select true with joins - includes all columns including id", () => {
466
+ const uow = createTestUOW();
467
+ uow.find("posts", (b) => b.whereIndex("primary").join((jb) => jb.author()));
468
+
469
+ const compiler = createKyselyUOWCompiler(userSchema, config);
470
+ const compiled = uow.compile(compiler);
471
+
472
+ expect(compiled.retrievalBatch).toHaveLength(1);
473
+ const query = compiled.retrievalBatch[0];
474
+ assert(query);
475
+ expect(query.sql).toContain('"posts"."id"');
476
+ expect(query.sql).toContain('"author"."id"');
477
+ expect(query.sql).toContain('"posts"."_internalId"');
478
+ expect(query.sql).toContain('"author"."_internalId"');
479
+ expect(query.sql).toMatchInlineSnapshot(
480
+ `"select "author"."id" as "author:id", "author"."name" as "author:name", "author"."email" as "author:email", "author"."invitedBy" as "author:invitedBy", "author"."_internalId" as "author:_internalId", "author"."_version" as "author:_version", "posts"."id" as "id", "posts"."title" as "title", "posts"."content" as "content", "posts"."userId" as "userId", "posts"."publishedAt" as "publishedAt", "posts"."_internalId" as "_internalId", "posts"."_version" as "_version" from "posts" left join "users" as "author" on "posts"."userId" = "author"."_internalId""`,
481
+ );
482
+ });
483
+
484
+ it("should handle id column in where clause on joined table", () => {
485
+ const uow = createTestUOW();
486
+ uow.find("posts", (b) =>
487
+ b
488
+ .whereIndex("primary")
489
+ .select(["id", "title"])
490
+ .join((jb) =>
491
+ jb.author((ab) =>
492
+ ab.select(["id", "name"]).whereIndex("idx_id", (eb) => eb("id", "=", "user-123")),
493
+ ),
494
+ ),
495
+ );
496
+
497
+ const compiler = createKyselyUOWCompiler(userSchema, config);
498
+ const compiled = uow.compile(compiler);
499
+
500
+ expect(compiled.retrievalBatch).toHaveLength(1);
501
+ const query = compiled.retrievalBatch[0];
502
+ assert(query);
503
+ expect(query.sql).toContain('"author"."_internalId" as "author:_internalId"');
504
+ expect(query.sql).toMatchInlineSnapshot(
505
+ `"select "author"."id" as "author:id", "author"."name" as "author:name", "author"."_internalId" as "author:_internalId", "author"."_version" as "author:_version", "posts"."id" as "id", "posts"."title" as "title", "posts"."_internalId" as "_internalId", "posts"."_version" as "_version" from "posts" left join "users" as "author" on ("posts"."userId" = "author"."_internalId" and "users"."id" = $1)"`,
506
+ );
507
+ });
508
+
509
+ it("should handle multiple joins with different id selections", () => {
510
+ const uow = createTestUOW();
511
+ uow.find("comments", (b) =>
512
+ b
513
+ .whereIndex("primary")
514
+ .select(["id", "content"])
515
+ .join((jb) =>
516
+ jb.post((pb) => pb.select(["id", "title"])).author((ab) => ab.select(["name"])),
517
+ ),
518
+ );
519
+
520
+ const compiler = createKyselyUOWCompiler(userSchema, config);
521
+ const compiled = uow.compile(compiler);
522
+
523
+ expect(compiled.retrievalBatch).toHaveLength(1);
524
+ const query = compiled.retrievalBatch[0];
525
+ assert(query);
526
+ expect(query.sql).toContain('"comments"."id"');
527
+ expect(query.sql).toContain('"post"."id"');
528
+ expect(query.sql).not.toContain('"author"."id"');
529
+ // Hidden columns are always included regardless of ID selection
530
+ expect(query.sql).toContain('"post"."_internalId" as "post:_internalId"');
531
+ expect(query.sql).toContain('"author"."_internalId" as "author:_internalId"');
532
+ expect(query.sql).toContain('"post"."_version" as "post:_version"');
533
+ expect(query.sql).toContain('"author"."_version" as "author:_version"');
534
+ expect(query.sql).toMatchInlineSnapshot(
535
+ `"select "post"."id" as "post:id", "post"."title" as "post:title", "post"."_internalId" as "post:_internalId", "post"."_version" as "post:_version", "author"."name" as "author:name", "author"."_internalId" as "author:_internalId", "author"."_version" as "author:_version", "comments"."id" as "id", "comments"."content" as "content", "comments"."_internalId" as "_internalId", "comments"."_version" as "_version" from "comments" left join "posts" as "post" on "comments"."postId" = "post"."_internalId" left join "users" as "author" on "comments"."authorId" = "author"."_internalId""`,
536
+ );
537
+ });
538
+ });
539
+
540
+ describe("custom-named id columns in joins", () => {
541
+ // Schema with custom id column names
542
+ const customIdSchema = schema((s) => {
543
+ return s
544
+ .addTable("products", (t) => {
545
+ return t
546
+ .addColumn("productId", idColumn())
547
+ .addColumn("name", column("string"))
548
+ .addColumn("price", column("integer"))
549
+ .createIndex("idx_product_id", ["productId"]);
550
+ })
551
+ .addTable("categories", (t) => {
552
+ return t
553
+ .addColumn("categoryId", idColumn())
554
+ .addColumn("categoryName", column("string"))
555
+ .createIndex("idx_category_id", ["categoryId"]);
556
+ })
557
+ .addTable("product_categories", (t) => {
558
+ return t
559
+ .addColumn("id", idColumn())
560
+ .addColumn("prodRef", referenceColumn())
561
+ .addColumn("catRef", referenceColumn())
562
+ .createIndex("idx_prod", ["prodRef"])
563
+ .createIndex("idx_cat", ["catRef"]);
564
+ })
565
+ .addReference("product", {
566
+ type: "one",
567
+ from: { table: "product_categories", column: "prodRef" },
568
+ to: { table: "products", column: "productId" },
569
+ })
570
+ .addReference("category", {
571
+ type: "one",
572
+ from: { table: "product_categories", column: "catRef" },
573
+ to: { table: "categories", column: "categoryId" },
574
+ });
575
+ });
576
+
577
+ function createCustomIdTestUOW() {
578
+ const mockCompiler = createKyselyUOWCompiler(customIdSchema, config);
579
+ const mockExecutor = {
580
+ executeRetrievalPhase: async () => [],
581
+ executeMutationPhase: async () => ({ success: true, createdInternalIds: [] }),
582
+ };
583
+ const mockDecoder: UOWDecoder<typeof customIdSchema> = (rawResults, operations) => {
584
+ if (rawResults.length !== operations.length) {
585
+ throw new Error("rawResults and ops must have the same length");
586
+ }
587
+ return rawResults;
588
+ };
589
+ return new UnitOfWork(customIdSchema, mockCompiler, mockExecutor, mockDecoder);
590
+ }
591
+
592
+ it("should compile join with custom id column names", () => {
593
+ const uow = createCustomIdTestUOW();
594
+ uow.find("product_categories", (b) =>
595
+ b
596
+ .whereIndex("primary")
597
+ .select(["id"])
598
+ .join((jb) =>
599
+ jb
600
+ .product((pb) => pb.select(["productId", "name"]))
601
+ .category((cb) => cb.select(["categoryId", "categoryName"])),
602
+ ),
603
+ );
604
+
605
+ const compiler = createKyselyUOWCompiler(customIdSchema, config);
606
+ const compiled = uow.compile(compiler);
607
+
608
+ expect(compiled.retrievalBatch).toHaveLength(1);
609
+ const query = compiled.retrievalBatch[0];
610
+ assert(query);
611
+ expect(query.sql).toContain('"product"."productId"');
612
+ expect(query.sql).toContain('"category"."categoryId"');
613
+ // Custom id columns also get their _internalId included
614
+ expect(query.sql).toContain('"product"."_internalId" as "product:_internalId"');
615
+ expect(query.sql).toContain('"category"."_internalId" as "category:_internalId"');
616
+ // Join conditions ALWAYS use _internalId (not custom column names) for performance
617
+ expect(query.sql).toContain('"product_categories"."prodRef" = "product"."_internalId"');
618
+ expect(query.sql).toContain('"product_categories"."catRef" = "category"."_internalId"');
619
+ expect(query.sql).toMatchInlineSnapshot(
620
+ `"select "product"."productId" as "product:productId", "product"."name" as "product:name", "product"."_internalId" as "product:_internalId", "product"."_version" as "product:_version", "category"."categoryId" as "category:categoryId", "category"."categoryName" as "category:categoryName", "category"."_internalId" as "category:_internalId", "category"."_version" as "category:_version", "product_categories"."id" as "id", "product_categories"."_internalId" as "_internalId", "product_categories"."_version" as "_version" from "product_categories" left join "products" as "product" on "product_categories"."prodRef" = "product"."_internalId" left join "categories" as "category" on "product_categories"."catRef" = "category"."_internalId""`,
621
+ );
622
+ });
623
+
624
+ it("should handle custom id in join where clause", () => {
625
+ const uow = createCustomIdTestUOW();
626
+ uow.find("product_categories", (b) =>
627
+ b
628
+ .whereIndex("primary")
629
+ .select(["id"])
630
+ .join((jb) =>
631
+ jb.product((pb) =>
632
+ pb
633
+ .select(["productId", "name"])
634
+ .whereIndex("idx_product_id", (eb) => eb("productId", "=", "prod-456")),
635
+ ),
636
+ ),
637
+ );
638
+
639
+ const compiler = createKyselyUOWCompiler(customIdSchema, config);
640
+ const compiled = uow.compile(compiler);
641
+
642
+ expect(compiled.retrievalBatch).toHaveLength(1);
643
+ const query = compiled.retrievalBatch[0];
644
+ assert(query);
645
+ expect(query.sql).toContain('"products"."productId" = $1');
646
+ expect(query.sql).toContain('"product"."_internalId" as "product:_internalId"');
647
+ // Join condition ALWAYS uses _internalId for performance
648
+ expect(query.sql).toContain('"product_categories"."prodRef" = "product"."_internalId"');
649
+ expect(query.sql).toMatchInlineSnapshot(
650
+ `"select "product"."productId" as "product:productId", "product"."name" as "product:name", "product"."_internalId" as "product:_internalId", "product"."_version" as "product:_version", "product_categories"."id" as "id", "product_categories"."_internalId" as "_internalId", "product_categories"."_version" as "_version" from "product_categories" left join "products" as "product" on ("product_categories"."prodRef" = "product"."_internalId" and "products"."productId" = $1)"`,
651
+ );
652
+ });
653
+
654
+ it("should handle select true with custom id columns", () => {
655
+ const uow = createCustomIdTestUOW();
656
+ uow.find("product_categories", (b) => b.whereIndex("primary").join((jb) => jb.product()));
657
+
658
+ const compiler = createKyselyUOWCompiler(customIdSchema, config);
659
+ const compiled = uow.compile(compiler);
660
+
661
+ expect(compiled.retrievalBatch).toHaveLength(1);
662
+ const query = compiled.retrievalBatch[0];
663
+ assert(query);
664
+ expect(query.sql).toContain('"product"."productId"');
665
+ expect(query.sql).toContain('"product_categories"."id"');
666
+ // With select true, _internalId IS now included when table has an id column
667
+ expect(query.sql).toContain('"product"."_internalId" as "product:_internalId"');
668
+ // Join condition ALWAYS uses _internalId for performance
669
+ expect(query.sql).toContain('"product_categories"."prodRef" = "product"."_internalId"');
670
+ expect(query.sql).toMatchInlineSnapshot(
671
+ `"select "product"."productId" as "product:productId", "product"."name" as "product:name", "product"."price" as "product:price", "product"."_internalId" as "product:_internalId", "product"."_version" as "product:_version", "product_categories"."id" as "id", "product_categories"."prodRef" as "prodRef", "product_categories"."catRef" as "catRef", "product_categories"."_internalId" as "_internalId", "product_categories"."_version" as "_version" from "product_categories" left join "products" as "product" on "product_categories"."prodRef" = "product"."_internalId""`,
672
+ );
673
+ });
674
+
675
+ it("should join tables with different custom id names", () => {
676
+ const uow = createCustomIdTestUOW();
677
+ uow.find("product_categories", (b) =>
678
+ b
679
+ .whereIndex("primary")
680
+ .select(["id"])
681
+ .join((jb) =>
682
+ jb
683
+ .product((pb) => pb.select(["productId"]))
684
+ .category((cb) => cb.select(["categoryId"])),
685
+ ),
686
+ );
687
+
688
+ const compiler = createKyselyUOWCompiler(customIdSchema, config);
689
+ const compiled = uow.compile(compiler);
690
+
691
+ expect(compiled.retrievalBatch).toHaveLength(1);
692
+ const query = compiled.retrievalBatch[0];
693
+ assert(query);
694
+ expect(query.sql).toContain('"product"."productId"');
695
+ expect(query.sql).toContain('"category"."categoryId"');
696
+ expect(query.sql).not.toContain('"product"."id"'); // Should not have 'id', only 'productId'
697
+ expect(query.sql).not.toContain('"category"."id"'); // Should not have 'id', only 'categoryId'
698
+ // _internalId included for both since custom id columns are selected
699
+ expect(query.sql).toContain('"product"."_internalId" as "product:_internalId"');
700
+ expect(query.sql).toContain('"category"."_internalId" as "category:_internalId"');
701
+ // Join conditions ALWAYS use _internalId for performance
702
+ expect(query.sql).toContain('"product_categories"."prodRef" = "product"."_internalId"');
703
+ expect(query.sql).toContain('"product_categories"."catRef" = "category"."_internalId"');
704
+ expect(query.sql).toMatchInlineSnapshot(
705
+ `"select "product"."productId" as "product:productId", "product"."_internalId" as "product:_internalId", "product"."_version" as "product:_version", "category"."categoryId" as "category:categoryId", "category"."_internalId" as "category:_internalId", "category"."_version" as "category:_version", "product_categories"."id" as "id", "product_categories"."_internalId" as "_internalId", "product_categories"."_version" as "_version" from "product_categories" left join "products" as "product" on "product_categories"."prodRef" = "product"."_internalId" left join "categories" as "category" on "product_categories"."catRef" = "category"."_internalId""`,
706
+ );
707
+ });
708
+ });
709
+
710
+ describe("special columns in joins - _internalId aliasing", () => {
711
+ it("should properly alias _internalId for joined tables", () => {
712
+ const uow = createTestUOW();
713
+ uow.find("posts", (b) =>
714
+ b
715
+ .whereIndex("primary")
716
+ .select(["id"])
717
+ .join((jb) => jb.author((ab) => ab.select(["id"]))),
718
+ );
719
+
720
+ const compiler = createKyselyUOWCompiler(userSchema, config);
721
+ const compiled = uow.compile(compiler);
722
+
723
+ expect(compiled.retrievalBatch).toHaveLength(1);
724
+ const query = compiled.retrievalBatch[0];
725
+ assert(query);
726
+ // Main table _internalId should be aliased as "_internalId"
727
+ expect(query.sql).toContain('"posts"."_internalId" as "_internalId"');
728
+ // Joined table _internalId should be aliased as "relation:_internalId" when id is selected
729
+ expect(query.sql).toContain('"author"."_internalId" as "author:_internalId"');
730
+ expect(query.sql).toMatchInlineSnapshot(
731
+ `"select "author"."id" as "author:id", "author"."_internalId" as "author:_internalId", "author"."_version" as "author:_version", "posts"."id" as "id", "posts"."_internalId" as "_internalId", "posts"."_version" as "_version" from "posts" left join "users" as "author" on "posts"."userId" = "author"."_internalId""`,
732
+ );
733
+ });
734
+
735
+ it("should handle _internalId in join conditions", () => {
736
+ const uow = createTestUOW();
737
+ uow.find("posts", (b) =>
738
+ b
739
+ .whereIndex("primary")
740
+ .select(["id", "title"])
741
+ .join((jb) => jb.author((ab) => ab.select(["id", "name"]))),
742
+ );
743
+
744
+ const compiler = createKyselyUOWCompiler(userSchema, config);
745
+ const compiled = uow.compile(compiler);
746
+
747
+ expect(compiled.retrievalBatch).toHaveLength(1);
748
+ const query = compiled.retrievalBatch[0];
749
+ assert(query);
750
+ // Join condition should use _internalId
751
+ expect(query.sql).toContain('"posts"."userId" = "author"."_internalId"');
752
+ // Joined table _internalId is included when id is selected
753
+ expect(query.sql).toContain('"author"."_internalId" as "author:_internalId"');
754
+ expect(query.sql).toMatchInlineSnapshot(
755
+ `"select "author"."id" as "author:id", "author"."name" as "author:name", "author"."_internalId" as "author:_internalId", "author"."_version" as "author:_version", "posts"."id" as "id", "posts"."title" as "title", "posts"."_internalId" as "_internalId", "posts"."_version" as "_version" from "posts" left join "users" as "author" on "posts"."userId" = "author"."_internalId""`,
756
+ );
757
+ });
758
+
759
+ it("should handle multiple joins with _internalId tracking", () => {
760
+ const uow = createTestUOW();
761
+ uow.find("comments", (b) =>
762
+ b
763
+ .whereIndex("primary")
764
+ .select(["id"])
765
+ .join((jb) => jb.post((pb) => pb.select(["id"])).author((ab) => ab.select(["id"]))),
766
+ );
767
+
768
+ const compiler = createKyselyUOWCompiler(userSchema, config);
769
+ const compiled = uow.compile(compiler);
770
+
771
+ expect(compiled.retrievalBatch).toHaveLength(1);
772
+ const query = compiled.retrievalBatch[0];
773
+ assert(query);
774
+ // All _internalId should be properly aliased
775
+ expect(query.sql).toContain('"comments"."_internalId" as "_internalId"');
776
+ // Joined tables with id selected also get _internalId
777
+ expect(query.sql).toContain('"post"."_internalId" as "post:_internalId"');
778
+ expect(query.sql).toContain('"author"."_internalId" as "author:_internalId"');
779
+ // Join should use _internalId for connection
780
+ expect(query.sql).toContain('"comments"."postId" = "post"."_internalId"');
781
+ expect(query.sql).toContain('"comments"."authorId" = "author"."_internalId"');
782
+ expect(query.sql).toMatchInlineSnapshot(
783
+ `"select "post"."id" as "post:id", "post"."_internalId" as "post:_internalId", "post"."_version" as "post:_version", "author"."id" as "author:id", "author"."_internalId" as "author:_internalId", "author"."_version" as "author:_version", "comments"."id" as "id", "comments"."_internalId" as "_internalId", "comments"."_version" as "_version" from "comments" left join "posts" as "post" on "comments"."postId" = "post"."_internalId" left join "users" as "author" on "comments"."authorId" = "author"."_internalId""`,
784
+ );
785
+ });
786
+
787
+ it("should handle self-referencing join with _internalId", () => {
788
+ const uow = createTestUOW();
789
+ uow.find("users", (b) =>
790
+ b
791
+ .whereIndex("primary")
792
+ .select(["id", "name"])
793
+ .join((jb) => jb.inviter((ib) => ib.select(["id", "name"]))),
794
+ );
795
+
796
+ const compiler = createKyselyUOWCompiler(userSchema, config);
797
+ const compiled = uow.compile(compiler);
798
+
799
+ expect(compiled.retrievalBatch).toHaveLength(1);
800
+ const query = compiled.retrievalBatch[0];
801
+ assert(query);
802
+ expect(query.sql).toContain('"users"."invitedBy" = "inviter"."_internalId"');
803
+ expect(query.sql).toContain('"users"."_internalId" as "_internalId"');
804
+ // Self-join with id selected also includes _internalId for joined instance
805
+ expect(query.sql).toContain('"inviter"."_internalId" as "inviter:_internalId"');
806
+ expect(query.sql).toMatchInlineSnapshot(
807
+ `"select "inviter"."id" as "inviter:id", "inviter"."name" as "inviter:name", "inviter"."_internalId" as "inviter:_internalId", "inviter"."_version" as "inviter:_version", "users"."id" as "id", "users"."name" as "name", "users"."_internalId" as "_internalId", "users"."_version" as "_version" from "users" left join "users" as "inviter" on "users"."invitedBy" = "inviter"."_internalId""`,
808
+ );
809
+ });
810
+ });
811
+ });