@simplysm/orm-common 13.0.99 → 14.0.1

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 (238) hide show
  1. package/dist/create-db-context.d.ts +10 -10
  2. package/dist/create-db-context.js +312 -276
  3. package/dist/create-db-context.js.map +1 -6
  4. package/dist/ddl/column-ddl.d.ts +4 -4
  5. package/dist/ddl/column-ddl.js +41 -35
  6. package/dist/ddl/column-ddl.js.map +1 -6
  7. package/dist/ddl/initialize.d.ts +17 -17
  8. package/dist/ddl/initialize.js +200 -142
  9. package/dist/ddl/initialize.js.map +1 -6
  10. package/dist/ddl/relation-ddl.d.ts +6 -6
  11. package/dist/ddl/relation-ddl.js +55 -48
  12. package/dist/ddl/relation-ddl.js.map +1 -6
  13. package/dist/ddl/schema-ddl.d.ts +4 -4
  14. package/dist/ddl/schema-ddl.js +21 -15
  15. package/dist/ddl/schema-ddl.js.map +1 -6
  16. package/dist/ddl/table-ddl.d.ts +20 -20
  17. package/dist/ddl/table-ddl.js +139 -93
  18. package/dist/ddl/table-ddl.js.map +1 -6
  19. package/dist/define-db-context.js +10 -13
  20. package/dist/define-db-context.js.map +1 -6
  21. package/dist/errors/db-transaction-error.d.ts +15 -15
  22. package/dist/errors/db-transaction-error.d.ts.map +1 -1
  23. package/dist/errors/db-transaction-error.js +53 -19
  24. package/dist/errors/db-transaction-error.js.map +1 -6
  25. package/dist/exec/executable.d.ts +23 -23
  26. package/dist/exec/executable.js +94 -40
  27. package/dist/exec/executable.js.map +1 -6
  28. package/dist/exec/queryable.d.ts +97 -97
  29. package/dist/exec/queryable.js +1310 -1204
  30. package/dist/exec/queryable.js.map +1 -6
  31. package/dist/exec/search-parser.d.ts +31 -31
  32. package/dist/exec/search-parser.d.ts.map +1 -1
  33. package/dist/exec/search-parser.js +158 -59
  34. package/dist/exec/search-parser.js.map +1 -6
  35. package/dist/expr/expr-unit.d.ts +4 -4
  36. package/dist/expr/expr-unit.js +24 -18
  37. package/dist/expr/expr-unit.js.map +1 -6
  38. package/dist/expr/expr.d.ts +6 -6
  39. package/dist/expr/expr.js +1872 -1844
  40. package/dist/expr/expr.js.map +1 -6
  41. package/dist/index.js +23 -1
  42. package/dist/index.js.map +1 -6
  43. package/dist/models/system-migration.js +7 -7
  44. package/dist/models/system-migration.js.map +1 -6
  45. package/dist/query-builder/base/expr-renderer-base.d.ts +10 -10
  46. package/dist/query-builder/base/expr-renderer-base.js +27 -21
  47. package/dist/query-builder/base/expr-renderer-base.js.map +1 -6
  48. package/dist/query-builder/base/query-builder-base.d.ts +21 -21
  49. package/dist/query-builder/base/query-builder-base.d.ts.map +1 -1
  50. package/dist/query-builder/base/query-builder-base.js +90 -80
  51. package/dist/query-builder/base/query-builder-base.js.map +1 -6
  52. package/dist/query-builder/mssql/mssql-expr-renderer.d.ts +4 -4
  53. package/dist/query-builder/mssql/mssql-expr-renderer.d.ts.map +1 -1
  54. package/dist/query-builder/mssql/mssql-expr-renderer.js +447 -420
  55. package/dist/query-builder/mssql/mssql-expr-renderer.js.map +1 -6
  56. package/dist/query-builder/mssql/mssql-query-builder.js +483 -443
  57. package/dist/query-builder/mssql/mssql-query-builder.js.map +1 -6
  58. package/dist/query-builder/mysql/mysql-expr-renderer.d.ts +4 -4
  59. package/dist/query-builder/mysql/mysql-expr-renderer.d.ts.map +1 -1
  60. package/dist/query-builder/mysql/mysql-expr-renderer.js +451 -419
  61. package/dist/query-builder/mysql/mysql-expr-renderer.js.map +1 -6
  62. package/dist/query-builder/mysql/mysql-query-builder.js +570 -479
  63. package/dist/query-builder/mysql/mysql-query-builder.js.map +1 -6
  64. package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts +4 -4
  65. package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts.map +1 -1
  66. package/dist/query-builder/postgresql/postgresql-expr-renderer.js +449 -422
  67. package/dist/query-builder/postgresql/postgresql-expr-renderer.js.map +1 -6
  68. package/dist/query-builder/postgresql/postgresql-query-builder.js +511 -460
  69. package/dist/query-builder/postgresql/postgresql-query-builder.js.map +1 -6
  70. package/dist/query-builder/query-builder.d.ts +1 -1
  71. package/dist/query-builder/query-builder.js +13 -13
  72. package/dist/query-builder/query-builder.js.map +1 -6
  73. package/dist/schema/factory/column-builder.d.ts +84 -84
  74. package/dist/schema/factory/column-builder.js +248 -185
  75. package/dist/schema/factory/column-builder.js.map +1 -6
  76. package/dist/schema/factory/index-builder.d.ts +38 -38
  77. package/dist/schema/factory/index-builder.js +144 -85
  78. package/dist/schema/factory/index-builder.js.map +1 -6
  79. package/dist/schema/factory/relation-builder.d.ts +91 -91
  80. package/dist/schema/factory/relation-builder.d.ts.map +1 -1
  81. package/dist/schema/factory/relation-builder.js +274 -136
  82. package/dist/schema/factory/relation-builder.js.map +1 -6
  83. package/dist/schema/procedure-builder.d.ts +51 -51
  84. package/dist/schema/procedure-builder.d.ts.map +1 -1
  85. package/dist/schema/procedure-builder.js +205 -131
  86. package/dist/schema/procedure-builder.js.map +1 -6
  87. package/dist/schema/table-builder.d.ts +55 -55
  88. package/dist/schema/table-builder.d.ts.map +1 -1
  89. package/dist/schema/table-builder.js +274 -205
  90. package/dist/schema/table-builder.js.map +1 -6
  91. package/dist/schema/view-builder.d.ts +44 -44
  92. package/dist/schema/view-builder.d.ts.map +1 -1
  93. package/dist/schema/view-builder.js +189 -116
  94. package/dist/schema/view-builder.js.map +1 -6
  95. package/dist/types/column.js +60 -30
  96. package/dist/types/column.js.map +1 -6
  97. package/dist/types/db-context-def.d.ts +9 -9
  98. package/dist/types/db-context-def.js +2 -1
  99. package/dist/types/db-context-def.js.map +1 -6
  100. package/dist/types/db.d.ts +47 -47
  101. package/dist/types/db.js +15 -5
  102. package/dist/types/db.js.map +1 -6
  103. package/dist/types/expr.d.ts +81 -81
  104. package/dist/types/expr.d.ts.map +1 -1
  105. package/dist/types/expr.js +3 -1
  106. package/dist/types/expr.js.map +1 -6
  107. package/dist/types/query-def.d.ts +46 -46
  108. package/dist/types/query-def.d.ts.map +1 -1
  109. package/dist/types/query-def.js +31 -24
  110. package/dist/types/query-def.js.map +1 -6
  111. package/dist/utils/result-parser.js +362 -221
  112. package/dist/utils/result-parser.js.map +1 -6
  113. package/package.json +5 -7
  114. package/src/create-db-context.ts +31 -31
  115. package/src/ddl/column-ddl.ts +4 -4
  116. package/src/ddl/initialize.ts +38 -38
  117. package/src/ddl/relation-ddl.ts +6 -6
  118. package/src/ddl/schema-ddl.ts +4 -4
  119. package/src/ddl/table-ddl.ts +24 -24
  120. package/src/errors/db-transaction-error.ts +13 -13
  121. package/src/exec/executable.ts +25 -25
  122. package/src/exec/queryable.ts +134 -134
  123. package/src/exec/search-parser.ts +50 -50
  124. package/src/expr/expr-unit.ts +4 -4
  125. package/src/expr/expr.ts +13 -13
  126. package/src/index.ts +8 -8
  127. package/src/models/system-migration.ts +1 -1
  128. package/src/query-builder/base/expr-renderer-base.ts +21 -21
  129. package/src/query-builder/base/query-builder-base.ts +33 -33
  130. package/src/query-builder/mssql/mssql-expr-renderer.ts +11 -11
  131. package/src/query-builder/mssql/mssql-query-builder.ts +11 -11
  132. package/src/query-builder/mysql/mysql-expr-renderer.ts +15 -15
  133. package/src/query-builder/mysql/mysql-query-builder.ts +3 -3
  134. package/src/query-builder/postgresql/postgresql-expr-renderer.ts +9 -9
  135. package/src/query-builder/postgresql/postgresql-query-builder.ts +7 -7
  136. package/src/query-builder/query-builder.ts +1 -1
  137. package/src/schema/factory/column-builder.ts +86 -86
  138. package/src/schema/factory/index-builder.ts +38 -38
  139. package/src/schema/factory/relation-builder.ts +93 -93
  140. package/src/schema/procedure-builder.ts +52 -52
  141. package/src/schema/table-builder.ts +56 -56
  142. package/src/schema/view-builder.ts +45 -45
  143. package/src/types/column.ts +1 -1
  144. package/src/types/db-context-def.ts +15 -15
  145. package/src/types/db.ts +50 -50
  146. package/src/types/expr.ts +103 -103
  147. package/src/types/query-def.ts +50 -50
  148. package/src/utils/result-parser.ts +39 -39
  149. package/README.md +0 -192
  150. package/docs/core.md +0 -234
  151. package/docs/expression.md +0 -234
  152. package/docs/query-builder.md +0 -93
  153. package/docs/queryable.md +0 -198
  154. package/docs/schema-builders.md +0 -463
  155. package/docs/types.md +0 -445
  156. package/docs/utilities.md +0 -27
  157. package/tests/db-context/create-db-context.spec.ts +0 -193
  158. package/tests/db-context/define-db-context.spec.ts +0 -17
  159. package/tests/ddl/basic.expected.ts +0 -341
  160. package/tests/ddl/basic.spec.ts +0 -557
  161. package/tests/ddl/column-builder.expected.ts +0 -310
  162. package/tests/ddl/column-builder.spec.ts +0 -525
  163. package/tests/ddl/index-builder.expected.ts +0 -38
  164. package/tests/ddl/index-builder.spec.ts +0 -148
  165. package/tests/ddl/procedure-builder.expected.ts +0 -52
  166. package/tests/ddl/procedure-builder.spec.ts +0 -128
  167. package/tests/ddl/relation-builder.expected.ts +0 -36
  168. package/tests/ddl/relation-builder.spec.ts +0 -171
  169. package/tests/ddl/table-builder.expected.ts +0 -113
  170. package/tests/ddl/table-builder.spec.ts +0 -399
  171. package/tests/ddl/view-builder.expected.ts +0 -38
  172. package/tests/ddl/view-builder.spec.ts +0 -116
  173. package/tests/dml/delete.expected.ts +0 -96
  174. package/tests/dml/delete.spec.ts +0 -127
  175. package/tests/dml/insert.expected.ts +0 -192
  176. package/tests/dml/insert.spec.ts +0 -210
  177. package/tests/dml/update.expected.ts +0 -176
  178. package/tests/dml/update.spec.ts +0 -222
  179. package/tests/dml/upsert.expected.ts +0 -215
  180. package/tests/dml/upsert.spec.ts +0 -190
  181. package/tests/errors/queryable-errors.spec.ts +0 -126
  182. package/tests/escape.spec.ts +0 -59
  183. package/tests/examples/pivot.expected.ts +0 -211
  184. package/tests/examples/pivot.spec.ts +0 -200
  185. package/tests/examples/sampling.expected.ts +0 -69
  186. package/tests/examples/sampling.spec.ts +0 -42
  187. package/tests/examples/unpivot.expected.ts +0 -120
  188. package/tests/examples/unpivot.spec.ts +0 -161
  189. package/tests/exec/search-parser.spec.ts +0 -267
  190. package/tests/executable/basic.expected.ts +0 -18
  191. package/tests/executable/basic.spec.ts +0 -54
  192. package/tests/expr/comparison.expected.ts +0 -282
  193. package/tests/expr/comparison.spec.ts +0 -334
  194. package/tests/expr/conditional.expected.ts +0 -134
  195. package/tests/expr/conditional.spec.ts +0 -249
  196. package/tests/expr/date.expected.ts +0 -332
  197. package/tests/expr/date.spec.ts +0 -459
  198. package/tests/expr/math.expected.ts +0 -62
  199. package/tests/expr/math.spec.ts +0 -59
  200. package/tests/expr/string.expected.ts +0 -218
  201. package/tests/expr/string.spec.ts +0 -300
  202. package/tests/expr/utility.expected.ts +0 -147
  203. package/tests/expr/utility.spec.ts +0 -155
  204. package/tests/select/basic.expected.ts +0 -322
  205. package/tests/select/basic.spec.ts +0 -433
  206. package/tests/select/filter.expected.ts +0 -357
  207. package/tests/select/filter.spec.ts +0 -954
  208. package/tests/select/group.expected.ts +0 -169
  209. package/tests/select/group.spec.ts +0 -159
  210. package/tests/select/join.expected.ts +0 -582
  211. package/tests/select/join.spec.ts +0 -692
  212. package/tests/select/order.expected.ts +0 -150
  213. package/tests/select/order.spec.ts +0 -140
  214. package/tests/select/recursive-cte.expected.ts +0 -244
  215. package/tests/select/recursive-cte.spec.ts +0 -514
  216. package/tests/select/result-meta.spec.ts +0 -270
  217. package/tests/select/subquery.expected.ts +0 -363
  218. package/tests/select/subquery.spec.ts +0 -441
  219. package/tests/select/view.expected.ts +0 -155
  220. package/tests/select/view.spec.ts +0 -235
  221. package/tests/select/window.expected.ts +0 -345
  222. package/tests/select/window.spec.ts +0 -433
  223. package/tests/setup/MockExecutor.ts +0 -18
  224. package/tests/setup/TestDbContext.ts +0 -59
  225. package/tests/setup/models/Company.ts +0 -13
  226. package/tests/setup/models/Employee.ts +0 -10
  227. package/tests/setup/models/MonthlySales.ts +0 -11
  228. package/tests/setup/models/Post.ts +0 -16
  229. package/tests/setup/models/Sales.ts +0 -10
  230. package/tests/setup/models/User.ts +0 -19
  231. package/tests/setup/procedure/GetAllUsers.ts +0 -9
  232. package/tests/setup/procedure/GetUserById.ts +0 -12
  233. package/tests/setup/test-utils.ts +0 -72
  234. package/tests/setup/views/ActiveUsers.ts +0 -8
  235. package/tests/setup/views/UserSummary.ts +0 -11
  236. package/tests/types/nullable-queryable-record.spec.ts +0 -97
  237. package/tests/utils/result-parser-perf.spec.ts +0 -143
  238. package/tests/utils/result-parser.spec.ts +0 -667
