@simplysm/orm-common 13.0.100 → 14.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/dist/create-db-context.d.ts +10 -10
  2. package/dist/create-db-context.js +312 -276
  3. package/dist/create-db-context.js.map +1 -6
  4. package/dist/ddl/column-ddl.d.ts +4 -4
  5. package/dist/ddl/column-ddl.js +41 -35
  6. package/dist/ddl/column-ddl.js.map +1 -6
  7. package/dist/ddl/initialize.d.ts +17 -17
  8. package/dist/ddl/initialize.js +200 -142
  9. package/dist/ddl/initialize.js.map +1 -6
  10. package/dist/ddl/relation-ddl.d.ts +6 -6
  11. package/dist/ddl/relation-ddl.js +55 -48
  12. package/dist/ddl/relation-ddl.js.map +1 -6
  13. package/dist/ddl/schema-ddl.d.ts +4 -4
  14. package/dist/ddl/schema-ddl.js +21 -15
  15. package/dist/ddl/schema-ddl.js.map +1 -6
  16. package/dist/ddl/table-ddl.d.ts +20 -20
  17. package/dist/ddl/table-ddl.js +139 -93
  18. package/dist/ddl/table-ddl.js.map +1 -6
  19. package/dist/define-db-context.js +10 -13
  20. package/dist/define-db-context.js.map +1 -6
  21. package/dist/errors/db-transaction-error.d.ts +15 -15
  22. package/dist/errors/db-transaction-error.d.ts.map +1 -1
  23. package/dist/errors/db-transaction-error.js +53 -19
  24. package/dist/errors/db-transaction-error.js.map +1 -6
  25. package/dist/exec/executable.d.ts +23 -23
  26. package/dist/exec/executable.js +94 -40
  27. package/dist/exec/executable.js.map +1 -6
  28. package/dist/exec/queryable.d.ts +97 -97
  29. package/dist/exec/queryable.js +1310 -1204
  30. package/dist/exec/queryable.js.map +1 -6
  31. package/dist/exec/search-parser.d.ts +31 -31
  32. package/dist/exec/search-parser.d.ts.map +1 -1
  33. package/dist/exec/search-parser.js +158 -59
  34. package/dist/exec/search-parser.js.map +1 -6
  35. package/dist/expr/expr-unit.d.ts +4 -4
  36. package/dist/expr/expr-unit.js +24 -18
  37. package/dist/expr/expr-unit.js.map +1 -6
  38. package/dist/expr/expr.d.ts +6 -6
  39. package/dist/expr/expr.js +1872 -1844
  40. package/dist/expr/expr.js.map +1 -6
  41. package/dist/index.js +23 -1
  42. package/dist/index.js.map +1 -6
  43. package/dist/models/system-migration.js +7 -7
  44. package/dist/models/system-migration.js.map +1 -6
  45. package/dist/query-builder/base/expr-renderer-base.d.ts +10 -10
  46. package/dist/query-builder/base/expr-renderer-base.js +27 -21
  47. package/dist/query-builder/base/expr-renderer-base.js.map +1 -6
  48. package/dist/query-builder/base/query-builder-base.d.ts +21 -21
  49. package/dist/query-builder/base/query-builder-base.d.ts.map +1 -1
  50. package/dist/query-builder/base/query-builder-base.js +90 -80
  51. package/dist/query-builder/base/query-builder-base.js.map +1 -6
  52. package/dist/query-builder/mssql/mssql-expr-renderer.d.ts +4 -4
  53. package/dist/query-builder/mssql/mssql-expr-renderer.d.ts.map +1 -1
  54. package/dist/query-builder/mssql/mssql-expr-renderer.js +447 -420
  55. package/dist/query-builder/mssql/mssql-expr-renderer.js.map +1 -6
  56. package/dist/query-builder/mssql/mssql-query-builder.js +483 -443
  57. package/dist/query-builder/mssql/mssql-query-builder.js.map +1 -6
  58. package/dist/query-builder/mysql/mysql-expr-renderer.d.ts +4 -4
  59. package/dist/query-builder/mysql/mysql-expr-renderer.d.ts.map +1 -1
  60. package/dist/query-builder/mysql/mysql-expr-renderer.js +451 -419
  61. package/dist/query-builder/mysql/mysql-expr-renderer.js.map +1 -6
  62. package/dist/query-builder/mysql/mysql-query-builder.js +570 -479
  63. package/dist/query-builder/mysql/mysql-query-builder.js.map +1 -6
  64. package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts +4 -4
  65. package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts.map +1 -1
  66. package/dist/query-builder/postgresql/postgresql-expr-renderer.js +449 -422
  67. package/dist/query-builder/postgresql/postgresql-expr-renderer.js.map +1 -6
  68. package/dist/query-builder/postgresql/postgresql-query-builder.js +511 -460
  69. package/dist/query-builder/postgresql/postgresql-query-builder.js.map +1 -6
  70. package/dist/query-builder/query-builder.d.ts +1 -1
  71. package/dist/query-builder/query-builder.js +13 -13
  72. package/dist/query-builder/query-builder.js.map +1 -6
  73. package/dist/schema/factory/column-builder.d.ts +84 -84
  74. package/dist/schema/factory/column-builder.js +248 -185
  75. package/dist/schema/factory/column-builder.js.map +1 -6
  76. package/dist/schema/factory/index-builder.d.ts +38 -38
  77. package/dist/schema/factory/index-builder.js +144 -85
  78. package/dist/schema/factory/index-builder.js.map +1 -6
  79. package/dist/schema/factory/relation-builder.d.ts +91 -91
  80. package/dist/schema/factory/relation-builder.d.ts.map +1 -1
  81. package/dist/schema/factory/relation-builder.js +274 -136
  82. package/dist/schema/factory/relation-builder.js.map +1 -6
  83. package/dist/schema/procedure-builder.d.ts +51 -51
  84. package/dist/schema/procedure-builder.d.ts.map +1 -1
  85. package/dist/schema/procedure-builder.js +205 -131
  86. package/dist/schema/procedure-builder.js.map +1 -6
  87. package/dist/schema/table-builder.d.ts +55 -55
  88. package/dist/schema/table-builder.d.ts.map +1 -1
  89. package/dist/schema/table-builder.js +274 -205
  90. package/dist/schema/table-builder.js.map +1 -6
  91. package/dist/schema/view-builder.d.ts +44 -44
  92. package/dist/schema/view-builder.d.ts.map +1 -1
  93. package/dist/schema/view-builder.js +189 -116
  94. package/dist/schema/view-builder.js.map +1 -6
  95. package/dist/types/column.js +60 -30
  96. package/dist/types/column.js.map +1 -6
  97. package/dist/types/db-context-def.d.ts +9 -9
  98. package/dist/types/db-context-def.js +2 -1
  99. package/dist/types/db-context-def.js.map +1 -6
  100. package/dist/types/db.d.ts +47 -47
  101. package/dist/types/db.js +15 -5
  102. package/dist/types/db.js.map +1 -6
  103. package/dist/types/expr.d.ts +81 -81
  104. package/dist/types/expr.d.ts.map +1 -1
  105. package/dist/types/expr.js +3 -1
  106. package/dist/types/expr.js.map +1 -6
  107. package/dist/types/query-def.d.ts +46 -46
  108. package/dist/types/query-def.d.ts.map +1 -1
  109. package/dist/types/query-def.js +31 -24
  110. package/dist/types/query-def.js.map +1 -6
  111. package/dist/utils/result-parser.js +362 -221
  112. package/dist/utils/result-parser.js.map +1 -6
  113. package/package.json +5 -7
  114. package/src/create-db-context.ts +31 -31
  115. package/src/ddl/column-ddl.ts +4 -4
  116. package/src/ddl/initialize.ts +38 -38
  117. package/src/ddl/relation-ddl.ts +6 -6
  118. package/src/ddl/schema-ddl.ts +4 -4
  119. package/src/ddl/table-ddl.ts +24 -24
  120. package/src/errors/db-transaction-error.ts +13 -13
  121. package/src/exec/executable.ts +25 -25
  122. package/src/exec/queryable.ts +134 -134
  123. package/src/exec/search-parser.ts +50 -50
  124. package/src/expr/expr-unit.ts +4 -4
  125. package/src/expr/expr.ts +13 -13
  126. package/src/index.ts +8 -8
  127. package/src/models/system-migration.ts +1 -1
  128. package/src/query-builder/base/expr-renderer-base.ts +21 -21
  129. package/src/query-builder/base/query-builder-base.ts +33 -33
  130. package/src/query-builder/mssql/mssql-expr-renderer.ts +11 -11
  131. package/src/query-builder/mssql/mssql-query-builder.ts +11 -11
  132. package/src/query-builder/mysql/mysql-expr-renderer.ts +15 -15
  133. package/src/query-builder/mysql/mysql-query-builder.ts +3 -3
  134. package/src/query-builder/postgresql/postgresql-expr-renderer.ts +9 -9
  135. package/src/query-builder/postgresql/postgresql-query-builder.ts +7 -7
  136. package/src/query-builder/query-builder.ts +1 -1
  137. package/src/schema/factory/column-builder.ts +86 -86
  138. package/src/schema/factory/index-builder.ts +38 -38
  139. package/src/schema/factory/relation-builder.ts +93 -93
  140. package/src/schema/procedure-builder.ts +52 -52
  141. package/src/schema/table-builder.ts +56 -56
  142. package/src/schema/view-builder.ts +45 -45
  143. package/src/types/column.ts +1 -1
  144. package/src/types/db-context-def.ts +15 -15
  145. package/src/types/db.ts +50 -50
  146. package/src/types/expr.ts +103 -103
  147. package/src/types/query-def.ts +50 -50
  148. package/src/utils/result-parser.ts +39 -39
  149. package/README.md +0 -192
  150. package/docs/core.md +0 -234
  151. package/docs/expression.md +0 -234
  152. package/docs/query-builder.md +0 -93
  153. package/docs/queryable.md +0 -198
  154. package/docs/schema-builders.md +0 -463
  155. package/docs/types.md +0 -445
  156. package/docs/utilities.md +0 -27
  157. package/tests/db-context/create-db-context.spec.ts +0 -193
  158. package/tests/db-context/define-db-context.spec.ts +0 -17
  159. package/tests/ddl/basic.expected.ts +0 -341
  160. package/tests/ddl/basic.spec.ts +0 -557
  161. package/tests/ddl/column-builder.expected.ts +0 -310
  162. package/tests/ddl/column-builder.spec.ts +0 -525
  163. package/tests/ddl/index-builder.expected.ts +0 -38
  164. package/tests/ddl/index-builder.spec.ts +0 -148
  165. package/tests/ddl/procedure-builder.expected.ts +0 -52
  166. package/tests/ddl/procedure-builder.spec.ts +0 -128
  167. package/tests/ddl/relation-builder.expected.ts +0 -36
  168. package/tests/ddl/relation-builder.spec.ts +0 -171
  169. package/tests/ddl/table-builder.expected.ts +0 -113
  170. package/tests/ddl/table-builder.spec.ts +0 -399
  171. package/tests/ddl/view-builder.expected.ts +0 -38
  172. package/tests/ddl/view-builder.spec.ts +0 -116
  173. package/tests/dml/delete.expected.ts +0 -96
  174. package/tests/dml/delete.spec.ts +0 -127
  175. package/tests/dml/insert.expected.ts +0 -192
  176. package/tests/dml/insert.spec.ts +0 -210
  177. package/tests/dml/update.expected.ts +0 -176
  178. package/tests/dml/update.spec.ts +0 -222
  179. package/tests/dml/upsert.expected.ts +0 -215
  180. package/tests/dml/upsert.spec.ts +0 -190
  181. package/tests/errors/queryable-errors.spec.ts +0 -126
  182. package/tests/escape.spec.ts +0 -59
  183. package/tests/examples/pivot.expected.ts +0 -211
  184. package/tests/examples/pivot.spec.ts +0 -200
  185. package/tests/examples/sampling.expected.ts +0 -69
  186. package/tests/examples/sampling.spec.ts +0 -42
  187. package/tests/examples/unpivot.expected.ts +0 -120
  188. package/tests/examples/unpivot.spec.ts +0 -161
  189. package/tests/exec/search-parser.spec.ts +0 -267
  190. package/tests/executable/basic.expected.ts +0 -18
  191. package/tests/executable/basic.spec.ts +0 -54
  192. package/tests/expr/comparison.expected.ts +0 -282
  193. package/tests/expr/comparison.spec.ts +0 -334
  194. package/tests/expr/conditional.expected.ts +0 -134
  195. package/tests/expr/conditional.spec.ts +0 -249
  196. package/tests/expr/date.expected.ts +0 -332
  197. package/tests/expr/date.spec.ts +0 -459
  198. package/tests/expr/math.expected.ts +0 -62
  199. package/tests/expr/math.spec.ts +0 -59
  200. package/tests/expr/string.expected.ts +0 -218
  201. package/tests/expr/string.spec.ts +0 -300
  202. package/tests/expr/utility.expected.ts +0 -147
  203. package/tests/expr/utility.spec.ts +0 -155
  204. package/tests/select/basic.expected.ts +0 -322
  205. package/tests/select/basic.spec.ts +0 -433
  206. package/tests/select/filter.expected.ts +0 -357
  207. package/tests/select/filter.spec.ts +0 -954
  208. package/tests/select/group.expected.ts +0 -169
  209. package/tests/select/group.spec.ts +0 -159
  210. package/tests/select/join.expected.ts +0 -582
  211. package/tests/select/join.spec.ts +0 -692
  212. package/tests/select/order.expected.ts +0 -150
  213. package/tests/select/order.spec.ts +0 -140
  214. package/tests/select/recursive-cte.expected.ts +0 -244
  215. package/tests/select/recursive-cte.spec.ts +0 -514
  216. package/tests/select/result-meta.spec.ts +0 -270
  217. package/tests/select/subquery.expected.ts +0 -363
  218. package/tests/select/subquery.spec.ts +0 -441
  219. package/tests/select/view.expected.ts +0 -155
  220. package/tests/select/view.spec.ts +0 -235
  221. package/tests/select/window.expected.ts +0 -345
  222. package/tests/select/window.spec.ts +0 -433
  223. package/tests/setup/MockExecutor.ts +0 -18
  224. package/tests/setup/TestDbContext.ts +0 -59
  225. package/tests/setup/models/Company.ts +0 -13
  226. package/tests/setup/models/Employee.ts +0 -10
  227. package/tests/setup/models/MonthlySales.ts +0 -11
  228. package/tests/setup/models/Post.ts +0 -16
  229. package/tests/setup/models/Sales.ts +0 -10
  230. package/tests/setup/models/User.ts +0 -19
  231. package/tests/setup/procedure/GetAllUsers.ts +0 -9
  232. package/tests/setup/procedure/GetUserById.ts +0 -12
  233. package/tests/setup/test-utils.ts +0 -72
  234. package/tests/setup/views/ActiveUsers.ts +0 -8
  235. package/tests/setup/views/UserSummary.ts +0 -11
  236. package/tests/types/nullable-queryable-record.spec.ts +0 -97
  237. package/tests/utils/result-parser-perf.spec.ts +0 -143
  238. package/tests/utils/result-parser.spec.ts +0 -667
package/dist/expr/expr.js CHANGED
@@ -1,1857 +1,1885 @@
1
1
  import { ArgumentError } from "@simplysm/core-common";
2
- import {
3
- dataTypeStrToColumnPrimitiveStr,
4
- inferColumnPrimitiveStr
5
- } from "../types/column.js";
2
+ import { dataTypeStrToColumnPrimitiveStr, inferColumnPrimitiveStr, } from "../types/column.js";
6
3
  import { ExprUnit, WhereExprUnit } from "./expr-unit.js";
