@simplysm/orm-common 13.0.68 → 13.0.70

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 (204) hide show
  1. package/README.md +54 -1447
  2. package/dist/create-db-context.d.ts +10 -10
  3. package/dist/create-db-context.js +9 -9
  4. package/dist/create-db-context.js.map +1 -1
  5. package/dist/ddl/column-ddl.d.ts +4 -4
  6. package/dist/ddl/initialize.d.ts +17 -17
  7. package/dist/ddl/initialize.js +2 -2
  8. package/dist/ddl/initialize.js.map +1 -1
  9. package/dist/ddl/relation-ddl.d.ts +6 -6
  10. package/dist/ddl/schema-ddl.d.ts +4 -4
  11. package/dist/ddl/table-ddl.d.ts +24 -24
  12. package/dist/ddl/table-ddl.js +4 -4
  13. package/dist/ddl/table-ddl.js.map +1 -1
  14. package/dist/errors/db-transaction-error.d.ts +15 -15
  15. package/dist/errors/db-transaction-error.d.ts.map +1 -1
  16. package/dist/exec/executable.d.ts +23 -23
  17. package/dist/exec/executable.js +3 -3
  18. package/dist/exec/executable.js.map +1 -1
  19. package/dist/exec/queryable.d.ts +160 -160
  20. package/dist/exec/queryable.js +119 -119
  21. package/dist/exec/queryable.js.map +1 -1
  22. package/dist/exec/search-parser.d.ts +37 -37
  23. package/dist/exec/search-parser.d.ts.map +1 -1
  24. package/dist/expr/expr-unit.d.ts +4 -4
  25. package/dist/expr/expr.d.ts +257 -257
  26. package/dist/expr/expr.js +265 -265
  27. package/dist/expr/expr.js.map +1 -1
  28. package/dist/query-builder/base/expr-renderer-base.d.ts +9 -9
  29. package/dist/query-builder/base/expr-renderer-base.js +2 -2
  30. package/dist/query-builder/base/expr-renderer-base.js.map +1 -1
  31. package/dist/query-builder/base/query-builder-base.d.ts +26 -26
  32. package/dist/query-builder/base/query-builder-base.d.ts.map +1 -1
  33. package/dist/query-builder/base/query-builder-base.js +22 -22
  34. package/dist/query-builder/base/query-builder-base.js.map +1 -1
  35. package/dist/query-builder/mssql/mssql-expr-renderer.d.ts +4 -4
  36. package/dist/query-builder/mssql/mssql-expr-renderer.d.ts.map +1 -1
  37. package/dist/query-builder/mssql/mssql-expr-renderer.js +18 -18
  38. package/dist/query-builder/mssql/mssql-expr-renderer.js.map +1 -1
  39. package/dist/query-builder/mssql/mssql-query-builder.d.ts +2 -2
  40. package/dist/query-builder/mssql/mssql-query-builder.d.ts.map +1 -1
  41. package/dist/query-builder/mssql/mssql-query-builder.js +11 -11
  42. package/dist/query-builder/mssql/mssql-query-builder.js.map +1 -1
  43. package/dist/query-builder/mysql/mysql-expr-renderer.d.ts +4 -4
  44. package/dist/query-builder/mysql/mysql-expr-renderer.d.ts.map +1 -1
  45. package/dist/query-builder/mysql/mysql-expr-renderer.js +17 -17
  46. package/dist/query-builder/mysql/mysql-expr-renderer.js.map +1 -1
  47. package/dist/query-builder/mysql/mysql-query-builder.d.ts +8 -8
  48. package/dist/query-builder/mysql/mysql-query-builder.d.ts.map +1 -1
  49. package/dist/query-builder/mysql/mysql-query-builder.js +5 -5
  50. package/dist/query-builder/mysql/mysql-query-builder.js.map +1 -1
  51. package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts +4 -4
  52. package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts.map +1 -1
  53. package/dist/query-builder/postgresql/postgresql-expr-renderer.js +17 -17
  54. package/dist/query-builder/postgresql/postgresql-expr-renderer.js.map +1 -1
  55. package/dist/query-builder/postgresql/postgresql-query-builder.d.ts +5 -5
  56. package/dist/query-builder/postgresql/postgresql-query-builder.d.ts.map +1 -1
  57. package/dist/query-builder/postgresql/postgresql-query-builder.js +8 -8
  58. package/dist/query-builder/postgresql/postgresql-query-builder.js.map +1 -1
  59. package/dist/query-builder/query-builder.d.ts +1 -1
  60. package/dist/schema/factory/column-builder.d.ts +79 -79
  61. package/dist/schema/factory/column-builder.js +42 -42
  62. package/dist/schema/factory/index-builder.d.ts +39 -39
  63. package/dist/schema/factory/index-builder.js +26 -26
  64. package/dist/schema/factory/relation-builder.d.ts +99 -99
  65. package/dist/schema/factory/relation-builder.d.ts.map +1 -1
  66. package/dist/schema/factory/relation-builder.js +38 -38
  67. package/dist/schema/procedure-builder.d.ts +49 -49
  68. package/dist/schema/procedure-builder.d.ts.map +1 -1
  69. package/dist/schema/procedure-builder.js +33 -33
  70. package/dist/schema/table-builder.d.ts +59 -59
  71. package/dist/schema/table-builder.d.ts.map +1 -1
  72. package/dist/schema/table-builder.js +43 -43
  73. package/dist/schema/view-builder.d.ts +49 -49
  74. package/dist/schema/view-builder.d.ts.map +1 -1
  75. package/dist/schema/view-builder.js +32 -32
  76. package/dist/types/column.d.ts +22 -22
  77. package/dist/types/column.js +1 -1
  78. package/dist/types/column.js.map +1 -1
  79. package/dist/types/db.d.ts +40 -40
  80. package/dist/types/expr.d.ts +59 -59
  81. package/dist/types/expr.d.ts.map +1 -1
  82. package/dist/types/query-def.d.ts +44 -44
  83. package/dist/types/query-def.d.ts.map +1 -1
  84. package/dist/utils/result-parser.d.ts +11 -11
  85. package/dist/utils/result-parser.js +3 -3
  86. package/dist/utils/result-parser.js.map +1 -1
  87. package/package.json +5 -5
  88. package/src/create-db-context.ts +20 -20
  89. package/src/ddl/column-ddl.ts +4 -4
  90. package/src/ddl/initialize.ts +259 -259
  91. package/src/ddl/relation-ddl.ts +89 -89
  92. package/src/ddl/schema-ddl.ts +4 -4
  93. package/src/ddl/table-ddl.ts +189 -189
  94. package/src/errors/db-transaction-error.ts +13 -13
  95. package/src/exec/executable.ts +25 -25
  96. package/src/exec/queryable.ts +2033 -2033
  97. package/src/exec/search-parser.ts +57 -57
  98. package/src/expr/expr-unit.ts +4 -4
  99. package/src/expr/expr.ts +2140 -2140
  100. package/src/query-builder/base/expr-renderer-base.ts +237 -237
  101. package/src/query-builder/base/query-builder-base.ts +213 -213
  102. package/src/query-builder/mssql/mssql-expr-renderer.ts +607 -607
  103. package/src/query-builder/mssql/mssql-query-builder.ts +650 -650
  104. package/src/query-builder/mysql/mysql-expr-renderer.ts +613 -613
  105. package/src/query-builder/mysql/mysql-query-builder.ts +759 -759
  106. package/src/query-builder/postgresql/postgresql-expr-renderer.ts +611 -611
  107. package/src/query-builder/postgresql/postgresql-query-builder.ts +686 -686
  108. package/src/query-builder/query-builder.ts +19 -19
  109. package/src/schema/factory/column-builder.ts +423 -423
  110. package/src/schema/factory/index-builder.ts +164 -164
  111. package/src/schema/factory/relation-builder.ts +453 -453
  112. package/src/schema/procedure-builder.ts +232 -232
  113. package/src/schema/table-builder.ts +319 -319
  114. package/src/schema/view-builder.ts +221 -221
  115. package/src/types/column.ts +188 -188
  116. package/src/types/db.ts +208 -208
  117. package/src/types/expr.ts +697 -697
  118. package/src/types/query-def.ts +513 -513
  119. package/src/utils/result-parser.ts +458 -458
  120. package/tests/db-context/create-db-context.spec.ts +224 -0
  121. package/tests/db-context/define-db-context.spec.ts +68 -0
  122. package/tests/ddl/basic.expected.ts +341 -0
  123. package/tests/ddl/basic.spec.ts +714 -0
  124. package/tests/ddl/column-builder.expected.ts +310 -0
  125. package/tests/ddl/column-builder.spec.ts +637 -0
  126. package/tests/ddl/index-builder.expected.ts +38 -0
  127. package/tests/ddl/index-builder.spec.ts +202 -0
  128. package/tests/ddl/procedure-builder.expected.ts +52 -0
  129. package/tests/ddl/procedure-builder.spec.ts +234 -0
  130. package/tests/ddl/relation-builder.expected.ts +36 -0
  131. package/tests/ddl/relation-builder.spec.ts +372 -0
  132. package/tests/ddl/table-builder.expected.ts +113 -0
  133. package/tests/ddl/table-builder.spec.ts +433 -0
  134. package/tests/ddl/view-builder.expected.ts +38 -0
  135. package/tests/ddl/view-builder.spec.ts +176 -0
  136. package/tests/dml/delete.expected.ts +96 -0
  137. package/tests/dml/delete.spec.ts +160 -0
  138. package/tests/dml/insert.expected.ts +192 -0
  139. package/tests/dml/insert.spec.ts +288 -0
  140. package/tests/dml/update.expected.ts +176 -0
  141. package/tests/dml/update.spec.ts +318 -0
  142. package/tests/dml/upsert.expected.ts +215 -0
  143. package/tests/dml/upsert.spec.ts +242 -0
  144. package/tests/errors/queryable-errors.spec.ts +177 -0
  145. package/tests/escape.spec.ts +100 -0
  146. package/tests/examples/pivot.expected.ts +211 -0
  147. package/tests/examples/pivot.spec.ts +533 -0
  148. package/tests/examples/sampling.expected.ts +69 -0
  149. package/tests/examples/sampling.spec.ts +104 -0
  150. package/tests/examples/unpivot.expected.ts +120 -0
  151. package/tests/examples/unpivot.spec.ts +226 -0
  152. package/tests/exec/search-parser.spec.ts +283 -0
  153. package/tests/executable/basic.expected.ts +18 -0
  154. package/tests/executable/basic.spec.ts +54 -0
  155. package/tests/expr/comparison.expected.ts +282 -0
  156. package/tests/expr/comparison.spec.ts +400 -0
  157. package/tests/expr/conditional.expected.ts +134 -0
  158. package/tests/expr/conditional.spec.ts +276 -0
  159. package/tests/expr/date.expected.ts +332 -0
  160. package/tests/expr/date.spec.ts +526 -0
  161. package/tests/expr/math.expected.ts +62 -0
  162. package/tests/expr/math.spec.ts +106 -0
  163. package/tests/expr/string.expected.ts +218 -0
  164. package/tests/expr/string.spec.ts +356 -0
  165. package/tests/expr/utility.expected.ts +147 -0
  166. package/tests/expr/utility.spec.ts +182 -0
  167. package/tests/select/basic.expected.ts +322 -0
  168. package/tests/select/basic.spec.ts +502 -0
  169. package/tests/select/filter.expected.ts +357 -0
  170. package/tests/select/filter.spec.ts +1068 -0
  171. package/tests/select/group.expected.ts +169 -0
  172. package/tests/select/group.spec.ts +244 -0
  173. package/tests/select/join.expected.ts +582 -0
  174. package/tests/select/join.spec.ts +805 -0
  175. package/tests/select/order.expected.ts +150 -0
  176. package/tests/select/order.spec.ts +189 -0
  177. package/tests/select/recursive-cte.expected.ts +244 -0
  178. package/tests/select/recursive-cte.spec.ts +514 -0
  179. package/tests/select/result-meta.spec.ts +270 -0
  180. package/tests/select/subquery.expected.ts +363 -0
  181. package/tests/select/subquery.spec.ts +537 -0
  182. package/tests/select/view.expected.ts +155 -0
  183. package/tests/select/view.spec.ts +235 -0
  184. package/tests/select/window.expected.ts +345 -0
  185. package/tests/select/window.spec.ts +618 -0
  186. package/tests/setup/MockExecutor.ts +18 -0
  187. package/tests/setup/TestDbContext.ts +59 -0
  188. package/tests/setup/models/Company.ts +13 -0
  189. package/tests/setup/models/Employee.ts +10 -0
  190. package/tests/setup/models/MonthlySales.ts +11 -0
  191. package/tests/setup/models/Post.ts +16 -0
  192. package/tests/setup/models/Sales.ts +10 -0
  193. package/tests/setup/models/User.ts +19 -0
  194. package/tests/setup/procedure/GetAllUsers.ts +9 -0
  195. package/tests/setup/procedure/GetUserById.ts +12 -0
  196. package/tests/setup/test-utils.ts +72 -0
  197. package/tests/setup/views/ActiveUsers.ts +8 -0
  198. package/tests/setup/views/UserSummary.ts +11 -0
  199. package/tests/types/nullable-queryable-record.spec.ts +145 -0
  200. package/tests/utils/result-parser-perf.spec.ts +210 -0
  201. package/tests/utils/result-parser.spec.ts +701 -0
  202. package/docs/expressions.md +0 -172
  203. package/docs/queries.md +0 -444
  204. package/docs/schema.md +0 -245
