@simplysm/orm-common 13.0.69 → 13.0.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/README.md +54 -1447
  2. package/dist/create-db-context.d.ts +10 -10
  3. package/dist/create-db-context.js +9 -9
  4. package/dist/create-db-context.js.map +1 -1
  5. package/dist/ddl/column-ddl.d.ts +4 -4
  6. package/dist/ddl/initialize.d.ts +17 -17
  7. package/dist/ddl/initialize.js +2 -2
  8. package/dist/ddl/initialize.js.map +1 -1
  9. package/dist/ddl/relation-ddl.d.ts +6 -6
  10. package/dist/ddl/schema-ddl.d.ts +4 -4
  11. package/dist/ddl/table-ddl.d.ts +24 -24
  12. package/dist/ddl/table-ddl.js +4 -4
  13. package/dist/ddl/table-ddl.js.map +1 -1
  14. package/dist/errors/db-transaction-error.d.ts +15 -15
  15. package/dist/errors/db-transaction-error.d.ts.map +1 -1
  16. package/dist/exec/executable.d.ts +23 -23
  17. package/dist/exec/executable.js +3 -3
  18. package/dist/exec/executable.js.map +1 -1
  19. package/dist/exec/queryable.d.ts +160 -160
  20. package/dist/exec/queryable.js +119 -119
  21. package/dist/exec/queryable.js.map +1 -1
  22. package/dist/exec/search-parser.d.ts +37 -37
  23. package/dist/exec/search-parser.d.ts.map +1 -1
  24. package/dist/expr/expr-unit.d.ts +4 -4
  25. package/dist/expr/expr.d.ts +257 -257
  26. package/dist/expr/expr.js +265 -265
  27. package/dist/expr/expr.js.map +1 -1
  28. package/dist/query-builder/base/expr-renderer-base.d.ts +9 -9
  29. package/dist/query-builder/base/expr-renderer-base.js +2 -2
  30. package/dist/query-builder/base/expr-renderer-base.js.map +1 -1
  31. package/dist/query-builder/base/query-builder-base.d.ts +26 -26
  32. package/dist/query-builder/base/query-builder-base.d.ts.map +1 -1
  33. package/dist/query-builder/base/query-builder-base.js +22 -22
  34. package/dist/query-builder/base/query-builder-base.js.map +1 -1
  35. package/dist/query-builder/mssql/mssql-expr-renderer.d.ts +4 -4
  36. package/dist/query-builder/mssql/mssql-expr-renderer.d.ts.map +1 -1
  37. package/dist/query-builder/mssql/mssql-expr-renderer.js +18 -18
  38. package/dist/query-builder/mssql/mssql-expr-renderer.js.map +1 -1
  39. package/dist/query-builder/mssql/mssql-query-builder.d.ts +2 -2
  40. package/dist/query-builder/mssql/mssql-query-builder.d.ts.map +1 -1
  41. package/dist/query-builder/mssql/mssql-query-builder.js +11 -11
  42. package/dist/query-builder/mssql/mssql-query-builder.js.map +1 -1
  43. package/dist/query-builder/mysql/mysql-expr-renderer.d.ts +4 -4
  44. package/dist/query-builder/mysql/mysql-expr-renderer.d.ts.map +1 -1
  45. package/dist/query-builder/mysql/mysql-expr-renderer.js +17 -17
  46. package/dist/query-builder/mysql/mysql-expr-renderer.js.map +1 -1
  47. package/dist/query-builder/mysql/mysql-query-builder.d.ts +8 -8
  48. package/dist/query-builder/mysql/mysql-query-builder.d.ts.map +1 -1
  49. package/dist/query-builder/mysql/mysql-query-builder.js +5 -5
  50. package/dist/query-builder/mysql/mysql-query-builder.js.map +1 -1
  51. package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts +4 -4
  52. package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts.map +1 -1
  53. package/dist/query-builder/postgresql/postgresql-expr-renderer.js +17 -17
  54. package/dist/query-builder/postgresql/postgresql-expr-renderer.js.map +1 -1
  55. package/dist/query-builder/postgresql/postgresql-query-builder.d.ts +5 -5
  56. package/dist/query-builder/postgresql/postgresql-query-builder.d.ts.map +1 -1
  57. package/dist/query-builder/postgresql/postgresql-query-builder.js +8 -8
  58. package/dist/query-builder/postgresql/postgresql-query-builder.js.map +1 -1
  59. package/dist/query-builder/query-builder.d.ts +1 -1
  60. package/dist/schema/factory/column-builder.d.ts +79 -79
  61. package/dist/schema/factory/column-builder.js +42 -42
  62. package/dist/schema/factory/index-builder.d.ts +39 -39
  63. package/dist/schema/factory/index-builder.js +26 -26
  64. package/dist/schema/factory/relation-builder.d.ts +99 -99
  65. package/dist/schema/factory/relation-builder.d.ts.map +1 -1
  66. package/dist/schema/factory/relation-builder.js +38 -38
  67. package/dist/schema/procedure-builder.d.ts +49 -49
  68. package/dist/schema/procedure-builder.d.ts.map +1 -1
  69. package/dist/schema/procedure-builder.js +33 -33
  70. package/dist/schema/table-builder.d.ts +59 -59
  71. package/dist/schema/table-builder.d.ts.map +1 -1
  72. package/dist/schema/table-builder.js +43 -43
  73. package/dist/schema/view-builder.d.ts +49 -49
  74. package/dist/schema/view-builder.d.ts.map +1 -1
  75. package/dist/schema/view-builder.js +32 -32
  76. package/dist/types/column.d.ts +22 -22
  77. package/dist/types/column.js +1 -1
  78. package/dist/types/column.js.map +1 -1
  79. package/dist/types/db.d.ts +40 -40
  80. package/dist/types/expr.d.ts +59 -59
  81. package/dist/types/expr.d.ts.map +1 -1
  82. package/dist/types/query-def.d.ts +44 -44
  83. package/dist/types/query-def.d.ts.map +1 -1
  84. package/dist/utils/result-parser.d.ts +11 -11
  85. package/dist/utils/result-parser.js +3 -3
  86. package/dist/utils/result-parser.js.map +1 -1
  87. package/package.json +5 -5
  88. package/src/create-db-context.ts +20 -20
  89. package/src/ddl/column-ddl.ts +4 -4
  90. package/src/ddl/initialize.ts +259 -259
  91. package/src/ddl/relation-ddl.ts +89 -89
  92. package/src/ddl/schema-ddl.ts +4 -4
  93. package/src/ddl/table-ddl.ts +189 -189
  94. package/src/errors/db-transaction-error.ts +13 -13
  95. package/src/exec/executable.ts +25 -25
  96. package/src/exec/queryable.ts +2033 -2033
  97. package/src/exec/search-parser.ts +57 -57
  98. package/src/expr/expr-unit.ts +4 -4
  99. package/src/expr/expr.ts +2140 -2140
  100. package/src/query-builder/base/expr-renderer-base.ts +237 -237
  101. package/src/query-builder/base/query-builder-base.ts +213 -213
  102. package/src/query-builder/mssql/mssql-expr-renderer.ts +607 -607
  103. package/src/query-builder/mssql/mssql-query-builder.ts +650 -650
  104. package/src/query-builder/mysql/mysql-expr-renderer.ts +613 -613
  105. package/src/query-builder/mysql/mysql-query-builder.ts +759 -759
  106. package/src/query-builder/postgresql/postgresql-expr-renderer.ts +611 -611
  107. package/src/query-builder/postgresql/postgresql-query-builder.ts +686 -686
  108. package/src/query-builder/query-builder.ts +19 -19
  109. package/src/schema/factory/column-builder.ts +423 -423
  110. package/src/schema/factory/index-builder.ts +164 -164
  111. package/src/schema/factory/relation-builder.ts +453 -453
  112. package/src/schema/procedure-builder.ts +232 -232
  113. package/src/schema/table-builder.ts +319 -319
  114. package/src/schema/view-builder.ts +221 -221
  115. package/src/types/column.ts +188 -188
  116. package/src/types/db.ts +208 -208
  117. package/src/types/expr.ts +697 -697
  118. package/src/types/query-def.ts +513 -513
  119. package/src/utils/result-parser.ts +458 -458
  120. package/tests/db-context/create-db-context.spec.ts +224 -0
  121. package/tests/db-context/define-db-context.spec.ts +68 -0
  122. package/tests/ddl/basic.expected.ts +341 -0
  123. package/tests/ddl/basic.spec.ts +714 -0
  124. package/tests/ddl/column-builder.expected.ts +310 -0
  125. package/tests/ddl/column-builder.spec.ts +637 -0
  126. package/tests/ddl/index-builder.expected.ts +38 -0
  127. package/tests/ddl/index-builder.spec.ts +202 -0
  128. package/tests/ddl/procedure-builder.expected.ts +52 -0
  129. package/tests/ddl/procedure-builder.spec.ts +234 -0
  130. package/tests/ddl/relation-builder.expected.ts +36 -0
  131. package/tests/ddl/relation-builder.spec.ts +372 -0
  132. package/tests/ddl/table-builder.expected.ts +113 -0
  133. package/tests/ddl/table-builder.spec.ts +433 -0
  134. package/tests/ddl/view-builder.expected.ts +38 -0
  135. package/tests/ddl/view-builder.spec.ts +176 -0
  136. package/tests/dml/delete.expected.ts +96 -0
  137. package/tests/dml/delete.spec.ts +160 -0
  138. package/tests/dml/insert.expected.ts +192 -0
  139. package/tests/dml/insert.spec.ts +288 -0
  140. package/tests/dml/update.expected.ts +176 -0
  141. package/tests/dml/update.spec.ts +318 -0
  142. package/tests/dml/upsert.expected.ts +215 -0
  143. package/tests/dml/upsert.spec.ts +242 -0
  144. package/tests/errors/queryable-errors.spec.ts +177 -0
  145. package/tests/escape.spec.ts +100 -0
  146. package/tests/examples/pivot.expected.ts +211 -0
  147. package/tests/examples/pivot.spec.ts +533 -0
  148. package/tests/examples/sampling.expected.ts +69 -0
  149. package/tests/examples/sampling.spec.ts +105 -0
  150. package/tests/examples/unpivot.expected.ts +120 -0
  151. package/tests/examples/unpivot.spec.ts +226 -0
  152. package/tests/exec/search-parser.spec.ts +283 -0
  153. package/tests/executable/basic.expected.ts +18 -0
  154. package/tests/executable/basic.spec.ts +54 -0
  155. package/tests/expr/comparison.expected.ts +282 -0
  156. package/tests/expr/comparison.spec.ts +400 -0
  157. package/tests/expr/conditional.expected.ts +134 -0
  158. package/tests/expr/conditional.spec.ts +276 -0
  159. package/tests/expr/date.expected.ts +332 -0
  160. package/tests/expr/date.spec.ts +526 -0
  161. package/tests/expr/math.expected.ts +62 -0
  162. package/tests/expr/math.spec.ts +106 -0
  163. package/tests/expr/string.expected.ts +218 -0
  164. package/tests/expr/string.spec.ts +356 -0
  165. package/tests/expr/utility.expected.ts +147 -0
  166. package/tests/expr/utility.spec.ts +182 -0
  167. package/tests/select/basic.expected.ts +322 -0
  168. package/tests/select/basic.spec.ts +502 -0
  169. package/tests/select/filter.expected.ts +357 -0
  170. package/tests/select/filter.spec.ts +1068 -0
  171. package/tests/select/group.expected.ts +169 -0
  172. package/tests/select/group.spec.ts +244 -0
  173. package/tests/select/join.expected.ts +582 -0
  174. package/tests/select/join.spec.ts +805 -0
  175. package/tests/select/order.expected.ts +150 -0
  176. package/tests/select/order.spec.ts +189 -0
  177. package/tests/select/recursive-cte.expected.ts +244 -0
  178. package/tests/select/recursive-cte.spec.ts +514 -0
  179. package/tests/select/result-meta.spec.ts +270 -0
  180. package/tests/select/subquery.expected.ts +363 -0
  181. package/tests/select/subquery.spec.ts +537 -0
  182. package/tests/select/view.expected.ts +155 -0
  183. package/tests/select/view.spec.ts +235 -0
  184. package/tests/select/window.expected.ts +345 -0
  185. package/tests/select/window.spec.ts +618 -0
  186. package/tests/setup/MockExecutor.ts +18 -0
  187. package/tests/setup/TestDbContext.ts +59 -0
  188. package/tests/setup/models/Company.ts +13 -0
  189. package/tests/setup/models/Employee.ts +10 -0
  190. package/tests/setup/models/MonthlySales.ts +11 -0
  191. package/tests/setup/models/Post.ts +16 -0
  192. package/tests/setup/models/Sales.ts +10 -0
  193. package/tests/setup/models/User.ts +19 -0
  194. package/tests/setup/procedure/GetAllUsers.ts +9 -0
  195. package/tests/setup/procedure/GetUserById.ts +12 -0
  196. package/tests/setup/test-utils.ts +72 -0
  197. package/tests/setup/views/ActiveUsers.ts +8 -0
  198. package/tests/setup/views/UserSummary.ts +11 -0
  199. package/tests/types/nullable-queryable-record.spec.ts +145 -0
  200. package/tests/utils/result-parser-perf.spec.ts +210 -0
  201. package/tests/utils/result-parser.spec.ts +701 -0
  202. package/docs/expressions.md +0 -172
  203. package/docs/queries.md +0 -444
  204. package/docs/schema.md +0 -245
