@simplysm/orm-common 13.0.99 → 14.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/dist/create-db-context.d.ts +10 -10
  2. package/dist/create-db-context.js +312 -276
  3. package/dist/create-db-context.js.map +1 -6
  4. package/dist/ddl/column-ddl.d.ts +4 -4
  5. package/dist/ddl/column-ddl.js +41 -35
  6. package/dist/ddl/column-ddl.js.map +1 -6
  7. package/dist/ddl/initialize.d.ts +17 -17
  8. package/dist/ddl/initialize.js +200 -142
  9. package/dist/ddl/initialize.js.map +1 -6
  10. package/dist/ddl/relation-ddl.d.ts +6 -6
  11. package/dist/ddl/relation-ddl.js +55 -48
  12. package/dist/ddl/relation-ddl.js.map +1 -6
  13. package/dist/ddl/schema-ddl.d.ts +4 -4
  14. package/dist/ddl/schema-ddl.js +21 -15
  15. package/dist/ddl/schema-ddl.js.map +1 -6
  16. package/dist/ddl/table-ddl.d.ts +20 -20
  17. package/dist/ddl/table-ddl.js +139 -93
  18. package/dist/ddl/table-ddl.js.map +1 -6
  19. package/dist/define-db-context.js +10 -13
  20. package/dist/define-db-context.js.map +1 -6
  21. package/dist/errors/db-transaction-error.d.ts +15 -15
  22. package/dist/errors/db-transaction-error.d.ts.map +1 -1
  23. package/dist/errors/db-transaction-error.js +53 -19
  24. package/dist/errors/db-transaction-error.js.map +1 -6
  25. package/dist/exec/executable.d.ts +23 -23
  26. package/dist/exec/executable.js +94 -40
  27. package/dist/exec/executable.js.map +1 -6
  28. package/dist/exec/queryable.d.ts +97 -97
  29. package/dist/exec/queryable.js +1310 -1204
  30. package/dist/exec/queryable.js.map +1 -6
  31. package/dist/exec/search-parser.d.ts +31 -31
  32. package/dist/exec/search-parser.d.ts.map +1 -1
  33. package/dist/exec/search-parser.js +158 -59
  34. package/dist/exec/search-parser.js.map +1 -6
  35. package/dist/expr/expr-unit.d.ts +4 -4
  36. package/dist/expr/expr-unit.js +24 -18
  37. package/dist/expr/expr-unit.js.map +1 -6
  38. package/dist/expr/expr.d.ts +6 -6
  39. package/dist/expr/expr.js +1872 -1844
  40. package/dist/expr/expr.js.map +1 -6
  41. package/dist/index.js +23 -1
  42. package/dist/index.js.map +1 -6
  43. package/dist/models/system-migration.js +7 -7
  44. package/dist/models/system-migration.js.map +1 -6
  45. package/dist/query-builder/base/expr-renderer-base.d.ts +10 -10
  46. package/dist/query-builder/base/expr-renderer-base.js +27 -21
  47. package/dist/query-builder/base/expr-renderer-base.js.map +1 -6
  48. package/dist/query-builder/base/query-builder-base.d.ts +21 -21
  49. package/dist/query-builder/base/query-builder-base.d.ts.map +1 -1
  50. package/dist/query-builder/base/query-builder-base.js +90 -80
  51. package/dist/query-builder/base/query-builder-base.js.map +1 -6
  52. package/dist/query-builder/mssql/mssql-expr-renderer.d.ts +4 -4
  53. package/dist/query-builder/mssql/mssql-expr-renderer.d.ts.map +1 -1
  54. package/dist/query-builder/mssql/mssql-expr-renderer.js +447 -420
  55. package/dist/query-builder/mssql/mssql-expr-renderer.js.map +1 -6
  56. package/dist/query-builder/mssql/mssql-query-builder.js +483 -443
  57. package/dist/query-builder/mssql/mssql-query-builder.js.map +1 -6
  58. package/dist/query-builder/mysql/mysql-expr-renderer.d.ts +4 -4
  59. package/dist/query-builder/mysql/mysql-expr-renderer.d.ts.map +1 -1
  60. package/dist/query-builder/mysql/mysql-expr-renderer.js +451 -419
  61. package/dist/query-builder/mysql/mysql-expr-renderer.js.map +1 -6
  62. package/dist/query-builder/mysql/mysql-query-builder.js +570 -479
  63. package/dist/query-builder/mysql/mysql-query-builder.js.map +1 -6
  64. package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts +4 -4
  65. package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts.map +1 -1
  66. package/dist/query-builder/postgresql/postgresql-expr-renderer.js +449 -422
  67. package/dist/query-builder/postgresql/postgresql-expr-renderer.js.map +1 -6
  68. package/dist/query-builder/postgresql/postgresql-query-builder.js +511 -460
  69. package/dist/query-builder/postgresql/postgresql-query-builder.js.map +1 -6
  70. package/dist/query-builder/query-builder.d.ts +1 -1
  71. package/dist/query-builder/query-builder.js +13 -13
  72. package/dist/query-builder/query-builder.js.map +1 -6
  73. package/dist/schema/factory/column-builder.d.ts +84 -84
  74. package/dist/schema/factory/column-builder.js +248 -185
  75. package/dist/schema/factory/column-builder.js.map +1 -6
  76. package/dist/schema/factory/index-builder.d.ts +38 -38
  77. package/dist/schema/factory/index-builder.js +144 -85
  78. package/dist/schema/factory/index-builder.js.map +1 -6
  79. package/dist/schema/factory/relation-builder.d.ts +91 -91
  80. package/dist/schema/factory/relation-builder.d.ts.map +1 -1
  81. package/dist/schema/factory/relation-builder.js +274 -136
  82. package/dist/schema/factory/relation-builder.js.map +1 -6
  83. package/dist/schema/procedure-builder.d.ts +51 -51
  84. package/dist/schema/procedure-builder.d.ts.map +1 -1
  85. package/dist/schema/procedure-builder.js +205 -131
  86. package/dist/schema/procedure-builder.js.map +1 -6
  87. package/dist/schema/table-builder.d.ts +55 -55
  88. package/dist/schema/table-builder.d.ts.map +1 -1
  89. package/dist/schema/table-builder.js +274 -205
  90. package/dist/schema/table-builder.js.map +1 -6
  91. package/dist/schema/view-builder.d.ts +44 -44
  92. package/dist/schema/view-builder.d.ts.map +1 -1
  93. package/dist/schema/view-builder.js +189 -116
  94. package/dist/schema/view-builder.js.map +1 -6
  95. package/dist/types/column.js +60 -30
  96. package/dist/types/column.js.map +1 -6
  97. package/dist/types/db-context-def.d.ts +9 -9
  98. package/dist/types/db-context-def.js +2 -1
  99. package/dist/types/db-context-def.js.map +1 -6
  100. package/dist/types/db.d.ts +47 -47
  101. package/dist/types/db.js +15 -5
  102. package/dist/types/db.js.map +1 -6
  103. package/dist/types/expr.d.ts +81 -81
  104. package/dist/types/expr.d.ts.map +1 -1
  105. package/dist/types/expr.js +3 -1
  106. package/dist/types/expr.js.map +1 -6
  107. package/dist/types/query-def.d.ts +46 -46
  108. package/dist/types/query-def.d.ts.map +1 -1
  109. package/dist/types/query-def.js +31 -24
  110. package/dist/types/query-def.js.map +1 -6
  111. package/dist/utils/result-parser.js +362 -221
  112. package/dist/utils/result-parser.js.map +1 -6
  113. package/package.json +5 -7
  114. package/src/create-db-context.ts +31 -31
  115. package/src/ddl/column-ddl.ts +4 -4
  116. package/src/ddl/initialize.ts +38 -38
  117. package/src/ddl/relation-ddl.ts +6 -6
  118. package/src/ddl/schema-ddl.ts +4 -4
  119. package/src/ddl/table-ddl.ts +24 -24
  120. package/src/errors/db-transaction-error.ts +13 -13
  121. package/src/exec/executable.ts +25 -25
  122. package/src/exec/queryable.ts +134 -134
  123. package/src/exec/search-parser.ts +50 -50
  124. package/src/expr/expr-unit.ts +4 -4
  125. package/src/expr/expr.ts +13 -13
  126. package/src/index.ts +8 -8
  127. package/src/models/system-migration.ts +1 -1
  128. package/src/query-builder/base/expr-renderer-base.ts +21 -21
  129. package/src/query-builder/base/query-builder-base.ts +33 -33
  130. package/src/query-builder/mssql/mssql-expr-renderer.ts +11 -11
  131. package/src/query-builder/mssql/mssql-query-builder.ts +11 -11
  132. package/src/query-builder/mysql/mysql-expr-renderer.ts +15 -15
  133. package/src/query-builder/mysql/mysql-query-builder.ts +3 -3
  134. package/src/query-builder/postgresql/postgresql-expr-renderer.ts +9 -9
  135. package/src/query-builder/postgresql/postgresql-query-builder.ts +7 -7
  136. package/src/query-builder/query-builder.ts +1 -1
  137. package/src/schema/factory/column-builder.ts +86 -86
  138. package/src/schema/factory/index-builder.ts +38 -38
  139. package/src/schema/factory/relation-builder.ts +93 -93
  140. package/src/schema/procedure-builder.ts +52 -52
  141. package/src/schema/table-builder.ts +56 -56
  142. package/src/schema/view-builder.ts +45 -45
  143. package/src/types/column.ts +1 -1
  144. package/src/types/db-context-def.ts +15 -15
  145. package/src/types/db.ts +50 -50
  146. package/src/types/expr.ts +103 -103
  147. package/src/types/query-def.ts +50 -50
  148. package/src/utils/result-parser.ts +39 -39
  149. package/README.md +0 -192
  150. package/docs/core.md +0 -234
  151. package/docs/expression.md +0 -234
  152. package/docs/query-builder.md +0 -93
  153. package/docs/queryable.md +0 -198
  154. package/docs/schema-builders.md +0 -463
  155. package/docs/types.md +0 -445
  156. package/docs/utilities.md +0 -27
  157. package/tests/db-context/create-db-context.spec.ts +0 -193
  158. package/tests/db-context/define-db-context.spec.ts +0 -17
  159. package/tests/ddl/basic.expected.ts +0 -341
  160. package/tests/ddl/basic.spec.ts +0 -557
  161. package/tests/ddl/column-builder.expected.ts +0 -310
  162. package/tests/ddl/column-builder.spec.ts +0 -525
  163. package/tests/ddl/index-builder.expected.ts +0 -38
  164. package/tests/ddl/index-builder.spec.ts +0 -148
  165. package/tests/ddl/procedure-builder.expected.ts +0 -52
  166. package/tests/ddl/procedure-builder.spec.ts +0 -128
  167. package/tests/ddl/relation-builder.expected.ts +0 -36
  168. package/tests/ddl/relation-builder.spec.ts +0 -171
  169. package/tests/ddl/table-builder.expected.ts +0 -113
  170. package/tests/ddl/table-builder.spec.ts +0 -399
  171. package/tests/ddl/view-builder.expected.ts +0 -38
  172. package/tests/ddl/view-builder.spec.ts +0 -116
  173. package/tests/dml/delete.expected.ts +0 -96
  174. package/tests/dml/delete.spec.ts +0 -127
  175. package/tests/dml/insert.expected.ts +0 -192
  176. package/tests/dml/insert.spec.ts +0 -210
  177. package/tests/dml/update.expected.ts +0 -176
  178. package/tests/dml/update.spec.ts +0 -222
  179. package/tests/dml/upsert.expected.ts +0 -215
  180. package/tests/dml/upsert.spec.ts +0 -190
  181. package/tests/errors/queryable-errors.spec.ts +0 -126
  182. package/tests/escape.spec.ts +0 -59
  183. package/tests/examples/pivot.expected.ts +0 -211
  184. package/tests/examples/pivot.spec.ts +0 -200
  185. package/tests/examples/sampling.expected.ts +0 -69
  186. package/tests/examples/sampling.spec.ts +0 -42
  187. package/tests/examples/unpivot.expected.ts +0 -120
  188. package/tests/examples/unpivot.spec.ts +0 -161
  189. package/tests/exec/search-parser.spec.ts +0 -267
  190. package/tests/executable/basic.expected.ts +0 -18
  191. package/tests/executable/basic.spec.ts +0 -54
  192. package/tests/expr/comparison.expected.ts +0 -282
  193. package/tests/expr/comparison.spec.ts +0 -334
  194. package/tests/expr/conditional.expected.ts +0 -134
  195. package/tests/expr/conditional.spec.ts +0 -249
  196. package/tests/expr/date.expected.ts +0 -332
  197. package/tests/expr/date.spec.ts +0 -459
  198. package/tests/expr/math.expected.ts +0 -62
  199. package/tests/expr/math.spec.ts +0 -59
  200. package/tests/expr/string.expected.ts +0 -218
  201. package/tests/expr/string.spec.ts +0 -300
  202. package/tests/expr/utility.expected.ts +0 -147
  203. package/tests/expr/utility.spec.ts +0 -155
  204. package/tests/select/basic.expected.ts +0 -322
  205. package/tests/select/basic.spec.ts +0 -433
  206. package/tests/select/filter.expected.ts +0 -357
  207. package/tests/select/filter.spec.ts +0 -954
  208. package/tests/select/group.expected.ts +0 -169
  209. package/tests/select/group.spec.ts +0 -159
  210. package/tests/select/join.expected.ts +0 -582
  211. package/tests/select/join.spec.ts +0 -692
  212. package/tests/select/order.expected.ts +0 -150
  213. package/tests/select/order.spec.ts +0 -140
  214. package/tests/select/recursive-cte.expected.ts +0 -244
  215. package/tests/select/recursive-cte.spec.ts +0 -514
  216. package/tests/select/result-meta.spec.ts +0 -270
  217. package/tests/select/subquery.expected.ts +0 -363
  218. package/tests/select/subquery.spec.ts +0 -441
  219. package/tests/select/view.expected.ts +0 -155
  220. package/tests/select/view.spec.ts +0 -235
  221. package/tests/select/window.expected.ts +0 -345
  222. package/tests/select/window.spec.ts +0 -433
  223. package/tests/setup/MockExecutor.ts +0 -18
  224. package/tests/setup/TestDbContext.ts +0 -59
  225. package/tests/setup/models/Company.ts +0 -13
  226. package/tests/setup/models/Employee.ts +0 -10
  227. package/tests/setup/models/MonthlySales.ts +0 -11
  228. package/tests/setup/models/Post.ts +0 -16
  229. package/tests/setup/models/Sales.ts +0 -10
  230. package/tests/setup/models/User.ts +0 -19
  231. package/tests/setup/procedure/GetAllUsers.ts +0 -9
  232. package/tests/setup/procedure/GetUserById.ts +0 -12
  233. package/tests/setup/test-utils.ts +0 -72
  234. package/tests/setup/views/ActiveUsers.ts +0 -8
  235. package/tests/setup/views/UserSummary.ts +0 -11
  236. package/tests/types/nullable-queryable-record.spec.ts +0 -97
  237. package/tests/utils/result-parser-perf.spec.ts +0 -143
  238. package/tests/utils/result-parser.spec.ts +0 -667
