@simplysm/orm-common 13.0.68 → 13.0.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/README.md +54 -1447
  2. package/dist/create-db-context.d.ts +10 -10
  3. package/dist/create-db-context.js +9 -9
  4. package/dist/create-db-context.js.map +1 -1
  5. package/dist/ddl/column-ddl.d.ts +4 -4
  6. package/dist/ddl/initialize.d.ts +17 -17
  7. package/dist/ddl/initialize.js +2 -2
  8. package/dist/ddl/initialize.js.map +1 -1
  9. package/dist/ddl/relation-ddl.d.ts +6 -6
  10. package/dist/ddl/schema-ddl.d.ts +4 -4
  11. package/dist/ddl/table-ddl.d.ts +24 -24
  12. package/dist/ddl/table-ddl.js +4 -4
  13. package/dist/ddl/table-ddl.js.map +1 -1
  14. package/dist/errors/db-transaction-error.d.ts +15 -15
  15. package/dist/errors/db-transaction-error.d.ts.map +1 -1
  16. package/dist/exec/executable.d.ts +23 -23
  17. package/dist/exec/executable.js +3 -3
  18. package/dist/exec/executable.js.map +1 -1
  19. package/dist/exec/queryable.d.ts +160 -160
  20. package/dist/exec/queryable.js +119 -119
  21. package/dist/exec/queryable.js.map +1 -1
  22. package/dist/exec/search-parser.d.ts +37 -37
  23. package/dist/exec/search-parser.d.ts.map +1 -1
  24. package/dist/expr/expr-unit.d.ts +4 -4
  25. package/dist/expr/expr.d.ts +257 -257
  26. package/dist/expr/expr.js +265 -265
  27. package/dist/expr/expr.js.map +1 -1
  28. package/dist/query-builder/base/expr-renderer-base.d.ts +9 -9
  29. package/dist/query-builder/base/expr-renderer-base.js +2 -2
  30. package/dist/query-builder/base/expr-renderer-base.js.map +1 -1
  31. package/dist/query-builder/base/query-builder-base.d.ts +26 -26
  32. package/dist/query-builder/base/query-builder-base.d.ts.map +1 -1
  33. package/dist/query-builder/base/query-builder-base.js +22 -22
  34. package/dist/query-builder/base/query-builder-base.js.map +1 -1
  35. package/dist/query-builder/mssql/mssql-expr-renderer.d.ts +4 -4
  36. package/dist/query-builder/mssql/mssql-expr-renderer.d.ts.map +1 -1
  37. package/dist/query-builder/mssql/mssql-expr-renderer.js +18 -18
  38. package/dist/query-builder/mssql/mssql-expr-renderer.js.map +1 -1
  39. package/dist/query-builder/mssql/mssql-query-builder.d.ts +2 -2
  40. package/dist/query-builder/mssql/mssql-query-builder.d.ts.map +1 -1
  41. package/dist/query-builder/mssql/mssql-query-builder.js +11 -11
  42. package/dist/query-builder/mssql/mssql-query-builder.js.map +1 -1
  43. package/dist/query-builder/mysql/mysql-expr-renderer.d.ts +4 -4
  44. package/dist/query-builder/mysql/mysql-expr-renderer.d.ts.map +1 -1
  45. package/dist/query-builder/mysql/mysql-expr-renderer.js +17 -17
  46. package/dist/query-builder/mysql/mysql-expr-renderer.js.map +1 -1
  47. package/dist/query-builder/mysql/mysql-query-builder.d.ts +8 -8
  48. package/dist/query-builder/mysql/mysql-query-builder.d.ts.map +1 -1
  49. package/dist/query-builder/mysql/mysql-query-builder.js +5 -5
  50. package/dist/query-builder/mysql/mysql-query-builder.js.map +1 -1
  51. package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts +4 -4
  52. package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts.map +1 -1
  53. package/dist/query-builder/postgresql/postgresql-expr-renderer.js +17 -17
  54. package/dist/query-builder/postgresql/postgresql-expr-renderer.js.map +1 -1
  55. package/dist/query-builder/postgresql/postgresql-query-builder.d.ts +5 -5
  56. package/dist/query-builder/postgresql/postgresql-query-builder.d.ts.map +1 -1
  57. package/dist/query-builder/postgresql/postgresql-query-builder.js +8 -8
  58. package/dist/query-builder/postgresql/postgresql-query-builder.js.map +1 -1
  59. package/dist/query-builder/query-builder.d.ts +1 -1
  60. package/dist/schema/factory/column-builder.d.ts +79 -79
  61. package/dist/schema/factory/column-builder.js +42 -42
  62. package/dist/schema/factory/index-builder.d.ts +39 -39
  63. package/dist/schema/factory/index-builder.js +26 -26
  64. package/dist/schema/factory/relation-builder.d.ts +99 -99
  65. package/dist/schema/factory/relation-builder.d.ts.map +1 -1
  66. package/dist/schema/factory/relation-builder.js +38 -38
  67. package/dist/schema/procedure-builder.d.ts +49 -49
  68. package/dist/schema/procedure-builder.d.ts.map +1 -1
  69. package/dist/schema/procedure-builder.js +33 -33
  70. package/dist/schema/table-builder.d.ts +59 -59
  71. package/dist/schema/table-builder.d.ts.map +1 -1
  72. package/dist/schema/table-builder.js +43 -43
  73. package/dist/schema/view-builder.d.ts +49 -49
  74. package/dist/schema/view-builder.d.ts.map +1 -1
  75. package/dist/schema/view-builder.js +32 -32
  76. package/dist/types/column.d.ts +22 -22
  77. package/dist/types/column.js +1 -1
  78. package/dist/types/column.js.map +1 -1
  79. package/dist/types/db.d.ts +40 -40
  80. package/dist/types/expr.d.ts +59 -59
  81. package/dist/types/expr.d.ts.map +1 -1
  82. package/dist/types/query-def.d.ts +44 -44
  83. package/dist/types/query-def.d.ts.map +1 -1
  84. package/dist/utils/result-parser.d.ts +11 -11
  85. package/dist/utils/result-parser.js +3 -3
  86. package/dist/utils/result-parser.js.map +1 -1
  87. package/package.json +5 -5
  88. package/src/create-db-context.ts +20 -20
  89. package/src/ddl/column-ddl.ts +4 -4
  90. package/src/ddl/initialize.ts +259 -259
  91. package/src/ddl/relation-ddl.ts +89 -89
  92. package/src/ddl/schema-ddl.ts +4 -4
  93. package/src/ddl/table-ddl.ts +189 -189
  94. package/src/errors/db-transaction-error.ts +13 -13
  95. package/src/exec/executable.ts +25 -25
  96. package/src/exec/queryable.ts +2033 -2033
  97. package/src/exec/search-parser.ts +57 -57
  98. package/src/expr/expr-unit.ts +4 -4
  99. package/src/expr/expr.ts +2140 -2140
  100. package/src/query-builder/base/expr-renderer-base.ts +237 -237
  101. package/src/query-builder/base/query-builder-base.ts +213 -213
  102. package/src/query-builder/mssql/mssql-expr-renderer.ts +607 -607
  103. package/src/query-builder/mssql/mssql-query-builder.ts +650 -650
  104. package/src/query-builder/mysql/mysql-expr-renderer.ts +613 -613
  105. package/src/query-builder/mysql/mysql-query-builder.ts +759 -759
  106. package/src/query-builder/postgresql/postgresql-expr-renderer.ts +611 -611
  107. package/src/query-builder/postgresql/postgresql-query-builder.ts +686 -686
  108. package/src/query-builder/query-builder.ts +19 -19
  109. package/src/schema/factory/column-builder.ts +423 -423
  110. package/src/schema/factory/index-builder.ts +164 -164
  111. package/src/schema/factory/relation-builder.ts +453 -453
  112. package/src/schema/procedure-builder.ts +232 -232
  113. package/src/schema/table-builder.ts +319 -319
  114. package/src/schema/view-builder.ts +221 -221
  115. package/src/types/column.ts +188 -188
  116. package/src/types/db.ts +208 -208
  117. package/src/types/expr.ts +697 -697
  118. package/src/types/query-def.ts +513 -513
  119. package/src/utils/result-parser.ts +458 -458
  120. package/tests/db-context/create-db-context.spec.ts +224 -0
  121. package/tests/db-context/define-db-context.spec.ts +68 -0
  122. package/tests/ddl/basic.expected.ts +341 -0
  123. package/tests/ddl/basic.spec.ts +714 -0
  124. package/tests/ddl/column-builder.expected.ts +310 -0
  125. package/tests/ddl/column-builder.spec.ts +637 -0
  126. package/tests/ddl/index-builder.expected.ts +38 -0
  127. package/tests/ddl/index-builder.spec.ts +202 -0
  128. package/tests/ddl/procedure-builder.expected.ts +52 -0
  129. package/tests/ddl/procedure-builder.spec.ts +234 -0
  130. package/tests/ddl/relation-builder.expected.ts +36 -0
  131. package/tests/ddl/relation-builder.spec.ts +372 -0
  132. package/tests/ddl/table-builder.expected.ts +113 -0
  133. package/tests/ddl/table-builder.spec.ts +433 -0
  134. package/tests/ddl/view-builder.expected.ts +38 -0
  135. package/tests/ddl/view-builder.spec.ts +176 -0
  136. package/tests/dml/delete.expected.ts +96 -0
  137. package/tests/dml/delete.spec.ts +160 -0
  138. package/tests/dml/insert.expected.ts +192 -0
  139. package/tests/dml/insert.spec.ts +288 -0
  140. package/tests/dml/update.expected.ts +176 -0
  141. package/tests/dml/update.spec.ts +318 -0
  142. package/tests/dml/upsert.expected.ts +215 -0
  143. package/tests/dml/upsert.spec.ts +242 -0
  144. package/tests/errors/queryable-errors.spec.ts +177 -0
  145. package/tests/escape.spec.ts +100 -0
  146. package/tests/examples/pivot.expected.ts +211 -0
  147. package/tests/examples/pivot.spec.ts +533 -0
  148. package/tests/examples/sampling.expected.ts +69 -0
  149. package/tests/examples/sampling.spec.ts +104 -0
  150. package/tests/examples/unpivot.expected.ts +120 -0
  151. package/tests/examples/unpivot.spec.ts +226 -0
  152. package/tests/exec/search-parser.spec.ts +283 -0
  153. package/tests/executable/basic.expected.ts +18 -0
  154. package/tests/executable/basic.spec.ts +54 -0
  155. package/tests/expr/comparison.expected.ts +282 -0
  156. package/tests/expr/comparison.spec.ts +400 -0
  157. package/tests/expr/conditional.expected.ts +134 -0
  158. package/tests/expr/conditional.spec.ts +276 -0
  159. package/tests/expr/date.expected.ts +332 -0
  160. package/tests/expr/date.spec.ts +526 -0
  161. package/tests/expr/math.expected.ts +62 -0
  162. package/tests/expr/math.spec.ts +106 -0
  163. package/tests/expr/string.expected.ts +218 -0
  164. package/tests/expr/string.spec.ts +356 -0
  165. package/tests/expr/utility.expected.ts +147 -0
  166. package/tests/expr/utility.spec.ts +182 -0
  167. package/tests/select/basic.expected.ts +322 -0
  168. package/tests/select/basic.spec.ts +502 -0
  169. package/tests/select/filter.expected.ts +357 -0
  170. package/tests/select/filter.spec.ts +1068 -0
  171. package/tests/select/group.expected.ts +169 -0
  172. package/tests/select/group.spec.ts +244 -0
  173. package/tests/select/join.expected.ts +582 -0
  174. package/tests/select/join.spec.ts +805 -0
  175. package/tests/select/order.expected.ts +150 -0
  176. package/tests/select/order.spec.ts +189 -0
  177. package/tests/select/recursive-cte.expected.ts +244 -0
  178. package/tests/select/recursive-cte.spec.ts +514 -0
  179. package/tests/select/result-meta.spec.ts +270 -0
  180. package/tests/select/subquery.expected.ts +363 -0
  181. package/tests/select/subquery.spec.ts +537 -0
  182. package/tests/select/view.expected.ts +155 -0
  183. package/tests/select/view.spec.ts +235 -0
  184. package/tests/select/window.expected.ts +345 -0
  185. package/tests/select/window.spec.ts +618 -0
  186. package/tests/setup/MockExecutor.ts +18 -0
  187. package/tests/setup/TestDbContext.ts +59 -0
  188. package/tests/setup/models/Company.ts +13 -0
  189. package/tests/setup/models/Employee.ts +10 -0
  190. package/tests/setup/models/MonthlySales.ts +11 -0
  191. package/tests/setup/models/Post.ts +16 -0
  192. package/tests/setup/models/Sales.ts +10 -0
  193. package/tests/setup/models/User.ts +19 -0
  194. package/tests/setup/procedure/GetAllUsers.ts +9 -0
  195. package/tests/setup/procedure/GetUserById.ts +12 -0
  196. package/tests/setup/test-utils.ts +72 -0
  197. package/tests/setup/views/ActiveUsers.ts +8 -0
  198. package/tests/setup/views/UserSummary.ts +11 -0
  199. package/tests/types/nullable-queryable-record.spec.ts +145 -0
  200. package/tests/utils/result-parser-perf.spec.ts +210 -0
  201. package/tests/utils/result-parser.spec.ts +701 -0
  202. package/docs/expressions.md +0 -172
  203. package/docs/queries.md +0 -444
  204. package/docs/schema.md +0 -245