@@ -0,0 +1,805 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createTestDb } from "../setup/TestDbContext";
3
+ import { Post } from "../setup/models/Post";
4
+ import { User } from "../setup/models/User";
5
+ import { Company } from "../setup/models/Company";
6
+ import { expr } from "../../src/expr/expr";
7
+ import { createQueryBuilder } from "../../src/query-builder/query-builder";
8
+ import { dialects } from "../setup/test-utils";
9
+ import "../setup/test-utils"; // toMatchSql matcher
10
+ import * as expected from "./join.expected";
11
+
12
+ describe("SELECT - JOIN", () => {
13
+ describe("Basic", () => {
14
+ const db = createTestDb();
15
+ const def = db
16
+ .user()
17
+ .join("post", (q, c) => q.from(Post).where((item) => [expr.eq(item.userId, c.id)]))
18
+ .getSelectQueryDef();
19
+
20
+ it("Verify QueryDef", () => {
21
+ expect(def).toEqual({
22
+ type: "select",
23
+ as: "T1",
24
+ from: { database: "TestDb", schema: "TestSchema", name: "User" },
25
+ select: {
26
+ "id": { type: "column", path: ["T1", "id"] },
27
+ "name": { type: "column", path: ["T1", "name"] },
28
+ "email": { type: "column", path: ["T1", "email"] },
29
+ "age": { type: "column", path: ["T1", "age"] },
30
+ "isActive": { type: "column", path: ["T1", "isActive"] },
31
+ "companyId": { type: "column", path: ["T1", "companyId"] },
32
+ "createdAt": { type: "column", path: ["T1", "createdAt"] },
33
+ "post.id": { type: "column", path: ["T1.post", "id"] },
34
+ "post.userId": { type: "column", path: ["T1.post", "userId"] },
35
+ "post.title": { type: "column", path: ["T1.post", "title"] },
36
+ "post.content": { type: "column", path: ["T1.post", "content"] },
37
+ "post.viewCount": { type: "column", path: ["T1.post", "viewCount"] },
38
+ "post.publishedAt": { type: "column", path: ["T1.post", "publishedAt"] },
39
+ },
40
+ joins: [
41
+ {
42
+ type: "select",
43
+ as: "T1.post",
44
+ from: { database: "TestDb", schema: "TestSchema", name: "Post" },
45
+ isSingle: false,
46
+ where: [
47
+ {
48
+ type: "eq",
49
+ source: { type: "column", path: ["T1.post", "userId"] },
50
+ target: { type: "column", path: ["T1", "id"] },
51
+ },
52
+ ],
53
+ },
54
+ ],
55
+ });
56
+ });
57
+
58
+ it.each(dialects)("[%s] Verify SQL", (dialect) => {
59
+ const builder = createQueryBuilder(dialect);
60
+ expect(builder.build(def)).toMatchSql(expected.joinBasic[dialect]);
61
+ });
62
+ });
63
+
64
+ describe("joinSingle", () => {
65
+ const db = createTestDb();
66
+ const def = db
67
+ .post()
68
+ .joinSingle("user", (q, c) => q.from(User).where((item) => [expr.eq(item.id, c.userId)]))
69
+ .getSelectQueryDef();
70
+
71
+ it("Verify QueryDef", () => {
72
+ expect(def).toEqual({
73
+ type: "select",
74
+ as: "T1",
75
+ from: { database: "TestDb", schema: "TestSchema", name: "Post" },
76
+ select: {
77
+ "id": { type: "column", path: ["T1", "id"] },
78
+ "userId": { type: "column", path: ["T1", "userId"] },
79
+ "title": { type: "column", path: ["T1", "title"] },
80
+ "content": { type: "column", path: ["T1", "content"] },
81
+ "viewCount": { type: "column", path: ["T1", "viewCount"] },
82
+ "publishedAt": { type: "column", path: ["T1", "publishedAt"] },
83
+ "user.id": { type: "column", path: ["T1.user", "id"] },
84
+ "user.name": { type: "column", path: ["T1.user", "name"] },
85
+ "user.email": { type: "column", path: ["T1.user", "email"] },
86
+ "user.age": { type: "column", path: ["T1.user", "age"] },
87
+ "user.isActive": { type: "column", path: ["T1.user", "isActive"] },
88
+ "user.companyId": { type: "column", path: ["T1.user", "companyId"] },
89
+ "user.createdAt": { type: "column", path: ["T1.user", "createdAt"] },
90
+ },
91
+ joins: [
92
+ {
93
+ type: "select",
94
+ as: "T1.user",
95
+ from: { database: "TestDb", schema: "TestSchema", name: "User" },
96
+ isSingle: true,
97
+ where: [
98
+ {
99
+ type: "eq",
100
+ source: { type: "column", path: ["T1.user", "id"] },
101
+ target: { type: "column", path: ["T1", "userId"] },
102
+ },
103
+ ],
104
+ },
105
+ ],
106
+ });
107
+ });
108
+
109
+ it.each(dialects)("[%s] Verify SQL", (dialect) => {
110
+ const builder = createQueryBuilder(dialect);
111
+ expect(builder.build(def)).toMatchSql(expected.joinSingle[dialect]);
112
+ });
113
+ });
114
+
115
+ it("join after select", () => {
116
+ const db = createTestDb();
117
+ const def = db
118
+ .user()
119
+ .select((item) => ({ id: item.id, name: item.name }))
120
+ .join("post", (q, c) => q.from(Post).where((item) => [expr.eq(item.userId, c.id)]))
121
+ .getSelectQueryDef();
122
+
123
+ expect(def).toEqual({
124
+ type: "select",
125
+ as: "T1",
126
+ from: { database: "TestDb", schema: "TestSchema", name: "User" },
127
+ select: {
128
+ "id": { type: "column", path: ["T1", "id"] },
129
+ "name": { type: "column", path: ["T1", "name"] },
130
+ "post.id": { type: "column", path: ["T1.post", "id"] },
131
+ "post.userId": { type: "column", path: ["T1.post", "userId"] },
132
+ "post.title": { type: "column", path: ["T1.post", "title"] },
133
+ "post.content": { type: "column", path: ["T1.post", "content"] },
134
+ "post.viewCount": { type: "column", path: ["T1.post", "viewCount"] },
135
+ "post.publishedAt": { type: "column", path: ["T1.post", "publishedAt"] },
136
+ },
137
+ joins: [
138
+ {
139
+ type: "select",
140
+ as: "T1.post",
141
+ from: { database: "TestDb", schema: "TestSchema", name: "Post" },
142
+ isSingle: false,
143
+ where: [
144
+ {
145
+ type: "eq",
146
+ source: { type: "column", path: ["T1.post", "userId"] },
147
+ target: { type: "column", path: ["T1", "id"] },
148
+ },
149
+ ],
150
+ },
151
+ ],
152
+ });
153
+ });
154
+
155
+ it("Multiple join", () => {
156
+ const db = createTestDb();
157
+ const def = db
158
+ .user()
159
+ .join("posts", (q, c) => q.from(Post).where((item) => [expr.eq(item.userId, c.id)]))
160
+ .join("company", (q, c) => q.from(Company).where((item) => [expr.eq(item.id, c.companyId)]))
161
+ .getSelectQueryDef();
162
+
163
+ expect(def).toEqual({
164
+ type: "select",
165
+ as: "T1",
166
+ from: { database: "TestDb", schema: "TestSchema", name: "User" },
167
+ select: {
168
+ "id": { type: "column", path: ["T1", "id"] },
169
+ "name": { type: "column", path: ["T1", "name"] },
170
+ "email": { type: "column", path: ["T1", "email"] },
171
+ "age": { type: "column", path: ["T1", "age"] },
172
+ "isActive": { type: "column", path: ["T1", "isActive"] },
173
+ "companyId": { type: "column", path: ["T1", "companyId"] },
174
+ "createdAt": { type: "column", path: ["T1", "createdAt"] },
175
+ "posts.id": { type: "column", path: ["T1.posts", "id"] },
176
+ "posts.userId": { type: "column", path: ["T1.posts", "userId"] },
177
+ "posts.title": { type: "column", path: ["T1.posts", "title"] },
178
+ "posts.content": { type: "column", path: ["T1.posts", "content"] },
179
+ "posts.viewCount": { type: "column", path: ["T1.posts", "viewCount"] },
180
+ "posts.publishedAt": { type: "column", path: ["T1.posts", "publishedAt"] },
181
+ "company.id": { type: "column", path: ["T1.company", "id"] },
182
+ "company.name": { type: "column", path: ["T1.company", "name"] },
183
+ "company.foundedAt": { type: "column", path: ["T1.company", "foundedAt"] },
184
+ },
185
+ joins: [
186
+ {
187
+ type: "select",
188
+ as: "T1.posts",
189
+ from: { database: "TestDb", schema: "TestSchema", name: "Post" },
190
+ isSingle: false,
191
+ where: [
192
+ {
193
+ type: "eq",
194
+ source: { type: "column", path: ["T1.posts", "userId"] },
195
+ target: { type: "column", path: ["T1", "id"] },
196
+ },
197
+ ],
198
+ },
199
+ {
200
+ type: "select",
201
+ as: "T1.company",
202
+ from: { database: "TestDb", schema: "TestSchema", name: "Company" },
203
+ isSingle: false,
204
+ where: [
205
+ {
206
+ type: "eq",
207
+ source: { type: "column", path: ["T1.company", "id"] },
208
+ target: { type: "column", path: ["T1", "companyId"] },
209
+ },
210
+ ],
211
+ },
212
+ ],
213
+ });
214
+ });
215
+
216
+ describe("다단계 join(Single)", () => {
217
+ const db = createTestDb();
218
+ const def = db
219
+ .post()
220
+ .joinSingle("user", (q, c) =>
221
+ q
222
+ .from(User)
223
+ .joinSingle("company", (q2, c2) =>
224
+ q2.from(Company).where((item) => [expr.eq(item.id, c2.companyId)]),
225
+ )
226
+ .where((item) => [expr.eq(item.id, c.userId)]),
227
+ )
228
+ .getSelectQueryDef();
229
+
230
+ it("Verify QueryDef", () => {
231
+ expect(def).toEqual({
232
+ type: "select",
233
+ as: "T1",
234
+ from: { database: "TestDb", schema: "TestSchema", name: "Post" },
235
+ select: {
236
+ "id": { type: "column", path: ["T1", "id"] },
237
+ "userId": { type: "column", path: ["T1", "userId"] },
238
+ "title": { type: "column", path: ["T1", "title"] },
239
+ "content": { type: "column", path: ["T1", "content"] },
240
+ "viewCount": { type: "column", path: ["T1", "viewCount"] },
241
+ "publishedAt": { type: "column", path: ["T1", "publishedAt"] },
242
+ "user.id": { type: "column", path: ["T1.user", "id"] },
243
+ "user.name": { type: "column", path: ["T1.user", "name"] },
244
+ "user.email": { type: "column", path: ["T1.user", "email"] },
245
+ "user.age": { type: "column", path: ["T1.user", "age"] },
246
+ "user.isActive": { type: "column", path: ["T1.user", "isActive"] },
247
+ "user.companyId": { type: "column", path: ["T1.user", "companyId"] },
248
+ "user.createdAt": { type: "column", path: ["T1.user", "createdAt"] },
249
+ "user.company.id": { type: "column", path: ["T1.user", "company.id"] },
250
+ "user.company.name": { type: "column", path: ["T1.user", "company.name"] },
251
+ "user.company.foundedAt": { type: "column", path: ["T1.user", "company.foundedAt"] },
252
+ },
253
+ joins: [
254
+ {
255
+ type: "select",
256
+ as: "T1.user",
257
+ from: { database: "TestDb", schema: "TestSchema", name: "User" },
258
+ isSingle: true,
259
+ select: {
260
+ "id": { type: "column", path: ["T1.user", "id"] },
261
+ "name": { type: "column", path: ["T1.user", "name"] },
262
+ "email": { type: "column", path: ["T1.user", "email"] },
263
+ "age": { type: "column", path: ["T1.user", "age"] },
264
+ "isActive": { type: "column", path: ["T1.user", "isActive"] },
265
+ "companyId": { type: "column", path: ["T1.user", "companyId"] },
266
+ "createdAt": { type: "column", path: ["T1.user", "createdAt"] },
267
+ "company.id": { type: "column", path: ["T1.user.company", "id"] },
268
+ "company.name": { type: "column", path: ["T1.user.company", "name"] },
269
+ "company.foundedAt": { type: "column", path: ["T1.user.company", "foundedAt"] },
270
+ },
271
+ where: [
272
+ {
273
+ type: "eq",
274
+ source: { type: "column", path: ["T1.user", "id"] },
275
+ target: { type: "column", path: ["T1", "userId"] },
276
+ },
277
+ ],
278
+ joins: [
279
+ {
280
+ type: "select",
281
+ as: "T1.user.company",
282
+ from: { database: "TestDb", schema: "TestSchema", name: "Company" },
283
+ isSingle: true,
284
+ where: [
285
+ {
286
+ type: "eq",
287
+ source: { type: "column", path: ["T1.user.company", "id"] },
288
+ target: { type: "column", path: ["T1.user", "companyId"] },
289
+ },
290
+ ],
291
+ },
292
+ ],
293
+ },
294
+ ],
295
+ });
296
+ });
297
+
298
+ it.each(dialects)("[%s] Verify SQL", (dialect) => {
299
+ const builder = createQueryBuilder(dialect);
300
+ expect(builder.build(def)).toMatchSql(expected.joinSingleMultiLevel[dialect]);
301
+ });
302
+ });
303
+
304
+ describe("joinSingle + LATERAL (orderBy + top)", () => {
305
+ const db = createTestDb();
306
+ const def = db
307
+ .user()
308
+ .joinSingle("latestPost", (qr, c) =>
309
+ qr
310
+ .from(Post)
311
+ .where((item) => [expr.eq(item.userId, c.id)])
312
+ .orderBy((item) => item.publishedAt, "DESC")
313
+ .top(1),
314
+ )
315
+ .getSelectQueryDef();
316
+
317
+ it("Verify QueryDef - includes orderBy, top", () => {
318
+ const join = def.joins![0];
319
+ expect(join.orderBy).toEqual([
320
+ [{ type: "column", path: ["T1.latestPost", "publishedAt"] }, "DESC"],
321
+ ]);
322
+ expect(join.top).toBe(1);
323
+ expect(join.isSingle).toBe(true);
324
+ });
325
+
326
+ it.each(dialects)("[%s] Verify SQL", (dialect) => {
327
+ const builder = createQueryBuilder(dialect);
328
+ expect(builder.build(def)).toMatchSql(expected.joinSingleLateral[dialect]);
329
+ });
330
+ });
331
+
332
+ describe("joinSingle + LATERAL (select aggregation)", () => {
333
+ const db = createTestDb();
334
+ const def = db
335
+ .user()
336
+ .joinSingle("postStats", (qr, c) =>
337
+ qr
338
+ .from(Post)
339
+ .where((item) => [expr.eq(item.userId, c.id)])
340
+ .select(() => ({ cnt: expr.count() })),
341
+ )
342
+ .getSelectQueryDef();
343
+
344
+ it("Verify QueryDef - includes select", () => {
345
+ const join = def.joins![0];
346
+ expect(join.select).toBeDefined();
347
+ expect(join.select!["cnt"]).toEqual({ type: "count" });
348
+ });
349
+
350
+ it.each(dialects)("[%s] Verify SQL", (dialect) => {
351
+ const builder = createQueryBuilder(dialect);
352
+ expect(builder.build(def)).toMatchSql(expected.joinSingleLateralAgg[dialect]);
353
+ });
354
+ });
355
+
356
+ it("Combination of join + where", () => {
357
+ const db = createTestDb();
358
+ const def = db
359
+ .user()
360
+ .join("post", (q, c) => q.from(Post).where((item) => [expr.eq(item.userId, c.id)]))
361
+ .where((item) => [expr.eq(item.isActive, true)])
362
+ .getSelectQueryDef();
363
+
364
+ expect(def).toEqual({
365
+ type: "select",
366
+ as: "T1",
367
+ from: { database: "TestDb", schema: "TestSchema", name: "User" },
368
+ select: {
369
+ "id": { type: "column", path: ["T1", "id"] },
370
+ "name": { type: "column", path: ["T1", "name"] },
371
+ "email": { type: "column", path: ["T1", "email"] },
372
+ "age": { type: "column", path: ["T1", "age"] },
373
+ "isActive": { type: "column", path: ["T1", "isActive"] },
374
+ "companyId": { type: "column", path: ["T1", "companyId"] },
375
+ "createdAt": { type: "column", path: ["T1", "createdAt"] },
376
+ "post.id": { type: "column", path: ["T1.post", "id"] },
377
+ "post.userId": { type: "column", path: ["T1.post", "userId"] },
378
+ "post.title": { type: "column", path: ["T1.post", "title"] },
379
+ "post.content": { type: "column", path: ["T1.post", "content"] },
380
+ "post.viewCount": { type: "column", path: ["T1.post", "viewCount"] },
381
+ "post.publishedAt": { type: "column", path: ["T1.post", "publishedAt"] },
382
+ },
383
+ where: [
384
+ {
385
+ type: "eq",
386
+ source: { type: "column", path: ["T1", "isActive"] },
387
+ target: { type: "value", value: true },
388
+ },
389
+ ],
390
+ joins: [
391
+ {
392
+ type: "select",
393
+ as: "T1.post",
394
+ from: { database: "TestDb", schema: "TestSchema", name: "Post" },
395
+ isSingle: false,
396
+ where: [
397
+ {
398
+ type: "eq",
399
+ source: { type: "column", path: ["T1.post", "userId"] },
400
+ target: { type: "column", path: ["T1", "id"] },
401
+ },
402
+ ],
403
+ },
404
+ ],
405
+ });
406
+ });
407
+ });
408
+
409
+ describe("SELECT - INCLUDE", () => {
410
+ it("FK (N:1)", () => {
411
+ const db = createTestDb();
412
+ const def = db
413
+ .post()
414
+ .include((item) => item.user)
415
+ .getSelectQueryDef();
416
+
417
+ expect(def).toEqual({
418
+ type: "select",
419
+ as: "T1",
420
+ from: { database: "TestDb", schema: "TestSchema", name: "Post" },
421
+ select: {
422
+ "id": { type: "column", path: ["T1", "id"] },
423
+ "userId": { type: "column", path: ["T1", "userId"] },
424
+ "title": { type: "column", path: ["T1", "title"] },
425
+ "content": { type: "column", path: ["T1", "content"] },
426
+ "viewCount": { type: "column", path: ["T1", "viewCount"] },
427
+ "publishedAt": { type: "column", path: ["T1", "publishedAt"] },
428
+ "user.id": { type: "column", path: ["T1.user", "id"] },
429
+ "user.name": { type: "column", path: ["T1.user", "name"] },
430
+ "user.email": { type: "column", path: ["T1.user", "email"] },
431
+ "user.age": { type: "column", path: ["T1.user", "age"] },
432
+ "user.isActive": { type: "column", path: ["T1.user", "isActive"] },
433
+ "user.companyId": { type: "column", path: ["T1.user", "companyId"] },
434
+ "user.createdAt": { type: "column", path: ["T1.user", "createdAt"] },
435
+ },
436
+ joins: [
437
+ {
438
+ type: "select",
439
+ as: "T1.user",
440
+ from: { database: "TestDb", schema: "TestSchema", name: "User" },
441
+ isSingle: true,
442
+ where: [
443
+ {
444
+ type: "eq",
445
+ source: { type: "column", path: ["T1.user", "id"] },
446
+ target: { type: "column", path: ["T1", "userId"] },
447
+ },
448
+ ],
449
+ },
450
+ ],
451
+ });
452
+ });
453
+
454
+ it("FKT (1:N)", () => {
455
+ const db = createTestDb();
456
+ const def = db
457
+ .user()
458
+ .include((item) => item.posts)
459
+ .getSelectQueryDef();
460
+
461
+ expect(def).toEqual({
462
+ type: "select",
463
+ as: "T1",
464
+ from: { database: "TestDb", schema: "TestSchema", name: "User" },
465
+ select: {
466
+ "id": { type: "column", path: ["T1", "id"] },
467
+ "name": { type: "column", path: ["T1", "name"] },
468
+ "email": { type: "column", path: ["T1", "email"] },
469
+ "age": { type: "column", path: ["T1", "age"] },
470
+ "isActive": { type: "column", path: ["T1", "isActive"] },
471
+ "companyId": { type: "column", path: ["T1", "companyId"] },
472
+ "createdAt": { type: "column", path: ["T1", "createdAt"] },
473
+ "posts.id": { type: "column", path: ["T1.posts", "id"] },
474
+ "posts.userId": { type: "column", path: ["T1.posts", "userId"] },
475
+ "posts.title": { type: "column", path: ["T1.posts", "title"] },
476
+ "posts.content": { type: "column", path: ["T1.posts", "content"] },
477
+ "posts.viewCount": { type: "column", path: ["T1.posts", "viewCount"] },
478
+ "posts.publishedAt": { type: "column", path: ["T1.posts", "publishedAt"] },
479
+ },
480
+ joins: [
481
+ {
482
+ type: "select",
483
+ as: "T1.posts",
484
+ from: { database: "TestDb", schema: "TestSchema", name: "Post" },
485
+ isSingle: false,
486
+ where: [
487
+ {
488
+ type: "eq",
489
+ source: { type: "column", path: ["T1.posts", "userId"] },
490
+ target: { type: "column", path: ["T1", "id"] },
491
+ },
492
+ ],
493
+ },
494
+ ],
495
+ });
496
+ });
497
+
498
+ it("Multi-level include (FK -> FK)", () => {
499
+ const db = createTestDb();
500
+ const def = db
501
+ .post()
502
+ .include((item) => item.user.company)
503
+ .getSelectQueryDef();
504
+
505
+ expect(def).toEqual({
506
+ type: "select",
507
+ as: "T1",
508
+ from: { database: "TestDb", schema: "TestSchema", name: "Post" },
509
+ select: {
510
+ "id": { type: "column", path: ["T1", "id"] },
511
+ "userId": { type: "column", path: ["T1", "userId"] },
512
+ "title": { type: "column", path: ["T1", "title"] },
513
+ "content": { type: "column", path: ["T1", "content"] },
514
+ "viewCount": { type: "column", path: ["T1", "viewCount"] },
515
+ "publishedAt": { type: "column", path: ["T1", "publishedAt"] },
516
+ "user.id": { type: "column", path: ["T1.user", "id"] },
517
+ "user.name": { type: "column", path: ["T1.user", "name"] },
518
+ "user.email": { type: "column", path: ["T1.user", "email"] },
519
+ "user.age": { type: "column", path: ["T1.user", "age"] },
520
+ "user.isActive": { type: "column", path: ["T1.user", "isActive"] },
521
+ "user.companyId": { type: "column", path: ["T1.user", "companyId"] },
522
+ "user.createdAt": { type: "column", path: ["T1.user", "createdAt"] },
523
+ "user.company.id": { type: "column", path: ["T1.user.company", "id"] },
524
+ "user.company.name": { type: "column", path: ["T1.user.company", "name"] },
525
+ "user.company.foundedAt": { type: "column", path: ["T1.user.company", "foundedAt"] },
526
+ },
527
+ joins: [
528
+ {
529
+ type: "select",
530
+ as: "T1.user",
531
+ from: { database: "TestDb", schema: "TestSchema", name: "User" },
532
+ isSingle: true,
533
+ where: [
534
+ {
535
+ type: "eq",
536
+ source: { type: "column", path: ["T1.user", "id"] },
537
+ target: { type: "column", path: ["T1", "userId"] },
538
+ },
539
+ ],
540
+ },
541
+ {
542
+ type: "select",
543
+ as: "T1.user.company",
544
+ from: { database: "TestDb", schema: "TestSchema", name: "Company" },
545
+ isSingle: true,
546
+ where: [
547
+ {
548
+ type: "eq",
549
+ source: { type: "column", path: ["T1.user.company", "id"] },
550
+ target: { type: "column", path: ["T1.user", "companyId"] },
551
+ },
552
+ ],
553
+ },
554
+ ],
555
+ });
556
+ });
557
+
558
+ it("Multiple include", () => {
559
+ const db = createTestDb();
560
+ const def = db
561
+ .post()
562
+ .include((item) => item.user)
563
+ .include((item) => item.user.company)
564
+ .getSelectQueryDef();
565
+
566
+ // 중복 include는 자동으로 제거됨 (user 1번, company 1번)
567
+ expect(def).toEqual({
568
+ type: "select",
569
+ as: "T1",
570
+ from: { database: "TestDb", schema: "TestSchema", name: "Post" },
571
+ select: {
572
+ "id": { type: "column", path: ["T1", "id"] },
573
+ "userId": { type: "column", path: ["T1", "userId"] },
574
+ "title": { type: "column", path: ["T1", "title"] },
575
+ "content": { type: "column", path: ["T1", "content"] },
576
+ "viewCount": { type: "column", path: ["T1", "viewCount"] },
577
+ "publishedAt": { type: "column", path: ["T1", "publishedAt"] },
578
+ "user.id": { type: "column", path: ["T1.user", "id"] },
579
+ "user.name": { type: "column", path: ["T1.user", "name"] },
580
+ "user.email": { type: "column", path: ["T1.user", "email"] },
581
+ "user.age": { type: "column", path: ["T1.user", "age"] },
582
+ "user.isActive": { type: "column", path: ["T1.user", "isActive"] },
583
+ "user.companyId": { type: "column", path: ["T1.user", "companyId"] },
584
+ "user.createdAt": { type: "column", path: ["T1.user", "createdAt"] },
585
+ "user.company.id": { type: "column", path: ["T1.user.company", "id"] },
586
+ "user.company.name": { type: "column", path: ["T1.user.company", "name"] },
587
+ "user.company.foundedAt": { type: "column", path: ["T1.user.company", "foundedAt"] },
588
+ },
589
+ joins: [
590
+ {
591
+ type: "select",
592
+ as: "T1.user",
593
+ from: { database: "TestDb", schema: "TestSchema", name: "User" },
594
+ isSingle: true,
595
+ where: [
596
+ {
597
+ type: "eq",
598
+ source: { type: "column", path: ["T1.user", "id"] },
599
+ target: { type: "column", path: ["T1", "userId"] },
600
+ },
601
+ ],
602
+ },
603
+ {
604
+ type: "select",
605
+ as: "T1.user.company",
606
+ from: { database: "TestDb", schema: "TestSchema", name: "Company" },
607
+ isSingle: true,
608
+ where: [
609
+ {
610
+ type: "eq",
611
+ source: { type: "column", path: ["T1.user.company", "id"] },
612
+ target: { type: "column", path: ["T1.user", "companyId"] },
613
+ },
614
+ ],
615
+ },
616
+ ],
617
+ });
618
+ });
619
+
620
+ it("Combination of include + select", () => {
621
+ const db = createTestDb();
622
+ const def = db
623
+ .post()
624
+ .include((item) => item.user)
625
+ .select((item) => ({
626
+ title: item.title,
627
+ userName: item.user!.name,
628
+ }))
629
+ .getSelectQueryDef();
630
+
631
+ expect(def).toEqual({
632
+ type: "select",
633
+ as: "T1",
634
+ from: { database: "TestDb", schema: "TestSchema", name: "Post" },
635
+ select: {
636
+ title: { type: "column", path: ["T1", "title"] },
637
+ userName: { type: "column", path: ["T1.user", "name"] },
638
+ },
639
+ joins: [
640
+ {
641
+ type: "select",
642
+ as: "T1.user",
643
+ from: { database: "TestDb", schema: "TestSchema", name: "User" },
644
+ isSingle: true,
645
+ where: [
646
+ {
647
+ type: "eq",
648
+ source: { type: "column", path: ["T1.user", "id"] },
649
+ target: { type: "column", path: ["T1", "userId"] },
650
+ },
651
+ ],
652
+ },
653
+ ],
654
+ });
655
+ });
656
+
657
+ it("Combination of include + where", () => {
658
+ const db = createTestDb();
659
+ const def = db
660
+ .post()
661
+ .include((item) => item.user)
662
+ .where((item) => [expr.eq(item.user!.isActive, true)])
663
+ .getSelectQueryDef();
664
+
665
+ expect(def).toEqual({
666
+ type: "select",
667
+ as: "T1",
668
+ from: { database: "TestDb", schema: "TestSchema", name: "Post" },
669
+ select: {
670
+ "id": { type: "column", path: ["T1", "id"] },
671
+ "userId": { type: "column", path: ["T1", "userId"] },
672
+ "title": { type: "column", path: ["T1", "title"] },
673
+ "content": { type: "column", path: ["T1", "content"] },
674
+ "viewCount": { type: "column", path: ["T1", "viewCount"] },
675
+ "publishedAt": { type: "column", path: ["T1", "publishedAt"] },
676
+ "user.id": { type: "column", path: ["T1.user", "id"] },
677
+ "user.name": { type: "column", path: ["T1.user", "name"] },
678
+ "user.email": { type: "column", path: ["T1.user", "email"] },
679
+ "user.age": { type: "column", path: ["T1.user", "age"] },
680
+ "user.isActive": { type: "column", path: ["T1.user", "isActive"] },
681
+ "user.companyId": { type: "column", path: ["T1.user", "companyId"] },
682
+ "user.createdAt": { type: "column", path: ["T1.user", "createdAt"] },
683
+ },
684
+ where: [
685
+ {
686
+ type: "eq",
687
+ source: { type: "column", path: ["T1.user", "isActive"] },
688
+ target: { type: "value", value: true },
689
+ },
690
+ ],
691
+ joins: [
692
+ {
693
+ type: "select",
694
+ as: "T1.user",
695
+ from: { database: "TestDb", schema: "TestSchema", name: "User" },
696
+ isSingle: true,
697
+ where: [
698
+ {
699
+ type: "eq",
700
+ source: { type: "column", path: ["T1.user", "id"] },
701
+ target: { type: "column", path: ["T1", "userId"] },
702
+ },
703
+ ],
704
+ },
705
+ ],
706
+ });
707
+ });
708
+
709
+ describe("3 depth include (FK -> FKT -> FK)", () => {
710
+ // Post → user(FK) → posts(FKT) → user(FK)
711
+ const db = createTestDb();
712
+ const def = db
713
+ .post()
714
+ .include((item) => item.user.posts.user)
715
+ .getSelectQueryDef();
716
+
717
+ it("Verify QueryDef", () => {
718
+ expect(def).toEqual({
719
+ type: "select",
720
+ as: "T1",
721
+ from: { database: "TestDb", schema: "TestSchema", name: "Post" },
722
+ select: {
723
+ "id": { type: "column", path: ["T1", "id"] },
724
+ "userId": { type: "column", path: ["T1", "userId"] },
725
+ "title": { type: "column", path: ["T1", "title"] },
726
+ "content": { type: "column", path: ["T1", "content"] },
727
+ "viewCount": { type: "column", path: ["T1", "viewCount"] },
728
+ "publishedAt": { type: "column", path: ["T1", "publishedAt"] },
729
+ "user.id": { type: "column", path: ["T1.user", "id"] },
730
+ "user.name": { type: "column", path: ["T1.user", "name"] },
731
+ "user.email": { type: "column", path: ["T1.user", "email"] },
732
+ "user.age": { type: "column", path: ["T1.user", "age"] },
733
+ "user.isActive": { type: "column", path: ["T1.user", "isActive"] },
734
+ "user.companyId": { type: "column", path: ["T1.user", "companyId"] },
735
+ "user.createdAt": { type: "column", path: ["T1.user", "createdAt"] },
736
+ "user.posts.id": { type: "column", path: ["T1.user.posts", "id"] },
737
+ "user.posts.userId": { type: "column", path: ["T1.user.posts", "userId"] },
738
+ "user.posts.title": { type: "column", path: ["T1.user.posts", "title"] },
739
+ "user.posts.content": { type: "column", path: ["T1.user.posts", "content"] },
740
+ "user.posts.viewCount": { type: "column", path: ["T1.user.posts", "viewCount"] },
741
+ "user.posts.publishedAt": { type: "column", path: ["T1.user.posts", "publishedAt"] },
742
+ "user.posts.user.id": { type: "column", path: ["T1.user.posts.user", "id"] },
743
+ "user.posts.user.name": { type: "column", path: ["T1.user.posts.user", "name"] },
744
+ "user.posts.user.email": { type: "column", path: ["T1.user.posts.user", "email"] },
745
+ "user.posts.user.age": { type: "column", path: ["T1.user.posts.user", "age"] },
746
+ "user.posts.user.isActive": { type: "column", path: ["T1.user.posts.user", "isActive"] },
747
+ "user.posts.user.companyId": {
748
+ type: "column",
749
+ path: ["T1.user.posts.user", "companyId"],
750
+ },
751
+ "user.posts.user.createdAt": {
752
+ type: "column",
753
+ path: ["T1.user.posts.user", "createdAt"],
754
+ },
755
+ },
756
+ joins: [
757
+ {
758
+ type: "select",
759
+ as: "T1.user",
760
+ from: { database: "TestDb", schema: "TestSchema", name: "User" },
761
+ isSingle: true,
762
+ where: [
763
+ {
764
+ type: "eq",
765
+ source: { type: "column", path: ["T1.user", "id"] },
766
+ target: { type: "column", path: ["T1", "userId"] },
767
+ },
768
+ ],
769
+ },
770
+ {
771
+ type: "select",
772
+ as: "T1.user.posts",
773
+ from: { database: "TestDb", schema: "TestSchema", name: "Post" },
774
+ isSingle: false,
775
+ where: [
776
+ {
777
+ type: "eq",
778
+ source: { type: "column", path: ["T1.user.posts", "userId"] },
779
+ target: { type: "column", path: ["T1.user", "id"] },
780
+ },
781
+ ],
782
+ },
783
+ {
784
+ type: "select",
785
+ as: "T1.user.posts.user",
786
+ from: { database: "TestDb", schema: "TestSchema", name: "User" },
787
+ isSingle: true,
788
+ where: [
789
+ {
790
+ type: "eq",
791
+ source: { type: "column", path: ["T1.user.posts.user", "id"] },
792
+ target: { type: "column", path: ["T1.user.posts", "userId"] },
793
+ },
794
+ ],
795
+ },
796
+ ],
797
+ });
798
+ });
799
+
800
+ it.each(dialects)("[%s] Verify SQL", (dialect) => {
801
+ const builder = createQueryBuilder(dialect);
802
+ expect(builder.build(def)).toMatchSql(expected.include3Depth[dialect]);
803
+ });
804
+ });
805
+ });