@@ -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
- class MysqlQueryBuilder extends QueryBuilderBase {
5
- expr = new MysqlExprRenderer((def) => this.select(def).sql);
6
- //#region ========== Utilities ==========
7
- /** Render table name (MySQL: ignores schema, uses database.table only) */
8
- tableName(obj) {
9
- if (obj.database != null) {
10
- return `${this.expr.wrap(obj.database)}.${this.expr.wrap(obj.name)}`;
11
- }
12
- return this.expr.wrap(obj.name);
13
- }
14
- /** Render LIMIT clause */
15
- renderLimit(limit, top) {
16
- if (limit != null) {
17
- const [offset, count] = limit;
18
- return ` LIMIT ${offset}, ${count}`;
19
- }
20
- if (top != null) {
21
- return ` LIMIT ${top}`;
22
- }
23
- return "";
24
- }
25
- renderJoin(join) {
26
- const alias = this.expr.wrap(join.as);
27
- if (this.needsLateral(join)) {
28
- const from2 = Array.isArray(join.from) ? this.renderFrom(join.from) : this.renderFrom(join);
29
- return ` LEFT OUTER JOIN LATERAL ${from2} AS ${alias} ON TRUE`;
30
- }
31
- const from = this.renderFrom(join.from);
32
- const where = join.where != null && join.where.length > 0 ? ` ON ${this.expr.renderWhere(join.where)}` : " ON TRUE";
33
- return ` LEFT OUTER JOIN ${from} AS ${alias}${where}`;
34
- }
35
- //#endregion
36
- //#region ========== DML - SELECT ==========
37
- select(def) {
38
- let sql = "";
39
- if (def.with != null) {
40
- const { name, base, recursive } = def.with;
41
- sql += `WITH ${this.expr.wrap(name)} AS (${this.select(base).sql} UNION ALL ${this.select(recursive).sql}) `;
42
- }
43
- sql += "SELECT";
44
- if (def.distinct) {
45
- sql += " DISTINCT";
46
- }
47
- if (def.select != null) {
48
- const cols = Object.entries(def.select).map(
49
- ([alias, expr]) => `${this.expr.render(expr)} AS ${this.expr.wrap(alias)}`
50
- );
51
- sql += ` ${cols.join(", ")}`;
52
- } else {
53
- sql += " *";
54
- }
55
- if (def.from != null) {
56
- const from = this.renderFrom(def.from);
57
- sql += ` FROM ${from} AS ${this.expr.wrap(def.as)}`;
58
- }
59
- if (def.lock) {
60
- }
61
- sql += this.renderJoins(def.joins);
62
- sql += this.renderWhere(def.where);
63
- sql += this.renderGroupBy(def.groupBy);
64
- sql += this.renderHaving(def.having);
65
- sql += this.renderOrderBy(def.orderBy);
66
- sql += this.renderLimit(def.limit, def.top);
67
- if (def.lock) {
68
- sql += " FOR UPDATE";
69
- }
70
- return { sql };
71
- }
72
- //#endregion
73
- //#region ========== DML - INSERT ==========
74
- insert(def) {
75
- const table = this.tableName(def.table);
76
- if (def.records.length === 0) {
77
- throw new Error("INSERT requires at least one record.");
78
- }
79
- const columns = Object.keys(def.records[0]);
80
- const colList = columns.map((c) => this.expr.wrap(c)).join(", ");
81
- if (def.output == null) {
82
- const valuesList = def.records.map((record) => {
83
- const values = columns.map((c) => this.expr.escapeValue(record[c]));
84
- return `(${values.join(", ")})`;
85
- });
86
- return { sql: `INSERT INTO ${table} (${colList}) VALUES ${valuesList.join(", ")}` };
87
- }
88
- const output = def.output;
89
- const outputCols = output.columns.map((c) => this.expr.wrap(c)).join(", ");
90
- const statements = [];
91
- for (const record of def.records) {
92
- const values = columns.map((c) => this.expr.escapeValue(record[c])).join(", ");
93
- statements.push(`INSERT INTO ${table} (${colList}) VALUES (${values})`);
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 specifics:
8
+ * - No OUTPUT support: workaround via multi-statement pattern (INSERT + SET @var + SELECT)
9
+ * - INSERT OUTPUT: uses LAST_INSERT_ID() for AI column, extracts PK from record for non-AI
10
+ * - UPDATE/UPSERT OUTPUT: saves PK to temp table first since WHERE condition may change after UPDATE, then SELECT
11
+ * - DELETE OUTPUT: saves output columns to temp table before delete
12
+ * - switchFk: global setting (SET FOREIGN_KEY_CHECKS), table parameter is ignored
13
+ * - Index is automatically created when adding FK
14
+ */
15
+ export class MysqlQueryBuilder extends QueryBuilderBase {
16
+ expr = new MysqlExprRenderer((def) => this.select(def).sql);
17
+ //#region ========== Utilities ==========
18
+ /** Render table name (MySQL: ignores schema, uses database.table only) */
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
+ /** Render LIMIT clause */
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
+ // Detect if LATERAL JOIN is needed
39
+ if (this.needsLateral(join)) {
40
+ // If from is an array (UNION ALL), use renderFrom(join.from),
41
+ // otherwise (orderBy, top, select, etc.) use renderFrom(join) to generate subquery
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
+ // Normal 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 (appended at the end)
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
- return { sql };
227
- }
228
- const outputCols = def.output.columns.map((c) => `${alias}.${this.expr.wrap(c)}`).join(", ");
229
- const tempTableName = this.expr.wrap("SD_TEMP_" + Uuid.generate().toString().replace(/-/g, ""));
230
- let createTempSql = `CREATE TEMPORARY TABLE ${tempTableName} AS SELECT ${outputCols} FROM ${table} AS ${alias}`;
231
- createTempSql += this.renderJoins(def.joins);
232
- createTempSql += this.renderWhere(def.where);
233
- let deleteSql = `DELETE ${alias} FROM ${table} AS ${alias}`;
234
- deleteSql += this.renderJoins(def.joins);
235
- deleteSql += this.renderWhere(def.where);
236
- if (def.top != null) deleteSql += ` LIMIT ${def.top}`;
237
- const selectSql = `SELECT * FROM ${tempTableName}`;
238
- const dropSql = `DROP TEMPORARY TABLE ${tempTableName}`;
239
- const statements = [createTempSql, deleteSql, selectSql, dropSql];
240
- return { sql: statements.join(";\n"), resultSetIndex: 2 };
241
- }
242
- //#endregion
243
- //#region ========== DML - UPSERT ==========
244
- upsert(def) {
245
- const table = this.tableName(def.table);
246
- const alias = this.expr.wrap(def.existsSelectQuery.as);
247
- const existsQuerySql = this.select(def.existsSelectQuery).sql;
248
- const updateSetParts = Object.entries(def.updateRecord).map(
249
- ([col, e]) => `${alias}.${this.expr.wrap(col)} = ${this.expr.render(e)}`
250
- );
251
- const insertColumns = Object.keys(def.insertRecord);
252
- const insertColList = insertColumns.map((c) => this.expr.wrap(c)).join(", ");
253
- const insertValues = insertColumns.map((c) => this.expr.render(def.insertRecord[c])).join(", ");
254
- const whereCondition = def.existsSelectQuery.where != null && def.existsSelectQuery.where.length > 0 ? this.expr.renderWhere(def.existsSelectQuery.where) : "1=1";
255
- if (def.output == null) {
256
- const statements2 = [
257
- `UPDATE ${table} AS ${alias} SET ${updateSetParts.join(", ")} WHERE ${whereCondition}`,
258
- `INSERT INTO ${table} (${insertColList}) SELECT ${insertValues} WHERE NOT EXISTS (${existsQuerySql})`
259
- ];
260
- return { sql: statements2.join(";\n") };
261
- }
262
- const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
263
- const tempTableName = this.expr.wrap("SD_TEMP_" + Uuid.generate().toString().replace(/-/g, ""));
264
- const pkSelectCols = def.output.pkColNames.map((pk) => this.expr.wrap(pk)).join(", ");
265
- const createTempSql = `CREATE TEMPORARY TABLE ${tempTableName} AS SELECT ${pkSelectCols} FROM ${table} AS ${alias} WHERE ${whereCondition}`;
266
- const updateSql = `UPDATE ${table} AS ${alias} SET ${updateSetParts.join(", ")} WHERE ${whereCondition}`;
267
- const insertSql = `INSERT INTO ${table} (${insertColList}) SELECT ${insertValues} WHERE NOT EXISTS (${existsQuerySql})`;
268
- const output = def.output;
269
- const updatePkConditions = output.pkColNames.map((pk) => {
270
- const wrappedPk = this.expr.wrap(pk);
271
- return `${table}.${wrappedPk} IN (SELECT ${wrappedPk} FROM ${tempTableName})`;
272
- });
273
- const selectUpdateSql = `SELECT ${outputCols} FROM ${table} WHERE ${updatePkConditions.join(" AND ")}`;
274
- const insertPkConditions = output.pkColNames.map((pk) => {
275
- const wrappedPk = this.expr.wrap(pk);
276
- if (pk === output.aiColName) {
277
- return `${wrappedPk} = LAST_INSERT_ID()`;
278
- }
279
- const pkExpr = def.insertRecord[pk];
280
- return `${wrappedPk} = ${this.expr.render(pkExpr)}`;
281
- });
282
- const selectInsertSql = `SELECT ${outputCols} FROM ${table} WHERE ${insertPkConditions.join(" AND ")} AND NOT EXISTS (SELECT 1 FROM ${tempTableName})`;
283
- const selectSql = `${selectUpdateSql} UNION ALL ${selectInsertSql}`;
284
- const dropSql = `DROP TEMPORARY TABLE ${tempTableName}`;
285
- const statements = [createTempSql, updateSql, insertSql, selectSql, dropSql];
286
- return { sql: statements.join(";\n"), resultSetIndex: 3 };
287
- }
288
- //#endregion
289
- //#region ========== DDL - Table ==========
290
- createTable(def) {
291
- const table = this.tableName(def.table);
292
- const colDefs = def.columns.map((col) => {
293
- let colSql = `${this.expr.wrap(col.name)} ${this.expr.renderDataType(col.dataType)}`;
294
- if (col.nullable === true) {
295
- colSql += " NULL";
296
- } else {
297
- colSql += " NOT NULL";
298
- }
299
- if (col.autoIncrement) {
300
- colSql += " AUTO_INCREMENT";
301
- }
302
- if (col.default !== void 0) {
303
- colSql += ` DEFAULT ${this.expr.escapeValue(col.default)}`;
304
- }
305
- return colSql;
306
- });
307
- if (def.primaryKey != null && def.primaryKey.length > 0) {
308
- const pkCols = def.primaryKey.map((c) => this.expr.wrap(c)).join(", ");
309
- const pkName = this.expr.wrap(`PK_${def.table.name}`);
310
- colDefs.push(`CONSTRAINT ${pkName} PRIMARY KEY (${pkCols})`);
311
- }
312
- return { sql: `CREATE TABLE ${table} (
313
- ${colDefs.join(",\n ")}
314
- )` };
315
- }
316
- dropTable(def) {
317
- return { sql: `DROP TABLE ${this.tableName(def.table)}` };
318
- }
319
- renameTable(def) {
320
- return { sql: `RENAME TABLE ${this.tableName(def.table)} TO ${this.expr.wrap(def.newName)}` };
321
- }
322
- truncate(def) {
323
- return { sql: `TRUNCATE TABLE ${this.tableName(def.table)}` };
324
- }
325
- //#endregion
326
- //#region ========== DDL - Column ==========
327
- addColumn(def) {
328
- const table = this.tableName(def.table);
329
- const col = def.column;
330
- let colSql = `${this.expr.wrap(col.name)} ${this.expr.renderDataType(col.dataType)}`;
331
- if (col.nullable === true) {
332
- colSql += " NULL";
333
- } else {
334
- colSql += " NOT NULL";
335
- }
336
- if (col.autoIncrement) {
337
- colSql += " AUTO_INCREMENT";
338
- }
339
- if (col.default !== void 0) {
340
- colSql += ` DEFAULT ${this.expr.escapeValue(col.default)}`;
341
- }
342
- return { sql: `ALTER TABLE ${table} ADD COLUMN ${colSql}` };
343
- }
344
- dropColumn(def) {
345
- return {
346
- sql: `ALTER TABLE ${this.tableName(def.table)} DROP COLUMN ${this.expr.wrap(def.column)}`
347
- };
348
- }
349
- modifyColumn(def) {
350
- const table = this.tableName(def.table);
351
- const col = def.column;
352
- let colSql = `${this.expr.wrap(col.name)} ${this.expr.renderDataType(col.dataType)}`;
353
- if (col.nullable === true) {
354
- colSql += " NULL";
355
- } else {
356
- colSql += " NOT NULL";
357
- }
358
- if (col.autoIncrement) {
359
- colSql += " AUTO_INCREMENT";
360
- }
361
- if (col.default !== void 0) {
362
- colSql += ` DEFAULT ${this.expr.escapeValue(col.default)}`;
363
- }
364
- return { sql: `ALTER TABLE ${table} MODIFY COLUMN ${colSql}` };
365
- }
366
- renameColumn(def) {
367
- const table = this.tableName(def.table);
368
- return {
369
- sql: `ALTER TABLE ${table} RENAME COLUMN ${this.expr.wrap(def.column)} TO ${this.expr.wrap(def.newName)}`
370
- };
371
- }
372
- //#endregion
373
- //#region ========== DDL - Constraint ==========
374
- addPrimaryKey(def) {
375
- const table = this.tableName(def.table);
376
- const cols = def.columns.map((c) => this.expr.wrap(c)).join(", ");
377
- return { sql: `ALTER TABLE ${table} ADD PRIMARY KEY (${cols})` };
378
- }
379
- dropPrimaryKey(def) {
380
- return { sql: `ALTER TABLE ${this.tableName(def.table)} DROP PRIMARY KEY` };
381
- }
382
- addForeignKey(def) {
383
- const table = this.tableName(def.table);
384
- const fk = def.foreignKey;
385
- const fkCols = fk.fkColumns.map((c) => this.expr.wrap(c)).join(", ");
386
- const targetTable = this.tableName(fk.targetTable);
387
- const targetCols = fk.targetPkColumns.map((c) => this.expr.wrap(c)).join(", ");
388
- return {
389
- sql: `ALTER TABLE ${table} ADD CONSTRAINT ${this.expr.wrap(fk.name)} FOREIGN KEY (${fkCols}) REFERENCES ${targetTable} (${targetCols})`
390
- };
391
- }
392
- dropForeignKey(def) {
393
- return {
394
- sql: `ALTER TABLE ${this.tableName(def.table)} DROP FOREIGN KEY ${this.expr.wrap(def.foreignKey)}`
395
- };
396
- }
397
- addIndex(def) {
398
- const table = this.tableName(def.table);
399
- const idx = def.index;
400
- const cols = idx.columns.map((c) => `${this.expr.wrap(c.name)} ${c.orderBy}`).join(", ");
401
- const unique = idx.unique ? "UNIQUE " : "";
402
- return { sql: `CREATE ${unique}INDEX ${this.expr.wrap(idx.name)} ON ${table} (${cols})` };
403
- }
404
- dropIndex(def) {
405
- return { sql: `DROP INDEX ${this.expr.wrap(def.index)} ON ${this.tableName(def.table)}` };
406
- }
407
- //#endregion
408
- //#region ========== DDL - View/Procedure ==========
409
- createView(def) {
410
- const view = this.tableName(def.view);
411
- const selectSql = this.select(def.queryDef).sql;
412
- return { sql: `CREATE OR REPLACE VIEW ${view} AS ${selectSql}` };
413
- }
414
- dropView(def) {
415
- return { sql: `DROP VIEW IF EXISTS ${this.tableName(def.view)}` };
416
- }
417
- createProc(def) {
418
- var _a;
419
- const proc = this.tableName(def.procedure);
420
- const paramList = ((_a = def.params) == null ? void 0 : _a.map((p) => {
421
- let sql2 = `IN ${this.expr.wrap(p.name)} ${this.expr.renderDataType(p.dataType)}`;
422
- if (p.default !== void 0) {
423
- sql2 += ` DEFAULT ${this.expr.escapeValue(p.default)}`;
424
- }
425
- return sql2;
426
- }).join(", ")) ?? "";
427
- let sql = `CREATE PROCEDURE ${proc}(${paramList})
428
- `;
429
- sql += `BEGIN
430
- `;
431
- sql += def.query;
432
- if (!def.query.trim().endsWith(";")) {
433
- sql += ";";
434
- }
435
- sql += `
436
- END`;
437
- return { sql };
438
- }
439
- dropProc(def) {
440
- return { sql: `DROP PROCEDURE IF EXISTS ${this.tableName(def.procedure)}` };
441
- }
442
- execProc(def) {
443
- const proc = this.tableName(def.procedure);
444
- if (def.params == null || Object.keys(def.params).length === 0) {
445
- return { sql: `CALL ${proc}()` };
446
- }
447
- const params = Object.values(def.params).map((p) => this.expr.render(p)).join(", ");
448
- return { sql: `CALL ${proc}(${params})` };
449
- }
450
- //#endregion
451
- //#region ========== Utils ==========
452
- clearSchema(def) {
453
- if (!/^[a-zA-Z0-9_]+$/.test(def.database)) {
454
- throw new Error(`Invalid database name: ${def.database}`);
455
- }
456
- const dbName = this.expr.escapeString(def.database);
457
- return {
458
- sql: `
459
- SET FOREIGN_KEY_CHECKS = 0;
460
- SET @tables = NULL;
461
- SELECT GROUP_CONCAT(table_name) INTO @tables FROM information_schema.tables WHERE table_schema = '${dbName}';
462
- SET @drop_stmt = IF(@tables IS NULL, 'SELECT 1', CONCAT('DROP TABLE IF EXISTS ', @tables));
463
- PREPARE stmt FROM @drop_stmt;
464
- EXECUTE stmt;
465
- DEALLOCATE PREPARE stmt;
466
- SET FOREIGN_KEY_CHECKS = 1`
467
- };
468
- }
469
- schemaExists(def) {
470
- const dbName = this.expr.escapeString(def.database);
471
- return {
472
- sql: `SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = '${dbName}'`
473
- };
474
- }
475
- /** MySQL only supports global setting (table parameter is ignored) */
476
- switchFk(def) {
477
- return def.enabled ? { sql: "SET FOREIGN_KEY_CHECKS = 1" } : { sql: "SET FOREIGN_KEY_CHECKS = 0" };
478
- }
479
- //#endregion
95
+ // LOCK (FOR UPDATE at end)
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
+ // No OUTPUT needed: simple batch 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 needed: execute INSERT + SELECT via multi-statement
119
+ // Result sets: [INSERT result, SELECT result, INSERT result, SELECT result, ...]
120
+ // → Extract only SELECT results with resultSetIndex=1, resultSetStride=2
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
+ // SELECT by PK (uses LAST_INSERT_ID() for aiColName)
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
+ // Render existsSelectQuery as SELECT 1 AS _
149
+ const existsQuerySql = this.select({
150
+ ...def.existsSelectQuery,
151
+ select: { _: { type: "value", value: 1 } },
152
+ }).sql;
153
+ // No OUTPUT needed: simple 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 needed: multi-statement (INSERT + SET @affected + SELECT)
159
+ const output = def.output;
160
+ const outputCols = output.columns.map((c) => this.expr.wrap(c)).join(", ");
161
+ // SELECT WHERE condition for OUTPUT
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 (result only if inserted)
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(empty result), 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
+ // Extract columns from INSERT INTO SELECT
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
+ // No OUTPUT needed: simple INSERT INTO SELECT
189
+ if (def.output == null) {
190
+ return { sql: `INSERT INTO ${table} (${colList}) ${selectSql}` };
191
+ }
192
+ // OUTPUT needed: multi-statement
193
+ const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
194
+ // When PK is AI: query range via 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(empty result), results[2]=SELECT
203
+ return { sql: statements.join(";\n"), resultSetIndex: 2 };
204
+ }
205
+ // PK is not AI: save PKs to temp table then query
206
+ const tempTableName = this.expr.wrap("SD_TEMP_" + Uuid.generate().toString().replace(/-/g, ""));
207
+ // Generate SELECT extracting only PK columns from recordsSelectQuery
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
+ // SELECT from target using PK from temp table
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
+ // No OUTPUT needed: simple 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 needed: multi-statement (save PK to temp table + 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
+ // Save target PKs to temp table (since WHERE condition may change after UPDATE)
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
+ // SELECT using PK from temp table (query updated values)
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
+ // Drop temp table
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
+ // No OUTPUT needed: simple 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 needed: multi-statement (save to temp table before delete + 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
+ // Save to temp table before delete
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
+ // Execute 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
+ // Return results from temp table
302
+ const selectSql = `SELECT * FROM ${tempTableName}`;
303
+ // Drop temp table
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 part (alias.column format)
315
+ const updateSetParts = Object.entries(def.updateRecord).map(([col, e]) => `${alias}.${this.expr.wrap(col)} = ${this.expr.render(e)}`);
316
+ // INSERT part
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
+ // Extract WHERE condition (from existsSelectQuery's 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 needed: 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
+ // Save target PKs to temp table (since WHERE condition may change after UPDATE)
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 (update if exists)
341
+ const updateSql = `UPDATE ${table} AS ${alias} SET ${updateSetParts.join(", ")} WHERE ${whereCondition}`;
342
+ // INSERT (NOT EXISTS Pattern)
343
+ const insertSql = `INSERT INTO ${table} (${insertColList}) SELECT ${insertValues} WHERE NOT EXISTS (${existsQuerySql})`;
344
+ // SELECT: query UPDATE result or INSERT result (merged with UNION ALL)
345
+ // UPDATE case: query by PK from temp table
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 case: query by PK from insertRecord (LAST_INSERT_ID() for AI, only when temp table is empty)
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
+ // DROP
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, else → 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
+ // Primary Key with CONSTRAINT name
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 automatically resets 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 supported
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 automatically creates index when adding FK, so no separate index needed
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
+ // Process params
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: DROP all tables (in MySQL, database and schema are synonymous)
543
+ // Query table list from information_schema then 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 and schema are synonymous
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 only supports global setting (table parameter is ignored) */
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
- export {
482
- MysqlQueryBuilder
483
- };
484
- //# sourceMappingURL=mysql-query-builder.js.map
575
+ //# sourceMappingURL=mysql-query-builder.js.map