package/src/expr/expr.ts CHANGED
@@ -1,2140 +1,2140 @@
1
- import { ArgumentError, type DateOnly, type DateTime, type Time } from "@simplysm/core-common";
2
- import {
3
- type ColumnPrimitive,
4
- type ColumnPrimitiveMap,
5
- type ColumnPrimitiveStr,
6
- type DataType,
7
- dataTypeStrToColumnPrimitiveStr,
8
- type InferColumnPrimitiveFromDataType,
9
- inferColumnPrimitiveStr,
10
- } from "../types/column";
11
- import type { ExprInput } from "./expr-unit";
12
- import { ExprUnit, WhereExprUnit } from "./expr-unit";
13
- import type { Expr, DateSeparator, WhereExpr, WinSpec } from "../types/expr";
14
- import type { SelectQueryDef } from "../types/query-def";
15
- import type { Queryable } from "../exec/queryable";
16
-
17
- // Window Function Spec Input (사용자 API)
18
- interface WinSpecInput {
19
- partitionBy?: ExprInput<ColumnPrimitive>[];
20
- orderBy?: [ExprInput<ColumnPrimitive>, ("ASC" | "DESC")?][];
21
- }
22
-
23
- /**
24
- * Switch 표현식 빌더 인터페이스
25
- */
26
- export interface SwitchExprBuilder<TPrimitive extends ColumnPrimitive> {
27
- case(condition: WhereExprUnit, then: ExprInput<TPrimitive>): SwitchExprBuilder<TPrimitive>;
28
- default(value: ExprInput<TPrimitive>): ExprUnit<TPrimitive>;
29
- }
30
-
31
- /**
32
- * Dialect 독립적인 SQL 표현식 빌더
33
- *
34
- * SQL 문자열이 아닌 JSON AST(Expr) 생성하여, QueryBuilder
35
- * DBMS(MySQL, MSSQL, PostgreSQL)에 맞게 변환
36
- *
37
- * @example
38
- * ```typescript
39
- * // WHERE 조건
40
- * db.user().where((u) => [
41
- * expr.eq(u.status, "active"),
42
- * expr.gt(u.age, 18),
43
- * ])
44
- *
45
- * // SELECT 표현식
46
- * db.user().select((u) => ({
47
- * name: expr.concat(u.firstName, " ", u.lastName),
48
- * age: expr.dateDiff("year", u.birthDate, expr.val("DateOnly", DateOnly.today())),
49
- * }))
50
- *
51
- * // 집계 함수
52
- * db.order().groupBy((o) => o.userId).select((o) => ({
53
- * userId: o.userId,
54
- * total: expr.sum(o.amount),
55
- * }))
56
- * ```
57
- *
58
- * @see {@link Queryable} 쿼리 빌더 클래스
59
- * @see {@link ExprUnit} 표현식 래퍼 클래스
60
- */
61
- export const expr = {
62
- //#region ========== 생성 ==========
63
-
64
- /**
65
- * 리터럴 값을 ExprUnit으로 래핑
66
- *
67
- * dataType에 맞는 base 타입으로 widening하여 리터럴 타입 제거
68
- *
69
- * @param dataType - 값의 데이터 타입 ("string", "number", "boolean", "DateTime", "DateOnly", "Time", "Uuid", "Buffer")
70
- * @param value - 래핑할 (undefined 허용)
71
- * @returns 래핑된 ExprUnit 인스턴스
72
- *
73
- * @example
74
- * ```typescript
75
- * // 문자열
76
- * expr.val("string", "active")
77
- *
78
- * // 숫자
79
- * expr.val("number", 100)
80
- *
81
- * // 날짜
82
- * expr.val("DateOnly", DateOnly.today())
83
- *
84
- * // undefined
85
- * expr.val("string", undefined)
86
- * ```
87
- */
88
- val<TStr extends ColumnPrimitiveStr, T extends ColumnPrimitiveMap[TStr] | undefined>(
89
- dataType: TStr,
90
- value: T,
91
- ): ExprUnit<
92
- T extends undefined ? ColumnPrimitiveMap[TStr] | undefined : ColumnPrimitiveMap[TStr]
93
- > {
94
- return new ExprUnit(dataType, { type: "value", value });
95
- },
96
-
97
- /**
98
- * 컬럼 참조를 생성
99
- *
100
- * 일반적으로 Queryable 콜백 프록시 객체를 사용하므로 직접 호출할 일이 적음
101
- *
102
- * @param dataType - 컬럼의 데이터 타입
103
- * @param path - 컬럼 경로 (테이블 alias, 컬럼명 )
104
- * @returns 컬럼 참조 ExprUnit 인스턴스
105
- *
106
- * @example
107
- * ```typescript
108
- * // 직접 컬럼 참조 (내부용)
109
- * expr.col("string", "T1", "name")
110
- * ```
111
- */
112
- col<TStr extends ColumnPrimitiveStr>(
113
- dataType: ColumnPrimitiveStr,
114
- ...path: string[]
115
- ): ExprUnit<ColumnPrimitiveMap[TStr] | undefined> {
116
- return new ExprUnit(dataType, { type: "column", path });
117
- },
118
-
119
- /**
120
- * Raw SQL 표현식 생성 (escape hatch)
121
- *
122
- * ORM에서 지원하지 않는 DB 함수나 문법을 직접 사용할 사용.
123
- * 태그 템플릿 리터럴 형식으로 사용하며, 보간된 값은 자동으로 파라미터화됨
124
- *
125
- * @param dataType - 반환될 값의 데이터 타입
126
- * @returns 태그 템플릿 함수
127
- *
128
- * @example
129
- * ```typescript
130
- * // MySQL JSON 함수 사용
131
- * db.user().select((u) => ({
132
- * name: u.name,
133
- * data: expr.raw("string")`JSON_EXTRACT(${u.metadata}, '$.email')`,
134
- * }))
135
- *
136
- * // PostgreSQL 배열 함수
137
- * expr.raw("number")`ARRAY_LENGTH(${u.tags}, 1)`
138
- * ```
139
- */
140
- raw<T extends ColumnPrimitiveStr>(
141
- dataType: T,
142
- ): (
143
- strings: TemplateStringsArray,
144
- ...values: ExprInput<ColumnPrimitive>[]
145
- ) => ExprUnit<ColumnPrimitiveMap[T] | undefined> {
146
- return (strings, ...values) => {
147
- const sql = strings.reduce((acc, str, i) => {
148
- if (i < values.length) {
149
- return acc + str + `$${i + 1}`; // 플레이스홀더 (ExprRenderer에서 변환)
150
- }
151
- return acc + str;
152
- }, "");
153
-
154
- const params = values.map((v) => toExpr(v));
155
-
156
- return new ExprUnit(dataType, { type: "raw", sql, params });
157
- };
158
- },
159
-
160
- //#endregion
161
-
162
- //#region ========== WHERE - 비교 연산 ==========
163
-
164
- /**
165
- * 동등 비교 (NULL-safe)
166
- *
167
- * NULL 값도 안전하게 비교 (MySQL: `<=>`, MSSQL/PostgreSQL: `IS NULL OR =`)
168
- *
169
- * @param source - 비교할 컬럼 또는 표현식
170
- * @param target - 비교 대상 또는 표현식
171
- * @returns WHERE 조건 표현식
172
- *
173
- * @example
174
- * ```typescript
175
- * db.user().where((u) => [expr.eq(u.status, "active")])
176
- * // WHERE status <=> 'active' (MySQL)
177
- * ```
178
- */
179
- eq<T extends ColumnPrimitive>(source: ExprUnit<T>, target: ExprInput<T>): WhereExprUnit {
180
- return new WhereExprUnit({
181
- type: "eq",
182
- source: toExpr(source),
183
- target: toExpr(target),
184
- });
185
- },
186
-
187
- /**
188
- * 초과 비교 (>)
189
- *
190
- * @param source - 비교할 컬럼 또는 표현식
191
- * @param target - 비교 대상 또는 표현식
192
- * @returns WHERE 조건 표현식
193
- *
194
- * @example
195
- * ```typescript
196
- * db.user().where((u) => [expr.gt(u.age, 18)])
197
- * // WHERE age > 18
198
- * ```
199
- */
200
- gt<T extends ColumnPrimitive>(source: ExprUnit<T>, target: ExprInput<T>): WhereExprUnit {
201
- return new WhereExprUnit({
202
- type: "gt",
203
- source: toExpr(source),
204
- target: toExpr(target),
205
- });
206
- },
207
-
208
- /**
209
- * 미만 비교 (<)
210
- *
211
- * @param source - 비교할 컬럼 또는 표현식
212
- * @param target - 비교 대상 또는 표현식
213
- * @returns WHERE 조건 표현식
214
- *
215
- * @example
216
- * ```typescript
217
- * db.user().where((u) => [expr.lt(u.score, 60)])
218
- * // WHERE score < 60
219
- * ```
220
- */
221
- lt<T extends ColumnPrimitive>(source: ExprUnit<T>, target: ExprInput<T>): WhereExprUnit {
222
- return new WhereExprUnit({
223
- type: "lt",
224
- source: toExpr(source),
225
- target: toExpr(target),
226
- });
227
- },
228
-
229
- /**
230
- * 이상 비교 (>=)
231
- *
232
- * @param source - 비교할 컬럼 또는 표현식
233
- * @param target - 비교 대상 또는 표현식
234
- * @returns WHERE 조건 표현식
235
- *
236
- * @example
237
- * ```typescript
238
- * db.user().where((u) => [expr.gte(u.age, 18)])
239
- * // WHERE age >= 18
240
- * ```
241
- */
242
- gte<T extends ColumnPrimitive>(source: ExprUnit<T>, target: ExprInput<T>): WhereExprUnit {
243
- return new WhereExprUnit({
244
- type: "gte",
245
- source: toExpr(source),
246
- target: toExpr(target),
247
- });
248
- },
249
-
250
- /**
251
- * 이하 비교 (<=)
252
- *
253
- * @param source - 비교할 컬럼 또는 표현식
254
- * @param target - 비교 대상 또는 표현식
255
- * @returns WHERE 조건 표현식
256
- *
257
- * @example
258
- * ```typescript
259
- * db.user().where((u) => [expr.lte(u.score, 100)])
260
- * // WHERE score <= 100
261
- * ```
262
- */
263
- lte<T extends ColumnPrimitive>(source: ExprUnit<T>, target: ExprInput<T>): WhereExprUnit {
264
- return new WhereExprUnit({
265
- type: "lte",
266
- source: toExpr(source),
267
- target: toExpr(target),
268
- });
269
- },
270
-
271
- /**
272
- * 범위 비교 (BETWEEN)
273
- *
274
- * from/to가 undefined이면 해당 방향은 무제한
275
- *
276
- * @param source - 비교할 컬럼 또는 표현식
277
- * @param from - 시작 (undefined이면 하한 없음)
278
- * @param to - 끝 (undefined이면 상한 없음)
279
- * @returns WHERE 조건 표현식
280
- *
281
- * @example
282
- * ```typescript
283
- * // 범위 지정
284
- * db.user().where((u) => [expr.between(u.age, 18, 65)])
285
- * // WHERE age BETWEEN 18 AND 65
286
- *
287
- * // 하한만 지정
288
- * db.user().where((u) => [expr.between(u.age, 18, undefined)])
289
- * // WHERE age >= 18
290
- * ```
291
- */
292
- between<T extends ColumnPrimitive>(
293
- source: ExprUnit<T>,
294
- from?: ExprInput<T>,
295
- to?: ExprInput<T>,
296
- ): WhereExprUnit {
297
- return new WhereExprUnit({
298
- type: "between",
299
- source: toExpr(source),
300
- from: from != null ? toExpr(from) : undefined,
301
- to: to != null ? toExpr(to) : undefined,
302
- });
303
- },
304
-
305
- //#endregion
306
-
307
- //#region ========== WHERE - NULL 체크 ==========
308
-
309
- /**
310
- * NULL 체크 (IS NULL)
311
- *
312
- * @param source - 체크할 컬럼 또는 표현식
313
- * @returns WHERE 조건 표현식
314
- *
315
- * @example
316
- * ```typescript
317
- * db.user().where((u) => [expr.null(u.deletedAt)])
318
- * // WHERE deletedAt IS NULL
319
- * ```
320
- */
321
- null<T extends ColumnPrimitive>(source: ExprUnit<T>): WhereExprUnit {
322
- return new WhereExprUnit({
323
- type: "null",
324
- arg: toExpr(source),
325
- });
326
- },
327
-
328
- //#endregion
329
-
330
- //#region ========== WHERE - 문자열 검색 ==========
331
-
332
- /**
333
- * LIKE 패턴 매칭
334
- *
335
- * `%`는 0개 이상의 문자, `_`는 단일 문자와 매칭.
336
- * 특수문자는 `\`로 이스케이프됨
337
- *
338
- * @param source - 검색할 컬럼 또는 표현식
339
- * @param pattern - 검색 패턴 (%, _ 와일드카드 사용 가능)
340
- * @returns WHERE 조건 표현식
341
- *
342
- * @example
343
- * ```typescript
344
- * // 접두사 검색
345
- * db.user().where((u) => [expr.like(u.name, "John%")])
346
- * // WHERE name LIKE 'John%' ESCAPE '\'
347
- *
348
- * // 포함 검색
349
- * db.user().where((u) => [expr.like(u.email, "%@gmail.com")])
350
- * ```
351
- */
352
- like(
353
- source: ExprUnit<string | undefined>,
354
- pattern: ExprInput<string | undefined>,
355
- ): WhereExprUnit {
356
- return new WhereExprUnit({
357
- type: "like",
358
- source: toExpr(source),
359
- pattern: toExpr(pattern),
360
- });
361
- },
362
-
363
- /**
364
- * 정규식 패턴 매칭
365
- *
366
- * DBMS별 정규식 문법 차이 주의 필요
367
- *
368
- * @param source - 검색할 컬럼 또는 표현식
369
- * @param pattern - 정규식 패턴
370
- * @returns WHERE 조건 표현식
371
- *
372
- * @example
373
- * ```typescript
374
- * db.user().where((u) => [expr.regexp(u.email, "^[a-z]+@")])
375
- * // MySQL: WHERE email REGEXP '^[a-z]+@'
376
- * ```
377
- */
378
- regexp(
379
- source: ExprUnit<string | undefined>,
380
- pattern: ExprInput<string | undefined>,
381
- ): WhereExprUnit {
382
- return new WhereExprUnit({
383
- type: "regexp",
384
- source: toExpr(source),
385
- pattern: toExpr(pattern),
386
- });
387
- },
388
-
389
- //#endregion
390
-
391
- //#region ========== WHERE - IN ==========
392
-
393
- /**
394
- * IN 연산자 - 목록과 비교
395
- *
396
- * @param source - 비교할 컬럼 또는 표현식
397
- * @param values - 비교할 목록
398
- * @returns WHERE 조건 표현식
399
- *
400
- * @example
401
- * ```typescript
402
- * db.user().where((u) => [expr.in(u.status, ["active", "pending"])])
403
- * // WHERE status IN ('active', 'pending')
404
- * ```
405
- */
406
- in<T extends ColumnPrimitive>(source: ExprUnit<T>, values: ExprInput<T>[]): WhereExprUnit {
407
- return new WhereExprUnit({
408
- type: "in",
409
- source: toExpr(source),
410
- values: values.map((v) => toExpr(v)),
411
- });
412
- },
413
-
414
- /**
415
- * IN (SELECT ...) - 서브쿼리 결과와 비교
416
- *
417
- * 서브쿼리는 반드시 단일 컬럼만 SELECT해야 함
418
- *
419
- * @param source - 비교할 컬럼 또는 표현식
420
- * @param query - 단일 컬럼을 반환하는 Queryable
421
- * @returns WHERE 조건 표현식
422
- * @throws {Error} 서브쿼리가 단일 컬럼이 아닌 경우
423
- *
424
- * @example
425
- * ```typescript
426
- * db.user().where((u) => [
427
- * expr.inQuery(
428
- * u.id,
429
- * db.order()
430
- * .where((o) => [expr.gt(o.amount, 1000)])
431
- * .select((o) => ({ userId: o.userId }))
432
- * ),
433
- * ])
434
- * // WHERE id IN (SELECT userId FROM Order WHERE amount > 1000)
435
- * ```
436
- */
437
- inQuery<T extends ColumnPrimitive, TData extends Record<string, T>>(
438
- source: ExprUnit<T>,
439
- query: Queryable<TData, any>,
440
- ): WhereExprUnit {
441
- const queryDef = query.getSelectQueryDef();
442
- if (queryDef.select == null || Object.keys(queryDef.select).length !== 1) {
443
- throw new Error("inQuery 서브쿼리는 단일 컬럼만 SELECT해야 합니다.");
444
- }
445
- return new WhereExprUnit({
446
- type: "inQuery",
447
- source: toExpr(source),
448
- query: queryDef,
449
- });
450
- },
451
-
452
- /**
453
- * EXISTS (SELECT ...) - 서브쿼리 결과 존재 여부 확인
454
- *
455
- * 서브쿼리가 하나 이상의 행을 반환하면 true
456
- *
457
- * @param query - 존재 여부를 확인할 Queryable
458
- * @returns WHERE 조건 표현식
459
- *
460
- * @example
461
- * ```typescript
462
- * // 주문이 있는 사용자 조회
463
- * db.user().where((u) => [
464
- * expr.exists(
465
- * db.order().where((o) => [expr.eq(o.userId, u.id)])
466
- * ),
467
- * ])
468
- * // WHERE EXISTS (SELECT 1 FROM Order WHERE userId = User.id)
469
- * ```
470
- */
471
- exists(query: Queryable<any, any>): WhereExprUnit {
472
- const { select: _, ...queryDefWithoutSelect } = query.getSelectQueryDef(); // EXISTS는 SELECT 절 불필요, 패킷 절약
473
- return new WhereExprUnit({
474
- type: "exists",
475
- query: queryDefWithoutSelect,
476
- });
477
- },
478
-
479
- //#endregion
480
-
481
- //#region ========== WHERE - 논리 연산 ==========
482
-
483
- /**
484
- * NOT 연산자 - 조건 부정
485
- *
486
- * @param arg - 부정할 조건
487
- * @returns 부정된 WHERE 조건 표현식
488
- *
489
- * @example
490
- * ```typescript
491
- * db.user().where((u) => [expr.not(expr.eq(u.status, "deleted"))])
492
- * // WHERE NOT (status <=> 'deleted')
493
- * ```
494
- */
495
- not(arg: WhereExprUnit): WhereExprUnit {
496
- return new WhereExprUnit({
497
- type: "not",
498
- arg: arg.expr,
499
- });
500
- },
501
-
502
- /**
503
- * AND 연산자 - 모든 조건 충족
504
- *
505
- * 여러 조건을 AND로 결합. where() 메서드에 배열로 전달하면 자동으로 AND 적용됨
506
- *
507
- * @param conditions - AND로 결합할 조건 목록
508
- * @returns 결합된 WHERE 조건 표현식
509
- *
510
- * @example
511
- * ```typescript
512
- * db.user().where((u) => [
513
- * expr.and([
514
- * expr.eq(u.status, "active"),
515
- * expr.gte(u.age, 18),
516
- * ]),
517
- * ])
518
- * // WHERE (status <=> 'active' AND age >= 18)
519
- * ```
520
- */
521
- and(conditions: WhereExprUnit[]): WhereExprUnit {
522
- if (conditions.length === 0) {
523
- throw new ArgumentError({ conditions: " 배열은 허용되지 않습니다" });
524
- }
525
- return new WhereExprUnit({
526
- type: "and",
527
- conditions: conditions.map((c) => c.expr),
528
- });
529
- },
530
-
531
- /**
532
- * OR 연산자 - 하나 이상의 조건 충족
533
- *
534
- * @param conditions - OR로 결합할 조건 목록
535
- * @returns 결합된 WHERE 조건 표현식
536
- *
537
- * @example
538
- * ```typescript
539
- * db.user().where((u) => [
540
- * expr.or([
541
- * expr.eq(u.status, "active"),
542
- * expr.eq(u.status, "pending"),
543
- * ]),
544
- * ])
545
- * // WHERE (status <=> 'active' OR status <=> 'pending')
546
- * ```
547
- */
548
- or(conditions: WhereExprUnit[]): WhereExprUnit {
549
- if (conditions.length === 0) {
550
- throw new ArgumentError({ conditions: " 배열은 허용되지 않습니다" });
551
- }
552
- return new WhereExprUnit({
553
- type: "or",
554
- conditions: conditions.map((c) => c.expr),
555
- });
556
- },
557
-
558
- //#endregion
559
-
560
- //#region ========== SELECT - 문자열 ==========
561
-
562
- /**
563
- * 문자열 연결 (CONCAT)
564
- *
565
- * NULL 값은 빈 문자열로 처리됨 (DBMS별 자동 변환)
566
- *
567
- * @param args - 연결할 문자열들
568
- * @returns 연결된 문자열 표현식
569
- *
570
- * @example
571
- * ```typescript
572
- * db.user().select((u) => ({
573
- * fullName: expr.concat(u.firstName, " ", u.lastName),
574
- * }))
575
- * // SELECT CONCAT(firstName, ' ', lastName) AS fullName
576
- * ```
577
- */
578
- concat(...args: ExprInput<string | undefined>[]): ExprUnit<string> {
579
- return new ExprUnit("string", {
580
- type: "concat",
581
- args: args.map((arg) => toExpr(arg)),
582
- });
583
- },
584
-
585
- /**
586
- * 문자열 왼쪽에서 지정 길이만큼 추출 (LEFT)
587
- *
588
- * @param source - 원본 문자열
589
- * @param length - 추출할 문자 수
590
- * @returns 추출된 문자열 표현식
591
- *
592
- * @example
593
- * ```typescript
594
- * db.user().select((u) => ({
595
- * initial: expr.left(u.name, 1),
596
- * }))
597
- * // SELECT LEFT(name, 1) AS initial
598
- * ```
599
- */
600
- left<T extends string | undefined>(source: ExprUnit<T>, length: ExprInput<number>): ExprUnit<T> {
601
- return new ExprUnit("string", {
602
- type: "left",
603
- source: toExpr(source),
604
- length: toExpr(length),
605
- });
606
- },
607
-
608
- /**
609
- * 문자열 오른쪽에서 지정 길이만큼 추출 (RIGHT)
610
- *
611
- * @param source - 원본 문자열
612
- * @param length - 추출할 문자 수
613
- * @returns 추출된 문자열 표현식
614
- *
615
- * @example
616
- * ```typescript
617
- * db.phone().select((p) => ({
618
- * lastFour: expr.right(p.number, 4),
619
- * }))
620
- * // SELECT RIGHT(number, 4) AS lastFour
621
- * ```
622
- */
623
- right<T extends string | undefined>(source: ExprUnit<T>, length: ExprInput<number>): ExprUnit<T> {
624
- return new ExprUnit("string", {
625
- type: "right",
626
- source: toExpr(source),
627
- length: toExpr(length),
628
- });
629
- },
630
-
631
- /**
632
- * 문자열 양쪽 공백 제거 (TRIM)
633
- *
634
- * @param source - 원본 문자열
635
- * @returns 공백이 제거된 문자열 표현식
636
- *
637
- * @example
638
- * ```typescript
639
- * db.user().select((u) => ({
640
- * name: expr.trim(u.name),
641
- * }))
642
- * // SELECT TRIM(name) AS name
643
- * ```
644
- */
645
- trim<T extends string | undefined>(source: ExprUnit<T>): ExprUnit<T> {
646
- return new ExprUnit("string", {
647
- type: "trim",
648
- arg: toExpr(source),
649
- });
650
- },
651
-
652
- /**
653
- * 문자열 왼쪽 패딩 (LPAD)
654
- *
655
- * 지정 길이가 될 때까지 왼쪽에 fillString 반복 추가
656
- *
657
- * @param source - 원본 문자열
658
- * @param length - 목표 길이
659
- * @param fillString - 패딩에 사용할 문자열
660
- * @returns 패딩된 문자열 표현식
661
- *
662
- * @example
663
- * ```typescript
664
- * db.order().select((o) => ({
665
- * orderNo: expr.padStart(expr.cast(o.id, { type: "varchar", length: 10 }), 8, "0"),
666
- * }))
667
- * // SELECT LPAD(CAST(id AS VARCHAR(10)), 8, '0') AS orderNo
668
- * // 결과: "00000123"
669
- * ```
670
- */
671
- padStart<T extends string | undefined>(
672
- source: ExprUnit<T>,
673
- length: ExprInput<number>,
674
- fillString: ExprInput<string>,
675
- ): ExprUnit<T> {
676
- return new ExprUnit("string", {
677
- type: "padStart",
678
- source: toExpr(source),
679
- length: toExpr(length),
680
- fillString: toExpr(fillString),
681
- });
682
- },
683
-
684
- /**
685
- * 문자열 치환 (REPLACE)
686
- *
687
- * @param source - 원본 문자열
688
- * @param from - 찾을 문자열
689
- * @param to - 대체할 문자열
690
- * @returns 치환된 문자열 표현식
691
- *
692
- * @example
693
- * ```typescript
694
- * db.user().select((u) => ({
695
- * phone: expr.replace(u.phone, "-", ""),
696
- * }))
697
- * // SELECT REPLACE(phone, '-', '') AS phone
698
- * ```
699
- */
700
- replace<T extends string | undefined>(
701
- source: ExprUnit<T>,
702
- from: ExprInput<string>,
703
- to: ExprInput<string>,
704
- ): ExprUnit<T> {
705
- return new ExprUnit("string", {
706
- type: "replace",
707
- source: toExpr(source),
708
- from: toExpr(from),
709
- to: toExpr(to),
710
- });
711
- },
712
-
713
- /**
714
- * 문자열 대문자 변환 (UPPER)
715
- *
716
- * @param source - 원본 문자열
717
- * @returns 대문자로 변환된 문자열 표현식
718
- *
719
- * @example
720
- * ```typescript
721
- * db.user().select((u) => ({
722
- * code: expr.upper(u.code),
723
- * }))
724
- * // SELECT UPPER(code) AS code
725
- * ```
726
- */
727
- upper<T extends string | undefined>(source: ExprUnit<T>): ExprUnit<T> {
728
- return new ExprUnit("string", {
729
- type: "upper",
730
- arg: toExpr(source),
731
- });
732
- },
733
-
734
- /**
735
- * 문자열 소문자 변환 (LOWER)
736
- *
737
- * @param source - 원본 문자열
738
- * @returns 소문자로 변환된 문자열 표현식
739
- *
740
- * @example
741
- * ```typescript
742
- * db.user().select((u) => ({
743
- * email: expr.lower(u.email),
744
- * }))
745
- * // SELECT LOWER(email) AS email
746
- * ```
747
- */
748
- lower<T extends string | undefined>(source: ExprUnit<T>): ExprUnit<T> {
749
- return new ExprUnit("string", {
750
- type: "lower",
751
- arg: toExpr(source),
752
- });
753
- },
754
-
755
- /**
756
- * 문자열 길이 (문자 수)
757
- *
758
- * @param source - 원본 문자열
759
- * @returns 문자 수
760
- *
761
- * @example
762
- * ```typescript
763
- * db.user().select((u) => ({
764
- * nameLength: expr.length(u.name),
765
- * }))
766
- * // SELECT CHAR_LENGTH(name) AS nameLength
767
- * ```
768
- */
769
- length(source: ExprUnit<string | undefined>): ExprUnit<number> {
770
- return new ExprUnit("number", {
771
- type: "length",
772
- arg: toExpr(source),
773
- });
774
- },
775
-
776
- /**
777
- * 문자열 바이트 길이
778
- *
779
- * UTF-8 환경에서 한글은 3바이트
780
- *
781
- * @param source - 원본 문자열
782
- * @returns 바이트 수
783
- *
784
- * @example
785
- * ```typescript
786
- * db.user().select((u) => ({
787
- * byteLen: expr.byteLength(u.name),
788
- * }))
789
- * // SELECT OCTET_LENGTH(name) AS byteLen
790
- * ```
791
- */
792
- byteLength(source: ExprUnit<string | undefined>): ExprUnit<number> {
793
- return new ExprUnit("number", {
794
- type: "byteLength",
795
- arg: toExpr(source),
796
- });
797
- },
798
-
799
- /**
800
- * 문자열 일부 추출 (SUBSTRING)
801
- *
802
- * SQL 표준에 따라 1-based index 사용
803
- *
804
- * @param source - 원본 문자열
805
- * @param start - 시작 위치 (1부터 시작)
806
- * @param length - 추출할 길이 (생략 시 끝까지)
807
- * @returns 추출된 문자열 표현식
808
- *
809
- * @example
810
- * ```typescript
811
- * db.user().select((u) => ({
812
- * // "Hello World"에서 인덱스 1부터 5글자: "Hello"
813
- * prefix: expr.substring(u.name, 1, 5),
814
- * }))
815
- * // SELECT SUBSTRING(name, 1, 5) AS prefix
816
- * ```
817
- */
818
- substring<T extends string | undefined>(
819
- source: ExprUnit<T>,
820
- start: ExprInput<number>,
821
- length?: ExprInput<number>,
822
- ): ExprUnit<T> {
823
- return new ExprUnit("string", {
824
- type: "substring",
825
- source: toExpr(source),
826
- start: toExpr(start),
827
- ...(length != null ? { length: toExpr(length) } : {}),
828
- });
829
- },
830
-
831
- /**
832
- * 문자열 내 위치 찾기 (LOCATE/CHARINDEX)
833
- *
834
- * 1-based index 반환, 없으면 0 반환
835
- *
836
- * @param source - 검색할 문자열
837
- * @param search - 찾을 문자열
838
- * @returns 위치 (1부터 시작, 없으면 0)
839
- *
840
- * @example
841
- * ```typescript
842
- * db.user().select((u) => ({
843
- * atPos: expr.indexOf(u.email, "@"),
844
- * }))
845
- * // SELECT LOCATE('@', email) AS atPos (MySQL)
846
- * // "john@example.com" → 5
847
- * ```
848
- */
849
- indexOf(source: ExprUnit<string | undefined>, search: ExprInput<string>): ExprUnit<number> {
850
- return new ExprUnit("number", {
851
- type: "indexOf",
852
- source: toExpr(source),
853
- search: toExpr(search),
854
- });
855
- },
856
-
857
- //#endregion
858
-
859
- //#region ========== SELECT - 숫자 ==========
860
-
861
- /**
862
- * 절대값 (ABS)
863
- *
864
- * @param source - 원본 숫자
865
- * @returns 절대값 표현식
866
- *
867
- * @example
868
- * ```typescript
869
- * db.account().select((a) => ({
870
- * balance: expr.abs(a.balance),
871
- * }))
872
- * // SELECT ABS(balance) AS balance
873
- * ```
874
- */
875
- abs<T extends number | undefined>(source: ExprUnit<T>): ExprUnit<T> {
876
- return new ExprUnit("number", {
877
- type: "abs",
878
- arg: toExpr(source),
879
- });
880
- },
881
-
882
- /**
883
- * 반올림 (ROUND)
884
- *
885
- * @param source - 원본 숫자
886
- * @param digits - 소수점 이하 자릿수
887
- * @returns 반올림된 숫자 표현식
888
- *
889
- * @example
890
- * ```typescript
891
- * db.product().select((p) => ({
892
- * price: expr.round(p.price, 2),
893
- * }))
894
- * // SELECT ROUND(price, 2) AS price
895
- * // 123.456 → 123.46
896
- * ```
897
- */
898
- round<T extends number | undefined>(source: ExprUnit<T>, digits: number): ExprUnit<T> {
899
- return new ExprUnit("number", {
900
- type: "round",
901
- arg: toExpr(source),
902
- digits,
903
- });
904
- },
905
-
906
- /**
907
- * 올림 (CEILING)
908
- *
909
- * @param source - 원본 숫자
910
- * @returns 올림된 숫자 표현식
911
- *
912
- * @example
913
- * ```typescript
914
- * db.order().select((o) => ({
915
- * pages: expr.ceil(expr.divide(o.itemCount, 10)),
916
- * }))
917
- * // SELECT CEILING(itemCount / 10) AS pages
918
- * // 25 / 10 = 2.5 → 3
919
- * ```
920
- */
921
- ceil<T extends number | undefined>(source: ExprUnit<T>): ExprUnit<T> {
922
- return new ExprUnit("number", {
923
- type: "ceil",
924
- arg: toExpr(source),
925
- });
926
- },
927
-
928
- /**
929
- * 버림 (FLOOR)
930
- *
931
- * @param source - 원본 숫자
932
- * @returns 버림된 숫자 표현식
933
- *
934
- * @example
935
- * ```typescript
936
- * db.user().select((u) => ({
937
- * ageGroup: expr.floor(expr.divide(u.age, 10)),
938
- * }))
939
- * // SELECT FLOOR(age / 10) AS ageGroup
940
- * // 25 / 10 = 2.5 → 2
941
- * ```
942
- */
943
- floor<T extends number | undefined>(source: ExprUnit<T>): ExprUnit<T> {
944
- return new ExprUnit("number", {
945
- type: "floor",
946
- arg: toExpr(source),
947
- });
948
- },
949
-
950
- //#endregion
951
-
952
- //#region ========== SELECT - 날짜 ==========
953
-
954
- /**
955
- * 연도 추출 (YEAR)
956
- *
957
- * @param source - DateTime 또는 DateOnly 표현식
958
- * @returns 연도 (4자리 숫자)
959
- *
960
- * @example
961
- * ```typescript
962
- * db.user().select((u) => ({
963
- * birthYear: expr.year(u.birthDate),
964
- * }))
965
- * // SELECT YEAR(birthDate) AS birthYear
966
- * ```
967
- */
968
- year<T extends DateTime | DateOnly | undefined>(
969
- source: ExprUnit<T>,
970
- ): ExprUnit<T extends undefined ? undefined : number> {
971
- return new ExprUnit("number", {
972
- type: "year",
973
- arg: toExpr(source),
974
- });
975
- },
976
-
977
- /**
978
- * 월 추출 (MONTH)
979
- *
980
- * @param source - DateTime 또는 DateOnly 표현식
981
- * @returns 월 (1~12)
982
- *
983
- * @example
984
- * ```typescript
985
- * db.order().select((o) => ({
986
- * orderMonth: expr.month(o.createdAt),
987
- * }))
988
- * // SELECT MONTH(createdAt) AS orderMonth
989
- * ```
990
- */
991
- month<T extends DateTime | DateOnly | undefined>(
992
- source: ExprUnit<T>,
993
- ): ExprUnit<T extends undefined ? undefined : number> {
994
- return new ExprUnit("number", {
995
- type: "month",
996
- arg: toExpr(source),
997
- });
998
- },
999
-
1000
- /**
1001
- * 일 추출 (DAY)
1002
- *
1003
- * @param source - DateTime 또는 DateOnly 표현식
1004
- * @returns 일 (1~31)
1005
- *
1006
- * @example
1007
- * ```typescript
1008
- * db.user().select((u) => ({
1009
- * birthDay: expr.day(u.birthDate),
1010
- * }))
1011
- * // SELECT DAY(birthDate) AS birthDay
1012
- * ```
1013
- */
1014
- day<T extends DateTime | DateOnly | undefined>(
1015
- source: ExprUnit<T>,
1016
- ): ExprUnit<T extends undefined ? undefined : number> {
1017
- return new ExprUnit("number", {
1018
- type: "day",
1019
- arg: toExpr(source),
1020
- });
1021
- },
1022
-
1023
- /**
1024
- * 시 추출 (HOUR)
1025
- *
1026
- * @param source - DateTime 또는 Time 표현식
1027
- * @returns 시 (0~23)
1028
- *
1029
- * @example
1030
- * ```typescript
1031
- * db.log().select((l) => ({
1032
- * logHour: expr.hour(l.createdAt),
1033
- * }))
1034
- * // SELECT HOUR(createdAt) AS logHour
1035
- * ```
1036
- */
1037
- hour<T extends DateTime | Time | undefined>(
1038
- source: ExprUnit<T>,
1039
- ): ExprUnit<T extends undefined ? undefined : number> {
1040
- return new ExprUnit("number", {
1041
- type: "hour",
1042
- arg: toExpr(source),
1043
- });
1044
- },
1045
-
1046
- /**
1047
- * 분 추출 (MINUTE)
1048
- *
1049
- * @param source - DateTime 또는 Time 표현식
1050
- * @returns 분 (0~59)
1051
- *
1052
- * @example
1053
- * ```typescript
1054
- * db.log().select((l) => ({
1055
- * logMinute: expr.minute(l.createdAt),
1056
- * }))
1057
- * // SELECT MINUTE(createdAt) AS logMinute
1058
- * ```
1059
- */
1060
- minute<T extends DateTime | Time | undefined>(
1061
- source: ExprUnit<T>,
1062
- ): ExprUnit<T extends undefined ? undefined : number> {
1063
- return new ExprUnit("number", {
1064
- type: "minute",
1065
- arg: toExpr(source),
1066
- });
1067
- },
1068
-
1069
- /**
1070
- * 초 추출 (SECOND)
1071
- *
1072
- * @param source - DateTime 또는 Time 표현식
1073
- * @returns 초 (0~59)
1074
- *
1075
- * @example
1076
- * ```typescript
1077
- * db.log().select((l) => ({
1078
- * logSecond: expr.second(l.createdAt),
1079
- * }))
1080
- * // SELECT SECOND(createdAt) AS logSecond
1081
- * ```
1082
- */
1083
- second<T extends DateTime | Time | undefined>(
1084
- source: ExprUnit<T>,
1085
- ): ExprUnit<T extends undefined ? undefined : number> {
1086
- return new ExprUnit("number", {
1087
- type: "second",
1088
- arg: toExpr(source),
1089
- });
1090
- },
1091
-
1092
- /**
1093
- * ISO 주차 추출
1094
- *
1095
- * ISO 8601 기준 주차 (월요일 시작, 1~53)
1096
- *
1097
- * @param source - DateOnly 표현식
1098
- * @returns ISO 주차 번호
1099
- *
1100
- * @example
1101
- * ```typescript
1102
- * db.order().select((o) => ({
1103
- * weekNum: expr.isoWeek(o.orderDate),
1104
- * }))
1105
- * // SELECT WEEK(orderDate, 3) AS weekNum (MySQL)
1106
- * ```
1107
- */
1108
- isoWeek<T extends DateOnly | undefined>(
1109
- source: ExprUnit<T>,
1110
- ): ExprUnit<T extends undefined ? undefined : number> {
1111
- return new ExprUnit("number", {
1112
- type: "isoWeek",
1113
- arg: toExpr(source),
1114
- });
1115
- },
1116
-
1117
- /**
1118
- * ISO 주 시작일 (월요일)
1119
- *
1120
- * 해당 날짜가 속한 주의 월요일 반환
1121
- *
1122
- * @param source - DateOnly 표현식
1123
- * @returns 주의 시작 날짜 (월요일)
1124
- *
1125
- * @example
1126
- * ```typescript
1127
- * db.order().select((o) => ({
1128
- * weekStart: expr.isoWeekStartDate(o.orderDate),
1129
- * }))
1130
- * // 2024-01-10 (수) → 2024-01-08 (월)
1131
- * ```
1132
- */
1133
- isoWeekStartDate<T extends DateOnly | undefined>(source: ExprUnit<T>): ExprUnit<T> {
1134
- return new ExprUnit("DateOnly", {
1135
- type: "isoWeekStartDate",
1136
- arg: toExpr(source),
1137
- });
1138
- },
1139
-
1140
- /**
1141
- * ISO 연월 (해당 월의 1일)
1142
- *
1143
- * 해당 날짜의 월 첫째 날 반환
1144
- *
1145
- * @param source - DateOnly 표현식
1146
- * @returns 월의 첫째 날
1147
- *
1148
- * @example
1149
- * ```typescript
1150
- * db.order().select((o) => ({
1151
- * yearMonth: expr.isoYearMonth(o.orderDate),
1152
- * }))
1153
- * // 2024-01-15 → 2024-01-01
1154
- * ```
1155
- */
1156
- isoYearMonth<T extends DateOnly | undefined>(source: ExprUnit<T>): ExprUnit<T> {
1157
- return new ExprUnit("DateOnly", {
1158
- type: "isoYearMonth",
1159
- arg: toExpr(source),
1160
- });
1161
- },
1162
-
1163
- /**
1164
- * 날짜 차이 계산 (DATEDIFF)
1165
- *
1166
- * @param separator - 단위 ("year", "month", "day", "hour", "minute", "second")
1167
- * @param from - 시작 날짜
1168
- * @param to - 끝 날짜
1169
- * @returns 차이 (to - from)
1170
- *
1171
- * @example
1172
- * ```typescript
1173
- * db.user().select((u) => ({
1174
- * age: expr.dateDiff("year", u.birthDate, expr.val("DateOnly", DateOnly.today())),
1175
- * }))
1176
- * // SELECT DATEDIFF(year, birthDate, '2024-01-15') AS age
1177
- * ```
1178
- */
1179
- dateDiff<T extends DateTime | DateOnly | Time | undefined>(
1180
- separator: DateSeparator,
1181
- from: ExprInput<T>,
1182
- to: ExprInput<T>,
1183
- ): ExprUnit<T extends undefined ? undefined : number> {
1184
- return new ExprUnit("number", {
1185
- type: "dateDiff",
1186
- separator,
1187
- from: toExpr(from),
1188
- to: toExpr(to),
1189
- });
1190
- },
1191
-
1192
- /**
1193
- * 날짜 더하기 (DATEADD)
1194
- *
1195
- * @param separator - 단위 ("year", "month", "day", "hour", "minute", "second")
1196
- * @param source - 원본 날짜
1197
- * @param value - 더할 (음수 가능)
1198
- * @returns 계산된 날짜
1199
- *
1200
- * @example
1201
- * ```typescript
1202
- * db.subscription().select((s) => ({
1203
- * expiresAt: expr.dateAdd("month", s.startDate, 12),
1204
- * }))
1205
- * // SELECT DATEADD(month, 12, startDate) AS expiresAt
1206
- * ```
1207
- */
1208
- dateAdd<T extends DateTime | DateOnly | Time | undefined>(
1209
- separator: DateSeparator,
1210
- source: ExprUnit<T>,
1211
- value: ExprInput<number>,
1212
- ): ExprUnit<T> {
1213
- return new ExprUnit(source.dataType, {
1214
- type: "dateAdd",
1215
- separator,
1216
- source: toExpr(source),
1217
- value: toExpr(value),
1218
- });
1219
- },
1220
-
1221
- /**
1222
- * 날짜 포맷 (DATE_FORMAT)
1223
- *
1224
- * DBMS별로 포맷 문자열 규칙이 다를 수 있음
1225
- *
1226
- * @param source - 날짜 표현식
1227
- * @param format - 포맷 문자열 (예: "%Y-%m-%d")
1228
- * @returns 포맷된 문자열 표현식
1229
- *
1230
- * @example
1231
- * ```typescript
1232
- * db.order().select((o) => ({
1233
- * orderDate: expr.formatDate(o.createdAt, "%Y-%m-%d"),
1234
- * }))
1235
- * // SELECT DATE_FORMAT(createdAt, '%Y-%m-%d') AS orderDate (MySQL)
1236
- * // 2024-01-15 10:30:00 → "2024-01-15"
1237
- * ```
1238
- */
1239
- formatDate<T extends DateTime | DateOnly | Time | undefined>(
1240
- source: ExprUnit<T>,
1241
- format: string,
1242
- ): ExprUnit<T extends undefined ? undefined : string> {
1243
- return new ExprUnit("string", {
1244
- type: "formatDate",
1245
- source: toExpr(source),
1246
- format,
1247
- });
1248
- },
1249
-
1250
- //#endregion
1251
-
1252
- //#region ========== SELECT - 조건 ==========
1253
-
1254
- /**
1255
- * NULL 대체 (COALESCE/IFNULL)
1256
- *
1257
- * 첫 번째 non-null 값을 반환. 마지막 인자가 non-nullable이면 결과도 non-nullable
1258
- *
1259
- * @param args - 검사할 값들 (마지막은 기본값)
1260
- * @returns 첫 번째 non-null
1261
- *
1262
- * @example
1263
- * ```typescript
1264
- * db.user().select((u) => ({
1265
- * displayName: expr.ifNull(u.nickname, u.name, "Guest"),
1266
- * }))
1267
- * // SELECT COALESCE(nickname, name, 'Guest') AS displayName
1268
- * ```
1269
- */
1270
- ifNull,
1271
-
1272
- /**
1273
- * 특정 값이면 NULL 반환 (NULLIF)
1274
- *
1275
- * source === value 이면 NULL 반환, 아니면 source 반환
1276
- *
1277
- * @param source - 원본
1278
- * @param value - 비교할
1279
- * @returns NULL 또는 원본
1280
- *
1281
- * @example
1282
- * ```typescript
1283
- * db.user().select((u) => ({
1284
- * // 빈 문자열을 NULL로 변환
1285
- * bio: expr.nullIf(u.bio, ""),
1286
- * }))
1287
- * // SELECT NULLIF(bio, '') AS bio
1288
- * ```
1289
- */
1290
- nullIf<T extends ColumnPrimitive>(
1291
- source: ExprUnit<T>,
1292
- value: ExprInput<T>,
1293
- ): ExprUnit<T | undefined> {
1294
- return new ExprUnit(source.dataType, {
1295
- type: "nullIf",
1296
- source: toExpr(source),
1297
- value: toExpr(value),
1298
- });
1299
- },
1300
-
1301
- /**
1302
- * WHERE 표현식을 boolean으로 변환
1303
- *
1304
- * SELECT 절에서 조건 결과를 boolean 컬럼으로 사용할 때 사용
1305
- *
1306
- * @param condition - 변환할 조건
1307
- * @returns boolean 표현식
1308
- *
1309
- * @example
1310
- * ```typescript
1311
- * db.user().select((u) => ({
1312
- * isActive: expr.is(expr.eq(u.status, "active")),
1313
- * }))
1314
- * // SELECT (status <=> 'active') AS isActive
1315
- * ```
1316
- */
1317
- is(condition: WhereExprUnit): ExprUnit<boolean> {
1318
- return new ExprUnit("boolean", {
1319
- type: "is",
1320
- condition: condition.expr,
1321
- });
1322
- },
1323
-
1324
- /**
1325
- * CASE WHEN 표현식 빌더
1326
- *
1327
- * 체이닝 방식으로 조건 분기를 구성
1328
- *
1329
- * @returns SwitchExprBuilder 인스턴스
1330
- *
1331
- * @example
1332
- * ```typescript
1333
- * db.user().select((u) => ({
1334
- * grade: expr.switch<string>()
1335
- * .case(expr.gte(u.score, 90), "A")
1336
- * .case(expr.gte(u.score, 80), "B")
1337
- * .case(expr.gte(u.score, 70), "C")
1338
- * .default("F"),
1339
- * }))
1340
- * // SELECT CASE WHEN score >= 90 THEN 'A' ... ELSE 'F' END AS grade
1341
- * ```
1342
- */
1343
- switch<T extends ColumnPrimitive>(): SwitchExprBuilder<T> {
1344
- return createSwitchBuilder<T>();
1345
- },
1346
-
1347
- /**
1348
- * 단순 IF 조건 (삼항 연산자)
1349
- *
1350
- * @param condition - 조건
1351
- * @param then - 조건이 참일 때
1352
- * @param else_ - 조건이 거짓일 때
1353
- * @returns 조건부 표현식
1354
- *
1355
- * @example
1356
- * ```typescript
1357
- * db.user().select((u) => ({
1358
- * type: expr.if(expr.gte(u.age, 18), "adult", "minor"),
1359
- * }))
1360
- * // SELECT IF(age >= 18, 'adult', 'minor') AS type
1361
- * ```
1362
- */
1363
- if<T extends ColumnPrimitive>(
1364
- condition: WhereExprUnit,
1365
- then: ExprInput<T>,
1366
- else_: ExprInput<T>,
1367
- ): ExprUnit<T> {
1368
- const allValues = [then, else_];
1369
- // 1. ExprUnit에서 dataType 찾기
1370
- const exprUnit = allValues.find((v): v is ExprUnit<T> => v instanceof ExprUnit);
1371
- if (exprUnit) {
1372
- return new ExprUnit(exprUnit.dataType, {
1373
- type: "if",
1374
- condition: condition.expr,
1375
- then: toExpr(then),
1376
- else: toExpr(else_),
1377
- });
1378
- }
1379
-
1380
- // 2. non-null 리터럴에서 추론
1381
- const nonNullLiteral = allValues.find((v) => v != null) as ColumnPrimitive;
1382
- if (nonNullLiteral == null) {
1383
- throw new Error("if then/else 적어도 하나는 non-null이어야 합니다.");
1384
- }
1385
-
1386
- return new ExprUnit(inferColumnPrimitiveStr(nonNullLiteral), {
1387
- type: "if",
1388
- condition: condition.expr,
1389
- then: toExpr(then),
1390
- else: toExpr(else_),
1391
- });
1392
- },
1393
-
1394
- //#endregion
1395
-
1396
- //#region ========== SELECT - 집계 ==========
1397
- // SUM, AVG, MAX등의 집계는 모든 값이 NULL이거나 행이 없을 때만 NULL 반환 (값이 NULL인 행은 무시함)
1398
-
1399
- /**
1400
- * 수 카운트 (COUNT)
1401
- *
1402
- * @param arg - 카운트할 컬럼 (생략 시 전체 수)
1403
- * @param distinct - true면 중복 제거
1404
- * @returns
1405
- *
1406
- * @example
1407
- * ```typescript
1408
- * // 전체
1409
- * db.user().select(() => ({ total: expr.count() }))
1410
- *
1411
- * // 중복 제거 카운트
1412
- * db.order().select((o) => ({
1413
- * uniqueCustomers: expr.count(o.customerId, true),
1414
- * }))
1415
- * ```
1416
- */
1417
- count(arg?: ExprUnit<ColumnPrimitive>, distinct?: boolean): ExprUnit<number> {
1418
- return new ExprUnit("number", {
1419
- type: "count",
1420
- arg: arg != null ? toExpr(arg) : undefined,
1421
- distinct,
1422
- });
1423
- },
1424
-
1425
- /**
1426
- * 합계 (SUM)
1427
- *
1428
- * NULL 값은 무시됨. 모든 값이 NULL이면 NULL 반환
1429
- *
1430
- * @param arg - 합계를 구할 숫자 컬럼
1431
- * @returns 합계 (또는 NULL)
1432
- *
1433
- * @example
1434
- * ```typescript
1435
- * db.order().groupBy((o) => o.userId).select((o) => ({
1436
- * userId: o.userId,
1437
- * totalAmount: expr.sum(o.amount),
1438
- * }))
1439
- * ```
1440
- */
1441
- sum(arg: ExprUnit<number | undefined>): ExprUnit<number | undefined> {
1442
- return new ExprUnit("number", {
1443
- type: "sum",
1444
- arg: toExpr(arg),
1445
- });
1446
- },
1447
-
1448
- /**
1449
- * 평균 (AVG)
1450
- *
1451
- * NULL 값은 무시됨. 모든 값이 NULL이면 NULL 반환
1452
- *
1453
- * @param arg - 평균을 구할 숫자 컬럼
1454
- * @returns 평균 (또는 NULL)
1455
- *
1456
- * @example
1457
- * ```typescript
1458
- * db.product().groupBy((p) => p.categoryId).select((p) => ({
1459
- * categoryId: p.categoryId,
1460
- * avgPrice: expr.avg(p.price),
1461
- * }))
1462
- * ```
1463
- */
1464
- avg(arg: ExprUnit<number | undefined>): ExprUnit<number | undefined> {
1465
- return new ExprUnit("number", {
1466
- type: "avg",
1467
- arg: toExpr(arg),
1468
- });
1469
- },
1470
-
1471
- /**
1472
- * 최대값 (MAX)
1473
- *
1474
- * NULL 값은 무시됨. 모든 값이 NULL이면 NULL 반환
1475
- *
1476
- * @param arg - 최대값을 구할 컬럼
1477
- * @returns 최대값 (또는 NULL)
1478
- *
1479
- * @example
1480
- * ```typescript
1481
- * db.order().groupBy((o) => o.userId).select((o) => ({
1482
- * userId: o.userId,
1483
- * lastOrderDate: expr.max(o.createdAt),
1484
- * }))
1485
- * ```
1486
- */
1487
- max<T extends ColumnPrimitive>(arg: ExprUnit<T>): ExprUnit<T | undefined> {
1488
- return new ExprUnit(arg.dataType, {
1489
- type: "max",
1490
- arg: toExpr(arg),
1491
- });
1492
- },
1493
-
1494
- /**
1495
- * 최소값 (MIN)
1496
- *
1497
- * NULL 값은 무시됨. 모든 값이 NULL이면 NULL 반환
1498
- *
1499
- * @param arg - 최소값을 구할 컬럼
1500
- * @returns 최소값 (또는 NULL)
1501
- *
1502
- * @example
1503
- * ```typescript
1504
- * db.product().groupBy((p) => p.categoryId).select((p) => ({
1505
- * categoryId: p.categoryId,
1506
- * minPrice: expr.min(p.price),
1507
- * }))
1508
- * ```
1509
- */
1510
- min<T extends ColumnPrimitive>(arg: ExprUnit<T>): ExprUnit<T | undefined> {
1511
- return new ExprUnit(arg.dataType, {
1512
- type: "min",
1513
- arg: toExpr(arg),
1514
- });
1515
- },
1516
-
1517
- //#endregion
1518
-
1519
- //#region ========== SELECT - 기타 ==========
1520
-
1521
- /**
1522
- * 여러 중 최대값 (GREATEST)
1523
- *
1524
- * @param args - 비교할 값들
1525
- * @returns 최대값
1526
- *
1527
- * @example
1528
- * ```typescript
1529
- * db.product().select((p) => ({
1530
- * effectivePrice: expr.greatest(p.price, p.minPrice),
1531
- * }))
1532
- * // SELECT GREATEST(price, minPrice) AS effectivePrice
1533
- * ```
1534
- */
1535
- greatest<T extends ColumnPrimitive>(...args: ExprInput<T>[]): ExprUnit<T> {
1536
- return new ExprUnit(findDataType(args), {
1537
- type: "greatest",
1538
- args: args.map((a) => toExpr(a)),
1539
- });
1540
- },
1541
-
1542
- /**
1543
- * 여러 중 최소값 (LEAST)
1544
- *
1545
- * @param args - 비교할 값들
1546
- * @returns 최소값
1547
- *
1548
- * @example
1549
- * ```typescript
1550
- * db.product().select((p) => ({
1551
- * effectivePrice: expr.least(p.price, p.maxDiscount),
1552
- * }))
1553
- * // SELECT LEAST(price, maxDiscount) AS effectivePrice
1554
- * ```
1555
- */
1556
- least<T extends ColumnPrimitive>(...args: ExprInput<T>[]): ExprUnit<T> {
1557
- return new ExprUnit(findDataType(args), {
1558
- type: "least",
1559
- args: args.map((a) => toExpr(a)),
1560
- });
1561
- },
1562
-
1563
- /**
1564
- * 번호 (ROW_NUMBER 없이 전체 행에 대한 순번)
1565
- *
1566
- * @returns 번호 (1부터 시작)
1567
- *
1568
- * @example
1569
- * ```typescript
1570
- * db.user().select((u) => ({
1571
- * rowNum: expr.rowNum(),
1572
- * name: u.name,
1573
- * }))
1574
- * ```
1575
- */
1576
- rowNum(): ExprUnit<number> {
1577
- return new ExprUnit("number", {
1578
- type: "rowNum",
1579
- });
1580
- },
1581
-
1582
- /**
1583
- * 난수 생성 (RAND/RANDOM)
1584
- *
1585
- * 0~1 사이의 난수 반환. ORDER BY에서 랜덤 정렬용으로 주로 사용
1586
- *
1587
- * @returns 0~1 사이의 난수
1588
- *
1589
- * @example
1590
- * ```typescript
1591
- * // 랜덤 정렬
1592
- * db.user().orderBy(() => expr.random()).limit(10)
1593
- * ```
1594
- */
1595
- random(): ExprUnit<number> {
1596
- return new ExprUnit("number", {
1597
- type: "random",
1598
- });
1599
- },
1600
-
1601
- /**
1602
- * 타입 변환 (CAST)
1603
- *
1604
- * @param source - 변환할 표현식
1605
- * @param targetType - 대상 데이터 타입
1606
- * @returns 변환된 표현식
1607
- *
1608
- * @example
1609
- * ```typescript
1610
- * db.order().select((o) => ({
1611
- * idStr: expr.cast(o.id, { type: "varchar", length: 20 }),
1612
- * }))
1613
- * // SELECT CAST(id AS VARCHAR(20)) AS idStr
1614
- * ```
1615
- */
1616
- cast<T extends ColumnPrimitive, TDataType extends DataType>(
1617
- source: ExprUnit<T>,
1618
- targetType: TDataType,
1619
- ): ExprUnit<T extends undefined ? undefined : InferColumnPrimitiveFromDataType<TDataType>> {
1620
- return new ExprUnit(dataTypeStrToColumnPrimitiveStr[targetType.type], {
1621
- type: "cast",
1622
- source: toExpr(source),
1623
- targetType,
1624
- });
1625
- },
1626
-
1627
- /**
1628
- * 스칼라 서브쿼리 - SELECT 절에서 단일 반환 서브쿼리
1629
- *
1630
- * 서브쿼리는 반드시 단일 행, 단일 컬럼을 반환해야 함
1631
- *
1632
- * @param dataType - 반환될 값의 데이터 타입
1633
- * @param queryable - 스칼라 값을 반환하는 Queryable
1634
- * @returns 서브쿼리 결과 표현식
1635
- *
1636
- * @example
1637
- * ```typescript
1638
- * db.user().select((u) => ({
1639
- * id: u.id,
1640
- * postCount: expr.subquery(
1641
- * "number",
1642
- * db.post()
1643
- * .where((p) => [expr.eq(p.userId, u.id)])
1644
- * .select(() => ({ cnt: expr.count() }))
1645
- * ),
1646
- * }))
1647
- * // SELECT id, (SELECT COUNT(*) FROM Post WHERE userId = User.id) AS postCount
1648
- * ```
1649
- */
1650
- subquery<TStr extends ColumnPrimitiveStr>(
1651
- dataType: TStr,
1652
- queryable: { getSelectQueryDef(): SelectQueryDef },
1653
- ): ExprUnit<ColumnPrimitiveMap[TStr] | undefined> {
1654
- return new ExprUnit(dataType, {
1655
- type: "subquery",
1656
- queryDef: queryable.getSelectQueryDef(),
1657
- });
1658
- },
1659
-
1660
- //#endregion
1661
-
1662
- //#region ========== SELECT - Window Functions ==========
1663
-
1664
- /**
1665
- * ROW_NUMBER() - 파티션 내 번호
1666
- *
1667
- * 각 파티션 내에서 1부터 시작하는 순차 번호 부여
1668
- *
1669
- * @param spec - 윈도우 스펙 (partitionBy, orderBy)
1670
- * @returns 번호 (1부터 시작)
1671
- *
1672
- * @example
1673
- * ```typescript
1674
- * db.order().select((o) => ({
1675
- * ...o,
1676
- * rowNum: expr.rowNumber({
1677
- * partitionBy: [o.userId],
1678
- * orderBy: [[o.createdAt, "DESC"]],
1679
- * }),
1680
- * }))
1681
- * // SELECT *, ROW_NUMBER() OVER (PARTITION BY userId ORDER BY createdAt DESC)
1682
- * ```
1683
- */
1684
- rowNumber(spec: WinSpecInput): ExprUnit<number> {
1685
- return new ExprUnit("number", {
1686
- type: "window",
1687
- fn: { type: "rowNumber" },
1688
- spec: toWinSpec(spec),
1689
- });
1690
- },
1691
-
1692
- /**
1693
- * RANK() - 파티션 내 순위 (동점 시 같은 순위, 다음 순위 건너뜀)
1694
- *
1695
- * @param spec - 윈도우 스펙 (partitionBy, orderBy)
1696
- * @returns 순위 (동점 후 건너뜀: 1, 1, 3)
1697
- *
1698
- * @example
1699
- * ```typescript
1700
- * db.student().select((s) => ({
1701
- * name: s.name,
1702
- * rank: expr.rank({
1703
- * orderBy: [[s.score, "DESC"]],
1704
- * }),
1705
- * }))
1706
- * ```
1707
- */
1708
- rank(spec: WinSpecInput): ExprUnit<number> {
1709
- return new ExprUnit("number", {
1710
- type: "window",
1711
- fn: { type: "rank" },
1712
- spec: toWinSpec(spec),
1713
- });
1714
- },
1715
-
1716
- /**
1717
- * DENSE_RANK() - 파티션 내 밀집 순위 (동점 시 같은 순위, 다음 순위 유지)
1718
- *
1719
- * @param spec - 윈도우 스펙 (partitionBy, orderBy)
1720
- * @returns 밀집 순위 (동점 후 연속: 1, 1, 2)
1721
- *
1722
- * @example
1723
- * ```typescript
1724
- * db.student().select((s) => ({
1725
- * name: s.name,
1726
- * denseRank: expr.denseRank({
1727
- * orderBy: [[s.score, "DESC"]],
1728
- * }),
1729
- * }))
1730
- * ```
1731
- */
1732
- denseRank(spec: WinSpecInput): ExprUnit<number> {
1733
- return new ExprUnit("number", {
1734
- type: "window",
1735
- fn: { type: "denseRank" },
1736
- spec: toWinSpec(spec),
1737
- });
1738
- },
1739
-
1740
- /**
1741
- * NTILE(n) - 파티션을 n개 그룹으로 분할
1742
- *
1743
- * @param n - 분할할 그룹 수
1744
- * @param spec - 윈도우 스펙 (partitionBy, orderBy)
1745
- * @returns 그룹 번호 (1 ~ n)
1746
- *
1747
- * @example
1748
- * ```typescript
1749
- * // 상위 25%를 찾기 위한 사분위 분할
1750
- * db.user().select((u) => ({
1751
- * name: u.name,
1752
- * quartile: expr.ntile(4, {
1753
- * orderBy: [[u.score, "DESC"]],
1754
- * }),
1755
- * }))
1756
- * ```
1757
- */
1758
- ntile(n: number, spec: WinSpecInput): ExprUnit<number> {
1759
- return new ExprUnit("number", {
1760
- type: "window",
1761
- fn: { type: "ntile", n },
1762
- spec: toWinSpec(spec),
1763
- });
1764
- },
1765
-
1766
- /**
1767
- * LAG() - 이전 행의 참조
1768
- *
1769
- * @param column - 참조할 컬럼
1770
- * @param spec - 윈도우 스펙 (partitionBy, orderBy)
1771
- * @param options - offset (기본 1), default (이전 행이 없을 때 기본값)
1772
- * @returns 이전 행의 (또는 기본값/NULL)
1773
- *
1774
- * @example
1775
- * ```typescript
1776
- * db.stock().select((s) => ({
1777
- * date: s.date,
1778
- * price: s.price,
1779
- * prevPrice: expr.lag(s.price, {
1780
- * partitionBy: [s.symbol],
1781
- * orderBy: [[s.date, "ASC"]],
1782
- * }),
1783
- * }))
1784
- * ```
1785
- */
1786
- lag<T extends ColumnPrimitive>(
1787
- column: ExprUnit<T>,
1788
- spec: WinSpecInput,
1789
- options?: { offset?: number; default?: ExprInput<T> },
1790
- ): ExprUnit<T | undefined> {
1791
- return new ExprUnit(column.dataType, {
1792
- type: "window",
1793
- fn: {
1794
- type: "lag",
1795
- column: toExpr(column),
1796
- offset: options?.offset,
1797
- default: options?.default != null ? toExpr(options.default) : undefined,
1798
- },
1799
- spec: toWinSpec(spec),
1800
- });
1801
- },
1802
-
1803
- /**
1804
- * LEAD() - 다음 행의 참조
1805
- *
1806
- * @param column - 참조할 컬럼
1807
- * @param spec - 윈도우 스펙 (partitionBy, orderBy)
1808
- * @param options - offset (기본 1), default (다음 행이 없을 때 기본값)
1809
- * @returns 다음 행의 (또는 기본값/NULL)
1810
- *
1811
- * @example
1812
- * ```typescript
1813
- * db.stock().select((s) => ({
1814
- * date: s.date,
1815
- * price: s.price,
1816
- * nextPrice: expr.lead(s.price, {
1817
- * partitionBy: [s.symbol],
1818
- * orderBy: [[s.date, "ASC"]],
1819
- * }),
1820
- * }))
1821
- * ```
1822
- */
1823
- lead<T extends ColumnPrimitive>(
1824
- column: ExprUnit<T>,
1825
- spec: WinSpecInput,
1826
- options?: { offset?: number; default?: ExprInput<T> },
1827
- ): ExprUnit<T | undefined> {
1828
- return new ExprUnit(column.dataType, {
1829
- type: "window",
1830
- fn: {
1831
- type: "lead",
1832
- column: toExpr(column),
1833
- offset: options?.offset,
1834
- default: options?.default != null ? toExpr(options.default) : undefined,
1835
- },
1836
- spec: toWinSpec(spec),
1837
- });
1838
- },
1839
-
1840
- /**
1841
- * FIRST_VALUE() - 파티션/프레임의 첫 번째
1842
- *
1843
- * @param column - 참조할 컬럼
1844
- * @param spec - 윈도우 스펙 (partitionBy, orderBy)
1845
- * @returns 첫 번째
1846
- *
1847
- * @example
1848
- * ```typescript
1849
- * db.order().select((o) => ({
1850
- * ...o,
1851
- * firstOrderAmount: expr.firstValue(o.amount, {
1852
- * partitionBy: [o.userId],
1853
- * orderBy: [[o.createdAt, "ASC"]],
1854
- * }),
1855
- * }))
1856
- * ```
1857
- */
1858
- firstValue<T extends ColumnPrimitive>(
1859
- column: ExprUnit<T>,
1860
- spec: WinSpecInput,
1861
- ): ExprUnit<T | undefined> {
1862
- return new ExprUnit(column.dataType, {
1863
- type: "window",
1864
- fn: { type: "firstValue", column: toExpr(column) },
1865
- spec: toWinSpec(spec),
1866
- });
1867
- },
1868
-
1869
- /**
1870
- * LAST_VALUE() - 파티션/프레임의 마지막
1871
- *
1872
- * @param column - 참조할 컬럼
1873
- * @param spec - 윈도우 스펙 (partitionBy, orderBy)
1874
- * @returns 마지막
1875
- *
1876
- * @example
1877
- * ```typescript
1878
- * db.order().select((o) => ({
1879
- * ...o,
1880
- * lastOrderAmount: expr.lastValue(o.amount, {
1881
- * partitionBy: [o.userId],
1882
- * orderBy: [[o.createdAt, "ASC"]],
1883
- * }),
1884
- * }))
1885
- * ```
1886
- */
1887
- lastValue<T extends ColumnPrimitive>(
1888
- column: ExprUnit<T>,
1889
- spec: WinSpecInput,
1890
- ): ExprUnit<T | undefined> {
1891
- return new ExprUnit(column.dataType, {
1892
- type: "window",
1893
- fn: { type: "lastValue", column: toExpr(column) },
1894
- spec: toWinSpec(spec),
1895
- });
1896
- },
1897
-
1898
- /**
1899
- * SUM() OVER - 윈도우 합계
1900
- *
1901
- * @param column - 합계를 구할 컬럼
1902
- * @param spec - 윈도우 스펙 (partitionBy, orderBy)
1903
- * @returns 윈도우 내 합계
1904
- *
1905
- * @example
1906
- * ```typescript
1907
- * // 누적 합계
1908
- * db.order().select((o) => ({
1909
- * ...o,
1910
- * runningTotal: expr.sumOver(o.amount, {
1911
- * partitionBy: [o.userId],
1912
- * orderBy: [[o.createdAt, "ASC"]],
1913
- * }),
1914
- * }))
1915
- * ```
1916
- */
1917
- sumOver(column: ExprUnit<number | undefined>, spec: WinSpecInput): ExprUnit<number | undefined> {
1918
- return new ExprUnit("number", {
1919
- type: "window",
1920
- fn: { type: "sum", column: toExpr(column) },
1921
- spec: toWinSpec(spec),
1922
- });
1923
- },
1924
-
1925
- /**
1926
- * AVG() OVER - 윈도우 평균
1927
- *
1928
- * @param column - 평균을 구할 컬럼
1929
- * @param spec - 윈도우 스펙 (partitionBy, orderBy)
1930
- * @returns 윈도우 내 평균
1931
- *
1932
- * @example
1933
- * ```typescript
1934
- * // 이동 평균
1935
- * db.stock().select((s) => ({
1936
- * ...s,
1937
- * movingAvg: expr.avgOver(s.price, {
1938
- * partitionBy: [s.symbol],
1939
- * orderBy: [[s.date, "ASC"]],
1940
- * }),
1941
- * }))
1942
- * ```
1943
- */
1944
- avgOver(column: ExprUnit<number | undefined>, spec: WinSpecInput): ExprUnit<number | undefined> {
1945
- return new ExprUnit("number", {
1946
- type: "window",
1947
- fn: { type: "avg", column: toExpr(column) },
1948
- spec: toWinSpec(spec),
1949
- });
1950
- },
1951
-
1952
- /**
1953
- * COUNT() OVER - 윈도우 카운트
1954
- *
1955
- * @param spec - 윈도우 스펙 (partitionBy, orderBy)
1956
- * @param column - 카운트할 컬럼 (생략 시 전체 수)
1957
- * @returns 윈도우
1958
- *
1959
- * @example
1960
- * ```typescript
1961
- * db.order().select((o) => ({
1962
- * ...o,
1963
- * totalOrdersPerUser: expr.countOver({
1964
- * partitionBy: [o.userId],
1965
- * }),
1966
- * }))
1967
- * ```
1968
- */
1969
- countOver(spec: WinSpecInput, column?: ExprUnit<ColumnPrimitive>): ExprUnit<number> {
1970
- return new ExprUnit("number", {
1971
- type: "window",
1972
- fn: { type: "count", column: column != null ? toExpr(column) : undefined },
1973
- spec: toWinSpec(spec),
1974
- });
1975
- },
1976
-
1977
- /**
1978
- * MIN() OVER - 윈도우 최소값
1979
- *
1980
- * @param column - 최소값을 구할 컬럼
1981
- * @param spec - 윈도우 스펙 (partitionBy, orderBy)
1982
- * @returns 윈도우 내 최소값
1983
- *
1984
- * @example
1985
- * ```typescript
1986
- * db.stock().select((s) => ({
1987
- * ...s,
1988
- * minPriceInPeriod: expr.minOver(s.price, {
1989
- * partitionBy: [s.symbol],
1990
- * }),
1991
- * }))
1992
- * ```
1993
- */
1994
- minOver<T extends ColumnPrimitive>(
1995
- column: ExprUnit<T>,
1996
- spec: WinSpecInput,
1997
- ): ExprUnit<T | undefined> {
1998
- return new ExprUnit(column.dataType, {
1999
- type: "window",
2000
- fn: { type: "min", column: toExpr(column) },
2001
- spec: toWinSpec(spec),
2002
- });
2003
- },
2004
-
2005
- /**
2006
- * MAX() OVER - 윈도우 최대값
2007
- *
2008
- * @param column - 최대값을 구할 컬럼
2009
- * @param spec - 윈도우 스펙 (partitionBy, orderBy)
2010
- * @returns 윈도우 내 최대값
2011
- *
2012
- * @example
2013
- * ```typescript
2014
- * db.stock().select((s) => ({
2015
- * ...s,
2016
- * maxPriceInPeriod: expr.maxOver(s.price, {
2017
- * partitionBy: [s.symbol],
2018
- * }),
2019
- * }))
2020
- * ```
2021
- */
2022
- maxOver<T extends ColumnPrimitive>(
2023
- column: ExprUnit<T>,
2024
- spec: WinSpecInput,
2025
- ): ExprUnit<T | undefined> {
2026
- return new ExprUnit(column.dataType, {
2027
- type: "window",
2028
- fn: { type: "max", column: toExpr(column) },
2029
- spec: toWinSpec(spec),
2030
- });
2031
- },
2032
-
2033
- //#endregion
2034
-
2035
- //#region ========== Helper ==========
2036
-
2037
- /**
2038
- * ExprInput을 Expr로 변환 (내부용)
2039
- *
2040
- * @param value - 변환할
2041
- * @returns Expr JSON AST
2042
- */
2043
- toExpr(value: ExprInput<ColumnPrimitive>): Expr {
2044
- return toExpr(value);
2045
- },
2046
-
2047
- //#endregion
2048
- };
2049
-
2050
- //#region ========== Internal Helpers ==========
2051
-
2052
- // 여러 중 첫 번째 non-null 반환 (COALESCE)
2053
- function ifNull<TPrimitive extends ColumnPrimitive>(
2054
- ...args: [
2055
- ExprInput<TPrimitive | undefined>,
2056
- ...ExprInput<TPrimitive | undefined>[],
2057
- ExprInput<NonNullable<TPrimitive>>,
2058
- ]
2059
- ): ExprUnit<NonNullable<TPrimitive>>;
2060
- function ifNull<TPrimitive extends ColumnPrimitive>(
2061
- ...args: ExprInput<TPrimitive>[]
2062
- ): ExprUnit<TPrimitive>;
2063
- function ifNull<TPrimitive extends ColumnPrimitive>(
2064
- ...args: ExprInput<TPrimitive>[]
2065
- ): ExprUnit<TPrimitive> {
2066
- return new ExprUnit(findDataType(args), {
2067
- type: "ifNull",
2068
- args: args.map((a) => toExpr(a)),
2069
- });
2070
- }
2071
-
2072
- function createSwitchBuilder<TPrimitive extends ColumnPrimitive>(): SwitchExprBuilder<TPrimitive> {
2073
- const cases: { when: WhereExpr; then: Expr }[] = [];
2074
- const thenValues: ExprInput<TPrimitive>[] = []; // then 값들 저장
2075
-
2076
- return {
2077
- case(condition: WhereExprUnit, then: ExprInput<TPrimitive>): typeof this {
2078
- cases.push({
2079
- when: condition.expr,
2080
- then: toExpr(then),
2081
- });
2082
- thenValues.push(then);
2083
- return this;
2084
- },
2085
- default(value: ExprInput<TPrimitive>): ExprUnit<TPrimitive> {
2086
- const allValues = [...thenValues, value];
2087
- // 1. ExprUnit에서 dataType 찾기
2088
- const exprUnit = allValues.find((v): v is ExprUnit<TPrimitive> => v instanceof ExprUnit);
2089
- if (exprUnit) {
2090
- return new ExprUnit(exprUnit.dataType, {
2091
- type: "switch",
2092
- cases,
2093
- else: toExpr(value),
2094
- });
2095
- }
2096
-
2097
- // 2. non-null 리터럴에서 추론
2098
- const nonNullLiteral = allValues.find((v) => v != null) as ColumnPrimitive;
2099
- if (nonNullLiteral == null) {
2100
- throw new Error("switch case/default 적어도 하나는 non-null이어야 합니다.");
2101
- }
2102
-
2103
- return new ExprUnit(inferColumnPrimitiveStr(nonNullLiteral), {
2104
- type: "switch",
2105
- cases,
2106
- else: toExpr(value),
2107
- });
2108
- },
2109
- };
2110
- }
2111
-
2112
- export function toExpr(value: ExprInput<ColumnPrimitive>): Expr {
2113
- if (value instanceof ExprUnit) {
2114
- return value.expr;
2115
- }
2116
- return { type: "value", value };
2117
- }
2118
-
2119
- function findDataType<TPrimitive extends ColumnPrimitive>(
2120
- args: ExprInput<TPrimitive>[],
2121
- ): ColumnPrimitiveStr {
2122
- const exprUnit = args.find((a): a is ExprUnit<TPrimitive> => a instanceof ExprUnit);
2123
- if (!exprUnit) {
2124
- throw new Error("args중 적어도 하나는 ExprUnit이어야 합니다.");
2125
- }
2126
- return exprUnit.dataType;
2127
- }
2128
-
2129
- function toWinSpec(spec: WinSpecInput): WinSpec {
2130
- const result: WinSpec = {};
2131
- if (spec.partitionBy != null) {
2132
- result.partitionBy = spec.partitionBy.map((e) => toExpr(e));
2133
- }
2134
- if (spec.orderBy != null) {
2135
- result.orderBy = spec.orderBy.map(([e, dir]) => [toExpr(e), dir]);
2136
- }
2137
- return result;
2138
- }
2139
-
2140
- //#endregion
1
+ import { ArgumentError, type DateOnly, type DateTime, type Time } from "@simplysm/core-common";
2
+ import {
3
+ type ColumnPrimitive,
4
+ type ColumnPrimitiveMap,
5
+ type ColumnPrimitiveStr,
6
+ type DataType,
7
+ dataTypeStrToColumnPrimitiveStr,
8
+ type InferColumnPrimitiveFromDataType,
9
+ inferColumnPrimitiveStr,
10
+ } from "../types/column";
11
+ import type { ExprInput } from "./expr-unit";
12
+ import { ExprUnit, WhereExprUnit } from "./expr-unit";
13
+ import type { Expr, DateSeparator, WhereExpr, WinSpec } from "../types/expr";
14
+ import type { SelectQueryDef } from "../types/query-def";
15
+ import type { Queryable } from "../exec/queryable";
16
+
17
+ // Window Function Spec Input (for user API)
18
+ interface WinSpecInput {
19
+ partitionBy?: ExprInput<ColumnPrimitive>[];
20
+ orderBy?: [ExprInput<ColumnPrimitive>, ("ASC" | "DESC")?][];
21
+ }
22
+
23
+ /**
24
+ * Switch expression builder interface
25
+ */
26
+ export interface SwitchExprBuilder<TPrimitive extends ColumnPrimitive> {
27
+ case(condition: WhereExprUnit, then: ExprInput<TPrimitive>): SwitchExprBuilder<TPrimitive>;
28
+ default(value: ExprInput<TPrimitive>): ExprUnit<TPrimitive>;
29
+ }
30
+
31
+ /**
32
+ * Dialect-independent SQL expression builder
33
+ *
34
+ * Generates JSON AST (Expr) instead of SQL strings, which QueryBuilder
35
+ * converts to each DBMS (MySQL, MSSQL, PostgreSQL)
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * // WHERE condition
40
+ * db.user().where((u) => [
41
+ * expr.eq(u.status, "active"),
42
+ * expr.gt(u.age, 18),
43
+ * ])
44
+ *
45
+ * // SELECT expression
46
+ * db.user().select((u) => ({
47
+ * name: expr.concat(u.firstName, " ", u.lastName),
48
+ * age: expr.dateDiff("year", u.birthDate, expr.val("DateOnly", DateOnly.today())),
49
+ * }))
50
+ *
51
+ * // Aggregate function
52
+ * db.order().groupBy((o) => o.userId).select((o) => ({
53
+ * userId: o.userId,
54
+ * total: expr.sum(o.amount),
55
+ * }))
56
+ * ```
57
+ *
58
+ * @see {@link Queryable} Query builder class
59
+ * @see {@link ExprUnit} Expression wrapper class
60
+ */
61
+ export const expr = {
62
+ //#region ========== Value creation ==========
63
+
64
+ /**
65
+ * Wrap literal value as ExprUnit
66
+ *
67
+ * Widen to base type matching dataType, remove literal type
68
+ *
69
+ * @param dataType - Value의 data type ("string", "number", "boolean", "DateTime", "DateOnly", "Time", "Uuid", "Buffer")
70
+ * @param value - 래핑할 value (undefined allow)
71
+ * @returns 래핑된 ExprUnit instance
72
+ *
73
+ * @example
74
+ * ```typescript
75
+ * // String value
76
+ * expr.val("string", "active")
77
+ *
78
+ * // Number value
79
+ * expr.val("number", 100)
80
+ *
81
+ * // Date value
82
+ * expr.val("DateOnly", DateOnly.today())
83
+ *
84
+ * // undefined value
85
+ * expr.val("string", undefined)
86
+ * ```
87
+ */
88
+ val<TStr extends ColumnPrimitiveStr, T extends ColumnPrimitiveMap[TStr] | undefined>(
89
+ dataType: TStr,
90
+ value: T,
91
+ ): ExprUnit<
92
+ T extends undefined ? ColumnPrimitiveMap[TStr] | undefined : ColumnPrimitiveMap[TStr]
93
+ > {
94
+ return new ExprUnit(dataType, { type: "value", value });
95
+ },
96
+
97
+ /**
98
+ * Generate column reference
99
+ *
100
+ * Typically proxy objects are used inside Queryable callbacks
101
+ *
102
+ * @param dataType - Column data type
103
+ * @param path - Column path (table alias, column name, etc.)
104
+ * @returns Column reference ExprUnit instance
105
+ *
106
+ * @example
107
+ * ```typescript
108
+ * // Direct column reference (internal use)
109
+ * expr.col("string", "T1", "name")
110
+ * ```
111
+ */
112
+ col<TStr extends ColumnPrimitiveStr>(
113
+ dataType: ColumnPrimitiveStr,
114
+ ...path: string[]
115
+ ): ExprUnit<ColumnPrimitiveMap[TStr] | undefined> {
116
+ return new ExprUnit(dataType, { type: "column", path });
117
+ },
118
+
119
+ /**
120
+ * Raw SQL expression Generate (escape hatch)
121
+ *
122
+ * Use when you need to directly use DB-specific functions or syntax not supported by the ORM.
123
+ * Used as tagged template literal, interpolated values are automatically parameterized
124
+ *
125
+ * @param dataType - Data type of the returned value
126
+ * @returns Tagged template function
127
+ *
128
+ * @example
129
+ * ```typescript
130
+ * // Using MySQL JSON function
131
+ * db.user().select((u) => ({
132
+ * name: u.name,
133
+ * data: expr.raw("string")`JSON_EXTRACT(${u.metadata}, '$.email')`,
134
+ * }))
135
+ *
136
+ * // PostgreSQL array function
137
+ * expr.raw("number")`ARRAY_LENGTH(${u.tags}, 1)`
138
+ * ```
139
+ */
140
+ raw<T extends ColumnPrimitiveStr>(
141
+ dataType: T,
142
+ ): (
143
+ strings: TemplateStringsArray,
144
+ ...values: ExprInput<ColumnPrimitive>[]
145
+ ) => ExprUnit<ColumnPrimitiveMap[T] | undefined> {
146
+ return (strings, ...values) => {
147
+ const sql = strings.reduce((acc, str, i) => {
148
+ if (i < values.length) {
149
+ return acc + str + `$${i + 1}`; // placeholder (transformed by ExprRenderer)
150
+ }
151
+ return acc + str;
152
+ }, "");
153
+
154
+ const params = values.map((v) => toExpr(v));
155
+
156
+ return new ExprUnit(dataType, { type: "raw", sql, params });
157
+ };
158
+ },
159
+
160
+ //#endregion
161
+
162
+ //#region ========== WHERE - Comparison operators ==========
163
+
164
+ /**
165
+ * Equality comparison (NULL-safe)
166
+ *
167
+ * Safely compare even NULL values (MySQL: `<=>`, MSSQL/PostgreSQL: `IS NULL OR =`)
168
+ *
169
+ * @param source - Column or expression to compare
170
+ * @param target - Target value or expression for comparison
171
+ * @returns WHERE condition expression
172
+ *
173
+ * @example
174
+ * ```typescript
175
+ * db.user().where((u) => [expr.eq(u.status, "active")])
176
+ * // WHERE status <=> 'active' (MySQL)
177
+ * ```
178
+ */
179
+ eq<T extends ColumnPrimitive>(source: ExprUnit<T>, target: ExprInput<T>): WhereExprUnit {
180
+ return new WhereExprUnit({
181
+ type: "eq",
182
+ source: toExpr(source),
183
+ target: toExpr(target),
184
+ });
185
+ },
186
+
187
+ /**
188
+ * Greater than comparison (>)
189
+ *
190
+ * @param source - Column or expression to compare
191
+ * @param target - Target value or expression for comparison
192
+ * @returns WHERE condition expression
193
+ *
194
+ * @example
195
+ * ```typescript
196
+ * db.user().where((u) => [expr.gt(u.age, 18)])
197
+ * // WHERE age > 18
198
+ * ```
199
+ */
200
+ gt<T extends ColumnPrimitive>(source: ExprUnit<T>, target: ExprInput<T>): WhereExprUnit {
201
+ return new WhereExprUnit({
202
+ type: "gt",
203
+ source: toExpr(source),
204
+ target: toExpr(target),
205
+ });
206
+ },
207
+
208
+ /**
209
+ * Less than comparison (<)
210
+ *
211
+ * @param source - Column or expression to compare
212
+ * @param target - Target value or expression for comparison
213
+ * @returns WHERE condition expression
214
+ *
215
+ * @example
216
+ * ```typescript
217
+ * db.user().where((u) => [expr.lt(u.score, 60)])
218
+ * // WHERE score < 60
219
+ * ```
220
+ */
221
+ lt<T extends ColumnPrimitive>(source: ExprUnit<T>, target: ExprInput<T>): WhereExprUnit {
222
+ return new WhereExprUnit({
223
+ type: "lt",
224
+ source: toExpr(source),
225
+ target: toExpr(target),
226
+ });
227
+ },
228
+
229
+ /**
230
+ * Greater than or equal comparison (>=)
231
+ *
232
+ * @param source - Column or expression to compare
233
+ * @param target - Target value or expression for comparison
234
+ * @returns WHERE condition expression
235
+ *
236
+ * @example
237
+ * ```typescript
238
+ * db.user().where((u) => [expr.gte(u.age, 18)])
239
+ * // WHERE age >= 18
240
+ * ```
241
+ */
242
+ gte<T extends ColumnPrimitive>(source: ExprUnit<T>, target: ExprInput<T>): WhereExprUnit {
243
+ return new WhereExprUnit({
244
+ type: "gte",
245
+ source: toExpr(source),
246
+ target: toExpr(target),
247
+ });
248
+ },
249
+
250
+ /**
251
+ * Less than or equal comparison (<=)
252
+ *
253
+ * @param source - Column or expression to compare
254
+ * @param target - Target value or expression for comparison
255
+ * @returns WHERE condition expression
256
+ *
257
+ * @example
258
+ * ```typescript
259
+ * db.user().where((u) => [expr.lte(u.score, 100)])
260
+ * // WHERE score <= 100
261
+ * ```
262
+ */
263
+ lte<T extends ColumnPrimitive>(source: ExprUnit<T>, target: ExprInput<T>): WhereExprUnit {
264
+ return new WhereExprUnit({
265
+ type: "lte",
266
+ source: toExpr(source),
267
+ target: toExpr(target),
268
+ });
269
+ },
270
+
271
+ /**
272
+ * range comparison (BETWEEN)
273
+ *
274
+ * from/to가 undefined이면 해당 방향은 무제한
275
+ *
276
+ * @param source - Column or expression to compare
277
+ * @param from - start value (undefined이면 하한 N/A)
278
+ * @param to - 끝 value (undefined이면 상한 N/A)
279
+ * @returns WHERE condition expression
280
+ *
281
+ * @example
282
+ * ```typescript
283
+ * // range 지정
284
+ * db.user().where((u) => [expr.between(u.age, 18, 65)])
285
+ * // WHERE age BETWEEN 18 AND 65
286
+ *
287
+ * // Specify only lower bound
288
+ * db.user().where((u) => [expr.between(u.age, 18, undefined)])
289
+ * // WHERE age >= 18
290
+ * ```
291
+ */
292
+ between<T extends ColumnPrimitive>(
293
+ source: ExprUnit<T>,
294
+ from?: ExprInput<T>,
295
+ to?: ExprInput<T>,
296
+ ): WhereExprUnit {
297
+ return new WhereExprUnit({
298
+ type: "between",
299
+ source: toExpr(source),
300
+ from: from != null ? toExpr(from) : undefined,
301
+ to: to != null ? toExpr(to) : undefined,
302
+ });
303
+ },
304
+
305
+ //#endregion
306
+
307
+ //#region ========== WHERE - NULL check ==========
308
+
309
+ /**
310
+ * NULL 체크 (IS NULL)
311
+ *
312
+ * @param source - 체크할 column 또는 expression
313
+ * @returns WHERE condition expression
314
+ *
315
+ * @example
316
+ * ```typescript
317
+ * db.user().where((u) => [expr.null(u.deletedAt)])
318
+ * // WHERE deletedAt IS NULL
319
+ * ```
320
+ */
321
+ null<T extends ColumnPrimitive>(source: ExprUnit<T>): WhereExprUnit {
322
+ return new WhereExprUnit({
323
+ type: "null",
324
+ arg: toExpr(source),
325
+ });
326
+ },
327
+
328
+ //#endregion
329
+
330
+ //#region ========== WHERE - String search ==========
331
+
332
+ /**
333
+ * LIKE pattern 매칭
334
+ *
335
+ * `%`는 0개 이상의 문자, `_`는 단일 문자와 매칭.
336
+ * 특수문자는 `\`로 escape됨
337
+ *
338
+ * @param source - 검색할 column 또는 expression
339
+ * @param pattern - 검색 pattern (%, _ wildcard 사용 가능)
340
+ * @returns WHERE condition expression
341
+ *
342
+ * @example
343
+ * ```typescript
344
+ * // 접두사 검색
345
+ * db.user().where((u) => [expr.like(u.name, "John%")])
346
+ * // WHERE name LIKE 'John%' ESCAPE '\'
347
+ *
348
+ * // include 검색
349
+ * db.user().where((u) => [expr.like(u.email, "%@gmail.com")])
350
+ * ```
351
+ */
352
+ like(
353
+ source: ExprUnit<string | undefined>,
354
+ pattern: ExprInput<string | undefined>,
355
+ ): WhereExprUnit {
356
+ return new WhereExprUnit({
357
+ type: "like",
358
+ source: toExpr(source),
359
+ pattern: toExpr(pattern),
360
+ });
361
+ },
362
+
363
+ /**
364
+ * regular expression pattern 매칭
365
+ *
366
+ * DBMS별 regular expression 문법 차이 주의 필요
367
+ *
368
+ * @param source - 검색할 column 또는 expression
369
+ * @param pattern - regular expression pattern
370
+ * @returns WHERE condition expression
371
+ *
372
+ * @example
373
+ * ```typescript
374
+ * db.user().where((u) => [expr.regexp(u.email, "^[a-z]+@")])
375
+ * // MySQL: WHERE email REGEXP '^[a-z]+@'
376
+ * ```
377
+ */
378
+ regexp(
379
+ source: ExprUnit<string | undefined>,
380
+ pattern: ExprInput<string | undefined>,
381
+ ): WhereExprUnit {
382
+ return new WhereExprUnit({
383
+ type: "regexp",
384
+ source: toExpr(source),
385
+ pattern: toExpr(pattern),
386
+ });
387
+ },
388
+
389
+ //#endregion
390
+
391
+ //#region ========== WHERE - IN ==========
392
+
393
+ /**
394
+ * IN operator - Value 목록과 comparison
395
+ *
396
+ * @param source - Column or expression to compare
397
+ * @param values - 비교할 value 목록
398
+ * @returns WHERE condition expression
399
+ *
400
+ * @example
401
+ * ```typescript
402
+ * db.user().where((u) => [expr.in(u.status, ["active", "pending"])])
403
+ * // WHERE status IN ('active', 'pending')
404
+ * ```
405
+ */
406
+ in<T extends ColumnPrimitive>(source: ExprUnit<T>, values: ExprInput<T>[]): WhereExprUnit {
407
+ return new WhereExprUnit({
408
+ type: "in",
409
+ source: toExpr(source),
410
+ values: values.map((v) => toExpr(v)),
411
+ });
412
+ },
413
+
414
+ /**
415
+ * IN (SELECT ...) - Subquery 결과와 comparison
416
+ *
417
+ * Subquery는 반드시 단일 column만 SELECT해야 함
418
+ *
419
+ * @param source - Column or expression to compare
420
+ * @param query - 단일 column을 반환하는 Queryable
421
+ * @returns WHERE condition expression
422
+ * @throws {Error} Subquery가 단일 column이 아닌 경우
423
+ *
424
+ * @example
425
+ * ```typescript
426
+ * db.user().where((u) => [
427
+ * expr.inQuery(
428
+ * u.id,
429
+ * db.order()
430
+ * .where((o) => [expr.gt(o.amount, 1000)])
431
+ * .select((o) => ({ userId: o.userId }))
432
+ * ),
433
+ * ])
434
+ * // WHERE id IN (SELECT userId FROM Order WHERE amount > 1000)
435
+ * ```
436
+ */
437
+ inQuery<T extends ColumnPrimitive, TData extends Record<string, T>>(
438
+ source: ExprUnit<T>,
439
+ query: Queryable<TData, any>,
440
+ ): WhereExprUnit {
441
+ const queryDef = query.getSelectQueryDef();
442
+ if (queryDef.select == null || Object.keys(queryDef.select).length !== 1) {
443
+ throw new Error("inQuery subquery must SELECT only a single column.");
444
+ }
445
+ return new WhereExprUnit({
446
+ type: "inQuery",
447
+ source: toExpr(source),
448
+ query: queryDef,
449
+ });
450
+ },
451
+
452
+ /**
453
+ * EXISTS (SELECT ...) - Subquery result 존재 여부 확인
454
+ *
455
+ * Subquery가 하나 이상의 행을 반환하면 true
456
+ *
457
+ * @param query - 존재 여부를 확인할 Queryable
458
+ * @returns WHERE condition expression
459
+ *
460
+ * @example
461
+ * ```typescript
462
+ * // 주문이 있는 사용자 조회
463
+ * db.user().where((u) => [
464
+ * expr.exists(
465
+ * db.order().where((o) => [expr.eq(o.userId, u.id)])
466
+ * ),
467
+ * ])
468
+ * // WHERE EXISTS (SELECT 1 FROM Order WHERE userId = User.id)
469
+ * ```
470
+ */
471
+ exists(query: Queryable<any, any>): WhereExprUnit {
472
+ const { select: _, ...queryDefWithoutSelect } = query.getSelectQueryDef(); // EXISTS는 SELECT 절 불필요, 패킷 절약
473
+ return new WhereExprUnit({
474
+ type: "exists",
475
+ query: queryDefWithoutSelect,
476
+ });
477
+ },
478
+
479
+ //#endregion
480
+
481
+ //#region ========== WHERE - Logical operators ==========
482
+
483
+ /**
484
+ * NOT operator - Condition 부정
485
+ *
486
+ * @param arg - 부정할 condition
487
+ * @returns 부정된 WHERE condition expression
488
+ *
489
+ * @example
490
+ * ```typescript
491
+ * db.user().where((u) => [expr.not(expr.eq(u.status, "deleted"))])
492
+ * // WHERE NOT (status <=> 'deleted')
493
+ * ```
494
+ */
495
+ not(arg: WhereExprUnit): WhereExprUnit {
496
+ return new WhereExprUnit({
497
+ type: "not",
498
+ arg: arg.expr,
499
+ });
500
+ },
501
+
502
+ /**
503
+ * AND operator - 모든 condition 충족
504
+ *
505
+ * 여러 조건을 AND로 결합. where() 메서드에 배열로 전달하면 automatic으로 AND applied
506
+ *
507
+ * @param conditions - AND로 결합할 condition 목록
508
+ * @returns 결합된 WHERE condition expression
509
+ *
510
+ * @example
511
+ * ```typescript
512
+ * db.user().where((u) => [
513
+ * expr.and([
514
+ * expr.eq(u.status, "active"),
515
+ * expr.gte(u.age, 18),
516
+ * ]),
517
+ * ])
518
+ * // WHERE (status <=> 'active' AND age >= 18)
519
+ * ```
520
+ */
521
+ and(conditions: WhereExprUnit[]): WhereExprUnit {
522
+ if (conditions.length === 0) {
523
+ throw new ArgumentError({ conditions: "empty arrays are not allowed" });
524
+ }
525
+ return new WhereExprUnit({
526
+ type: "and",
527
+ conditions: conditions.map((c) => c.expr),
528
+ });
529
+ },
530
+
531
+ /**
532
+ * OR operator - 하나 이상의 condition 충족
533
+ *
534
+ * @param conditions - OR로 결합할 condition 목록
535
+ * @returns 결합된 WHERE condition expression
536
+ *
537
+ * @example
538
+ * ```typescript
539
+ * db.user().where((u) => [
540
+ * expr.or([
541
+ * expr.eq(u.status, "active"),
542
+ * expr.eq(u.status, "pending"),
543
+ * ]),
544
+ * ])
545
+ * // WHERE (status <=> 'active' OR status <=> 'pending')
546
+ * ```
547
+ */
548
+ or(conditions: WhereExprUnit[]): WhereExprUnit {
549
+ if (conditions.length === 0) {
550
+ throw new ArgumentError({ conditions: "empty arrays are not allowed" });
551
+ }
552
+ return new WhereExprUnit({
553
+ type: "or",
554
+ conditions: conditions.map((c) => c.expr),
555
+ });
556
+ },
557
+
558
+ //#endregion
559
+
560
+ //#region ========== SELECT - 문자열 ==========
561
+
562
+ /**
563
+ * 문자열 연결 (CONCAT)
564
+ *
565
+ * NULL 값은 빈 문자열로 processing됨 (DBMS별 automatic Transform)
566
+ *
567
+ * @param args - 연결할 문자열들
568
+ * @returns 연결된 문자열 expression
569
+ *
570
+ * @example
571
+ * ```typescript
572
+ * db.user().select((u) => ({
573
+ * fullName: expr.concat(u.firstName, " ", u.lastName),
574
+ * }))
575
+ * // SELECT CONCAT(firstName, ' ', lastName) AS fullName
576
+ * ```
577
+ */
578
+ concat(...args: ExprInput<string | undefined>[]): ExprUnit<string> {
579
+ return new ExprUnit("string", {
580
+ type: "concat",
581
+ args: args.map((arg) => toExpr(arg)),
582
+ });
583
+ },
584
+
585
+ /**
586
+ * 문자열 왼쪽에서 지정 길이만큼 추출 (LEFT)
587
+ *
588
+ * @param source - original string
589
+ * @param length - 추출할 문자 수
590
+ * @returns 추출된 문자열 expression
591
+ *
592
+ * @example
593
+ * ```typescript
594
+ * db.user().select((u) => ({
595
+ * initial: expr.left(u.name, 1),
596
+ * }))
597
+ * // SELECT LEFT(name, 1) AS initial
598
+ * ```
599
+ */
600
+ left<T extends string | undefined>(source: ExprUnit<T>, length: ExprInput<number>): ExprUnit<T> {
601
+ return new ExprUnit("string", {
602
+ type: "left",
603
+ source: toExpr(source),
604
+ length: toExpr(length),
605
+ });
606
+ },
607
+
608
+ /**
609
+ * 문자열 오른쪽에서 지정 길이만큼 추출 (RIGHT)
610
+ *
611
+ * @param source - original string
612
+ * @param length - 추출할 문자 수
613
+ * @returns 추출된 문자열 expression
614
+ *
615
+ * @example
616
+ * ```typescript
617
+ * db.phone().select((p) => ({
618
+ * lastFour: expr.right(p.number, 4),
619
+ * }))
620
+ * // SELECT RIGHT(number, 4) AS lastFour
621
+ * ```
622
+ */
623
+ right<T extends string | undefined>(source: ExprUnit<T>, length: ExprInput<number>): ExprUnit<T> {
624
+ return new ExprUnit("string", {
625
+ type: "right",
626
+ source: toExpr(source),
627
+ length: toExpr(length),
628
+ });
629
+ },
630
+
631
+ /**
632
+ * 문자열 양쪽 공백 Remove (TRIM)
633
+ *
634
+ * @param source - original string
635
+ * @returns 공백이 제거된 문자열 expression
636
+ *
637
+ * @example
638
+ * ```typescript
639
+ * db.user().select((u) => ({
640
+ * name: expr.trim(u.name),
641
+ * }))
642
+ * // SELECT TRIM(name) AS name
643
+ * ```
644
+ */
645
+ trim<T extends string | undefined>(source: ExprUnit<T>): ExprUnit<T> {
646
+ return new ExprUnit("string", {
647
+ type: "trim",
648
+ arg: toExpr(source),
649
+ });
650
+ },
651
+
652
+ /**
653
+ * 문자열 왼쪽 패딩 (LPAD)
654
+ *
655
+ * 지정 길이가 될 때까지 왼쪽에 fillString loop Add
656
+ *
657
+ * @param source - original string
658
+ * @param length - 목표 길이
659
+ * @param fillString - 패딩에 사용할 문자열
660
+ * @returns 패딩된 문자열 expression
661
+ *
662
+ * @example
663
+ * ```typescript
664
+ * db.order().select((o) => ({
665
+ * orderNo: expr.padStart(expr.cast(o.id, { type: "varchar", length: 10 }), 8, "0"),
666
+ * }))
667
+ * // SELECT LPAD(CAST(id AS VARCHAR(10)), 8, '0') AS orderNo
668
+ * // Result: "00000123"
669
+ * ```
670
+ */
671
+ padStart<T extends string | undefined>(
672
+ source: ExprUnit<T>,
673
+ length: ExprInput<number>,
674
+ fillString: ExprInput<string>,
675
+ ): ExprUnit<T> {
676
+ return new ExprUnit("string", {
677
+ type: "padStart",
678
+ source: toExpr(source),
679
+ length: toExpr(length),
680
+ fillString: toExpr(fillString),
681
+ });
682
+ },
683
+
684
+ /**
685
+ * 문자열 치환 (REPLACE)
686
+ *
687
+ * @param source - original string
688
+ * @param from - 찾을 문자열
689
+ * @param to - 대체할 문자열
690
+ * @returns 치환된 문자열 expression
691
+ *
692
+ * @example
693
+ * ```typescript
694
+ * db.user().select((u) => ({
695
+ * phone: expr.replace(u.phone, "-", ""),
696
+ * }))
697
+ * // SELECT REPLACE(phone, '-', '') AS phone
698
+ * ```
699
+ */
700
+ replace<T extends string | undefined>(
701
+ source: ExprUnit<T>,
702
+ from: ExprInput<string>,
703
+ to: ExprInput<string>,
704
+ ): ExprUnit<T> {
705
+ return new ExprUnit("string", {
706
+ type: "replace",
707
+ source: toExpr(source),
708
+ from: toExpr(from),
709
+ to: toExpr(to),
710
+ });
711
+ },
712
+
713
+ /**
714
+ * 문자열 대문자 Transform (UPPER)
715
+ *
716
+ * @param source - original string
717
+ * @returns 대문자로 Transform된 문자열 expression
718
+ *
719
+ * @example
720
+ * ```typescript
721
+ * db.user().select((u) => ({
722
+ * code: expr.upper(u.code),
723
+ * }))
724
+ * // SELECT UPPER(code) AS code
725
+ * ```
726
+ */
727
+ upper<T extends string | undefined>(source: ExprUnit<T>): ExprUnit<T> {
728
+ return new ExprUnit("string", {
729
+ type: "upper",
730
+ arg: toExpr(source),
731
+ });
732
+ },
733
+
734
+ /**
735
+ * 문자열 소문자 Transform (LOWER)
736
+ *
737
+ * @param source - original string
738
+ * @returns 소문자로 Transform된 문자열 expression
739
+ *
740
+ * @example
741
+ * ```typescript
742
+ * db.user().select((u) => ({
743
+ * email: expr.lower(u.email),
744
+ * }))
745
+ * // SELECT LOWER(email) AS email
746
+ * ```
747
+ */
748
+ lower<T extends string | undefined>(source: ExprUnit<T>): ExprUnit<T> {
749
+ return new ExprUnit("string", {
750
+ type: "lower",
751
+ arg: toExpr(source),
752
+ });
753
+ },
754
+
755
+ /**
756
+ * 문자열 길이 (문자 수)
757
+ *
758
+ * @param source - original string
759
+ * @returns 문자 수
760
+ *
761
+ * @example
762
+ * ```typescript
763
+ * db.user().select((u) => ({
764
+ * nameLength: expr.length(u.name),
765
+ * }))
766
+ * // SELECT CHAR_LENGTH(name) AS nameLength
767
+ * ```
768
+ */
769
+ length(source: ExprUnit<string | undefined>): ExprUnit<number> {
770
+ return new ExprUnit("number", {
771
+ type: "length",
772
+ arg: toExpr(source),
773
+ });
774
+ },
775
+
776
+ /**
777
+ * 문자열 바이트 길이
778
+ *
779
+ * UTF-8 환경에서 한글은 3바이트
780
+ *
781
+ * @param source - original string
782
+ * @returns 바이트 수
783
+ *
784
+ * @example
785
+ * ```typescript
786
+ * db.user().select((u) => ({
787
+ * byteLen: expr.byteLength(u.name),
788
+ * }))
789
+ * // SELECT OCTET_LENGTH(name) AS byteLen
790
+ * ```
791
+ */
792
+ byteLength(source: ExprUnit<string | undefined>): ExprUnit<number> {
793
+ return new ExprUnit("number", {
794
+ type: "byteLength",
795
+ arg: toExpr(source),
796
+ });
797
+ },
798
+
799
+ /**
800
+ * 문자열 일부 추출 (SUBSTRING)
801
+ *
802
+ * SQL 표준에 따라 1-based index 사용
803
+ *
804
+ * @param source - original string
805
+ * @param start - start 위치 (1부터 start)
806
+ * @param length - 추출할 길이 (생략 시 끝까지)
807
+ * @returns 추출된 문자열 expression
808
+ *
809
+ * @example
810
+ * ```typescript
811
+ * db.user().select((u) => ({
812
+ * // "Hello World"에서 Index 1부터 5글자: "Hello"
813
+ * prefix: expr.substring(u.name, 1, 5),
814
+ * }))
815
+ * // SELECT SUBSTRING(name, 1, 5) AS prefix
816
+ * ```
817
+ */
818
+ substring<T extends string | undefined>(
819
+ source: ExprUnit<T>,
820
+ start: ExprInput<number>,
821
+ length?: ExprInput<number>,
822
+ ): ExprUnit<T> {
823
+ return new ExprUnit("string", {
824
+ type: "substring",
825
+ source: toExpr(source),
826
+ start: toExpr(start),
827
+ ...(length != null ? { length: toExpr(length) } : {}),
828
+ });
829
+ },
830
+
831
+ /**
832
+ * 문자열 내 위치 찾기 (LOCATE/CHARINDEX)
833
+ *
834
+ * 1-based index return, 없으면 0 return
835
+ *
836
+ * @param source - 검색할 문자열
837
+ * @param search - 찾을 문자열
838
+ * @returns 위치 (1부터 start, 없으면 0)
839
+ *
840
+ * @example
841
+ * ```typescript
842
+ * db.user().select((u) => ({
843
+ * atPos: expr.indexOf(u.email, "@"),
844
+ * }))
845
+ * // SELECT LOCATE('@', email) AS atPos (MySQL)
846
+ * // "john@example.com" → 5
847
+ * ```
848
+ */
849
+ indexOf(source: ExprUnit<string | undefined>, search: ExprInput<string>): ExprUnit<number> {
850
+ return new ExprUnit("number", {
851
+ type: "indexOf",
852
+ source: toExpr(source),
853
+ search: toExpr(search),
854
+ });
855
+ },
856
+
857
+ //#endregion
858
+
859
+ //#region ========== SELECT - Number ==========
860
+
861
+ /**
862
+ * 절대값 (ABS)
863
+ *
864
+ * @param source - 원본 Number
865
+ * @returns 절대값 expression
866
+ *
867
+ * @example
868
+ * ```typescript
869
+ * db.account().select((a) => ({
870
+ * balance: expr.abs(a.balance),
871
+ * }))
872
+ * // SELECT ABS(balance) AS balance
873
+ * ```
874
+ */
875
+ abs<T extends number | undefined>(source: ExprUnit<T>): ExprUnit<T> {
876
+ return new ExprUnit("number", {
877
+ type: "abs",
878
+ arg: toExpr(source),
879
+ });
880
+ },
881
+
882
+ /**
883
+ * 반올림 (ROUND)
884
+ *
885
+ * @param source - 원본 Number
886
+ * @param digits - 소수점 이하 자릿수
887
+ * @returns 반올림된 Number expression
888
+ *
889
+ * @example
890
+ * ```typescript
891
+ * db.product().select((p) => ({
892
+ * price: expr.round(p.price, 2),
893
+ * }))
894
+ * // SELECT ROUND(price, 2) AS price
895
+ * // 123.456 → 123.46
896
+ * ```
897
+ */
898
+ round<T extends number | undefined>(source: ExprUnit<T>, digits: number): ExprUnit<T> {
899
+ return new ExprUnit("number", {
900
+ type: "round",
901
+ arg: toExpr(source),
902
+ digits,
903
+ });
904
+ },
905
+
906
+ /**
907
+ * 올림 (CEILING)
908
+ *
909
+ * @param source - 원본 Number
910
+ * @returns 올림된 Number expression
911
+ *
912
+ * @example
913
+ * ```typescript
914
+ * db.order().select((o) => ({
915
+ * pages: expr.ceil(expr.divide(o.itemCount, 10)),
916
+ * }))
917
+ * // SELECT CEILING(itemCount / 10) AS pages
918
+ * // 25 / 10 = 2.5 → 3
919
+ * ```
920
+ */
921
+ ceil<T extends number | undefined>(source: ExprUnit<T>): ExprUnit<T> {
922
+ return new ExprUnit("number", {
923
+ type: "ceil",
924
+ arg: toExpr(source),
925
+ });
926
+ },
927
+
928
+ /**
929
+ * 버림 (FLOOR)
930
+ *
931
+ * @param source - 원본 Number
932
+ * @returns 버림된 Number expression
933
+ *
934
+ * @example
935
+ * ```typescript
936
+ * db.user().select((u) => ({
937
+ * ageGroup: expr.floor(expr.divide(u.age, 10)),
938
+ * }))
939
+ * // SELECT FLOOR(age / 10) AS ageGroup
940
+ * // 25 / 10 = 2.5 → 2
941
+ * ```
942
+ */
943
+ floor<T extends number | undefined>(source: ExprUnit<T>): ExprUnit<T> {
944
+ return new ExprUnit("number", {
945
+ type: "floor",
946
+ arg: toExpr(source),
947
+ });
948
+ },
949
+
950
+ //#endregion
951
+
952
+ //#region ========== SELECT - Date ==========
953
+
954
+ /**
955
+ * 연도 추출 (YEAR)
956
+ *
957
+ * @param source - DateTime 또는 DateOnly expression
958
+ * @returns 연도 (4자리 Number)
959
+ *
960
+ * @example
961
+ * ```typescript
962
+ * db.user().select((u) => ({
963
+ * birthYear: expr.year(u.birthDate),
964
+ * }))
965
+ * // SELECT YEAR(birthDate) AS birthYear
966
+ * ```
967
+ */
968
+ year<T extends DateTime | DateOnly | undefined>(
969
+ source: ExprUnit<T>,
970
+ ): ExprUnit<T extends undefined ? undefined : number> {
971
+ return new ExprUnit("number", {
972
+ type: "year",
973
+ arg: toExpr(source),
974
+ });
975
+ },
976
+
977
+ /**
978
+ * 월 추출 (MONTH)
979
+ *
980
+ * @param source - DateTime 또는 DateOnly expression
981
+ * @returns 월 (1~12)
982
+ *
983
+ * @example
984
+ * ```typescript
985
+ * db.order().select((o) => ({
986
+ * orderMonth: expr.month(o.createdAt),
987
+ * }))
988
+ * // SELECT MONTH(createdAt) AS orderMonth
989
+ * ```
990
+ */
991
+ month<T extends DateTime | DateOnly | undefined>(
992
+ source: ExprUnit<T>,
993
+ ): ExprUnit<T extends undefined ? undefined : number> {
994
+ return new ExprUnit("number", {
995
+ type: "month",
996
+ arg: toExpr(source),
997
+ });
998
+ },
999
+
1000
+ /**
1001
+ * 일 추출 (DAY)
1002
+ *
1003
+ * @param source - DateTime 또는 DateOnly expression
1004
+ * @returns 일 (1~31)
1005
+ *
1006
+ * @example
1007
+ * ```typescript
1008
+ * db.user().select((u) => ({
1009
+ * birthDay: expr.day(u.birthDate),
1010
+ * }))
1011
+ * // SELECT DAY(birthDate) AS birthDay
1012
+ * ```
1013
+ */
1014
+ day<T extends DateTime | DateOnly | undefined>(
1015
+ source: ExprUnit<T>,
1016
+ ): ExprUnit<T extends undefined ? undefined : number> {
1017
+ return new ExprUnit("number", {
1018
+ type: "day",
1019
+ arg: toExpr(source),
1020
+ });
1021
+ },
1022
+
1023
+ /**
1024
+ * 시 추출 (HOUR)
1025
+ *
1026
+ * @param source - DateTime 또는 Time expression
1027
+ * @returns 시 (0~23)
1028
+ *
1029
+ * @example
1030
+ * ```typescript
1031
+ * db.log().select((l) => ({
1032
+ * logHour: expr.hour(l.createdAt),
1033
+ * }))
1034
+ * // SELECT HOUR(createdAt) AS logHour
1035
+ * ```
1036
+ */
1037
+ hour<T extends DateTime | Time | undefined>(
1038
+ source: ExprUnit<T>,
1039
+ ): ExprUnit<T extends undefined ? undefined : number> {
1040
+ return new ExprUnit("number", {
1041
+ type: "hour",
1042
+ arg: toExpr(source),
1043
+ });
1044
+ },
1045
+
1046
+ /**
1047
+ * 분 추출 (MINUTE)
1048
+ *
1049
+ * @param source - DateTime 또는 Time expression
1050
+ * @returns 분 (0~59)
1051
+ *
1052
+ * @example
1053
+ * ```typescript
1054
+ * db.log().select((l) => ({
1055
+ * logMinute: expr.minute(l.createdAt),
1056
+ * }))
1057
+ * // SELECT MINUTE(createdAt) AS logMinute
1058
+ * ```
1059
+ */
1060
+ minute<T extends DateTime | Time | undefined>(
1061
+ source: ExprUnit<T>,
1062
+ ): ExprUnit<T extends undefined ? undefined : number> {
1063
+ return new ExprUnit("number", {
1064
+ type: "minute",
1065
+ arg: toExpr(source),
1066
+ });
1067
+ },
1068
+
1069
+ /**
1070
+ * 초 추출 (SECOND)
1071
+ *
1072
+ * @param source - DateTime 또는 Time expression
1073
+ * @returns 초 (0~59)
1074
+ *
1075
+ * @example
1076
+ * ```typescript
1077
+ * db.log().select((l) => ({
1078
+ * logSecond: expr.second(l.createdAt),
1079
+ * }))
1080
+ * // SELECT SECOND(createdAt) AS logSecond
1081
+ * ```
1082
+ */
1083
+ second<T extends DateTime | Time | undefined>(
1084
+ source: ExprUnit<T>,
1085
+ ): ExprUnit<T extends undefined ? undefined : number> {
1086
+ return new ExprUnit("number", {
1087
+ type: "second",
1088
+ arg: toExpr(source),
1089
+ });
1090
+ },
1091
+
1092
+ /**
1093
+ * ISO 주차 추출
1094
+ *
1095
+ * ISO 8601 기준 주차 (월요일 start, 1~53)
1096
+ *
1097
+ * @param source - DateOnly expression
1098
+ * @returns ISO 주차 번호
1099
+ *
1100
+ * @example
1101
+ * ```typescript
1102
+ * db.order().select((o) => ({
1103
+ * weekNum: expr.isoWeek(o.orderDate),
1104
+ * }))
1105
+ * // SELECT WEEK(orderDate, 3) AS weekNum (MySQL)
1106
+ * ```
1107
+ */
1108
+ isoWeek<T extends DateOnly | undefined>(
1109
+ source: ExprUnit<T>,
1110
+ ): ExprUnit<T extends undefined ? undefined : number> {
1111
+ return new ExprUnit("number", {
1112
+ type: "isoWeek",
1113
+ arg: toExpr(source),
1114
+ });
1115
+ },
1116
+
1117
+ /**
1118
+ * ISO 주 시작일 (월요일)
1119
+ *
1120
+ * 해당 Date가 속한 주의 월요일 return
1121
+ *
1122
+ * @param source - DateOnly expression
1123
+ * @returns 주의 start Date (월요일)
1124
+ *
1125
+ * @example
1126
+ * ```typescript
1127
+ * db.order().select((o) => ({
1128
+ * weekStart: expr.isoWeekStartDate(o.orderDate),
1129
+ * }))
1130
+ * // 2024-01-10 (수) → 2024-01-08 (월)
1131
+ * ```
1132
+ */
1133
+ isoWeekStartDate<T extends DateOnly | undefined>(source: ExprUnit<T>): ExprUnit<T> {
1134
+ return new ExprUnit("DateOnly", {
1135
+ type: "isoWeekStartDate",
1136
+ arg: toExpr(source),
1137
+ });
1138
+ },
1139
+
1140
+ /**
1141
+ * ISO 연월 (해당 월의 1일)
1142
+ *
1143
+ * 해당 Date의 월 첫째 날 return
1144
+ *
1145
+ * @param source - DateOnly expression
1146
+ * @returns 월의 첫째 날
1147
+ *
1148
+ * @example
1149
+ * ```typescript
1150
+ * db.order().select((o) => ({
1151
+ * yearMonth: expr.isoYearMonth(o.orderDate),
1152
+ * }))
1153
+ * // 2024-01-15 → 2024-01-01
1154
+ * ```
1155
+ */
1156
+ isoYearMonth<T extends DateOnly | undefined>(source: ExprUnit<T>): ExprUnit<T> {
1157
+ return new ExprUnit("DateOnly", {
1158
+ type: "isoYearMonth",
1159
+ arg: toExpr(source),
1160
+ });
1161
+ },
1162
+
1163
+ /**
1164
+ * Date 차이 계산 (DATEDIFF)
1165
+ *
1166
+ * @param separator - 단위 ("year", "month", "day", "hour", "minute", "second")
1167
+ * @param from - start Date
1168
+ * @param to - 끝 Date
1169
+ * @returns 차이 value (to - from)
1170
+ *
1171
+ * @example
1172
+ * ```typescript
1173
+ * db.user().select((u) => ({
1174
+ * age: expr.dateDiff("year", u.birthDate, expr.val("DateOnly", DateOnly.today())),
1175
+ * }))
1176
+ * // SELECT DATEDIFF(year, birthDate, '2024-01-15') AS age
1177
+ * ```
1178
+ */
1179
+ dateDiff<T extends DateTime | DateOnly | Time | undefined>(
1180
+ separator: DateSeparator,
1181
+ from: ExprInput<T>,
1182
+ to: ExprInput<T>,
1183
+ ): ExprUnit<T extends undefined ? undefined : number> {
1184
+ return new ExprUnit("number", {
1185
+ type: "dateDiff",
1186
+ separator,
1187
+ from: toExpr(from),
1188
+ to: toExpr(to),
1189
+ });
1190
+ },
1191
+
1192
+ /**
1193
+ * Date 더하기 (DATEADD)
1194
+ *
1195
+ * @param separator - 단위 ("year", "month", "day", "hour", "minute", "second")
1196
+ * @param source - 원본 Date
1197
+ * @param value - 더할 value (음수 가능)
1198
+ * @returns 계산된 Date
1199
+ *
1200
+ * @example
1201
+ * ```typescript
1202
+ * db.subscription().select((s) => ({
1203
+ * expiresAt: expr.dateAdd("month", s.startDate, 12),
1204
+ * }))
1205
+ * // SELECT DATEADD(month, 12, startDate) AS expiresAt
1206
+ * ```
1207
+ */
1208
+ dateAdd<T extends DateTime | DateOnly | Time | undefined>(
1209
+ separator: DateSeparator,
1210
+ source: ExprUnit<T>,
1211
+ value: ExprInput<number>,
1212
+ ): ExprUnit<T> {
1213
+ return new ExprUnit(source.dataType, {
1214
+ type: "dateAdd",
1215
+ separator,
1216
+ source: toExpr(source),
1217
+ value: toExpr(value),
1218
+ });
1219
+ },
1220
+
1221
+ /**
1222
+ * Date 포맷 (DATE_FORMAT)
1223
+ *
1224
+ * DBMS별로 포맷 문자열 규칙이 다를 수 있음
1225
+ *
1226
+ * @param source - Date expression
1227
+ * @param format - 포맷 문자열 (예: "%Y-%m-%d")
1228
+ * @returns 포맷된 문자열 expression
1229
+ *
1230
+ * @example
1231
+ * ```typescript
1232
+ * db.order().select((o) => ({
1233
+ * orderDate: expr.formatDate(o.createdAt, "%Y-%m-%d"),
1234
+ * }))
1235
+ * // SELECT DATE_FORMAT(createdAt, '%Y-%m-%d') AS orderDate (MySQL)
1236
+ * // 2024-01-15 10:30:00 → "2024-01-15"
1237
+ * ```
1238
+ */
1239
+ formatDate<T extends DateTime | DateOnly | Time | undefined>(
1240
+ source: ExprUnit<T>,
1241
+ format: string,
1242
+ ): ExprUnit<T extends undefined ? undefined : string> {
1243
+ return new ExprUnit("string", {
1244
+ type: "formatDate",
1245
+ source: toExpr(source),
1246
+ format,
1247
+ });
1248
+ },
1249
+
1250
+ //#endregion
1251
+
1252
+ //#region ========== SELECT - Condition ==========
1253
+
1254
+ /**
1255
+ * NULL 대체 (COALESCE/IFNULL)
1256
+ *
1257
+ * 첫 번째 non-null 값을 return. 마지막 인자가 non-nullable이면 결과도 non-nullable
1258
+ *
1259
+ * @param args - Inspect할 값들 (마지막은 Default value)
1260
+ * @returns 첫 번째 non-null value
1261
+ *
1262
+ * @example
1263
+ * ```typescript
1264
+ * db.user().select((u) => ({
1265
+ * displayName: expr.ifNull(u.nickname, u.name, "Guest"),
1266
+ * }))
1267
+ * // SELECT COALESCE(nickname, name, 'Guest') AS displayName
1268
+ * ```
1269
+ */
1270
+ ifNull,
1271
+
1272
+ /**
1273
+ * 특정 값이면 NULL return (NULLIF)
1274
+ *
1275
+ * source === value 이면 NULL return, 아니면 source return
1276
+ *
1277
+ * @param source - 원본 value
1278
+ * @param value - 비교할 value
1279
+ * @returns NULL 또는 원본 value
1280
+ *
1281
+ * @example
1282
+ * ```typescript
1283
+ * db.user().select((u) => ({
1284
+ * // 빈 문자열을 NULL로 Transform
1285
+ * bio: expr.nullIf(u.bio, ""),
1286
+ * }))
1287
+ * // SELECT NULLIF(bio, '') AS bio
1288
+ * ```
1289
+ */
1290
+ nullIf<T extends ColumnPrimitive>(
1291
+ source: ExprUnit<T>,
1292
+ value: ExprInput<T>,
1293
+ ): ExprUnit<T | undefined> {
1294
+ return new ExprUnit(source.dataType, {
1295
+ type: "nullIf",
1296
+ source: toExpr(source),
1297
+ value: toExpr(value),
1298
+ });
1299
+ },
1300
+
1301
+ /**
1302
+ * WHERE 표현식을 boolean으로 Transform
1303
+ *
1304
+ * SELECT 절에서 condition 결과를 boolean column으로 사용할 때 사용
1305
+ *
1306
+ * @param condition - Transform할 condition
1307
+ * @returns boolean expression
1308
+ *
1309
+ * @example
1310
+ * ```typescript
1311
+ * db.user().select((u) => ({
1312
+ * isActive: expr.is(expr.eq(u.status, "active")),
1313
+ * }))
1314
+ * // SELECT (status <=> 'active') AS isActive
1315
+ * ```
1316
+ */
1317
+ is(condition: WhereExprUnit): ExprUnit<boolean> {
1318
+ return new ExprUnit("boolean", {
1319
+ type: "is",
1320
+ condition: condition.expr,
1321
+ });
1322
+ },
1323
+
1324
+ /**
1325
+ * CASE WHEN expression builder
1326
+ *
1327
+ * 체이닝 방식으로 condition 분기를 구성
1328
+ *
1329
+ * @returns SwitchExprBuilder instance
1330
+ *
1331
+ * @example
1332
+ * ```typescript
1333
+ * db.user().select((u) => ({
1334
+ * grade: expr.switch<string>()
1335
+ * .case(expr.gte(u.score, 90), "A")
1336
+ * .case(expr.gte(u.score, 80), "B")
1337
+ * .case(expr.gte(u.score, 70), "C")
1338
+ * .default("F"),
1339
+ * }))
1340
+ * // SELECT CASE WHEN score >= 90 THEN 'A' ... ELSE 'F' END AS grade
1341
+ * ```
1342
+ */
1343
+ switch<T extends ColumnPrimitive>(): SwitchExprBuilder<T> {
1344
+ return createSwitchBuilder<T>();
1345
+ },
1346
+
1347
+ /**
1348
+ * 단순 IF condition (삼항 operator)
1349
+ *
1350
+ * @param condition - Condition
1351
+ * @param then - Condition이 참일 때 value
1352
+ * @param else_ - Condition이 거짓일 때 value
1353
+ * @returns 조건부 value expression
1354
+ *
1355
+ * @example
1356
+ * ```typescript
1357
+ * db.user().select((u) => ({
1358
+ * type: expr.if(expr.gte(u.age, 18), "adult", "minor"),
1359
+ * }))
1360
+ * // SELECT IF(age >= 18, 'adult', 'minor') AS type
1361
+ * ```
1362
+ */
1363
+ if<T extends ColumnPrimitive>(
1364
+ condition: WhereExprUnit,
1365
+ then: ExprInput<T>,
1366
+ else_: ExprInput<T>,
1367
+ ): ExprUnit<T> {
1368
+ const allValues = [then, else_];
1369
+ // 1. ExprUnit에서 dataType 찾기
1370
+ const exprUnit = allValues.find((v): v is ExprUnit<T> => v instanceof ExprUnit);
1371
+ if (exprUnit) {
1372
+ return new ExprUnit(exprUnit.dataType, {
1373
+ type: "if",
1374
+ condition: condition.expr,
1375
+ then: toExpr(then),
1376
+ else: toExpr(else_),
1377
+ });
1378
+ }
1379
+
1380
+ // 2. non-null 리터럴에서 추론
1381
+ const nonNullLiteral = allValues.find((v) => v != null) as ColumnPrimitive;
1382
+ if (nonNullLiteral == null) {
1383
+ throw new Error("At least one of if's then/else must be non-null.");
1384
+ }
1385
+
1386
+ return new ExprUnit(inferColumnPrimitiveStr(nonNullLiteral), {
1387
+ type: "if",
1388
+ condition: condition.expr,
1389
+ then: toExpr(then),
1390
+ else: toExpr(else_),
1391
+ });
1392
+ },
1393
+
1394
+ //#endregion
1395
+
1396
+ //#region ========== SELECT - Aggregate ==========
1397
+ // SUM, AVG, MAX등의 집계는 모든 값이 NULL이거나 행이 없을 때만 NULL return (값이 NULL인 행은 무시함)
1398
+
1399
+ /**
1400
+ * row 수 카운트 (COUNT)
1401
+ *
1402
+ * @param arg - 카운트할 column (생략 시 전체 row 수)
1403
+ * @param distinct - true면 중복 Remove
1404
+ * @returns row
1405
+ *
1406
+ * @example
1407
+ * ```typescript
1408
+ * // 전체 row
1409
+ * db.user().select(() => ({ total: expr.count() }))
1410
+ *
1411
+ * // 중복 Remove 카운트
1412
+ * db.order().select((o) => ({
1413
+ * uniqueCustomers: expr.count(o.customerId, true),
1414
+ * }))
1415
+ * ```
1416
+ */
1417
+ count(arg?: ExprUnit<ColumnPrimitive>, distinct?: boolean): ExprUnit<number> {
1418
+ return new ExprUnit("number", {
1419
+ type: "count",
1420
+ arg: arg != null ? toExpr(arg) : undefined,
1421
+ distinct,
1422
+ });
1423
+ },
1424
+
1425
+ /**
1426
+ * 합계 (SUM)
1427
+ *
1428
+ * NULL 값은 무시됨. 모든 값이 NULL이면 NULL return
1429
+ *
1430
+ * @param arg - 합계를 구할 Number column
1431
+ * @returns 합계 (또는 NULL)
1432
+ *
1433
+ * @example
1434
+ * ```typescript
1435
+ * db.order().groupBy((o) => o.userId).select((o) => ({
1436
+ * userId: o.userId,
1437
+ * totalAmount: expr.sum(o.amount),
1438
+ * }))
1439
+ * ```
1440
+ */
1441
+ sum(arg: ExprUnit<number | undefined>): ExprUnit<number | undefined> {
1442
+ return new ExprUnit("number", {
1443
+ type: "sum",
1444
+ arg: toExpr(arg),
1445
+ });
1446
+ },
1447
+
1448
+ /**
1449
+ * 평균 (AVG)
1450
+ *
1451
+ * NULL 값은 무시됨. 모든 값이 NULL이면 NULL return
1452
+ *
1453
+ * @param arg - 평균을 구할 Number column
1454
+ * @returns 평균 (또는 NULL)
1455
+ *
1456
+ * @example
1457
+ * ```typescript
1458
+ * db.product().groupBy((p) => p.categoryId).select((p) => ({
1459
+ * categoryId: p.categoryId,
1460
+ * avgPrice: expr.avg(p.price),
1461
+ * }))
1462
+ * ```
1463
+ */
1464
+ avg(arg: ExprUnit<number | undefined>): ExprUnit<number | undefined> {
1465
+ return new ExprUnit("number", {
1466
+ type: "avg",
1467
+ arg: toExpr(arg),
1468
+ });
1469
+ },
1470
+
1471
+ /**
1472
+ * 최대값 (MAX)
1473
+ *
1474
+ * NULL 값은 무시됨. 모든 값이 NULL이면 NULL return
1475
+ *
1476
+ * @param arg - 최대값을 구할 column
1477
+ * @returns 최대값 (또는 NULL)
1478
+ *
1479
+ * @example
1480
+ * ```typescript
1481
+ * db.order().groupBy((o) => o.userId).select((o) => ({
1482
+ * userId: o.userId,
1483
+ * lastOrderDate: expr.max(o.createdAt),
1484
+ * }))
1485
+ * ```
1486
+ */
1487
+ max<T extends ColumnPrimitive>(arg: ExprUnit<T>): ExprUnit<T | undefined> {
1488
+ return new ExprUnit(arg.dataType, {
1489
+ type: "max",
1490
+ arg: toExpr(arg),
1491
+ });
1492
+ },
1493
+
1494
+ /**
1495
+ * 최소값 (MIN)
1496
+ *
1497
+ * NULL 값은 무시됨. 모든 값이 NULL이면 NULL return
1498
+ *
1499
+ * @param arg - 최소값을 구할 column
1500
+ * @returns 최소값 (또는 NULL)
1501
+ *
1502
+ * @example
1503
+ * ```typescript
1504
+ * db.product().groupBy((p) => p.categoryId).select((p) => ({
1505
+ * categoryId: p.categoryId,
1506
+ * minPrice: expr.min(p.price),
1507
+ * }))
1508
+ * ```
1509
+ */
1510
+ min<T extends ColumnPrimitive>(arg: ExprUnit<T>): ExprUnit<T | undefined> {
1511
+ return new ExprUnit(arg.dataType, {
1512
+ type: "min",
1513
+ arg: toExpr(arg),
1514
+ });
1515
+ },
1516
+
1517
+ //#endregion
1518
+
1519
+ //#region ========== SELECT - Other ==========
1520
+
1521
+ /**
1522
+ * 여러 value 중 최대값 (GREATEST)
1523
+ *
1524
+ * @param args - 비교할 값들
1525
+ * @returns 최대값
1526
+ *
1527
+ * @example
1528
+ * ```typescript
1529
+ * db.product().select((p) => ({
1530
+ * effectivePrice: expr.greatest(p.price, p.minPrice),
1531
+ * }))
1532
+ * // SELECT GREATEST(price, minPrice) AS effectivePrice
1533
+ * ```
1534
+ */
1535
+ greatest<T extends ColumnPrimitive>(...args: ExprInput<T>[]): ExprUnit<T> {
1536
+ return new ExprUnit(findDataType(args), {
1537
+ type: "greatest",
1538
+ args: args.map((a) => toExpr(a)),
1539
+ });
1540
+ },
1541
+
1542
+ /**
1543
+ * 여러 value 중 최소값 (LEAST)
1544
+ *
1545
+ * @param args - 비교할 값들
1546
+ * @returns 최소값
1547
+ *
1548
+ * @example
1549
+ * ```typescript
1550
+ * db.product().select((p) => ({
1551
+ * effectivePrice: expr.least(p.price, p.maxDiscount),
1552
+ * }))
1553
+ * // SELECT LEAST(price, maxDiscount) AS effectivePrice
1554
+ * ```
1555
+ */
1556
+ least<T extends ColumnPrimitive>(...args: ExprInput<T>[]): ExprUnit<T> {
1557
+ return new ExprUnit(findDataType(args), {
1558
+ type: "least",
1559
+ args: args.map((a) => toExpr(a)),
1560
+ });
1561
+ },
1562
+
1563
+ /**
1564
+ * row 번호 (ROW_NUMBER 없이 전체 행에 대한 순번)
1565
+ *
1566
+ * @returns row 번호 (1부터 start)
1567
+ *
1568
+ * @example
1569
+ * ```typescript
1570
+ * db.user().select((u) => ({
1571
+ * rowNum: expr.rowNum(),
1572
+ * name: u.name,
1573
+ * }))
1574
+ * ```
1575
+ */
1576
+ rowNum(): ExprUnit<number> {
1577
+ return new ExprUnit("number", {
1578
+ type: "rowNum",
1579
+ });
1580
+ },
1581
+
1582
+ /**
1583
+ * 난수 Generate (RAND/RANDOM)
1584
+ *
1585
+ * 0~1 사이의 난수 return. ORDER BY에서 랜덤 정렬용으로 주로 사용
1586
+ *
1587
+ * @returns 0~1 사이의 난수
1588
+ *
1589
+ * @example
1590
+ * ```typescript
1591
+ * // 랜덤 sorting
1592
+ * db.user().orderBy(() => expr.random()).limit(10)
1593
+ * ```
1594
+ */
1595
+ random(): ExprUnit<number> {
1596
+ return new ExprUnit("number", {
1597
+ type: "random",
1598
+ });
1599
+ },
1600
+
1601
+ /**
1602
+ * type transformation (CAST)
1603
+ *
1604
+ * @param source - Transform할 expression
1605
+ * @param targetType - 대상 data type
1606
+ * @returns Transform된 expression
1607
+ *
1608
+ * @example
1609
+ * ```typescript
1610
+ * db.order().select((o) => ({
1611
+ * idStr: expr.cast(o.id, { type: "varchar", length: 20 }),
1612
+ * }))
1613
+ * // SELECT CAST(id AS VARCHAR(20)) AS idStr
1614
+ * ```
1615
+ */
1616
+ cast<T extends ColumnPrimitive, TDataType extends DataType>(
1617
+ source: ExprUnit<T>,
1618
+ targetType: TDataType,
1619
+ ): ExprUnit<T extends undefined ? undefined : InferColumnPrimitiveFromDataType<TDataType>> {
1620
+ return new ExprUnit(dataTypeStrToColumnPrimitiveStr[targetType.type], {
1621
+ type: "cast",
1622
+ source: toExpr(source),
1623
+ targetType,
1624
+ });
1625
+ },
1626
+
1627
+ /**
1628
+ * 스칼라 Subquery - SELECT 절에서 단일 value return Subquery
1629
+ *
1630
+ * Subquery는 반드시 단일 row, 단일 column을 반환해야 함
1631
+ *
1632
+ * @param dataType - Data type of the returned value
1633
+ * @param queryable - 스칼라 값을 반환하는 Queryable
1634
+ * @returns Subquery result expression
1635
+ *
1636
+ * @example
1637
+ * ```typescript
1638
+ * db.user().select((u) => ({
1639
+ * id: u.id,
1640
+ * postCount: expr.subquery(
1641
+ * "number",
1642
+ * db.post()
1643
+ * .where((p) => [expr.eq(p.userId, u.id)])
1644
+ * .select(() => ({ cnt: expr.count() }))
1645
+ * ),
1646
+ * }))
1647
+ * // SELECT id, (SELECT COUNT(*) FROM Post WHERE userId = User.id) AS postCount
1648
+ * ```
1649
+ */
1650
+ subquery<TStr extends ColumnPrimitiveStr>(
1651
+ dataType: TStr,
1652
+ queryable: { getSelectQueryDef(): SelectQueryDef },
1653
+ ): ExprUnit<ColumnPrimitiveMap[TStr] | undefined> {
1654
+ return new ExprUnit(dataType, {
1655
+ type: "subquery",
1656
+ queryDef: queryable.getSelectQueryDef(),
1657
+ });
1658
+ },
1659
+
1660
+ //#endregion
1661
+
1662
+ //#region ========== SELECT - Window Functions ==========
1663
+
1664
+ /**
1665
+ * ROW_NUMBER() - 파티션 내 row 번호
1666
+ *
1667
+ * 각 파티션 내에서 1부터 시작하는 sequential 번호 부여
1668
+ *
1669
+ * @param spec - Window spec (partitionBy, orderBy)
1670
+ * @returns row 번호 (1부터 start)
1671
+ *
1672
+ * @example
1673
+ * ```typescript
1674
+ * db.order().select((o) => ({
1675
+ * ...o,
1676
+ * rowNum: expr.rowNumber({
1677
+ * partitionBy: [o.userId],
1678
+ * orderBy: [[o.createdAt, "DESC"]],
1679
+ * }),
1680
+ * }))
1681
+ * // SELECT *, ROW_NUMBER() OVER (PARTITION BY userId ORDER BY createdAt DESC)
1682
+ * ```
1683
+ */
1684
+ rowNumber(spec: WinSpecInput): ExprUnit<number> {
1685
+ return new ExprUnit("number", {
1686
+ type: "window",
1687
+ fn: { type: "rowNumber" },
1688
+ spec: toWinSpec(spec),
1689
+ });
1690
+ },
1691
+
1692
+ /**
1693
+ * RANK() - 파티션 내 순위 (동점 시 같은 순위, 다음 순위 건너뜀)
1694
+ *
1695
+ * @param spec - Window spec (partitionBy, orderBy)
1696
+ * @returns 순위 (동점 후 건너뜀: 1, 1, 3)
1697
+ *
1698
+ * @example
1699
+ * ```typescript
1700
+ * db.student().select((s) => ({
1701
+ * name: s.name,
1702
+ * rank: expr.rank({
1703
+ * orderBy: [[s.score, "DESC"]],
1704
+ * }),
1705
+ * }))
1706
+ * ```
1707
+ */
1708
+ rank(spec: WinSpecInput): ExprUnit<number> {
1709
+ return new ExprUnit("number", {
1710
+ type: "window",
1711
+ fn: { type: "rank" },
1712
+ spec: toWinSpec(spec),
1713
+ });
1714
+ },
1715
+
1716
+ /**
1717
+ * DENSE_RANK() - 파티션 내 밀집 순위 (동점 시 같은 순위, 다음 순위 유지)
1718
+ *
1719
+ * @param spec - Window spec (partitionBy, orderBy)
1720
+ * @returns 밀집 순위 (동점 후 연속: 1, 1, 2)
1721
+ *
1722
+ * @example
1723
+ * ```typescript
1724
+ * db.student().select((s) => ({
1725
+ * name: s.name,
1726
+ * denseRank: expr.denseRank({
1727
+ * orderBy: [[s.score, "DESC"]],
1728
+ * }),
1729
+ * }))
1730
+ * ```
1731
+ */
1732
+ denseRank(spec: WinSpecInput): ExprUnit<number> {
1733
+ return new ExprUnit("number", {
1734
+ type: "window",
1735
+ fn: { type: "denseRank" },
1736
+ spec: toWinSpec(spec),
1737
+ });
1738
+ },
1739
+
1740
+ /**
1741
+ * NTILE(n) - 파티션을 n개 그룹으로 split
1742
+ *
1743
+ * @param n - 분할할 그룹 수
1744
+ * @param spec - Window spec (partitionBy, orderBy)
1745
+ * @returns 그룹 번호 (1 ~ n)
1746
+ *
1747
+ * @example
1748
+ * ```typescript
1749
+ * // 상위 25%를 찾기 위한 사분위 split
1750
+ * db.user().select((u) => ({
1751
+ * name: u.name,
1752
+ * quartile: expr.ntile(4, {
1753
+ * orderBy: [[u.score, "DESC"]],
1754
+ * }),
1755
+ * }))
1756
+ * ```
1757
+ */
1758
+ ntile(n: number, spec: WinSpecInput): ExprUnit<number> {
1759
+ return new ExprUnit("number", {
1760
+ type: "window",
1761
+ fn: { type: "ntile", n },
1762
+ spec: toWinSpec(spec),
1763
+ });
1764
+ },
1765
+
1766
+ /**
1767
+ * LAG() - 이전 행의 value 참조
1768
+ *
1769
+ * @param column - column to reference
1770
+ * @param spec - Window spec (partitionBy, orderBy)
1771
+ * @param options - offset (Basic 1), default (이전 행이 없을 때 Default value)
1772
+ * @returns 이전 행의 value (또는 Default value/NULL)
1773
+ *
1774
+ * @example
1775
+ * ```typescript
1776
+ * db.stock().select((s) => ({
1777
+ * date: s.date,
1778
+ * price: s.price,
1779
+ * prevPrice: expr.lag(s.price, {
1780
+ * partitionBy: [s.symbol],
1781
+ * orderBy: [[s.date, "ASC"]],
1782
+ * }),
1783
+ * }))
1784
+ * ```
1785
+ */
1786
+ lag<T extends ColumnPrimitive>(
1787
+ column: ExprUnit<T>,
1788
+ spec: WinSpecInput,
1789
+ options?: { offset?: number; default?: ExprInput<T> },
1790
+ ): ExprUnit<T | undefined> {
1791
+ return new ExprUnit(column.dataType, {
1792
+ type: "window",
1793
+ fn: {
1794
+ type: "lag",
1795
+ column: toExpr(column),
1796
+ offset: options?.offset,
1797
+ default: options?.default != null ? toExpr(options.default) : undefined,
1798
+ },
1799
+ spec: toWinSpec(spec),
1800
+ });
1801
+ },
1802
+
1803
+ /**
1804
+ * LEAD() - 다음 행의 value 참조
1805
+ *
1806
+ * @param column - column to reference
1807
+ * @param spec - Window spec (partitionBy, orderBy)
1808
+ * @param options - offset (Basic 1), default (다음 행이 없을 때 Default value)
1809
+ * @returns 다음 행의 value (또는 Default value/NULL)
1810
+ *
1811
+ * @example
1812
+ * ```typescript
1813
+ * db.stock().select((s) => ({
1814
+ * date: s.date,
1815
+ * price: s.price,
1816
+ * nextPrice: expr.lead(s.price, {
1817
+ * partitionBy: [s.symbol],
1818
+ * orderBy: [[s.date, "ASC"]],
1819
+ * }),
1820
+ * }))
1821
+ * ```
1822
+ */
1823
+ lead<T extends ColumnPrimitive>(
1824
+ column: ExprUnit<T>,
1825
+ spec: WinSpecInput,
1826
+ options?: { offset?: number; default?: ExprInput<T> },
1827
+ ): ExprUnit<T | undefined> {
1828
+ return new ExprUnit(column.dataType, {
1829
+ type: "window",
1830
+ fn: {
1831
+ type: "lead",
1832
+ column: toExpr(column),
1833
+ offset: options?.offset,
1834
+ default: options?.default != null ? toExpr(options.default) : undefined,
1835
+ },
1836
+ spec: toWinSpec(spec),
1837
+ });
1838
+ },
1839
+
1840
+ /**
1841
+ * FIRST_VALUE() - 파티션/프레임의 첫 번째 value
1842
+ *
1843
+ * @param column - column to reference
1844
+ * @param spec - Window spec (partitionBy, orderBy)
1845
+ * @returns 첫 번째 value
1846
+ *
1847
+ * @example
1848
+ * ```typescript
1849
+ * db.order().select((o) => ({
1850
+ * ...o,
1851
+ * firstOrderAmount: expr.firstValue(o.amount, {
1852
+ * partitionBy: [o.userId],
1853
+ * orderBy: [[o.createdAt, "ASC"]],
1854
+ * }),
1855
+ * }))
1856
+ * ```
1857
+ */
1858
+ firstValue<T extends ColumnPrimitive>(
1859
+ column: ExprUnit<T>,
1860
+ spec: WinSpecInput,
1861
+ ): ExprUnit<T | undefined> {
1862
+ return new ExprUnit(column.dataType, {
1863
+ type: "window",
1864
+ fn: { type: "firstValue", column: toExpr(column) },
1865
+ spec: toWinSpec(spec),
1866
+ });
1867
+ },
1868
+
1869
+ /**
1870
+ * LAST_VALUE() - 파티션/프레임의 마지막 value
1871
+ *
1872
+ * @param column - column to reference
1873
+ * @param spec - Window spec (partitionBy, orderBy)
1874
+ * @returns 마지막 value
1875
+ *
1876
+ * @example
1877
+ * ```typescript
1878
+ * db.order().select((o) => ({
1879
+ * ...o,
1880
+ * lastOrderAmount: expr.lastValue(o.amount, {
1881
+ * partitionBy: [o.userId],
1882
+ * orderBy: [[o.createdAt, "ASC"]],
1883
+ * }),
1884
+ * }))
1885
+ * ```
1886
+ */
1887
+ lastValue<T extends ColumnPrimitive>(
1888
+ column: ExprUnit<T>,
1889
+ spec: WinSpecInput,
1890
+ ): ExprUnit<T | undefined> {
1891
+ return new ExprUnit(column.dataType, {
1892
+ type: "window",
1893
+ fn: { type: "lastValue", column: toExpr(column) },
1894
+ spec: toWinSpec(spec),
1895
+ });
1896
+ },
1897
+
1898
+ /**
1899
+ * SUM() OVER - Window 합계
1900
+ *
1901
+ * @param column - 합계를 구할 column
1902
+ * @param spec - Window spec (partitionBy, orderBy)
1903
+ * @returns Window 내 합계
1904
+ *
1905
+ * @example
1906
+ * ```typescript
1907
+ * // 누적 합계
1908
+ * db.order().select((o) => ({
1909
+ * ...o,
1910
+ * runningTotal: expr.sumOver(o.amount, {
1911
+ * partitionBy: [o.userId],
1912
+ * orderBy: [[o.createdAt, "ASC"]],
1913
+ * }),
1914
+ * }))
1915
+ * ```
1916
+ */
1917
+ sumOver(column: ExprUnit<number | undefined>, spec: WinSpecInput): ExprUnit<number | undefined> {
1918
+ return new ExprUnit("number", {
1919
+ type: "window",
1920
+ fn: { type: "sum", column: toExpr(column) },
1921
+ spec: toWinSpec(spec),
1922
+ });
1923
+ },
1924
+
1925
+ /**
1926
+ * AVG() OVER - Window 평균
1927
+ *
1928
+ * @param column - 평균을 구할 column
1929
+ * @param spec - Window spec (partitionBy, orderBy)
1930
+ * @returns Window 내 평균
1931
+ *
1932
+ * @example
1933
+ * ```typescript
1934
+ * // move 평균
1935
+ * db.stock().select((s) => ({
1936
+ * ...s,
1937
+ * movingAvg: expr.avgOver(s.price, {
1938
+ * partitionBy: [s.symbol],
1939
+ * orderBy: [[s.date, "ASC"]],
1940
+ * }),
1941
+ * }))
1942
+ * ```
1943
+ */
1944
+ avgOver(column: ExprUnit<number | undefined>, spec: WinSpecInput): ExprUnit<number | undefined> {
1945
+ return new ExprUnit("number", {
1946
+ type: "window",
1947
+ fn: { type: "avg", column: toExpr(column) },
1948
+ spec: toWinSpec(spec),
1949
+ });
1950
+ },
1951
+
1952
+ /**
1953
+ * COUNT() OVER - Window 카운트
1954
+ *
1955
+ * @param spec - Window spec (partitionBy, orderBy)
1956
+ * @param column - 카운트할 column (생략 시 전체 row 수)
1957
+ * @returns Windowrow
1958
+ *
1959
+ * @example
1960
+ * ```typescript
1961
+ * db.order().select((o) => ({
1962
+ * ...o,
1963
+ * totalOrdersPerUser: expr.countOver({
1964
+ * partitionBy: [o.userId],
1965
+ * }),
1966
+ * }))
1967
+ * ```
1968
+ */
1969
+ countOver(spec: WinSpecInput, column?: ExprUnit<ColumnPrimitive>): ExprUnit<number> {
1970
+ return new ExprUnit("number", {
1971
+ type: "window",
1972
+ fn: { type: "count", column: column != null ? toExpr(column) : undefined },
1973
+ spec: toWinSpec(spec),
1974
+ });
1975
+ },
1976
+
1977
+ /**
1978
+ * MIN() OVER - Window 최소값
1979
+ *
1980
+ * @param column - 최소값을 구할 column
1981
+ * @param spec - Window spec (partitionBy, orderBy)
1982
+ * @returns Window 내 최소값
1983
+ *
1984
+ * @example
1985
+ * ```typescript
1986
+ * db.stock().select((s) => ({
1987
+ * ...s,
1988
+ * minPriceInPeriod: expr.minOver(s.price, {
1989
+ * partitionBy: [s.symbol],
1990
+ * }),
1991
+ * }))
1992
+ * ```
1993
+ */
1994
+ minOver<T extends ColumnPrimitive>(
1995
+ column: ExprUnit<T>,
1996
+ spec: WinSpecInput,
1997
+ ): ExprUnit<T | undefined> {
1998
+ return new ExprUnit(column.dataType, {
1999
+ type: "window",
2000
+ fn: { type: "min", column: toExpr(column) },
2001
+ spec: toWinSpec(spec),
2002
+ });
2003
+ },
2004
+
2005
+ /**
2006
+ * MAX() OVER - Window 최대값
2007
+ *
2008
+ * @param column - 최대값을 구할 column
2009
+ * @param spec - Window spec (partitionBy, orderBy)
2010
+ * @returns Window 내 최대값
2011
+ *
2012
+ * @example
2013
+ * ```typescript
2014
+ * db.stock().select((s) => ({
2015
+ * ...s,
2016
+ * maxPriceInPeriod: expr.maxOver(s.price, {
2017
+ * partitionBy: [s.symbol],
2018
+ * }),
2019
+ * }))
2020
+ * ```
2021
+ */
2022
+ maxOver<T extends ColumnPrimitive>(
2023
+ column: ExprUnit<T>,
2024
+ spec: WinSpecInput,
2025
+ ): ExprUnit<T | undefined> {
2026
+ return new ExprUnit(column.dataType, {
2027
+ type: "window",
2028
+ fn: { type: "max", column: toExpr(column) },
2029
+ spec: toWinSpec(spec),
2030
+ });
2031
+ },
2032
+
2033
+ //#endregion
2034
+
2035
+ //#region ========== Helper ==========
2036
+
2037
+ /**
2038
+ * ExprInput을 Expr로 Transform (내부용)
2039
+ *
2040
+ * @param value - Transform할 value
2041
+ * @returns Expr JSON AST
2042
+ */
2043
+ toExpr(value: ExprInput<ColumnPrimitive>): Expr {
2044
+ return toExpr(value);
2045
+ },
2046
+
2047
+ //#endregion
2048
+ };
2049
+
2050
+ //#region ========== Internal Helpers ==========
2051
+
2052
+ // 여러 value 중 첫 번째 non-null return (COALESCE)
2053
+ function ifNull<TPrimitive extends ColumnPrimitive>(
2054
+ ...args: [
2055
+ ExprInput<TPrimitive | undefined>,
2056
+ ...ExprInput<TPrimitive | undefined>[],
2057
+ ExprInput<NonNullable<TPrimitive>>,
2058
+ ]
2059
+ ): ExprUnit<NonNullable<TPrimitive>>;
2060
+ function ifNull<TPrimitive extends ColumnPrimitive>(
2061
+ ...args: ExprInput<TPrimitive>[]
2062
+ ): ExprUnit<TPrimitive>;
2063
+ function ifNull<TPrimitive extends ColumnPrimitive>(
2064
+ ...args: ExprInput<TPrimitive>[]
2065
+ ): ExprUnit<TPrimitive> {
2066
+ return new ExprUnit(findDataType(args), {
2067
+ type: "ifNull",
2068
+ args: args.map((a) => toExpr(a)),
2069
+ });
2070
+ }
2071
+
2072
+ function createSwitchBuilder<TPrimitive extends ColumnPrimitive>(): SwitchExprBuilder<TPrimitive> {
2073
+ const cases: { when: WhereExpr; then: Expr }[] = [];
2074
+ const thenValues: ExprInput<TPrimitive>[] = []; // then 값들 저장
2075
+
2076
+ return {
2077
+ case(condition: WhereExprUnit, then: ExprInput<TPrimitive>): typeof this {
2078
+ cases.push({
2079
+ when: condition.expr,
2080
+ then: toExpr(then),
2081
+ });
2082
+ thenValues.push(then);
2083
+ return this;
2084
+ },
2085
+ default(value: ExprInput<TPrimitive>): ExprUnit<TPrimitive> {
2086
+ const allValues = [...thenValues, value];
2087
+ // 1. ExprUnit에서 dataType 찾기
2088
+ const exprUnit = allValues.find((v): v is ExprUnit<TPrimitive> => v instanceof ExprUnit);
2089
+ if (exprUnit) {
2090
+ return new ExprUnit(exprUnit.dataType, {
2091
+ type: "switch",
2092
+ cases,
2093
+ else: toExpr(value),
2094
+ });
2095
+ }
2096
+
2097
+ // 2. non-null 리터럴에서 추론
2098
+ const nonNullLiteral = allValues.find((v) => v != null) as ColumnPrimitive;
2099
+ if (nonNullLiteral == null) {
2100
+ throw new Error("At least one of switch's case/default must be non-null.");
2101
+ }
2102
+
2103
+ return new ExprUnit(inferColumnPrimitiveStr(nonNullLiteral), {
2104
+ type: "switch",
2105
+ cases,
2106
+ else: toExpr(value),
2107
+ });
2108
+ },
2109
+ };
2110
+ }
2111
+
2112
+ export function toExpr(value: ExprInput<ColumnPrimitive>): Expr {
2113
+ if (value instanceof ExprUnit) {
2114
+ return value.expr;
2115
+ }
2116
+ return { type: "value", value };
2117
+ }
2118
+
2119
+ function findDataType<TPrimitive extends ColumnPrimitive>(
2120
+ args: ExprInput<TPrimitive>[],
2121
+ ): ColumnPrimitiveStr {
2122
+ const exprUnit = args.find((a): a is ExprUnit<TPrimitive> => a instanceof ExprUnit);
2123
+ if (!exprUnit) {
2124
+ throw new Error("At least one of the arguments must be an ExprUnit.");
2125
+ }
2126
+ return exprUnit.dataType;
2127
+ }
2128
+
2129
+ function toWinSpec(spec: WinSpecInput): WinSpec {
2130
+ const result: WinSpec = {};
2131
+ if (spec.partitionBy != null) {
2132
+ result.partitionBy = spec.partitionBy.map((e) => toExpr(e));
2133
+ }
2134
+ if (spec.orderBy != null) {
2135
+ result.orderBy = spec.orderBy.map(([e, dir]) => [toExpr(e), dir]);
2136
+ }
2137
+ return result;
2138
+ }
2139
+
2140
+ //#endregion