@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.
- package/README.md +54 -1447
- package/dist/create-db-context.d.ts +10 -10
- package/dist/create-db-context.js +9 -9
- package/dist/create-db-context.js.map +1 -1
- package/dist/ddl/column-ddl.d.ts +4 -4
- package/dist/ddl/initialize.d.ts +17 -17
- package/dist/ddl/initialize.js +2 -2
- package/dist/ddl/initialize.js.map +1 -1
- package/dist/ddl/relation-ddl.d.ts +6 -6
- package/dist/ddl/schema-ddl.d.ts +4 -4
- package/dist/ddl/table-ddl.d.ts +24 -24
- package/dist/ddl/table-ddl.js +4 -4
- package/dist/ddl/table-ddl.js.map +1 -1
- package/dist/errors/db-transaction-error.d.ts +15 -15
- package/dist/errors/db-transaction-error.d.ts.map +1 -1
- package/dist/exec/executable.d.ts +23 -23
- package/dist/exec/executable.js +3 -3
- package/dist/exec/executable.js.map +1 -1
- package/dist/exec/queryable.d.ts +160 -160
- package/dist/exec/queryable.js +119 -119
- package/dist/exec/queryable.js.map +1 -1
- package/dist/exec/search-parser.d.ts +37 -37
- package/dist/exec/search-parser.d.ts.map +1 -1
- package/dist/expr/expr-unit.d.ts +4 -4
- package/dist/expr/expr.d.ts +257 -257
- package/dist/expr/expr.js +265 -265
- package/dist/expr/expr.js.map +1 -1
- package/dist/query-builder/base/expr-renderer-base.d.ts +9 -9
- package/dist/query-builder/base/expr-renderer-base.js +2 -2
- package/dist/query-builder/base/expr-renderer-base.js.map +1 -1
- package/dist/query-builder/base/query-builder-base.d.ts +26 -26
- package/dist/query-builder/base/query-builder-base.d.ts.map +1 -1
- package/dist/query-builder/base/query-builder-base.js +22 -22
- package/dist/query-builder/base/query-builder-base.js.map +1 -1
- package/dist/query-builder/mssql/mssql-expr-renderer.d.ts +4 -4
- package/dist/query-builder/mssql/mssql-expr-renderer.d.ts.map +1 -1
- package/dist/query-builder/mssql/mssql-expr-renderer.js +18 -18
- package/dist/query-builder/mssql/mssql-expr-renderer.js.map +1 -1
- package/dist/query-builder/mssql/mssql-query-builder.d.ts +2 -2
- package/dist/query-builder/mssql/mssql-query-builder.d.ts.map +1 -1
- package/dist/query-builder/mssql/mssql-query-builder.js +11 -11
- package/dist/query-builder/mssql/mssql-query-builder.js.map +1 -1
- package/dist/query-builder/mysql/mysql-expr-renderer.d.ts +4 -4
- package/dist/query-builder/mysql/mysql-expr-renderer.d.ts.map +1 -1
- package/dist/query-builder/mysql/mysql-expr-renderer.js +17 -17
- package/dist/query-builder/mysql/mysql-expr-renderer.js.map +1 -1
- package/dist/query-builder/mysql/mysql-query-builder.d.ts +8 -8
- package/dist/query-builder/mysql/mysql-query-builder.d.ts.map +1 -1
- package/dist/query-builder/mysql/mysql-query-builder.js +5 -5
- package/dist/query-builder/mysql/mysql-query-builder.js.map +1 -1
- package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts +4 -4
- package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts.map +1 -1
- package/dist/query-builder/postgresql/postgresql-expr-renderer.js +17 -17
- package/dist/query-builder/postgresql/postgresql-expr-renderer.js.map +1 -1
- package/dist/query-builder/postgresql/postgresql-query-builder.d.ts +5 -5
- package/dist/query-builder/postgresql/postgresql-query-builder.d.ts.map +1 -1
- package/dist/query-builder/postgresql/postgresql-query-builder.js +8 -8
- package/dist/query-builder/postgresql/postgresql-query-builder.js.map +1 -1
- package/dist/query-builder/query-builder.d.ts +1 -1
- package/dist/schema/factory/column-builder.d.ts +79 -79
- package/dist/schema/factory/column-builder.js +42 -42
- package/dist/schema/factory/index-builder.d.ts +39 -39
- package/dist/schema/factory/index-builder.js +26 -26
- package/dist/schema/factory/relation-builder.d.ts +99 -99
- package/dist/schema/factory/relation-builder.d.ts.map +1 -1
- package/dist/schema/factory/relation-builder.js +38 -38
- package/dist/schema/procedure-builder.d.ts +49 -49
- package/dist/schema/procedure-builder.d.ts.map +1 -1
- package/dist/schema/procedure-builder.js +33 -33
- package/dist/schema/table-builder.d.ts +59 -59
- package/dist/schema/table-builder.d.ts.map +1 -1
- package/dist/schema/table-builder.js +43 -43
- package/dist/schema/view-builder.d.ts +49 -49
- package/dist/schema/view-builder.d.ts.map +1 -1
- package/dist/schema/view-builder.js +32 -32
- package/dist/types/column.d.ts +22 -22
- package/dist/types/column.js +1 -1
- package/dist/types/column.js.map +1 -1
- package/dist/types/db.d.ts +40 -40
- package/dist/types/expr.d.ts +59 -59
- package/dist/types/expr.d.ts.map +1 -1
- package/dist/types/query-def.d.ts +44 -44
- package/dist/types/query-def.d.ts.map +1 -1
- package/dist/utils/result-parser.d.ts +11 -11
- package/dist/utils/result-parser.js +3 -3
- package/dist/utils/result-parser.js.map +1 -1
- package/package.json +5 -5
- package/src/create-db-context.ts +20 -20
- package/src/ddl/column-ddl.ts +4 -4
- package/src/ddl/initialize.ts +259 -259
- package/src/ddl/relation-ddl.ts +89 -89
- package/src/ddl/schema-ddl.ts +4 -4
- package/src/ddl/table-ddl.ts +189 -189
- package/src/errors/db-transaction-error.ts +13 -13
- package/src/exec/executable.ts +25 -25
- package/src/exec/queryable.ts +2033 -2033
- package/src/exec/search-parser.ts +57 -57
- package/src/expr/expr-unit.ts +4 -4
- package/src/expr/expr.ts +2140 -2140
- package/src/query-builder/base/expr-renderer-base.ts +237 -237
- package/src/query-builder/base/query-builder-base.ts +213 -213
- package/src/query-builder/mssql/mssql-expr-renderer.ts +607 -607
- package/src/query-builder/mssql/mssql-query-builder.ts +650 -650
- package/src/query-builder/mysql/mysql-expr-renderer.ts +613 -613
- package/src/query-builder/mysql/mysql-query-builder.ts +759 -759
- package/src/query-builder/postgresql/postgresql-expr-renderer.ts +611 -611
- package/src/query-builder/postgresql/postgresql-query-builder.ts +686 -686
- package/src/query-builder/query-builder.ts +19 -19
- package/src/schema/factory/column-builder.ts +423 -423
- package/src/schema/factory/index-builder.ts +164 -164
- package/src/schema/factory/relation-builder.ts +453 -453
- package/src/schema/procedure-builder.ts +232 -232
- package/src/schema/table-builder.ts +319 -319
- package/src/schema/view-builder.ts +221 -221
- package/src/types/column.ts +188 -188
- package/src/types/db.ts +208 -208
- package/src/types/expr.ts +697 -697
- package/src/types/query-def.ts +513 -513
- package/src/utils/result-parser.ts +458 -458
- package/tests/db-context/create-db-context.spec.ts +224 -0
- package/tests/db-context/define-db-context.spec.ts +68 -0
- package/tests/ddl/basic.expected.ts +341 -0
- package/tests/ddl/basic.spec.ts +714 -0
- package/tests/ddl/column-builder.expected.ts +310 -0
- package/tests/ddl/column-builder.spec.ts +637 -0
- package/tests/ddl/index-builder.expected.ts +38 -0
- package/tests/ddl/index-builder.spec.ts +202 -0
- package/tests/ddl/procedure-builder.expected.ts +52 -0
- package/tests/ddl/procedure-builder.spec.ts +234 -0
- package/tests/ddl/relation-builder.expected.ts +36 -0
- package/tests/ddl/relation-builder.spec.ts +372 -0
- package/tests/ddl/table-builder.expected.ts +113 -0
- package/tests/ddl/table-builder.spec.ts +433 -0
- package/tests/ddl/view-builder.expected.ts +38 -0
- package/tests/ddl/view-builder.spec.ts +176 -0
- package/tests/dml/delete.expected.ts +96 -0
- package/tests/dml/delete.spec.ts +160 -0
- package/tests/dml/insert.expected.ts +192 -0
- package/tests/dml/insert.spec.ts +288 -0
- package/tests/dml/update.expected.ts +176 -0
- package/tests/dml/update.spec.ts +318 -0
- package/tests/dml/upsert.expected.ts +215 -0
- package/tests/dml/upsert.spec.ts +242 -0
- package/tests/errors/queryable-errors.spec.ts +177 -0
- package/tests/escape.spec.ts +100 -0
- package/tests/examples/pivot.expected.ts +211 -0
- package/tests/examples/pivot.spec.ts +533 -0
- package/tests/examples/sampling.expected.ts +69 -0
- package/tests/examples/sampling.spec.ts +105 -0
- package/tests/examples/unpivot.expected.ts +120 -0
- package/tests/examples/unpivot.spec.ts +226 -0
- package/tests/exec/search-parser.spec.ts +283 -0
- package/tests/executable/basic.expected.ts +18 -0
- package/tests/executable/basic.spec.ts +54 -0
- package/tests/expr/comparison.expected.ts +282 -0
- package/tests/expr/comparison.spec.ts +400 -0
- package/tests/expr/conditional.expected.ts +134 -0
- package/tests/expr/conditional.spec.ts +276 -0
- package/tests/expr/date.expected.ts +332 -0
- package/tests/expr/date.spec.ts +526 -0
- package/tests/expr/math.expected.ts +62 -0
- package/tests/expr/math.spec.ts +106 -0
- package/tests/expr/string.expected.ts +218 -0
- package/tests/expr/string.spec.ts +356 -0
- package/tests/expr/utility.expected.ts +147 -0
- package/tests/expr/utility.spec.ts +182 -0
- package/tests/select/basic.expected.ts +322 -0
- package/tests/select/basic.spec.ts +502 -0
- package/tests/select/filter.expected.ts +357 -0
- package/tests/select/filter.spec.ts +1068 -0
- package/tests/select/group.expected.ts +169 -0
- package/tests/select/group.spec.ts +244 -0
- package/tests/select/join.expected.ts +582 -0
- package/tests/select/join.spec.ts +805 -0
- package/tests/select/order.expected.ts +150 -0
- package/tests/select/order.spec.ts +189 -0
- package/tests/select/recursive-cte.expected.ts +244 -0
- package/tests/select/recursive-cte.spec.ts +514 -0
- package/tests/select/result-meta.spec.ts +270 -0
- package/tests/select/subquery.expected.ts +363 -0
- package/tests/select/subquery.spec.ts +537 -0
- package/tests/select/view.expected.ts +155 -0
- package/tests/select/view.spec.ts +235 -0
- package/tests/select/window.expected.ts +345 -0
- package/tests/select/window.spec.ts +618 -0
- package/tests/setup/MockExecutor.ts +18 -0
- package/tests/setup/TestDbContext.ts +59 -0
- package/tests/setup/models/Company.ts +13 -0
- package/tests/setup/models/Employee.ts +10 -0
- package/tests/setup/models/MonthlySales.ts +11 -0
- package/tests/setup/models/Post.ts +16 -0
- package/tests/setup/models/Sales.ts +10 -0
- package/tests/setup/models/User.ts +19 -0
- package/tests/setup/procedure/GetAllUsers.ts +9 -0
- package/tests/setup/procedure/GetUserById.ts +12 -0
- package/tests/setup/test-utils.ts +72 -0
- package/tests/setup/views/ActiveUsers.ts +8 -0
- package/tests/setup/views/UserSummary.ts +11 -0
- package/tests/types/nullable-queryable-record.spec.ts +145 -0
- package/tests/utils/result-parser-perf.spec.ts +210 -0
- package/tests/utils/result-parser.spec.ts +701 -0
- package/docs/expressions.md +0 -172
- package/docs/queries.md +0 -444
- 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으로
|
|
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(
|
|
580
|
-
}
|
|
581
|
-
const schemaName = def.schema ?? "dbo";
|
|
582
|
-
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(schemaName)) {
|
|
583
|
-
throw new Error(
|
|
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(
|
|
621
|
-
}
|
|
622
|
-
const schemaName = def.schema ?? "dbo";
|
|
623
|
-
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(schemaName)) {
|
|
624
|
-
throw new Error(
|
|
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
|
+
}
|