@@ -1,650 +1,650 @@
1
- import type {
2
- AddColumnQueryDef,
3
- AddFkQueryDef,
4
- AddIdxQueryDef,
5
- AddPkQueryDef,
6
- ClearSchemaQueryDef,
7
- CreateProcQueryDef,
8
- CreateTableQueryDef,
9
- CreateViewQueryDef,
10
- SchemaExistsQueryDef,
11
- DeleteQueryDef,
12
- DropColumnQueryDef,
13
- DropFkQueryDef,
14
- DropIdxQueryDef,
15
- DropPkQueryDef,
16
- DropProcQueryDef,
17
- DropTableQueryDef,
18
- DropViewQueryDef,
19
- ExecProcQueryDef,
20
- InsertIfNotExistsQueryDef,
21
- InsertIntoQueryDef,
22
- InsertQueryDef,
23
- ModifyColumnQueryDef,
24
- QueryDefObjectName,
25
- RenameColumnQueryDef,
26
- RenameTableQueryDef,
27
- SelectQueryDef,
28
- SelectQueryDefJoin,
29
- SwitchFkQueryDef,
30
- TruncateQueryDef,
31
- UpdateQueryDef,
32
- UpsertQueryDef,
33
- } from "../../types/query-def";
34
- import type { QueryBuildResult } from "../../types/db";
35
- import { QueryBuilderBase } from "../base/query-builder-base";
36
- import { MssqlExprRenderer } from "./mssql-expr-renderer";
37
-
38
- /**
39
- * MSSQL QueryBuilder
40
- */
41
- export class MssqlQueryBuilder extends QueryBuilderBase {
42
- protected expr = new MssqlExprRenderer((def) => this.select(def).sql);
43
-
44
- //#region ========== 유틸리티 ==========
45
-
46
- /** 테이블명 렌더링 */
47
- protected tableName(obj: QueryDefObjectName): string {
48
- const parts: string[] = [];
49
- if (obj.database != null) {
50
- parts.push(this.expr.wrap(obj.database));
51
- }
52
- if (obj.schema != null) {
53
- parts.push(this.expr.wrap(obj.schema));
54
- } else if (obj.database != null) {
55
- parts.push("[dbo]");
56
- }
57
- parts.push(this.expr.wrap(obj.name));
58
- return parts.join(".");
59
- }
60
-
61
- /** OFFSET...FETCH 절 렌더링 */
62
- protected renderLimit(limit: [number, number] | undefined): string {
63
- if (limit == null) return "";
64
- const [offset, count] = limit;
65
- return ` OFFSET ${offset} ROWS FETCH NEXT ${count} ROWS ONLY`;
66
- }
67
-
68
- protected renderJoin(join: SelectQueryDefJoin): string {
69
- const alias = this.expr.wrap(join.as);
70
-
71
- // LATERAL JOIN 필요 여부 감지 → MSSQL은 OUTER APPLY 사용
72
- if (this.needsLateral(join)) {
73
- // from이 배열(UNION ALL)이면 renderFrom(join.from),
74
- // 그 외(orderBy, top, select 등)면 renderFrom(join)으로 서브쿼리 생성
75
- const from = Array.isArray(join.from) ? this.renderFrom(join.from) : this.renderFrom(join);
76
- return ` OUTER APPLY ${from} AS ${alias}`;
77
- }
78
-
79
- // 일반 JOIN
80
- const from = this.renderFrom(join.from);
81
- const where =
82
- join.where != null && join.where.length > 0
83
- ? ` ON ${this.expr.renderWhere(join.where)}`
84
- : " ON 1=1";
85
- return ` LEFT OUTER JOIN ${from} AS ${alias}${where}`;
86
- }
87
-
88
- //#endregion
89
-
90
- //#region ========== DML - SELECT ==========
91
-
92
- protected select(def: SelectQueryDef): QueryBuildResult {
93
- // WITH (CTE)
94
- let sql = "";
95
- if (def.with != null) {
96
- const { name, base, recursive } = def.with;
97
- sql += `WITH ${this.expr.wrap(name)} AS (${this.select(base).sql} UNION ALL ${this.select(recursive).sql}) `;
98
- }
99
-
100
- // SELECT
101
- sql += "SELECT";
102
- if (def.distinct) {
103
- sql += " DISTINCT";
104
- }
105
- // TOP
106
- if (def.top != null) {
107
- sql += ` TOP ${def.top}`;
108
- }
109
-
110
- // columns
111
- if (def.select != null) {
112
- const cols = Object.entries(def.select).map(
113
- ([alias, expr]) => `${this.expr.render(expr)} AS ${this.expr.wrap(alias)}`,
114
- );
115
- sql += ` ${cols.join(", ")}`;
116
- } else {
117
- sql += " *";
118
- }
119
-
120
- // FROM
121
- if (def.from != null) {
122
- const from = this.renderFrom(def.from);
123
- sql += ` FROM ${from} AS ${this.expr.wrap(def.as)}`;
124
-
125
- // LOCK (ROWLOCK으로 수준 락 강제 - MySQL/PostgreSQL FOR UPDATE와 동일 동작)
126
- if (def.lock) {
127
- sql += " WITH (UPDLOCK, ROWLOCK)";
128
- }
129
- }
130
-
131
- // JOINs
132
- sql += this.renderJoins(def.joins);
133
-
134
- // WHERE
135
- sql += this.renderWhere(def.where);
136
-
137
- // GROUP BY
138
- sql += this.renderGroupBy(def.groupBy);
139
-
140
- // HAVING
141
- sql += this.renderHaving(def.having);
142
-
143
- // ORDER BY
144
- sql += this.renderOrderBy(def.orderBy);
145
-
146
- // LIMIT (OFFSET...FETCH)
147
- sql += this.renderLimit(def.limit);
148
-
149
- return { sql };
150
- }
151
-
152
- //#endregion
153
-
154
- //#region ========== DML - INSERT ==========
155
-
156
- protected insert(def: InsertQueryDef): QueryBuildResult {
157
- const table = this.tableName(def.table);
158
-
159
- if (def.records.length === 0) {
160
- throw new Error("INSERT 최소 하나의 레코드가 필요합니다.");
161
- }
162
-
163
- const columns = Object.keys(def.records[0]);
164
- const colList = columns.map((c) => this.expr.wrap(c)).join(", ");
165
-
166
- let sql = "";
167
-
168
- // IDENTITY_INSERT ON (AI 컬럼에 명시적 삽입 시)
169
- if (def.overrideIdentity) {
170
- sql += `SET IDENTITY_INSERT ${table} ON;\n`;
171
- }
172
-
173
- sql += `INSERT INTO ${table} (${colList})`;
174
-
175
- // OUTPUT (MSSQL 네이티브 지원)
176
- if (def.output != null) {
177
- const outputCols = def.output.columns.map((c) => `INSERTED.${this.expr.wrap(c)}`).join(", ");
178
- sql += ` OUTPUT ${outputCols}`;
179
- }
180
-
181
- sql += ` VALUES`;
182
-
183
- const valuesList = def.records.map((record) => {
184
- const values = columns.map((c) => this.expr.escapeValue(record[c]));
185
- return `(${values.join(", ")})`;
186
- });
187
- sql += ` ${valuesList.join(", ")}`;
188
-
189
- // IDENTITY_INSERT OFF
190
- if (def.overrideIdentity) {
191
- sql += `;\nSET IDENTITY_INSERT ${table} OFF;`;
192
- }
193
-
194
- // overrideIdentity 시: SET ON → results[0], INSERT → results[1], SET OFF → results[2]
195
- return { sql, resultSetIndex: def.overrideIdentity ? 1 : undefined };
196
- }
197
-
198
- protected insertIfNotExists(def: InsertIfNotExistsQueryDef): QueryBuildResult {
199
- const table = this.tableName(def.table);
200
-
201
- const columns = Object.keys(def.record);
202
- const colList = columns.map((c) => this.expr.wrap(c)).join(", ");
203
- const values = columns.map((c) => this.expr.escapeValue(def.record[c])).join(", ");
204
-
205
- // existsSelectQuery를 SELECT 1 AS _ 형태로 렌더링
206
- const existsQuerySql = this.select({
207
- ...def.existsSelectQuery,
208
- select: { _: { type: "value", value: 1 } },
209
- }).sql;
210
-
211
- let sql = `INSERT INTO ${table} (${colList})`;
212
-
213
- // OUTPUT (MSSQL: OUTPUT은 SELECT 앞에 위치해야 함)
214
- if (def.output != null) {
215
- const outputCols = def.output.columns.map((c) => `INSERTED.${this.expr.wrap(c)}`).join(", ");
216
- sql += ` OUTPUT ${outputCols}`;
217
- }
218
-
219
- sql += ` SELECT ${values} WHERE NOT EXISTS (${existsQuerySql})`;
220
-
221
- return { sql };
222
- }
223
-
224
- protected insertInto(def: InsertIntoQueryDef): QueryBuildResult {
225
- const table = this.tableName(def.table);
226
- const selectSql = this.select(def.recordsSelectQuery).sql;
227
-
228
- // INSERT INTO SELECT에서 columns 추출
229
- const selectDef = def.recordsSelectQuery;
230
- const colList =
231
- selectDef.select != null
232
- ? Object.keys(selectDef.select)
233
- .map((c) => this.expr.wrap(c))
234
- .join(", ")
235
- : "*";
236
-
237
- let sql = `INSERT INTO ${table} (${colList})`;
238
-
239
- // OUTPUT
240
- if (def.output != null) {
241
- const outputCols = def.output.columns.map((c) => `INSERTED.${this.expr.wrap(c)}`).join(", ");
242
- sql += ` OUTPUT ${outputCols}`;
243
- }
244
-
245
- sql += ` ${selectSql}`;
246
- return { sql };
247
- }
248
-
249
- //#endregion
250
-
251
- //#region ========== DML - UPDATE ==========
252
-
253
- protected update(def: UpdateQueryDef): QueryBuildResult {
254
- const table = this.tableName(def.table);
255
- const alias = this.expr.wrap(def.as);
256
-
257
- // SET
258
- const setParts = Object.entries(def.record).map(
259
- ([col, e]) => `${alias}.${this.expr.wrap(col)} = ${this.expr.render(e)}`,
260
- );
261
-
262
- let sql = "UPDATE";
263
- if (def.top != null) {
264
- sql += ` TOP ${def.top}`;
265
- }
266
- sql += ` ${alias} SET ${setParts.join(", ")}`;
267
-
268
- // OUTPUT
269
- if (def.output != null) {
270
- const outputCols = def.output.columns.map((c) => `INSERTED.${this.expr.wrap(c)}`).join(", ");
271
- sql += ` OUTPUT ${outputCols}`;
272
- }
273
-
274
- sql += ` FROM ${table} AS ${alias}`;
275
- sql += this.renderJoins(def.joins);
276
- sql += this.renderWhere(def.where);
277
-
278
- return { sql };
279
- }
280
-
281
- //#endregion
282
-
283
- //#region ========== DML - DELETE ==========
284
-
285
- protected delete(def: DeleteQueryDef): QueryBuildResult {
286
- const table = this.tableName(def.table);
287
- const alias = this.expr.wrap(def.as);
288
-
289
- let sql = "DELETE";
290
- if (def.top != null) {
291
- sql += ` TOP ${def.top}`;
292
- }
293
- sql += ` ${alias}`;
294
-
295
- // OUTPUT (MSSQL: DELETED for DELETE)
296
- if (def.output != null) {
297
- const outputCols = def.output.columns.map((c) => `DELETED.${this.expr.wrap(c)}`).join(", ");
298
- sql += ` OUTPUT ${outputCols}`;
299
- }
300
-
301
- sql += ` FROM ${table} AS ${alias}`;
302
- sql += this.renderJoins(def.joins);
303
- sql += this.renderWhere(def.where);
304
-
305
- return { sql };
306
- }
307
-
308
- //#endregion
309
-
310
- //#region ========== DML - UPSERT ==========
311
-
312
- protected upsert(def: UpsertQueryDef): QueryBuildResult {
313
- // MSSQL: MERGE 사용
314
- const table = this.tableName(def.table);
315
- const alias = this.expr.wrap(def.existsSelectQuery.as);
316
- const existsWhere = def.existsSelectQuery.where;
317
-
318
- // UPDATE SET 부분
319
- const updateSetParts = Object.entries(def.updateRecord).map(
320
- ([col, e]) => `${this.expr.wrap(col)} = ${this.expr.render(e)}`,
321
- );
322
-
323
- // INSERT 부분
324
- const insertColumns = Object.keys(def.insertRecord);
325
- const insertColList = insertColumns.map((c) => this.expr.wrap(c)).join(", ");
326
- const insertValues = insertColumns.map((c) => this.expr.render(def.insertRecord[c])).join(", ");
327
-
328
- let sql = `MERGE ${table} AS ${alias}\n`;
329
- sql += `USING (SELECT 1 AS [_]) AS [_src] ON `;
330
- sql +=
331
- existsWhere != null && existsWhere.length > 0 ? this.expr.renderWhere(existsWhere) : "1=0";
332
-
333
- if (updateSetParts.length > 0) {
334
- sql += `\nWHEN MATCHED THEN UPDATE SET ${updateSetParts.join(", ")}`;
335
- }
336
-
337
- sql += `\nWHEN NOT MATCHED THEN INSERT (${insertColList}) VALUES (${insertValues})`;
338
-
339
- // OUTPUT
340
- if (def.output != null) {
341
- const outputCols = def.output.columns.map((c) => `INSERTED.${this.expr.wrap(c)}`).join(", ");
342
- sql += `\nOUTPUT ${outputCols}`;
343
- }
344
-
345
- sql += ";";
346
- return { sql };
347
- }
348
-
349
- //#endregion
350
-
351
- //#region ========== DDL - Table ==========
352
-
353
- protected createTable(def: CreateTableQueryDef): QueryBuildResult {
354
- const table = this.tableName(def.table);
355
-
356
- const colDefs = def.columns.map((col) => {
357
- let colSql = `${this.expr.wrap(col.name)} ${this.expr.renderDataType(col.dataType)}`;
358
-
359
- // nullable: true → NULL, else → NOT NULL
360
- if (col.nullable === true) {
361
- colSql += " NULL";
362
- } else {
363
- colSql += " NOT NULL";
364
- }
365
-
366
- if (col.autoIncrement) {
367
- colSql += " IDENTITY(1,1)";
368
- }
369
-
370
- if (col.default !== undefined) {
371
- colSql += ` DEFAULT ${this.expr.escapeValue(col.default)}`;
372
- }
373
-
374
- return colSql;
375
- });
376
-
377
- // Primary Key with CONSTRAINT name
378
- if (def.primaryKey != null && def.primaryKey.length > 0) {
379
- const pkCols = def.primaryKey.map((c) => this.expr.wrap(c)).join(", ");
380
- const pkName = this.expr.wrap(`PK_${def.table.name}`);
381
- colDefs.push(`CONSTRAINT ${pkName} PRIMARY KEY (${pkCols})`);
382
- }
383
-
384
- return { sql: `CREATE TABLE ${table} (\n ${colDefs.join(",\n ")}\n)` };
385
- }
386
-
387
- protected dropTable(def: DropTableQueryDef): QueryBuildResult {
388
- return { sql: `DROP TABLE ${this.tableName(def.table)}` };
389
- }
390
-
391
- protected renameTable(def: RenameTableQueryDef): QueryBuildResult {
392
- // MSSQL: sp_rename 사용
393
- const tableName = this.expr.escapeString(this.tableName(def.table));
394
- const newName = this.expr.escapeString(def.newName);
395
- return { sql: `EXEC sp_rename '${tableName}', '${newName}'` };
396
- }
397
-
398
- protected truncate(def: TruncateQueryDef): QueryBuildResult {
399
- // MSSQL: TRUNCATE는 IDENTITY 자동 리셋
400
- return { sql: `TRUNCATE TABLE ${this.tableName(def.table)}` };
401
- }
402
-
403
- //#endregion
404
-
405
- //#region ========== DDL - Column ==========
406
-
407
- protected addColumn(def: AddColumnQueryDef): QueryBuildResult {
408
- const table = this.tableName(def.table);
409
- const col = def.column;
410
-
411
- let colSql = `${this.expr.wrap(col.name)} ${this.expr.renderDataType(col.dataType)}`;
412
-
413
- // nullable: true → NULL, else → NOT NULL
414
- if (col.nullable === true) {
415
- colSql += " NULL";
416
- } else {
417
- colSql += " NOT NULL";
418
- }
419
-
420
- if (col.autoIncrement) {
421
- colSql += " IDENTITY(1,1)";
422
- }
423
-
424
- if (col.default !== undefined) {
425
- colSql += ` DEFAULT ${this.expr.escapeValue(col.default)}`;
426
- }
427
-
428
- return { sql: `ALTER TABLE ${table} ADD ${colSql}` };
429
- }
430
-
431
- protected dropColumn(def: DropColumnQueryDef): QueryBuildResult {
432
- return {
433
- sql: `ALTER TABLE ${this.tableName(def.table)} DROP COLUMN ${this.expr.wrap(def.column)}`,
434
- };
435
- }
436
-
437
- protected modifyColumn(def: ModifyColumnQueryDef): QueryBuildResult {
438
- const table = this.tableName(def.table);
439
- const col = def.column;
440
-
441
- let colSql = `${this.expr.wrap(col.name)} ${this.expr.renderDataType(col.dataType)}`;
442
-
443
- // nullable: true → NULL, else → NOT NULL
444
- if (col.nullable === true) {
445
- colSql += " NULL";
446
- } else {
447
- colSql += " NOT NULL";
448
- }
449
-
450
- // MSSQL: ALTER COLUMN (IDENTITY와 DEFAULT는 별도 처리 필요)
451
- return { sql: `ALTER TABLE ${table} ALTER COLUMN ${colSql}` };
452
- }
453
-
454
- protected renameColumn(def: RenameColumnQueryDef): QueryBuildResult {
455
- const table = this.tableName(def.table);
456
- // MSSQL: sp_rename 사용
457
- const tableCol = this.expr.escapeString(`${table}.${def.column}`);
458
- const newName = this.expr.escapeString(def.newName);
459
- return { sql: `EXEC sp_rename '${tableCol}', '${newName}', 'COLUMN'` };
460
- }
461
-
462
- //#endregion
463
-
464
- //#region ========== DDL - Constraint ==========
465
-
466
- protected addPk(def: AddPkQueryDef): QueryBuildResult {
467
- const table = this.tableName(def.table);
468
- const cols = def.columns.map((c) => this.expr.wrap(c)).join(", ");
469
- const pkName = `PK_${def.table.name}`;
470
- return {
471
- sql: `ALTER TABLE ${table} ADD CONSTRAINT ${this.expr.wrap(pkName)} PRIMARY KEY (${cols})`,
472
- };
473
- }
474
-
475
- protected dropPk(def: DropPkQueryDef): QueryBuildResult {
476
- const table = this.tableName(def.table);
477
- const pkName = `PK_${def.table.name}`;
478
- return { sql: `ALTER TABLE ${table} DROP CONSTRAINT ${this.expr.wrap(pkName)}` };
479
- }
480
-
481
- protected addFk(def: AddFkQueryDef): QueryBuildResult {
482
- const table = this.tableName(def.table);
483
- const fk = def.foreignKey;
484
- const fkCols = fk.fkColumns.map((c) => this.expr.wrap(c)).join(", ");
485
- const targetTable = this.tableName(fk.targetTable);
486
- const targetCols = fk.targetPkColumns.map((c) => this.expr.wrap(c)).join(", ");
487
-
488
- let sql = `ALTER TABLE ${table} ADD CONSTRAINT ${this.expr.wrap(fk.name)} FOREIGN KEY (${fkCols}) REFERENCES ${targetTable} (${targetCols})`;
489
-
490
- // MSSQL/PostgreSQL: FK용 인덱스 별도 생성 필요
491
- const idxName = `IDX_${def.table.name}_${fk.name.replace(/^FK_/, "")}`;
492
- sql += `;\nCREATE INDEX ${this.expr.wrap(idxName)} ON ${table} (${fkCols});`;
493
-
494
- return { sql };
495
- }
496
-
497
- protected dropFk(def: DropFkQueryDef): QueryBuildResult {
498
- return {
499
- sql: `ALTER TABLE ${this.tableName(def.table)} DROP CONSTRAINT ${this.expr.wrap(def.foreignKey)}`,
500
- };
501
- }
502
-
503
- protected addIdx(def: AddIdxQueryDef): QueryBuildResult {
504
- const table = this.tableName(def.table);
505
- const idx = def.index;
506
- const cols = idx.columns.map((c) => `${this.expr.wrap(c.name)} ${c.orderBy}`).join(", ");
507
- const unique = idx.unique ? "UNIQUE " : "";
508
- return { sql: `CREATE ${unique}INDEX ${this.expr.wrap(idx.name)} ON ${table} (${cols})` };
509
- }
510
-
511
- protected dropIdx(def: DropIdxQueryDef): QueryBuildResult {
512
- return { sql: `DROP INDEX ${this.expr.wrap(def.index)} ON ${this.tableName(def.table)}` };
513
- }
514
-
515
- //#endregion
516
-
517
- //#region ========== DDL - View/Procedure ==========
518
-
519
- protected createView(def: CreateViewQueryDef): QueryBuildResult {
520
- const view = this.tableName(def.view);
521
- const selectSql = this.select(def.queryDef).sql;
522
- // MSSQL: CREATE OR ALTER VIEW (2016 SP1+)
523
- return { sql: `CREATE OR ALTER VIEW ${view} AS ${selectSql}` };
524
- }
525
-
526
- protected dropView(def: DropViewQueryDef): QueryBuildResult {
527
- return { sql: `DROP VIEW IF EXISTS ${this.tableName(def.view)}` };
528
- }
529
-
530
- protected createProc(def: CreateProcQueryDef): QueryBuildResult {
531
- const proc = this.tableName(def.procedure);
532
-
533
- // params 처리
534
- const paramList =
535
- def.params
536
- ?.map((p) => {
537
- let sql = `@${p.name} ${this.expr.renderDataType(p.dataType)}`;
538
- if (p.default !== undefined) {
539
- sql += ` = ${this.expr.escapeValue(p.default)}`;
540
- }
541
- return sql;
542
- })
543
- .join(", ") ?? "";
544
-
545
- let sql = `CREATE OR ALTER PROCEDURE ${proc}`;
546
- if (paramList) {
547
- sql += ` ${paramList}`;
548
- }
549
- sql += `\nAS\nBEGIN\n`;
550
- sql += `SET NOCOUNT ON;\n`;
551
- sql += def.query;
552
- sql += `\nEND`;
553
-
554
- return { sql };
555
- }
556
-
557
- protected dropProc(def: DropProcQueryDef): QueryBuildResult {
558
- return { sql: `DROP PROCEDURE IF EXISTS ${this.tableName(def.procedure)}` };
559
- }
560
-
561
- protected execProc(def: ExecProcQueryDef): QueryBuildResult {
562
- const proc = this.tableName(def.procedure);
563
- if (def.params == null || Object.keys(def.params).length === 0) {
564
- return { sql: `EXEC ${proc}` };
565
- }
566
- const params = Object.values(def.params)
567
- .map((p) => this.expr.render(p))
568
- .join(", ");
569
- return { sql: `EXEC ${proc} ${params}` };
570
- }
571
-
572
- //#endregion
573
-
574
- //#region ========== Utils ==========
575
-
576
- protected clearSchema(def: ClearSchemaQueryDef): QueryBuildResult {
577
- // SQL Injection 방지: 식별자 유효성 검증
578
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(def.database)) {
579
- throw new Error(`유효하지 않은 데이터베이스명: ${def.database}`);
580
- }
581
- const schemaName = def.schema ?? "dbo";
582
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(schemaName)) {
583
- throw new Error(`유효하지 않은 스키마명: ${schemaName}`);
584
- }
585
-
586
- const db = this.expr.wrap(def.database);
587
- const schema = this.expr.escapeString(schemaName);
588
- return {
589
- sql: `
590
- DECLARE @sql NVARCHAR(MAX);
591
- SET @sql = N'';
592
-
593
- -- FK 제약조건 삭제
594
- SELECT @sql = @sql + N'ALTER TABLE ' + QUOTENAME(OBJECT_SCHEMA_NAME(parent_object_id)) + '.' + QUOTENAME(OBJECT_NAME(parent_object_id)) + N' DROP CONSTRAINT ' + QUOTENAME(name) + N';' + CHAR(13)
595
- FROM ${db}.sys.foreign_keys
596
- WHERE OBJECT_SCHEMA_NAME(parent_object_id) = '${schema}';
597
-
598
- -- 테이블 삭제
599
- SELECT @sql = @sql + N'DROP TABLE ' + QUOTENAME(SCHEMA_NAME(schema_id)) + '.' + QUOTENAME(name) + N';' + CHAR(13)
600
- FROM ${db}.sys.tables
601
- WHERE SCHEMA_NAME(schema_id) = '${schema}';
602
-
603
- -- 삭제
604
- SELECT @sql = @sql + N'DROP VIEW ' + QUOTENAME(SCHEMA_NAME(schema_id)) + '.' + QUOTENAME(name) + N';' + CHAR(13)
605
- FROM ${db}.sys.views
606
- WHERE schema_id = SCHEMA_ID('${schema}');
607
-
608
- -- 프로시저 삭제
609
- SELECT @sql = @sql + N'DROP PROCEDURE ' + QUOTENAME(SCHEMA_NAME(schema_id)) + '.' + QUOTENAME(name) + N';' + CHAR(13)
610
- FROM ${db}.sys.procedures
611
- WHERE SCHEMA_NAME(schema_id) = '${schema}';
612
-
613
- EXEC sp_executesql @sql;`,
614
- };
615
- }
616
-
617
- protected schemaExists(def: SchemaExistsQueryDef): QueryBuildResult {
618
- // SQL Injection 방지: 식별자 유효성 검증
619
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(def.database)) {
620
- throw new Error(`유효하지 않은 데이터베이스명: ${def.database}`);
621
- }
622
- const schemaName = def.schema ?? "dbo";
623
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(schemaName)) {
624
- throw new Error(`유효하지 않은 스키마명: ${schemaName}`);
625
- }
626
-
627
- const dbName = this.expr.escapeString(def.database);
628
- const schema = this.expr.escapeString(schemaName);
629
- // MSSQL: database 존재 확인 후 schema 확인 (동적 SQL 사용)
630
- return {
631
- sql: `DECLARE @result NVARCHAR(MAX) = NULL;
632
- IF EXISTS (SELECT 1 FROM sys.databases WHERE name = '${dbName}')
633
- BEGIN
634
- DECLARE @sql NVARCHAR(MAX) = N'SELECT @result = name FROM ' + QUOTENAME('${dbName}') + N'.sys.schemas WHERE name = ''${schema}''';
635
- EXEC sp_executesql @sql, N'@result NVARCHAR(MAX) OUTPUT', @result OUTPUT;
636
- END
637
- SELECT @result AS name WHERE @result IS NOT NULL`,
638
- };
639
- }
640
-
641
- protected switchFk(def: SwitchFkQueryDef): QueryBuildResult {
642
- const table = this.tableName(def.table);
643
- if (def.switch === "on") {
644
- return { sql: `ALTER TABLE ${table} WITH CHECK CHECK CONSTRAINT ALL` };
645
- }
646
- return { sql: `ALTER TABLE ${table} NOCHECK CONSTRAINT ALL` };
647
- }
648
-
649
- //#endregion
650
- }
1
+ import type {
2
+ AddColumnQueryDef,
3
+ AddFkQueryDef,
4
+ AddIdxQueryDef,
5
+ AddPkQueryDef,
6
+ ClearSchemaQueryDef,
7
+ CreateProcQueryDef,
8
+ CreateTableQueryDef,
9
+ CreateViewQueryDef,
10
+ SchemaExistsQueryDef,
11
+ DeleteQueryDef,
12
+ DropColumnQueryDef,
13
+ DropFkQueryDef,
14
+ DropIdxQueryDef,
15
+ DropPkQueryDef,
16
+ DropProcQueryDef,
17
+ DropTableQueryDef,
18
+ DropViewQueryDef,
19
+ ExecProcQueryDef,
20
+ InsertIfNotExistsQueryDef,
21
+ InsertIntoQueryDef,
22
+ InsertQueryDef,
23
+ ModifyColumnQueryDef,
24
+ QueryDefObjectName,
25
+ RenameColumnQueryDef,
26
+ RenameTableQueryDef,
27
+ SelectQueryDef,
28
+ SelectQueryDefJoin,
29
+ SwitchFkQueryDef,
30
+ TruncateQueryDef,
31
+ UpdateQueryDef,
32
+ UpsertQueryDef,
33
+ } from "../../types/query-def";
34
+ import type { QueryBuildResult } from "../../types/db";
35
+ import { QueryBuilderBase } from "../base/query-builder-base";
36
+ import { MssqlExprRenderer } from "./mssql-expr-renderer";
37
+
38
+ /**
39
+ * MSSQL QueryBuilder
40
+ */
41
+ export class MssqlQueryBuilder extends QueryBuilderBase {
42
+ protected expr = new MssqlExprRenderer((def) => this.select(def).sql);
43
+
44
+ //#region ========== 유틸리티 ==========
45
+
46
+ /** Table명 Render */
47
+ protected tableName(obj: QueryDefObjectName): string {
48
+ const parts: string[] = [];
49
+ if (obj.database != null) {
50
+ parts.push(this.expr.wrap(obj.database));
51
+ }
52
+ if (obj.schema != null) {
53
+ parts.push(this.expr.wrap(obj.schema));
54
+ } else if (obj.database != null) {
55
+ parts.push("[dbo]");
56
+ }
57
+ parts.push(this.expr.wrap(obj.name));
58
+ return parts.join(".");
59
+ }
60
+
61
+ /** OFFSET...FETCH 절 Render */
62
+ protected renderLimit(limit: [number, number] | undefined): string {
63
+ if (limit == null) return "";
64
+ const [offset, count] = limit;
65
+ return ` OFFSET ${offset} ROWS FETCH NEXT ${count} ROWS ONLY`;
66
+ }
67
+
68
+ protected renderJoin(join: SelectQueryDefJoin): string {
69
+ const alias = this.expr.wrap(join.as);
70
+
71
+ // LATERAL JOIN 필요 여부 감지 → MSSQL은 OUTER APPLY 사용
72
+ if (this.needsLateral(join)) {
73
+ // from이 배열(UNION ALL)이면 renderFrom(join.from),
74
+ // 그 외(orderBy, top, select 등)면 renderFrom(join)으로 Subquery Generate
75
+ const from = Array.isArray(join.from) ? this.renderFrom(join.from) : this.renderFrom(join);
76
+ return ` OUTER APPLY ${from} AS ${alias}`;
77
+ }
78
+
79
+ // 일반 JOIN
80
+ const from = this.renderFrom(join.from);
81
+ const where =
82
+ join.where != null && join.where.length > 0
83
+ ? ` ON ${this.expr.renderWhere(join.where)}`
84
+ : " ON 1=1";
85
+ return ` LEFT OUTER JOIN ${from} AS ${alias}${where}`;
86
+ }
87
+
88
+ //#endregion
89
+
90
+ //#region ========== DML - SELECT ==========
91
+
92
+ protected select(def: SelectQueryDef): QueryBuildResult {
93
+ // WITH (CTE)
94
+ let sql = "";
95
+ if (def.with != null) {
96
+ const { name, base, recursive } = def.with;
97
+ sql += `WITH ${this.expr.wrap(name)} AS (${this.select(base).sql} UNION ALL ${this.select(recursive).sql}) `;
98
+ }
99
+
100
+ // SELECT
101
+ sql += "SELECT";
102
+ if (def.distinct) {
103
+ sql += " DISTINCT";
104
+ }
105
+ // TOP
106
+ if (def.top != null) {
107
+ sql += ` TOP ${def.top}`;
108
+ }
109
+
110
+ // columns
111
+ if (def.select != null) {
112
+ const cols = Object.entries(def.select).map(
113
+ ([alias, expr]) => `${this.expr.render(expr)} AS ${this.expr.wrap(alias)}`,
114
+ );
115
+ sql += ` ${cols.join(", ")}`;
116
+ } else {
117
+ sql += " *";
118
+ }
119
+
120
+ // FROM
121
+ if (def.from != null) {
122
+ const from = this.renderFrom(def.from);
123
+ sql += ` FROM ${from} AS ${this.expr.wrap(def.as)}`;
124
+
125
+ // LOCK (ROWLOCK으로 row 수준 락 강제 - MySQL/PostgreSQL FOR UPDATE와 동일 Behavior)
126
+ if (def.lock) {
127
+ sql += " WITH (UPDLOCK, ROWLOCK)";
128
+ }
129
+ }
130
+
131
+ // JOINs
132
+ sql += this.renderJoins(def.joins);
133
+
134
+ // WHERE
135
+ sql += this.renderWhere(def.where);
136
+
137
+ // GROUP BY
138
+ sql += this.renderGroupBy(def.groupBy);
139
+
140
+ // HAVING
141
+ sql += this.renderHaving(def.having);
142
+
143
+ // ORDER BY
144
+ sql += this.renderOrderBy(def.orderBy);
145
+
146
+ // LIMIT (OFFSET...FETCH)
147
+ sql += this.renderLimit(def.limit);
148
+
149
+ return { sql };
150
+ }
151
+
152
+ //#endregion
153
+
154
+ //#region ========== DML - INSERT ==========
155
+
156
+ protected insert(def: InsertQueryDef): QueryBuildResult {
157
+ const table = this.tableName(def.table);
158
+
159
+ if (def.records.length === 0) {
160
+ throw new Error("INSERT requires at least one record.");
161
+ }
162
+
163
+ const columns = Object.keys(def.records[0]);
164
+ const colList = columns.map((c) => this.expr.wrap(c)).join(", ");
165
+
166
+ let sql = "";
167
+
168
+ // IDENTITY_INSERT ON (AI column에 explicit value 삽입 시)
169
+ if (def.overrideIdentity) {
170
+ sql += `SET IDENTITY_INSERT ${table} ON;\n`;
171
+ }
172
+
173
+ sql += `INSERT INTO ${table} (${colList})`;
174
+
175
+ // OUTPUT (MSSQL 네이티브 지원)
176
+ if (def.output != null) {
177
+ const outputCols = def.output.columns.map((c) => `INSERTED.${this.expr.wrap(c)}`).join(", ");
178
+ sql += ` OUTPUT ${outputCols}`;
179
+ }
180
+
181
+ sql += ` VALUES`;
182
+
183
+ const valuesList = def.records.map((record) => {
184
+ const values = columns.map((c) => this.expr.escapeValue(record[c]));
185
+ return `(${values.join(", ")})`;
186
+ });
187
+ sql += ` ${valuesList.join(", ")}`;
188
+
189
+ // IDENTITY_INSERT OFF
190
+ if (def.overrideIdentity) {
191
+ sql += `;\nSET IDENTITY_INSERT ${table} OFF;`;
192
+ }
193
+
194
+ // overrideIdentity 시: SET ON → results[0], INSERT → results[1], SET OFF → results[2]
195
+ return { sql, resultSetIndex: def.overrideIdentity ? 1 : undefined };
196
+ }
197
+
198
+ protected insertIfNotExists(def: InsertIfNotExistsQueryDef): QueryBuildResult {
199
+ const table = this.tableName(def.table);
200
+
201
+ const columns = Object.keys(def.record);
202
+ const colList = columns.map((c) => this.expr.wrap(c)).join(", ");
203
+ const values = columns.map((c) => this.expr.escapeValue(def.record[c])).join(", ");
204
+
205
+ // existsSelectQuery를 SELECT 1 AS _ 형태로 Render
206
+ const existsQuerySql = this.select({
207
+ ...def.existsSelectQuery,
208
+ select: { _: { type: "value", value: 1 } },
209
+ }).sql;
210
+
211
+ let sql = `INSERT INTO ${table} (${colList})`;
212
+
213
+ // OUTPUT (MSSQL: OUTPUT은 SELECT 앞에 위치해야 함)
214
+ if (def.output != null) {
215
+ const outputCols = def.output.columns.map((c) => `INSERTED.${this.expr.wrap(c)}`).join(", ");
216
+ sql += ` OUTPUT ${outputCols}`;
217
+ }
218
+
219
+ sql += ` SELECT ${values} WHERE NOT EXISTS (${existsQuerySql})`;
220
+
221
+ return { sql };
222
+ }
223
+
224
+ protected insertInto(def: InsertIntoQueryDef): QueryBuildResult {
225
+ const table = this.tableName(def.table);
226
+ const selectSql = this.select(def.recordsSelectQuery).sql;
227
+
228
+ // INSERT INTO SELECT에서 columns 추출
229
+ const selectDef = def.recordsSelectQuery;
230
+ const colList =
231
+ selectDef.select != null
232
+ ? Object.keys(selectDef.select)
233
+ .map((c) => this.expr.wrap(c))
234
+ .join(", ")
235
+ : "*";
236
+
237
+ let sql = `INSERT INTO ${table} (${colList})`;
238
+
239
+ // OUTPUT
240
+ if (def.output != null) {
241
+ const outputCols = def.output.columns.map((c) => `INSERTED.${this.expr.wrap(c)}`).join(", ");
242
+ sql += ` OUTPUT ${outputCols}`;
243
+ }
244
+
245
+ sql += ` ${selectSql}`;
246
+ return { sql };
247
+ }
248
+
249
+ //#endregion
250
+
251
+ //#region ========== DML - UPDATE ==========
252
+
253
+ protected update(def: UpdateQueryDef): QueryBuildResult {
254
+ const table = this.tableName(def.table);
255
+ const alias = this.expr.wrap(def.as);
256
+
257
+ // SET
258
+ const setParts = Object.entries(def.record).map(
259
+ ([col, e]) => `${alias}.${this.expr.wrap(col)} = ${this.expr.render(e)}`,
260
+ );
261
+
262
+ let sql = "UPDATE";
263
+ if (def.top != null) {
264
+ sql += ` TOP ${def.top}`;
265
+ }
266
+ sql += ` ${alias} SET ${setParts.join(", ")}`;
267
+
268
+ // OUTPUT
269
+ if (def.output != null) {
270
+ const outputCols = def.output.columns.map((c) => `INSERTED.${this.expr.wrap(c)}`).join(", ");
271
+ sql += ` OUTPUT ${outputCols}`;
272
+ }
273
+
274
+ sql += ` FROM ${table} AS ${alias}`;
275
+ sql += this.renderJoins(def.joins);
276
+ sql += this.renderWhere(def.where);
277
+
278
+ return { sql };
279
+ }
280
+
281
+ //#endregion
282
+
283
+ //#region ========== DML - DELETE ==========
284
+
285
+ protected delete(def: DeleteQueryDef): QueryBuildResult {
286
+ const table = this.tableName(def.table);
287
+ const alias = this.expr.wrap(def.as);
288
+
289
+ let sql = "DELETE";
290
+ if (def.top != null) {
291
+ sql += ` TOP ${def.top}`;
292
+ }
293
+ sql += ` ${alias}`;
294
+
295
+ // OUTPUT (MSSQL: DELETED for DELETE)
296
+ if (def.output != null) {
297
+ const outputCols = def.output.columns.map((c) => `DELETED.${this.expr.wrap(c)}`).join(", ");
298
+ sql += ` OUTPUT ${outputCols}`;
299
+ }
300
+
301
+ sql += ` FROM ${table} AS ${alias}`;
302
+ sql += this.renderJoins(def.joins);
303
+ sql += this.renderWhere(def.where);
304
+
305
+ return { sql };
306
+ }
307
+
308
+ //#endregion
309
+
310
+ //#region ========== DML - UPSERT ==========
311
+
312
+ protected upsert(def: UpsertQueryDef): QueryBuildResult {
313
+ // MSSQL: MERGE 사용
314
+ const table = this.tableName(def.table);
315
+ const alias = this.expr.wrap(def.existsSelectQuery.as);
316
+ const existsWhere = def.existsSelectQuery.where;
317
+
318
+ // UPDATE SET part
319
+ const updateSetParts = Object.entries(def.updateRecord).map(
320
+ ([col, e]) => `${this.expr.wrap(col)} = ${this.expr.render(e)}`,
321
+ );
322
+
323
+ // INSERT part
324
+ const insertColumns = Object.keys(def.insertRecord);
325
+ const insertColList = insertColumns.map((c) => this.expr.wrap(c)).join(", ");
326
+ const insertValues = insertColumns.map((c) => this.expr.render(def.insertRecord[c])).join(", ");
327
+
328
+ let sql = `MERGE ${table} AS ${alias}\n`;
329
+ sql += `USING (SELECT 1 AS [_]) AS [_src] ON `;
330
+ sql +=
331
+ existsWhere != null && existsWhere.length > 0 ? this.expr.renderWhere(existsWhere) : "1=0";
332
+
333
+ if (updateSetParts.length > 0) {
334
+ sql += `\nWHEN MATCHED THEN UPDATE SET ${updateSetParts.join(", ")}`;
335
+ }
336
+
337
+ sql += `\nWHEN NOT MATCHED THEN INSERT (${insertColList}) VALUES (${insertValues})`;
338
+
339
+ // OUTPUT
340
+ if (def.output != null) {
341
+ const outputCols = def.output.columns.map((c) => `INSERTED.${this.expr.wrap(c)}`).join(", ");
342
+ sql += `\nOUTPUT ${outputCols}`;
343
+ }
344
+
345
+ sql += ";";
346
+ return { sql };
347
+ }
348
+
349
+ //#endregion
350
+
351
+ //#region ========== DDL - Table ==========
352
+
353
+ protected createTable(def: CreateTableQueryDef): QueryBuildResult {
354
+ const table = this.tableName(def.table);
355
+
356
+ const colDefs = def.columns.map((col) => {
357
+ let colSql = `${this.expr.wrap(col.name)} ${this.expr.renderDataType(col.dataType)}`;
358
+
359
+ // nullable: true → NULL, else → NOT NULL
360
+ if (col.nullable === true) {
361
+ colSql += " NULL";
362
+ } else {
363
+ colSql += " NOT NULL";
364
+ }
365
+
366
+ if (col.autoIncrement) {
367
+ colSql += " IDENTITY(1,1)";
368
+ }
369
+
370
+ if (col.default !== undefined) {
371
+ colSql += ` DEFAULT ${this.expr.escapeValue(col.default)}`;
372
+ }
373
+
374
+ return colSql;
375
+ });
376
+
377
+ // Primary Key with CONSTRAINT name
378
+ if (def.primaryKey != null && def.primaryKey.length > 0) {
379
+ const pkCols = def.primaryKey.map((c) => this.expr.wrap(c)).join(", ");
380
+ const pkName = this.expr.wrap(`PK_${def.table.name}`);
381
+ colDefs.push(`CONSTRAINT ${pkName} PRIMARY KEY (${pkCols})`);
382
+ }
383
+
384
+ return { sql: `CREATE TABLE ${table} (\n ${colDefs.join(",\n ")}\n)` };
385
+ }
386
+
387
+ protected dropTable(def: DropTableQueryDef): QueryBuildResult {
388
+ return { sql: `DROP TABLE ${this.tableName(def.table)}` };
389
+ }
390
+
391
+ protected renameTable(def: RenameTableQueryDef): QueryBuildResult {
392
+ // MSSQL: sp_rename 사용
393
+ const tableName = this.expr.escapeString(this.tableName(def.table));
394
+ const newName = this.expr.escapeString(def.newName);
395
+ return { sql: `EXEC sp_rename '${tableName}', '${newName}'` };
396
+ }
397
+
398
+ protected truncate(def: TruncateQueryDef): QueryBuildResult {
399
+ // MSSQL: TRUNCATE는 IDENTITY automatic 리셋
400
+ return { sql: `TRUNCATE TABLE ${this.tableName(def.table)}` };
401
+ }
402
+
403
+ //#endregion
404
+
405
+ //#region ========== DDL - Column ==========
406
+
407
+ protected addColumn(def: AddColumnQueryDef): QueryBuildResult {
408
+ const table = this.tableName(def.table);
409
+ const col = def.column;
410
+
411
+ let colSql = `${this.expr.wrap(col.name)} ${this.expr.renderDataType(col.dataType)}`;
412
+
413
+ // nullable: true → NULL, else → NOT NULL
414
+ if (col.nullable === true) {
415
+ colSql += " NULL";
416
+ } else {
417
+ colSql += " NOT NULL";
418
+ }
419
+
420
+ if (col.autoIncrement) {
421
+ colSql += " IDENTITY(1,1)";
422
+ }
423
+
424
+ if (col.default !== undefined) {
425
+ colSql += ` DEFAULT ${this.expr.escapeValue(col.default)}`;
426
+ }
427
+
428
+ return { sql: `ALTER TABLE ${table} ADD ${colSql}` };
429
+ }
430
+
431
+ protected dropColumn(def: DropColumnQueryDef): QueryBuildResult {
432
+ return {
433
+ sql: `ALTER TABLE ${this.tableName(def.table)} DROP COLUMN ${this.expr.wrap(def.column)}`,
434
+ };
435
+ }
436
+
437
+ protected modifyColumn(def: ModifyColumnQueryDef): QueryBuildResult {
438
+ const table = this.tableName(def.table);
439
+ const col = def.column;
440
+
441
+ let colSql = `${this.expr.wrap(col.name)} ${this.expr.renderDataType(col.dataType)}`;
442
+
443
+ // nullable: true → NULL, else → NOT NULL
444
+ if (col.nullable === true) {
445
+ colSql += " NULL";
446
+ } else {
447
+ colSql += " NOT NULL";
448
+ }
449
+
450
+ // MSSQL: ALTER COLUMN (IDENTITY와 DEFAULT는 별도 processing 필요)
451
+ return { sql: `ALTER TABLE ${table} ALTER COLUMN ${colSql}` };
452
+ }
453
+
454
+ protected renameColumn(def: RenameColumnQueryDef): QueryBuildResult {
455
+ const table = this.tableName(def.table);
456
+ // MSSQL: sp_rename 사용
457
+ const tableCol = this.expr.escapeString(`${table}.${def.column}`);
458
+ const newName = this.expr.escapeString(def.newName);
459
+ return { sql: `EXEC sp_rename '${tableCol}', '${newName}', 'COLUMN'` };
460
+ }
461
+
462
+ //#endregion
463
+
464
+ //#region ========== DDL - Constraint ==========
465
+
466
+ protected addPk(def: AddPkQueryDef): QueryBuildResult {
467
+ const table = this.tableName(def.table);
468
+ const cols = def.columns.map((c) => this.expr.wrap(c)).join(", ");
469
+ const pkName = `PK_${def.table.name}`;
470
+ return {
471
+ sql: `ALTER TABLE ${table} ADD CONSTRAINT ${this.expr.wrap(pkName)} PRIMARY KEY (${cols})`,
472
+ };
473
+ }
474
+
475
+ protected dropPk(def: DropPkQueryDef): QueryBuildResult {
476
+ const table = this.tableName(def.table);
477
+ const pkName = `PK_${def.table.name}`;
478
+ return { sql: `ALTER TABLE ${table} DROP CONSTRAINT ${this.expr.wrap(pkName)}` };
479
+ }
480
+
481
+ protected addFk(def: AddFkQueryDef): QueryBuildResult {
482
+ const table = this.tableName(def.table);
483
+ const fk = def.foreignKey;
484
+ const fkCols = fk.fkColumns.map((c) => this.expr.wrap(c)).join(", ");
485
+ const targetTable = this.tableName(fk.targetTable);
486
+ const targetCols = fk.targetPkColumns.map((c) => this.expr.wrap(c)).join(", ");
487
+
488
+ let sql = `ALTER TABLE ${table} ADD CONSTRAINT ${this.expr.wrap(fk.name)} FOREIGN KEY (${fkCols}) REFERENCES ${targetTable} (${targetCols})`;
489
+
490
+ // MSSQL/PostgreSQL: FK용 Index 별도 Generate 필요
491
+ const idxName = `IDX_${def.table.name}_${fk.name.replace(/^FK_/, "")}`;
492
+ sql += `;\nCREATE INDEX ${this.expr.wrap(idxName)} ON ${table} (${fkCols});`;
493
+
494
+ return { sql };
495
+ }
496
+
497
+ protected dropFk(def: DropFkQueryDef): QueryBuildResult {
498
+ return {
499
+ sql: `ALTER TABLE ${this.tableName(def.table)} DROP CONSTRAINT ${this.expr.wrap(def.foreignKey)}`,
500
+ };
501
+ }
502
+
503
+ protected addIdx(def: AddIdxQueryDef): QueryBuildResult {
504
+ const table = this.tableName(def.table);
505
+ const idx = def.index;
506
+ const cols = idx.columns.map((c) => `${this.expr.wrap(c.name)} ${c.orderBy}`).join(", ");
507
+ const unique = idx.unique ? "UNIQUE " : "";
508
+ return { sql: `CREATE ${unique}INDEX ${this.expr.wrap(idx.name)} ON ${table} (${cols})` };
509
+ }
510
+
511
+ protected dropIdx(def: DropIdxQueryDef): QueryBuildResult {
512
+ return { sql: `DROP INDEX ${this.expr.wrap(def.index)} ON ${this.tableName(def.table)}` };
513
+ }
514
+
515
+ //#endregion
516
+
517
+ //#region ========== DDL - View/Procedure ==========
518
+
519
+ protected createView(def: CreateViewQueryDef): QueryBuildResult {
520
+ const view = this.tableName(def.view);
521
+ const selectSql = this.select(def.queryDef).sql;
522
+ // MSSQL: CREATE OR ALTER VIEW (2016 SP1+)
523
+ return { sql: `CREATE OR ALTER VIEW ${view} AS ${selectSql}` };
524
+ }
525
+
526
+ protected dropView(def: DropViewQueryDef): QueryBuildResult {
527
+ return { sql: `DROP VIEW IF EXISTS ${this.tableName(def.view)}` };
528
+ }
529
+
530
+ protected createProc(def: CreateProcQueryDef): QueryBuildResult {
531
+ const proc = this.tableName(def.procedure);
532
+
533
+ // params processing
534
+ const paramList =
535
+ def.params
536
+ ?.map((p) => {
537
+ let sql = `@${p.name} ${this.expr.renderDataType(p.dataType)}`;
538
+ if (p.default !== undefined) {
539
+ sql += ` = ${this.expr.escapeValue(p.default)}`;
540
+ }
541
+ return sql;
542
+ })
543
+ .join(", ") ?? "";
544
+
545
+ let sql = `CREATE OR ALTER PROCEDURE ${proc}`;
546
+ if (paramList) {
547
+ sql += ` ${paramList}`;
548
+ }
549
+ sql += `\nAS\nBEGIN\n`;
550
+ sql += `SET NOCOUNT ON;\n`;
551
+ sql += def.query;
552
+ sql += `\nEND`;
553
+
554
+ return { sql };
555
+ }
556
+
557
+ protected dropProc(def: DropProcQueryDef): QueryBuildResult {
558
+ return { sql: `DROP PROCEDURE IF EXISTS ${this.tableName(def.procedure)}` };
559
+ }
560
+
561
+ protected execProc(def: ExecProcQueryDef): QueryBuildResult {
562
+ const proc = this.tableName(def.procedure);
563
+ if (def.params == null || Object.keys(def.params).length === 0) {
564
+ return { sql: `EXEC ${proc}` };
565
+ }
566
+ const params = Object.values(def.params)
567
+ .map((p) => this.expr.render(p))
568
+ .join(", ");
569
+ return { sql: `EXEC ${proc} ${params}` };
570
+ }
571
+
572
+ //#endregion
573
+
574
+ //#region ========== Utils ==========
575
+
576
+ protected clearSchema(def: ClearSchemaQueryDef): QueryBuildResult {
577
+ // SQL Injection 방지: 식별자 유효성 Validation
578
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(def.database)) {
579
+ throw new Error(`Invalid database name: ${def.database}`);
580
+ }
581
+ const schemaName = def.schema ?? "dbo";
582
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(schemaName)) {
583
+ throw new Error(`Invalid schema name: ${schemaName}`);
584
+ }
585
+
586
+ const db = this.expr.wrap(def.database);
587
+ const schema = this.expr.escapeString(schemaName);
588
+ return {
589
+ sql: `
590
+ DECLARE @sql NVARCHAR(MAX);
591
+ SET @sql = N'';
592
+
593
+ -- FK constraint Delete
594
+ SELECT @sql = @sql + N'ALTER TABLE ' + QUOTENAME(OBJECT_SCHEMA_NAME(parent_object_id)) + '.' + QUOTENAME(OBJECT_NAME(parent_object_id)) + N' DROP CONSTRAINT ' + QUOTENAME(name) + N';' + CHAR(13)
595
+ FROM ${db}.sys.foreign_keys
596
+ WHERE OBJECT_SCHEMA_NAME(parent_object_id) = '${schema}';
597
+
598
+ -- Drop table
599
+ SELECT @sql = @sql + N'DROP TABLE ' + QUOTENAME(SCHEMA_NAME(schema_id)) + '.' + QUOTENAME(name) + N';' + CHAR(13)
600
+ FROM ${db}.sys.tables
601
+ WHERE SCHEMA_NAME(schema_id) = '${schema}';
602
+
603
+ -- Drop view
604
+ SELECT @sql = @sql + N'DROP VIEW ' + QUOTENAME(SCHEMA_NAME(schema_id)) + '.' + QUOTENAME(name) + N';' + CHAR(13)
605
+ FROM ${db}.sys.views
606
+ WHERE schema_id = SCHEMA_ID('${schema}');
607
+
608
+ -- Procedure Delete
609
+ SELECT @sql = @sql + N'DROP PROCEDURE ' + QUOTENAME(SCHEMA_NAME(schema_id)) + '.' + QUOTENAME(name) + N';' + CHAR(13)
610
+ FROM ${db}.sys.procedures
611
+ WHERE SCHEMA_NAME(schema_id) = '${schema}';
612
+
613
+ EXEC sp_executesql @sql;`,
614
+ };
615
+ }
616
+
617
+ protected schemaExists(def: SchemaExistsQueryDef): QueryBuildResult {
618
+ // SQL Injection 방지: 식별자 유효성 Validation
619
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(def.database)) {
620
+ throw new Error(`Invalid database name: ${def.database}`);
621
+ }
622
+ const schemaName = def.schema ?? "dbo";
623
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(schemaName)) {
624
+ throw new Error(`Invalid schema name: ${schemaName}`);
625
+ }
626
+
627
+ const dbName = this.expr.escapeString(def.database);
628
+ const schema = this.expr.escapeString(schemaName);
629
+ // MSSQL: database 존재 확인 후 schema 확인 (동적 SQL 사용)
630
+ return {
631
+ sql: `DECLARE @result NVARCHAR(MAX) = NULL;
632
+ IF EXISTS (SELECT 1 FROM sys.databases WHERE name = '${dbName}')
633
+ BEGIN
634
+ DECLARE @sql NVARCHAR(MAX) = N'SELECT @result = name FROM ' + QUOTENAME('${dbName}') + N'.sys.schemas WHERE name = ''${schema}''';
635
+ EXEC sp_executesql @sql, N'@result NVARCHAR(MAX) OUTPUT', @result OUTPUT;
636
+ END
637
+ SELECT @result AS name WHERE @result IS NOT NULL`,
638
+ };
639
+ }
640
+
641
+ protected switchFk(def: SwitchFkQueryDef): QueryBuildResult {
642
+ const table = this.tableName(def.table);
643
+ if (def.switch === "on") {
644
+ return { sql: `ALTER TABLE ${table} WITH CHECK CHECK CONSTRAINT ALL` };
645
+ }
646
+ return { sql: `ALTER TABLE ${table} NOCHECK CONSTRAINT ALL` };
647
+ }
648
+
649
+ //#endregion
650
+ }