@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,759 +1,759 @@
|
|
|
1
|
-
import { Uuid } from "@simplysm/core-common";
|
|
2
|
-
import type {
|
|
3
|
-
AddColumnQueryDef,
|
|
4
|
-
AddFkQueryDef,
|
|
5
|
-
AddIdxQueryDef,
|
|
6
|
-
AddPkQueryDef,
|
|
7
|
-
ClearSchemaQueryDef,
|
|
8
|
-
CreateProcQueryDef,
|
|
9
|
-
CreateTableQueryDef,
|
|
10
|
-
CreateViewQueryDef,
|
|
11
|
-
SchemaExistsQueryDef,
|
|
12
|
-
DeleteQueryDef,
|
|
13
|
-
DropColumnQueryDef,
|
|
14
|
-
DropFkQueryDef,
|
|
15
|
-
DropIdxQueryDef,
|
|
16
|
-
DropPkQueryDef,
|
|
17
|
-
DropProcQueryDef,
|
|
18
|
-
DropTableQueryDef,
|
|
19
|
-
DropViewQueryDef,
|
|
20
|
-
ExecProcQueryDef,
|
|
21
|
-
InsertIfNotExistsQueryDef,
|
|
22
|
-
InsertIntoQueryDef,
|
|
23
|
-
InsertQueryDef,
|
|
24
|
-
ModifyColumnQueryDef,
|
|
25
|
-
QueryDefObjectName,
|
|
26
|
-
RenameColumnQueryDef,
|
|
27
|
-
RenameTableQueryDef,
|
|
28
|
-
SelectQueryDef,
|
|
29
|
-
SelectQueryDefJoin,
|
|
30
|
-
SwitchFkQueryDef,
|
|
31
|
-
TruncateQueryDef,
|
|
32
|
-
UpdateQueryDef,
|
|
33
|
-
UpsertQueryDef,
|
|
34
|
-
} from "../../types/query-def";
|
|
35
|
-
import type { QueryBuildResult } from "../../types/db";
|
|
36
|
-
import { QueryBuilderBase } from "../base/query-builder-base";
|
|
37
|
-
import { MysqlExprRenderer } from "./mysql-expr-renderer";
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* MySQL QueryBuilder
|
|
41
|
-
*
|
|
42
|
-
* MySQL 특이사항:
|
|
43
|
-
* - OUTPUT 미지원: multi-statement 패턴으로 우회 (INSERT + SET @var + SELECT)
|
|
44
|
-
* - INSERT OUTPUT: LAST_INSERT_ID()로 AI
|
|
45
|
-
* - UPDATE/UPSERT OUTPUT: WHERE
|
|
46
|
-
* - DELETE OUTPUT:
|
|
47
|
-
* - switchFk: 전역 설정 (SET FOREIGN_KEY_CHECKS),
|
|
48
|
-
* - FK
|
|
49
|
-
*/
|
|
50
|
-
export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
51
|
-
protected expr = new MysqlExprRenderer((def) => this.select(def).sql);
|
|
52
|
-
|
|
53
|
-
//#region ========== 유틸리티 ==========
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
protected tableName(obj: QueryDefObjectName): string {
|
|
57
|
-
if (obj.database != null) {
|
|
58
|
-
return `${this.expr.wrap(obj.database)}.${this.expr.wrap(obj.name)}`;
|
|
59
|
-
}
|
|
60
|
-
return this.expr.wrap(obj.name);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** LIMIT 절
|
|
64
|
-
protected renderLimit(limit: [number, number] | undefined, top: number | undefined): string {
|
|
65
|
-
if (limit != null) {
|
|
66
|
-
const [offset, count] = limit;
|
|
67
|
-
return ` LIMIT ${offset}, ${count}`;
|
|
68
|
-
}
|
|
69
|
-
if (top != null) {
|
|
70
|
-
return ` LIMIT ${top}`;
|
|
71
|
-
}
|
|
72
|
-
return "";
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
protected renderJoin(join: SelectQueryDefJoin): string {
|
|
76
|
-
const alias = this.expr.wrap(join.as);
|
|
77
|
-
|
|
78
|
-
// LATERAL JOIN 필요 여부 감지
|
|
79
|
-
if (this.needsLateral(join)) {
|
|
80
|
-
// from이 배열(UNION ALL)이면 renderFrom(join.from),
|
|
81
|
-
// 그 외(orderBy, top, select 등)면 renderFrom(join)으로
|
|
82
|
-
const from = Array.isArray(join.from) ? this.renderFrom(join.from) : this.renderFrom(join);
|
|
83
|
-
return ` LEFT OUTER JOIN LATERAL ${from} AS ${alias} ON TRUE`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// 일반 JOIN
|
|
87
|
-
const from = this.renderFrom(join.from);
|
|
88
|
-
const where =
|
|
89
|
-
join.where != null && join.where.length > 0
|
|
90
|
-
? ` ON ${this.expr.renderWhere(join.where)}`
|
|
91
|
-
: " ON TRUE";
|
|
92
|
-
return ` LEFT OUTER JOIN ${from} AS ${alias}${where}`;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
//#endregion
|
|
96
|
-
|
|
97
|
-
//#region ========== DML - SELECT ==========
|
|
98
|
-
|
|
99
|
-
protected select(def: SelectQueryDef): QueryBuildResult {
|
|
100
|
-
// WITH (CTE)
|
|
101
|
-
let sql = "";
|
|
102
|
-
if (def.with != null) {
|
|
103
|
-
const { name, base, recursive } = def.with;
|
|
104
|
-
sql += `WITH ${this.expr.wrap(name)} AS (${this.select(base).sql} UNION ALL ${this.select(recursive).sql}) `;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// SELECT
|
|
108
|
-
sql += "SELECT";
|
|
109
|
-
if (def.distinct) {
|
|
110
|
-
sql += " DISTINCT";
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// columns
|
|
114
|
-
if (def.select != null) {
|
|
115
|
-
const cols = Object.entries(def.select).map(
|
|
116
|
-
([alias, expr]) => `${this.expr.render(expr)} AS ${this.expr.wrap(alias)}`,
|
|
117
|
-
);
|
|
118
|
-
sql += ` ${cols.join(", ")}`;
|
|
119
|
-
} else {
|
|
120
|
-
sql += " *";
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// FROM
|
|
124
|
-
if (def.from != null) {
|
|
125
|
-
const from = this.renderFrom(def.from);
|
|
126
|
-
sql += ` FROM ${from} AS ${this.expr.wrap(def.as)}`;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// LOCK
|
|
130
|
-
if (def.lock) {
|
|
131
|
-
// MySQL에서는 SELECT ... FOR UPDATE (마지막에 붙임)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// JOINs
|
|
135
|
-
sql += this.renderJoins(def.joins);
|
|
136
|
-
|
|
137
|
-
// WHERE
|
|
138
|
-
sql += this.renderWhere(def.where);
|
|
139
|
-
|
|
140
|
-
// GROUP BY
|
|
141
|
-
sql += this.renderGroupBy(def.groupBy);
|
|
142
|
-
|
|
143
|
-
// HAVING
|
|
144
|
-
sql += this.renderHaving(def.having);
|
|
145
|
-
|
|
146
|
-
// ORDER BY
|
|
147
|
-
sql += this.renderOrderBy(def.orderBy);
|
|
148
|
-
|
|
149
|
-
// LIMIT
|
|
150
|
-
sql += this.renderLimit(def.limit, def.top);
|
|
151
|
-
|
|
152
|
-
// LOCK (FOR UPDATE at end)
|
|
153
|
-
if (def.lock) {
|
|
154
|
-
sql += " FOR UPDATE";
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return { sql };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
//#endregion
|
|
161
|
-
|
|
162
|
-
//#region ========== DML - INSERT ==========
|
|
163
|
-
|
|
164
|
-
protected insert(def: InsertQueryDef): QueryBuildResult {
|
|
165
|
-
const table = this.tableName(def.table);
|
|
166
|
-
|
|
167
|
-
if (def.records.length === 0) {
|
|
168
|
-
throw new Error("INSERT
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const columns = Object.keys(def.records[0]);
|
|
172
|
-
const colList = columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
173
|
-
|
|
174
|
-
// OUTPUT 불필요: 단순 배치 INSERT
|
|
175
|
-
if (def.output == null) {
|
|
176
|
-
const valuesList = def.records.map((record) => {
|
|
177
|
-
const values = columns.map((c) => this.expr.escapeValue(record[c]));
|
|
178
|
-
return `(${values.join(", ")})`;
|
|
179
|
-
});
|
|
180
|
-
return { sql: `INSERT INTO ${table} (${colList}) VALUES ${valuesList.join(", ")}` };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// OUTPUT 필요: multi-statement로 INSERT + SELECT 실행
|
|
184
|
-
//
|
|
185
|
-
// → resultSetIndex=1, resultSetStride=2 로 SELECT 결과만 추출
|
|
186
|
-
const output = def.output;
|
|
187
|
-
const outputCols = output.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
188
|
-
const statements: string[] = [];
|
|
189
|
-
|
|
190
|
-
for (const record of def.records) {
|
|
191
|
-
const values = columns.map((c) => this.expr.escapeValue(record[c])).join(", ");
|
|
192
|
-
statements.push(`INSERT INTO ${table} (${colList}) VALUES (${values})`);
|
|
193
|
-
|
|
194
|
-
// PK로 SELECT (aiColName이면 LAST_INSERT_ID() 사용)
|
|
195
|
-
const whereForSelect = output.pkColNames.map((pk) => {
|
|
196
|
-
const wrappedPk = this.expr.wrap(pk);
|
|
197
|
-
if (pk === output.aiColName) {
|
|
198
|
-
return `${wrappedPk} = LAST_INSERT_ID()`;
|
|
199
|
-
}
|
|
200
|
-
return `${wrappedPk} = ${this.expr.escapeValue(record[pk])}`;
|
|
201
|
-
});
|
|
202
|
-
statements.push(`SELECT ${outputCols} FROM ${table} WHERE ${whereForSelect.join(" AND ")}`);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return {
|
|
206
|
-
sql: statements.join(";\n"),
|
|
207
|
-
resultSetIndex: 1,
|
|
208
|
-
resultSetStride: 2,
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
protected insertIfNotExists(def: InsertIfNotExistsQueryDef): QueryBuildResult {
|
|
213
|
-
const table = this.tableName(def.table);
|
|
214
|
-
|
|
215
|
-
const columns = Object.keys(def.record);
|
|
216
|
-
const colList = columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
217
|
-
const values = columns.map((c) => this.expr.escapeValue(def.record[c])).join(", ");
|
|
218
|
-
|
|
219
|
-
// existsSelectQuery를 SELECT 1 AS _ 형태로
|
|
220
|
-
const existsQuerySql = this.select({
|
|
221
|
-
...def.existsSelectQuery,
|
|
222
|
-
select: { _: { type: "value", value: 1 } },
|
|
223
|
-
}).sql;
|
|
224
|
-
|
|
225
|
-
// OUTPUT 불필요: 단순 INSERT IF NOT EXISTS
|
|
226
|
-
if (def.output == null) {
|
|
227
|
-
const sql = `INSERT INTO ${table} (${colList}) SELECT ${values} WHERE NOT EXISTS (${existsQuerySql})`;
|
|
228
|
-
return { sql };
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// OUTPUT 필요: multi-statement (INSERT + SET @affected + SELECT)
|
|
232
|
-
const output = def.output;
|
|
233
|
-
const outputCols = output.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
234
|
-
|
|
235
|
-
// OUTPUT을 위한 SELECT WHERE
|
|
236
|
-
const whereForSelect = output.pkColNames.map((pk) => {
|
|
237
|
-
const wrappedPk = this.expr.wrap(pk);
|
|
238
|
-
if (pk === output.aiColName) {
|
|
239
|
-
return `${wrappedPk} = LAST_INSERT_ID()`;
|
|
240
|
-
}
|
|
241
|
-
return `${wrappedPk} = ${this.expr.escapeValue(def.record[pk])}`;
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
// multi-statement: INSERT → SET @affected → SELECT (삽입된 경우만
|
|
245
|
-
const statements = [
|
|
246
|
-
`INSERT INTO ${table} (${colList}) SELECT ${values} WHERE NOT EXISTS (${existsQuerySql})`,
|
|
247
|
-
`SET @sd_affected = ROW_COUNT()`,
|
|
248
|
-
`SELECT ${outputCols} FROM ${table} WHERE ${whereForSelect.join(" AND ")} AND @sd_affected > 0`,
|
|
249
|
-
];
|
|
250
|
-
|
|
251
|
-
// results[0]=INSERT, results[1]=SET(빈결과), results[2]=SELECT
|
|
252
|
-
return { sql: statements.join(";\n"), resultSetIndex: 2 };
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
protected insertInto(def: InsertIntoQueryDef): QueryBuildResult {
|
|
256
|
-
const table = this.tableName(def.table);
|
|
257
|
-
const selectSql = this.select(def.recordsSelectQuery).sql;
|
|
258
|
-
|
|
259
|
-
// INSERT INTO SELECT에서 columns 추출
|
|
260
|
-
const selectDef = def.recordsSelectQuery;
|
|
261
|
-
const colList =
|
|
262
|
-
selectDef.select != null
|
|
263
|
-
? Object.keys(selectDef.select)
|
|
264
|
-
.map((c) => this.expr.wrap(c))
|
|
265
|
-
.join(", ")
|
|
266
|
-
: "*";
|
|
267
|
-
|
|
268
|
-
// OUTPUT 불필요: 단순 INSERT INTO SELECT
|
|
269
|
-
if (def.output == null) {
|
|
270
|
-
return { sql: `INSERT INTO ${table} (${colList}) ${selectSql}` };
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// OUTPUT 필요: multi-statement
|
|
274
|
-
const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
275
|
-
|
|
276
|
-
// PK가 AI일 때: LAST_INSERT_ID() + ROW_COUNT()
|
|
277
|
-
if (def.output.aiColName != null) {
|
|
278
|
-
const aiCol = this.expr.wrap(def.output.aiColName);
|
|
279
|
-
|
|
280
|
-
const statements = [
|
|
281
|
-
`INSERT INTO ${table} (${colList}) ${selectSql}`,
|
|
282
|
-
`SET @sd_first_id = LAST_INSERT_ID(), @sd_count = ROW_COUNT()`,
|
|
283
|
-
`SELECT ${outputCols} FROM ${table} WHERE ${aiCol} >= @sd_first_id AND ${aiCol} < @sd_first_id + @sd_count`,
|
|
284
|
-
];
|
|
285
|
-
|
|
286
|
-
// results[0]=INSERT, results[1]=SET(빈결과), results[2]=SELECT
|
|
287
|
-
return { sql: statements.join(";\n"), resultSetIndex: 2 };
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// PK가 AI 아님: 임시
|
|
291
|
-
const tempTableName = this.expr.wrap("SD_TEMP_" + Uuid.new().toString().replace(/-/g, ""));
|
|
292
|
-
|
|
293
|
-
// recordsSelectQuery에서 PK
|
|
294
|
-
const pkSelectDef: SelectQueryDef = {
|
|
295
|
-
...def.recordsSelectQuery,
|
|
296
|
-
select: Object.fromEntries(
|
|
297
|
-
def.output.pkColNames.map((pk) => [pk, def.recordsSelectQuery.select![pk]]),
|
|
298
|
-
),
|
|
299
|
-
};
|
|
300
|
-
const pkSelectSql = this.select(pkSelectDef).sql;
|
|
301
|
-
|
|
302
|
-
// 임시
|
|
303
|
-
const pkConditions = def.output.pkColNames.map((pk) => {
|
|
304
|
-
const wrappedPk = this.expr.wrap(pk);
|
|
305
|
-
return `${table}.${wrappedPk} = ${tempTableName}.${wrappedPk}`;
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
const statements = [
|
|
309
|
-
`CREATE TEMPORARY TABLE ${tempTableName} AS ${pkSelectSql}`,
|
|
310
|
-
`INSERT INTO ${table} (${colList}) ${selectSql}`,
|
|
311
|
-
`SELECT ${outputCols} FROM ${table}, ${tempTableName} WHERE ${pkConditions.join(" AND ")}`,
|
|
312
|
-
`DROP TEMPORARY TABLE ${tempTableName}`,
|
|
313
|
-
];
|
|
314
|
-
|
|
315
|
-
// results[0]=CREATE, results[1]=INSERT, results[2]=SELECT, results[3]=DROP
|
|
316
|
-
return { sql: statements.join(";\n"), resultSetIndex: 2 };
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
//#endregion
|
|
320
|
-
|
|
321
|
-
//#region ========== DML - UPDATE ==========
|
|
322
|
-
|
|
323
|
-
protected update(def: UpdateQueryDef): QueryBuildResult {
|
|
324
|
-
const table = this.tableName(def.table);
|
|
325
|
-
const alias = this.expr.wrap(def.as);
|
|
326
|
-
|
|
327
|
-
// SET
|
|
328
|
-
const setParts = Object.entries(def.record).map(
|
|
329
|
-
([col, expr]) => `${alias}.${this.expr.wrap(col)} = ${this.expr.render(expr)}`,
|
|
330
|
-
);
|
|
331
|
-
|
|
332
|
-
// OUTPUT 불필요: 단순 UPDATE
|
|
333
|
-
if (def.output == null) {
|
|
334
|
-
let sql = `UPDATE ${table} AS ${alias}`;
|
|
335
|
-
sql += this.renderJoins(def.joins);
|
|
336
|
-
sql += ` SET ${setParts.join(", ")}`;
|
|
337
|
-
sql += this.renderWhere(def.where);
|
|
338
|
-
if (def.limit != null || def.top != null) {
|
|
339
|
-
sql += this.renderLimit(def.limit, def.top);
|
|
340
|
-
}
|
|
341
|
-
return { sql };
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// OUTPUT 필요: multi-statement (
|
|
345
|
-
const outputCols = def.output.columns.map((c) => `${alias}.${this.expr.wrap(c)}`).join(", ");
|
|
346
|
-
const tempTableName = this.expr.wrap("SD_TEMP_" + Uuid.new().toString().replace(/-/g, ""));
|
|
347
|
-
|
|
348
|
-
// UPDATE 대상 PK를 임시
|
|
349
|
-
const pkSelectCols = def.output.pkColNames
|
|
350
|
-
.map((pk) => `${alias}.${this.expr.wrap(pk)} AS ${this.expr.wrap(pk)}`)
|
|
351
|
-
.join(", ");
|
|
352
|
-
let createTempSql = `CREATE TEMPORARY TABLE ${tempTableName} AS SELECT ${pkSelectCols} FROM ${table} AS ${alias}`;
|
|
353
|
-
createTempSql += this.renderJoins(def.joins);
|
|
354
|
-
createTempSql += this.renderWhere(def.where);
|
|
355
|
-
|
|
356
|
-
// UPDATE
|
|
357
|
-
let updateSql = `UPDATE ${table} AS ${alias}`;
|
|
358
|
-
updateSql += this.renderJoins(def.joins);
|
|
359
|
-
updateSql += ` SET ${setParts.join(", ")}`;
|
|
360
|
-
updateSql += this.renderWhere(def.where);
|
|
361
|
-
if (def.top != null) updateSql += ` LIMIT ${def.top}`;
|
|
362
|
-
|
|
363
|
-
// 임시
|
|
364
|
-
const pkConditions = def.output.pkColNames.map((pk) => {
|
|
365
|
-
const wrappedPk = this.expr.wrap(pk);
|
|
366
|
-
return `${alias}.${wrappedPk} = ${tempTableName}.${wrappedPk}`;
|
|
367
|
-
});
|
|
368
|
-
const selectSql = `SELECT ${outputCols} FROM ${table} AS ${alias}, ${tempTableName} WHERE ${pkConditions.join(" AND ")}`;
|
|
369
|
-
|
|
370
|
-
// 임시
|
|
371
|
-
const dropSql = `DROP TEMPORARY TABLE ${tempTableName}`;
|
|
372
|
-
|
|
373
|
-
const statements = [createTempSql, updateSql, selectSql, dropSql];
|
|
374
|
-
return { sql: statements.join(";\n"), resultSetIndex: 2 };
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
//#endregion
|
|
378
|
-
|
|
379
|
-
//#region ========== DML - DELETE ==========
|
|
380
|
-
|
|
381
|
-
protected delete(def: DeleteQueryDef): QueryBuildResult {
|
|
382
|
-
const table = this.tableName(def.table);
|
|
383
|
-
const alias = this.expr.wrap(def.as);
|
|
384
|
-
|
|
385
|
-
// OUTPUT 불필요: 단순 DELETE
|
|
386
|
-
if (def.output == null) {
|
|
387
|
-
let sql = `DELETE ${alias} FROM ${table} AS ${alias}`;
|
|
388
|
-
sql += this.renderJoins(def.joins);
|
|
389
|
-
sql += this.renderWhere(def.where);
|
|
390
|
-
if (def.limit != null || def.top != null) {
|
|
391
|
-
sql += this.renderLimit(def.limit, def.top);
|
|
392
|
-
}
|
|
393
|
-
return { sql };
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// OUTPUT 필요: multi-statement (
|
|
397
|
-
const outputCols = def.output.columns.map((c) => `${alias}.${this.expr.wrap(c)}`).join(", ");
|
|
398
|
-
const tempTableName = this.expr.wrap("SD_TEMP_" + Uuid.new().toString().replace(/-/g, ""));
|
|
399
|
-
|
|
400
|
-
//
|
|
401
|
-
let createTempSql = `CREATE TEMPORARY TABLE ${tempTableName} AS SELECT ${outputCols} FROM ${table} AS ${alias}`;
|
|
402
|
-
createTempSql += this.renderJoins(def.joins);
|
|
403
|
-
createTempSql += this.renderWhere(def.where);
|
|
404
|
-
|
|
405
|
-
// DELETE 실행
|
|
406
|
-
let deleteSql = `DELETE ${alias} FROM ${table} AS ${alias}`;
|
|
407
|
-
deleteSql += this.renderJoins(def.joins);
|
|
408
|
-
deleteSql += this.renderWhere(def.where);
|
|
409
|
-
if (def.top != null) deleteSql += ` LIMIT ${def.top}`;
|
|
410
|
-
|
|
411
|
-
// 임시
|
|
412
|
-
const selectSql = `SELECT * FROM ${tempTableName}`;
|
|
413
|
-
|
|
414
|
-
// 임시
|
|
415
|
-
const dropSql = `DROP TEMPORARY TABLE ${tempTableName}`;
|
|
416
|
-
|
|
417
|
-
const statements = [createTempSql, deleteSql, selectSql, dropSql];
|
|
418
|
-
return { sql: statements.join(";\n"), resultSetIndex: 2 };
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
//#endregion
|
|
422
|
-
|
|
423
|
-
//#region ========== DML - UPSERT ==========
|
|
424
|
-
|
|
425
|
-
protected upsert(def: UpsertQueryDef): QueryBuildResult {
|
|
426
|
-
const table = this.tableName(def.table);
|
|
427
|
-
const alias = this.expr.wrap(def.existsSelectQuery.as);
|
|
428
|
-
const existsQuerySql = this.select(def.existsSelectQuery).sql;
|
|
429
|
-
|
|
430
|
-
// UPDATE SET
|
|
431
|
-
const updateSetParts = Object.entries(def.updateRecord).map(
|
|
432
|
-
([col, e]) => `${alias}.${this.expr.wrap(col)} = ${this.expr.render(e)}`,
|
|
433
|
-
);
|
|
434
|
-
|
|
435
|
-
// INSERT
|
|
436
|
-
const insertColumns = Object.keys(def.insertRecord);
|
|
437
|
-
const insertColList = insertColumns.map((c) => this.expr.wrap(c)).join(", ");
|
|
438
|
-
const insertValues = insertColumns.map((c) => this.expr.render(def.insertRecord[c])).join(", ");
|
|
439
|
-
|
|
440
|
-
// WHERE
|
|
441
|
-
const whereCondition =
|
|
442
|
-
def.existsSelectQuery.where != null && def.existsSelectQuery.where.length > 0
|
|
443
|
-
? this.expr.renderWhere(def.existsSelectQuery.where)
|
|
444
|
-
: "1=1";
|
|
445
|
-
|
|
446
|
-
// OUTPUT 불필요: multi-statement (UPDATE + INSERT WHERE NOT EXISTS)
|
|
447
|
-
if (def.output == null) {
|
|
448
|
-
// UPDATE: 존재하면 UPDATE 됨
|
|
449
|
-
// INSERT SELECT WHERE NOT EXISTS: 존재 안하면 INSERT 됨
|
|
450
|
-
const statements = [
|
|
451
|
-
`UPDATE ${table} AS ${alias} SET ${updateSetParts.join(", ")} WHERE ${whereCondition}`,
|
|
452
|
-
`INSERT INTO ${table} (${insertColList}) SELECT ${insertValues} WHERE NOT EXISTS (${existsQuerySql})`,
|
|
453
|
-
];
|
|
454
|
-
return { sql: statements.join(";\n") };
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// OUTPUT 필요: multi-statement (CREATE TEMP + UPDATE + INSERT + SELECT + DROP)
|
|
458
|
-
const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
459
|
-
const tempTableName = this.expr.wrap("SD_TEMP_" + Uuid.new().toString().replace(/-/g, ""));
|
|
460
|
-
|
|
461
|
-
// UPDATE 대상 PK를 임시
|
|
462
|
-
const pkSelectCols = def.output.pkColNames.map((pk) => this.expr.wrap(pk)).join(", ");
|
|
463
|
-
const createTempSql = `CREATE TEMPORARY TABLE ${tempTableName} AS SELECT ${pkSelectCols} FROM ${table} AS ${alias} WHERE ${whereCondition}`;
|
|
464
|
-
|
|
465
|
-
// UPDATE (존재하면
|
|
466
|
-
const updateSql = `UPDATE ${table} AS ${alias} SET ${updateSetParts.join(", ")} WHERE ${whereCondition}`;
|
|
467
|
-
|
|
468
|
-
// INSERT (NOT EXISTS
|
|
469
|
-
const insertSql = `INSERT INTO ${table} (${insertColList}) SELECT ${insertValues} WHERE NOT EXISTS (${existsQuerySql})`;
|
|
470
|
-
|
|
471
|
-
// SELECT: UPDATE
|
|
472
|
-
// UPDATE 케이스: temp
|
|
473
|
-
const output = def.output;
|
|
474
|
-
const updatePkConditions = output.pkColNames.map((pk) => {
|
|
475
|
-
const wrappedPk = this.expr.wrap(pk);
|
|
476
|
-
return `${table}.${wrappedPk} IN (SELECT ${wrappedPk} FROM ${tempTableName})`;
|
|
477
|
-
});
|
|
478
|
-
const selectUpdateSql = `SELECT ${outputCols} FROM ${table} WHERE ${updatePkConditions.join(" AND ")}`;
|
|
479
|
-
|
|
480
|
-
// INSERT 케이스: insertRecord의 PK로 조회 (AI면 LAST_INSERT_ID(), 임시
|
|
481
|
-
const insertPkConditions = output.pkColNames.map((pk) => {
|
|
482
|
-
const wrappedPk = this.expr.wrap(pk);
|
|
483
|
-
if (pk === output.aiColName) {
|
|
484
|
-
return `${wrappedPk} = LAST_INSERT_ID()`;
|
|
485
|
-
}
|
|
486
|
-
const pkExpr = def.insertRecord[pk];
|
|
487
|
-
return `${wrappedPk} = ${this.expr.render(pkExpr)}`;
|
|
488
|
-
});
|
|
489
|
-
const selectInsertSql = `SELECT ${outputCols} FROM ${table} WHERE ${insertPkConditions.join(" AND ")} AND NOT EXISTS (SELECT 1 FROM ${tempTableName})`;
|
|
490
|
-
|
|
491
|
-
const selectSql = `${selectUpdateSql} UNION ALL ${selectInsertSql}`;
|
|
492
|
-
|
|
493
|
-
// DROP
|
|
494
|
-
const dropSql = `DROP TEMPORARY TABLE ${tempTableName}`;
|
|
495
|
-
|
|
496
|
-
const statements = [createTempSql, updateSql, insertSql, selectSql, dropSql];
|
|
497
|
-
return { sql: statements.join(";\n"), resultSetIndex: 3 };
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
//#endregion
|
|
501
|
-
|
|
502
|
-
//#region ========== DDL - Table ==========
|
|
503
|
-
|
|
504
|
-
protected createTable(def: CreateTableQueryDef): QueryBuildResult {
|
|
505
|
-
const table = this.tableName(def.table);
|
|
506
|
-
|
|
507
|
-
const colDefs = def.columns.map((col) => {
|
|
508
|
-
let colSql = `${this.expr.wrap(col.name)} ${this.expr.renderDataType(col.dataType)}`;
|
|
509
|
-
|
|
510
|
-
// nullable: true → NULL, else → NOT NULL
|
|
511
|
-
if (col.nullable === true) {
|
|
512
|
-
colSql += " NULL";
|
|
513
|
-
} else {
|
|
514
|
-
colSql += " NOT NULL";
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
if (col.autoIncrement) {
|
|
518
|
-
colSql += " AUTO_INCREMENT";
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
if (col.default !== undefined) {
|
|
522
|
-
colSql += ` DEFAULT ${this.expr.escapeValue(col.default)}`;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
return colSql;
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
// Primary Key with CONSTRAINT name
|
|
529
|
-
if (def.primaryKey != null && def.primaryKey.length > 0) {
|
|
530
|
-
const pkCols = def.primaryKey.map((c) => this.expr.wrap(c)).join(", ");
|
|
531
|
-
const pkName = this.expr.wrap(`PK_${def.table.name}`);
|
|
532
|
-
colDefs.push(`CONSTRAINT ${pkName} PRIMARY KEY (${pkCols})`);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
return { sql: `CREATE TABLE ${table} (\n ${colDefs.join(",\n ")}\n)` };
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
protected dropTable(def: DropTableQueryDef): QueryBuildResult {
|
|
539
|
-
return { sql: `DROP TABLE ${this.tableName(def.table)}` };
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
protected renameTable(def: RenameTableQueryDef): QueryBuildResult {
|
|
543
|
-
return { sql: `RENAME TABLE ${this.tableName(def.table)} TO ${this.expr.wrap(def.newName)}` };
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
protected truncate(def: TruncateQueryDef): QueryBuildResult {
|
|
547
|
-
// MySQL: TRUNCATE는 AUTO_INCREMENT
|
|
548
|
-
return { sql: `TRUNCATE TABLE ${this.tableName(def.table)}` };
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
//#endregion
|
|
552
|
-
|
|
553
|
-
//#region ========== DDL - Column ==========
|
|
554
|
-
|
|
555
|
-
protected addColumn(def: AddColumnQueryDef): QueryBuildResult {
|
|
556
|
-
const table = this.tableName(def.table);
|
|
557
|
-
const col = def.column;
|
|
558
|
-
|
|
559
|
-
let colSql = `${this.expr.wrap(col.name)} ${this.expr.renderDataType(col.dataType)}`;
|
|
560
|
-
|
|
561
|
-
// nullable: true → NULL, else → NOT NULL
|
|
562
|
-
if (col.nullable === true) {
|
|
563
|
-
colSql += " NULL";
|
|
564
|
-
} else {
|
|
565
|
-
colSql += " NOT NULL";
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
if (col.autoIncrement) {
|
|
569
|
-
colSql += " AUTO_INCREMENT";
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
if (col.default !== undefined) {
|
|
573
|
-
colSql += ` DEFAULT ${this.expr.escapeValue(col.default)}`;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
return { sql: `ALTER TABLE ${table} ADD COLUMN ${colSql}` };
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
protected dropColumn(def: DropColumnQueryDef): QueryBuildResult {
|
|
580
|
-
return {
|
|
581
|
-
sql: `ALTER TABLE ${this.tableName(def.table)} DROP COLUMN ${this.expr.wrap(def.column)}`,
|
|
582
|
-
};
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
protected modifyColumn(def: ModifyColumnQueryDef): QueryBuildResult {
|
|
586
|
-
const table = this.tableName(def.table);
|
|
587
|
-
const col = def.column;
|
|
588
|
-
|
|
589
|
-
let colSql = `${this.expr.wrap(col.name)} ${this.expr.renderDataType(col.dataType)}`;
|
|
590
|
-
|
|
591
|
-
// nullable: true → NULL, else → NOT NULL
|
|
592
|
-
if (col.nullable === true) {
|
|
593
|
-
colSql += " NULL";
|
|
594
|
-
} else {
|
|
595
|
-
colSql += " NOT NULL";
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
if (col.autoIncrement) {
|
|
599
|
-
colSql += " AUTO_INCREMENT";
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
if (col.default !== undefined) {
|
|
603
|
-
colSql += ` DEFAULT ${this.expr.escapeValue(col.default)}`;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
return { sql: `ALTER TABLE ${table} MODIFY COLUMN ${colSql}` };
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
protected renameColumn(def: RenameColumnQueryDef): QueryBuildResult {
|
|
610
|
-
const table = this.tableName(def.table);
|
|
611
|
-
// MySQL 8.0+: RENAME COLUMN 지원
|
|
612
|
-
return {
|
|
613
|
-
sql: `ALTER TABLE ${table} RENAME COLUMN ${this.expr.wrap(def.column)} TO ${this.expr.wrap(def.newName)}`,
|
|
614
|
-
};
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
//#endregion
|
|
618
|
-
|
|
619
|
-
//#region ========== DDL - Constraint ==========
|
|
620
|
-
|
|
621
|
-
protected addPk(def: AddPkQueryDef): QueryBuildResult {
|
|
622
|
-
const table = this.tableName(def.table);
|
|
623
|
-
const cols = def.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
624
|
-
return { sql: `ALTER TABLE ${table} ADD PRIMARY KEY (${cols})` };
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
protected dropPk(def: DropPkQueryDef): QueryBuildResult {
|
|
628
|
-
return { sql: `ALTER TABLE ${this.tableName(def.table)} DROP PRIMARY KEY` };
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
protected addFk(def: AddFkQueryDef): QueryBuildResult {
|
|
632
|
-
const table = this.tableName(def.table);
|
|
633
|
-
const fk = def.foreignKey;
|
|
634
|
-
const fkCols = fk.fkColumns.map((c) => this.expr.wrap(c)).join(", ");
|
|
635
|
-
const targetTable = this.tableName(fk.targetTable);
|
|
636
|
-
const targetCols = fk.targetPkColumns.map((c) => this.expr.wrap(c)).join(", ");
|
|
637
|
-
|
|
638
|
-
// MySQL은 FK
|
|
639
|
-
return {
|
|
640
|
-
sql: `ALTER TABLE ${table} ADD CONSTRAINT ${this.expr.wrap(fk.name)} FOREIGN KEY (${fkCols}) REFERENCES ${targetTable} (${targetCols})`,
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
protected dropFk(def: DropFkQueryDef): QueryBuildResult {
|
|
645
|
-
return {
|
|
646
|
-
sql: `ALTER TABLE ${this.tableName(def.table)} DROP FOREIGN KEY ${this.expr.wrap(def.foreignKey)}`,
|
|
647
|
-
};
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
protected addIdx(def: AddIdxQueryDef): QueryBuildResult {
|
|
651
|
-
const table = this.tableName(def.table);
|
|
652
|
-
const idx = def.index;
|
|
653
|
-
const cols = idx.columns.map((c) => `${this.expr.wrap(c.name)} ${c.orderBy}`).join(", ");
|
|
654
|
-
const unique = idx.unique ? "UNIQUE " : "";
|
|
655
|
-
return { sql: `CREATE ${unique}INDEX ${this.expr.wrap(idx.name)} ON ${table} (${cols})` };
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
protected dropIdx(def: DropIdxQueryDef): QueryBuildResult {
|
|
659
|
-
return { sql: `DROP INDEX ${this.expr.wrap(def.index)} ON ${this.tableName(def.table)}` };
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
//#endregion
|
|
663
|
-
|
|
664
|
-
//#region ========== DDL - View/Procedure ==========
|
|
665
|
-
|
|
666
|
-
protected createView(def: CreateViewQueryDef): QueryBuildResult {
|
|
667
|
-
const view = this.tableName(def.view);
|
|
668
|
-
const selectSql = this.select(def.queryDef).sql;
|
|
669
|
-
return { sql: `CREATE OR REPLACE VIEW ${view} AS ${selectSql}` };
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
protected dropView(def: DropViewQueryDef): QueryBuildResult {
|
|
673
|
-
return { sql: `DROP VIEW IF EXISTS ${this.tableName(def.view)}` };
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
protected createProc(def: CreateProcQueryDef): QueryBuildResult {
|
|
677
|
-
const proc = this.tableName(def.procedure);
|
|
678
|
-
|
|
679
|
-
// params
|
|
680
|
-
const paramList =
|
|
681
|
-
def.params
|
|
682
|
-
?.map((p) => {
|
|
683
|
-
let sql = `IN ${this.expr.wrap(p.name)} ${this.expr.renderDataType(p.dataType)}`;
|
|
684
|
-
if (p.default !== undefined) {
|
|
685
|
-
sql += ` DEFAULT ${this.expr.escapeValue(p.default)}`;
|
|
686
|
-
}
|
|
687
|
-
return sql;
|
|
688
|
-
})
|
|
689
|
-
.join(", ") ?? "";
|
|
690
|
-
|
|
691
|
-
let sql = `CREATE PROCEDURE ${proc}(${paramList})\n`;
|
|
692
|
-
sql += `BEGIN\n`;
|
|
693
|
-
sql += def.query;
|
|
694
|
-
if (!def.query.trim().endsWith(";")) {
|
|
695
|
-
sql += ";";
|
|
696
|
-
}
|
|
697
|
-
sql += `\nEND`;
|
|
698
|
-
|
|
699
|
-
return { sql };
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
protected dropProc(def: DropProcQueryDef): QueryBuildResult {
|
|
703
|
-
return { sql: `DROP PROCEDURE IF EXISTS ${this.tableName(def.procedure)}` };
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
protected execProc(def: ExecProcQueryDef): QueryBuildResult {
|
|
707
|
-
const proc = this.tableName(def.procedure);
|
|
708
|
-
if (def.params == null || Object.keys(def.params).length === 0) {
|
|
709
|
-
return { sql: `CALL ${proc}()` };
|
|
710
|
-
}
|
|
711
|
-
const params = Object.values(def.params)
|
|
712
|
-
.map((p) => this.expr.render(p))
|
|
713
|
-
.join(", ");
|
|
714
|
-
return { sql: `CALL ${proc}(${params})` };
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
//#endregion
|
|
718
|
-
|
|
719
|
-
//#region ========== Utils ==========
|
|
720
|
-
|
|
721
|
-
protected clearSchema(def: ClearSchemaQueryDef): QueryBuildResult {
|
|
722
|
-
// MySQL: 모든
|
|
723
|
-
// information_schema에서
|
|
724
|
-
// SQL Injection 방지: 식별자 유효성
|
|
725
|
-
if (!/^[a-zA-Z0-9_]+$/.test(def.database)) {
|
|
726
|
-
throw new Error(
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
const dbName = this.expr.escapeString(def.database);
|
|
730
|
-
return {
|
|
731
|
-
sql: `
|
|
732
|
-
SET FOREIGN_KEY_CHECKS = 0;
|
|
733
|
-
SET @tables = NULL;
|
|
734
|
-
SELECT GROUP_CONCAT(table_name) INTO @tables FROM information_schema.tables WHERE table_schema = '${dbName}';
|
|
735
|
-
SET @drop_stmt = IF(@tables IS NULL, 'SELECT 1', CONCAT('DROP TABLE IF EXISTS ', @tables));
|
|
736
|
-
PREPARE stmt FROM @drop_stmt;
|
|
737
|
-
EXECUTE stmt;
|
|
738
|
-
DEALLOCATE PREPARE stmt;
|
|
739
|
-
SET FOREIGN_KEY_CHECKS = 1`,
|
|
740
|
-
};
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
protected schemaExists(def: SchemaExistsQueryDef): QueryBuildResult {
|
|
744
|
-
// MySQL: database와 schema는 동의어
|
|
745
|
-
const dbName = this.expr.escapeString(def.database);
|
|
746
|
-
return {
|
|
747
|
-
sql: `SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = '${dbName}'`,
|
|
748
|
-
};
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
/** MySQL은 전역 설정만 지원 (
|
|
752
|
-
protected switchFk(def: SwitchFkQueryDef): QueryBuildResult {
|
|
753
|
-
return def.switch === "on"
|
|
754
|
-
? { sql: "SET FOREIGN_KEY_CHECKS = 1" }
|
|
755
|
-
: { sql: "SET FOREIGN_KEY_CHECKS = 0" };
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
//#endregion
|
|
759
|
-
}
|
|
1
|
+
import { Uuid } from "@simplysm/core-common";
|
|
2
|
+
import type {
|
|
3
|
+
AddColumnQueryDef,
|
|
4
|
+
AddFkQueryDef,
|
|
5
|
+
AddIdxQueryDef,
|
|
6
|
+
AddPkQueryDef,
|
|
7
|
+
ClearSchemaQueryDef,
|
|
8
|
+
CreateProcQueryDef,
|
|
9
|
+
CreateTableQueryDef,
|
|
10
|
+
CreateViewQueryDef,
|
|
11
|
+
SchemaExistsQueryDef,
|
|
12
|
+
DeleteQueryDef,
|
|
13
|
+
DropColumnQueryDef,
|
|
14
|
+
DropFkQueryDef,
|
|
15
|
+
DropIdxQueryDef,
|
|
16
|
+
DropPkQueryDef,
|
|
17
|
+
DropProcQueryDef,
|
|
18
|
+
DropTableQueryDef,
|
|
19
|
+
DropViewQueryDef,
|
|
20
|
+
ExecProcQueryDef,
|
|
21
|
+
InsertIfNotExistsQueryDef,
|
|
22
|
+
InsertIntoQueryDef,
|
|
23
|
+
InsertQueryDef,
|
|
24
|
+
ModifyColumnQueryDef,
|
|
25
|
+
QueryDefObjectName,
|
|
26
|
+
RenameColumnQueryDef,
|
|
27
|
+
RenameTableQueryDef,
|
|
28
|
+
SelectQueryDef,
|
|
29
|
+
SelectQueryDefJoin,
|
|
30
|
+
SwitchFkQueryDef,
|
|
31
|
+
TruncateQueryDef,
|
|
32
|
+
UpdateQueryDef,
|
|
33
|
+
UpsertQueryDef,
|
|
34
|
+
} from "../../types/query-def";
|
|
35
|
+
import type { QueryBuildResult } from "../../types/db";
|
|
36
|
+
import { QueryBuilderBase } from "../base/query-builder-base";
|
|
37
|
+
import { MysqlExprRenderer } from "./mysql-expr-renderer";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* MySQL QueryBuilder
|
|
41
|
+
*
|
|
42
|
+
* MySQL 특이사항:
|
|
43
|
+
* - OUTPUT 미지원: multi-statement 패턴으로 우회 (INSERT + SET @var + SELECT)
|
|
44
|
+
* - INSERT OUTPUT: LAST_INSERT_ID()로 AI column 조회, 비-AI는 record에서 PK 추출
|
|
45
|
+
* - UPDATE/UPSERT OUTPUT: WHERE condition이 변경될 수 있으므로 PK를 먼저 임시 Table에 저장 후 SELECT
|
|
46
|
+
* - DELETE OUTPUT: Delete 전 output columns를 임시 Table에 저장
|
|
47
|
+
* - switchFk: 전역 설정 (SET FOREIGN_KEY_CHECKS), Table 파라미터 무시됨
|
|
48
|
+
* - FK Add 시 Index automatic 생성됨
|
|
49
|
+
*/
|
|
50
|
+
export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
51
|
+
protected expr = new MysqlExprRenderer((def) => this.select(def).sql);
|
|
52
|
+
|
|
53
|
+
//#region ========== 유틸리티 ==========
|
|
54
|
+
|
|
55
|
+
/** Table명 Render (MySQL: schema 무시, database.table만 사용) */
|
|
56
|
+
protected tableName(obj: QueryDefObjectName): string {
|
|
57
|
+
if (obj.database != null) {
|
|
58
|
+
return `${this.expr.wrap(obj.database)}.${this.expr.wrap(obj.name)}`;
|
|
59
|
+
}
|
|
60
|
+
return this.expr.wrap(obj.name);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** LIMIT 절 Render */
|
|
64
|
+
protected renderLimit(limit: [number, number] | undefined, top: number | undefined): string {
|
|
65
|
+
if (limit != null) {
|
|
66
|
+
const [offset, count] = limit;
|
|
67
|
+
return ` LIMIT ${offset}, ${count}`;
|
|
68
|
+
}
|
|
69
|
+
if (top != null) {
|
|
70
|
+
return ` LIMIT ${top}`;
|
|
71
|
+
}
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
protected renderJoin(join: SelectQueryDefJoin): string {
|
|
76
|
+
const alias = this.expr.wrap(join.as);
|
|
77
|
+
|
|
78
|
+
// LATERAL JOIN 필요 여부 감지
|
|
79
|
+
if (this.needsLateral(join)) {
|
|
80
|
+
// from이 배열(UNION ALL)이면 renderFrom(join.from),
|
|
81
|
+
// 그 외(orderBy, top, select 등)면 renderFrom(join)으로 Subquery Generate
|
|
82
|
+
const from = Array.isArray(join.from) ? this.renderFrom(join.from) : this.renderFrom(join);
|
|
83
|
+
return ` LEFT OUTER JOIN LATERAL ${from} AS ${alias} ON TRUE`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 일반 JOIN
|
|
87
|
+
const from = this.renderFrom(join.from);
|
|
88
|
+
const where =
|
|
89
|
+
join.where != null && join.where.length > 0
|
|
90
|
+
? ` ON ${this.expr.renderWhere(join.where)}`
|
|
91
|
+
: " ON TRUE";
|
|
92
|
+
return ` LEFT OUTER JOIN ${from} AS ${alias}${where}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
//#endregion
|
|
96
|
+
|
|
97
|
+
//#region ========== DML - SELECT ==========
|
|
98
|
+
|
|
99
|
+
protected select(def: SelectQueryDef): QueryBuildResult {
|
|
100
|
+
// WITH (CTE)
|
|
101
|
+
let sql = "";
|
|
102
|
+
if (def.with != null) {
|
|
103
|
+
const { name, base, recursive } = def.with;
|
|
104
|
+
sql += `WITH ${this.expr.wrap(name)} AS (${this.select(base).sql} UNION ALL ${this.select(recursive).sql}) `;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// SELECT
|
|
108
|
+
sql += "SELECT";
|
|
109
|
+
if (def.distinct) {
|
|
110
|
+
sql += " DISTINCT";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// columns
|
|
114
|
+
if (def.select != null) {
|
|
115
|
+
const cols = Object.entries(def.select).map(
|
|
116
|
+
([alias, expr]) => `${this.expr.render(expr)} AS ${this.expr.wrap(alias)}`,
|
|
117
|
+
);
|
|
118
|
+
sql += ` ${cols.join(", ")}`;
|
|
119
|
+
} else {
|
|
120
|
+
sql += " *";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// FROM
|
|
124
|
+
if (def.from != null) {
|
|
125
|
+
const from = this.renderFrom(def.from);
|
|
126
|
+
sql += ` FROM ${from} AS ${this.expr.wrap(def.as)}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// LOCK
|
|
130
|
+
if (def.lock) {
|
|
131
|
+
// MySQL에서는 SELECT ... FOR UPDATE (마지막에 붙임)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// JOINs
|
|
135
|
+
sql += this.renderJoins(def.joins);
|
|
136
|
+
|
|
137
|
+
// WHERE
|
|
138
|
+
sql += this.renderWhere(def.where);
|
|
139
|
+
|
|
140
|
+
// GROUP BY
|
|
141
|
+
sql += this.renderGroupBy(def.groupBy);
|
|
142
|
+
|
|
143
|
+
// HAVING
|
|
144
|
+
sql += this.renderHaving(def.having);
|
|
145
|
+
|
|
146
|
+
// ORDER BY
|
|
147
|
+
sql += this.renderOrderBy(def.orderBy);
|
|
148
|
+
|
|
149
|
+
// LIMIT
|
|
150
|
+
sql += this.renderLimit(def.limit, def.top);
|
|
151
|
+
|
|
152
|
+
// LOCK (FOR UPDATE at end)
|
|
153
|
+
if (def.lock) {
|
|
154
|
+
sql += " FOR UPDATE";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { sql };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
//#endregion
|
|
161
|
+
|
|
162
|
+
//#region ========== DML - INSERT ==========
|
|
163
|
+
|
|
164
|
+
protected insert(def: InsertQueryDef): QueryBuildResult {
|
|
165
|
+
const table = this.tableName(def.table);
|
|
166
|
+
|
|
167
|
+
if (def.records.length === 0) {
|
|
168
|
+
throw new Error("INSERT requires at least one record.");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const columns = Object.keys(def.records[0]);
|
|
172
|
+
const colList = columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
173
|
+
|
|
174
|
+
// OUTPUT 불필요: 단순 배치 INSERT
|
|
175
|
+
if (def.output == null) {
|
|
176
|
+
const valuesList = def.records.map((record) => {
|
|
177
|
+
const values = columns.map((c) => this.expr.escapeValue(record[c]));
|
|
178
|
+
return `(${values.join(", ")})`;
|
|
179
|
+
});
|
|
180
|
+
return { sql: `INSERT INTO ${table} (${colList}) VALUES ${valuesList.join(", ")}` };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// OUTPUT 필요: multi-statement로 INSERT + SELECT 실행
|
|
184
|
+
// Result셋: [INSERT결과, SELECT결과, INSERT결과, SELECT결과, ...]
|
|
185
|
+
// → resultSetIndex=1, resultSetStride=2 로 SELECT 결과만 추출
|
|
186
|
+
const output = def.output;
|
|
187
|
+
const outputCols = output.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
188
|
+
const statements: string[] = [];
|
|
189
|
+
|
|
190
|
+
for (const record of def.records) {
|
|
191
|
+
const values = columns.map((c) => this.expr.escapeValue(record[c])).join(", ");
|
|
192
|
+
statements.push(`INSERT INTO ${table} (${colList}) VALUES (${values})`);
|
|
193
|
+
|
|
194
|
+
// PK로 SELECT (aiColName이면 LAST_INSERT_ID() 사용)
|
|
195
|
+
const whereForSelect = output.pkColNames.map((pk) => {
|
|
196
|
+
const wrappedPk = this.expr.wrap(pk);
|
|
197
|
+
if (pk === output.aiColName) {
|
|
198
|
+
return `${wrappedPk} = LAST_INSERT_ID()`;
|
|
199
|
+
}
|
|
200
|
+
return `${wrappedPk} = ${this.expr.escapeValue(record[pk])}`;
|
|
201
|
+
});
|
|
202
|
+
statements.push(`SELECT ${outputCols} FROM ${table} WHERE ${whereForSelect.join(" AND ")}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
sql: statements.join(";\n"),
|
|
207
|
+
resultSetIndex: 1,
|
|
208
|
+
resultSetStride: 2,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
protected insertIfNotExists(def: InsertIfNotExistsQueryDef): QueryBuildResult {
|
|
213
|
+
const table = this.tableName(def.table);
|
|
214
|
+
|
|
215
|
+
const columns = Object.keys(def.record);
|
|
216
|
+
const colList = columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
217
|
+
const values = columns.map((c) => this.expr.escapeValue(def.record[c])).join(", ");
|
|
218
|
+
|
|
219
|
+
// existsSelectQuery를 SELECT 1 AS _ 형태로 Render
|
|
220
|
+
const existsQuerySql = this.select({
|
|
221
|
+
...def.existsSelectQuery,
|
|
222
|
+
select: { _: { type: "value", value: 1 } },
|
|
223
|
+
}).sql;
|
|
224
|
+
|
|
225
|
+
// OUTPUT 불필요: 단순 INSERT IF NOT EXISTS
|
|
226
|
+
if (def.output == null) {
|
|
227
|
+
const sql = `INSERT INTO ${table} (${colList}) SELECT ${values} WHERE NOT EXISTS (${existsQuerySql})`;
|
|
228
|
+
return { sql };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// OUTPUT 필요: multi-statement (INSERT + SET @affected + SELECT)
|
|
232
|
+
const output = def.output;
|
|
233
|
+
const outputCols = output.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
234
|
+
|
|
235
|
+
// OUTPUT을 위한 SELECT WHERE condition
|
|
236
|
+
const whereForSelect = output.pkColNames.map((pk) => {
|
|
237
|
+
const wrappedPk = this.expr.wrap(pk);
|
|
238
|
+
if (pk === output.aiColName) {
|
|
239
|
+
return `${wrappedPk} = LAST_INSERT_ID()`;
|
|
240
|
+
}
|
|
241
|
+
return `${wrappedPk} = ${this.expr.escapeValue(def.record[pk])}`;
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// multi-statement: INSERT → SET @affected → SELECT (삽입된 경우만 Result)
|
|
245
|
+
const statements = [
|
|
246
|
+
`INSERT INTO ${table} (${colList}) SELECT ${values} WHERE NOT EXISTS (${existsQuerySql})`,
|
|
247
|
+
`SET @sd_affected = ROW_COUNT()`,
|
|
248
|
+
`SELECT ${outputCols} FROM ${table} WHERE ${whereForSelect.join(" AND ")} AND @sd_affected > 0`,
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
// results[0]=INSERT, results[1]=SET(빈결과), results[2]=SELECT
|
|
252
|
+
return { sql: statements.join(";\n"), resultSetIndex: 2 };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
protected insertInto(def: InsertIntoQueryDef): QueryBuildResult {
|
|
256
|
+
const table = this.tableName(def.table);
|
|
257
|
+
const selectSql = this.select(def.recordsSelectQuery).sql;
|
|
258
|
+
|
|
259
|
+
// INSERT INTO SELECT에서 columns 추출
|
|
260
|
+
const selectDef = def.recordsSelectQuery;
|
|
261
|
+
const colList =
|
|
262
|
+
selectDef.select != null
|
|
263
|
+
? Object.keys(selectDef.select)
|
|
264
|
+
.map((c) => this.expr.wrap(c))
|
|
265
|
+
.join(", ")
|
|
266
|
+
: "*";
|
|
267
|
+
|
|
268
|
+
// OUTPUT 불필요: 단순 INSERT INTO SELECT
|
|
269
|
+
if (def.output == null) {
|
|
270
|
+
return { sql: `INSERT INTO ${table} (${colList}) ${selectSql}` };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// OUTPUT 필요: multi-statement
|
|
274
|
+
const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
275
|
+
|
|
276
|
+
// PK가 AI일 때: LAST_INSERT_ID() + ROW_COUNT() range 조회
|
|
277
|
+
if (def.output.aiColName != null) {
|
|
278
|
+
const aiCol = this.expr.wrap(def.output.aiColName);
|
|
279
|
+
|
|
280
|
+
const statements = [
|
|
281
|
+
`INSERT INTO ${table} (${colList}) ${selectSql}`,
|
|
282
|
+
`SET @sd_first_id = LAST_INSERT_ID(), @sd_count = ROW_COUNT()`,
|
|
283
|
+
`SELECT ${outputCols} FROM ${table} WHERE ${aiCol} >= @sd_first_id AND ${aiCol} < @sd_first_id + @sd_count`,
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
// results[0]=INSERT, results[1]=SET(빈결과), results[2]=SELECT
|
|
287
|
+
return { sql: statements.join(";\n"), resultSetIndex: 2 };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// PK가 AI 아님: 임시 Table로 PK 저장 후 조회
|
|
291
|
+
const tempTableName = this.expr.wrap("SD_TEMP_" + Uuid.new().toString().replace(/-/g, ""));
|
|
292
|
+
|
|
293
|
+
// recordsSelectQuery에서 PK column만 추출한 SELECT Generate
|
|
294
|
+
const pkSelectDef: SelectQueryDef = {
|
|
295
|
+
...def.recordsSelectQuery,
|
|
296
|
+
select: Object.fromEntries(
|
|
297
|
+
def.output.pkColNames.map((pk) => [pk, def.recordsSelectQuery.select![pk]]),
|
|
298
|
+
),
|
|
299
|
+
};
|
|
300
|
+
const pkSelectSql = this.select(pkSelectDef).sql;
|
|
301
|
+
|
|
302
|
+
// 임시 Table의 PK로 target에서 SELECT
|
|
303
|
+
const pkConditions = def.output.pkColNames.map((pk) => {
|
|
304
|
+
const wrappedPk = this.expr.wrap(pk);
|
|
305
|
+
return `${table}.${wrappedPk} = ${tempTableName}.${wrappedPk}`;
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const statements = [
|
|
309
|
+
`CREATE TEMPORARY TABLE ${tempTableName} AS ${pkSelectSql}`,
|
|
310
|
+
`INSERT INTO ${table} (${colList}) ${selectSql}`,
|
|
311
|
+
`SELECT ${outputCols} FROM ${table}, ${tempTableName} WHERE ${pkConditions.join(" AND ")}`,
|
|
312
|
+
`DROP TEMPORARY TABLE ${tempTableName}`,
|
|
313
|
+
];
|
|
314
|
+
|
|
315
|
+
// results[0]=CREATE, results[1]=INSERT, results[2]=SELECT, results[3]=DROP
|
|
316
|
+
return { sql: statements.join(";\n"), resultSetIndex: 2 };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
//#endregion
|
|
320
|
+
|
|
321
|
+
//#region ========== DML - UPDATE ==========
|
|
322
|
+
|
|
323
|
+
protected update(def: UpdateQueryDef): QueryBuildResult {
|
|
324
|
+
const table = this.tableName(def.table);
|
|
325
|
+
const alias = this.expr.wrap(def.as);
|
|
326
|
+
|
|
327
|
+
// SET
|
|
328
|
+
const setParts = Object.entries(def.record).map(
|
|
329
|
+
([col, expr]) => `${alias}.${this.expr.wrap(col)} = ${this.expr.render(expr)}`,
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
// OUTPUT 불필요: 단순 UPDATE
|
|
333
|
+
if (def.output == null) {
|
|
334
|
+
let sql = `UPDATE ${table} AS ${alias}`;
|
|
335
|
+
sql += this.renderJoins(def.joins);
|
|
336
|
+
sql += ` SET ${setParts.join(", ")}`;
|
|
337
|
+
sql += this.renderWhere(def.where);
|
|
338
|
+
if (def.limit != null || def.top != null) {
|
|
339
|
+
sql += this.renderLimit(def.limit, def.top);
|
|
340
|
+
}
|
|
341
|
+
return { sql };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// OUTPUT 필요: multi-statement (임시table에 PK 저장 + UPDATE + SELECT + DROP)
|
|
345
|
+
const outputCols = def.output.columns.map((c) => `${alias}.${this.expr.wrap(c)}`).join(", ");
|
|
346
|
+
const tempTableName = this.expr.wrap("SD_TEMP_" + Uuid.new().toString().replace(/-/g, ""));
|
|
347
|
+
|
|
348
|
+
// UPDATE 대상 PK를 임시 Table에 저장 (UPDATE 후 WHERE condition이 달라질 수 있으므로)
|
|
349
|
+
const pkSelectCols = def.output.pkColNames
|
|
350
|
+
.map((pk) => `${alias}.${this.expr.wrap(pk)} AS ${this.expr.wrap(pk)}`)
|
|
351
|
+
.join(", ");
|
|
352
|
+
let createTempSql = `CREATE TEMPORARY TABLE ${tempTableName} AS SELECT ${pkSelectCols} FROM ${table} AS ${alias}`;
|
|
353
|
+
createTempSql += this.renderJoins(def.joins);
|
|
354
|
+
createTempSql += this.renderWhere(def.where);
|
|
355
|
+
|
|
356
|
+
// UPDATE
|
|
357
|
+
let updateSql = `UPDATE ${table} AS ${alias}`;
|
|
358
|
+
updateSql += this.renderJoins(def.joins);
|
|
359
|
+
updateSql += ` SET ${setParts.join(", ")}`;
|
|
360
|
+
updateSql += this.renderWhere(def.where);
|
|
361
|
+
if (def.top != null) updateSql += ` LIMIT ${def.top}`;
|
|
362
|
+
|
|
363
|
+
// 임시 Table의 PK로 SELECT (변경된 value 조회)
|
|
364
|
+
const pkConditions = def.output.pkColNames.map((pk) => {
|
|
365
|
+
const wrappedPk = this.expr.wrap(pk);
|
|
366
|
+
return `${alias}.${wrappedPk} = ${tempTableName}.${wrappedPk}`;
|
|
367
|
+
});
|
|
368
|
+
const selectSql = `SELECT ${outputCols} FROM ${table} AS ${alias}, ${tempTableName} WHERE ${pkConditions.join(" AND ")}`;
|
|
369
|
+
|
|
370
|
+
// 임시 Drop table
|
|
371
|
+
const dropSql = `DROP TEMPORARY TABLE ${tempTableName}`;
|
|
372
|
+
|
|
373
|
+
const statements = [createTempSql, updateSql, selectSql, dropSql];
|
|
374
|
+
return { sql: statements.join(";\n"), resultSetIndex: 2 };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
//#endregion
|
|
378
|
+
|
|
379
|
+
//#region ========== DML - DELETE ==========
|
|
380
|
+
|
|
381
|
+
protected delete(def: DeleteQueryDef): QueryBuildResult {
|
|
382
|
+
const table = this.tableName(def.table);
|
|
383
|
+
const alias = this.expr.wrap(def.as);
|
|
384
|
+
|
|
385
|
+
// OUTPUT 불필요: 단순 DELETE
|
|
386
|
+
if (def.output == null) {
|
|
387
|
+
let sql = `DELETE ${alias} FROM ${table} AS ${alias}`;
|
|
388
|
+
sql += this.renderJoins(def.joins);
|
|
389
|
+
sql += this.renderWhere(def.where);
|
|
390
|
+
if (def.limit != null || def.top != null) {
|
|
391
|
+
sql += this.renderLimit(def.limit, def.top);
|
|
392
|
+
}
|
|
393
|
+
return { sql };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// OUTPUT 필요: multi-statement (Delete 전 임시table에 저장 + DELETE + SELECT + DROP)
|
|
397
|
+
const outputCols = def.output.columns.map((c) => `${alias}.${this.expr.wrap(c)}`).join(", ");
|
|
398
|
+
const tempTableName = this.expr.wrap("SD_TEMP_" + Uuid.new().toString().replace(/-/g, ""));
|
|
399
|
+
|
|
400
|
+
// Delete 전 임시 Table에 저장
|
|
401
|
+
let createTempSql = `CREATE TEMPORARY TABLE ${tempTableName} AS SELECT ${outputCols} FROM ${table} AS ${alias}`;
|
|
402
|
+
createTempSql += this.renderJoins(def.joins);
|
|
403
|
+
createTempSql += this.renderWhere(def.where);
|
|
404
|
+
|
|
405
|
+
// DELETE 실행
|
|
406
|
+
let deleteSql = `DELETE ${alias} FROM ${table} AS ${alias}`;
|
|
407
|
+
deleteSql += this.renderJoins(def.joins);
|
|
408
|
+
deleteSql += this.renderWhere(def.where);
|
|
409
|
+
if (def.top != null) deleteSql += ` LIMIT ${def.top}`;
|
|
410
|
+
|
|
411
|
+
// 임시 Table에서 result return
|
|
412
|
+
const selectSql = `SELECT * FROM ${tempTableName}`;
|
|
413
|
+
|
|
414
|
+
// 임시 Drop table
|
|
415
|
+
const dropSql = `DROP TEMPORARY TABLE ${tempTableName}`;
|
|
416
|
+
|
|
417
|
+
const statements = [createTempSql, deleteSql, selectSql, dropSql];
|
|
418
|
+
return { sql: statements.join(";\n"), resultSetIndex: 2 };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
//#endregion
|
|
422
|
+
|
|
423
|
+
//#region ========== DML - UPSERT ==========
|
|
424
|
+
|
|
425
|
+
protected upsert(def: UpsertQueryDef): QueryBuildResult {
|
|
426
|
+
const table = this.tableName(def.table);
|
|
427
|
+
const alias = this.expr.wrap(def.existsSelectQuery.as);
|
|
428
|
+
const existsQuerySql = this.select(def.existsSelectQuery).sql;
|
|
429
|
+
|
|
430
|
+
// UPDATE SET part (alias.column 형태)
|
|
431
|
+
const updateSetParts = Object.entries(def.updateRecord).map(
|
|
432
|
+
([col, e]) => `${alias}.${this.expr.wrap(col)} = ${this.expr.render(e)}`,
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
// INSERT part
|
|
436
|
+
const insertColumns = Object.keys(def.insertRecord);
|
|
437
|
+
const insertColList = insertColumns.map((c) => this.expr.wrap(c)).join(", ");
|
|
438
|
+
const insertValues = insertColumns.map((c) => this.expr.render(def.insertRecord[c])).join(", ");
|
|
439
|
+
|
|
440
|
+
// WHERE condition 추출 (existsSelectQuery의 where)
|
|
441
|
+
const whereCondition =
|
|
442
|
+
def.existsSelectQuery.where != null && def.existsSelectQuery.where.length > 0
|
|
443
|
+
? this.expr.renderWhere(def.existsSelectQuery.where)
|
|
444
|
+
: "1=1";
|
|
445
|
+
|
|
446
|
+
// OUTPUT 불필요: multi-statement (UPDATE + INSERT WHERE NOT EXISTS)
|
|
447
|
+
if (def.output == null) {
|
|
448
|
+
// UPDATE: 존재하면 UPDATE 됨
|
|
449
|
+
// INSERT SELECT WHERE NOT EXISTS: 존재 안하면 INSERT 됨
|
|
450
|
+
const statements = [
|
|
451
|
+
`UPDATE ${table} AS ${alias} SET ${updateSetParts.join(", ")} WHERE ${whereCondition}`,
|
|
452
|
+
`INSERT INTO ${table} (${insertColList}) SELECT ${insertValues} WHERE NOT EXISTS (${existsQuerySql})`,
|
|
453
|
+
];
|
|
454
|
+
return { sql: statements.join(";\n") };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// OUTPUT 필요: multi-statement (CREATE TEMP + UPDATE + INSERT + SELECT + DROP)
|
|
458
|
+
const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
459
|
+
const tempTableName = this.expr.wrap("SD_TEMP_" + Uuid.new().toString().replace(/-/g, ""));
|
|
460
|
+
|
|
461
|
+
// UPDATE 대상 PK를 임시 Table에 저장 (UPDATE 후 WHERE condition이 달라질 수 있으므로)
|
|
462
|
+
const pkSelectCols = def.output.pkColNames.map((pk) => this.expr.wrap(pk)).join(", ");
|
|
463
|
+
const createTempSql = `CREATE TEMPORARY TABLE ${tempTableName} AS SELECT ${pkSelectCols} FROM ${table} AS ${alias} WHERE ${whereCondition}`;
|
|
464
|
+
|
|
465
|
+
// UPDATE (존재하면 Update)
|
|
466
|
+
const updateSql = `UPDATE ${table} AS ${alias} SET ${updateSetParts.join(", ")} WHERE ${whereCondition}`;
|
|
467
|
+
|
|
468
|
+
// INSERT (NOT EXISTS Pattern)
|
|
469
|
+
const insertSql = `INSERT INTO ${table} (${insertColList}) SELECT ${insertValues} WHERE NOT EXISTS (${existsQuerySql})`;
|
|
470
|
+
|
|
471
|
+
// SELECT: UPDATE result 또는 INSERT result 조회 (UNION ALL로 합침)
|
|
472
|
+
// UPDATE 케이스: temp Table의 PK로 조회
|
|
473
|
+
const output = def.output;
|
|
474
|
+
const updatePkConditions = output.pkColNames.map((pk) => {
|
|
475
|
+
const wrappedPk = this.expr.wrap(pk);
|
|
476
|
+
return `${table}.${wrappedPk} IN (SELECT ${wrappedPk} FROM ${tempTableName})`;
|
|
477
|
+
});
|
|
478
|
+
const selectUpdateSql = `SELECT ${outputCols} FROM ${table} WHERE ${updatePkConditions.join(" AND ")}`;
|
|
479
|
+
|
|
480
|
+
// INSERT 케이스: insertRecord의 PK로 조회 (AI면 LAST_INSERT_ID(), 임시 Table이 비어있을 때만)
|
|
481
|
+
const insertPkConditions = output.pkColNames.map((pk) => {
|
|
482
|
+
const wrappedPk = this.expr.wrap(pk);
|
|
483
|
+
if (pk === output.aiColName) {
|
|
484
|
+
return `${wrappedPk} = LAST_INSERT_ID()`;
|
|
485
|
+
}
|
|
486
|
+
const pkExpr = def.insertRecord[pk];
|
|
487
|
+
return `${wrappedPk} = ${this.expr.render(pkExpr)}`;
|
|
488
|
+
});
|
|
489
|
+
const selectInsertSql = `SELECT ${outputCols} FROM ${table} WHERE ${insertPkConditions.join(" AND ")} AND NOT EXISTS (SELECT 1 FROM ${tempTableName})`;
|
|
490
|
+
|
|
491
|
+
const selectSql = `${selectUpdateSql} UNION ALL ${selectInsertSql}`;
|
|
492
|
+
|
|
493
|
+
// DROP
|
|
494
|
+
const dropSql = `DROP TEMPORARY TABLE ${tempTableName}`;
|
|
495
|
+
|
|
496
|
+
const statements = [createTempSql, updateSql, insertSql, selectSql, dropSql];
|
|
497
|
+
return { sql: statements.join(";\n"), resultSetIndex: 3 };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
//#endregion
|
|
501
|
+
|
|
502
|
+
//#region ========== DDL - Table ==========
|
|
503
|
+
|
|
504
|
+
protected createTable(def: CreateTableQueryDef): QueryBuildResult {
|
|
505
|
+
const table = this.tableName(def.table);
|
|
506
|
+
|
|
507
|
+
const colDefs = def.columns.map((col) => {
|
|
508
|
+
let colSql = `${this.expr.wrap(col.name)} ${this.expr.renderDataType(col.dataType)}`;
|
|
509
|
+
|
|
510
|
+
// nullable: true → NULL, else → NOT NULL
|
|
511
|
+
if (col.nullable === true) {
|
|
512
|
+
colSql += " NULL";
|
|
513
|
+
} else {
|
|
514
|
+
colSql += " NOT NULL";
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (col.autoIncrement) {
|
|
518
|
+
colSql += " AUTO_INCREMENT";
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (col.default !== undefined) {
|
|
522
|
+
colSql += ` DEFAULT ${this.expr.escapeValue(col.default)}`;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return colSql;
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// Primary Key with CONSTRAINT name
|
|
529
|
+
if (def.primaryKey != null && def.primaryKey.length > 0) {
|
|
530
|
+
const pkCols = def.primaryKey.map((c) => this.expr.wrap(c)).join(", ");
|
|
531
|
+
const pkName = this.expr.wrap(`PK_${def.table.name}`);
|
|
532
|
+
colDefs.push(`CONSTRAINT ${pkName} PRIMARY KEY (${pkCols})`);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return { sql: `CREATE TABLE ${table} (\n ${colDefs.join(",\n ")}\n)` };
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
protected dropTable(def: DropTableQueryDef): QueryBuildResult {
|
|
539
|
+
return { sql: `DROP TABLE ${this.tableName(def.table)}` };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
protected renameTable(def: RenameTableQueryDef): QueryBuildResult {
|
|
543
|
+
return { sql: `RENAME TABLE ${this.tableName(def.table)} TO ${this.expr.wrap(def.newName)}` };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
protected truncate(def: TruncateQueryDef): QueryBuildResult {
|
|
547
|
+
// MySQL: TRUNCATE는 AUTO_INCREMENT automatic 리셋
|
|
548
|
+
return { sql: `TRUNCATE TABLE ${this.tableName(def.table)}` };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
//#endregion
|
|
552
|
+
|
|
553
|
+
//#region ========== DDL - Column ==========
|
|
554
|
+
|
|
555
|
+
protected addColumn(def: AddColumnQueryDef): QueryBuildResult {
|
|
556
|
+
const table = this.tableName(def.table);
|
|
557
|
+
const col = def.column;
|
|
558
|
+
|
|
559
|
+
let colSql = `${this.expr.wrap(col.name)} ${this.expr.renderDataType(col.dataType)}`;
|
|
560
|
+
|
|
561
|
+
// nullable: true → NULL, else → NOT NULL
|
|
562
|
+
if (col.nullable === true) {
|
|
563
|
+
colSql += " NULL";
|
|
564
|
+
} else {
|
|
565
|
+
colSql += " NOT NULL";
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (col.autoIncrement) {
|
|
569
|
+
colSql += " AUTO_INCREMENT";
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (col.default !== undefined) {
|
|
573
|
+
colSql += ` DEFAULT ${this.expr.escapeValue(col.default)}`;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return { sql: `ALTER TABLE ${table} ADD COLUMN ${colSql}` };
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
protected dropColumn(def: DropColumnQueryDef): QueryBuildResult {
|
|
580
|
+
return {
|
|
581
|
+
sql: `ALTER TABLE ${this.tableName(def.table)} DROP COLUMN ${this.expr.wrap(def.column)}`,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
protected modifyColumn(def: ModifyColumnQueryDef): QueryBuildResult {
|
|
586
|
+
const table = this.tableName(def.table);
|
|
587
|
+
const col = def.column;
|
|
588
|
+
|
|
589
|
+
let colSql = `${this.expr.wrap(col.name)} ${this.expr.renderDataType(col.dataType)}`;
|
|
590
|
+
|
|
591
|
+
// nullable: true → NULL, else → NOT NULL
|
|
592
|
+
if (col.nullable === true) {
|
|
593
|
+
colSql += " NULL";
|
|
594
|
+
} else {
|
|
595
|
+
colSql += " NOT NULL";
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (col.autoIncrement) {
|
|
599
|
+
colSql += " AUTO_INCREMENT";
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (col.default !== undefined) {
|
|
603
|
+
colSql += ` DEFAULT ${this.expr.escapeValue(col.default)}`;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return { sql: `ALTER TABLE ${table} MODIFY COLUMN ${colSql}` };
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
protected renameColumn(def: RenameColumnQueryDef): QueryBuildResult {
|
|
610
|
+
const table = this.tableName(def.table);
|
|
611
|
+
// MySQL 8.0+: RENAME COLUMN 지원
|
|
612
|
+
return {
|
|
613
|
+
sql: `ALTER TABLE ${table} RENAME COLUMN ${this.expr.wrap(def.column)} TO ${this.expr.wrap(def.newName)}`,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
//#endregion
|
|
618
|
+
|
|
619
|
+
//#region ========== DDL - Constraint ==========
|
|
620
|
+
|
|
621
|
+
protected addPk(def: AddPkQueryDef): QueryBuildResult {
|
|
622
|
+
const table = this.tableName(def.table);
|
|
623
|
+
const cols = def.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
624
|
+
return { sql: `ALTER TABLE ${table} ADD PRIMARY KEY (${cols})` };
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
protected dropPk(def: DropPkQueryDef): QueryBuildResult {
|
|
628
|
+
return { sql: `ALTER TABLE ${this.tableName(def.table)} DROP PRIMARY KEY` };
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
protected addFk(def: AddFkQueryDef): QueryBuildResult {
|
|
632
|
+
const table = this.tableName(def.table);
|
|
633
|
+
const fk = def.foreignKey;
|
|
634
|
+
const fkCols = fk.fkColumns.map((c) => this.expr.wrap(c)).join(", ");
|
|
635
|
+
const targetTable = this.tableName(fk.targetTable);
|
|
636
|
+
const targetCols = fk.targetPkColumns.map((c) => this.expr.wrap(c)).join(", ");
|
|
637
|
+
|
|
638
|
+
// MySQL은 FK Add 시 automatic으로 Index 생성하므로 별도 IDX 불필요
|
|
639
|
+
return {
|
|
640
|
+
sql: `ALTER TABLE ${table} ADD CONSTRAINT ${this.expr.wrap(fk.name)} FOREIGN KEY (${fkCols}) REFERENCES ${targetTable} (${targetCols})`,
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
protected dropFk(def: DropFkQueryDef): QueryBuildResult {
|
|
645
|
+
return {
|
|
646
|
+
sql: `ALTER TABLE ${this.tableName(def.table)} DROP FOREIGN KEY ${this.expr.wrap(def.foreignKey)}`,
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
protected addIdx(def: AddIdxQueryDef): QueryBuildResult {
|
|
651
|
+
const table = this.tableName(def.table);
|
|
652
|
+
const idx = def.index;
|
|
653
|
+
const cols = idx.columns.map((c) => `${this.expr.wrap(c.name)} ${c.orderBy}`).join(", ");
|
|
654
|
+
const unique = idx.unique ? "UNIQUE " : "";
|
|
655
|
+
return { sql: `CREATE ${unique}INDEX ${this.expr.wrap(idx.name)} ON ${table} (${cols})` };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
protected dropIdx(def: DropIdxQueryDef): QueryBuildResult {
|
|
659
|
+
return { sql: `DROP INDEX ${this.expr.wrap(def.index)} ON ${this.tableName(def.table)}` };
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
//#endregion
|
|
663
|
+
|
|
664
|
+
//#region ========== DDL - View/Procedure ==========
|
|
665
|
+
|
|
666
|
+
protected createView(def: CreateViewQueryDef): QueryBuildResult {
|
|
667
|
+
const view = this.tableName(def.view);
|
|
668
|
+
const selectSql = this.select(def.queryDef).sql;
|
|
669
|
+
return { sql: `CREATE OR REPLACE VIEW ${view} AS ${selectSql}` };
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
protected dropView(def: DropViewQueryDef): QueryBuildResult {
|
|
673
|
+
return { sql: `DROP VIEW IF EXISTS ${this.tableName(def.view)}` };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
protected createProc(def: CreateProcQueryDef): QueryBuildResult {
|
|
677
|
+
const proc = this.tableName(def.procedure);
|
|
678
|
+
|
|
679
|
+
// params processing
|
|
680
|
+
const paramList =
|
|
681
|
+
def.params
|
|
682
|
+
?.map((p) => {
|
|
683
|
+
let sql = `IN ${this.expr.wrap(p.name)} ${this.expr.renderDataType(p.dataType)}`;
|
|
684
|
+
if (p.default !== undefined) {
|
|
685
|
+
sql += ` DEFAULT ${this.expr.escapeValue(p.default)}`;
|
|
686
|
+
}
|
|
687
|
+
return sql;
|
|
688
|
+
})
|
|
689
|
+
.join(", ") ?? "";
|
|
690
|
+
|
|
691
|
+
let sql = `CREATE PROCEDURE ${proc}(${paramList})\n`;
|
|
692
|
+
sql += `BEGIN\n`;
|
|
693
|
+
sql += def.query;
|
|
694
|
+
if (!def.query.trim().endsWith(";")) {
|
|
695
|
+
sql += ";";
|
|
696
|
+
}
|
|
697
|
+
sql += `\nEND`;
|
|
698
|
+
|
|
699
|
+
return { sql };
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
protected dropProc(def: DropProcQueryDef): QueryBuildResult {
|
|
703
|
+
return { sql: `DROP PROCEDURE IF EXISTS ${this.tableName(def.procedure)}` };
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
protected execProc(def: ExecProcQueryDef): QueryBuildResult {
|
|
707
|
+
const proc = this.tableName(def.procedure);
|
|
708
|
+
if (def.params == null || Object.keys(def.params).length === 0) {
|
|
709
|
+
return { sql: `CALL ${proc}()` };
|
|
710
|
+
}
|
|
711
|
+
const params = Object.values(def.params)
|
|
712
|
+
.map((p) => this.expr.render(p))
|
|
713
|
+
.join(", ");
|
|
714
|
+
return { sql: `CALL ${proc}(${params})` };
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
//#endregion
|
|
718
|
+
|
|
719
|
+
//#region ========== Utils ==========
|
|
720
|
+
|
|
721
|
+
protected clearSchema(def: ClearSchemaQueryDef): QueryBuildResult {
|
|
722
|
+
// MySQL: 모든 Table DROP (MySQL에서 database와 schema는 동의어)
|
|
723
|
+
// information_schema에서 Table 목록 조회 후 DROP
|
|
724
|
+
// SQL Injection 방지: 식별자 유효성 Validation
|
|
725
|
+
if (!/^[a-zA-Z0-9_]+$/.test(def.database)) {
|
|
726
|
+
throw new Error(`Invalid database name: ${def.database}`);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const dbName = this.expr.escapeString(def.database);
|
|
730
|
+
return {
|
|
731
|
+
sql: `
|
|
732
|
+
SET FOREIGN_KEY_CHECKS = 0;
|
|
733
|
+
SET @tables = NULL;
|
|
734
|
+
SELECT GROUP_CONCAT(table_name) INTO @tables FROM information_schema.tables WHERE table_schema = '${dbName}';
|
|
735
|
+
SET @drop_stmt = IF(@tables IS NULL, 'SELECT 1', CONCAT('DROP TABLE IF EXISTS ', @tables));
|
|
736
|
+
PREPARE stmt FROM @drop_stmt;
|
|
737
|
+
EXECUTE stmt;
|
|
738
|
+
DEALLOCATE PREPARE stmt;
|
|
739
|
+
SET FOREIGN_KEY_CHECKS = 1`,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
protected schemaExists(def: SchemaExistsQueryDef): QueryBuildResult {
|
|
744
|
+
// MySQL: database와 schema는 동의어
|
|
745
|
+
const dbName = this.expr.escapeString(def.database);
|
|
746
|
+
return {
|
|
747
|
+
sql: `SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = '${dbName}'`,
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/** MySQL은 전역 설정만 지원 (table 파라미터 무시됨) */
|
|
752
|
+
protected switchFk(def: SwitchFkQueryDef): QueryBuildResult {
|
|
753
|
+
return def.switch === "on"
|
|
754
|
+
? { sql: "SET FOREIGN_KEY_CHECKS = 1" }
|
|
755
|
+
: { sql: "SET FOREIGN_KEY_CHECKS = 0" };
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
//#endregion
|
|
759
|
+
}
|