@@ -1,1270 +1,1376 @@
1
1
  import { TableBuilder } from "../schema/table-builder.js";
2
2
  import { ViewBuilder } from "../schema/view-builder.js";
3
- import {
4
- } from "../schema/factory/column-builder.js";
3
+ import {} from "../schema/factory/column-builder.js";
5
4
  import { ExprUnit } from "../expr/expr-unit.js";
6
5
  import { ArgumentError, obj } from "@simplysm/core-common";
7
- import {
8
- ForeignKeyBuilder,
9
- ForeignKeyTargetBuilder,
10
- RelationKeyBuilder,
11
- RelationKeyTargetBuilder
12
- } from "../schema/factory/relation-builder.js";
6
+ import { ForeignKeyBuilder, ForeignKeyTargetBuilder, RelationKeyBuilder, RelationKeyTargetBuilder, } from "../schema/factory/relation-builder.js";
13
7
  import { parseSearchQuery } from "./search-parser.js";
14
8
  import { expr } from "../expr/expr.js";
9
+ /**
10
+ * JOIN query builder
11
+ *
12
+ * join/joinSingle 메서드 내부에서 조인할 table을 지정하는 데 사용
13
+ */
15
14
  class JoinQueryable {
16
- constructor(_db, _joinAlias) {
17
- this._db = _db;
18
- this._joinAlias = _joinAlias;
19
- }
20
- /**
21
- * Specify the table to join
22
- *
23
- * @param table - Table to join
24
- * @returns Joined Queryable
25
- */
26
- from(table) {
27
- return queryable(this._db, table, this._joinAlias)();
28
- }
29
- /**
30
- * Directly specify columns in join result
31
- *
32
- * @param columns - Custom column definition
33
- * @returns Queryable with custom columns applied
34
- */
35
- select(columns) {
36
- return new Queryable({
37
- db: this._db,
38
- as: this._joinAlias,
39
- columns,
40
- isCustomColumns: true
41
- });
42
- }
43
- /**
44
- * Combine multiple Queryables with UNION
45
- *
46
- * @param queries - Array of Queryables to UNION (minimum 2)
47
- * @returns UNION-ed Queryable
48
- * @throws If less than 2 queryables are passed
49
- */
50
- union(...queries) {
51
- if (queries.length < 2) {
52
- throw new ArgumentError("union requires at least 2 queryables.", {
53
- provided: queries.length,
54
- minimum: 2
55
- });
15
+ _db;
16
+ _joinAlias;
17
+ constructor(_db, _joinAlias) {
18
+ this._db = _db;
19
+ this._joinAlias = _joinAlias;
20
+ }
21
+ /**
22
+ * 조인할 table 지정
23
+ *
24
+ * @param table - 조인할 table
25
+ * @returns 조인된 Queryable
26
+ */
27
+ from(table) {
28
+ return queryable(this._db, table, this._joinAlias)();
29
+ }
30
+ /**
31
+ * 조인 결과의 column을 직접 지정
32
+ *
33
+ * @param columns - 커스텀 column 정의
34
+ * @returns 커스텀 column이 적용된 Queryable
35
+ */
36
+ select(columns) {
37
+ return new Queryable({
38
+ db: this._db,
39
+ as: this._joinAlias,
40
+ columns,
41
+ isCustomColumns: true,
42
+ });
43
+ }
44
+ /**
45
+ * 여러 Queryable을 UNION으로 결합
46
+ *
47
+ * @param queries - UNION할 Queryable 배열 (최소 2개)
48
+ * @returns UNION된 Queryable
49
+ * @throws 2개 미만의 queryable이 전달되면 에러
50
+ */
51
+ union(...queries) {
52
+ if (queries.length < 2) {
53
+ throw new ArgumentError("union은 최소 2개의 queryable이 필요합니다.", {
54
+ provided: queries.length,
55
+ minimum: 2,
56
+ });
57
+ }
58
+ const first = queries[0];
59
+ return new Queryable({
60
+ db: first.meta.db,
61
+ from: queries, // Queryable[] 배열로 저장
62
+ as: this._joinAlias,
63
+ columns: transformColumnsAlias(first.meta.columns, this._joinAlias, ""),
64
+ });
56
65
  }
57
- const first = queries[0];
58
- return new Queryable({
59
- db: first.meta.db,
60
- from: queries,
61
- // stored as Queryable[] array
62
- as: this._joinAlias,
63
- columns: transformColumnsAlias(first.meta.columns, this._joinAlias, "")
64
- });
65
- }
66
66
  }
67
+ /**
68
+ * 재귀 CTE (Common Table Expression) builder
69
+ *
70
+ * recursive() 메서드 내부에서 사용되며, 재귀 쿼리의 본문을 정의한다
71
+ *
72
+ * @template TBaseData - Base query data type
73
+ */
67
74
  class RecursiveQueryable {
68
- constructor(_baseQr, _cteName) {
69
- this._baseQr = _baseQr;
70
- this._cteName = _cteName;
71
- }
72
- /**
73
- * specify the target table for recursive query
74
- *
75
- * @param table - Target table to recurse
76
- * @returns Queryable with self property added (for self-reference)
77
- */
78
- from(table) {
79
- const selfAlias = `${this._cteName}.self`;
80
- return queryable(this._baseQr.meta.db, table, this._cteName)().join(
81
- "self",
82
- () => new Queryable({
83
- db: this._baseQr.meta.db,
84
- from: this._cteName,
85
- as: selfAlias,
86
- columns: transformColumnsAlias(this._baseQr.meta.columns, selfAlias, ""),
87
- isCustomColumns: false
88
- })
89
- );
90
- }
91
- /**
92
- * Directly specify columns in recursive query
93
- *
94
- * @param columns - Custom column definition
95
- * @returns Queryable with self property added
96
- */
97
- select(columns) {
98
- const selfAlias = `${this._cteName}.self`;
99
- return new Queryable({
100
- db: this._baseQr.meta.db,
101
- as: this._cteName,
102
- columns,
103
- isCustomColumns: true
104
- }).join(
105
- "self",
106
- () => new Queryable({
107
- db: this._baseQr.meta.db,
108
- from: this._cteName,
109
- as: selfAlias,
110
- columns: transformColumnsAlias(this._baseQr.meta.columns, selfAlias, ""),
111
- isCustomColumns: false
112
- })
113
- );
114
- }
115
- /**
116
- * Combine multiple Queryables with UNION (for recursive query)
117
- *
118
- * @param queries - Array of Queryables to UNION (minimum 2)
119
- * @returns UNION Queryable with self property added
120
- * @throws If less than 2 queryables are passed
121
- */
122
- union(...queries) {
123
- if (queries.length < 2) {
124
- throw new ArgumentError("union requires at least 2 queryables.", {
125
- provided: queries.length,
126
- minimum: 2
127
- });
75
+ _baseQr;
76
+ _cteName;
77
+ constructor(_baseQr, _cteName) {
78
+ this._baseQr = _baseQr;
79
+ this._cteName = _cteName;
128
80
  }
129
- const first = queries[0];
130
- const selfAlias = `${this._cteName}.self`;
131
- return new Queryable({
132
- db: first.meta.db,
133
- from: queries,
134
- // stored as Queryable[] array
135
- as: this._cteName,
136
- columns: transformColumnsAlias(first.meta.columns, this._cteName, "")
137
- }).join(
138
- "self",
139
- () => new Queryable({
140
- db: this._baseQr.meta.db,
141
- from: this._cteName,
142
- as: selfAlias,
143
- columns: transformColumnsAlias(this._baseQr.meta.columns, selfAlias, ""),
144
- isCustomColumns: false
145
- })
146
- );
147
- }
148
- }
149
- class Queryable {
150
- constructor(meta) {
151
- this.meta = meta;
152
- }
153
- //#region ========== option - SELECT / DISTINCT / LOCK ==========
154
- /**
155
- * Specify columns to SELECT.
156
- *
157
- * @param fn - Column mapping function. Receives original columns and returns new column structure
158
- * @returns Queryable with new column structure applied
159
- *
160
- * @example
161
- * ```typescript
162
- * db.user().select((u) => ({
163
- * userName: u.name,
164
- * userEmail: u.email,
165
- * }))
166
- * ```
167
- */
168
- select(fn) {
169
- if (Array.isArray(this.meta.from)) {
170
- const newFroms = this.meta.from.map((from) => from.select(fn));
171
- return new Queryable({
172
- ...this.meta,
173
- from: newFroms,
174
- columns: transformColumnsAlias(newFroms[0].meta.columns, this.meta.as, "")
175
- });
81
+ /**
82
+ * 재귀 query의 대상 table 지정
83
+ *
84
+ * @param table - 재귀할 대상 table
85
+ * @returns self 속성이 추가된 Queryable (자기 참조용)
86
+ */
87
+ from(table) {
88
+ const selfAlias = `${this._cteName}.self`;
89
+ return queryable(this._baseQr.meta.db, table, this._cteName)().join("self", () => new Queryable({
90
+ db: this._baseQr.meta.db,
91
+ from: this._cteName,
92
+ as: selfAlias,
93
+ columns: transformColumnsAlias(this._baseQr.meta.columns, selfAlias, ""),
94
+ isCustomColumns: false,
95
+ }));
176
96
  }
177
- const newColumns = fn(this.meta.columns);
178
- return new Queryable({
179
- ...this.meta,
180
- columns: newColumns,
181
- isCustomColumns: true
182
- });
183
- }
184
- /**
185
- * Apply DISTINCT option to remove duplicate rows
186
- *
187
- * @returns Queryable with DISTINCT applied
188
- *
189
- * @example
190
- * ```typescript
191
- * db.user()
192
- * .select((u) => ({ name: u.name }))
193
- * .distinct()
194
- * ```
195
- */
196
- distinct() {
197
- if (Array.isArray(this.meta.from)) {
198
- const newFroms = this.meta.from.map((from) => from.distinct());
199
- return new Queryable({
200
- ...this.meta,
201
- from: newFroms
202
- });
97
+ /**
98
+ * 재귀 query의 column을 직접 지정
99
+ *
100
+ * @param columns - 커스텀 column 정의
101
+ * @returns self 속성이 추가된 Queryable
102
+ */
103
+ select(columns) {
104
+ const selfAlias = `${this._cteName}.self`;
105
+ return new Queryable({
106
+ db: this._baseQr.meta.db,
107
+ as: this._cteName,
108
+ columns,
109
+ isCustomColumns: true,
110
+ }).join("self", () => new Queryable({
111
+ db: this._baseQr.meta.db,
112
+ from: this._cteName,
113
+ as: selfAlias,
114
+ columns: transformColumnsAlias(this._baseQr.meta.columns, selfAlias, ""),
115
+ isCustomColumns: false,
116
+ }));
203
117
  }
204
- return new Queryable({
205
- ...this.meta,
206
- distinct: true
207
- });
208
- }
209
- /**
210
- * Apply row lock (FOR UPDATE)
211
- *
212
- * Acquire exclusive lock on selected rows within transaction
213
- *
214
- * @returns Queryable with lock applied
215
- *
216
- * @example
217
- * ```typescript
218
- * await db.connect(async () => {
219
- * const user = await db.user()
220
- * .where((u) => [expr.eq(u.id, 1)])
221
- * .lock()
222
- * .single();
223
- * });
224
- * ```
225
- */
226
- lock() {
227
- if (Array.isArray(this.meta.from)) {
228
- const newFroms = this.meta.from.map((from) => from.lock());
229
- return new Queryable({
230
- ...this.meta,
231
- from: newFroms
232
- });
118
+ /**
119
+ * 여러 Queryable을 UNION으로 결합 (재귀 query용)
120
+ *
121
+ * @param queries - UNION할 Queryable 배열 (최소 2개)
122
+ * @returns self 속성이 추가된 UNION Queryable
123
+ * @throws 2개 미만의 queryable이 전달되면 에러
124
+ */
125
+ union(...queries) {
126
+ if (queries.length < 2) {
127
+ throw new ArgumentError("union은 최소 2개의 queryable이 필요합니다.", {
128
+ provided: queries.length,
129
+ minimum: 2,
130
+ });
131
+ }
132
+ const first = queries[0];
133
+ const selfAlias = `${this._cteName}.self`;
134
+ return new Queryable({
135
+ db: first.meta.db,
136
+ from: queries, // Queryable[] 배열로 저장
137
+ as: this._cteName,
138
+ columns: transformColumnsAlias(first.meta.columns, this._cteName, ""),
139
+ }).join("self", () => new Queryable({
140
+ db: this._baseQr.meta.db,
141
+ from: this._cteName,
142
+ as: selfAlias,
143
+ columns: transformColumnsAlias(this._baseQr.meta.columns, selfAlias, ""),
144
+ isCustomColumns: false,
145
+ }));
233
146
  }
234
- return new Queryable({
235
- ...this.meta,
236
- lock: true
237
- });
238
- }
239
- //#endregion
240
- //#region ========== restrict - TOP / LIMIT ==========
241
- /**
242
- * Select only top N rows (can be used without ORDER BY)
243
- *
244
- * @param count - number of rows to select
245
- * @returns Queryable with TOP applied
246
- *
247
- * @example
248
- * ```typescript
249
- * // Latest 10 users
250
- * db.user()
251
- * .orderBy((u) => u.createdAt, "DESC")
252
- * .top(10)
253
- * ```
254
- */
255
- top(count) {
256
- if (Array.isArray(this.meta.from)) {
257
- const newFroms = this.meta.from.map((from) => from.top(count));
258
- return new Queryable({
259
- ...this.meta,
260
- from: newFroms
261
- });
147
+ }
148
+ /**
149
+ * Query builder 클래스
150
+ *
151
+ * 체이닝 방식으로 table/view에 대한 SELECT, INSERT, UPDATE, DELETE query를 구성
152
+ *
153
+ * @template TData - Query 결과의 데이터 타입
154
+ * @template TFrom - 소스 table (CUD 연산에 필요)
155
+ *
156
+ * @example
157
+ * ```typescript
158
+ * // Basic query
159
+ * const users = await db.user()
160
+ * .where((u) => [expr.eq(u.isActive, true)])
161
+ * .orderBy((u) => u.name)
162
+ * .execute();
163
+ *
164
+ * // JOIN query
165
+ * const posts = await db.post()
166
+ * .include((p) => p.user)
167
+ * .execute();
168
+ *
169
+ * // INSERT
170
+ * await db.user().insert([{ name: "Gildong Hong", email: "test@test.com" }]);
171
+ * ```
172
+ */
173
+ export class Queryable {
174
+ meta;
175
+ constructor(meta) {
176
+ this.meta = meta;
262
177
  }
263
- return new Queryable({
264
- ...this.meta,
265
- top: count
266
- });
267
- }
268
- /**
269
- * Set LIMIT/OFFSET for pagination.
270
- * Must call orderBy() first.
271
- *
272
- * @param skip - number of rows to skip (OFFSET)
273
- * @param take - number of rows to fetch (LIMIT)
274
- * @returns Queryable with pagination applied
275
- * @throws Error if no ORDER BY clause
276
- *
277
- * @example
278
- * ```typescript
279
- * db.user
280
- * .orderBy((u) => u.createdAt)
281
- * .limit(0, 20) // first 20
282
- * ```
283
- */
284
- limit(skip, take) {
285
- if (Array.isArray(this.meta.from)) {
286
- const newFroms = this.meta.from.map((from) => from.limit(skip, take));
287
- return new Queryable({
288
- ...this.meta,
289
- from: newFroms
290
- });
178
+ //#region ========== option - SELECT / DISTINCT / LOCK ==========
179
+ /**
180
+ * SELECT할 column 지정.
181
+ *
182
+ * @param fn - Column 매핑 함수. 원본 column을 받아 새 column 구조를 반환
183
+ * @returns 새 column 구조가 적용된 Queryable
184
+ *
185
+ * @example
186
+ * ```typescript
187
+ * db.user().select((u) => ({
188
+ * userName: u.name,
189
+ * userEmail: u.email,
190
+ * }))
191
+ * ```
192
+ */
193
+ select(fn) {
194
+ if (Array.isArray(this.meta.from)) {
195
+ const newFroms = this.meta.from.map((from) => from.select(fn));
196
+ return new Queryable({
197
+ ...this.meta,
198
+ from: newFroms,
199
+ columns: transformColumnsAlias(newFroms[0].meta.columns, this.meta.as, ""),
200
+ });
201
+ }
202
+ const newColumns = fn(this.meta.columns);
203
+ return new Queryable({
204
+ ...this.meta,
205
+ columns: newColumns,
206
+ isCustomColumns: true,
207
+ });
291
208
  }
292
- if (!this.meta.orderBy) {
293
- throw new ArgumentError("limit() requires ORDER BY clause.", {
294
- method: "limit",
295
- required: "orderBy"
296
- });
209
+ /**
210
+ * 중복 제거를 위한 DISTINCT 옵션 적용
211
+ *
212
+ * @returns DISTINCT가 적용된 Queryable
213
+ *
214
+ * @example
215
+ * ```typescript
216
+ * db.user()
217
+ * .select((u) => ({ name: u.name }))
218
+ * .distinct()
219
+ * ```
220
+ */
221
+ distinct() {
222
+ if (Array.isArray(this.meta.from)) {
223
+ const newFroms = this.meta.from.map((from) => from.distinct());
224
+ return new Queryable({
225
+ ...this.meta,
226
+ from: newFroms,
227
+ });
228
+ }
229
+ return new Queryable({
230
+ ...this.meta,
231
+ distinct: true,
232
+ });
297
233
  }
298
- return new Queryable({
299
- ...this.meta,
300
- limit: [skip, take]
301
- });
302
- }
303
- //#endregion
304
- //#region ========== sorting - ORDER BY ==========
305
- /**
306
- * Add sorting condition. Multiple calls apply in order.
307
- *
308
- * @param fn - function returning columns to sort by
309
- * @param orderBy - Sort direction (ASC/DESC). Default: ASC
310
- * @returns Queryable with sorting conditions added
311
- *
312
- * @example
313
- * ```typescript
314
- * db.user
315
- * .orderBy((u) => u.name) // name ASC
316
- * .orderBy((u) => u.age, "DESC") // age DESC
317
- * ```
318
- */
319
- orderBy(fn, orderBy) {
320
- if (Array.isArray(this.meta.from)) {
321
- const newFroms = this.meta.from.map((from) => from.orderBy(fn, orderBy));
322
- return new Queryable({
323
- ...this.meta,
324
- from: newFroms
325
- });
234
+ /**
235
+ * 행 잠금 적용 (FOR UPDATE)
236
+ *
237
+ * 트랜잭션 내에서 선택된 행에 대한 배타적 잠금 획득
238
+ *
239
+ * @returns 잠금이 적용된 Queryable
240
+ *
241
+ * @example
242
+ * ```typescript
243
+ * await db.connect(async () => {
244
+ * const user = await db.user()
245
+ * .where((u) => [expr.eq(u.id, 1)])
246
+ * .lock()
247
+ * .single();
248
+ * });
249
+ * ```
250
+ */
251
+ lock() {
252
+ if (Array.isArray(this.meta.from)) {
253
+ const newFroms = this.meta.from.map((from) => from.lock());
254
+ return new Queryable({
255
+ ...this.meta,
256
+ from: newFroms,
257
+ });
258
+ }
259
+ return new Queryable({
260
+ ...this.meta,
261
+ lock: true,
262
+ });
326
263
  }
327
- const column = fn(this.meta.columns);
328
- return new Queryable({
329
- ...this.meta,
330
- orderBy: [...this.meta.orderBy ?? [], [column, orderBy]]
331
- });
332
- }
333
- //#endregion
334
- //#region ========== Search - WHERE ==========
335
- /**
336
- * Add WHERE condition. Multiple calls are combined with AND.
337
- *
338
- * @param predicate - Function returning an array of conditions
339
- * @returns Queryable with conditions added
340
- *
341
- * @example
342
- * ```typescript
343
- * db.user
344
- * .where((u) => [expr.eq(u.isActive, true)])
345
- * .where((u) => [expr.gte(u.age, 18)])
346
- * ```
347
- */
348
- where(predicate) {
349
- if (Array.isArray(this.meta.from)) {
350
- const newFroms = this.meta.from.map((from) => from.where(predicate));
351
- return new Queryable({
352
- ...this.meta,
353
- from: newFroms
354
- });
264
+ //#endregion
265
+ //#region ========== restrict - TOP / LIMIT ==========
266
+ /**
267
+ * 상위 N개 행만 선택 (ORDER BY 없이도 사용 가능)
268
+ *
269
+ * @param count - 선택할 행 수
270
+ * @returns TOP이 적용된 Queryable
271
+ *
272
+ * @example
273
+ * ```typescript
274
+ * // Latest 10 users
275
+ * db.user()
276
+ * .orderBy((u) => u.createdAt, "DESC")
277
+ * .top(10)
278
+ * ```
279
+ */
280
+ top(count) {
281
+ if (Array.isArray(this.meta.from)) {
282
+ const newFroms = this.meta.from.map((from) => from.top(count));
283
+ return new Queryable({
284
+ ...this.meta,
285
+ from: newFroms,
286
+ });
287
+ }
288
+ return new Queryable({
289
+ ...this.meta,
290
+ top: count,
291
+ });
355
292
  }
356
- const conditions = predicate(this.meta.columns);
357
- return new Queryable({
358
- ...this.meta,
359
- where: [...this.meta.where ?? [], ...conditions]
360
- });
361
- }
362
- /**
363
- * Perform text search
364
- *
365
- * See {@link parseSearchQuery} for search syntax
366
- * - Space-separated words are OR conditions
367
- * - Words starting with `+` are required includes (AND condition)
368
- * - Words starting with `-` are excludes (NOT condition)
369
- *
370
- * @param fn - Function returning target columns to search
371
- * @param searchText - Search text
372
- * @returns Queryable with search conditions added
373
- *
374
- * @example
375
- * ```typescript
376
- * db.user()
377
- * .search((u) => [u.name, u.email], "John Doe -withdrawn")
378
- * ```
379
- */
380
- search(fn, searchText) {
381
- if (Array.isArray(this.meta.from)) {
382
- const newFroms = this.meta.from.map((from) => from.search(fn, searchText));
383
- return new Queryable({
384
- ...this.meta,
385
- from: newFroms
386
- });
293
+ /**
294
+ * 페이지네이션을 위한 LIMIT/OFFSET 설정.
295
+ * 먼저 orderBy()를 호출해야 함.
296
+ *
297
+ * @param skip - 건너뛸 행 수 (OFFSET)
298
+ * @param take - 가져올 행 수 (LIMIT)
299
+ * @returns 페이지네이션이 적용된 Queryable
300
+ * @throws ORDER BY 절이 없으면 에러
301
+ *
302
+ * @example
303
+ * ```typescript
304
+ * db.user
305
+ * .orderBy((u) => u.createdAt)
306
+ * .limit(0, 20) // first 20
307
+ * ```
308
+ */
309
+ limit(skip, take) {
310
+ if (Array.isArray(this.meta.from)) {
311
+ const newFroms = this.meta.from.map((from) => from.limit(skip, take));
312
+ return new Queryable({
313
+ ...this.meta,
314
+ from: newFroms,
315
+ });
316
+ }
317
+ if (!this.meta.orderBy) {
318
+ throw new ArgumentError("limit() ORDER BY 절이 필요합니다.", {
319
+ method: "limit",
320
+ required: "orderBy",
321
+ });
322
+ }
323
+ return new Queryable({
324
+ ...this.meta,
325
+ limit: [skip, take],
326
+ });
387
327
  }
388
- if (searchText.trim() === "") {
389
- return this;
328
+ //#endregion
329
+ //#region ========== sorting - ORDER BY ==========
330
+ /**
331
+ * 정렬 조건 추가. 여러 번 호출 시 순서대로 적용됨.
332
+ *
333
+ * @param fn - 정렬할 column을 반환하는 함수
334
+ * @param orderBy - 정렬 방향 (ASC/DESC). 기본값: ASC
335
+ * @returns 정렬 조건이 추가된 Queryable
336
+ *
337
+ * @example
338
+ * ```typescript
339
+ * db.user
340
+ * .orderBy((u) => u.name) // name ASC
341
+ * .orderBy((u) => u.age, "DESC") // age DESC
342
+ * ```
343
+ */
344
+ orderBy(fn, orderBy) {
345
+ if (Array.isArray(this.meta.from)) {
346
+ const newFroms = this.meta.from.map((from) => from.orderBy(fn, orderBy));
347
+ return new Queryable({
348
+ ...this.meta,
349
+ from: newFroms,
350
+ });
351
+ }
352
+ const column = fn(this.meta.columns);
353
+ return new Queryable({
354
+ ...this.meta,
355
+ orderBy: [...(this.meta.orderBy ?? []), [column, orderBy]],
356
+ });
390
357
  }
391
- const columns = fn(this.meta.columns);
392
- const parsed = parseSearchQuery(searchText);
393
- const conditions = [];
394
- if (parsed.or.length === 1) {
395
- const pattern = parsed.or[0];
396
- const columnMatches = columns.map((col) => expr.like(expr.lower(col), pattern.toLowerCase()));
397
- conditions.push(expr.or(columnMatches));
398
- } else if (parsed.or.length > 1) {
399
- const orConditions = parsed.or.map((pattern) => {
400
- const columnMatches = columns.map(
401
- (col) => expr.like(expr.lower(col), pattern.toLowerCase())
402
- );
403
- return expr.or(columnMatches);
404
- });
405
- conditions.push(expr.or(orConditions));
358
+ //#endregion
359
+ //#region ========== Search - WHERE ==========
360
+ /**
361
+ * WHERE 조건 추가. 여러 번 호출 시 AND로 결합됨.
362
+ *
363
+ * @param predicate - 조건 배열을 반환하는 함수
364
+ * @returns 조건이 추가된 Queryable
365
+ *
366
+ * @example
367
+ * ```typescript
368
+ * db.user
369
+ * .where((u) => [expr.eq(u.isActive, true)])
370
+ * .where((u) => [expr.gte(u.age, 18)])
371
+ * ```
372
+ */
373
+ where(predicate) {
374
+ if (Array.isArray(this.meta.from)) {
375
+ const newFroms = this.meta.from.map((from) => from.where(predicate));
376
+ return new Queryable({
377
+ ...this.meta,
378
+ from: newFroms,
379
+ });
380
+ }
381
+ const conditions = predicate(this.meta.columns);
382
+ return new Queryable({
383
+ ...this.meta,
384
+ where: [...(this.meta.where ?? []), ...conditions],
385
+ });
406
386
  }
407
- for (const pattern of parsed.must) {
408
- const columnMatches = columns.map((col) => expr.like(expr.lower(col), pattern.toLowerCase()));
409
- conditions.push(expr.or(columnMatches));
387
+ /**
388
+ * 텍스트 검색 수행
389
+ *
390
+ * 검색 구문은 {@link parseSearchQuery} 참조
391
+ * - 공백으로 구분된 단어는 OR 조건
392
+ * - `+`로 시작하는 단어는 필수 포함 (AND 조건)
393
+ * - `-`로 시작하는 단어는 제외 (NOT 조건)
394
+ *
395
+ * @param fn - 검색 대상 column을 반환하는 함수
396
+ * @param searchText - 검색 텍스트
397
+ * @returns 검색 조건이 추가된 Queryable
398
+ *
399
+ * @example
400
+ * ```typescript
401
+ * db.user()
402
+ * .search((u) => [u.name, u.email], "John Doe -withdrawn")
403
+ * ```
404
+ */
405
+ search(fn, searchText) {
406
+ if (Array.isArray(this.meta.from)) {
407
+ const newFroms = this.meta.from.map((from) => from.search(fn, searchText));
408
+ return new Queryable({
409
+ ...this.meta,
410
+ from: newFroms,
411
+ });
412
+ }
413
+ if (searchText.trim() === "") {
414
+ return this;
415
+ }
416
+ const columns = fn(this.meta.columns);
417
+ const parsed = parseSearchQuery(searchText);
418
+ const conditions = [];
419
+ // OR 조건: 아무 column에서 아무 패턴이 매칭되면 일치
420
+ if (parsed.or.length === 1) {
421
+ const pattern = parsed.or[0];
422
+ const columnMatches = columns.map((col) => expr.like(expr.lower(col), pattern.toLowerCase()));
423
+ conditions.push(expr.or(columnMatches));
424
+ }
425
+ else if (parsed.or.length > 1) {
426
+ const orConditions = parsed.or.map((pattern) => {
427
+ const columnMatches = columns.map((col) => expr.like(expr.lower(col), pattern.toLowerCase()));
428
+ return expr.or(columnMatches);
429
+ });
430
+ conditions.push(expr.or(orConditions));
431
+ }
432
+ // MUST 조건: 각 패턴이 최소 하나의 column에서 매칭되어야 함 (AND)
433
+ for (const pattern of parsed.must) {
434
+ const columnMatches = columns.map((col) => expr.like(expr.lower(col), pattern.toLowerCase()));
435
+ conditions.push(expr.or(columnMatches));
436
+ }
437
+ // NOT 조건: 아무 column에서도 매칭되지 않아야 함 (AND NOT)
438
+ for (const pattern of parsed.not) {
439
+ const columnMatches = columns.map((col) => expr.like(expr.lower(col), pattern.toLowerCase()));
440
+ conditions.push(expr.not(expr.or(columnMatches)));
441
+ }
442
+ if (conditions.length === 0) {
443
+ return this;
444
+ }
445
+ return this.where(() => [expr.and(conditions)]);
410
446
  }
411
- for (const pattern of parsed.not) {
412
- const columnMatches = columns.map((col) => expr.like(expr.lower(col), pattern.toLowerCase()));
413
- conditions.push(expr.not(expr.or(columnMatches)));
447
+ //#endregion
448
+ //#region ========== Group - GROUP BY / HAVING ==========
449
+ /**
450
+ * GROUP BY 절 추가
451
+ *
452
+ * @param fn - 그룹화할 column을 반환하는 함수
453
+ * @returns GROUP BY가 적용된 Queryable
454
+ *
455
+ * @example
456
+ * ```typescript
457
+ * db.order()
458
+ * .select((o) => ({
459
+ * userId: o.userId,
460
+ * totalAmount: expr.sum(o.amount),
461
+ * }))
462
+ * .groupBy((o) => [o.userId])
463
+ * ```
464
+ */
465
+ groupBy(fn) {
466
+ if (Array.isArray(this.meta.from)) {
467
+ const newFroms = this.meta.from.map((from) => from.groupBy(fn));
468
+ return new Queryable({
469
+ ...this.meta,
470
+ from: newFroms,
471
+ });
472
+ }
473
+ const groupBy = fn(this.meta.columns);
474
+ return new Queryable({ ...this.meta, groupBy });
414
475
  }
415
- if (conditions.length === 0) {
416
- return this;
476
+ /**
477
+ * HAVING 절 추가 (GROUP BY 이후 필터링)
478
+ *
479
+ * @param predicate - 조건 배열을 반환하는 함수
480
+ * @returns HAVING이 적용된 Queryable
481
+ *
482
+ * @example
483
+ * ```typescript
484
+ * db.order()
485
+ * .select((o) => ({
486
+ * userId: o.userId,
487
+ * totalAmount: expr.sum(o.amount),
488
+ * }))
489
+ * .groupBy((o) => [o.userId])
490
+ * .having((o) => [expr.gte(o.totalAmount, 10000)])
491
+ * ```
492
+ */
493
+ having(predicate) {
494
+ if (Array.isArray(this.meta.from)) {
495
+ const newFroms = this.meta.from.map((from) => from.having(predicate));
496
+ return new Queryable({
497
+ ...this.meta,
498
+ from: newFroms,
499
+ });
500
+ }
501
+ const conditions = predicate(this.meta.columns);
502
+ return new Queryable({
503
+ ...this.meta,
504
+ having: [...(this.meta.having ?? []), ...conditions],
505
+ });
417
506
  }
418
- return this.where(() => [expr.and(conditions)]);
419
- }
420
- //#endregion
421
- //#region ========== Group - GROUP BY / HAVING ==========
422
- /**
423
- * Add GROUP BY clause
424
- *
425
- * @param fn - Function returning columns to group by
426
- * @returns Queryable with GROUP BY applied
427
- *
428
- * @example
429
- * ```typescript
430
- * db.order()
431
- * .select((o) => ({
432
- * userId: o.userId,
433
- * totalAmount: expr.sum(o.amount),
434
- * }))
435
- * .groupBy((o) => [o.userId])
436
- * ```
437
- */
438
- groupBy(fn) {
439
- if (Array.isArray(this.meta.from)) {
440
- const newFroms = this.meta.from.map((from) => from.groupBy(fn));
441
- return new Queryable({
442
- ...this.meta,
443
- from: newFroms
444
- });
507
+ //#endregion
508
+ //#region ========== join - JOIN / JOIN SINGLE ==========
509
+ /**
510
+ * 1:N 관계에 대한 LEFT OUTER JOIN 수행 (결과에 배열로 추가)
511
+ *
512
+ * @param as - 결과에 추가할 속성 이름
513
+ * @param fn - 조인 조건을 정의하는 콜백 함수
514
+ * @returns 조인 결과가 배열로 추가된 Queryable
515
+ *
516
+ * @example
517
+ * ```typescript
518
+ * db.user()
519
+ * .join("posts", (qr, u) =>
520
+ * qr.from(Post)
521
+ * .where((p) => [expr.eq(p.userId, u.id)])
522
+ * )
523
+ * // Result: { id, name, posts: [{ id, title }, ...] }
524
+ * ```
525
+ */
526
+ join(as, fn) {
527
+ if (Array.isArray(this.meta.from)) {
528
+ const newFroms = this.meta.from.map((from) => from.join(as, fn));
529
+ return new Queryable({
530
+ ...this.meta,
531
+ from: newFroms,
532
+ columns: transformColumnsAlias(newFroms[0].meta.columns, this.meta.as, ""),
533
+ });
534
+ }
535
+ // 1. join alias Generate
536
+ const joinAlias = `${this.meta.as}.${as}`;
537
+ // 2. Transform target → Queryable (pass alias)
538
+ const joinQr = new JoinQueryable(this.meta.db, joinAlias);
539
+ // 3. Execute fn (returns Queryable with conditions like where added)
540
+ const resultQr = fn(joinQr, this.meta.columns);
541
+ // 4. Add join result to new columns
542
+ const joinColumns = transformColumnsAlias(resultQr.meta.columns, joinAlias);
543
+ return new Queryable({
544
+ ...this.meta,
545
+ columns: {
546
+ ...this.meta.columns,
547
+ [as]: [joinColumns],
548
+ },
549
+ isCustomColumns: true,
550
+ joins: [...(this.meta.joins ?? []), { queryable: resultQr, isSingle: false }],
551
+ });
445
552
  }
446
- const groupBy = fn(this.meta.columns);
447
- return new Queryable({ ...this.meta, groupBy });
448
- }
449
- /**
450
- * Add HAVING clause (filtering after GROUP BY)
451
- *
452
- * @param predicate - Function returning an array of conditions
453
- * @returns Queryable with HAVING applied
454
- *
455
- * @example
456
- * ```typescript
457
- * db.order()
458
- * .select((o) => ({
459
- * userId: o.userId,
460
- * totalAmount: expr.sum(o.amount),
461
- * }))
462
- * .groupBy((o) => [o.userId])
463
- * .having((o) => [expr.gte(o.totalAmount, 10000)])
464
- * ```
465
- */
466
- having(predicate) {
467
- if (Array.isArray(this.meta.from)) {
468
- const newFroms = this.meta.from.map((from) => from.having(predicate));
469
- return new Queryable({
470
- ...this.meta,
471
- from: newFroms
472
- });
553
+ /**
554
+ * N:1 또는 1:1 관계에 대한 LEFT OUTER JOIN 수행 (결과에 단일 객체로 추가)
555
+ *
556
+ * @param as - 결과에 추가할 속성 이름
557
+ * @param fn - 조인 조건을 정의하는 콜백 함수
558
+ * @returns 조인 결과가 단일 객체로 추가된 Queryable
559
+ *
560
+ * @example
561
+ * ```typescript
562
+ * db.post()
563
+ * .joinSingle("user", (qr, p) =>
564
+ * qr.from(User)
565
+ * .where((u) => [expr.eq(u.id, p.userId)])
566
+ * )
567
+ * // Result: { id, title, user: { id, name } | undefined }
568
+ * ```
569
+ */
570
+ joinSingle(as, fn) {
571
+ if (Array.isArray(this.meta.from)) {
572
+ const newFroms = this.meta.from.map((from) => from.joinSingle(as, fn));
573
+ return new Queryable({
574
+ ...this.meta,
575
+ from: newFroms,
576
+ columns: transformColumnsAlias(newFroms[0].meta.columns, this.meta.as, ""),
577
+ });
578
+ }
579
+ // 1. join alias Generate
580
+ const joinAlias = `${this.meta.as}.${as}`;
581
+ // 2. Transform target → Queryable (pass alias)
582
+ const joinQr = new JoinQueryable(this.meta.db, joinAlias);
583
+ // 3. Execute fn (returns Queryable with conditions like where added)
584
+ const resultQr = fn(joinQr, this.meta.columns);
585
+ // 4. Add join result to new columns
586
+ const joinColumns = transformColumnsAlias(resultQr.meta.columns, joinAlias);
587
+ return new Queryable({
588
+ ...this.meta,
589
+ columns: {
590
+ ...this.meta.columns,
591
+ [as]: joinColumns,
592
+ },
593
+ isCustomColumns: true,
594
+ joins: [...(this.meta.joins ?? []), { queryable: resultQr, isSingle: true }],
595
+ });
473
596
  }
474
- const conditions = predicate(this.meta.columns);
475
- return new Queryable({
476
- ...this.meta,
477
- having: [...this.meta.having ?? [], ...conditions]
478
- });
479
- }
480
- //#endregion
481
- //#region ========== join - JOIN / JOIN SINGLE ==========
482
- /**
483
- * Perform LEFT OUTER JOIN for 1:N relation (added as array to result)
484
- *
485
- * @param as - Property name to add to result
486
- * @param fn - Callback function defining join conditions
487
- * @returns Queryable with join result added as array
488
- *
489
- * @example
490
- * ```typescript
491
- * db.user()
492
- * .join("posts", (qr, u) =>
493
- * qr.from(Post)
494
- * .where((p) => [expr.eq(p.userId, u.id)])
495
- * )
496
- * // Result: { id, name, posts: [{ id, title }, ...] }
497
- * ```
498
- */
499
- join(as, fn) {
500
- if (Array.isArray(this.meta.from)) {
501
- const newFroms = this.meta.from.map((from) => from.join(as, fn));
502
- return new Queryable({
503
- ...this.meta,
504
- from: newFroms,
505
- columns: transformColumnsAlias(newFroms[0].meta.columns, this.meta.as, "")
506
- });
597
+ //#endregion
598
+ //#region ========== join - INCLUDE ==========
599
+ /**
600
+ * 관련 table을 자동으로 JOIN.
601
+ * TableBuilder에 정의된 FK/FKT 관계를 기반으로 동작.
602
+ *
603
+ * @param fn - 포함할 관계를 선택하는 함수 (PathProxy를 통해 타입 체크)
604
+ * @returns JOIN 추가된 Queryable
605
+ * @throws 관계가 정의되지 않은 경우 에러
606
+ *
607
+ * @example
608
+ * ```typescript
609
+ * // Single relationship include
610
+ * db.post.include((p) => p.user)
611
+ *
612
+ * // Nested relationship include
613
+ * db.post.include((p) => p.user.company)
614
+ *
615
+ * // Multiple relationship include
616
+ * db.user
617
+ * .include((u) => u.company)
618
+ * .include((u) => u.posts)
619
+ * ```
620
+ */
621
+ include(fn) {
622
+ if (Array.isArray(this.meta.from)) {
623
+ const newFroms = this.meta.from.map((from) => from.include(fn));
624
+ return new Queryable({
625
+ ...this.meta,
626
+ from: newFroms,
627
+ columns: transformColumnsAlias(newFroms[0].meta.columns, this.meta.as, ""),
628
+ });
629
+ }
630
+ const proxy = createPathProxy();
631
+ const result = fn(proxy);
632
+ const relationChain = result[PATH_SYMBOL].join(".");
633
+ return this._include(relationChain);
507
634
  }
508
- const joinAlias = `${this.meta.as}.${as}`;
509
- const joinQr = new JoinQueryable(this.meta.db, joinAlias);
510
- const resultQr = fn(joinQr, this.meta.columns);
511
- const joinColumns = transformColumnsAlias(resultQr.meta.columns, joinAlias);
512
- return new Queryable({
513
- ...this.meta,
514
- columns: {
515
- ...this.meta.columns,
516
- [as]: [joinColumns]
517
- },
518
- isCustomColumns: true,
519
- joins: [...this.meta.joins ?? [], { queryable: resultQr, isSingle: false }]
520
- });
521
- }
522
- /**
523
- * Perform LEFT OUTER JOIN for N:1 or 1:1 relation (added as single object to result)
524
- *
525
- * @param as - Property name to add to result
526
- * @param fn - Callback function defining join conditions
527
- * @returns Queryable with join result added as single object
528
- *
529
- * @example
530
- * ```typescript
531
- * db.post()
532
- * .joinSingle("user", (qr, p) =>
533
- * qr.from(User)
534
- * .where((u) => [expr.eq(u.id, p.userId)])
535
- * )
536
- * // Result: { id, title, user: { id, name } | undefined }
537
- * ```
538
- */
539
- joinSingle(as, fn) {
540
- if (Array.isArray(this.meta.from)) {
541
- const newFroms = this.meta.from.map((from) => from.joinSingle(as, fn));
542
- return new Queryable({
543
- ...this.meta,
544
- from: newFroms,
545
- columns: transformColumnsAlias(newFroms[0].meta.columns, this.meta.as, "")
546
- });
635
+ _include(relationChain) {
636
+ const relationNames = relationChain.split(".");
637
+ let result = this;
638
+ let currentTable = this.meta.from;
639
+ const chainParts = [];
640
+ for (const relationName of relationNames) {
641
+ if (!(currentTable instanceof TableBuilder)) {
642
+ throw new Error("include()는 TableBuilder 기반 queryable에서만 사용할 수 있습니다.");
643
+ }
644
+ const parentChain = chainParts.join(".");
645
+ chainParts.push(relationName);
646
+ // 이미 JOIN된 경우 중복 추가 방지
647
+ const targetAlias = `${result.meta.as}.${chainParts.join(".")}`;
648
+ const existingJoin = result.meta.joins?.find((j) => j.queryable.meta.as === targetAlias);
649
+ if (existingJoin) {
650
+ // 기존 JOIN table로 currentTable 갱신 계속
651
+ const existingFrom = existingJoin.queryable.meta.from;
652
+ if (existingFrom instanceof TableBuilder) {
653
+ currentTable = existingFrom;
654
+ }
655
+ continue;
656
+ }
657
+ const relationDef = currentTable.meta.relations?.[relationName];
658
+ if (relationDef == null) {
659
+ throw new Error(`관계 '${relationName}'을(를) 찾을 수 없습니다.`);
660
+ }
661
+ if (relationDef instanceof ForeignKeyBuilder || relationDef instanceof RelationKeyBuilder) {
662
+ // FK/RelationKey (N:1): Post.user → User
663
+ // condition: Post.userId = User.id
664
+ const targetTable = relationDef.meta.targetFn();
665
+ const fkColKeys = relationDef.meta.columns;
666
+ const targetPkColKeys = getMatchedPrimaryKeys(fkColKeys, targetTable);
667
+ result = result.joinSingle(chainParts.join("."), (joinQr, parentCols) => {
668
+ const qr = joinQr.from(targetTable);
669
+ // FKT join is stored as array, so use first element if array
670
+ const srcColsRaw = parentChain ? parentCols[parentChain] : parentCols;
671
+ const srcCols = (Array.isArray(srcColsRaw) ? srcColsRaw[0] : srcColsRaw);
672
+ const conditions = [];
673
+ for (let i = 0; i < fkColKeys.length; i++) {
674
+ const fkCol = srcCols[fkColKeys[i]];
675
+ const pkCol = qr.meta.columns[targetPkColKeys[i]];
676
+ conditions.push(expr.eq(pkCol, fkCol));
677
+ }
678
+ return qr.where(() => conditions);
679
+ });
680
+ currentTable = targetTable;
681
+ }
682
+ else if (relationDef instanceof ForeignKeyTargetBuilder ||
683
+ relationDef instanceof RelationKeyTargetBuilder) {
684
+ // FKT/RelationKeyTarget (1:N or 1:1): User.posts → Post[]
685
+ // condition: Post.userId = User.id
686
+ const targetTable = relationDef.meta.targetTableFn();
687
+ const fkRelName = relationDef.meta.relationName;
688
+ const sourceFk = targetTable.meta.relations?.[fkRelName];
689
+ if (!(sourceFk instanceof ForeignKeyBuilder) && !(sourceFk instanceof RelationKeyBuilder)) {
690
+ throw new Error(`'${relationName}'이(가) 참조하는 '${fkRelName}'은(는) ` +
691
+ `${targetTable.meta.name} 테이블에서 유효한 ForeignKey/RelationKey가 아닙니다.`);
692
+ }
693
+ const sourceTable = targetTable;
694
+ const isSingle = relationDef.meta.isSingle ?? false;
695
+ const fkColKeys = sourceFk.meta.columns;
696
+ const pkColKeys = getMatchedPrimaryKeys(fkColKeys, currentTable);
697
+ const buildJoin = (joinQr, parentCols) => {
698
+ const qr = joinQr.from(sourceTable);
699
+ // FKT join is stored as array, so use first element if array
700
+ const srcColsRaw = parentChain ? parentCols[parentChain] : parentCols;
701
+ const srcCols = (Array.isArray(srcColsRaw) ? srcColsRaw[0] : srcColsRaw);
702
+ const conditions = [];
703
+ for (let i = 0; i < fkColKeys.length; i++) {
704
+ const pkCol = srcCols[pkColKeys[i]];
705
+ const fkCol = qr.meta.columns[fkColKeys[i]];
706
+ conditions.push(expr.eq(fkCol, pkCol));
707
+ }
708
+ return qr.where(() => conditions);
709
+ };
710
+ result = isSingle
711
+ ? result.joinSingle(chainParts.join("."), buildJoin)
712
+ : result.join(chainParts.join("."), buildJoin);
713
+ currentTable = sourceTable;
714
+ }
715
+ }
716
+ return result;
547
717
  }
548
- const joinAlias = `${this.meta.as}.${as}`;
549
- const joinQr = new JoinQueryable(this.meta.db, joinAlias);
550
- const resultQr = fn(joinQr, this.meta.columns);
551
- const joinColumns = transformColumnsAlias(resultQr.meta.columns, joinAlias);
552
- return new Queryable({
553
- ...this.meta,
554
- columns: {
555
- ...this.meta.columns,
556
- [as]: joinColumns
557
- },
558
- isCustomColumns: true,
559
- joins: [...this.meta.joins ?? [], { queryable: resultQr, isSingle: true }]
560
- });
561
- }
562
- //#endregion
563
- //#region ========== join - INCLUDE ==========
564
- /**
565
- * Automatically JOIN related tables.
566
- * Operates based on FK/FKT relations defined in TableBuilder.
567
- *
568
- * @param fn - Function selecting relations to include (type-checked via PathProxy)
569
- * @returns Queryable with JOINs added
570
- * @throws Error if relation is not defined
571
- *
572
- * @example
573
- * ```typescript
574
- * // Single relationship include
575
- * db.post.include((p) => p.user)
576
- *
577
- * // Nested relationship include
578
- * db.post.include((p) => p.user.company)
579
- *
580
- * // Multiple relationship include
581
- * db.user
582
- * .include((u) => u.company)
583
- * .include((u) => u.posts)
584
- * ```
585
- */
586
- include(fn) {
587
- if (Array.isArray(this.meta.from)) {
588
- const newFroms = this.meta.from.map((from) => from.include(fn));
589
- return new Queryable({
590
- ...this.meta,
591
- from: newFroms,
592
- columns: transformColumnsAlias(newFroms[0].meta.columns, this.meta.as, "")
593
- });
718
+ //#endregion
719
+ //#region ========== Subquery - WRAP / UNION ==========
720
+ /**
721
+ * Wrap the current Queryable as a Subquery
722
+ *
723
+ * Required when using count() after distinct() or groupBy()
724
+ *
725
+ * @returns Queryable wrapped as a Subquery
726
+ *
727
+ * @example
728
+ * ```typescript
729
+ * // Count after DISTINCT
730
+ * const count = await db.user()
731
+ * .select((u) => ({ name: u.name }))
732
+ * .distinct()
733
+ * .wrap()
734
+ * .count();
735
+ * ```
736
+ */
737
+ wrap() {
738
+ // Wrap the current Queryable as a Subquery
739
+ const wrapAlias = this.meta.db.getNextAlias();
740
+ return new Queryable({
741
+ db: this.meta.db,
742
+ from: this,
743
+ as: wrapAlias,
744
+ columns: transformColumnsAlias(this.meta.columns, wrapAlias, ""),
745
+ });
594
746
  }
595
- const proxy = createPathProxy();
596
- const result = fn(proxy);
597
- const relationChain = result[PATH_SYMBOL].join(".");
598
- return this._include(relationChain);
599
- }
600
- _include(relationChain) {
601
- var _a, _b, _c;
602
- const relationNames = relationChain.split(".");
603
- let result = this;
604
- let currentTable = this.meta.from;
605
- const chainParts = [];
606
- for (const relationName of relationNames) {
607
- if (!(currentTable instanceof TableBuilder)) {
608
- throw new Error("include() can only be used on TableBuilder-based queryables.");
609
- }
610
- const parentChain = chainParts.join(".");
611
- chainParts.push(relationName);
612
- const targetAlias = `${result.meta.as}.${chainParts.join(".")}`;
613
- const existingJoin = (_a = result.meta.joins) == null ? void 0 : _a.find((j) => j.queryable.meta.as === targetAlias);
614
- if (existingJoin) {
615
- const existingFrom = existingJoin.queryable.meta.from;
616
- if (existingFrom instanceof TableBuilder) {
617
- currentTable = existingFrom;
747
+ /**
748
+ * Combine multiple Queryables with UNION (remove duplicates)
749
+ *
750
+ * @param queries - Array of Queryables to UNION (minimum 2)
751
+ * @returns UNION-ed Queryable
752
+ * @throws If less than 2 queryables are passed
753
+ *
754
+ * @example
755
+ * ```typescript
756
+ * const combined = Queryable.union(
757
+ * db.user().where((u) => [expr.eq(u.type, "admin")]),
758
+ * db.user().where((u) => [expr.eq(u.type, "manager")]),
759
+ * );
760
+ * ```
761
+ */
762
+ static union(...queries) {
763
+ if (queries.length < 2) {
764
+ throw new ArgumentError("union은 최소 2개의 queryable이 필요합니다.", {
765
+ provided: queries.length,
766
+ minimum: 2,
767
+ });
618
768
  }
619
- continue;
620
- }
621
- const relationDef = (_b = currentTable.meta.relations) == null ? void 0 : _b[relationName];
622
- if (relationDef == null) {
623
- throw new Error(`Relation '${relationName}' not found.`);
624
- }
625
- if (relationDef instanceof ForeignKeyBuilder || relationDef instanceof RelationKeyBuilder) {
626
- const targetTable = relationDef.meta.targetFn();
627
- const fkColKeys = relationDef.meta.columns;
628
- const targetPkColKeys = getMatchedPrimaryKeys(fkColKeys, targetTable);
629
- result = result.joinSingle(chainParts.join("."), (joinQr, parentCols) => {
630
- const qr = joinQr.from(targetTable);
631
- const srcColsRaw = parentChain ? parentCols[parentChain] : parentCols;
632
- const srcCols = Array.isArray(srcColsRaw) ? srcColsRaw[0] : srcColsRaw;
633
- const conditions = [];
634
- for (let i = 0; i < fkColKeys.length; i++) {
635
- const fkCol = srcCols[fkColKeys[i]];
636
- const pkCol = qr.meta.columns[targetPkColKeys[i]];
637
- conditions.push(expr.eq(pkCol, fkCol));
638
- }
639
- return qr.where(() => conditions);
769
+ const first = queries[0];
770
+ const unionAlias = first.meta.db.getNextAlias();
771
+ return new Queryable({
772
+ db: first.meta.db,
773
+ from: queries, // Queryable[] 배열로 저장
774
+ as: unionAlias,
775
+ columns: transformColumnsAlias(first.meta.columns, unionAlias, ""),
640
776
  });
641
- currentTable = targetTable;
642
- } else if (relationDef instanceof ForeignKeyTargetBuilder || relationDef instanceof RelationKeyTargetBuilder) {
643
- const targetTable = relationDef.meta.targetTableFn();
644
- const fkRelName = relationDef.meta.relationName;
645
- const sourceFk = (_c = targetTable.meta.relations) == null ? void 0 : _c[fkRelName];
646
- if (!(sourceFk instanceof ForeignKeyBuilder) && !(sourceFk instanceof RelationKeyBuilder)) {
647
- throw new Error(
648
- `'${fkRelName}' referenced by '${relationName}' is not a valid ForeignKey/RelationKey in ${targetTable.meta.name} table.`
649
- );
777
+ }
778
+ //#endregion
779
+ //#region ========== recursive - WITH RECURSIVE ==========
780
+ /**
781
+ * Generate a recursive CTE (Common Table Expression)
782
+ *
783
+ * Used for querying hierarchical data (org charts, category trees, etc.)
784
+ *
785
+ * @param fn - Callback function that defines the recursive part
786
+ * @returns Queryable with the recursive CTE applied
787
+ *
788
+ * @example
789
+ * ```typescript
790
+ * // Query org chart hierarchy
791
+ * db.employee()
792
+ * .where((e) => [expr.null(e.managerId)]) // Root nodes
793
+ * .recursive((cte) =>
794
+ * cte.from(Employee)
795
+ * .where((e) => [expr.eq(e.managerId, e.self[0].id)])
796
+ * )
797
+ * ```
798
+ */
799
+ recursive(fn) {
800
+ if (Array.isArray(this.meta.from)) {
801
+ const newFroms = this.meta.from.map((from) => from.recursive(fn));
802
+ return new Queryable({
803
+ ...this.meta,
804
+ from: newFroms,
805
+ columns: transformColumnsAlias(newFroms[0].meta.columns, this.meta.as, ""),
806
+ });
650
807
  }
651
- const sourceTable = targetTable;
652
- const isSingle = relationDef.meta.isSingle ?? false;
653
- const fkColKeys = sourceFk.meta.columns;
654
- const pkColKeys = getMatchedPrimaryKeys(fkColKeys, currentTable);
655
- const buildJoin = (joinQr, parentCols) => {
656
- const qr = joinQr.from(sourceTable);
657
- const srcColsRaw = parentChain ? parentCols[parentChain] : parentCols;
658
- const srcCols = Array.isArray(srcColsRaw) ? srcColsRaw[0] : srcColsRaw;
659
- const conditions = [];
660
- for (let i = 0; i < fkColKeys.length; i++) {
661
- const pkCol = srcCols[pkColKeys[i]];
662
- const fkCol = qr.meta.columns[fkColKeys[i]];
663
- conditions.push(expr.eq(fkCol, pkCol));
664
- }
665
- return qr.where(() => conditions);
666
- };
667
- result = isSingle ? result.joinSingle(chainParts.join("."), buildJoin) : result.join(chainParts.join("."), buildJoin);
668
- currentTable = sourceTable;
669
- }
808
+ // Generate dynamic CTE name
809
+ const cteName = this.meta.db.getNextAlias();
810
+ // 2. Transform target to Queryable (pass CTE name)
811
+ const cteQr = new RecursiveQueryable(this, cteName);
812
+ // 3. Execute fn (returns Queryable with conditions like where added)
813
+ const resultQr = fn(cteQr);
814
+ return new Queryable({
815
+ db: this.meta.db,
816
+ as: this.meta.as,
817
+ from: cteName,
818
+ columns: transformColumnsAlias(this.meta.columns, this.meta.as, ""),
819
+ with: {
820
+ name: cteName,
821
+ base: this, // Block circular reference type inference
822
+ recursive: resultQr,
823
+ },
824
+ });
670
825
  }
671
- return result;
672
- }
673
- //#endregion
674
- //#region ========== Subquery - WRAP / UNION ==========
675
- /**
676
- * Wrap the current Queryable as a Subquery
677
- *
678
- * Required when using count() after distinct() or groupBy()
679
- *
680
- * @returns Queryable wrapped as a Subquery
681
- *
682
- * @example
683
- * ```typescript
684
- * // Count after DISTINCT
685
- * const count = await db.user()
686
- * .select((u) => ({ name: u.name }))
687
- * .distinct()
688
- * .wrap()
689
- * .count();
690
- * ```
691
- */
692
- wrap() {
693
- const wrapAlias = this.meta.db.getNextAlias();
694
- return new Queryable({
695
- db: this.meta.db,
696
- from: this,
697
- as: wrapAlias,
698
- columns: transformColumnsAlias(this.meta.columns, wrapAlias, "")
699
- });
700
- }
701
- /**
702
- * Combine multiple Queryables with UNION (remove duplicates)
703
- *
704
- * @param queries - Array of Queryables to UNION (minimum 2)
705
- * @returns UNION-ed Queryable
706
- * @throws If less than 2 queryables are passed
707
- *
708
- * @example
709
- * ```typescript
710
- * const combined = Queryable.union(
711
- * db.user().where((u) => [expr.eq(u.type, "admin")]),
712
- * db.user().where((u) => [expr.eq(u.type, "manager")]),
713
- * );
714
- * ```
715
- */
716
- static union(...queries) {
717
- if (queries.length < 2) {
718
- throw new ArgumentError("union requires at least 2 queryables.", {
719
- provided: queries.length,
720
- minimum: 2
721
- });
826
+ //#endregion
827
+ //#region ========== [query] Select - SELECT ==========
828
+ /**
829
+ * Execute a SELECT query and return the result array
830
+ *
831
+ * @returns Query result array
832
+ *
833
+ * @example
834
+ * ```typescript
835
+ * const users = await db.user()
836
+ * .where((u) => [expr.eq(u.isActive, true)])
837
+ * .execute();
838
+ * ```
839
+ */
840
+ async execute() {
841
+ const results = await this.meta.db.executeDefs([this.getSelectQueryDef()], [this.getResultMeta()]);
842
+ return results[0];
722
843
  }
723
- const first = queries[0];
724
- const unionAlias = first.meta.db.getNextAlias();
725
- return new Queryable({
726
- db: first.meta.db,
727
- from: queries,
728
- // stored as Queryable[] array
729
- as: unionAlias,
730
- columns: transformColumnsAlias(first.meta.columns, unionAlias, "")
731
- });
732
- }
733
- //#endregion
734
- //#region ========== recursive - WITH RECURSIVE ==========
735
- /**
736
- * Generate a recursive CTE (Common Table Expression)
737
- *
738
- * Used for querying hierarchical data (org charts, category trees, etc.)
739
- *
740
- * @param fn - Callback function that defines the recursive part
741
- * @returns Queryable with the recursive CTE applied
742
- *
743
- * @example
744
- * ```typescript
745
- * // Query org chart hierarchy
746
- * db.employee()
747
- * .where((e) => [expr.null(e.managerId)]) // Root nodes
748
- * .recursive((cte) =>
749
- * cte.from(Employee)
750
- * .where((e) => [expr.eq(e.managerId, e.self[0].id)])
751
- * )
752
- * ```
753
- */
754
- recursive(fn) {
755
- if (Array.isArray(this.meta.from)) {
756
- const newFroms = this.meta.from.map((from) => from.recursive(fn));
757
- return new Queryable({
758
- ...this.meta,
759
- from: newFroms,
760
- columns: transformColumnsAlias(newFroms[0].meta.columns, this.meta.as, "")
761
- });
844
+ /**
845
+ * Return a single result (Error if more than 1)
846
+ *
847
+ * @returns Single result or undefined
848
+ * @throws When more than one result is returned
849
+ *
850
+ * @example
851
+ * ```typescript
852
+ * const user = await db.user()
853
+ * .where((u) => [expr.eq(u.id, 1)])
854
+ * .single();
855
+ * ```
856
+ */
857
+ async single() {
858
+ const result = await this.top(2).execute();
859
+ if (result.length > 1) {
860
+ throw new ArgumentError("단일 결과를 기대했으나 복수 결과가 반환되었습니다.", {
861
+ table: this._getSourceName(),
862
+ resultCount: result.length,
863
+ });
864
+ }
865
+ return result[0];
762
866
  }
763
- const cteName = this.meta.db.getNextAlias();
764
- const cteQr = new RecursiveQueryable(this, cteName);
765
- const resultQr = fn(cteQr);
766
- return new Queryable({
767
- db: this.meta.db,
768
- as: this.meta.as,
769
- from: cteName,
770
- columns: transformColumnsAlias(this.meta.columns, this.meta.as, ""),
771
- with: {
772
- name: cteName,
773
- base: this,
774
- // Block circular reference type inference
775
- recursive: resultQr
776
- }
777
- });
778
- }
779
- //#endregion
780
- //#region ========== [query] Select - SELECT ==========
781
- /**
782
- * Execute a SELECT query and return the result array
783
- *
784
- * @returns Query result array
785
- *
786
- * @example
787
- * ```typescript
788
- * const users = await db.user()
789
- * .where((u) => [expr.eq(u.isActive, true)])
790
- * .execute();
791
- * ```
792
- */
793
- async execute() {
794
- const results = await this.meta.db.executeDefs(
795
- [this.getSelectQueryDef()],
796
- [this.getResultMeta()]
797
- );
798
- return results[0];
799
- }
800
- /**
801
- * Return a single result (Error if more than 1)
802
- *
803
- * @returns Single result or undefined
804
- * @throws When more than one result is returned
805
- *
806
- * @example
807
- * ```typescript
808
- * const user = await db.user()
809
- * .where((u) => [expr.eq(u.id, 1)])
810
- * .single();
811
- * ```
812
- */
813
- async single() {
814
- const result = await this.top(2).execute();
815
- if (result.length > 1) {
816
- throw new ArgumentError("Expected single result but multiple results returned.", {
817
- table: this._getSourceName(),
818
- resultCount: result.length
819
- });
867
+ /**
868
+ * Query 소스 이름 반환 (에러 메시지용)
869
+ */
870
+ _getSourceName() {
871
+ const from = this.meta.from;
872
+ if (from instanceof TableBuilder || from instanceof ViewBuilder) {
873
+ return from.meta.name;
874
+ }
875
+ if (typeof from === "string") {
876
+ return from;
877
+ }
878
+ return this.meta.as;
820
879
  }
821
- return result[0];
822
- }
823
- /**
824
- * Return query source name (for error messages)
825
- */
826
- _getSourceName() {
827
- const from = this.meta.from;
828
- if (from instanceof TableBuilder || from instanceof ViewBuilder) {
829
- return from.meta.name;
880
+ /**
881
+ * Return the first result (only the first even if multiple exist)
882
+ *
883
+ * @returns First result or undefined
884
+ *
885
+ * @example
886
+ * ```typescript
887
+ * const latestUser = await db.user()
888
+ * .orderBy((u) => u.createdAt, "DESC")
889
+ * .first();
890
+ * ```
891
+ */
892
+ async first() {
893
+ const results = await this.top(1).execute();
894
+ return results[0];
830
895
  }
831
- if (typeof from === "string") {
832
- return from;
896
+ /**
897
+ * Return the number of result rows
898
+ *
899
+ * @param fn - Function to specify the column to count (optional)
900
+ * @returns Number of rows
901
+ * @throws Error when called directly after distinct() or groupBy() (use wrap() first)
902
+ *
903
+ * @example
904
+ * ```typescript
905
+ * const count = await db.user()
906
+ * .where((u) => [expr.eq(u.isActive, true)])
907
+ * .count();
908
+ * ```
909
+ */
910
+ async count(fn) {
911
+ if (this.meta.distinct) {
912
+ throw new Error("distinct() 이후에 count()를 사용할 수 없습니다. wrap()을 먼저 사용하세요.");
913
+ }
914
+ if (this.meta.groupBy) {
915
+ throw new Error("groupBy() 이후에 count()를 사용할 수 없습니다. wrap()을 먼저 사용하세요.");
916
+ }
917
+ const countQr = fn
918
+ ? this.select((c) => ({ cnt: expr.count(fn(c)) }))
919
+ : this.select(() => ({ cnt: expr.count() }));
920
+ const result = await countQr.single();
921
+ return result?.cnt ?? 0;
833
922
  }
834
- return this.meta.as;
835
- }
836
- /**
837
- * Return the first result (only the first even if multiple exist)
838
- *
839
- * @returns First result or undefined
840
- *
841
- * @example
842
- * ```typescript
843
- * const latestUser = await db.user()
844
- * .orderBy((u) => u.createdAt, "DESC")
845
- * .first();
846
- * ```
847
- */
848
- async first() {
849
- const results = await this.top(1).execute();
850
- return results[0];
851
- }
852
- /**
853
- * Return the number of result rows
854
- *
855
- * @param fn - Function to specify the column to count (optional)
856
- * @returns Number of rows
857
- * @throws Error when called directly after distinct() or groupBy() (use wrap() first)
858
- *
859
- * @example
860
- * ```typescript
861
- * const count = await db.user()
862
- * .where((u) => [expr.eq(u.isActive, true)])
863
- * .count();
864
- * ```
865
- */
866
- async count(fn) {
867
- if (this.meta.distinct) {
868
- throw new Error("Cannot use count() after distinct(). Use wrap() first.");
923
+ /**
924
+ * Check whether data matching the conditions exists
925
+ *
926
+ * @returns true if exists, false otherwise
927
+ *
928
+ * @example
929
+ * ```typescript
930
+ * const hasAdmin = await db.user()
931
+ * .where((u) => [expr.eq(u.role, "admin")])
932
+ * .exists();
933
+ * ```
934
+ */
935
+ async exists() {
936
+ const count = await this.count();
937
+ return count > 0;
869
938
  }
870
- if (this.meta.groupBy) {
871
- throw new Error("Cannot use count() after groupBy(). Use wrap() first.");
939
+ getSelectQueryDef() {
940
+ return obj.clearUndefined({
941
+ type: "select",
942
+ from: this._buildFromDef(),
943
+ as: this.meta.as,
944
+ select: this.meta.isCustomColumns ? this._buildSelectDef(this.meta.columns, "") : undefined,
945
+ distinct: this.meta.distinct,
946
+ top: this.meta.top,
947
+ lock: this.meta.lock,
948
+ where: this.meta.where?.map((w) => w.expr),
949
+ joins: this.meta.joins ? this._buildJoinDefs(this.meta.joins) : undefined,
950
+ orderBy: this.meta.orderBy?.map((o) => (o[1] ? [o[0].expr, o[1]] : [o[0].expr])),
951
+ limit: this.meta.limit,
952
+ groupBy: this.meta.groupBy?.map((g) => g.expr),
953
+ having: this.meta.having?.map((w) => w.expr),
954
+ with: this.meta.with
955
+ ? {
956
+ name: this.meta.with.name,
957
+ base: this.meta.with.base.getSelectQueryDef(),
958
+ recursive: this.meta.with.recursive.getSelectQueryDef(),
959
+ }
960
+ : undefined,
961
+ });
872
962
  }
873
- const countQr = fn ? this.select((c) => ({ cnt: expr.count(fn(c)) })) : this.select(() => ({ cnt: expr.count() }));
874
- const result = await countQr.single();
875
- return (result == null ? void 0 : result.cnt) ?? 0;
876
- }
877
- /**
878
- * Check whether data matching the conditions exists
879
- *
880
- * @returns true if exists, false otherwise
881
- *
882
- * @example
883
- * ```typescript
884
- * const hasAdmin = await db.user()
885
- * .where((u) => [expr.eq(u.role, "admin")])
886
- * .exists();
887
- * ```
888
- */
889
- async exists() {
890
- const count = await this.count();
891
- return count > 0;
892
- }
893
- getSelectQueryDef() {
894
- var _a, _b, _c, _d;
895
- return obj.clearUndefined({
896
- type: "select",
897
- from: this._buildFromDef(),
898
- as: this.meta.as,
899
- select: this.meta.isCustomColumns ? this._buildSelectDef(this.meta.columns, "") : void 0,
900
- distinct: this.meta.distinct,
901
- top: this.meta.top,
902
- lock: this.meta.lock,
903
- where: (_a = this.meta.where) == null ? void 0 : _a.map((w) => w.expr),
904
- joins: this.meta.joins ? this._buildJoinDefs(this.meta.joins) : void 0,
905
- orderBy: (_b = this.meta.orderBy) == null ? void 0 : _b.map((o) => o[1] ? [o[0].expr, o[1]] : [o[0].expr]),
906
- limit: this.meta.limit,
907
- groupBy: (_c = this.meta.groupBy) == null ? void 0 : _c.map((g) => g.expr),
908
- having: (_d = this.meta.having) == null ? void 0 : _d.map((w) => w.expr),
909
- with: this.meta.with ? {
910
- name: this.meta.with.name,
911
- base: this.meta.with.base.getSelectQueryDef(),
912
- recursive: this.meta.with.recursive.getSelectQueryDef()
913
- } : void 0
914
- });
915
- }
916
- _buildFromDef() {
917
- const from = this.meta.from;
918
- if (from instanceof TableBuilder || from instanceof ViewBuilder) {
919
- return this.meta.db.getQueryDefObjectName(from);
920
- } else if (from instanceof Queryable) {
921
- return from.getSelectQueryDef();
922
- } else if (Array.isArray(from)) {
923
- return from.map((qr) => qr.getSelectQueryDef());
963
+ _buildFromDef() {
964
+ const from = this.meta.from;
965
+ if (from instanceof TableBuilder || from instanceof ViewBuilder) {
966
+ return this.meta.db.getQueryDefObjectName(from);
967
+ }
968
+ else if (from instanceof Queryable) {
969
+ return from.getSelectQueryDef();
970
+ }
971
+ else if (Array.isArray(from)) {
972
+ return from.map((qr) => qr.getSelectQueryDef());
973
+ }
974
+ return from;
924
975
  }
925
- return from;
926
- }
927
- _buildSelectDef(columns, prefix) {
928
- const result = {};
929
- for (const [key, val] of Object.entries(columns)) {
930
- const fullKey = prefix ? `${prefix}.${key}` : key;
931
- if (val instanceof ExprUnit) {
932
- result[fullKey] = val.expr;
933
- } else if (Array.isArray(val)) {
934
- if (val.length > 0) {
935
- Object.assign(result, this._buildSelectDef(val[0], fullKey));
976
+ _buildSelectDef(columns, prefix) {
977
+ const result = {};
978
+ for (const [key, val] of Object.entries(columns)) {
979
+ const fullKey = prefix ? `${prefix}.${key}` : key;
980
+ if (val instanceof ExprUnit) {
981
+ result[fullKey] = val.expr;
982
+ }
983
+ else if (Array.isArray(val)) {
984
+ if (val.length > 0) {
985
+ Object.assign(result, this._buildSelectDef(val[0], fullKey));
986
+ }
987
+ }
988
+ else if (typeof val === "object" && val != null) {
989
+ Object.assign(result, this._buildSelectDef(val, fullKey));
990
+ }
991
+ else {
992
+ // 일반 값 (string, number, boolean 등) — Expr로 변환
993
+ result[fullKey] = expr.toExpr(val);
994
+ }
936
995
  }
937
- } else if (typeof val === "object" && val != null) {
938
- Object.assign(result, this._buildSelectDef(val, fullKey));
939
- } else {
940
- result[fullKey] = expr.toExpr(val);
941
- }
996
+ return result;
942
997
  }
943
- return result;
944
- }
945
- _buildJoinDefs(joins) {
946
- const result = [];
947
- for (const join of joins) {
948
- const joinQr = join.queryable;
949
- const selectDef = joinQr.getSelectQueryDef();
950
- const joinDef = {
951
- ...selectDef,
952
- as: joinQr.meta.as,
953
- isSingle: join.isSingle
954
- };
955
- result.push(joinDef);
998
+ _buildJoinDefs(joins) {
999
+ const result = [];
1000
+ for (const join of joins) {
1001
+ const joinQr = join.queryable;
1002
+ const selectDef = joinQr.getSelectQueryDef();
1003
+ const joinDef = {
1004
+ ...selectDef,
1005
+ as: joinQr.meta.as,
1006
+ isSingle: join.isSingle,
1007
+ };
1008
+ result.push(joinDef);
1009
+ }
1010
+ return result;
956
1011
  }
957
- return result;
958
- }
959
- getResultMeta(outputColumns) {
960
- const columns = {};
961
- const joins = {};
962
- const buildResultMeta = (cols, prefix) => {
963
- for (const [key, val] of Object.entries(cols)) {
964
- const fullKey = prefix ? `${prefix}.${key}` : key;
965
- if (outputColumns && !outputColumns.includes(fullKey)) continue;
966
- if (val instanceof ExprUnit) {
967
- columns[fullKey] = val.dataType;
968
- } else if (Array.isArray(val)) {
969
- if (val.length > 0) {
970
- joins[fullKey] = { isSingle: false };
971
- buildResultMeta(val[0], fullKey);
972
- }
973
- } else if (typeof val === "object") {
974
- joins[fullKey] = { isSingle: true };
975
- buildResultMeta(val, fullKey);
1012
+ getResultMeta(outputColumns) {
1013
+ const columns = {};
1014
+ const joins = {};
1015
+ const buildResultMeta = (cols, prefix) => {
1016
+ for (const [key, val] of Object.entries(cols)) {
1017
+ const fullKey = prefix ? `${prefix}.${key}` : key;
1018
+ if (outputColumns && !outputColumns.includes(fullKey))
1019
+ continue;
1020
+ if (val instanceof ExprUnit) {
1021
+ // 원시 column
1022
+ columns[fullKey] = val.dataType;
1023
+ }
1024
+ else if (Array.isArray(val)) {
1025
+ // 배열 (1:N 관계)
1026
+ if (val.length > 0) {
1027
+ joins[fullKey] = { isSingle: false };
1028
+ buildResultMeta(val[0], fullKey);
1029
+ }
1030
+ }
1031
+ else if (typeof val === "object") {
1032
+ // 단일 객체 (N:1, 1:1 관계)
1033
+ joins[fullKey] = { isSingle: true };
1034
+ buildResultMeta(val, fullKey);
1035
+ }
1036
+ }
1037
+ };
1038
+ buildResultMeta(this.meta.columns, "");
1039
+ return { columns, joins };
1040
+ }
1041
+ async insert(records, outputColumns) {
1042
+ if (records.length === 0) {
1043
+ return outputColumns ? [] : undefined;
1044
+ }
1045
+ // MSSQL의 1000행 제한을 위해 청크로 분할
1046
+ const CHUNK_SIZE = 1000;
1047
+ const allResults = [];
1048
+ for (let i = 0; i < records.length; i += CHUNK_SIZE) {
1049
+ const chunk = records.slice(i, i + CHUNK_SIZE);
1050
+ const results = await this.meta.db.executeDefs([this.getInsertQueryDef(chunk, outputColumns)], outputColumns ? [this.getResultMeta(outputColumns)] : undefined);
1051
+ if (outputColumns) {
1052
+ allResults.push(...results[0]);
1053
+ }
1054
+ }
1055
+ if (outputColumns) {
1056
+ return allResults;
976
1057
  }
977
- }
978
- };
979
- buildResultMeta(this.meta.columns, "");
980
- return { columns, joins };
981
- }
982
- async insert(records, outputColumns) {
983
- if (records.length === 0) {
984
- return outputColumns ? [] : void 0;
985
1058
  }
986
- const CHUNK_SIZE = 1e3;
987
- const allResults = [];
988
- for (let i = 0; i < records.length; i += CHUNK_SIZE) {
989
- const chunk = records.slice(i, i + CHUNK_SIZE);
990
- const results = await this.meta.db.executeDefs(
991
- [this.getInsertQueryDef(chunk, outputColumns)],
992
- outputColumns ? [this.getResultMeta(outputColumns)] : void 0
993
- );
994
- if (outputColumns) {
995
- allResults.push(...results[0]);
996
- }
1059
+ async insertIfNotExists(record, outputColumns) {
1060
+ const results = await this.meta.db.executeDefs([this.getInsertIfNotExistsQueryDef(record)], outputColumns ? [this.getResultMeta(outputColumns)] : undefined);
1061
+ if (outputColumns) {
1062
+ return results[0][0];
1063
+ }
997
1064
  }
998
- if (outputColumns) {
999
- return allResults;
1065
+ async insertInto(targetTable, outputColumns) {
1066
+ const results = await this.meta.db.executeDefs([this.getInsertIntoQueryDef(targetTable)], outputColumns ? [this.getResultMeta(outputColumns)] : undefined);
1067
+ if (outputColumns) {
1068
+ return results[0];
1069
+ }
1000
1070
  }
1001
- }
1002
- async insertIfNotExists(record, outputColumns) {
1003
- const results = await this.meta.db.executeDefs(
1004
- [this.getInsertIfNotExistsQueryDef(record)],
1005
- outputColumns ? [this.getResultMeta(outputColumns)] : void 0
1006
- );
1007
- if (outputColumns) {
1008
- return results[0][0];
1071
+ getInsertQueryDef(records, outputColumns) {
1072
+ const from = this.meta.from;
1073
+ const outputDef = this._getCudOutputDef();
1074
+ // AI column에 명시적 값이 있으면 overrideIdentity 설정
1075
+ const overrideIdentity = outputDef.aiColName != null &&
1076
+ records.some((r) => r[outputDef.aiColName] !== undefined);
1077
+ return obj.clearUndefined({
1078
+ type: "insert",
1079
+ table: this.meta.db.getQueryDefObjectName(from),
1080
+ records,
1081
+ overrideIdentity: overrideIdentity || undefined,
1082
+ output: outputColumns
1083
+ ? {
1084
+ columns: outputColumns,
1085
+ pkColNames: outputDef.pkColNames,
1086
+ aiColName: outputDef.aiColName,
1087
+ }
1088
+ : undefined,
1089
+ });
1009
1090
  }
1010
- }
1011
- async insertInto(targetTable, outputColumns) {
1012
- const results = await this.meta.db.executeDefs(
1013
- [this.getInsertIntoQueryDef(targetTable)],
1014
- outputColumns ? [this.getResultMeta(outputColumns)] : void 0
1015
- );
1016
- if (outputColumns) {
1017
- return results[0];
1091
+ getInsertIfNotExistsQueryDef(record, outputColumns) {
1092
+ const from = this.meta.from;
1093
+ const outputDef = this._getCudOutputDef();
1094
+ const { select: _, ...existsSelectQuery } = this.getSelectQueryDef();
1095
+ return obj.clearUndefined({
1096
+ type: "insertIfNotExists",
1097
+ table: this.meta.db.getQueryDefObjectName(from),
1098
+ record,
1099
+ existsSelectQuery,
1100
+ output: outputColumns
1101
+ ? {
1102
+ columns: outputColumns,
1103
+ pkColNames: outputDef.pkColNames,
1104
+ aiColName: outputDef.aiColName,
1105
+ }
1106
+ : undefined,
1107
+ });
1018
1108
  }
1019
- }
1020
- getInsertQueryDef(records, outputColumns) {
1021
- const from = this.meta.from;
1022
- const outputDef = this._getCudOutputDef();
1023
- const overrideIdentity = outputDef.aiColName != null && records.some((r) => r[outputDef.aiColName] !== void 0);
1024
- return obj.clearUndefined({
1025
- type: "insert",
1026
- table: this.meta.db.getQueryDefObjectName(from),
1027
- records,
1028
- overrideIdentity: overrideIdentity || void 0,
1029
- output: outputColumns ? {
1030
- columns: outputColumns,
1031
- pkColNames: outputDef.pkColNames,
1032
- aiColName: outputDef.aiColName
1033
- } : void 0
1034
- });
1035
- }
1036
- getInsertIfNotExistsQueryDef(record, outputColumns) {
1037
- const from = this.meta.from;
1038
- const outputDef = this._getCudOutputDef();
1039
- const { select: _, ...existsSelectQuery } = this.getSelectQueryDef();
1040
- return obj.clearUndefined({
1041
- type: "insertIfNotExists",
1042
- table: this.meta.db.getQueryDefObjectName(from),
1043
- record,
1044
- existsSelectQuery,
1045
- output: outputColumns ? {
1046
- columns: outputColumns,
1047
- pkColNames: outputDef.pkColNames,
1048
- aiColName: outputDef.aiColName
1049
- } : void 0
1050
- });
1051
- }
1052
- getInsertIntoQueryDef(targetTable, outputColumns) {
1053
- const outputDef = this._getCudOutputDef();
1054
- return obj.clearUndefined({
1055
- type: "insertInto",
1056
- table: this.meta.db.getQueryDefObjectName(targetTable),
1057
- recordsSelectQuery: this.getSelectQueryDef(),
1058
- output: outputColumns ? {
1059
- columns: outputColumns,
1060
- pkColNames: outputDef.pkColNames,
1061
- aiColName: outputDef.aiColName
1062
- } : void 0
1063
- });
1064
- }
1065
- async update(recordFwd, outputColumns) {
1066
- const results = await this.meta.db.executeDefs(
1067
- [this.getUpdateQueryDef(recordFwd, outputColumns)],
1068
- outputColumns ? [this.getResultMeta(outputColumns)] : void 0
1069
- );
1070
- if (outputColumns) {
1071
- return results[0];
1109
+ getInsertIntoQueryDef(targetTable, outputColumns) {
1110
+ const outputDef = this._getCudOutputDef();
1111
+ return obj.clearUndefined({
1112
+ type: "insertInto",
1113
+ table: this.meta.db.getQueryDefObjectName(targetTable),
1114
+ recordsSelectQuery: this.getSelectQueryDef(),
1115
+ output: outputColumns
1116
+ ? {
1117
+ columns: outputColumns,
1118
+ pkColNames: outputDef.pkColNames,
1119
+ aiColName: outputDef.aiColName,
1120
+ }
1121
+ : undefined,
1122
+ });
1072
1123
  }
1073
- }
1074
- async delete(outputColumns) {
1075
- const results = await this.meta.db.executeDefs(
1076
- [this.getDeleteQueryDef(outputColumns)],
1077
- outputColumns ? [this.getResultMeta(outputColumns)] : void 0
1078
- );
1079
- if (outputColumns) {
1080
- return results[0];
1124
+ async update(recordFwd, outputColumns) {
1125
+ const results = await this.meta.db.executeDefs([this.getUpdateQueryDef(recordFwd, outputColumns)], outputColumns ? [this.getResultMeta(outputColumns)] : undefined);
1126
+ if (outputColumns) {
1127
+ return results[0];
1128
+ }
1081
1129
  }
1082
- }
1083
- getUpdateQueryDef(recordFwd, outputColumns) {
1084
- var _a;
1085
- const from = this.meta.from;
1086
- const outputDef = this._getCudOutputDef();
1087
- return obj.clearUndefined({
1088
- type: "update",
1089
- table: this.meta.db.getQueryDefObjectName(from),
1090
- as: this.meta.as,
1091
- record: this._buildSelectDef(recordFwd(this.meta.columns), ""),
1092
- top: this.meta.top,
1093
- where: (_a = this.meta.where) == null ? void 0 : _a.map((w) => w.expr),
1094
- joins: this.meta.joins ? this._buildJoinDefs(this.meta.joins) : void 0,
1095
- limit: this.meta.limit,
1096
- output: outputColumns ? {
1097
- columns: outputColumns,
1098
- pkColNames: outputDef.pkColNames,
1099
- aiColName: outputDef.aiColName
1100
- } : void 0
1101
- });
1102
- }
1103
- getDeleteQueryDef(outputColumns) {
1104
- var _a;
1105
- const from = this.meta.from;
1106
- const outputDef = this._getCudOutputDef();
1107
- return obj.clearUndefined({
1108
- type: "delete",
1109
- table: this.meta.db.getQueryDefObjectName(from),
1110
- as: this.meta.as,
1111
- top: this.meta.top,
1112
- where: (_a = this.meta.where) == null ? void 0 : _a.map((w) => w.expr),
1113
- joins: this.meta.joins ? this._buildJoinDefs(this.meta.joins) : void 0,
1114
- limit: this.meta.limit,
1115
- output: outputColumns ? {
1116
- columns: outputColumns,
1117
- pkColNames: outputDef.pkColNames,
1118
- aiColName: outputDef.aiColName
1119
- } : void 0
1120
- });
1121
- }
1122
- async upsert(updateFnOrInsertFn, insertFnOrOutputColumns, outputColumns) {
1123
- const updateRecordFn = updateFnOrInsertFn;
1124
- const insertRecordFn = insertFnOrOutputColumns instanceof Function ? insertFnOrOutputColumns : updateFnOrInsertFn;
1125
- const realOutputColumns = insertFnOrOutputColumns instanceof Function ? outputColumns : insertFnOrOutputColumns;
1126
- const results = await this.meta.db.executeDefs(
1127
- [this.getUpsertQueryDef(updateRecordFn, insertRecordFn, realOutputColumns)],
1128
- [realOutputColumns ? this.getResultMeta(realOutputColumns) : void 0]
1129
- );
1130
- if (realOutputColumns) {
1131
- return results[0];
1130
+ async delete(outputColumns) {
1131
+ const results = await this.meta.db.executeDefs([this.getDeleteQueryDef(outputColumns)], outputColumns ? [this.getResultMeta(outputColumns)] : undefined);
1132
+ if (outputColumns) {
1133
+ return results[0];
1134
+ }
1132
1135
  }
1133
- }
1134
- getUpsertQueryDef(updateRecordFn, insertRecordFn, outputColumns) {
1135
- const from = this.meta.from;
1136
- const outputDef = this._getCudOutputDef();
1137
- const { select: _sel, ...existsSelectQuery } = this.getSelectQueryDef();
1138
- const updateQrRecord = updateRecordFn(this.meta.columns);
1139
- const updateRecord = {};
1140
- for (const [key, value] of Object.entries(updateQrRecord)) {
1141
- updateRecord[key] = expr.toExpr(value);
1136
+ getUpdateQueryDef(recordFwd, outputColumns) {
1137
+ const from = this.meta.from;
1138
+ const outputDef = this._getCudOutputDef();
1139
+ return obj.clearUndefined({
1140
+ type: "update",
1141
+ table: this.meta.db.getQueryDefObjectName(from),
1142
+ as: this.meta.as,
1143
+ record: this._buildSelectDef(recordFwd(this.meta.columns), ""),
1144
+ top: this.meta.top,
1145
+ where: this.meta.where?.map((w) => w.expr),
1146
+ joins: this.meta.joins ? this._buildJoinDefs(this.meta.joins) : undefined,
1147
+ limit: this.meta.limit,
1148
+ output: outputColumns
1149
+ ? {
1150
+ columns: outputColumns,
1151
+ pkColNames: outputDef.pkColNames,
1152
+ aiColName: outputDef.aiColName,
1153
+ }
1154
+ : undefined,
1155
+ });
1142
1156
  }
1143
- const insertRecordRaw = insertRecordFn(updateQrRecord);
1144
- const insertRecord = Object.fromEntries(
1145
- Object.entries(insertRecordRaw).map(([key, value]) => [key, expr.toExpr(value)])
1146
- );
1147
- return obj.clearUndefined({
1148
- type: "upsert",
1149
- table: this.meta.db.getQueryDefObjectName(from),
1150
- existsSelectQuery,
1151
- updateRecord,
1152
- insertRecord,
1153
- output: outputColumns ? {
1154
- columns: outputColumns,
1155
- pkColNames: outputDef.pkColNames,
1156
- aiColName: outputDef.aiColName
1157
- } : void 0
1158
- });
1159
- }
1160
- //#endregion
1161
- //#region ========== DDL Helper ==========
1162
- /**
1163
- * FK constraint on/off (can be used within a transaction)
1164
- */
1165
- async switchFk(enabled) {
1166
- const from = this.meta.from;
1167
- if (!(from instanceof TableBuilder) && !(from instanceof ViewBuilder)) {
1168
- throw new Error(
1169
- "switchFk can only be used on TableBuilder or ViewBuilder based queryables."
1170
- );
1157
+ getDeleteQueryDef(outputColumns) {
1158
+ const from = this.meta.from;
1159
+ const outputDef = this._getCudOutputDef();
1160
+ return obj.clearUndefined({
1161
+ type: "delete",
1162
+ table: this.meta.db.getQueryDefObjectName(from),
1163
+ as: this.meta.as,
1164
+ top: this.meta.top,
1165
+ where: this.meta.where?.map((w) => w.expr),
1166
+ joins: this.meta.joins ? this._buildJoinDefs(this.meta.joins) : undefined,
1167
+ limit: this.meta.limit,
1168
+ output: outputColumns
1169
+ ? {
1170
+ columns: outputColumns,
1171
+ pkColNames: outputDef.pkColNames,
1172
+ aiColName: outputDef.aiColName,
1173
+ }
1174
+ : undefined,
1175
+ });
1176
+ }
1177
+ async upsert(updateFnOrInsertFn, insertFnOrOutputColumns, outputColumns) {
1178
+ const updateRecordFn = updateFnOrInsertFn;
1179
+ const insertRecordFn = (insertFnOrOutputColumns instanceof Function ? insertFnOrOutputColumns : updateFnOrInsertFn);
1180
+ const realOutputColumns = insertFnOrOutputColumns instanceof Function ? outputColumns : insertFnOrOutputColumns;
1181
+ const results = await this.meta.db.executeDefs([this.getUpsertQueryDef(updateRecordFn, insertRecordFn, realOutputColumns)], [realOutputColumns ? this.getResultMeta(realOutputColumns) : undefined]);
1182
+ if (realOutputColumns) {
1183
+ return results[0];
1184
+ }
1185
+ }
1186
+ getUpsertQueryDef(updateRecordFn, insertRecordFn, outputColumns) {
1187
+ const from = this.meta.from;
1188
+ const outputDef = this._getCudOutputDef();
1189
+ const { select: _sel, ...existsSelectQuery } = this.getSelectQueryDef();
1190
+ // updateRecord 생성
1191
+ const updateQrRecord = updateRecordFn(this.meta.columns);
1192
+ const updateRecord = {};
1193
+ for (const [key, value] of Object.entries(updateQrRecord)) {
1194
+ updateRecord[key] = expr.toExpr(value);
1195
+ }
1196
+ // insertRecord 생성 (updateRecordRaw를 두 번째 인자로 전달)
1197
+ const insertRecordRaw = insertRecordFn(updateQrRecord);
1198
+ const insertRecord = Object.fromEntries(Object.entries(insertRecordRaw).map(([key, value]) => [key, expr.toExpr(value)]));
1199
+ return obj.clearUndefined({
1200
+ type: "upsert",
1201
+ table: this.meta.db.getQueryDefObjectName(from),
1202
+ existsSelectQuery,
1203
+ updateRecord,
1204
+ insertRecord,
1205
+ output: outputColumns
1206
+ ? {
1207
+ columns: outputColumns,
1208
+ pkColNames: outputDef.pkColNames,
1209
+ aiColName: outputDef.aiColName,
1210
+ }
1211
+ : undefined,
1212
+ });
1213
+ }
1214
+ //#endregion
1215
+ //#region ========== DDL Helper ==========
1216
+ /**
1217
+ * FK 제약조건 활성화/비활성화 (트랜잭션 내에서 사용 가능)
1218
+ */
1219
+ async switchFk(enabled) {
1220
+ const from = this.meta.from;
1221
+ if (!(from instanceof TableBuilder) && !(from instanceof ViewBuilder)) {
1222
+ throw new Error("switchFk는 TableBuilder 또는 ViewBuilder 기반 queryable에서만 사용할 수 있습니다.");
1223
+ }
1224
+ await this.meta.db.switchFk(this.meta.db.getQueryDefObjectName(from), enabled);
1171
1225
  }
1172
- await this.meta.db.switchFk(this.meta.db.getQueryDefObjectName(from), enabled);
1173
- }
1174
- //#endregion
1175
- //#region ========== CUD Common ==========
1176
- _getCudOutputDef() {
1177
- const from = this.meta.from;
1178
- if (from instanceof TableBuilder) {
1179
- if (from.meta.columns == null) {
1180
- throw new Error(`Table '${from.meta.name}' has no Column definition.`);
1181
- }
1182
- let aiColName;
1183
- for (const [key, col] of Object.entries(from.meta.columns)) {
1184
- if (col.meta.autoIncrement) {
1185
- aiColName = key;
1226
+ //#endregion
1227
+ //#region ========== CUD Common ==========
1228
+ _getCudOutputDef() {
1229
+ const from = this.meta.from;
1230
+ if (from instanceof TableBuilder) {
1231
+ if (from.meta.columns == null) {
1232
+ throw new Error(`테이블 '${from.meta.name}'에 Column 정의가 없습니다.`);
1233
+ }
1234
+ let aiColName;
1235
+ for (const [key, col] of Object.entries(from.meta.columns)) {
1236
+ if (col.meta.autoIncrement) {
1237
+ aiColName = key;
1238
+ }
1239
+ }
1240
+ return {
1241
+ pkColNames: from.meta.primaryKey ?? [],
1242
+ aiColName,
1243
+ };
1186
1244
  }
1187
- }
1188
- return {
1189
- pkColNames: from.meta.primaryKey ?? [],
1190
- aiColName
1191
- };
1245
+ throw new Error("CUD 작업은 TableBuilder 기반 queryable에서만 사용할 수 있습니다.");
1192
1246
  }
1193
- throw new Error("CUD operations can only be used on TableBuilder-based queryables.");
1194
- }
1195
- //#endregion
1196
1247
  }
1197
- function getMatchedPrimaryKeys(fkCols, targetTable) {
1198
- const pk = targetTable.meta.primaryKey;
1199
- if (pk == null || fkCols.length !== pk.length) {
1200
- throw new Error(
1201
- `FK/PK column count mismatch (target: ${targetTable.meta.name}, FK: ${fkCols.length}, PK: ${(pk == null ? void 0 : pk.length) ?? 0})`
1202
- );
1203
- }
1204
- return pk;
1248
+ //#region ========== Helper Functions ==========
1249
+ /**
1250
+ * Match FK column array with the target Table's PK and return PK column name array
1251
+ *
1252
+ * @param fkCols - FK column name array
1253
+ * @param targetTable - Target Table builder being referenced
1254
+ * @returns Matched PK column name array
1255
+ * @throws When FK/PK column count mismatch
1256
+ */
1257
+ export function getMatchedPrimaryKeys(fkCols, targetTable) {
1258
+ const pk = targetTable.meta.primaryKey;
1259
+ if (pk == null || fkCols.length !== pk.length) {
1260
+ throw new Error(`FK/PK column count mismatch (target: ${targetTable.meta.name}, FK: ${fkCols.length}, PK: ${pk?.length ?? 0})`);
1261
+ }
1262
+ return pk;
1205
1263
  }
1264
+ /**
1265
+ * Common helper to transform nested columns structure to a new alias
1266
+ *
1267
+ * When wrapping as Subquery/JOIN, transforms existing alias to new alias while
1268
+ * keeping nested keys (posts.userId) as flattened keys.
1269
+ *
1270
+ * e.g.: If the path of posts[0].userId column is ["T1.posts", "userId"],
1271
+ * transforming to new alias "T2" yields ["T2", "posts.userId"].
1272
+ *
1273
+ * @param columns - Column record to transform
1274
+ * @param alias - New Table alias (e.g., "T2")
1275
+ * @param keyPrefix - Current nested path (for recursive calls, default "")
1276
+ * @returns Transformed column record
1277
+ */
1206
1278
  function transformColumnsAlias(columns, alias, keyPrefix = "") {
1207
- const result = {};
1208
- for (const [key, value] of Object.entries(columns)) {
1209
- const fullKey = keyPrefix ? `${keyPrefix}.${key}` : key;
1210
- if (value instanceof ExprUnit) {
1211
- result[key] = expr.col(value.dataType, alias, fullKey);
1212
- } else if (Array.isArray(value)) {
1213
- if (value.length > 0) {
1214
- result[key] = [
1215
- transformColumnsAlias(value[0], alias, fullKey)
1216
- ];
1217
- }
1218
- } else if (typeof value === "object" && value != null) {
1219
- result[key] = transformColumnsAlias(value, alias, fullKey);
1220
- } else {
1221
- result[key] = value;
1279
+ const result = {};
1280
+ for (const [key, value] of Object.entries(columns)) {
1281
+ const fullKey = keyPrefix ? `${keyPrefix}.${key}` : key;
1282
+ if (value instanceof ExprUnit) {
1283
+ result[key] = expr.col(value.dataType, alias, fullKey);
1284
+ }
1285
+ else if (Array.isArray(value)) {
1286
+ if (value.length > 0) {
1287
+ result[key] = [
1288
+ transformColumnsAlias(value[0], alias, fullKey),
1289
+ ];
1290
+ }
1291
+ }
1292
+ else if (typeof value === "object" && value != null) {
1293
+ result[key] = transformColumnsAlias(value, alias, fullKey);
1294
+ }
1295
+ else {
1296
+ result[key] = value;
1297
+ }
1222
1298
  }
1223
- }
1224
- return result;
1299
+ return result;
1225
1300
  }
1226
- const PATH_SYMBOL = /* @__PURE__ */ Symbol("path");
1301
+ const PATH_SYMBOL = Symbol("path");
1302
+ /**
1303
+ * PathProxy 인스턴스 생성
1304
+ * Proxy를 사용하여 속성 접근을 가로채고 경로를 수집
1305
+ */
1227
1306
  function createPathProxy(path = []) {
1228
- return new Proxy({}, {
1229
- get(_, prop) {
1230
- if (prop === PATH_SYMBOL) return path;
1231
- if (typeof prop === "symbol") return void 0;
1232
- return createPathProxy([...path, prop]);
1233
- }
1234
- });
1307
+ return new Proxy({}, {
1308
+ get(_, prop) {
1309
+ if (prop === PATH_SYMBOL)
1310
+ return path;
1311
+ if (typeof prop === "symbol")
1312
+ return undefined;
1313
+ return createPathProxy([...path, prop]);
1314
+ },
1315
+ });
1235
1316
  }
1236
- function queryable(db, tableOrView, as) {
1237
- return () => {
1238
- const finalAs = as ?? db.getNextAlias();
1239
- if (tableOrView instanceof TableBuilder && tableOrView.meta.columns != null) {
1240
- const columnDefs = tableOrView.meta.columns;
1241
- return new Queryable({
1242
- db,
1243
- from: tableOrView,
1244
- as: finalAs,
1245
- columns: Object.fromEntries(
1246
- Object.entries(columnDefs).map(([key, colDef]) => [
1247
- key,
1248
- expr.col(colDef.meta.type, finalAs, key)
1249
- ])
1250
- )
1251
- });
1252
- }
1253
- if (tableOrView instanceof ViewBuilder && tableOrView.meta.viewFn != null) {
1254
- const baseQr = tableOrView.meta.viewFn(db);
1255
- return new Queryable({
1256
- db,
1257
- from: tableOrView,
1258
- as: finalAs,
1259
- columns: transformColumnsAlias(baseQr.meta.columns, finalAs)
1260
- });
1261
- }
1262
- throw new Error(`Invalid Table/View Metadata: ${tableOrView.meta.name}`);
1263
- };
1317
+ //#endregion
1318
+ /**
1319
+ * Table 또는 View용 Queryable factory 함수 생성
1320
+ *
1321
+ * DbContext에서 Table/View별 getter를 정의할 때 사용
1322
+ *
1323
+ * @param db - DbContext 인스턴스
1324
+ * @param tableOrView - TableBuilder 또는 ViewBuilder 인스턴스
1325
+ * @param as - Alias 지정 (선택, 미지정 시 자동 생성)
1326
+ * @returns Queryable을 반환하는 factory 함수
1327
+ *
1328
+ * @example
1329
+ * ```typescript
1330
+ * class AppDbContext extends DbContext {
1331
+ * // 호출할 때마다 새 alias가 할당됨
1332
+ * user = queryable(this, User);
1333
+ *
1334
+ * // 사용 예시
1335
+ * async getActiveUsers() {
1336
+ * return this.user()
1337
+ * .where((u) => [expr.eq(u.isActive, true)])
1338
+ * .execute();
1339
+ * }
1340
+ * }
1341
+ * ```
1342
+ */
1343
+ export function queryable(db, tableOrView, as) {
1344
+ return () => {
1345
+ // as가 미지정이면 db.getNextAlias() 사용 (카운터 증가)
1346
+ // as가 지정되면 그대로 사용 (카운터 증가 없음)
1347
+ const finalAs = as ?? db.getNextAlias();
1348
+ // TableBuilder + columns
1349
+ if (tableOrView instanceof TableBuilder && tableOrView.meta.columns != null) {
1350
+ const columnDefs = tableOrView.meta.columns;
1351
+ return new Queryable({
1352
+ db,
1353
+ from: tableOrView,
1354
+ as: finalAs,
1355
+ columns: Object.fromEntries(Object.entries(columnDefs).map(([key, colDef]) => [
1356
+ key,
1357
+ expr.col(colDef.meta.type, finalAs, key),
1358
+ ])),
1359
+ });
1360
+ }
1361
+ // ViewBuilder + viewFn
1362
+ if (tableOrView instanceof ViewBuilder && tableOrView.meta.viewFn != null) {
1363
+ const baseQr = tableOrView.meta.viewFn(db);
1364
+ // TFrom을 ViewBuilder로 설정하여 반환
1365
+ return new Queryable({
1366
+ db,
1367
+ from: tableOrView,
1368
+ as: finalAs,
1369
+ columns: transformColumnsAlias(baseQr.meta.columns, finalAs),
1370
+ });
1371
+ }
1372
+ throw new Error(`Invalid Table/View Metadata: ${tableOrView.meta.name}`);
1373
+ };
1264
1374
  }
1265
- export {
1266
- Queryable,
1267
- getMatchedPrimaryKeys,
1268
- queryable
1269
- };
1270
- //# sourceMappingURL=queryable.js.map
1375
+ //#endregion
1376
+ //# sourceMappingURL=queryable.js.map