@simplysm/orm-common 13.0.100 → 14.0.4
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 +90 -147
- package/dist/create-db-context.d.ts +10 -10
- package/dist/create-db-context.js +312 -276
- package/dist/create-db-context.js.map +1 -6
- package/dist/ddl/column-ddl.d.ts +4 -4
- package/dist/ddl/column-ddl.js +41 -35
- package/dist/ddl/column-ddl.js.map +1 -6
- package/dist/ddl/initialize.d.ts +17 -17
- package/dist/ddl/initialize.js +200 -142
- package/dist/ddl/initialize.js.map +1 -6
- package/dist/ddl/relation-ddl.d.ts +6 -6
- package/dist/ddl/relation-ddl.js +55 -48
- package/dist/ddl/relation-ddl.js.map +1 -6
- package/dist/ddl/schema-ddl.d.ts +4 -4
- package/dist/ddl/schema-ddl.js +21 -15
- package/dist/ddl/schema-ddl.js.map +1 -6
- package/dist/ddl/table-ddl.d.ts +20 -20
- package/dist/ddl/table-ddl.js +139 -93
- package/dist/ddl/table-ddl.js.map +1 -6
- package/dist/define-db-context.js +10 -13
- package/dist/define-db-context.js.map +1 -6
- package/dist/errors/db-transaction-error.d.ts +15 -15
- package/dist/errors/db-transaction-error.d.ts.map +1 -1
- package/dist/errors/db-transaction-error.js +53 -19
- package/dist/errors/db-transaction-error.js.map +1 -6
- package/dist/exec/executable.d.ts +23 -23
- package/dist/exec/executable.js +94 -40
- package/dist/exec/executable.js.map +1 -6
- package/dist/exec/queryable.d.ts +97 -97
- package/dist/exec/queryable.js +1310 -1204
- package/dist/exec/queryable.js.map +1 -6
- package/dist/exec/search-parser.d.ts +31 -31
- package/dist/exec/search-parser.d.ts.map +1 -1
- package/dist/exec/search-parser.js +158 -59
- package/dist/exec/search-parser.js.map +1 -6
- package/dist/expr/expr-unit.d.ts +4 -4
- package/dist/expr/expr-unit.js +24 -18
- package/dist/expr/expr-unit.js.map +1 -6
- package/dist/expr/expr.d.ts +108 -108
- package/dist/expr/expr.js +1872 -1844
- package/dist/expr/expr.js.map +1 -6
- package/dist/index.js +23 -1
- package/dist/index.js.map +1 -6
- package/dist/models/system-migration.js +7 -7
- package/dist/models/system-migration.js.map +1 -6
- package/dist/query-builder/base/expr-renderer-base.d.ts +10 -10
- package/dist/query-builder/base/expr-renderer-base.js +27 -21
- package/dist/query-builder/base/expr-renderer-base.js.map +1 -6
- package/dist/query-builder/base/query-builder-base.d.ts +21 -21
- package/dist/query-builder/base/query-builder-base.d.ts.map +1 -1
- package/dist/query-builder/base/query-builder-base.js +90 -80
- package/dist/query-builder/base/query-builder-base.js.map +1 -6
- package/dist/query-builder/mssql/mssql-expr-renderer.d.ts +5 -5
- package/dist/query-builder/mssql/mssql-expr-renderer.d.ts.map +1 -1
- package/dist/query-builder/mssql/mssql-expr-renderer.js +447 -420
- package/dist/query-builder/mssql/mssql-expr-renderer.js.map +1 -6
- 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 +483 -443
- package/dist/query-builder/mssql/mssql-query-builder.js.map +1 -6
- package/dist/query-builder/mysql/mysql-expr-renderer.d.ts +5 -5
- package/dist/query-builder/mysql/mysql-expr-renderer.d.ts.map +1 -1
- package/dist/query-builder/mysql/mysql-expr-renderer.js +451 -419
- package/dist/query-builder/mysql/mysql-expr-renderer.js.map +1 -6
- package/dist/query-builder/mysql/mysql-query-builder.d.ts +10 -10
- package/dist/query-builder/mysql/mysql-query-builder.d.ts.map +1 -1
- package/dist/query-builder/mysql/mysql-query-builder.js +570 -479
- package/dist/query-builder/mysql/mysql-query-builder.js.map +1 -6
- package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts +5 -5
- package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts.map +1 -1
- package/dist/query-builder/postgresql/postgresql-expr-renderer.js +449 -422
- package/dist/query-builder/postgresql/postgresql-expr-renderer.js.map +1 -6
- package/dist/query-builder/postgresql/postgresql-query-builder.d.ts +8 -8
- package/dist/query-builder/postgresql/postgresql-query-builder.d.ts.map +1 -1
- package/dist/query-builder/postgresql/postgresql-query-builder.js +511 -460
- package/dist/query-builder/postgresql/postgresql-query-builder.js.map +1 -6
- package/dist/query-builder/query-builder.d.ts +1 -1
- package/dist/query-builder/query-builder.js +13 -13
- package/dist/query-builder/query-builder.js.map +1 -6
- package/dist/schema/factory/column-builder.d.ts +84 -84
- package/dist/schema/factory/column-builder.js +248 -185
- package/dist/schema/factory/column-builder.js.map +1 -6
- package/dist/schema/factory/index-builder.d.ts +38 -38
- package/dist/schema/factory/index-builder.js +144 -85
- package/dist/schema/factory/index-builder.js.map +1 -6
- 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 +274 -136
- package/dist/schema/factory/relation-builder.js.map +1 -6
- package/dist/schema/procedure-builder.d.ts +51 -51
- package/dist/schema/procedure-builder.d.ts.map +1 -1
- package/dist/schema/procedure-builder.js +205 -131
- package/dist/schema/procedure-builder.js.map +1 -6
- package/dist/schema/table-builder.d.ts +55 -55
- package/dist/schema/table-builder.d.ts.map +1 -1
- package/dist/schema/table-builder.js +274 -205
- package/dist/schema/table-builder.js.map +1 -6
- package/dist/schema/view-builder.d.ts +44 -44
- package/dist/schema/view-builder.d.ts.map +1 -1
- package/dist/schema/view-builder.js +189 -116
- package/dist/schema/view-builder.js.map +1 -6
- package/dist/types/column.d.ts +21 -21
- package/dist/types/column.js +60 -30
- package/dist/types/column.js.map +1 -6
- package/dist/types/db-context-def.d.ts +9 -9
- package/dist/types/db-context-def.js +2 -1
- package/dist/types/db-context-def.js.map +1 -6
- package/dist/types/db.d.ts +47 -47
- package/dist/types/db.js +15 -5
- package/dist/types/db.js.map +1 -6
- package/dist/types/expr.d.ts +81 -81
- package/dist/types/expr.d.ts.map +1 -1
- package/dist/types/expr.js +3 -1
- package/dist/types/expr.js.map +1 -6
- package/dist/types/query-def.d.ts +46 -46
- package/dist/types/query-def.d.ts.map +1 -1
- package/dist/types/query-def.js +31 -24
- package/dist/types/query-def.js.map +1 -6
- package/dist/utils/result-parser.d.ts +11 -11
- package/dist/utils/result-parser.js +362 -221
- package/dist/utils/result-parser.js.map +1 -6
- package/docs/core.md +117 -145
- package/docs/expression.md +186 -203
- package/docs/query-builder.md +75 -42
- package/docs/queryable.md +189 -151
- package/docs/schema-builders.md +172 -283
- package/docs/types.md +229 -173
- package/package.json +7 -5
- package/src/create-db-context.ts +31 -31
- package/src/ddl/column-ddl.ts +4 -4
- package/src/ddl/initialize.ts +38 -38
- package/src/ddl/relation-ddl.ts +6 -6
- package/src/ddl/schema-ddl.ts +4 -4
- package/src/ddl/table-ddl.ts +24 -24
- package/src/errors/db-transaction-error.ts +13 -13
- package/src/exec/executable.ts +25 -25
- package/src/exec/queryable.ts +152 -152
- package/src/exec/search-parser.ts +50 -50
- package/src/expr/expr-unit.ts +4 -4
- package/src/expr/expr.ts +118 -118
- package/src/index.ts +8 -8
- package/src/models/system-migration.ts +1 -1
- package/src/query-builder/base/expr-renderer-base.ts +21 -21
- package/src/query-builder/base/query-builder-base.ts +33 -33
- package/src/query-builder/mssql/mssql-expr-renderer.ts +28 -28
- package/src/query-builder/mssql/mssql-query-builder.ts +37 -37
- package/src/query-builder/mysql/mysql-expr-renderer.ts +29 -29
- package/src/query-builder/mysql/mysql-query-builder.ts +70 -70
- package/src/query-builder/postgresql/postgresql-expr-renderer.ts +22 -22
- package/src/query-builder/postgresql/postgresql-query-builder.ts +54 -54
- package/src/query-builder/query-builder.ts +1 -1
- package/src/schema/factory/column-builder.ts +86 -86
- package/src/schema/factory/index-builder.ts +38 -38
- package/src/schema/factory/relation-builder.ts +102 -102
- package/src/schema/procedure-builder.ts +52 -52
- package/src/schema/table-builder.ts +56 -56
- package/src/schema/view-builder.ts +47 -47
- package/src/types/column.ts +24 -24
- package/src/types/db-context-def.ts +15 -15
- package/src/types/db.ts +50 -50
- package/src/types/expr.ts +103 -103
- package/src/types/query-def.ts +50 -50
- package/src/utils/result-parser.ts +88 -88
- package/docs/utilities.md +0 -27
- package/tests/db-context/create-db-context.spec.ts +0 -193
- package/tests/db-context/define-db-context.spec.ts +0 -17
- package/tests/ddl/basic.expected.ts +0 -341
- package/tests/ddl/basic.spec.ts +0 -557
- package/tests/ddl/column-builder.expected.ts +0 -310
- package/tests/ddl/column-builder.spec.ts +0 -525
- package/tests/ddl/index-builder.expected.ts +0 -38
- package/tests/ddl/index-builder.spec.ts +0 -148
- package/tests/ddl/procedure-builder.expected.ts +0 -52
- package/tests/ddl/procedure-builder.spec.ts +0 -128
- package/tests/ddl/relation-builder.expected.ts +0 -36
- package/tests/ddl/relation-builder.spec.ts +0 -171
- package/tests/ddl/table-builder.expected.ts +0 -113
- package/tests/ddl/table-builder.spec.ts +0 -399
- package/tests/ddl/view-builder.expected.ts +0 -38
- package/tests/ddl/view-builder.spec.ts +0 -116
- package/tests/dml/delete.expected.ts +0 -96
- package/tests/dml/delete.spec.ts +0 -127
- package/tests/dml/insert.expected.ts +0 -192
- package/tests/dml/insert.spec.ts +0 -210
- package/tests/dml/update.expected.ts +0 -176
- package/tests/dml/update.spec.ts +0 -222
- package/tests/dml/upsert.expected.ts +0 -215
- package/tests/dml/upsert.spec.ts +0 -190
- package/tests/errors/queryable-errors.spec.ts +0 -126
- package/tests/escape.spec.ts +0 -59
- package/tests/examples/pivot.expected.ts +0 -211
- package/tests/examples/pivot.spec.ts +0 -200
- package/tests/examples/sampling.expected.ts +0 -69
- package/tests/examples/sampling.spec.ts +0 -42
- package/tests/examples/unpivot.expected.ts +0 -120
- package/tests/examples/unpivot.spec.ts +0 -161
- package/tests/exec/search-parser.spec.ts +0 -267
- package/tests/executable/basic.expected.ts +0 -18
- package/tests/executable/basic.spec.ts +0 -54
- package/tests/expr/comparison.expected.ts +0 -282
- package/tests/expr/comparison.spec.ts +0 -334
- package/tests/expr/conditional.expected.ts +0 -134
- package/tests/expr/conditional.spec.ts +0 -249
- package/tests/expr/date.expected.ts +0 -332
- package/tests/expr/date.spec.ts +0 -459
- package/tests/expr/math.expected.ts +0 -62
- package/tests/expr/math.spec.ts +0 -59
- package/tests/expr/string.expected.ts +0 -218
- package/tests/expr/string.spec.ts +0 -300
- package/tests/expr/utility.expected.ts +0 -147
- package/tests/expr/utility.spec.ts +0 -155
- package/tests/select/basic.expected.ts +0 -322
- package/tests/select/basic.spec.ts +0 -433
- package/tests/select/filter.expected.ts +0 -357
- package/tests/select/filter.spec.ts +0 -954
- package/tests/select/group.expected.ts +0 -169
- package/tests/select/group.spec.ts +0 -159
- package/tests/select/join.expected.ts +0 -582
- package/tests/select/join.spec.ts +0 -692
- package/tests/select/order.expected.ts +0 -150
- package/tests/select/order.spec.ts +0 -140
- package/tests/select/recursive-cte.expected.ts +0 -244
- package/tests/select/recursive-cte.spec.ts +0 -514
- package/tests/select/result-meta.spec.ts +0 -270
- package/tests/select/subquery.expected.ts +0 -363
- package/tests/select/subquery.spec.ts +0 -441
- package/tests/select/view.expected.ts +0 -155
- package/tests/select/view.spec.ts +0 -235
- package/tests/select/window.expected.ts +0 -345
- package/tests/select/window.spec.ts +0 -433
- package/tests/setup/MockExecutor.ts +0 -18
- package/tests/setup/TestDbContext.ts +0 -59
- package/tests/setup/models/Company.ts +0 -13
- package/tests/setup/models/Employee.ts +0 -10
- package/tests/setup/models/MonthlySales.ts +0 -11
- package/tests/setup/models/Post.ts +0 -16
- package/tests/setup/models/Sales.ts +0 -10
- package/tests/setup/models/User.ts +0 -19
- package/tests/setup/procedure/GetAllUsers.ts +0 -9
- package/tests/setup/procedure/GetUserById.ts +0 -12
- package/tests/setup/test-utils.ts +0 -72
- package/tests/setup/views/ActiveUsers.ts +0 -8
- package/tests/setup/views/UserSummary.ts +0 -11
- package/tests/types/nullable-queryable-record.spec.ts +0 -97
- package/tests/utils/result-parser-perf.spec.ts +0 -143
- package/tests/utils/result-parser.spec.ts +0 -667
|
@@ -1,484 +1,575 @@
|
|
|
1
1
|
import { Uuid } from "@simplysm/core-common";
|
|
2
2
|
import { QueryBuilderBase } from "../base/query-builder-base.js";
|
|
3
3
|
import { MysqlExprRenderer } from "./mysql-expr-renderer.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const whereForSelect = output.pkColNames.map((pk) => {
|
|
95
|
-
const wrappedPk = this.expr.wrap(pk);
|
|
96
|
-
if (pk === output.aiColName) {
|
|
97
|
-
return `${wrappedPk} = LAST_INSERT_ID()`;
|
|
98
|
-
}
|
|
99
|
-
return `${wrappedPk} = ${this.expr.escapeValue(record[pk])}`;
|
|
100
|
-
});
|
|
101
|
-
statements.push(`SELECT ${outputCols} FROM ${table} WHERE ${whereForSelect.join(" AND ")}`);
|
|
102
|
-
}
|
|
103
|
-
return {
|
|
104
|
-
sql: statements.join(";\n"),
|
|
105
|
-
resultSetIndex: 1,
|
|
106
|
-
resultSetStride: 2
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
insertIfNotExists(def) {
|
|
110
|
-
const table = this.tableName(def.table);
|
|
111
|
-
const columns = Object.keys(def.record);
|
|
112
|
-
const colList = columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
113
|
-
const values = columns.map((c) => this.expr.escapeValue(def.record[c])).join(", ");
|
|
114
|
-
const existsQuerySql = this.select({
|
|
115
|
-
...def.existsSelectQuery,
|
|
116
|
-
select: { _: { type: "value", value: 1 } }
|
|
117
|
-
}).sql;
|
|
118
|
-
if (def.output == null) {
|
|
119
|
-
const sql = `INSERT INTO ${table} (${colList}) SELECT ${values} WHERE NOT EXISTS (${existsQuerySql})`;
|
|
120
|
-
return { sql };
|
|
121
|
-
}
|
|
122
|
-
const output = def.output;
|
|
123
|
-
const outputCols = output.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
124
|
-
const whereForSelect = output.pkColNames.map((pk) => {
|
|
125
|
-
const wrappedPk = this.expr.wrap(pk);
|
|
126
|
-
if (pk === output.aiColName) {
|
|
127
|
-
return `${wrappedPk} = LAST_INSERT_ID()`;
|
|
128
|
-
}
|
|
129
|
-
return `${wrappedPk} = ${this.expr.escapeValue(def.record[pk])}`;
|
|
130
|
-
});
|
|
131
|
-
const statements = [
|
|
132
|
-
`INSERT INTO ${table} (${colList}) SELECT ${values} WHERE NOT EXISTS (${existsQuerySql})`,
|
|
133
|
-
`SET @sd_affected = ROW_COUNT()`,
|
|
134
|
-
`SELECT ${outputCols} FROM ${table} WHERE ${whereForSelect.join(" AND ")} AND @sd_affected > 0`
|
|
135
|
-
];
|
|
136
|
-
return { sql: statements.join(";\n"), resultSetIndex: 2 };
|
|
137
|
-
}
|
|
138
|
-
insertInto(def) {
|
|
139
|
-
const table = this.tableName(def.table);
|
|
140
|
-
const selectSql = this.select(def.recordsSelectQuery).sql;
|
|
141
|
-
const selectDef = def.recordsSelectQuery;
|
|
142
|
-
const colList = selectDef.select != null ? Object.keys(selectDef.select).map((c) => this.expr.wrap(c)).join(", ") : "*";
|
|
143
|
-
if (def.output == null) {
|
|
144
|
-
return { sql: `INSERT INTO ${table} (${colList}) ${selectSql}` };
|
|
145
|
-
}
|
|
146
|
-
const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
147
|
-
if (def.output.aiColName != null) {
|
|
148
|
-
const aiCol = this.expr.wrap(def.output.aiColName);
|
|
149
|
-
const statements2 = [
|
|
150
|
-
`INSERT INTO ${table} (${colList}) ${selectSql}`,
|
|
151
|
-
`SET @sd_first_id = LAST_INSERT_ID(), @sd_count = ROW_COUNT()`,
|
|
152
|
-
`SELECT ${outputCols} FROM ${table} WHERE ${aiCol} >= @sd_first_id AND ${aiCol} < @sd_first_id + @sd_count`
|
|
153
|
-
];
|
|
154
|
-
return { sql: statements2.join(";\n"), resultSetIndex: 2 };
|
|
155
|
-
}
|
|
156
|
-
const tempTableName = this.expr.wrap("SD_TEMP_" + Uuid.generate().toString().replace(/-/g, ""));
|
|
157
|
-
const pkSelectDef = {
|
|
158
|
-
...def.recordsSelectQuery,
|
|
159
|
-
select: Object.fromEntries(
|
|
160
|
-
def.output.pkColNames.map((pk) => [pk, def.recordsSelectQuery.select[pk]])
|
|
161
|
-
)
|
|
162
|
-
};
|
|
163
|
-
const pkSelectSql = this.select(pkSelectDef).sql;
|
|
164
|
-
const pkConditions = def.output.pkColNames.map((pk) => {
|
|
165
|
-
const wrappedPk = this.expr.wrap(pk);
|
|
166
|
-
return `${table}.${wrappedPk} = ${tempTableName}.${wrappedPk}`;
|
|
167
|
-
});
|
|
168
|
-
const statements = [
|
|
169
|
-
`CREATE TEMPORARY TABLE ${tempTableName} AS ${pkSelectSql}`,
|
|
170
|
-
`INSERT INTO ${table} (${colList}) ${selectSql}`,
|
|
171
|
-
`SELECT ${outputCols} FROM ${table}, ${tempTableName} WHERE ${pkConditions.join(" AND ")}`,
|
|
172
|
-
`DROP TEMPORARY TABLE ${tempTableName}`
|
|
173
|
-
];
|
|
174
|
-
return { sql: statements.join(";\n"), resultSetIndex: 2 };
|
|
175
|
-
}
|
|
176
|
-
//#endregion
|
|
177
|
-
//#region ========== DML - UPDATE ==========
|
|
178
|
-
update(def) {
|
|
179
|
-
const table = this.tableName(def.table);
|
|
180
|
-
const alias = this.expr.wrap(def.as);
|
|
181
|
-
const setParts = Object.entries(def.record).map(
|
|
182
|
-
([col, expr]) => `${alias}.${this.expr.wrap(col)} = ${this.expr.render(expr)}`
|
|
183
|
-
);
|
|
184
|
-
if (def.output == null) {
|
|
185
|
-
let sql = `UPDATE ${table} AS ${alias}`;
|
|
186
|
-
sql += this.renderJoins(def.joins);
|
|
187
|
-
sql += ` SET ${setParts.join(", ")}`;
|
|
188
|
-
sql += this.renderWhere(def.where);
|
|
189
|
-
if (def.limit != null || def.top != null) {
|
|
190
|
-
sql += this.renderLimit(def.limit, def.top);
|
|
191
|
-
}
|
|
192
|
-
return { sql };
|
|
193
|
-
}
|
|
194
|
-
const outputCols = def.output.columns.map((c) => `${alias}.${this.expr.wrap(c)}`).join(", ");
|
|
195
|
-
const tempTableName = this.expr.wrap("SD_TEMP_" + Uuid.generate().toString().replace(/-/g, ""));
|
|
196
|
-
const pkSelectCols = def.output.pkColNames.map((pk) => `${alias}.${this.expr.wrap(pk)} AS ${this.expr.wrap(pk)}`).join(", ");
|
|
197
|
-
let createTempSql = `CREATE TEMPORARY TABLE ${tempTableName} AS SELECT ${pkSelectCols} FROM ${table} AS ${alias}`;
|
|
198
|
-
createTempSql += this.renderJoins(def.joins);
|
|
199
|
-
createTempSql += this.renderWhere(def.where);
|
|
200
|
-
let updateSql = `UPDATE ${table} AS ${alias}`;
|
|
201
|
-
updateSql += this.renderJoins(def.joins);
|
|
202
|
-
updateSql += ` SET ${setParts.join(", ")}`;
|
|
203
|
-
updateSql += this.renderWhere(def.where);
|
|
204
|
-
if (def.top != null) updateSql += ` LIMIT ${def.top}`;
|
|
205
|
-
const pkConditions = def.output.pkColNames.map((pk) => {
|
|
206
|
-
const wrappedPk = this.expr.wrap(pk);
|
|
207
|
-
return `${alias}.${wrappedPk} = ${tempTableName}.${wrappedPk}`;
|
|
208
|
-
});
|
|
209
|
-
const selectSql = `SELECT ${outputCols} FROM ${table} AS ${alias}, ${tempTableName} WHERE ${pkConditions.join(" AND ")}`;
|
|
210
|
-
const dropSql = `DROP TEMPORARY TABLE ${tempTableName}`;
|
|
211
|
-
const statements = [createTempSql, updateSql, selectSql, dropSql];
|
|
212
|
-
return { sql: statements.join(";\n"), resultSetIndex: 2 };
|
|
213
|
-
}
|
|
214
|
-
//#endregion
|
|
215
|
-
//#region ========== DML - DELETE ==========
|
|
216
|
-
delete(def) {
|
|
217
|
-
const table = this.tableName(def.table);
|
|
218
|
-
const alias = this.expr.wrap(def.as);
|
|
219
|
-
if (def.output == null) {
|
|
220
|
-
let sql = `DELETE ${alias} FROM ${table} AS ${alias}`;
|
|
221
|
-
sql += this.renderJoins(def.joins);
|
|
222
|
-
sql += this.renderWhere(def.where);
|
|
223
|
-
if (def.limit != null || def.top != null) {
|
|
4
|
+
/**
|
|
5
|
+
* MySQL QueryBuilder
|
|
6
|
+
*
|
|
7
|
+
* MySQL 고유 사항:
|
|
8
|
+
* - OUTPUT 미지원: multi-statement 패턴으로 우회 (INSERT + SET @var + SELECT)
|
|
9
|
+
* - INSERT OUTPUT: AI column은 LAST_INSERT_ID() 사용, 비 AI는 레코드에서 PK 추출
|
|
10
|
+
* - UPDATE/UPSERT OUTPUT: UPDATE 후 WHERE 조건이 변경될 수 있으므로 먼저 PK를 임시 테이블에 저장 후 SELECT
|
|
11
|
+
* - DELETE OUTPUT: 삭제 전 output column을 임시 테이블에 저장
|
|
12
|
+
* - switchFk: 전역 설정 (SET FOREIGN_KEY_CHECKS), table 파라미터 무시
|
|
13
|
+
* - FK 추가 시 인덱스 자동 생성
|
|
14
|
+
*/
|
|
15
|
+
export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
16
|
+
expr = new MysqlExprRenderer((def) => this.select(def).sql);
|
|
17
|
+
//#region ========== Utilities ==========
|
|
18
|
+
/** 테이블명 렌더링 (MySQL: schema 무시, database.table만 사용) */
|
|
19
|
+
tableName(obj) {
|
|
20
|
+
if (obj.database != null) {
|
|
21
|
+
return `${this.expr.wrap(obj.database)}.${this.expr.wrap(obj.name)}`;
|
|
22
|
+
}
|
|
23
|
+
return this.expr.wrap(obj.name);
|
|
24
|
+
}
|
|
25
|
+
/** LIMIT 절 렌더링 */
|
|
26
|
+
renderLimit(limit, top) {
|
|
27
|
+
if (limit != null) {
|
|
28
|
+
const [offset, count] = limit;
|
|
29
|
+
return ` LIMIT ${offset}, ${count}`;
|
|
30
|
+
}
|
|
31
|
+
if (top != null) {
|
|
32
|
+
return ` LIMIT ${top}`;
|
|
33
|
+
}
|
|
34
|
+
return "";
|
|
35
|
+
}
|
|
36
|
+
renderJoin(join) {
|
|
37
|
+
const alias = this.expr.wrap(join.as);
|
|
38
|
+
// LATERAL JOIN이 필요한지 감지
|
|
39
|
+
if (this.needsLateral(join)) {
|
|
40
|
+
// from이 배열(UNION ALL)이면 renderFrom(join.from) 사용,
|
|
41
|
+
// 그 외(orderBy, top, select 등)이면 renderFrom(join)으로 서브쿼리 생성
|
|
42
|
+
const from = Array.isArray(join.from) ? this.renderFrom(join.from) : this.renderFrom(join);
|
|
43
|
+
return ` LEFT OUTER JOIN LATERAL ${from} AS ${alias} ON TRUE`;
|
|
44
|
+
}
|
|
45
|
+
// 일반 JOIN
|
|
46
|
+
const from = this.renderFrom(join.from);
|
|
47
|
+
const where = join.where != null && join.where.length > 0
|
|
48
|
+
? ` ON ${this.expr.renderWhere(join.where)}`
|
|
49
|
+
: " ON TRUE";
|
|
50
|
+
return ` LEFT OUTER JOIN ${from} AS ${alias}${where}`;
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region ========== DML - SELECT ==========
|
|
54
|
+
select(def) {
|
|
55
|
+
// WITH (CTE)
|
|
56
|
+
let sql = "";
|
|
57
|
+
if (def.with != null) {
|
|
58
|
+
const { name, base, recursive } = def.with;
|
|
59
|
+
sql += `WITH ${this.expr.wrap(name)} AS (${this.select(base).sql} UNION ALL ${this.select(recursive).sql}) `;
|
|
60
|
+
}
|
|
61
|
+
// SELECT
|
|
62
|
+
sql += "SELECT";
|
|
63
|
+
if (def.distinct) {
|
|
64
|
+
sql += " DISTINCT";
|
|
65
|
+
}
|
|
66
|
+
// columns
|
|
67
|
+
if (def.select != null) {
|
|
68
|
+
const cols = Object.entries(def.select).map(([alias, expr]) => `${this.expr.render(expr)} AS ${this.expr.wrap(alias)}`);
|
|
69
|
+
sql += ` ${cols.join(", ")}`;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
sql += " *";
|
|
73
|
+
}
|
|
74
|
+
// FROM
|
|
75
|
+
if (def.from != null) {
|
|
76
|
+
const from = this.renderFrom(def.from);
|
|
77
|
+
sql += ` FROM ${from} AS ${this.expr.wrap(def.as)}`;
|
|
78
|
+
}
|
|
79
|
+
// LOCK
|
|
80
|
+
if (def.lock) {
|
|
81
|
+
// MySQL: SELECT ... FOR UPDATE (끝에 추가)
|
|
82
|
+
}
|
|
83
|
+
// JOINs
|
|
84
|
+
sql += this.renderJoins(def.joins);
|
|
85
|
+
// WHERE
|
|
86
|
+
sql += this.renderWhere(def.where);
|
|
87
|
+
// GROUP BY
|
|
88
|
+
sql += this.renderGroupBy(def.groupBy);
|
|
89
|
+
// HAVING
|
|
90
|
+
sql += this.renderHaving(def.having);
|
|
91
|
+
// ORDER BY
|
|
92
|
+
sql += this.renderOrderBy(def.orderBy);
|
|
93
|
+
// LIMIT
|
|
224
94
|
sql += this.renderLimit(def.limit, def.top);
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
)
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
`;
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
SET
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
95
|
+
// LOCK (끝에 FOR UPDATE 추가)
|
|
96
|
+
if (def.lock) {
|
|
97
|
+
sql += " FOR UPDATE";
|
|
98
|
+
}
|
|
99
|
+
return { sql };
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region ========== DML - INSERT ==========
|
|
103
|
+
insert(def) {
|
|
104
|
+
const table = this.tableName(def.table);
|
|
105
|
+
if (def.records.length === 0) {
|
|
106
|
+
throw new Error("INSERT에는 최소 1개의 레코드가 필요합니다.");
|
|
107
|
+
}
|
|
108
|
+
const columns = Object.keys(def.records[0]);
|
|
109
|
+
const colList = columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
110
|
+
// OUTPUT 불필요: 단순 배치 INSERT
|
|
111
|
+
if (def.output == null) {
|
|
112
|
+
const valuesList = def.records.map((record) => {
|
|
113
|
+
const values = columns.map((c) => this.expr.escapeValue(record[c]));
|
|
114
|
+
return `(${values.join(", ")})`;
|
|
115
|
+
});
|
|
116
|
+
return { sql: `INSERT INTO ${table} (${colList}) VALUES ${valuesList.join(", ")}` };
|
|
117
|
+
}
|
|
118
|
+
// OUTPUT 필요: multi-statement로 INSERT + SELECT 실행
|
|
119
|
+
// 결과 셋: [INSERT 결과, SELECT 결과, INSERT 결과, SELECT 결과, ...]
|
|
120
|
+
// → resultSetIndex=1, resultSetStride=2로 SELECT 결과만 추출
|
|
121
|
+
const output = def.output;
|
|
122
|
+
const outputCols = output.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
123
|
+
const statements = [];
|
|
124
|
+
for (const record of def.records) {
|
|
125
|
+
const values = columns.map((c) => this.expr.escapeValue(record[c])).join(", ");
|
|
126
|
+
statements.push(`INSERT INTO ${table} (${colList}) VALUES (${values})`);
|
|
127
|
+
// PK로 SELECT (aiColName에는 LAST_INSERT_ID() 사용)
|
|
128
|
+
const whereForSelect = output.pkColNames.map((pk) => {
|
|
129
|
+
const wrappedPk = this.expr.wrap(pk);
|
|
130
|
+
if (pk === output.aiColName) {
|
|
131
|
+
return `${wrappedPk} = LAST_INSERT_ID()`;
|
|
132
|
+
}
|
|
133
|
+
return `${wrappedPk} = ${this.expr.escapeValue(record[pk])}`;
|
|
134
|
+
});
|
|
135
|
+
statements.push(`SELECT ${outputCols} FROM ${table} WHERE ${whereForSelect.join(" AND ")}`);
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
sql: statements.join(";\n"),
|
|
139
|
+
resultSetIndex: 1,
|
|
140
|
+
resultSetStride: 2,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
insertIfNotExists(def) {
|
|
144
|
+
const table = this.tableName(def.table);
|
|
145
|
+
const columns = Object.keys(def.record);
|
|
146
|
+
const colList = columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
147
|
+
const values = columns.map((c) => this.expr.escapeValue(def.record[c])).join(", ");
|
|
148
|
+
// existsSelectQuery를 SELECT 1 AS _로 렌더링
|
|
149
|
+
const existsQuerySql = this.select({
|
|
150
|
+
...def.existsSelectQuery,
|
|
151
|
+
select: { _: { type: "value", value: 1 } },
|
|
152
|
+
}).sql;
|
|
153
|
+
// OUTPUT 불필요: 단순 INSERT IF NOT EXISTS
|
|
154
|
+
if (def.output == null) {
|
|
155
|
+
const sql = `INSERT INTO ${table} (${colList}) SELECT ${values} WHERE NOT EXISTS (${existsQuerySql})`;
|
|
156
|
+
return { sql };
|
|
157
|
+
}
|
|
158
|
+
// OUTPUT 필요: multi-statement (INSERT + SET @affected + SELECT)
|
|
159
|
+
const output = def.output;
|
|
160
|
+
const outputCols = output.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
161
|
+
// OUTPUT용 SELECT WHERE 조건
|
|
162
|
+
const whereForSelect = output.pkColNames.map((pk) => {
|
|
163
|
+
const wrappedPk = this.expr.wrap(pk);
|
|
164
|
+
if (pk === output.aiColName) {
|
|
165
|
+
return `${wrappedPk} = LAST_INSERT_ID()`;
|
|
166
|
+
}
|
|
167
|
+
return `${wrappedPk} = ${this.expr.escapeValue(def.record[pk])}`;
|
|
168
|
+
});
|
|
169
|
+
// multi-statement: INSERT → SET @affected → SELECT (삽입된 경우에만 결과 반환)
|
|
170
|
+
const statements = [
|
|
171
|
+
`INSERT INTO ${table} (${colList}) SELECT ${values} WHERE NOT EXISTS (${existsQuerySql})`,
|
|
172
|
+
`SET @sd_affected = ROW_COUNT()`,
|
|
173
|
+
`SELECT ${outputCols} FROM ${table} WHERE ${whereForSelect.join(" AND ")} AND @sd_affected > 0`,
|
|
174
|
+
];
|
|
175
|
+
// results[0]=INSERT, results[1]=SET(빈 결과), results[2]=SELECT
|
|
176
|
+
return { sql: statements.join(";\n"), resultSetIndex: 2 };
|
|
177
|
+
}
|
|
178
|
+
insertInto(def) {
|
|
179
|
+
const table = this.tableName(def.table);
|
|
180
|
+
const selectSql = this.select(def.recordsSelectQuery).sql;
|
|
181
|
+
// INSERT INTO SELECT에서 column 추출
|
|
182
|
+
const selectDef = def.recordsSelectQuery;
|
|
183
|
+
const colList = selectDef.select != null
|
|
184
|
+
? Object.keys(selectDef.select)
|
|
185
|
+
.map((c) => this.expr.wrap(c))
|
|
186
|
+
.join(", ")
|
|
187
|
+
: "*";
|
|
188
|
+
// OUTPUT 불필요: 단순 INSERT INTO SELECT
|
|
189
|
+
if (def.output == null) {
|
|
190
|
+
return { sql: `INSERT INTO ${table} (${colList}) ${selectSql}` };
|
|
191
|
+
}
|
|
192
|
+
// OUTPUT 필요: multi-statement
|
|
193
|
+
const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
194
|
+
// PK가 AI인 경우: LAST_INSERT_ID() + ROW_COUNT()로 범위 조회
|
|
195
|
+
if (def.output.aiColName != null) {
|
|
196
|
+
const aiCol = this.expr.wrap(def.output.aiColName);
|
|
197
|
+
const statements = [
|
|
198
|
+
`INSERT INTO ${table} (${colList}) ${selectSql}`,
|
|
199
|
+
`SET @sd_first_id = LAST_INSERT_ID(), @sd_count = ROW_COUNT()`,
|
|
200
|
+
`SELECT ${outputCols} FROM ${table} WHERE ${aiCol} >= @sd_first_id AND ${aiCol} < @sd_first_id + @sd_count`,
|
|
201
|
+
];
|
|
202
|
+
// results[0]=INSERT, results[1]=SET(빈 결과), results[2]=SELECT
|
|
203
|
+
return { sql: statements.join(";\n"), resultSetIndex: 2 };
|
|
204
|
+
}
|
|
205
|
+
// PK가 AI가 아닌 경우: PK를 임시 테이블에 저장 후 조회
|
|
206
|
+
const tempTableName = this.expr.wrap("SD_TEMP_" + Uuid.generate().toString().replace(/-/g, ""));
|
|
207
|
+
// recordsSelectQuery에서 PK column만 추출하는 SELECT 생성
|
|
208
|
+
const pkSelectDef = {
|
|
209
|
+
...def.recordsSelectQuery,
|
|
210
|
+
select: Object.fromEntries(def.output.pkColNames.map((pk) => [pk, def.recordsSelectQuery.select[pk]])),
|
|
211
|
+
};
|
|
212
|
+
const pkSelectSql = this.select(pkSelectDef).sql;
|
|
213
|
+
// 임시 테이블의 PK를 사용하여 대상에서 SELECT
|
|
214
|
+
const pkConditions = def.output.pkColNames.map((pk) => {
|
|
215
|
+
const wrappedPk = this.expr.wrap(pk);
|
|
216
|
+
return `${table}.${wrappedPk} = ${tempTableName}.${wrappedPk}`;
|
|
217
|
+
});
|
|
218
|
+
const statements = [
|
|
219
|
+
`CREATE TEMPORARY TABLE ${tempTableName} AS ${pkSelectSql}`,
|
|
220
|
+
`INSERT INTO ${table} (${colList}) ${selectSql}`,
|
|
221
|
+
`SELECT ${outputCols} FROM ${table}, ${tempTableName} WHERE ${pkConditions.join(" AND ")}`,
|
|
222
|
+
`DROP TEMPORARY TABLE ${tempTableName}`,
|
|
223
|
+
];
|
|
224
|
+
// results[0]=CREATE, results[1]=INSERT, results[2]=SELECT, results[3]=DROP 결과
|
|
225
|
+
return { sql: statements.join(";\n"), resultSetIndex: 2 };
|
|
226
|
+
}
|
|
227
|
+
//#endregion
|
|
228
|
+
//#region ========== DML - UPDATE ==========
|
|
229
|
+
update(def) {
|
|
230
|
+
const table = this.tableName(def.table);
|
|
231
|
+
const alias = this.expr.wrap(def.as);
|
|
232
|
+
// SET
|
|
233
|
+
const setParts = Object.entries(def.record).map(([col, expr]) => `${alias}.${this.expr.wrap(col)} = ${this.expr.render(expr)}`);
|
|
234
|
+
// OUTPUT 불필요: 단순 UPDATE
|
|
235
|
+
if (def.output == null) {
|
|
236
|
+
let sql = `UPDATE ${table} AS ${alias}`;
|
|
237
|
+
sql += this.renderJoins(def.joins);
|
|
238
|
+
sql += ` SET ${setParts.join(", ")}`;
|
|
239
|
+
sql += this.renderWhere(def.where);
|
|
240
|
+
if (def.limit != null || def.top != null) {
|
|
241
|
+
sql += this.renderLimit(def.limit, def.top);
|
|
242
|
+
}
|
|
243
|
+
return { sql };
|
|
244
|
+
}
|
|
245
|
+
// OUTPUT 필요: multi-statement (PK를 임시 테이블에 저장 + UPDATE + SELECT + DROP)
|
|
246
|
+
const outputCols = def.output.columns.map((c) => `${alias}.${this.expr.wrap(c)}`).join(", ");
|
|
247
|
+
const tempTableName = this.expr.wrap("SD_TEMP_" + Uuid.generate().toString().replace(/-/g, ""));
|
|
248
|
+
// 대상 PK를 임시 테이블에 저장 (UPDATE 후 WHERE 조건이 변경될 수 있으므로)
|
|
249
|
+
const pkSelectCols = def.output.pkColNames
|
|
250
|
+
.map((pk) => `${alias}.${this.expr.wrap(pk)} AS ${this.expr.wrap(pk)}`)
|
|
251
|
+
.join(", ");
|
|
252
|
+
let createTempSql = `CREATE TEMPORARY TABLE ${tempTableName} AS SELECT ${pkSelectCols} FROM ${table} AS ${alias}`;
|
|
253
|
+
createTempSql += this.renderJoins(def.joins);
|
|
254
|
+
createTempSql += this.renderWhere(def.where);
|
|
255
|
+
// UPDATE
|
|
256
|
+
let updateSql = `UPDATE ${table} AS ${alias}`;
|
|
257
|
+
updateSql += this.renderJoins(def.joins);
|
|
258
|
+
updateSql += ` SET ${setParts.join(", ")}`;
|
|
259
|
+
updateSql += this.renderWhere(def.where);
|
|
260
|
+
if (def.top != null)
|
|
261
|
+
updateSql += ` LIMIT ${def.top}`;
|
|
262
|
+
// 임시 테이블의 PK로 SELECT (업데이트된 값 조회)
|
|
263
|
+
const pkConditions = def.output.pkColNames.map((pk) => {
|
|
264
|
+
const wrappedPk = this.expr.wrap(pk);
|
|
265
|
+
return `${alias}.${wrappedPk} = ${tempTableName}.${wrappedPk}`;
|
|
266
|
+
});
|
|
267
|
+
const selectSql = `SELECT ${outputCols} FROM ${table} AS ${alias}, ${tempTableName} WHERE ${pkConditions.join(" AND ")}`;
|
|
268
|
+
// 임시 테이블 삭제
|
|
269
|
+
const dropSql = `DROP TEMPORARY TABLE ${tempTableName}`;
|
|
270
|
+
const statements = [createTempSql, updateSql, selectSql, dropSql];
|
|
271
|
+
return { sql: statements.join(";\n"), resultSetIndex: 2 };
|
|
272
|
+
}
|
|
273
|
+
//#endregion
|
|
274
|
+
//#region ========== DML - DELETE ==========
|
|
275
|
+
delete(def) {
|
|
276
|
+
const table = this.tableName(def.table);
|
|
277
|
+
const alias = this.expr.wrap(def.as);
|
|
278
|
+
// OUTPUT 불필요: 단순 DELETE
|
|
279
|
+
if (def.output == null) {
|
|
280
|
+
let sql = `DELETE ${alias} FROM ${table} AS ${alias}`;
|
|
281
|
+
sql += this.renderJoins(def.joins);
|
|
282
|
+
sql += this.renderWhere(def.where);
|
|
283
|
+
if (def.limit != null || def.top != null) {
|
|
284
|
+
sql += this.renderLimit(def.limit, def.top);
|
|
285
|
+
}
|
|
286
|
+
return { sql };
|
|
287
|
+
}
|
|
288
|
+
// OUTPUT 필요: multi-statement (삭제 전 임시 테이블에 저장 + DELETE + SELECT + DROP)
|
|
289
|
+
const outputCols = def.output.columns.map((c) => `${alias}.${this.expr.wrap(c)}`).join(", ");
|
|
290
|
+
const tempTableName = this.expr.wrap("SD_TEMP_" + Uuid.generate().toString().replace(/-/g, ""));
|
|
291
|
+
// 삭제 전 임시 테이블에 저장
|
|
292
|
+
let createTempSql = `CREATE TEMPORARY TABLE ${tempTableName} AS SELECT ${outputCols} FROM ${table} AS ${alias}`;
|
|
293
|
+
createTempSql += this.renderJoins(def.joins);
|
|
294
|
+
createTempSql += this.renderWhere(def.where);
|
|
295
|
+
// DELETE 실행
|
|
296
|
+
let deleteSql = `DELETE ${alias} FROM ${table} AS ${alias}`;
|
|
297
|
+
deleteSql += this.renderJoins(def.joins);
|
|
298
|
+
deleteSql += this.renderWhere(def.where);
|
|
299
|
+
if (def.top != null)
|
|
300
|
+
deleteSql += ` LIMIT ${def.top}`;
|
|
301
|
+
// 임시 테이블에서 결과 반환
|
|
302
|
+
const selectSql = `SELECT * FROM ${tempTableName}`;
|
|
303
|
+
// 임시 테이블 삭제
|
|
304
|
+
const dropSql = `DROP TEMPORARY TABLE ${tempTableName}`;
|
|
305
|
+
const statements = [createTempSql, deleteSql, selectSql, dropSql];
|
|
306
|
+
return { sql: statements.join(";\n"), resultSetIndex: 2 };
|
|
307
|
+
}
|
|
308
|
+
//#endregion
|
|
309
|
+
//#region ========== DML - UPSERT ==========
|
|
310
|
+
upsert(def) {
|
|
311
|
+
const table = this.tableName(def.table);
|
|
312
|
+
const alias = this.expr.wrap(def.existsSelectQuery.as);
|
|
313
|
+
const existsQuerySql = this.select(def.existsSelectQuery).sql;
|
|
314
|
+
// UPDATE SET 부분 (alias.column 형식)
|
|
315
|
+
const updateSetParts = Object.entries(def.updateRecord).map(([col, e]) => `${alias}.${this.expr.wrap(col)} = ${this.expr.render(e)}`);
|
|
316
|
+
// INSERT 부분
|
|
317
|
+
const insertColumns = Object.keys(def.insertRecord);
|
|
318
|
+
const insertColList = insertColumns.map((c) => this.expr.wrap(c)).join(", ");
|
|
319
|
+
const insertValues = insertColumns.map((c) => this.expr.render(def.insertRecord[c])).join(", ");
|
|
320
|
+
// WHERE 조건 추출 (existsSelectQuery의 where에서)
|
|
321
|
+
const whereCondition = def.existsSelectQuery.where != null && def.existsSelectQuery.where.length > 0
|
|
322
|
+
? this.expr.renderWhere(def.existsSelectQuery.where)
|
|
323
|
+
: "1=1";
|
|
324
|
+
// No OUTPUT needed: multi-statement (UPDATE + INSERT WHERE NOT EXISTS)
|
|
325
|
+
if (def.output == null) {
|
|
326
|
+
// UPDATE: updates if exists
|
|
327
|
+
// INSERT SELECT WHERE NOT EXISTS: inserts if not exists
|
|
328
|
+
const statements = [
|
|
329
|
+
`UPDATE ${table} AS ${alias} SET ${updateSetParts.join(", ")} WHERE ${whereCondition}`,
|
|
330
|
+
`INSERT INTO ${table} (${insertColList}) SELECT ${insertValues} WHERE NOT EXISTS (${existsQuerySql})`,
|
|
331
|
+
];
|
|
332
|
+
return { sql: statements.join(";\n") };
|
|
333
|
+
}
|
|
334
|
+
// OUTPUT 필요: multi-statement (CREATE TEMP + UPDATE + INSERT + SELECT + DROP)
|
|
335
|
+
const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
336
|
+
const tempTableName = this.expr.wrap("SD_TEMP_" + Uuid.generate().toString().replace(/-/g, ""));
|
|
337
|
+
// 대상 PK를 임시 테이블에 저장 (UPDATE 후 WHERE 조건이 변경될 수 있으므로)
|
|
338
|
+
const pkSelectCols = def.output.pkColNames.map((pk) => this.expr.wrap(pk)).join(", ");
|
|
339
|
+
const createTempSql = `CREATE TEMPORARY TABLE ${tempTableName} AS SELECT ${pkSelectCols} FROM ${table} AS ${alias} WHERE ${whereCondition}`;
|
|
340
|
+
// UPDATE (존재하면 업데이트)
|
|
341
|
+
const updateSql = `UPDATE ${table} AS ${alias} SET ${updateSetParts.join(", ")} WHERE ${whereCondition}`;
|
|
342
|
+
// INSERT (NOT EXISTS 패턴)
|
|
343
|
+
const insertSql = `INSERT INTO ${table} (${insertColList}) SELECT ${insertValues} WHERE NOT EXISTS (${existsQuerySql})`;
|
|
344
|
+
// SELECT: UPDATE 결과 또는 INSERT 결과 조회 (UNION ALL로 병합)
|
|
345
|
+
// UPDATE 경우: 임시 테이블의 PK로 조회
|
|
346
|
+
const output = def.output;
|
|
347
|
+
const updatePkConditions = output.pkColNames.map((pk) => {
|
|
348
|
+
const wrappedPk = this.expr.wrap(pk);
|
|
349
|
+
return `${table}.${wrappedPk} IN (SELECT ${wrappedPk} FROM ${tempTableName})`;
|
|
350
|
+
});
|
|
351
|
+
const selectUpdateSql = `SELECT ${outputCols} FROM ${table} WHERE ${updatePkConditions.join(" AND ")}`;
|
|
352
|
+
// INSERT 경우: insertRecord의 PK로 조회 (AI에는 LAST_INSERT_ID() 사용, 임시 테이블이 비어있을 때만)
|
|
353
|
+
const insertPkConditions = output.pkColNames.map((pk) => {
|
|
354
|
+
const wrappedPk = this.expr.wrap(pk);
|
|
355
|
+
if (pk === output.aiColName) {
|
|
356
|
+
return `${wrappedPk} = LAST_INSERT_ID()`;
|
|
357
|
+
}
|
|
358
|
+
const pkExpr = def.insertRecord[pk];
|
|
359
|
+
return `${wrappedPk} = ${this.expr.render(pkExpr)}`;
|
|
360
|
+
});
|
|
361
|
+
const selectInsertSql = `SELECT ${outputCols} FROM ${table} WHERE ${insertPkConditions.join(" AND ")} AND NOT EXISTS (SELECT 1 FROM ${tempTableName})`;
|
|
362
|
+
const selectSql = `${selectUpdateSql} UNION ALL ${selectInsertSql}`;
|
|
363
|
+
// 임시 테이블 삭제
|
|
364
|
+
const dropSql = `DROP TEMPORARY TABLE ${tempTableName}`;
|
|
365
|
+
const statements = [createTempSql, updateSql, insertSql, selectSql, dropSql];
|
|
366
|
+
return { sql: statements.join(";\n"), resultSetIndex: 3 };
|
|
367
|
+
}
|
|
368
|
+
//#endregion
|
|
369
|
+
//#region ========== DDL - Table ==========
|
|
370
|
+
createTable(def) {
|
|
371
|
+
const table = this.tableName(def.table);
|
|
372
|
+
const colDefs = def.columns.map((col) => {
|
|
373
|
+
let colSql = `${this.expr.wrap(col.name)} ${this.expr.renderDataType(col.dataType)}`;
|
|
374
|
+
// nullable: true → NULL, 아니면 → NOT NULL
|
|
375
|
+
if (col.nullable === true) {
|
|
376
|
+
colSql += " NULL";
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
colSql += " NOT NULL";
|
|
380
|
+
}
|
|
381
|
+
if (col.autoIncrement) {
|
|
382
|
+
colSql += " AUTO_INCREMENT";
|
|
383
|
+
}
|
|
384
|
+
if (col.default !== undefined) {
|
|
385
|
+
colSql += ` DEFAULT ${this.expr.escapeValue(col.default)}`;
|
|
386
|
+
}
|
|
387
|
+
return colSql;
|
|
388
|
+
});
|
|
389
|
+
// CONSTRAINT 이름이 포함된 Primary Key
|
|
390
|
+
if (def.primaryKey != null && def.primaryKey.length > 0) {
|
|
391
|
+
const pkCols = def.primaryKey.map((c) => this.expr.wrap(c)).join(", ");
|
|
392
|
+
const pkName = this.expr.wrap(`PK_${def.table.name}`);
|
|
393
|
+
colDefs.push(`CONSTRAINT ${pkName} PRIMARY KEY (${pkCols})`);
|
|
394
|
+
}
|
|
395
|
+
return { sql: `CREATE TABLE ${table} (\n ${colDefs.join(",\n ")}\n)` };
|
|
396
|
+
}
|
|
397
|
+
dropTable(def) {
|
|
398
|
+
return { sql: `DROP TABLE ${this.tableName(def.table)}` };
|
|
399
|
+
}
|
|
400
|
+
renameTable(def) {
|
|
401
|
+
return { sql: `RENAME TABLE ${this.tableName(def.table)} TO ${this.expr.wrap(def.newName)}` };
|
|
402
|
+
}
|
|
403
|
+
truncate(def) {
|
|
404
|
+
// MySQL: TRUNCATE는 AUTO_INCREMENT를 자동으로 초기화
|
|
405
|
+
return { sql: `TRUNCATE TABLE ${this.tableName(def.table)}` };
|
|
406
|
+
}
|
|
407
|
+
//#endregion
|
|
408
|
+
//#region ========== DDL - Column ==========
|
|
409
|
+
addColumn(def) {
|
|
410
|
+
const table = this.tableName(def.table);
|
|
411
|
+
const col = def.column;
|
|
412
|
+
let colSql = `${this.expr.wrap(col.name)} ${this.expr.renderDataType(col.dataType)}`;
|
|
413
|
+
// nullable: true → NULL, else → NOT NULL
|
|
414
|
+
if (col.nullable === true) {
|
|
415
|
+
colSql += " NULL";
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
colSql += " NOT NULL";
|
|
419
|
+
}
|
|
420
|
+
if (col.autoIncrement) {
|
|
421
|
+
colSql += " AUTO_INCREMENT";
|
|
422
|
+
}
|
|
423
|
+
if (col.default !== undefined) {
|
|
424
|
+
colSql += ` DEFAULT ${this.expr.escapeValue(col.default)}`;
|
|
425
|
+
}
|
|
426
|
+
return { sql: `ALTER TABLE ${table} ADD COLUMN ${colSql}` };
|
|
427
|
+
}
|
|
428
|
+
dropColumn(def) {
|
|
429
|
+
return {
|
|
430
|
+
sql: `ALTER TABLE ${this.tableName(def.table)} DROP COLUMN ${this.expr.wrap(def.column)}`,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
modifyColumn(def) {
|
|
434
|
+
const table = this.tableName(def.table);
|
|
435
|
+
const col = def.column;
|
|
436
|
+
let colSql = `${this.expr.wrap(col.name)} ${this.expr.renderDataType(col.dataType)}`;
|
|
437
|
+
// nullable: true → NULL, else → NOT NULL
|
|
438
|
+
if (col.nullable === true) {
|
|
439
|
+
colSql += " NULL";
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
colSql += " NOT NULL";
|
|
443
|
+
}
|
|
444
|
+
if (col.autoIncrement) {
|
|
445
|
+
colSql += " AUTO_INCREMENT";
|
|
446
|
+
}
|
|
447
|
+
if (col.default !== undefined) {
|
|
448
|
+
colSql += ` DEFAULT ${this.expr.escapeValue(col.default)}`;
|
|
449
|
+
}
|
|
450
|
+
return { sql: `ALTER TABLE ${table} MODIFY COLUMN ${colSql}` };
|
|
451
|
+
}
|
|
452
|
+
renameColumn(def) {
|
|
453
|
+
const table = this.tableName(def.table);
|
|
454
|
+
// MySQL 8.0+: RENAME COLUMN 지원
|
|
455
|
+
return {
|
|
456
|
+
sql: `ALTER TABLE ${table} RENAME COLUMN ${this.expr.wrap(def.column)} TO ${this.expr.wrap(def.newName)}`,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
//#endregion
|
|
460
|
+
//#region ========== DDL - Constraint ==========
|
|
461
|
+
addPrimaryKey(def) {
|
|
462
|
+
const table = this.tableName(def.table);
|
|
463
|
+
const cols = def.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
464
|
+
return { sql: `ALTER TABLE ${table} ADD PRIMARY KEY (${cols})` };
|
|
465
|
+
}
|
|
466
|
+
dropPrimaryKey(def) {
|
|
467
|
+
return { sql: `ALTER TABLE ${this.tableName(def.table)} DROP PRIMARY KEY` };
|
|
468
|
+
}
|
|
469
|
+
addForeignKey(def) {
|
|
470
|
+
const table = this.tableName(def.table);
|
|
471
|
+
const fk = def.foreignKey;
|
|
472
|
+
const fkCols = fk.fkColumns.map((c) => this.expr.wrap(c)).join(", ");
|
|
473
|
+
const targetTable = this.tableName(fk.targetTable);
|
|
474
|
+
const targetCols = fk.targetPkColumns.map((c) => this.expr.wrap(c)).join(", ");
|
|
475
|
+
// MySQL은 FK 추가 시 인덱스를 자동 생성하므로 별도 인덱스 불필요
|
|
476
|
+
return {
|
|
477
|
+
sql: `ALTER TABLE ${table} ADD CONSTRAINT ${this.expr.wrap(fk.name)} FOREIGN KEY (${fkCols}) REFERENCES ${targetTable} (${targetCols})`,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
dropForeignKey(def) {
|
|
481
|
+
return {
|
|
482
|
+
sql: `ALTER TABLE ${this.tableName(def.table)} DROP FOREIGN KEY ${this.expr.wrap(def.foreignKey)}`,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
addIndex(def) {
|
|
486
|
+
const table = this.tableName(def.table);
|
|
487
|
+
const idx = def.index;
|
|
488
|
+
const cols = idx.columns.map((c) => `${this.expr.wrap(c.name)} ${c.orderBy}`).join(", ");
|
|
489
|
+
const unique = idx.unique ? "UNIQUE " : "";
|
|
490
|
+
return { sql: `CREATE ${unique}INDEX ${this.expr.wrap(idx.name)} ON ${table} (${cols})` };
|
|
491
|
+
}
|
|
492
|
+
dropIndex(def) {
|
|
493
|
+
return { sql: `DROP INDEX ${this.expr.wrap(def.index)} ON ${this.tableName(def.table)}` };
|
|
494
|
+
}
|
|
495
|
+
//#endregion
|
|
496
|
+
//#region ========== DDL - View/Procedure ==========
|
|
497
|
+
createView(def) {
|
|
498
|
+
const view = this.tableName(def.view);
|
|
499
|
+
const selectSql = this.select(def.queryDef).sql;
|
|
500
|
+
return { sql: `CREATE OR REPLACE VIEW ${view} AS ${selectSql}` };
|
|
501
|
+
}
|
|
502
|
+
dropView(def) {
|
|
503
|
+
return { sql: `DROP VIEW IF EXISTS ${this.tableName(def.view)}` };
|
|
504
|
+
}
|
|
505
|
+
createProc(def) {
|
|
506
|
+
const proc = this.tableName(def.procedure);
|
|
507
|
+
// 파라미터 처리
|
|
508
|
+
const paramList = def.params
|
|
509
|
+
?.map((p) => {
|
|
510
|
+
let sql = `IN ${this.expr.wrap(p.name)} ${this.expr.renderDataType(p.dataType)}`;
|
|
511
|
+
if (p.default !== undefined) {
|
|
512
|
+
sql += ` DEFAULT ${this.expr.escapeValue(p.default)}`;
|
|
513
|
+
}
|
|
514
|
+
return sql;
|
|
515
|
+
})
|
|
516
|
+
.join(", ") ?? "";
|
|
517
|
+
let sql = `CREATE PROCEDURE ${proc}(${paramList})\n`;
|
|
518
|
+
sql += `BEGIN\n`;
|
|
519
|
+
sql += def.query;
|
|
520
|
+
if (!def.query.trim().endsWith(";")) {
|
|
521
|
+
sql += ";";
|
|
522
|
+
}
|
|
523
|
+
sql += `\nEND`;
|
|
524
|
+
return { sql };
|
|
525
|
+
}
|
|
526
|
+
dropProc(def) {
|
|
527
|
+
return { sql: `DROP PROCEDURE IF EXISTS ${this.tableName(def.procedure)}` };
|
|
528
|
+
}
|
|
529
|
+
execProc(def) {
|
|
530
|
+
const proc = this.tableName(def.procedure);
|
|
531
|
+
if (def.params == null || Object.keys(def.params).length === 0) {
|
|
532
|
+
return { sql: `CALL ${proc}()` };
|
|
533
|
+
}
|
|
534
|
+
const params = Object.values(def.params)
|
|
535
|
+
.map((p) => this.expr.render(p))
|
|
536
|
+
.join(", ");
|
|
537
|
+
return { sql: `CALL ${proc}(${params})` };
|
|
538
|
+
}
|
|
539
|
+
//#endregion
|
|
540
|
+
//#region ========== Utils ==========
|
|
541
|
+
clearSchema(def) {
|
|
542
|
+
// MySQL: 모든 테이블 삭제 (MySQL에서 database와 schema는 동의어)
|
|
543
|
+
// information_schema에서 테이블 목록 조회 후 DROP
|
|
544
|
+
// SQL 인젝션 방지: 식별자 유효성 검사
|
|
545
|
+
if (!/^[a-zA-Z0-9_]+$/.test(def.database)) {
|
|
546
|
+
throw new Error(`잘못된 데이터베이스 이름: ${def.database}`);
|
|
547
|
+
}
|
|
548
|
+
const dbName = this.expr.escapeString(def.database);
|
|
549
|
+
return {
|
|
550
|
+
sql: `
|
|
551
|
+
SET FOREIGN_KEY_CHECKS = 0;
|
|
552
|
+
SET @tables = NULL;
|
|
553
|
+
SELECT GROUP_CONCAT(table_name) INTO @tables FROM information_schema.tables WHERE table_schema = '${dbName}';
|
|
554
|
+
SET @drop_stmt = IF(@tables IS NULL, 'SELECT 1', CONCAT('DROP TABLE IF EXISTS ', @tables));
|
|
555
|
+
PREPARE stmt FROM @drop_stmt;
|
|
556
|
+
EXECUTE stmt;
|
|
557
|
+
DEALLOCATE PREPARE stmt;
|
|
558
|
+
SET FOREIGN_KEY_CHECKS = 1`,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
schemaExists(def) {
|
|
562
|
+
// MySQL: database와 schema는 동의어
|
|
563
|
+
const dbName = this.expr.escapeString(def.database);
|
|
564
|
+
return {
|
|
565
|
+
sql: `SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = '${dbName}'`,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
/** MySQL은 전역 설정만 지원 (table 파라미터 무시) */
|
|
569
|
+
switchFk(def) {
|
|
570
|
+
return def.enabled
|
|
571
|
+
? { sql: "SET FOREIGN_KEY_CHECKS = 1" }
|
|
572
|
+
: { sql: "SET FOREIGN_KEY_CHECKS = 0" };
|
|
573
|
+
}
|
|
480
574
|
}
|
|
481
|
-
|
|
482
|
-
MysqlQueryBuilder
|
|
483
|
-
};
|
|
484
|
-
//# sourceMappingURL=mysql-query-builder.js.map
|
|
575
|
+
//# sourceMappingURL=mysql-query-builder.js.map
|