7
- const expr = {
8
- //#region ========== Value creation ==========
9
- /**
10
- * Wrap literal value as ExprUnit
11
- *
12
- * Widen to base type matching dataType, remove literal type
13
- *
14
- * @param dataType - The data type of the value ("string", "number", "boolean", "DateTime", "DateOnly", "Time", "Uuid", "Buffer")
15
- * @param value - Value to wrap (undefined allowed)
16
- * @returns Wrapped ExprUnit instance
17
- *
18
- * @example
19
- * ```typescript
20
- * // String value
21
- * expr.val("string", "active")
22
- *
23
- * // Number value
24
- * expr.val("number", 100)
25
- *
26
- * // Date value
27
- * expr.val("DateOnly", DateOnly.today())
28
- *
29
- * // undefined value
30
- * expr.val("string", undefined)
31
- * ```
32
- */
33
- val(dataType, value) {
34
- return new ExprUnit(dataType, { type: "value", value });
35
- },
36
- /**
37
- * Generate column reference
38
- *
39
- * Typically proxy objects are used inside Queryable callbacks
40
- *
41
- * @param dataType - Column data type
42
- * @param path - Column path (table alias, column name, etc.)
43
- * @returns Column reference ExprUnit instance
44
- *
45
- * @example
46
- * ```typescript
47
- * // Direct column reference (internal use)
48
- * expr.col("string", "T1", "name")
49
- * ```
50
- */
51
- col(dataType, ...path) {
52
- return new ExprUnit(dataType, { type: "column", path });
53
- },
54
- /**
55
- * Raw SQL expression Generate (escape hatch)
56
- *
57
- * Use when you need to directly use DB-specific functions or syntax not supported by the ORM.
58
- * Used as tagged template literal, interpolated values are automatically parameterized
59
- *
60
- * @param dataType - Data type of the returned value
61
- * @returns Tagged template function
62
- *
63
- * @example
64
- * ```typescript
65
- * // Using MySQL JSON function
66
- * db.user().select((u) => ({
67
- * name: u.name,
68
- * data: expr.raw("string")`JSON_EXTRACT(${u.metadata}, '$.email')`,
69
- * }))
70
- *
71
- * // PostgreSQL array function
72
- * expr.raw("number")`ARRAY_LENGTH(${u.tags}, 1)`
73
- * ```
74
- */
75
- raw(dataType) {
76
- return (strings, ...values) => {
77
- const sql = strings.reduce((acc, str, i) => {
78
- if (i < values.length) {
79
- return acc + str + `$${i + 1}`;
4
+ /**
5
+ * Dialect 독립적 SQL expression builder
6
+ *
7
+ * SQL 문자열 대신 JSON AST(Expr)를 생성하며, QueryBuilder가
8
+ * 각 DBMS(MySQL, MSSQL, PostgreSQL)로 변환
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * // WHERE condition
13
+ * db.user().where((u) => [
14
+ * expr.eq(u.status, "active"),
15
+ * expr.gt(u.age, 18),
16
+ * ])
17
+ *
18
+ * // SELECT expression
19
+ * db.user().select((u) => ({
20
+ * name: expr.concat(u.firstName, " ", u.lastName),
21
+ * age: expr.dateDiff("year", u.birthDate, expr.val("DateOnly", DateOnly.today())),
22
+ * }))
23
+ *
24
+ * // Aggregate function
25
+ * db.order().groupBy((o) => o.userId).select((o) => ({
26
+ * userId: o.userId,
27
+ * total: expr.sum(o.amount),
28
+ * }))
29
+ * ```
30
+ *
31
+ * @see {@link Queryable} Query builder 클래스
32
+ * @see {@link ExprUnit} Expression 래퍼 클래스
33
+ */
34
+ export const expr = {
35
+ //#region ========== 값 생성 ==========
36
+ /**
37
+ * Wrap literal value as ExprUnit
38
+ *
39
+ * Widen to base type matching dataType, remove literal type
40
+ *
41
+ * @param dataType - The data type of the value ("string", "number", "boolean", "DateTime", "DateOnly", "Time", "Uuid", "Buffer")
42
+ * @param value - Value to wrap (undefined allowed)
43
+ * @returns Wrapped ExprUnit instance
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * // String value
48
+ * expr.val("string", "active")
49
+ *
50
+ * // Number value
51
+ * expr.val("number", 100)
52
+ *
53
+ * // Date value
54
+ * expr.val("DateOnly", DateOnly.today())
55
+ *
56
+ * // undefined value
57
+ * expr.val("string", undefined)
58
+ * ```
59
+ */
60
+ val(dataType, value) {
61
+ return new ExprUnit(dataType, { type: "value", value });
62
+ },
63
+ /**
64
+ * Generate column reference
65
+ *
66
+ * Typically proxy objects are used inside Queryable callbacks
67
+ *
68
+ * @param dataType - Column data type
69
+ * @param path - Column path (table alias, column name, etc.)
70
+ * @returns Column reference ExprUnit instance
71
+ *
72
+ * @example
73
+ * ```typescript
74
+ * // Direct column reference (internal use)
75
+ * expr.col("string", "T1", "name")
76
+ * ```
77
+ */
78
+ col(dataType, ...path) {
79
+ return new ExprUnit(dataType, { type: "column", path });
80
+ },
81
+ /**
82
+ * Raw SQL expression Generate (escape hatch)
83
+ *
84
+ * Use when you need to directly use DB-specific functions or syntax not supported by the ORM.
85
+ * Used as tagged template literal, interpolated values are automatically parameterized
86
+ *
87
+ * @param dataType - Data type of the returned value
88
+ * @returns Tagged template function
89
+ *
90
+ * @example
91
+ * ```typescript
92
+ * // Using MySQL JSON function
93
+ * db.user().select((u) => ({
94
+ * name: u.name,
95
+ * data: expr.raw("string")`JSON_EXTRACT(${u.metadata}, '$.email')`,
96
+ * }))
97
+ *
98
+ * // PostgreSQL array function
99
+ * expr.raw("number")`ARRAY_LENGTH(${u.tags}, 1)`
100
+ * ```
101
+ */
102
+ raw(dataType) {
103
+ return (strings, ...values) => {
104
+ const sql = strings.reduce((acc, str, i) => {
105
+ if (i < values.length) {
106
+ return acc + str + `$${i + 1}`; // placeholder (transformed by ExprRenderer)
107
+ }
108
+ return acc + str;
109
+ }, "");
110
+ const params = values.map((v) => toExpr(v));
111
+ return new ExprUnit(dataType, { type: "raw", sql, params });
112
+ };
113
+ },
114
+ //#endregion
115
+ //#region ========== WHERE - Comparison operators ==========
116
+ /**
117
+ * Equality comparison (NULL-safe)
118
+ *
119
+ * Safely compare even NULL values (MySQL: `<=>`, MSSQL/PostgreSQL: `IS NULL OR =`)
120
+ *
121
+ * @param source - Column or expression to compare
122
+ * @param target - Target value or expression for comparison
123
+ * @returns WHERE condition expression
124
+ *
125
+ * @example
126
+ * ```typescript
127
+ * db.user().where((u) => [expr.eq(u.status, "active")])
128
+ * // WHERE status <=> 'active' (MySQL)
129
+ * ```
130
+ */
131
+ eq(source, target) {
132
+ return new WhereExprUnit({
133
+ type: "eq",
134
+ source: toExpr(source),
135
+ target: toExpr(target),
136
+ });
137
+ },
138
+ /**
139
+ * Greater than comparison (>)
140
+ *
141
+ * @param source - Column or expression to compare
142
+ * @param target - Target value or expression for comparison
143
+ * @returns WHERE condition expression
144
+ *
145
+ * @example
146
+ * ```typescript
147
+ * db.user().where((u) => [expr.gt(u.age, 18)])
148
+ * // WHERE age > 18
149
+ * ```
150
+ */
151
+ gt(source, target) {
152
+ return new WhereExprUnit({
153
+ type: "gt",
154
+ source: toExpr(source),
155
+ target: toExpr(target),
156
+ });
157
+ },
158
+ /**
159
+ * Less than comparison (<)
160
+ *
161
+ * @param source - Column or expression to compare
162
+ * @param target - Target value or expression for comparison
163
+ * @returns WHERE condition expression
164
+ *
165
+ * @example
166
+ * ```typescript
167
+ * db.user().where((u) => [expr.lt(u.score, 60)])
168
+ * // WHERE score < 60
169
+ * ```
170
+ */
171
+ lt(source, target) {
172
+ return new WhereExprUnit({
173
+ type: "lt",
174
+ source: toExpr(source),
175
+ target: toExpr(target),
176
+ });
177
+ },
178
+ /**
179
+ * Greater than or equal comparison (>=)
180
+ *
181
+ * @param source - Column or expression to compare
182
+ * @param target - Target value or expression for comparison
183
+ * @returns WHERE condition expression
184
+ *
185
+ * @example
186
+ * ```typescript
187
+ * db.user().where((u) => [expr.gte(u.age, 18)])
188
+ * // WHERE age >= 18
189
+ * ```
190
+ */
191
+ gte(source, target) {
192
+ return new WhereExprUnit({
193
+ type: "gte",
194
+ source: toExpr(source),
195
+ target: toExpr(target),
196
+ });
197
+ },
198
+ /**
199
+ * Less than or equal comparison (<=)
200
+ *
201
+ * @param source - Column or expression to compare
202
+ * @param target - Target value or expression for comparison
203
+ * @returns WHERE condition expression
204
+ *
205
+ * @example
206
+ * ```typescript
207
+ * db.user().where((u) => [expr.lte(u.score, 100)])
208
+ * // WHERE score <= 100
209
+ * ```
210
+ */
211
+ lte(source, target) {
212
+ return new WhereExprUnit({
213
+ type: "lte",
214
+ source: toExpr(source),
215
+ target: toExpr(target),
216
+ });
217
+ },
218
+ /**
219
+ * range comparison (BETWEEN)
220
+ *
221
+ * If from/to is undefined, that direction is unbounded
222
+ *
223
+ * @param source - Column or expression to compare
224
+ * @param from - Start value (no lower bound if undefined)
225
+ * @param to - End value (no upper bound if undefined)
226
+ * @returns WHERE condition expression
227
+ *
228
+ * @example
229
+ * ```typescript
230
+ * // Specify range
231
+ * db.user().where((u) => [expr.between(u.age, 18, 65)])
232
+ * // WHERE age BETWEEN 18 AND 65
233
+ *
234
+ * // Specify only lower bound
235
+ * db.user().where((u) => [expr.between(u.age, 18, undefined)])
236
+ * // WHERE age >= 18
237
+ * ```
238
+ */
239
+ between(source, from, to) {
240
+ return new WhereExprUnit({
241
+ type: "between",
242
+ source: toExpr(source),
243
+ from: from != null ? toExpr(from) : undefined,
244
+ to: to != null ? toExpr(to) : undefined,
245
+ });
246
+ },
247
+ //#endregion
248
+ //#region ========== WHERE - NULL check ==========
249
+ /**
250
+ * NULL check (IS NULL)
251
+ *
252
+ * @param source - Column or expression to check
253
+ * @returns WHERE condition expression
254
+ *
255
+ * @example
256
+ * ```typescript
257
+ * db.user().where((u) => [expr.null(u.deletedAt)])
258
+ * // WHERE deletedAt IS NULL
259
+ * ```
260
+ */
261
+ null(source) {
262
+ return new WhereExprUnit({
263
+ type: "null",
264
+ arg: toExpr(source),
265
+ });
266
+ },
267
+ //#endregion
268
+ //#region ========== WHERE - String search ==========
269
+ /**
270
+ * LIKE pattern matching
271
+ *
272
+ * `%` matches zero or more characters, `_` matches a single character.
273
+ * Special characters are escaped with `\`
274
+ *
275
+ * @param source - Column or expression to search
276
+ * @param pattern - Search pattern (%, _ wildcards available)
277
+ * @returns WHERE condition expression
278
+ *
279
+ * @example
280
+ * ```typescript
281
+ * // Prefix search
282
+ * db.user().where((u) => [expr.like(u.name, "John%")])
283
+ * // WHERE name LIKE 'John%' ESCAPE '\'
284
+ *
285
+ * // Contains search
286
+ * db.user().where((u) => [expr.like(u.email, "%@gmail.com")])
287
+ * ```
288
+ */
289
+ like(source, pattern) {
290
+ return new WhereExprUnit({
291
+ type: "like",
292
+ source: toExpr(source),
293
+ pattern: toExpr(pattern),
294
+ });
295
+ },
296
+ /**
297
+ * Regular expression pattern matching
298
+ *
299
+ * Note: regex syntax may differ between DBMS implementations
300
+ *
301
+ * @param source - Column or expression to search
302
+ * @param pattern - Regular expression pattern
303
+ * @returns WHERE condition expression
304
+ *
305
+ * @example
306
+ * ```typescript
307
+ * db.user().where((u) => [expr.regexp(u.email, "^[a-z]+@")])
308
+ * // MySQL: WHERE email REGEXP '^[a-z]+@'
309
+ * ```
310
+ */
311
+ regexp(source, pattern) {
312
+ return new WhereExprUnit({
313
+ type: "regexp",
314
+ source: toExpr(source),
315
+ pattern: toExpr(pattern),
316
+ });
317
+ },
318
+ //#endregion
319
+ //#region ========== WHERE - IN ==========
320
+ /**
321
+ * IN operator - Compare against a list of values
322
+ *
323
+ * @param source - Column or expression to compare
324
+ * @param values - List of values to compare against
325
+ * @returns WHERE condition expression
326
+ *
327
+ * @example
328
+ * ```typescript
329
+ * db.user().where((u) => [expr.in(u.status, ["active", "pending"])])
330
+ * // WHERE status IN ('active', 'pending')
331
+ * ```
332
+ */
333
+ in(source, values) {
334
+ return new WhereExprUnit({
335
+ type: "in",
336
+ source: toExpr(source),
337
+ values: values.map((v) => toExpr(v)),
338
+ });
339
+ },
340
+ /**
341
+ * IN (SELECT ...) - Compare against subquery results
342
+ *
343
+ * The subquery must SELECT only a single column
344
+ *
345
+ * @param source - Column or expression to compare
346
+ * @param query - Queryable that returns a single column
347
+ * @returns WHERE condition expression
348
+ * @throws {Error} When the subquery does not return a single column
349
+ *
350
+ * @example
351
+ * ```typescript
352
+ * db.user().where((u) => [
353
+ * expr.inQuery(
354
+ * u.id,
355
+ * db.order()
356
+ * .where((o) => [expr.gt(o.amount, 1000)])
357
+ * .select((o) => ({ userId: o.userId }))
358
+ * ),
359
+ * ])
360
+ * // WHERE id IN (SELECT userId FROM Order WHERE amount > 1000)
361
+ * ```
362
+ */
363
+ inQuery(source, query) {
364
+ const queryDef = query.getSelectQueryDef();
365
+ if (queryDef.select == null || Object.keys(queryDef.select).length !== 1) {
366
+ throw new Error("inQuery subquery must SELECT only a single column.");
80
367
  }
81
- return acc + str;
82
- }, "");
83
- const params = values.map((v) => toExpr(v));
84
- return new ExprUnit(dataType, { type: "raw", sql, params });
85
- };
86
- },
87
- //#endregion
88
- //#region ========== WHERE - Comparison operators ==========
89
- /**
90
- * Equality comparison (NULL-safe)
91
- *
92
- * Safely compare even NULL values (MySQL: `<=>`, MSSQL/PostgreSQL: `IS NULL OR =`)
93
- *
94
- * @param source - Column or expression to compare
95
- * @param target - Target value or expression for comparison
96
- * @returns WHERE condition expression
97
- *
98
- * @example
99
- * ```typescript
100
- * db.user().where((u) => [expr.eq(u.status, "active")])
101
- * // WHERE status <=> 'active' (MySQL)
102
- * ```
103
- */
104
- eq(source, target) {
105
- return new WhereExprUnit({
106
- type: "eq",
107
- source: toExpr(source),
108
- target: toExpr(target)
109
- });
110
- },
111
- /**
112
- * Greater than comparison (>)
113
- *
114
- * @param source - Column or expression to compare
115
- * @param target - Target value or expression for comparison
116
- * @returns WHERE condition expression
117
- *
118
- * @example
119
- * ```typescript
120
- * db.user().where((u) => [expr.gt(u.age, 18)])
121
- * // WHERE age > 18
122
- * ```
123
- */
124
- gt(source, target) {
125
- return new WhereExprUnit({
126
- type: "gt",
127
- source: toExpr(source),
128
- target: toExpr(target)
129
- });
130
- },
131
- /**
132
- * Less than comparison (<)
133
- *
134
- * @param source - Column or expression to compare
135
- * @param target - Target value or expression for comparison
136
- * @returns WHERE condition expression
137
- *
138
- * @example
139
- * ```typescript
140
- * db.user().where((u) => [expr.lt(u.score, 60)])
141
- * // WHERE score < 60
142
- * ```
143
- */
144
- lt(source, target) {
145
- return new WhereExprUnit({
146
- type: "lt",
147
- source: toExpr(source),
148
- target: toExpr(target)
149
- });
150
- },
151
- /**
152
- * Greater than or equal comparison (>=)
153
- *
154
- * @param source - Column or expression to compare
155
- * @param target - Target value or expression for comparison
156
- * @returns WHERE condition expression
157
- *
158
- * @example
159
- * ```typescript
160
- * db.user().where((u) => [expr.gte(u.age, 18)])
161
- * // WHERE age >= 18
162
- * ```
163
- */
164
- gte(source, target) {
165
- return new WhereExprUnit({
166
- type: "gte",
167
- source: toExpr(source),
168
- target: toExpr(target)
169
- });
170
- },
171
- /**
172
- * Less than or equal comparison (<=)
173
- *
174
- * @param source - Column or expression to compare
175
- * @param target - Target value or expression for comparison
176
- * @returns WHERE condition expression
177
- *
178
- * @example
179
- * ```typescript
180
- * db.user().where((u) => [expr.lte(u.score, 100)])
181
- * // WHERE score <= 100
182
- * ```
183
- */
184
- lte(source, target) {
185
- return new WhereExprUnit({
186
- type: "lte",
187
- source: toExpr(source),
188
- target: toExpr(target)
189
- });
190
- },
191
- /**
192
- * range comparison (BETWEEN)
193
- *
194
- * If from/to is undefined, that direction is unbounded
195
- *
196
- * @param source - Column or expression to compare
197
- * @param from - Start value (no lower bound if undefined)
198
- * @param to - End value (no upper bound if undefined)
199
- * @returns WHERE condition expression
200
- *
201
- * @example
202
- * ```typescript
203
- * // Specify range
204
- * db.user().where((u) => [expr.between(u.age, 18, 65)])
205
- * // WHERE age BETWEEN 18 AND 65
206
- *
207
- * // Specify only lower bound
208
- * db.user().where((u) => [expr.between(u.age, 18, undefined)])
209
- * // WHERE age >= 18
210
- * ```
211
- */
212
- between(source, from, to) {
213
- return new WhereExprUnit({
214
- type: "between",
215
- source: toExpr(source),
216
- from: from != null ? toExpr(from) : void 0,
217
- to: to != null ? toExpr(to) : void 0
218
- });
219
- },
220
- //#endregion
221
- //#region ========== WHERE - NULL check ==========
222
- /**
223
- * NULL check (IS NULL)
224
- *
225
- * @param source - Column or expression to check
226
- * @returns WHERE condition expression
227
- *
228
- * @example
229
- * ```typescript
230
- * db.user().where((u) => [expr.null(u.deletedAt)])
231
- * // WHERE deletedAt IS NULL
232
- * ```
233
- */
234
- null(source) {
235
- return new WhereExprUnit({
236
- type: "null",
237
- arg: toExpr(source)
238
- });
239
- },
240
- //#endregion
241
- //#region ========== WHERE - String search ==========
242
- /**
243
- * LIKE pattern matching
244
- *
245
- * `%` matches zero or more characters, `_` matches a single character.
246
- * Special characters are escaped with `\`
247
- *
248
- * @param source - Column or expression to search
249
- * @param pattern - Search pattern (%, _ wildcards available)
250
- * @returns WHERE condition expression
251
- *
252
- * @example
253
- * ```typescript
254
- * // Prefix search
255
- * db.user().where((u) => [expr.like(u.name, "John%")])
256
- * // WHERE name LIKE 'John%' ESCAPE '\'
257
- *
258
- * // Contains search
259
- * db.user().where((u) => [expr.like(u.email, "%@gmail.com")])
260
- * ```
261
- */
262
- like(source, pattern) {
263
- return new WhereExprUnit({
264
- type: "like",
265
- source: toExpr(source),
266
- pattern: toExpr(pattern)
267
- });
268
- },
269
- /**
270
- * Regular expression pattern matching
271
- *
272
- * Note: regex syntax may differ between DBMS implementations
273
- *
274
- * @param source - Column or expression to search
275
- * @param pattern - Regular expression pattern
276
- * @returns WHERE condition expression
277
- *
278
- * @example
279
- * ```typescript
280
- * db.user().where((u) => [expr.regexp(u.email, "^[a-z]+@")])
281
- * // MySQL: WHERE email REGEXP '^[a-z]+@'
282
- * ```
283
- */
284
- regexp(source, pattern) {
285
- return new WhereExprUnit({
286
- type: "regexp",
287
- source: toExpr(source),
288
- pattern: toExpr(pattern)
289
- });
290
- },
291
- //#endregion
292
- //#region ========== WHERE - IN ==========
293
- /**
294
- * IN operator - Compare against a list of values
295
- *
296
- * @param source - Column or expression to compare
297
- * @param values - List of values to compare against
298
- * @returns WHERE condition expression
299
- *
300
- * @example
301
- * ```typescript
302
- * db.user().where((u) => [expr.in(u.status, ["active", "pending"])])
303
- * // WHERE status IN ('active', 'pending')
304
- * ```
305
- */
306
- in(source, values) {
307
- return new WhereExprUnit({
308
- type: "in",
309
- source: toExpr(source),
310
- values: values.map((v) => toExpr(v))
311
- });
312
- },
313
- /**
314
- * IN (SELECT ...) - Compare against subquery results
315
- *
316
- * The subquery must SELECT only a single column
317
- *
318
- * @param source - Column or expression to compare
319
- * @param query - Queryable that returns a single column
320
- * @returns WHERE condition expression
321
- * @throws {Error} When the subquery does not return a single column
322
- *
323
- * @example
324
- * ```typescript
325
- * db.user().where((u) => [
326
- * expr.inQuery(
327
- * u.id,
328
- * db.order()
329
- * .where((o) => [expr.gt(o.amount, 1000)])
330
- * .select((o) => ({ userId: o.userId }))
331
- * ),
332
- * ])
333
- * // WHERE id IN (SELECT userId FROM Order WHERE amount > 1000)
334
- * ```
335
- */
336
- inQuery(source, query) {
337
- const queryDef = query.getSelectQueryDef();
338
- if (queryDef.select == null || Object.keys(queryDef.select).length !== 1) {
339
- throw new Error("inQuery subquery must SELECT only a single column.");
340
- }
341
- return new WhereExprUnit({
342
- type: "inQuery",
343
- source: toExpr(source),
344
- query: queryDef
345
- });
346
- },
347
- /**
348
- * EXISTS (SELECT ...) - Check if subquery returns any rows
349
- *
350
- * Returns true if the subquery returns one or more rows
351
- *
352
- * @param query - Queryable to check for existence
353
- * @returns WHERE condition expression
354
- *
355
- * @example
356
- * ```typescript
357
- * // Query users who have orders
358
- * db.user().where((u) => [
359
- * expr.exists(
360
- * db.order().where((o) => [expr.eq(o.userId, u.id)])
361
- * ),
362
- * ])
363
- * // WHERE EXISTS (SELECT 1 FROM Order WHERE userId = User.id)
364
- * ```
365
- */
366
- exists(query) {
367
- const { select: _, ...queryDefWithoutSelect } = query.getSelectQueryDef();
368
- return new WhereExprUnit({
369
- type: "exists",
370
- query: queryDefWithoutSelect
371
- });
372
- },
373
- //#endregion
374
- //#region ========== WHERE - Logical operators ==========
375
- /**
376
- * NOT operator - Negate a condition
377
- *
378
- * @param arg - Condition to negate
379
- * @returns Negated WHERE condition expression
380
- *
381
- * @example
382
- * ```typescript
383
- * db.user().where((u) => [expr.not(expr.eq(u.status, "deleted"))])
384
- * // WHERE NOT (status <=> 'deleted')
385
- * ```
386
- */
387
- not(arg) {
388
- return new WhereExprUnit({
389
- type: "not",
390
- arg: arg.expr
391
- });
392
- },
393
- /**
394
- * AND operator - All conditions must be satisfied
395
- *
396
- * Combines multiple conditions with AND. Passing an array to where() automatically applies AND
397
- *
398
- * @param conditions - List of conditions to combine with AND
399
- * @returns Combined WHERE condition expression
400
- *
401
- * @example
402
- * ```typescript
403
- * db.user().where((u) => [
404
- * expr.and([
405
- * expr.eq(u.status, "active"),
406
- * expr.gte(u.age, 18),
407
- * ]),
408
- * ])
409
- * // WHERE (status <=> 'active' AND age >= 18)
410
- * ```
411
- */
412
- and(conditions) {
413
- if (conditions.length === 0) {
414
- throw new ArgumentError({ conditions: "empty arrays are not allowed" });
415
- }
416
- return new WhereExprUnit({
417
- type: "and",
418
- conditions: conditions.map((c) => c.expr)
419
- });
420
- },
421
- /**
422
- * OR operator - At least one condition must be satisfied
423
- *
424
- * @param conditions - List of conditions to combine with OR
425
- * @returns Combined WHERE condition expression
426
- *
427
- * @example
428
- * ```typescript
429
- * db.user().where((u) => [
430
- * expr.or([
431
- * expr.eq(u.status, "active"),
432
- * expr.eq(u.status, "pending"),
433
- * ]),
434
- * ])
435
- * // WHERE (status <=> 'active' OR status <=> 'pending')
436
- * ```
437
- */
438
- or(conditions) {
439
- if (conditions.length === 0) {
440
- throw new ArgumentError({ conditions: "empty arrays are not allowed" });
441
- }
442
- return new WhereExprUnit({
443
- type: "or",
444
- conditions: conditions.map((c) => c.expr)
445
- });
446
- },
447
- //#endregion
448
- //#region ========== SELECT - String ==========
449
- /**
450
- * String concatenation (CONCAT)
451
- *
452
- * NULL values are treated as empty strings (auto-transformed per DBMS)
453
- *
454
- * @param args - Strings to concatenate
455
- * @returns Concatenated string expression
456
- *
457
- * @example
458
- * ```typescript
459
- * db.user().select((u) => ({
460
- * fullName: expr.concat(u.firstName, " ", u.lastName),
461
- * }))
462
- * // SELECT CONCAT(firstName, ' ', lastName) AS fullName
463
- * ```
464
- */
465
- concat(...args) {
466
- return new ExprUnit("string", {
467
- type: "concat",
468
- args: args.map((arg) => toExpr(arg))
469
- });
470
- },
471
- /**
472
- * Extract specified length from the left of a string (LEFT)
473
- *
474
- * @param source - Original string
475
- * @param length - Number of characters to extract
476
- * @returns Extracted string expression
477
- *
478
- * @example
479
- * ```typescript
480
- * db.user().select((u) => ({
481
- * initial: expr.left(u.name, 1),
482
- * }))
483
- * // SELECT LEFT(name, 1) AS initial
484
- * ```
485
- */
486
- left(source, length) {
487
- return new ExprUnit("string", {
488
- type: "left",
489
- source: toExpr(source),
490
- length: toExpr(length)
491
- });
492
- },
493
- /**
494
- * Extract specified length from the right of a string (RIGHT)
495
- *
496
- * @param source - Original string
497
- * @param length - Number of characters to extract
498
- * @returns Extracted string expression
499
- *
500
- * @example
501
- * ```typescript
502
- * db.phone().select((p) => ({
503
- * lastFour: expr.right(p.number, 4),
504
- * }))
505
- * // SELECT RIGHT(number, 4) AS lastFour
506
- * ```
507
- */
508
- right(source, length) {
509
- return new ExprUnit("string", {
510
- type: "right",
511
- source: toExpr(source),
512
- length: toExpr(length)
513
- });
514
- },
515
- /**
516
- * Remove whitespace from both sides of a string (TRIM)
517
- *
518
- * @param source - Original string
519
- * @returns String expression with whitespace removed
520
- *
521
- * @example
522
- * ```typescript
523
- * db.user().select((u) => ({
524
- * name: expr.trim(u.name),
525
- * }))
526
- * // SELECT TRIM(name) AS name
527
- * ```
528
- */
529
- trim(source) {
530
- return new ExprUnit("string", {
531
- type: "trim",
532
- arg: toExpr(source)
533
- });
534
- },
535
- /**
536
- * Left padding (LPAD)
537
- *
538
- * Repeatedly adds fillString on the left until the target length is reached
539
- *
540
- * @param source - Original string
541
- * @param length - Target length
542
- * @param fillString - String to use for padding
543
- * @returns Padded string expression
544
- *
545
- * @example
546
- * ```typescript
547
- * db.order().select((o) => ({
548
- * orderNo: expr.padStart(expr.cast(o.id, { type: "varchar", length: 10 }), 8, "0"),
549
- * }))
550
- * // SELECT LPAD(CAST(id AS VARCHAR(10)), 8, '0') AS orderNo
551
- * // Result: "00000123"
552
- * ```
553
- */
554
- padStart(source, length, fillString) {
555
- return new ExprUnit("string", {
556
- type: "padStart",
557
- source: toExpr(source),
558
- length: toExpr(length),
559
- fillString: toExpr(fillString)
560
- });
561
- },
562
- /**
563
- * String replacement (REPLACE)
564
- *
565
- * @param source - Original string
566
- * @param from - String to find
567
- * @param to - Replacement string
568
- * @returns Replaced string expression
569
- *
570
- * @example
571
- * ```typescript
572
- * db.user().select((u) => ({
573
- * phone: expr.replace(u.phone, "-", ""),
574
- * }))
575
- * // SELECT REPLACE(phone, '-', '') AS phone
576
- * ```
577
- */
578
- replace(source, from, to) {
579
- return new ExprUnit("string", {
580
- type: "replace",
581
- source: toExpr(source),
582
- from: toExpr(from),
583
- to: toExpr(to)
584
- });
585
- },
586
- /**
587
- * Convert string to uppercase (UPPER)
588
- *
589
- * @param source - Original string
590
- * @returns Uppercase string expression
591
- *
592
- * @example
593
- * ```typescript
594
- * db.user().select((u) => ({
595
- * code: expr.upper(u.code),
596
- * }))
597
- * // SELECT UPPER(code) AS code
598
- * ```
599
- */
600
- upper(source) {
601
- return new ExprUnit("string", {
602
- type: "upper",
603
- arg: toExpr(source)
604
- });
605
- },
606
- /**
607
- * Convert string to lowercase (LOWER)
608
- *
609
- * @param source - Original string
610
- * @returns Lowercase string expression
611
- *
612
- * @example
613
- * ```typescript
614
- * db.user().select((u) => ({
615
- * email: expr.lower(u.email),
616
- * }))
617
- * // SELECT LOWER(email) AS email
618
- * ```
619
- */
620
- lower(source) {
621
- return new ExprUnit("string", {
622
- type: "lower",
623
- arg: toExpr(source)
624
- });
625
- },
626
- /**
627
- * String length (character count)
628
- *
629
- * @param source - Original string
630
- * @returns Character count
631
- *
632
- * @example
633
- * ```typescript
634
- * db.user().select((u) => ({
635
- * nameLength: expr.length(u.name),
636
- * }))
637
- * // SELECT CHAR_LENGTH(name) AS nameLength
638
- * ```
639
- */
640
- length(source) {
641
- return new ExprUnit("number", {
642
- type: "length",
643
- arg: toExpr(source)
644
- });
645
- },
646
- /**
647
- * String byte length
648
- *
649
- * In UTF-8, CJK characters are 3 bytes each
650
- *
651
- * @param source - Original string
652
- * @returns Byte count
653
- *
654
- * @example
655
- * ```typescript
656
- * db.user().select((u) => ({
657
- * byteLen: expr.byteLength(u.name),
658
- * }))
659
- * // SELECT OCTET_LENGTH(name) AS byteLen
660
- * ```
661
- */
662
- byteLength(source) {
663
- return new ExprUnit("number", {
664
- type: "byteLength",
665
- arg: toExpr(source)
666
- });
667
- },
668
- /**
669
- * Extract part of a string (SUBSTRING)
670
- *
671
- * Uses 1-based index per SQL standard
672
- *
673
- * @param source - Original string
674
- * @param start - Start position (starting from 1)
675
- * @param length - Length to extract (to the end if omitted)
676
- * @returns Extracted string expression
677
- *
678
- * @example
679
- * ```typescript
680
- * db.user().select((u) => ({
681
- * // From "Hello World", 5 characters starting at index 1: "Hello"
682
- * prefix: expr.substring(u.name, 1, 5),
683
- * }))
684
- * // SELECT SUBSTRING(name, 1, 5) AS prefix
685
- * ```
686
- */
687
- substring(source, start, length) {
688
- return new ExprUnit("string", {
689
- type: "substring",
690
- source: toExpr(source),
691
- start: toExpr(start),
692
- ...length != null ? { length: toExpr(length) } : {}
693
- });
694
- },
695
- /**
696
- * Find position within a string (LOCATE/CHARINDEX)
697
- *
698
- * Returns 1-based index, or 0 if not found
699
- *
700
- * @param source - String to search in
701
- * @param search - String to find
702
- * @returns Position (starting from 1, 0 if not found)
703
- *
704
- * @example
705
- * ```typescript
706
- * db.user().select((u) => ({
707
- * atPos: expr.indexOf(u.email, "@"),
708
- * }))
709
- * // SELECT LOCATE('@', email) AS atPos (MySQL)
710
- * // "john@example.com" → 5
711
- * ```
712
- */
713
- indexOf(source, search) {
714
- return new ExprUnit("number", {
715
- type: "indexOf",
716
- source: toExpr(source),
717
- search: toExpr(search)
718
- });
719
- },
720
- //#endregion
721
- //#region ========== SELECT - Number ==========
722
- /**
723
- * Absolute value (ABS)
724
- *
725
- * @param source - Original number
726
- * @returns Absolute value expression
727
- *
728
- * @example
729
- * ```typescript
730
- * db.account().select((a) => ({
731
- * balance: expr.abs(a.balance),
732
- * }))
733
- * // SELECT ABS(balance) AS balance
734
- * ```
735
- */
736
- abs(source) {
737
- return new ExprUnit("number", {
738
- type: "abs",
739
- arg: toExpr(source)
740
- });
741
- },
742
- /**
743
- * Round (ROUND)
744
- *
745
- * @param source - Original number
746
- * @param digits - Number of decimal places
747
- * @returns Rounded number expression
748
- *
749
- * @example
750
- * ```typescript
751
- * db.product().select((p) => ({
752
- * price: expr.round(p.price, 2),
753
- * }))
754
- * // SELECT ROUND(price, 2) AS price
755
- * // 123.456 → 123.46
756
- * ```
757
- */
758
- round(source, digits) {
759
- return new ExprUnit("number", {
760
- type: "round",
761
- arg: toExpr(source),
762
- digits
763
- });
764
- },
765
- /**
766
- * Ceiling (CEILING)
767
- *
768
- * @param source - Original number
769
- * @returns Ceiling number expression
770
- *
771
- * @example
772
- * ```typescript
773
- * db.order().select((o) => ({
774
- * pages: expr.ceil(expr.divide(o.itemCount, 10)),
775
- * }))
776
- * // SELECT CEILING(itemCount / 10) AS pages
777
- * // 25 / 10 = 2.5 → 3
778
- * ```
779
- */
780
- ceil(source) {
781
- return new ExprUnit("number", {
782
- type: "ceil",
783
- arg: toExpr(source)
784
- });
785
- },
786
- /**
787
- * Floor (FLOOR)
788
- *
789
- * @param source - Original number
790
- * @returns Floor number expression
791
- *
792
- * @example
793
- * ```typescript
794
- * db.user().select((u) => ({
795
- * ageGroup: expr.floor(expr.divide(u.age, 10)),
796
- * }))
797
- * // SELECT FLOOR(age / 10) AS ageGroup
798
- * // 25 / 10 = 2.5 2
799
- * ```
800
- */
801
- floor(source) {
802
- return new ExprUnit("number", {
803
- type: "floor",
804
- arg: toExpr(source)
805
- });
806
- },
807
- //#endregion
808
- //#region ========== SELECT - Date ==========
809
- /**
810
- * Extract year (YEAR)
811
- *
812
- * @param source - DateTime or DateOnly expression
813
- * @returns Year (4-digit number)
814
- *
815
- * @example
816
- * ```typescript
817
- * db.user().select((u) => ({
818
- * birthYear: expr.year(u.birthDate),
819
- * }))
820
- * // SELECT YEAR(birthDate) AS birthYear
821
- * ```
822
- */
823
- year(source) {
824
- return new ExprUnit("number", {
825
- type: "year",
826
- arg: toExpr(source)
827
- });
828
- },
829
- /**
830
- * Extract month (MONTH)
831
- *
832
- * @param source - DateTime or DateOnly expression
833
- * @returns Month (1~12)
834
- *
835
- * @example
836
- * ```typescript
837
- * db.order().select((o) => ({
838
- * orderMonth: expr.month(o.createdAt),
839
- * }))
840
- * // SELECT MONTH(createdAt) AS orderMonth
841
- * ```
842
- */
843
- month(source) {
844
- return new ExprUnit("number", {
845
- type: "month",
846
- arg: toExpr(source)
847
- });
848
- },
849
- /**
850
- * Extract day (DAY)
851
- *
852
- * @param source - DateTime or DateOnly expression
853
- * @returns Day (1~31)
854
- *
855
- * @example
856
- * ```typescript
857
- * db.user().select((u) => ({
858
- * birthDay: expr.day(u.birthDate),
859
- * }))
860
- * // SELECT DAY(birthDate) AS birthDay
861
- * ```
862
- */
863
- day(source) {
864
- return new ExprUnit("number", {
865
- type: "day",
866
- arg: toExpr(source)
867
- });
868
- },
869
- /**
870
- * Extract hour (HOUR)
871
- *
872
- * @param source - DateTime or Time expression
873
- * @returns Hour (0~23)
874
- *
875
- * @example
876
- * ```typescript
877
- * db.log().select((l) => ({
878
- * logHour: expr.hour(l.createdAt),
879
- * }))
880
- * // SELECT HOUR(createdAt) AS logHour
881
- * ```
882
- */
883
- hour(source) {
884
- return new ExprUnit("number", {
885
- type: "hour",
886
- arg: toExpr(source)
887
- });
888
- },
889
- /**
890
- * Extract minute (MINUTE)
891
- *
892
- * @param source - DateTime or Time expression
893
- * @returns Minute (0~59)
894
- *
895
- * @example
896
- * ```typescript
897
- * db.log().select((l) => ({
898
- * logMinute: expr.minute(l.createdAt),
899
- * }))
900
- * // SELECT MINUTE(createdAt) AS logMinute
901
- * ```
902
- */
903
- minute(source) {
904
- return new ExprUnit("number", {
905
- type: "minute",
906
- arg: toExpr(source)
907
- });
908
- },
909
- /**
910
- * Extract second (SECOND)
911
- *
912
- * @param source - DateTime or Time expression
913
- * @returns Second (0~59)
914
- *
915
- * @example
916
- * ```typescript
917
- * db.log().select((l) => ({
918
- * logSecond: expr.second(l.createdAt),
919
- * }))
920
- * // SELECT SECOND(createdAt) AS logSecond
921
- * ```
922
- */
923
- second(source) {
924
- return new ExprUnit("number", {
925
- type: "second",
926
- arg: toExpr(source)
927
- });
928
- },
929
- /**
930
- * Extract ISO week number
931
- *
932
- * ISO 8601 week number (starts Monday, 1~53)
933
- *
934
- * @param source - DateOnly expression
935
- * @returns ISO week number
936
- *
937
- * @example
938
- * ```typescript
939
- * db.order().select((o) => ({
940
- * weekNum: expr.isoWeek(o.orderDate),
941
- * }))
942
- * // SELECT WEEK(orderDate, 3) AS weekNum (MySQL)
943
- * ```
944
- */
945
- isoWeek(source) {
946
- return new ExprUnit("number", {
947
- type: "isoWeek",
948
- arg: toExpr(source)
949
- });
950
- },
951
- /**
952
- * ISO week start date (Monday)
953
- *
954
- * Returns the Monday of the week the given date belongs to
955
- *
956
- * @param source - DateOnly expression
957
- * @returns Week start date (Monday)
958
- *
959
- * @example
960
- * ```typescript
961
- * db.order().select((o) => ({
962
- * weekStart: expr.isoWeekStartDate(o.orderDate),
963
- * }))
964
- * // 2024-01-10 (Wed) → 2024-01-08 (Mon)
965
- * ```
966
- */
967
- isoWeekStartDate(source) {
968
- return new ExprUnit("DateOnly", {
969
- type: "isoWeekStartDate",
970
- arg: toExpr(source)
971
- });
972
- },
973
- /**
974
- * ISO year-month (first day of the month)
975
- *
976
- * Returns the first day of the month for the given date
977
- *
978
- * @param source - DateOnly expression
979
- * @returns First day of the month
980
- *
981
- * @example
982
- * ```typescript
983
- * db.order().select((o) => ({
984
- * yearMonth: expr.isoYearMonth(o.orderDate),
985
- * }))
986
- * // 2024-01-15 → 2024-01-01
987
- * ```
988
- */
989
- isoYearMonth(source) {
990
- return new ExprUnit("DateOnly", {
991
- type: "isoYearMonth",
992
- arg: toExpr(source)
993
- });
994
- },
995
- /**
996
- * Calculate date difference (DATEDIFF)
997
- *
998
- * @param unit - Unit ("year", "month", "day", "hour", "minute", "second")
999
- * @param from - Start date
1000
- * @param to - End date
1001
- * @returns Difference value (to - from)
1002
- *
1003
- * @example
1004
- * ```typescript
1005
- * db.user().select((u) => ({
1006
- * age: expr.dateDiff("year", u.birthDate, expr.val("DateOnly", DateOnly.today())),
1007
- * }))
1008
- * // SELECT DATEDIFF(year, birthDate, '2024-01-15') AS age
1009
- * ```
1010
- */
1011
- dateDiff(unit, from, to) {
1012
- return new ExprUnit("number", {
1013
- type: "dateDiff",
1014
- unit,
1015
- from: toExpr(from),
1016
- to: toExpr(to)
1017
- });
1018
- },
1019
- /**
1020
- * Add to date (DATEADD)
1021
- *
1022
- * @param unit - Unit ("year", "month", "day", "hour", "minute", "second")
1023
- * @param source - Original date
1024
- * @param value - Value to add (negative allowed)
1025
- * @returns Calculated date
1026
- *
1027
- * @example
1028
- * ```typescript
1029
- * db.subscription().select((s) => ({
1030
- * expiresAt: expr.dateAdd("month", s.startDate, 12),
1031
- * }))
1032
- * // SELECT DATEADD(month, 12, startDate) AS expiresAt
1033
- * ```
1034
- */
1035
- dateAdd(unit, source, value) {
1036
- return new ExprUnit(source.dataType, {
1037
- type: "dateAdd",
1038
- unit,
1039
- source: toExpr(source),
1040
- value: toExpr(value)
1041
- });
1042
- },
1043
- /**
1044
- * Date format (DATE_FORMAT)
1045
- *
1046
- * Format string rules may differ between DBMS implementations
1047
- *
1048
- * @param source - Date expression
1049
- * @param format - Format string (e.g., "%Y-%m-%d")
1050
- * @returns Formatted string expression
1051
- *
1052
- * @example
1053
- * ```typescript
1054
- * db.order().select((o) => ({
1055
- * orderDate: expr.formatDate(o.createdAt, "%Y-%m-%d"),
1056
- * }))
1057
- * // SELECT DATE_FORMAT(createdAt, '%Y-%m-%d') AS orderDate (MySQL)
1058
- * // 2024-01-15 10:30:00 → "2024-01-15"
1059
- * ```
1060
- */
1061
- formatDate(source, format) {
1062
- return new ExprUnit("string", {
1063
- type: "formatDate",
1064
- source: toExpr(source),
1065
- format
1066
- });
1067
- },
1068
- //#endregion
1069
- //#region ========== SELECT - Condition ==========
1070
- /**
1071
- * NULL replacement (COALESCE/IFNULL)
1072
- *
1073
- * Returns the first non-null value. If the last argument is non-nullable, the result is also non-nullable
1074
- *
1075
- * @param args - Values to inspect (last is default value)
1076
- * @returns First non-null value
1077
- *
1078
- * @example
1079
- * ```typescript
1080
- * db.user().select((u) => ({
1081
- * displayName: expr.coalesce(u.nickname, u.name, "Guest"),
1082
- * }))
1083
- * // SELECT COALESCE(nickname, name, 'Guest') AS displayName
1084
- * ```
1085
- */
1086
- coalesce,
1087
- /**
1088
- * Return NULL if value matches (NULLIF)
1089
- *
1090
- * Returns NULL if source === value, otherwise returns source
1091
- *
1092
- * @param source - Original value
1093
- * @param value - Value to compare
1094
- * @returns NULL or original value
1095
- *
1096
- * @example
1097
- * ```typescript
1098
- * db.user().select((u) => ({
1099
- * // Convert empty string to NULL
1100
- * bio: expr.nullIf(u.bio, ""),
1101
- * }))
1102
- * // SELECT NULLIF(bio, '') AS bio
1103
- * ```
1104
- */
1105
- nullIf(source, value) {
1106
- return new ExprUnit(source.dataType, {
1107
- type: "nullIf",
1108
- source: toExpr(source),
1109
- value: toExpr(value)
1110
- });
1111
- },
1112
- /**
1113
- * Transform WHERE expression to boolean
1114
- *
1115
- * Used when condition results should be used as a boolean column in SELECT clause
1116
- *
1117
- * @param condition - Condition to transform
1118
- * @returns boolean expression
1119
- *
1120
- * @example
1121
- * ```typescript
1122
- * db.user().select((u) => ({
1123
- * isActive: expr.is(expr.eq(u.status, "active")),
1124
- * }))
1125
- * // SELECT (status <=> 'active') AS isActive
1126
- * ```
1127
- */
1128
- is(condition) {
1129
- return new ExprUnit("boolean", {
1130
- type: "is",
1131
- condition: condition.expr
1132
- });
1133
- },
1134
- /**
1135
- * CASE WHEN expression builder
1136
- *
1137
- * Build conditional branches using method chaining
1138
- *
1139
- * @returns SwitchExprBuilder instance
1140
- *
1141
- * @example
1142
- * ```typescript
1143
- * db.user().select((u) => ({
1144
- * grade: expr.switch<string>()
1145
- * .case(expr.gte(u.score, 90), "A")
1146
- * .case(expr.gte(u.score, 80), "B")
1147
- * .case(expr.gte(u.score, 70), "C")
1148
- * .default("F"),
1149
- * }))
1150
- * // SELECT CASE WHEN score >= 90 THEN 'A' ... ELSE 'F' END AS grade
1151
- * ```
1152
- */
1153
- switch() {
1154
- return createSwitchBuilder();
1155
- },
1156
- /**
1157
- * Simple IF condition (ternary operator)
1158
- *
1159
- * @param condition - Condition
1160
- * @param then - Value when condition is true
1161
- * @param else_ - Value when condition is false
1162
- * @returns Conditional value expression
1163
- *
1164
- * @example
1165
- * ```typescript
1166
- * db.user().select((u) => ({
1167
- * type: expr.if(expr.gte(u.age, 18), "adult", "minor"),
1168
- * }))
1169
- * // SELECT IF(age >= 18, 'adult', 'minor') AS type
1170
- * ```
1171
- */
1172
- if(condition, then, else_) {
1173
- const allValues = [then, else_];
1174
- const exprUnit = allValues.find((v) => v instanceof ExprUnit);
1175
- if (exprUnit) {
1176
- return new ExprUnit(exprUnit.dataType, {
1177
- type: "if",
1178
- condition: condition.expr,
1179
- then: toExpr(then),
1180
- else: toExpr(else_)
1181
- });
1182
- }
1183
- const nonNullLiteral = allValues.find((v) => v != null);
1184
- if (nonNullLiteral == null) {
1185
- throw new Error("At least one of if's then/else must be non-null.");
1186
- }
1187
- return new ExprUnit(inferColumnPrimitiveStr(nonNullLiteral), {
1188
- type: "if",
1189
- condition: condition.expr,
1190
- then: toExpr(then),
1191
- else: toExpr(else_)
1192
- });
1193
- },
1194
- //#endregion
1195
- //#region ========== SELECT - Aggregate ==========
1196
- // Aggregates like SUM, AVG, MAX return NULL only when all values are NULL or no rows exist (rows with NULL values are ignored)
1197
- /**
1198
- * Count rows (COUNT)
1199
- *
1200
- * @param arg - Column to count (all rows if omitted)
1201
- * @param distinct - If true, remove duplicates
1202
- * @returns Row count
1203
- *
1204
- * @example
1205
- * ```typescript
1206
- * // Total row count
1207
- * db.user().select(() => ({ total: expr.count() }))
1208
- *
1209
- * // Distinct count
1210
- * db.order().select((o) => ({
1211
- * uniqueCustomers: expr.count(o.customerId, true),
1212
- * }))
1213
- * ```
1214
- */
1215
- count(arg, distinct) {
1216
- return new ExprUnit("number", {
1217
- type: "count",
1218
- arg: arg != null ? toExpr(arg) : void 0,
1219
- distinct
1220
- });
1221
- },
1222
- /**
1223
- * Sum (SUM)
1224
- *
1225
- * NULL values are ignored. Returns NULL if all values are NULL
1226
- *
1227
- * @param arg - Number column to sum
1228
- * @returns Sum (or NULL)
1229
- *
1230
- * @example
1231
- * ```typescript
1232
- * db.order().groupBy((o) => o.userId).select((o) => ({
1233
- * userId: o.userId,
1234
- * totalAmount: expr.sum(o.amount),
1235
- * }))
1236
- * ```
1237
- */
1238
- sum(arg) {
1239
- return new ExprUnit("number", {
1240
- type: "sum",
1241
- arg: toExpr(arg)
1242
- });
1243
- },
1244
- /**
1245
- * Average (AVG)
1246
- *
1247
- * NULL values are ignored. Returns NULL if all values are NULL
1248
- *
1249
- * @param arg - Number column to average
1250
- * @returns Average (or NULL)
1251
- *
1252
- * @example
1253
- * ```typescript
1254
- * db.product().groupBy((p) => p.categoryId).select((p) => ({
1255
- * categoryId: p.categoryId,
1256
- * avgPrice: expr.avg(p.price),
1257
- * }))
1258
- * ```
1259
- */
1260
- avg(arg) {
1261
- return new ExprUnit("number", {
1262
- type: "avg",
1263
- arg: toExpr(arg)
1264
- });
1265
- },
1266
- /**
1267
- * Maximum value (MAX)
1268
- *
1269
- * NULL values are ignored. Returns NULL if all values are NULL
1270
- *
1271
- * @param arg - Column to find maximum of
1272
- * @returns Maximum value (or NULL)
1273
- *
1274
- * @example
1275
- * ```typescript
1276
- * db.order().groupBy((o) => o.userId).select((o) => ({
1277
- * userId: o.userId,
1278
- * lastOrderDate: expr.max(o.createdAt),
1279
- * }))
1280
- * ```
1281
- */
1282
- max(arg) {
1283
- return new ExprUnit(arg.dataType, {
1284
- type: "max",
1285
- arg: toExpr(arg)
1286
- });
1287
- },
1288
- /**
1289
- * Minimum value (MIN)
1290
- *
1291
- * NULL values are ignored. Returns NULL if all values are NULL
1292
- *
1293
- * @param arg - Column to find minimum of
1294
- * @returns Minimum value (or NULL)
1295
- *
1296
- * @example
1297
- * ```typescript
1298
- * db.product().groupBy((p) => p.categoryId).select((p) => ({
1299
- * categoryId: p.categoryId,
1300
- * minPrice: expr.min(p.price),
1301
- * }))
1302
- * ```
1303
- */
1304
- min(arg) {
1305
- return new ExprUnit(arg.dataType, {
1306
- type: "min",
1307
- arg: toExpr(arg)
1308
- });
1309
- },
1310
- //#endregion
1311
- //#region ========== SELECT - Other ==========
1312
- /**
1313
- * Greatest value among multiple values (GREATEST)
1314
- *
1315
- * @param args - Values to compare
1316
- * @returns Greatest value
1317
- *
1318
- * @example
1319
- * ```typescript
1320
- * db.product().select((p) => ({
1321
- * effectivePrice: expr.greatest(p.price, p.minPrice),
1322
- * }))
1323
- * // SELECT GREATEST(price, minPrice) AS effectivePrice
1324
- * ```
1325
- */
1326
- greatest(...args) {
1327
- return new ExprUnit(findDataType(args), {
1328
- type: "greatest",
1329
- args: args.map((a) => toExpr(a))
1330
- });
1331
- },
1332
- /**
1333
- * Least value among multiple values (LEAST)
1334
- *
1335
- * @param args - Values to compare
1336
- * @returns Least value
1337
- *
1338
- * @example
1339
- * ```typescript
1340
- * db.product().select((p) => ({
1341
- * effectivePrice: expr.least(p.price, p.maxDiscount),
1342
- * }))
1343
- * // SELECT LEAST(price, maxDiscount) AS effectivePrice
1344
- * ```
1345
- */
1346
- least(...args) {
1347
- return new ExprUnit(findDataType(args), {
1348
- type: "least",
1349
- args: args.map((a) => toExpr(a))
1350
- });
1351
- },
1352
- /**
1353
- * Row number (sequential number for all rows without ROW_NUMBER)
1354
- *
1355
- * @returns Row number (starting from 1)
1356
- *
1357
- * @example
1358
- * ```typescript
1359
- * db.user().select((u) => ({
1360
- * rowNum: expr.rowNum(),
1361
- * name: u.name,
1362
- * }))
1363
- * ```
1364
- */
1365
- rowNum() {
1366
- return new ExprUnit("number", {
1367
- type: "rowNum"
1368
- });
1369
- },
1370
- /**
1371
- * Generate random number (RAND/RANDOM)
1372
- *
1373
- * Returns a random number between 0 and 1. Mainly used for random ordering in ORDER BY
1374
- *
1375
- * @returns Random number between 0 and 1
1376
- *
1377
- * @example
1378
- * ```typescript
1379
- * // Random sorting
1380
- * db.user().orderBy(() => expr.random()).limit(10)
1381
- * ```
1382
- */
1383
- random() {
1384
- return new ExprUnit("number", {
1385
- type: "random"
1386
- });
1387
- },
1388
- /**
1389
- * type transformation (CAST)
1390
- *
1391
- * @param source - Expression to transform
1392
- * @param targetType - Target data type
1393
- * @returns Transformed expression
1394
- *
1395
- * @example
1396
- * ```typescript
1397
- * db.order().select((o) => ({
1398
- * idStr: expr.cast(o.id, { type: "varchar", length: 20 }),
1399
- * }))
1400
- * // SELECT CAST(id AS VARCHAR(20)) AS idStr
1401
- * ```
1402
- */
1403
- cast(source, targetType) {
1404
- return new ExprUnit(dataTypeStrToColumnPrimitiveStr[targetType.type], {
1405
- type: "cast",
1406
- source: toExpr(source),
1407
- targetType
1408
- });
1409
- },
1410
- /**
1411
- * Scalar Subquery - Subquery that returns a single value in SELECT clause
1412
- *
1413
- * The subquery must return exactly one row and one column
1414
- *
1415
- * @param dataType - Data type of the returned value
1416
- * @param queryable - Queryable that returns a scalar value
1417
- * @returns Subquery result expression
1418
- *
1419
- * @example
1420
- * ```typescript
1421
- * db.user().select((u) => ({
1422
- * id: u.id,
1423
- * postCount: expr.subquery(
1424
- * "number",
1425
- * db.post()
1426
- * .where((p) => [expr.eq(p.userId, u.id)])
1427
- * .select(() => ({ cnt: expr.count() }))
1428
- * ),
1429
- * }))
1430
- * // SELECT id, (SELECT COUNT(*) FROM Post WHERE userId = User.id) AS postCount
1431
- * ```
1432
- */
1433
- subquery(dataType, queryable) {
1434
- return new ExprUnit(dataType, {
1435
- type: "subquery",
1436
- queryDef: queryable.getSelectQueryDef()
1437
- });
1438
- },
1439
- //#endregion
1440
- //#region ========== SELECT - Window Functions ==========
1441
- /**
1442
- * ROW_NUMBER() - Row number within a partition
1443
- *
1444
- * Assigns sequential numbers starting from 1 within each partition
1445
- *
1446
- * @param spec - Window spec (partitionBy, orderBy)
1447
- * @returns Row number (starting from 1)
1448
- *
1449
- * @example
1450
- * ```typescript
1451
- * db.order().select((o) => ({
1452
- * ...o,
1453
- * rowNum: expr.rowNumber({
1454
- * partitionBy: [o.userId],
1455
- * orderBy: [[o.createdAt, "DESC"]],
1456
- * }),
1457
- * }))
1458
- * // SELECT *, ROW_NUMBER() OVER (PARTITION BY userId ORDER BY createdAt DESC)
1459
- * ```
1460
- */
1461
- rowNumber(spec) {
1462
- return new ExprUnit("number", {
1463
- type: "window",
1464
- fn: { type: "rowNumber" },
1465
- spec: toWinSpec(spec)
1466
- });
1467
- },
1468
- /**
1469
- * RANK() - Rank within a partition (ties get same rank, next rank is skipped)
1470
- *
1471
- * @param spec - Window spec (partitionBy, orderBy)
1472
- * @returns Rank (skips after ties: 1, 1, 3)
1473
- *
1474
- * @example
1475
- * ```typescript
1476
- * db.student().select((s) => ({
1477
- * name: s.name,
1478
- * rank: expr.rank({
1479
- * orderBy: [[s.score, "DESC"]],
1480
- * }),
1481
- * }))
1482
- * ```
1483
- */
1484
- rank(spec) {
1485
- return new ExprUnit("number", {
1486
- type: "window",
1487
- fn: { type: "rank" },
1488
- spec: toWinSpec(spec)
1489
- });
1490
- },
1491
- /**
1492
- * DENSE_RANK() - Dense rank within a partition (ties get same rank, next rank is consecutive)
1493
- *
1494
- * @param spec - Window spec (partitionBy, orderBy)
1495
- * @returns Dense rank (consecutive after ties: 1, 1, 2)
1496
- *
1497
- * @example
1498
- * ```typescript
1499
- * db.student().select((s) => ({
1500
- * name: s.name,
1501
- * denseRank: expr.denseRank({
1502
- * orderBy: [[s.score, "DESC"]],
1503
- * }),
1504
- * }))
1505
- * ```
1506
- */
1507
- denseRank(spec) {
1508
- return new ExprUnit("number", {
1509
- type: "window",
1510
- fn: { type: "denseRank" },
1511
- spec: toWinSpec(spec)
1512
- });
1513
- },
1514
- /**
1515
- * NTILE(n) - Split partition into n groups
1516
- *
1517
- * @param n - Number of groups to split into
1518
- * @param spec - Window spec (partitionBy, orderBy)
1519
- * @returns Group number (1 ~ n)
1520
- *
1521
- * @example
1522
- * ```typescript
1523
- * // Quartile split to find top 25%
1524
- * db.user().select((u) => ({
1525
- * name: u.name,
1526
- * quartile: expr.ntile(4, {
1527
- * orderBy: [[u.score, "DESC"]],
1528
- * }),
1529
- * }))
1530
- * ```
1531
- */
1532
- ntile(n, spec) {
1533
- return new ExprUnit("number", {
1534
- type: "window",
1535
- fn: { type: "ntile", n },
1536
- spec: toWinSpec(spec)
1537
- });
1538
- },
1539
- /**
1540
- * LAG() - Reference value from a previous row
1541
- *
1542
- * @param column - Column to reference
1543
- * @param spec - Window spec (partitionBy, orderBy)
1544
- * @param options - offset (default 1), default (default value when no previous row)
1545
- * @returns Previous row's value (or default value/NULL)
1546
- *
1547
- * @example
1548
- * ```typescript
1549
- * db.stock().select((s) => ({
1550
- * date: s.date,
1551
- * price: s.price,
1552
- * prevPrice: expr.lag(s.price, {
1553
- * partitionBy: [s.symbol],
1554
- * orderBy: [[s.date, "ASC"]],
1555
- * }),
1556
- * }))
1557
- * ```
1558
- */
1559
- lag(column, spec, options) {
1560
- return new ExprUnit(column.dataType, {
1561
- type: "window",
1562
- fn: {
1563
- type: "lag",
1564
- column: toExpr(column),
1565
- offset: options == null ? void 0 : options.offset,
1566
- default: (options == null ? void 0 : options.default) != null ? toExpr(options.default) : void 0
1567
- },
1568
- spec: toWinSpec(spec)
1569
- });
1570
- },
1571
- /**
1572
- * LEAD() - Reference value from a following row
1573
- *
1574
- * @param column - Column to reference
1575
- * @param spec - Window spec (partitionBy, orderBy)
1576
- * @param options - offset (default 1), default (default value when no following row)
1577
- * @returns Following row's value (or default value/NULL)
1578
- *
1579
- * @example
1580
- * ```typescript
1581
- * db.stock().select((s) => ({
1582
- * date: s.date,
1583
- * price: s.price,
1584
- * nextPrice: expr.lead(s.price, {
1585
- * partitionBy: [s.symbol],
1586
- * orderBy: [[s.date, "ASC"]],
1587
- * }),
1588
- * }))
1589
- * ```
1590
- */
1591
- lead(column, spec, options) {
1592
- return new ExprUnit(column.dataType, {
1593
- type: "window",
1594
- fn: {
1595
- type: "lead",
1596
- column: toExpr(column),
1597
- offset: options == null ? void 0 : options.offset,
1598
- default: (options == null ? void 0 : options.default) != null ? toExpr(options.default) : void 0
1599
- },
1600
- spec: toWinSpec(spec)
1601
- });
1602
- },
1603
- /**
1604
- * FIRST_VALUE() - First value in the partition/frame
1605
- *
1606
- * @param column - Column to reference
1607
- * @param spec - Window spec (partitionBy, orderBy)
1608
- * @returns First value
1609
- *
1610
- * @example
1611
- * ```typescript
1612
- * db.order().select((o) => ({
1613
- * ...o,
1614
- * firstOrderAmount: expr.firstValue(o.amount, {
1615
- * partitionBy: [o.userId],
1616
- * orderBy: [[o.createdAt, "ASC"]],
1617
- * }),
1618
- * }))
1619
- * ```
1620
- */
1621
- firstValue(column, spec) {
1622
- return new ExprUnit(column.dataType, {
1623
- type: "window",
1624
- fn: { type: "firstValue", column: toExpr(column) },
1625
- spec: toWinSpec(spec)
1626
- });
1627
- },
1628
- /**
1629
- * LAST_VALUE() - Last value in the partition/frame
1630
- *
1631
- * @param column - Column to reference
1632
- * @param spec - Window spec (partitionBy, orderBy)
1633
- * @returns Last value
1634
- *
1635
- * @example
1636
- * ```typescript
1637
- * db.order().select((o) => ({
1638
- * ...o,
1639
- * lastOrderAmount: expr.lastValue(o.amount, {
1640
- * partitionBy: [o.userId],
1641
- * orderBy: [[o.createdAt, "ASC"]],
1642
- * }),
1643
- * }))
1644
- * ```
1645
- */
1646
- lastValue(column, spec) {
1647
- return new ExprUnit(column.dataType, {
1648
- type: "window",
1649
- fn: { type: "lastValue", column: toExpr(column) },
1650
- spec: toWinSpec(spec)
1651
- });
1652
- },
1653
- /**
1654
- * SUM() OVER - Window sum
1655
- *
1656
- * @param column - Column to sum
1657
- * @param spec - Window spec (partitionBy, orderBy)
1658
- * @returns Sum within window
1659
- *
1660
- * @example
1661
- * ```typescript
1662
- * // Running total
1663
- * db.order().select((o) => ({
1664
- * ...o,
1665
- * runningTotal: expr.sumOver(o.amount, {
1666
- * partitionBy: [o.userId],
1667
- * orderBy: [[o.createdAt, "ASC"]],
1668
- * }),
1669
- * }))
1670
- * ```
1671
- */
1672
- sumOver(column, spec) {
1673
- return new ExprUnit("number", {
1674
- type: "window",
1675
- fn: { type: "sum", column: toExpr(column) },
1676
- spec: toWinSpec(spec)
1677
- });
1678
- },
1679
- /**
1680
- * AVG() OVER - Window average
1681
- *
1682
- * @param column - Column to average
1683
- * @param spec - Window spec (partitionBy, orderBy)
1684
- * @returns Average within window
1685
- *
1686
- * @example
1687
- * ```typescript
1688
- * // Moving average
1689
- * db.stock().select((s) => ({
1690
- * ...s,
1691
- * movingAvg: expr.avgOver(s.price, {
1692
- * partitionBy: [s.symbol],
1693
- * orderBy: [[s.date, "ASC"]],
1694
- * }),
1695
- * }))
1696
- * ```
1697
- */
1698
- avgOver(column, spec) {
1699
- return new ExprUnit("number", {
1700
- type: "window",
1701
- fn: { type: "avg", column: toExpr(column) },
1702
- spec: toWinSpec(spec)
1703
- });
1704
- },
1705
- /**
1706
- * COUNT() OVER - Window count
1707
- *
1708
- * @param spec - Window spec (partitionBy, orderBy)
1709
- * @param column - Column to count (all rows if omitted)
1710
- * @returns Row count within window
1711
- *
1712
- * @example
1713
- * ```typescript
1714
- * db.order().select((o) => ({
1715
- * ...o,
1716
- * totalOrdersPerUser: expr.countOver({
1717
- * partitionBy: [o.userId],
1718
- * }),
1719
- * }))
1720
- * ```
1721
- */
1722
- countOver(spec, column) {
1723
- return new ExprUnit("number", {
1724
- type: "window",
1725
- fn: { type: "count", column: column != null ? toExpr(column) : void 0 },
1726
- spec: toWinSpec(spec)
1727
- });
1728
- },
1729
- /**
1730
- * MIN() OVER - Window minimum
1731
- *
1732
- * @param column - Column to find minimum of
1733
- * @param spec - Window spec (partitionBy, orderBy)
1734
- * @returns Minimum value within window
1735
- *
1736
- * @example
1737
- * ```typescript
1738
- * db.stock().select((s) => ({
1739
- * ...s,
1740
- * minPriceInPeriod: expr.minOver(s.price, {
1741
- * partitionBy: [s.symbol],
1742
- * }),
1743
- * }))
1744
- * ```
1745
- */
1746
- minOver(column, spec) {
1747
- return new ExprUnit(column.dataType, {
1748
- type: "window",
1749
- fn: { type: "min", column: toExpr(column) },
1750
- spec: toWinSpec(spec)
1751
- });
1752
- },
1753
- /**
1754
- * MAX() OVER - Window maximum
1755
- *
1756
- * @param column - Column to find maximum of
1757
- * @param spec - Window spec (partitionBy, orderBy)
1758
- * @returns Maximum value within window
1759
- *
1760
- * @example
1761
- * ```typescript
1762
- * db.stock().select((s) => ({
1763
- * ...s,
1764
- * maxPriceInPeriod: expr.maxOver(s.price, {
1765
- * partitionBy: [s.symbol],
1766
- * }),
1767
- * }))
1768
- * ```
1769
- */
1770
- maxOver(column, spec) {
1771
- return new ExprUnit(column.dataType, {
1772
- type: "window",
1773
- fn: { type: "max", column: toExpr(column) },
1774
- spec: toWinSpec(spec)
1775
- });
1776
- },
1777
- //#endregion
1778
- //#region ========== Helper ==========
1779
- /**
1780
- * Transform ExprInput to Expr (internal use)
1781
- *
1782
- * @param value - Value to transform
1783
- * @returns Expr JSON AST
1784
- */
1785
- toExpr(value) {
1786
- return toExpr(value);
1787
- }
1788
- //#endregion
368
+ return new WhereExprUnit({
369
+ type: "inQuery",
370
+ source: toExpr(source),
371
+ query: queryDef,
372
+ });
373
+ },
374
+ /**
375
+ * EXISTS (SELECT ...) - Check if subquery returns any rows
376
+ *
377
+ * Returns true if the subquery returns one or more rows
378
+ *
379
+ * @param query - Queryable to check for existence
380
+ * @returns WHERE condition expression
381
+ *
382
+ * @example
383
+ * ```typescript
384
+ * // Query users who have orders
385
+ * db.user().where((u) => [
386
+ * expr.exists(
387
+ * db.order().where((o) => [expr.eq(o.userId, u.id)])
388
+ * ),
389
+ * ])
390
+ * // WHERE EXISTS (SELECT 1 FROM Order WHERE userId = User.id)
391
+ * ```
392
+ */
393
+ exists(query) {
394
+ const { select: _, ...queryDefWithoutSelect } = query.getSelectQueryDef(); // EXISTS does not need SELECT clause, saves packet size
395
+ return new WhereExprUnit({
396
+ type: "exists",
397
+ query: queryDefWithoutSelect,
398
+ });
399
+ },
400
+ //#endregion
401
+ //#region ========== WHERE - Logical operators ==========
402
+ /**
403
+ * NOT operator - Negate a condition
404
+ *
405
+ * @param arg - Condition to negate
406
+ * @returns Negated WHERE condition expression
407
+ *
408
+ * @example
409
+ * ```typescript
410
+ * db.user().where((u) => [expr.not(expr.eq(u.status, "deleted"))])
411
+ * // WHERE NOT (status <=> 'deleted')
412
+ * ```
413
+ */
414
+ not(arg) {
415
+ return new WhereExprUnit({
416
+ type: "not",
417
+ arg: arg.expr,
418
+ });
419
+ },
420
+ /**
421
+ * AND operator - All conditions must be satisfied
422
+ *
423
+ * Combines multiple conditions with AND. Passing an array to where() automatically applies AND
424
+ *
425
+ * @param conditions - List of conditions to combine with AND
426
+ * @returns Combined WHERE condition expression
427
+ *
428
+ * @example
429
+ * ```typescript
430
+ * db.user().where((u) => [
431
+ * expr.and([
432
+ * expr.eq(u.status, "active"),
433
+ * expr.gte(u.age, 18),
434
+ * ]),
435
+ * ])
436
+ * // WHERE (status <=> 'active' AND age >= 18)
437
+ * ```
438
+ */
439
+ and(conditions) {
440
+ if (conditions.length === 0) {
441
+ throw new ArgumentError({ conditions: "empty arrays are not allowed" });
442
+ }
443
+ return new WhereExprUnit({
444
+ type: "and",
445
+ conditions: conditions.map((c) => c.expr),
446
+ });
447
+ },
448
+ /**
449
+ * OR operator - At least one condition must be satisfied
450
+ *
451
+ * @param conditions - List of conditions to combine with OR
452
+ * @returns Combined WHERE condition expression
453
+ *
454
+ * @example
455
+ * ```typescript
456
+ * db.user().where((u) => [
457
+ * expr.or([
458
+ * expr.eq(u.status, "active"),
459
+ * expr.eq(u.status, "pending"),
460
+ * ]),
461
+ * ])
462
+ * // WHERE (status <=> 'active' OR status <=> 'pending')
463
+ * ```
464
+ */
465
+ or(conditions) {
466
+ if (conditions.length === 0) {
467
+ throw new ArgumentError({ conditions: "empty arrays are not allowed" });
468
+ }
469
+ return new WhereExprUnit({
470
+ type: "or",
471
+ conditions: conditions.map((c) => c.expr),
472
+ });
473
+ },
474
+ //#endregion
475
+ //#region ========== SELECT - String ==========
476
+ /**
477
+ * String concatenation (CONCAT)
478
+ *
479
+ * NULL values are treated as empty strings (auto-transformed per DBMS)
480
+ *
481
+ * @param args - Strings to concatenate
482
+ * @returns Concatenated string expression
483
+ *
484
+ * @example
485
+ * ```typescript
486
+ * db.user().select((u) => ({
487
+ * fullName: expr.concat(u.firstName, " ", u.lastName),
488
+ * }))
489
+ * // SELECT CONCAT(firstName, ' ', lastName) AS fullName
490
+ * ```
491
+ */
492
+ concat(...args) {
493
+ return new ExprUnit("string", {
494
+ type: "concat",
495
+ args: args.map((arg) => toExpr(arg)),
496
+ });
497
+ },
498
+ /**
499
+ * Extract specified length from the left of a string (LEFT)
500
+ *
501
+ * @param source - Original string
502
+ * @param length - Number of characters to extract
503
+ * @returns Extracted string expression
504
+ *
505
+ * @example
506
+ * ```typescript
507
+ * db.user().select((u) => ({
508
+ * initial: expr.left(u.name, 1),
509
+ * }))
510
+ * // SELECT LEFT(name, 1) AS initial
511
+ * ```
512
+ */
513
+ left(source, length) {
514
+ return new ExprUnit("string", {
515
+ type: "left",
516
+ source: toExpr(source),
517
+ length: toExpr(length),
518
+ });
519
+ },
520
+ /**
521
+ * Extract specified length from the right of a string (RIGHT)
522
+ *
523
+ * @param source - Original string
524
+ * @param length - Number of characters to extract
525
+ * @returns Extracted string expression
526
+ *
527
+ * @example
528
+ * ```typescript
529
+ * db.phone().select((p) => ({
530
+ * lastFour: expr.right(p.number, 4),
531
+ * }))
532
+ * // SELECT RIGHT(number, 4) AS lastFour
533
+ * ```
534
+ */
535
+ right(source, length) {
536
+ return new ExprUnit("string", {
537
+ type: "right",
538
+ source: toExpr(source),
539
+ length: toExpr(length),
540
+ });
541
+ },
542
+ /**
543
+ * Remove whitespace from both sides of a string (TRIM)
544
+ *
545
+ * @param source - Original string
546
+ * @returns String expression with whitespace removed
547
+ *
548
+ * @example
549
+ * ```typescript
550
+ * db.user().select((u) => ({
551
+ * name: expr.trim(u.name),
552
+ * }))
553
+ * // SELECT TRIM(name) AS name
554
+ * ```
555
+ */
556
+ trim(source) {
557
+ return new ExprUnit("string", {
558
+ type: "trim",
559
+ arg: toExpr(source),
560
+ });
561
+ },
562
+ /**
563
+ * Left padding (LPAD)
564
+ *
565
+ * Repeatedly adds fillString on the left until the target length is reached
566
+ *
567
+ * @param source - Original string
568
+ * @param length - Target length
569
+ * @param fillString - String to use for padding
570
+ * @returns Padded string expression
571
+ *
572
+ * @example
573
+ * ```typescript
574
+ * db.order().select((o) => ({
575
+ * orderNo: expr.padStart(expr.cast(o.id, { type: "varchar", length: 10 }), 8, "0"),
576
+ * }))
577
+ * // SELECT LPAD(CAST(id AS VARCHAR(10)), 8, '0') AS orderNo
578
+ * // Result: "00000123"
579
+ * ```
580
+ */
581
+ padStart(source, length, fillString) {
582
+ return new ExprUnit("string", {
583
+ type: "padStart",
584
+ source: toExpr(source),
585
+ length: toExpr(length),
586
+ fillString: toExpr(fillString),
587
+ });
588
+ },
589
+ /**
590
+ * String replacement (REPLACE)
591
+ *
592
+ * @param source - Original string
593
+ * @param from - String to find
594
+ * @param to - Replacement string
595
+ * @returns Replaced string expression
596
+ *
597
+ * @example
598
+ * ```typescript
599
+ * db.user().select((u) => ({
600
+ * phone: expr.replace(u.phone, "-", ""),
601
+ * }))
602
+ * // SELECT REPLACE(phone, '-', '') AS phone
603
+ * ```
604
+ */
605
+ replace(source, from, to) {
606
+ return new ExprUnit("string", {
607
+ type: "replace",
608
+ source: toExpr(source),
609
+ from: toExpr(from),
610
+ to: toExpr(to),
611
+ });
612
+ },
613
+ /**
614
+ * Convert string to uppercase (UPPER)
615
+ *
616
+ * @param source - Original string
617
+ * @returns Uppercase string expression
618
+ *
619
+ * @example
620
+ * ```typescript
621
+ * db.user().select((u) => ({
622
+ * code: expr.upper(u.code),
623
+ * }))
624
+ * // SELECT UPPER(code) AS code
625
+ * ```
626
+ */
627
+ upper(source) {
628
+ return new ExprUnit("string", {
629
+ type: "upper",
630
+ arg: toExpr(source),
631
+ });
632
+ },
633
+ /**
634
+ * Convert string to lowercase (LOWER)
635
+ *
636
+ * @param source - Original string
637
+ * @returns Lowercase string expression
638
+ *
639
+ * @example
640
+ * ```typescript
641
+ * db.user().select((u) => ({
642
+ * email: expr.lower(u.email),
643
+ * }))
644
+ * // SELECT LOWER(email) AS email
645
+ * ```
646
+ */
647
+ lower(source) {
648
+ return new ExprUnit("string", {
649
+ type: "lower",
650
+ arg: toExpr(source),
651
+ });
652
+ },
653
+ /**
654
+ * String length (character count)
655
+ *
656
+ * @param source - Original string
657
+ * @returns Character count
658
+ *
659
+ * @example
660
+ * ```typescript
661
+ * db.user().select((u) => ({
662
+ * nameLength: expr.length(u.name),
663
+ * }))
664
+ * // SELECT CHAR_LENGTH(name) AS nameLength
665
+ * ```
666
+ */
667
+ length(source) {
668
+ return new ExprUnit("number", {
669
+ type: "length",
670
+ arg: toExpr(source),
671
+ });
672
+ },
673
+ /**
674
+ * String byte length
675
+ *
676
+ * In UTF-8, CJK characters are 3 bytes each
677
+ *
678
+ * @param source - Original string
679
+ * @returns Byte count
680
+ *
681
+ * @example
682
+ * ```typescript
683
+ * db.user().select((u) => ({
684
+ * byteLen: expr.byteLength(u.name),
685
+ * }))
686
+ * // SELECT OCTET_LENGTH(name) AS byteLen
687
+ * ```
688
+ */
689
+ byteLength(source) {
690
+ return new ExprUnit("number", {
691
+ type: "byteLength",
692
+ arg: toExpr(source),
693
+ });
694
+ },
695
+ /**
696
+ * Extract part of a string (SUBSTRING)
697
+ *
698
+ * Uses 1-based index per SQL standard
699
+ *
700
+ * @param source - Original string
701
+ * @param start - Start position (starting from 1)
702
+ * @param length - Length to extract (to the end if omitted)
703
+ * @returns Extracted string expression
704
+ *
705
+ * @example
706
+ * ```typescript
707
+ * db.user().select((u) => ({
708
+ * // From "Hello World", 5 characters starting at index 1: "Hello"
709
+ * prefix: expr.substring(u.name, 1, 5),
710
+ * }))
711
+ * // SELECT SUBSTRING(name, 1, 5) AS prefix
712
+ * ```
713
+ */
714
+ substring(source, start, length) {
715
+ return new ExprUnit("string", {
716
+ type: "substring",
717
+ source: toExpr(source),
718
+ start: toExpr(start),
719
+ ...(length != null ? { length: toExpr(length) } : {}),
720
+ });
721
+ },
722
+ /**
723
+ * Find position within a string (LOCATE/CHARINDEX)
724
+ *
725
+ * Returns 1-based index, or 0 if not found
726
+ *
727
+ * @param source - String to search in
728
+ * @param search - String to find
729
+ * @returns Position (starting from 1, 0 if not found)
730
+ *
731
+ * @example
732
+ * ```typescript
733
+ * db.user().select((u) => ({
734
+ * atPos: expr.indexOf(u.email, "@"),
735
+ * }))
736
+ * // SELECT LOCATE('@', email) AS atPos (MySQL)
737
+ * // "john@example.com" → 5
738
+ * ```
739
+ */
740
+ indexOf(source, search) {
741
+ return new ExprUnit("number", {
742
+ type: "indexOf",
743
+ source: toExpr(source),
744
+ search: toExpr(search),
745
+ });
746
+ },
747
+ //#endregion
748
+ //#region ========== SELECT - Number ==========
749
+ /**
750
+ * Absolute value (ABS)
751
+ *
752
+ * @param source - Original number
753
+ * @returns Absolute value expression
754
+ *
755
+ * @example
756
+ * ```typescript
757
+ * db.account().select((a) => ({
758
+ * balance: expr.abs(a.balance),
759
+ * }))
760
+ * // SELECT ABS(balance) AS balance
761
+ * ```
762
+ */
763
+ abs(source) {
764
+ return new ExprUnit("number", {
765
+ type: "abs",
766
+ arg: toExpr(source),
767
+ });
768
+ },
769
+ /**
770
+ * Round (ROUND)
771
+ *
772
+ * @param source - Original number
773
+ * @param digits - Number of decimal places
774
+ * @returns Rounded number expression
775
+ *
776
+ * @example
777
+ * ```typescript
778
+ * db.product().select((p) => ({
779
+ * price: expr.round(p.price, 2),
780
+ * }))
781
+ * // SELECT ROUND(price, 2) AS price
782
+ * // 123.456 → 123.46
783
+ * ```
784
+ */
785
+ round(source, digits) {
786
+ return new ExprUnit("number", {
787
+ type: "round",
788
+ arg: toExpr(source),
789
+ digits,
790
+ });
791
+ },
792
+ /**
793
+ * Ceiling (CEILING)
794
+ *
795
+ * @param source - Original number
796
+ * @returns Ceiling number expression
797
+ *
798
+ * @example
799
+ * ```typescript
800
+ * db.order().select((o) => ({
801
+ * pages: expr.ceil(expr.divide(o.itemCount, 10)),
802
+ * }))
803
+ * // SELECT CEILING(itemCount / 10) AS pages
804
+ * // 25 / 10 = 2.5 → 3
805
+ * ```
806
+ */
807
+ ceil(source) {
808
+ return new ExprUnit("number", {
809
+ type: "ceil",
810
+ arg: toExpr(source),
811
+ });
812
+ },
813
+ /**
814
+ * Floor (FLOOR)
815
+ *
816
+ * @param source - Original number
817
+ * @returns Floor number expression
818
+ *
819
+ * @example
820
+ * ```typescript
821
+ * db.user().select((u) => ({
822
+ * ageGroup: expr.floor(expr.divide(u.age, 10)),
823
+ * }))
824
+ * // SELECT FLOOR(age / 10) AS ageGroup
825
+ * // 25 / 10 = 2.5 2
826
+ * ```
827
+ */
828
+ floor(source) {
829
+ return new ExprUnit("number", {
830
+ type: "floor",
831
+ arg: toExpr(source),
832
+ });
833
+ },
834
+ //#endregion
835
+ //#region ========== SELECT - Date ==========
836
+ /**
837
+ * Extract year (YEAR)
838
+ *
839
+ * @param source - DateTime or DateOnly expression
840
+ * @returns Year (4-digit number)
841
+ *
842
+ * @example
843
+ * ```typescript
844
+ * db.user().select((u) => ({
845
+ * birthYear: expr.year(u.birthDate),
846
+ * }))
847
+ * // SELECT YEAR(birthDate) AS birthYear
848
+ * ```
849
+ */
850
+ year(source) {
851
+ return new ExprUnit("number", {
852
+ type: "year",
853
+ arg: toExpr(source),
854
+ });
855
+ },
856
+ /**
857
+ * Extract month (MONTH)
858
+ *
859
+ * @param source - DateTime or DateOnly expression
860
+ * @returns Month (1~12)
861
+ *
862
+ * @example
863
+ * ```typescript
864
+ * db.order().select((o) => ({
865
+ * orderMonth: expr.month(o.createdAt),
866
+ * }))
867
+ * // SELECT MONTH(createdAt) AS orderMonth
868
+ * ```
869
+ */
870
+ month(source) {
871
+ return new ExprUnit("number", {
872
+ type: "month",
873
+ arg: toExpr(source),
874
+ });
875
+ },
876
+ /**
877
+ * Extract day (DAY)
878
+ *
879
+ * @param source - DateTime or DateOnly expression
880
+ * @returns Day (1~31)
881
+ *
882
+ * @example
883
+ * ```typescript
884
+ * db.user().select((u) => ({
885
+ * birthDay: expr.day(u.birthDate),
886
+ * }))
887
+ * // SELECT DAY(birthDate) AS birthDay
888
+ * ```
889
+ */
890
+ day(source) {
891
+ return new ExprUnit("number", {
892
+ type: "day",
893
+ arg: toExpr(source),
894
+ });
895
+ },
896
+ /**
897
+ * Extract hour (HOUR)
898
+ *
899
+ * @param source - DateTime or Time expression
900
+ * @returns Hour (0~23)
901
+ *
902
+ * @example
903
+ * ```typescript
904
+ * db.log().select((l) => ({
905
+ * logHour: expr.hour(l.createdAt),
906
+ * }))
907
+ * // SELECT HOUR(createdAt) AS logHour
908
+ * ```
909
+ */
910
+ hour(source) {
911
+ return new ExprUnit("number", {
912
+ type: "hour",
913
+ arg: toExpr(source),
914
+ });
915
+ },
916
+ /**
917
+ * Extract minute (MINUTE)
918
+ *
919
+ * @param source - DateTime or Time expression
920
+ * @returns Minute (0~59)
921
+ *
922
+ * @example
923
+ * ```typescript
924
+ * db.log().select((l) => ({
925
+ * logMinute: expr.minute(l.createdAt),
926
+ * }))
927
+ * // SELECT MINUTE(createdAt) AS logMinute
928
+ * ```
929
+ */
930
+ minute(source) {
931
+ return new ExprUnit("number", {
932
+ type: "minute",
933
+ arg: toExpr(source),
934
+ });
935
+ },
936
+ /**
937
+ * Extract second (SECOND)
938
+ *
939
+ * @param source - DateTime or Time expression
940
+ * @returns Second (0~59)
941
+ *
942
+ * @example
943
+ * ```typescript
944
+ * db.log().select((l) => ({
945
+ * logSecond: expr.second(l.createdAt),
946
+ * }))
947
+ * // SELECT SECOND(createdAt) AS logSecond
948
+ * ```
949
+ */
950
+ second(source) {
951
+ return new ExprUnit("number", {
952
+ type: "second",
953
+ arg: toExpr(source),
954
+ });
955
+ },
956
+ /**
957
+ * Extract ISO week number
958
+ *
959
+ * ISO 8601 week number (starts Monday, 1~53)
960
+ *
961
+ * @param source - DateOnly expression
962
+ * @returns ISO week number
963
+ *
964
+ * @example
965
+ * ```typescript
966
+ * db.order().select((o) => ({
967
+ * weekNum: expr.isoWeek(o.orderDate),
968
+ * }))
969
+ * // SELECT WEEK(orderDate, 3) AS weekNum (MySQL)
970
+ * ```
971
+ */
972
+ isoWeek(source) {
973
+ return new ExprUnit("number", {
974
+ type: "isoWeek",
975
+ arg: toExpr(source),
976
+ });
977
+ },
978
+ /**
979
+ * ISO week start date (Monday)
980
+ *
981
+ * Returns the Monday of the week the given date belongs to
982
+ *
983
+ * @param source - DateOnly expression
984
+ * @returns Week start date (Monday)
985
+ *
986
+ * @example
987
+ * ```typescript
988
+ * db.order().select((o) => ({
989
+ * weekStart: expr.isoWeekStartDate(o.orderDate),
990
+ * }))
991
+ * // 2024-01-10 (Wed) → 2024-01-08 (Mon)
992
+ * ```
993
+ */
994
+ isoWeekStartDate(source) {
995
+ return new ExprUnit("DateOnly", {
996
+ type: "isoWeekStartDate",
997
+ arg: toExpr(source),
998
+ });
999
+ },
1000
+ /**
1001
+ * ISO year-month (first day of the month)
1002
+ *
1003
+ * Returns the first day of the month for the given date
1004
+ *
1005
+ * @param source - DateOnly expression
1006
+ * @returns First day of the month
1007
+ *
1008
+ * @example
1009
+ * ```typescript
1010
+ * db.order().select((o) => ({
1011
+ * yearMonth: expr.isoYearMonth(o.orderDate),
1012
+ * }))
1013
+ * // 2024-01-15 2024-01-01
1014
+ * ```
1015
+ */
1016
+ isoYearMonth(source) {
1017
+ return new ExprUnit("DateOnly", {
1018
+ type: "isoYearMonth",
1019
+ arg: toExpr(source),
1020
+ });
1021
+ },
1022
+ /**
1023
+ * Calculate date difference (DATEDIFF)
1024
+ *
1025
+ * @param unit - Unit ("year", "month", "day", "hour", "minute", "second")
1026
+ * @param from - Start date
1027
+ * @param to - End date
1028
+ * @returns Difference value (to - from)
1029
+ *
1030
+ * @example
1031
+ * ```typescript
1032
+ * db.user().select((u) => ({
1033
+ * age: expr.dateDiff("year", u.birthDate, expr.val("DateOnly", DateOnly.today())),
1034
+ * }))
1035
+ * // SELECT DATEDIFF(year, birthDate, '2024-01-15') AS age
1036
+ * ```
1037
+ */
1038
+ dateDiff(unit, from, to) {
1039
+ return new ExprUnit("number", {
1040
+ type: "dateDiff",
1041
+ unit,
1042
+ from: toExpr(from),
1043
+ to: toExpr(to),
1044
+ });
1045
+ },
1046
+ /**
1047
+ * Add to date (DATEADD)
1048
+ *
1049
+ * @param unit - Unit ("year", "month", "day", "hour", "minute", "second")
1050
+ * @param source - Original date
1051
+ * @param value - Value to add (negative allowed)
1052
+ * @returns Calculated date
1053
+ *
1054
+ * @example
1055
+ * ```typescript
1056
+ * db.subscription().select((s) => ({
1057
+ * expiresAt: expr.dateAdd("month", s.startDate, 12),
1058
+ * }))
1059
+ * // SELECT DATEADD(month, 12, startDate) AS expiresAt
1060
+ * ```
1061
+ */
1062
+ dateAdd(unit, source, value) {
1063
+ return new ExprUnit(source.dataType, {
1064
+ type: "dateAdd",
1065
+ unit,
1066
+ source: toExpr(source),
1067
+ value: toExpr(value),
1068
+ });
1069
+ },
1070
+ /**
1071
+ * Date format (DATE_FORMAT)
1072
+ *
1073
+ * Format string rules may differ between DBMS implementations
1074
+ *
1075
+ * @param source - Date expression
1076
+ * @param format - Format string (e.g., "%Y-%m-%d")
1077
+ * @returns Formatted string expression
1078
+ *
1079
+ * @example
1080
+ * ```typescript
1081
+ * db.order().select((o) => ({
1082
+ * orderDate: expr.formatDate(o.createdAt, "%Y-%m-%d"),
1083
+ * }))
1084
+ * // SELECT DATE_FORMAT(createdAt, '%Y-%m-%d') AS orderDate (MySQL)
1085
+ * // 2024-01-15 10:30:00"2024-01-15"
1086
+ * ```
1087
+ */
1088
+ formatDate(source, format) {
1089
+ return new ExprUnit("string", {
1090
+ type: "formatDate",
1091
+ source: toExpr(source),
1092
+ format,
1093
+ });
1094
+ },
1095
+ //#endregion
1096
+ //#region ========== SELECT - Condition ==========
1097
+ /**
1098
+ * NULL replacement (COALESCE/IFNULL)
1099
+ *
1100
+ * Returns the first non-null value. If the last argument is non-nullable, the result is also non-nullable
1101
+ *
1102
+ * @param args - Values to inspect (last is default value)
1103
+ * @returns First non-null value
1104
+ *
1105
+ * @example
1106
+ * ```typescript
1107
+ * db.user().select((u) => ({
1108
+ * displayName: expr.coalesce(u.nickname, u.name, "Guest"),
1109
+ * }))
1110
+ * // SELECT COALESCE(nickname, name, 'Guest') AS displayName
1111
+ * ```
1112
+ */
1113
+ coalesce,
1114
+ /**
1115
+ * Return NULL if value matches (NULLIF)
1116
+ *
1117
+ * Returns NULL if source === value, otherwise returns source
1118
+ *
1119
+ * @param source - Original value
1120
+ * @param value - Value to compare
1121
+ * @returns NULL or original value
1122
+ *
1123
+ * @example
1124
+ * ```typescript
1125
+ * db.user().select((u) => ({
1126
+ * // Convert empty string to NULL
1127
+ * bio: expr.nullIf(u.bio, ""),
1128
+ * }))
1129
+ * // SELECT NULLIF(bio, '') AS bio
1130
+ * ```
1131
+ */
1132
+ nullIf(source, value) {
1133
+ return new ExprUnit(source.dataType, {
1134
+ type: "nullIf",
1135
+ source: toExpr(source),
1136
+ value: toExpr(value),
1137
+ });
1138
+ },
1139
+ /**
1140
+ * Transform WHERE expression to boolean
1141
+ *
1142
+ * Used when condition results should be used as a boolean column in SELECT clause
1143
+ *
1144
+ * @param condition - Condition to transform
1145
+ * @returns boolean expression
1146
+ *
1147
+ * @example
1148
+ * ```typescript
1149
+ * db.user().select((u) => ({
1150
+ * isActive: expr.is(expr.eq(u.status, "active")),
1151
+ * }))
1152
+ * // SELECT (status <=> 'active') AS isActive
1153
+ * ```
1154
+ */
1155
+ is(condition) {
1156
+ return new ExprUnit("boolean", {
1157
+ type: "is",
1158
+ condition: condition.expr,
1159
+ });
1160
+ },
1161
+ /**
1162
+ * CASE WHEN expression builder
1163
+ *
1164
+ * Build conditional branches using method chaining
1165
+ *
1166
+ * @returns SwitchExprBuilder instance
1167
+ *
1168
+ * @example
1169
+ * ```typescript
1170
+ * db.user().select((u) => ({
1171
+ * grade: expr.switch<string>()
1172
+ * .case(expr.gte(u.score, 90), "A")
1173
+ * .case(expr.gte(u.score, 80), "B")
1174
+ * .case(expr.gte(u.score, 70), "C")
1175
+ * .default("F"),
1176
+ * }))
1177
+ * // SELECT CASE WHEN score >= 90 THEN 'A' ... ELSE 'F' END AS grade
1178
+ * ```
1179
+ */
1180
+ switch() {
1181
+ return createSwitchBuilder();
1182
+ },
1183
+ /**
1184
+ * Simple IF condition (ternary operator)
1185
+ *
1186
+ * @param condition - Condition
1187
+ * @param then - Value when condition is true
1188
+ * @param else_ - Value when condition is false
1189
+ * @returns Conditional value expression
1190
+ *
1191
+ * @example
1192
+ * ```typescript
1193
+ * db.user().select((u) => ({
1194
+ * type: expr.if(expr.gte(u.age, 18), "adult", "minor"),
1195
+ * }))
1196
+ * // SELECT IF(age >= 18, 'adult', 'minor') AS type
1197
+ * ```
1198
+ */
1199
+ if(condition, then, else_) {
1200
+ const allValues = [then, else_];
1201
+ // 1. Find dataType from ExprUnit
1202
+ const exprUnit = allValues.find((v) => v instanceof ExprUnit);
1203
+ if (exprUnit) {
1204
+ return new ExprUnit(exprUnit.dataType, {
1205
+ type: "if",
1206
+ condition: condition.expr,
1207
+ then: toExpr(then),
1208
+ else: toExpr(else_),
1209
+ });
1210
+ }
1211
+ // 2. Infer from non-null literal
1212
+ const nonNullLiteral = allValues.find((v) => v != null);
1213
+ if (nonNullLiteral == null) {
1214
+ throw new Error("At least one of if's then/else must be non-null.");
1215
+ }
1216
+ return new ExprUnit(inferColumnPrimitiveStr(nonNullLiteral), {
1217
+ type: "if",
1218
+ condition: condition.expr,
1219
+ then: toExpr(then),
1220
+ else: toExpr(else_),
1221
+ });
1222
+ },
1223
+ //#endregion
1224
+ //#region ========== SELECT - Aggregate ==========
1225
+ // SUM, AVG, MAX 등 집계는 모든 값이 NULL이거나 행이 없을 때만 NULL 반환 (NULL 값을 가진 행은 무시됨)
1226
+ /**
1227
+ * Count rows (COUNT)
1228
+ *
1229
+ * @param arg - Column to count (all rows if omitted)
1230
+ * @param distinct - If true, remove duplicates
1231
+ * @returns Row count
1232
+ *
1233
+ * @example
1234
+ * ```typescript
1235
+ * // Total row count
1236
+ * db.user().select(() => ({ total: expr.count() }))
1237
+ *
1238
+ * // Distinct count
1239
+ * db.order().select((o) => ({
1240
+ * uniqueCustomers: expr.count(o.customerId, true),
1241
+ * }))
1242
+ * ```
1243
+ */
1244
+ count(arg, distinct) {
1245
+ return new ExprUnit("number", {
1246
+ type: "count",
1247
+ arg: arg != null ? toExpr(arg) : undefined,
1248
+ distinct,
1249
+ });
1250
+ },
1251
+ /**
1252
+ * Sum (SUM)
1253
+ *
1254
+ * NULL values are ignored. Returns NULL if all values are NULL
1255
+ *
1256
+ * @param arg - Number column to sum
1257
+ * @returns Sum (or NULL)
1258
+ *
1259
+ * @example
1260
+ * ```typescript
1261
+ * db.order().groupBy((o) => o.userId).select((o) => ({
1262
+ * userId: o.userId,
1263
+ * totalAmount: expr.sum(o.amount),
1264
+ * }))
1265
+ * ```
1266
+ */
1267
+ sum(arg) {
1268
+ return new ExprUnit("number", {
1269
+ type: "sum",
1270
+ arg: toExpr(arg),
1271
+ });
1272
+ },
1273
+ /**
1274
+ * Average (AVG)
1275
+ *
1276
+ * NULL values are ignored. Returns NULL if all values are NULL
1277
+ *
1278
+ * @param arg - Number column to average
1279
+ * @returns Average (or NULL)
1280
+ *
1281
+ * @example
1282
+ * ```typescript
1283
+ * db.product().groupBy((p) => p.categoryId).select((p) => ({
1284
+ * categoryId: p.categoryId,
1285
+ * avgPrice: expr.avg(p.price),
1286
+ * }))
1287
+ * ```
1288
+ */
1289
+ avg(arg) {
1290
+ return new ExprUnit("number", {
1291
+ type: "avg",
1292
+ arg: toExpr(arg),
1293
+ });
1294
+ },
1295
+ /**
1296
+ * Maximum value (MAX)
1297
+ *
1298
+ * NULL values are ignored. Returns NULL if all values are NULL
1299
+ *
1300
+ * @param arg - Column to find maximum of
1301
+ * @returns Maximum value (or NULL)
1302
+ *
1303
+ * @example
1304
+ * ```typescript
1305
+ * db.order().groupBy((o) => o.userId).select((o) => ({
1306
+ * userId: o.userId,
1307
+ * lastOrderDate: expr.max(o.createdAt),
1308
+ * }))
1309
+ * ```
1310
+ */
1311
+ max(arg) {
1312
+ return new ExprUnit(arg.dataType, {
1313
+ type: "max",
1314
+ arg: toExpr(arg),
1315
+ });
1316
+ },
1317
+ /**
1318
+ * Minimum value (MIN)
1319
+ *
1320
+ * NULL values are ignored. Returns NULL if all values are NULL
1321
+ *
1322
+ * @param arg - Column to find minimum of
1323
+ * @returns Minimum value (or NULL)
1324
+ *
1325
+ * @example
1326
+ * ```typescript
1327
+ * db.product().groupBy((p) => p.categoryId).select((p) => ({
1328
+ * categoryId: p.categoryId,
1329
+ * minPrice: expr.min(p.price),
1330
+ * }))
1331
+ * ```
1332
+ */
1333
+ min(arg) {
1334
+ return new ExprUnit(arg.dataType, {
1335
+ type: "min",
1336
+ arg: toExpr(arg),
1337
+ });
1338
+ },
1339
+ //#endregion
1340
+ //#region ========== SELECT - Other ==========
1341
+ /**
1342
+ * Greatest value among multiple values (GREATEST)
1343
+ *
1344
+ * @param args - Values to compare
1345
+ * @returns Greatest value
1346
+ *
1347
+ * @example
1348
+ * ```typescript
1349
+ * db.product().select((p) => ({
1350
+ * effectivePrice: expr.greatest(p.price, p.minPrice),
1351
+ * }))
1352
+ * // SELECT GREATEST(price, minPrice) AS effectivePrice
1353
+ * ```
1354
+ */
1355
+ greatest(...args) {
1356
+ return new ExprUnit(findDataType(args), {
1357
+ type: "greatest",
1358
+ args: args.map((a) => toExpr(a)),
1359
+ });
1360
+ },
1361
+ /**
1362
+ * Least value among multiple values (LEAST)
1363
+ *
1364
+ * @param args - Values to compare
1365
+ * @returns Least value
1366
+ *
1367
+ * @example
1368
+ * ```typescript
1369
+ * db.product().select((p) => ({
1370
+ * effectivePrice: expr.least(p.price, p.maxDiscount),
1371
+ * }))
1372
+ * // SELECT LEAST(price, maxDiscount) AS effectivePrice
1373
+ * ```
1374
+ */
1375
+ least(...args) {
1376
+ return new ExprUnit(findDataType(args), {
1377
+ type: "least",
1378
+ args: args.map((a) => toExpr(a)),
1379
+ });
1380
+ },
1381
+ /**
1382
+ * Row number (sequential number for all rows without ROW_NUMBER)
1383
+ *
1384
+ * @returns Row number (starting from 1)
1385
+ *
1386
+ * @example
1387
+ * ```typescript
1388
+ * db.user().select((u) => ({
1389
+ * rowNum: expr.rowNum(),
1390
+ * name: u.name,
1391
+ * }))
1392
+ * ```
1393
+ */
1394
+ rowNum() {
1395
+ return new ExprUnit("number", {
1396
+ type: "rowNum",
1397
+ });
1398
+ },
1399
+ /**
1400
+ * Generate random number (RAND/RANDOM)
1401
+ *
1402
+ * Returns a random number between 0 and 1. Mainly used for random ordering in ORDER BY
1403
+ *
1404
+ * @returns Random number between 0 and 1
1405
+ *
1406
+ * @example
1407
+ * ```typescript
1408
+ * // Random sorting
1409
+ * db.user().orderBy(() => expr.random()).limit(10)
1410
+ * ```
1411
+ */
1412
+ random() {
1413
+ return new ExprUnit("number", {
1414
+ type: "random",
1415
+ });
1416
+ },
1417
+ /**
1418
+ * type transformation (CAST)
1419
+ *
1420
+ * @param source - Expression to transform
1421
+ * @param targetType - Target data type
1422
+ * @returns Transformed expression
1423
+ *
1424
+ * @example
1425
+ * ```typescript
1426
+ * db.order().select((o) => ({
1427
+ * idStr: expr.cast(o.id, { type: "varchar", length: 20 }),
1428
+ * }))
1429
+ * // SELECT CAST(id AS VARCHAR(20)) AS idStr
1430
+ * ```
1431
+ */
1432
+ cast(source, targetType) {
1433
+ return new ExprUnit(dataTypeStrToColumnPrimitiveStr[targetType.type], {
1434
+ type: "cast",
1435
+ source: toExpr(source),
1436
+ targetType,
1437
+ });
1438
+ },
1439
+ /**
1440
+ * Scalar Subquery - Subquery that returns a single value in SELECT clause
1441
+ *
1442
+ * The subquery must return exactly one row and one column
1443
+ *
1444
+ * @param dataType - Data type of the returned value
1445
+ * @param queryable - Queryable that returns a scalar value
1446
+ * @returns Subquery result expression
1447
+ *
1448
+ * @example
1449
+ * ```typescript
1450
+ * db.user().select((u) => ({
1451
+ * id: u.id,
1452
+ * postCount: expr.subquery(
1453
+ * "number",
1454
+ * db.post()
1455
+ * .where((p) => [expr.eq(p.userId, u.id)])
1456
+ * .select(() => ({ cnt: expr.count() }))
1457
+ * ),
1458
+ * }))
1459
+ * // SELECT id, (SELECT COUNT(*) FROM Post WHERE userId = User.id) AS postCount
1460
+ * ```
1461
+ */
1462
+ subquery(dataType, queryable) {
1463
+ return new ExprUnit(dataType, {
1464
+ type: "subquery",
1465
+ queryDef: queryable.getSelectQueryDef(),
1466
+ });
1467
+ },
1468
+ //#endregion
1469
+ //#region ========== SELECT - Window Functions ==========
1470
+ /**
1471
+ * ROW_NUMBER() - Row number within a partition
1472
+ *
1473
+ * Assigns sequential numbers starting from 1 within each partition
1474
+ *
1475
+ * @param spec - Window spec (partitionBy, orderBy)
1476
+ * @returns Row number (starting from 1)
1477
+ *
1478
+ * @example
1479
+ * ```typescript
1480
+ * db.order().select((o) => ({
1481
+ * ...o,
1482
+ * rowNum: expr.rowNumber({
1483
+ * partitionBy: [o.userId],
1484
+ * orderBy: [[o.createdAt, "DESC"]],
1485
+ * }),
1486
+ * }))
1487
+ * // SELECT *, ROW_NUMBER() OVER (PARTITION BY userId ORDER BY createdAt DESC)
1488
+ * ```
1489
+ */
1490
+ rowNumber(spec) {
1491
+ return new ExprUnit("number", {
1492
+ type: "window",
1493
+ fn: { type: "rowNumber" },
1494
+ spec: toWinSpec(spec),
1495
+ });
1496
+ },
1497
+ /**
1498
+ * RANK() - Rank within a partition (ties get same rank, next rank is skipped)
1499
+ *
1500
+ * @param spec - Window spec (partitionBy, orderBy)
1501
+ * @returns Rank (skips after ties: 1, 1, 3)
1502
+ *
1503
+ * @example
1504
+ * ```typescript
1505
+ * db.student().select((s) => ({
1506
+ * name: s.name,
1507
+ * rank: expr.rank({
1508
+ * orderBy: [[s.score, "DESC"]],
1509
+ * }),
1510
+ * }))
1511
+ * ```
1512
+ */
1513
+ rank(spec) {
1514
+ return new ExprUnit("number", {
1515
+ type: "window",
1516
+ fn: { type: "rank" },
1517
+ spec: toWinSpec(spec),
1518
+ });
1519
+ },
1520
+ /**
1521
+ * DENSE_RANK() - Dense rank within a partition (ties get same rank, next rank is consecutive)
1522
+ *
1523
+ * @param spec - Window spec (partitionBy, orderBy)
1524
+ * @returns Dense rank (consecutive after ties: 1, 1, 2)
1525
+ *
1526
+ * @example
1527
+ * ```typescript
1528
+ * db.student().select((s) => ({
1529
+ * name: s.name,
1530
+ * denseRank: expr.denseRank({
1531
+ * orderBy: [[s.score, "DESC"]],
1532
+ * }),
1533
+ * }))
1534
+ * ```
1535
+ */
1536
+ denseRank(spec) {
1537
+ return new ExprUnit("number", {
1538
+ type: "window",
1539
+ fn: { type: "denseRank" },
1540
+ spec: toWinSpec(spec),
1541
+ });
1542
+ },
1543
+ /**
1544
+ * NTILE(n) - Split partition into n groups
1545
+ *
1546
+ * @param n - Number of groups to split into
1547
+ * @param spec - Window spec (partitionBy, orderBy)
1548
+ * @returns Group number (1 ~ n)
1549
+ *
1550
+ * @example
1551
+ * ```typescript
1552
+ * // Quartile split to find top 25%
1553
+ * db.user().select((u) => ({
1554
+ * name: u.name,
1555
+ * quartile: expr.ntile(4, {
1556
+ * orderBy: [[u.score, "DESC"]],
1557
+ * }),
1558
+ * }))
1559
+ * ```
1560
+ */
1561
+ ntile(n, spec) {
1562
+ return new ExprUnit("number", {
1563
+ type: "window",
1564
+ fn: { type: "ntile", n },
1565
+ spec: toWinSpec(spec),
1566
+ });
1567
+ },
1568
+ /**
1569
+ * LAG() - Reference value from a previous row
1570
+ *
1571
+ * @param column - Column to reference
1572
+ * @param spec - Window spec (partitionBy, orderBy)
1573
+ * @param options - offset (default 1), default (default value when no previous row)
1574
+ * @returns Previous row's value (or default value/NULL)
1575
+ *
1576
+ * @example
1577
+ * ```typescript
1578
+ * db.stock().select((s) => ({
1579
+ * date: s.date,
1580
+ * price: s.price,
1581
+ * prevPrice: expr.lag(s.price, {
1582
+ * partitionBy: [s.symbol],
1583
+ * orderBy: [[s.date, "ASC"]],
1584
+ * }),
1585
+ * }))
1586
+ * ```
1587
+ */
1588
+ lag(column, spec, options) {
1589
+ return new ExprUnit(column.dataType, {
1590
+ type: "window",
1591
+ fn: {
1592
+ type: "lag",
1593
+ column: toExpr(column),
1594
+ offset: options?.offset,
1595
+ default: options?.default != null ? toExpr(options.default) : undefined,
1596
+ },
1597
+ spec: toWinSpec(spec),
1598
+ });
1599
+ },
1600
+ /**
1601
+ * LEAD() - Reference value from a following row
1602
+ *
1603
+ * @param column - Column to reference
1604
+ * @param spec - Window spec (partitionBy, orderBy)
1605
+ * @param options - offset (default 1), default (default value when no following row)
1606
+ * @returns Following row's value (or default value/NULL)
1607
+ *
1608
+ * @example
1609
+ * ```typescript
1610
+ * db.stock().select((s) => ({
1611
+ * date: s.date,
1612
+ * price: s.price,
1613
+ * nextPrice: expr.lead(s.price, {
1614
+ * partitionBy: [s.symbol],
1615
+ * orderBy: [[s.date, "ASC"]],
1616
+ * }),
1617
+ * }))
1618
+ * ```
1619
+ */
1620
+ lead(column, spec, options) {
1621
+ return new ExprUnit(column.dataType, {
1622
+ type: "window",
1623
+ fn: {
1624
+ type: "lead",
1625
+ column: toExpr(column),
1626
+ offset: options?.offset,
1627
+ default: options?.default != null ? toExpr(options.default) : undefined,
1628
+ },
1629
+ spec: toWinSpec(spec),
1630
+ });
1631
+ },
1632
+ /**
1633
+ * FIRST_VALUE() - First value in the partition/frame
1634
+ *
1635
+ * @param column - Column to reference
1636
+ * @param spec - Window spec (partitionBy, orderBy)
1637
+ * @returns First value
1638
+ *
1639
+ * @example
1640
+ * ```typescript
1641
+ * db.order().select((o) => ({
1642
+ * ...o,
1643
+ * firstOrderAmount: expr.firstValue(o.amount, {
1644
+ * partitionBy: [o.userId],
1645
+ * orderBy: [[o.createdAt, "ASC"]],
1646
+ * }),
1647
+ * }))
1648
+ * ```
1649
+ */
1650
+ firstValue(column, spec) {
1651
+ return new ExprUnit(column.dataType, {
1652
+ type: "window",
1653
+ fn: { type: "firstValue", column: toExpr(column) },
1654
+ spec: toWinSpec(spec),
1655
+ });
1656
+ },
1657
+ /**
1658
+ * LAST_VALUE() - Last value in the partition/frame
1659
+ *
1660
+ * @param column - Column to reference
1661
+ * @param spec - Window spec (partitionBy, orderBy)
1662
+ * @returns Last value
1663
+ *
1664
+ * @example
1665
+ * ```typescript
1666
+ * db.order().select((o) => ({
1667
+ * ...o,
1668
+ * lastOrderAmount: expr.lastValue(o.amount, {
1669
+ * partitionBy: [o.userId],
1670
+ * orderBy: [[o.createdAt, "ASC"]],
1671
+ * }),
1672
+ * }))
1673
+ * ```
1674
+ */
1675
+ lastValue(column, spec) {
1676
+ return new ExprUnit(column.dataType, {
1677
+ type: "window",
1678
+ fn: { type: "lastValue", column: toExpr(column) },
1679
+ spec: toWinSpec(spec),
1680
+ });
1681
+ },
1682
+ /**
1683
+ * SUM() OVER - Window sum
1684
+ *
1685
+ * @param column - Column to sum
1686
+ * @param spec - Window spec (partitionBy, orderBy)
1687
+ * @returns Sum within window
1688
+ *
1689
+ * @example
1690
+ * ```typescript
1691
+ * // Running total
1692
+ * db.order().select((o) => ({
1693
+ * ...o,
1694
+ * runningTotal: expr.sumOver(o.amount, {
1695
+ * partitionBy: [o.userId],
1696
+ * orderBy: [[o.createdAt, "ASC"]],
1697
+ * }),
1698
+ * }))
1699
+ * ```
1700
+ */
1701
+ sumOver(column, spec) {
1702
+ return new ExprUnit("number", {
1703
+ type: "window",
1704
+ fn: { type: "sum", column: toExpr(column) },
1705
+ spec: toWinSpec(spec),
1706
+ });
1707
+ },
1708
+ /**
1709
+ * AVG() OVER - Window average
1710
+ *
1711
+ * @param column - Column to average
1712
+ * @param spec - Window spec (partitionBy, orderBy)
1713
+ * @returns Average within window
1714
+ *
1715
+ * @example
1716
+ * ```typescript
1717
+ * // Moving average
1718
+ * db.stock().select((s) => ({
1719
+ * ...s,
1720
+ * movingAvg: expr.avgOver(s.price, {
1721
+ * partitionBy: [s.symbol],
1722
+ * orderBy: [[s.date, "ASC"]],
1723
+ * }),
1724
+ * }))
1725
+ * ```
1726
+ */
1727
+ avgOver(column, spec) {
1728
+ return new ExprUnit("number", {
1729
+ type: "window",
1730
+ fn: { type: "avg", column: toExpr(column) },
1731
+ spec: toWinSpec(spec),
1732
+ });
1733
+ },
1734
+ /**
1735
+ * COUNT() OVER - Window count
1736
+ *
1737
+ * @param spec - Window spec (partitionBy, orderBy)
1738
+ * @param column - Column to count (all rows if omitted)
1739
+ * @returns Row count within window
1740
+ *
1741
+ * @example
1742
+ * ```typescript
1743
+ * db.order().select((o) => ({
1744
+ * ...o,
1745
+ * totalOrdersPerUser: expr.countOver({
1746
+ * partitionBy: [o.userId],
1747
+ * }),
1748
+ * }))
1749
+ * ```
1750
+ */
1751
+ countOver(spec, column) {
1752
+ return new ExprUnit("number", {
1753
+ type: "window",
1754
+ fn: { type: "count", column: column != null ? toExpr(column) : undefined },
1755
+ spec: toWinSpec(spec),
1756
+ });
1757
+ },
1758
+ /**
1759
+ * MIN() OVER - Window minimum
1760
+ *
1761
+ * @param column - Column to find minimum of
1762
+ * @param spec - Window spec (partitionBy, orderBy)
1763
+ * @returns Minimum value within window
1764
+ *
1765
+ * @example
1766
+ * ```typescript
1767
+ * db.stock().select((s) => ({
1768
+ * ...s,
1769
+ * minPriceInPeriod: expr.minOver(s.price, {
1770
+ * partitionBy: [s.symbol],
1771
+ * }),
1772
+ * }))
1773
+ * ```
1774
+ */
1775
+ minOver(column, spec) {
1776
+ return new ExprUnit(column.dataType, {
1777
+ type: "window",
1778
+ fn: { type: "min", column: toExpr(column) },
1779
+ spec: toWinSpec(spec),
1780
+ });
1781
+ },
1782
+ /**
1783
+ * MAX() OVER - Window maximum
1784
+ *
1785
+ * @param column - Column to find maximum of
1786
+ * @param spec - Window spec (partitionBy, orderBy)
1787
+ * @returns Maximum value within window
1788
+ *
1789
+ * @example
1790
+ * ```typescript
1791
+ * db.stock().select((s) => ({
1792
+ * ...s,
1793
+ * maxPriceInPeriod: expr.maxOver(s.price, {
1794
+ * partitionBy: [s.symbol],
1795
+ * }),
1796
+ * }))
1797
+ * ```
1798
+ */
1799
+ maxOver(column, spec) {
1800
+ return new ExprUnit(column.dataType, {
1801
+ type: "window",
1802
+ fn: { type: "max", column: toExpr(column) },
1803
+ spec: toWinSpec(spec),
1804
+ });
1805
+ },
1806
+ //#endregion
1807
+ //#region ========== Helper ==========
1808
+ /**
1809
+ * Transform ExprInput to Expr (internal use)
1810
+ *
1811
+ * @param value - Value to transform
1812
+ * @returns Expr JSON AST
1813
+ */
1814
+ toExpr(value) {
1815
+ return toExpr(value);
1816
+ },
1817
+ //#endregion
1789
1818
  };
1790
1819
  function coalesce(...args) {
1791
- return new ExprUnit(findDataType(args), {
1792
- type: "coalesce",
1793
- args: args.map((a) => toExpr(a))
1794
- });
1820
+ return new ExprUnit(findDataType(args), {
1821
+ type: "coalesce",
1822
+ args: args.map((a) => toExpr(a)),
1823
+ });
1795
1824
  }
1796
1825
  function createSwitchBuilder() {
1797
- const cases = [];
1798
- const thenValues = [];
1799
- return {
1800
- case(condition, then) {
1801
- cases.push({
1802
- when: condition.expr,
1803
- then: toExpr(then)
1804
- });
1805
- thenValues.push(then);
1806
- return this;
1807
- },
1808
- default(value) {
1809
- const allValues = [...thenValues, value];
1810
- const exprUnit = allValues.find((v) => v instanceof ExprUnit);
1811
- if (exprUnit) {
1812
- return new ExprUnit(exprUnit.dataType, {
1813
- type: "switch",
1814
- cases,
1815
- else: toExpr(value)
1816
- });
1817
- }
1818
- const nonNullLiteral = allValues.find((v) => v != null);
1819
- if (nonNullLiteral == null) {
1820
- throw new Error("At least one of switch's case/default must be non-null.");
1821
- }
1822
- return new ExprUnit(inferColumnPrimitiveStr(nonNullLiteral), {
1823
- type: "switch",
1824
- cases,
1825
- else: toExpr(value)
1826
- });
1827
- }
1828
- };
1826
+ const cases = [];
1827
+ const thenValues = []; // then 값 저장
1828
+ return {
1829
+ case(condition, then) {
1830
+ cases.push({
1831
+ when: condition.expr,
1832
+ then: toExpr(then),
1833
+ });
1834
+ thenValues.push(then);
1835
+ return this;
1836
+ },
1837
+ default(value) {
1838
+ const allValues = [...thenValues, value];
1839
+ // 1. ExprUnit에서 dataType 찾기
1840
+ const exprUnit = allValues.find((v) => v instanceof ExprUnit);
1841
+ if (exprUnit) {
1842
+ return new ExprUnit(exprUnit.dataType, {
1843
+ type: "switch",
1844
+ cases,
1845
+ else: toExpr(value),
1846
+ });
1847
+ }
1848
+ // 2. non-null 리터럴에서 추론
1849
+ const nonNullLiteral = allValues.find((v) => v != null);
1850
+ if (nonNullLiteral == null) {
1851
+ throw new Error("At least one of switch's case/default must be non-null.");
1852
+ }
1853
+ return new ExprUnit(inferColumnPrimitiveStr(nonNullLiteral), {
1854
+ type: "switch",
1855
+ cases,
1856
+ else: toExpr(value),
1857
+ });
1858
+ },
1859
+ };
1829
1860
  }
1830
- function toExpr(value) {
1831
- if (value instanceof ExprUnit) {
1832
- return value.expr;
1833
- }
1834
- return { type: "value", value };
1861
+ export function toExpr(value) {
1862
+ if (value instanceof ExprUnit) {
1863
+ return value.expr;
1864
+ }
1865
+ return { type: "value", value };
1835
1866
  }
1836
1867
  function findDataType(args) {
1837
- const exprUnit = args.find((a) => a instanceof ExprUnit);
1838
- if (!exprUnit) {
1839
- throw new Error("At least one of the arguments must be an ExprUnit.");
1840
- }
1841
- return exprUnit.dataType;
1868
+ const exprUnit = args.find((a) => a instanceof ExprUnit);
1869
+ if (!exprUnit) {
1870
+ throw new Error("At least one of the arguments must be an ExprUnit.");
1871
+ }
1872
+ return exprUnit.dataType;
1842
1873
  }
1843
1874
  function toWinSpec(spec) {
1844
- const result = {};
1845
- if (spec.partitionBy != null) {
1846
- result.partitionBy = spec.partitionBy.map((e) => toExpr(e));
1847
- }
1848
- if (spec.orderBy != null) {
1849
- result.orderBy = spec.orderBy.map(([e, dir]) => [toExpr(e), dir]);
1850
- }
1851
- return result;
1875
+ const result = {};
1876
+ if (spec.partitionBy != null) {
1877
+ result.partitionBy = spec.partitionBy.map((e) => toExpr(e));
1878
+ }
1879
+ if (spec.orderBy != null) {
1880
+ result.orderBy = spec.orderBy.map(([e, dir]) => [toExpr(e), dir]);
1881
+ }
1882
+ return result;
1852
1883
  }
1853
- export {
1854
- expr,
1855
- toExpr
1856
- };
1857
- //# sourceMappingURL=expr.js.map
1884
+ //#endregion
1885
+ //# sourceMappingURL=expr.js.map