@simplysm/orm-common 13.0.82 → 13.0.84
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 +106 -0
- package/dist/ddl/initialize.d.ts +2 -2
- package/dist/ddl/initialize.js +1 -1
- package/dist/ddl/initialize.js.map +1 -1
- package/dist/ddl/table-ddl.d.ts +1 -1
- package/dist/exec/queryable.d.ts +115 -115
- package/dist/exec/queryable.js +68 -68
- package/dist/exec/queryable.js.map +1 -1
- package/dist/expr/expr.d.ts +248 -248
- package/dist/expr/expr.js +250 -250
- package/dist/query-builder/base/expr-renderer-base.d.ts +7 -7
- package/dist/query-builder/mssql/mssql-expr-renderer.d.ts +3 -3
- package/dist/query-builder/mssql/mssql-expr-renderer.d.ts.map +1 -1
- package/dist/query-builder/mssql/mssql-expr-renderer.js +5 -5
- 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 +7 -7
- package/dist/query-builder/mysql/mysql-expr-renderer.d.ts +2 -2
- package/dist/query-builder/mysql/mysql-expr-renderer.d.ts.map +1 -1
- package/dist/query-builder/mysql/mysql-expr-renderer.js +4 -4
- 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 +4 -4
- package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts +2 -2
- package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts.map +1 -1
- package/dist/query-builder/postgresql/postgresql-expr-renderer.js +4 -4
- 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 +7 -7
- package/dist/query-builder/query-builder.d.ts +1 -1
- package/dist/schema/factory/column-builder.d.ts +46 -46
- package/dist/schema/factory/column-builder.js +25 -25
- package/dist/schema/factory/index-builder.d.ts +22 -22
- package/dist/schema/factory/index-builder.js +14 -14
- package/dist/schema/factory/relation-builder.d.ts +93 -93
- package/dist/schema/factory/relation-builder.d.ts.map +1 -1
- package/dist/schema/factory/relation-builder.js +37 -37
- package/dist/schema/procedure-builder.d.ts +38 -38
- package/dist/schema/procedure-builder.d.ts.map +1 -1
- package/dist/schema/procedure-builder.js +26 -26
- package/dist/schema/table-builder.d.ts +38 -38
- package/dist/schema/table-builder.d.ts.map +1 -1
- package/dist/schema/table-builder.js +29 -29
- package/dist/schema/view-builder.d.ts +26 -26
- package/dist/schema/view-builder.d.ts.map +1 -1
- package/dist/schema/view-builder.js +18 -18
- package/dist/types/db.d.ts +40 -40
- package/dist/types/expr.d.ts +75 -75
- package/dist/types/expr.d.ts.map +1 -1
- package/dist/types/query-def.d.ts +32 -32
- package/dist/types/query-def.d.ts.map +1 -1
- package/docs/db-context.md +238 -0
- package/docs/expressions.md +413 -0
- package/docs/query-builder.md +198 -0
- package/docs/queryable.md +420 -0
- package/docs/schema-builders.md +216 -0
- package/docs/types-and-utilities.md +353 -0
- package/package.json +4 -3
- package/src/ddl/initialize.ts +16 -16
- package/src/ddl/table-ddl.ts +1 -1
- package/src/exec/queryable.ts +163 -163
- package/src/expr/expr.ts +257 -257
- package/src/query-builder/base/expr-renderer-base.ts +8 -8
- package/src/query-builder/mssql/mssql-expr-renderer.ts +20 -20
- package/src/query-builder/mssql/mssql-query-builder.ts +28 -28
- package/src/query-builder/mysql/mysql-expr-renderer.ts +22 -22
- package/src/query-builder/mysql/mysql-query-builder.ts +65 -65
- package/src/query-builder/postgresql/postgresql-expr-renderer.ts +15 -15
- package/src/query-builder/postgresql/postgresql-query-builder.ts +43 -43
- package/src/query-builder/query-builder.ts +1 -1
- package/src/schema/factory/column-builder.ts +48 -48
- package/src/schema/factory/index-builder.ts +22 -22
- package/src/schema/factory/relation-builder.ts +95 -95
- package/src/schema/procedure-builder.ts +38 -38
- package/src/schema/table-builder.ts +38 -38
- package/src/schema/view-builder.ts +28 -28
- package/src/types/db.ts +41 -41
- package/src/types/expr.ts +79 -79
- package/src/types/query-def.ts +37 -37
- package/tests/ddl/basic.expected.ts +8 -8
|
@@ -39,20 +39,20 @@ import { MysqlExprRenderer } from "./mysql-expr-renderer";
|
|
|
39
39
|
/**
|
|
40
40
|
* MySQL QueryBuilder
|
|
41
41
|
*
|
|
42
|
-
* MySQL
|
|
43
|
-
* - OUTPUT
|
|
44
|
-
* - INSERT OUTPUT: LAST_INSERT_ID()
|
|
45
|
-
* - UPDATE/UPSERT OUTPUT:
|
|
46
|
-
* - DELETE OUTPUT:
|
|
47
|
-
* - switchFk:
|
|
48
|
-
* -
|
|
42
|
+
* MySQL specifics:
|
|
43
|
+
* - No OUTPUT support: workaround via multi-statement pattern (INSERT + SET @var + SELECT)
|
|
44
|
+
* - INSERT OUTPUT: uses LAST_INSERT_ID() for AI column, extracts PK from record for non-AI
|
|
45
|
+
* - UPDATE/UPSERT OUTPUT: saves PK to temp table first since WHERE condition may change after UPDATE, then SELECT
|
|
46
|
+
* - DELETE OUTPUT: saves output columns to temp table before delete
|
|
47
|
+
* - switchFk: global setting (SET FOREIGN_KEY_CHECKS), table parameter is ignored
|
|
48
|
+
* - Index is automatically created when adding FK
|
|
49
49
|
*/
|
|
50
50
|
export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
51
51
|
protected expr = new MysqlExprRenderer((def) => this.select(def).sql);
|
|
52
52
|
|
|
53
|
-
//#region ==========
|
|
53
|
+
//#region ========== Utilities ==========
|
|
54
54
|
|
|
55
|
-
/**
|
|
55
|
+
/** Render table name (MySQL: ignores schema, uses database.table only) */
|
|
56
56
|
protected tableName(obj: QueryDefObjectName): string {
|
|
57
57
|
if (obj.database != null) {
|
|
58
58
|
return `${this.expr.wrap(obj.database)}.${this.expr.wrap(obj.name)}`;
|
|
@@ -60,7 +60,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
60
60
|
return this.expr.wrap(obj.name);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
/** LIMIT
|
|
63
|
+
/** Render LIMIT clause */
|
|
64
64
|
protected renderLimit(limit: [number, number] | undefined, top: number | undefined): string {
|
|
65
65
|
if (limit != null) {
|
|
66
66
|
const [offset, count] = limit;
|
|
@@ -75,15 +75,15 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
75
75
|
protected renderJoin(join: SelectQueryDefJoin): string {
|
|
76
76
|
const alias = this.expr.wrap(join.as);
|
|
77
77
|
|
|
78
|
-
// LATERAL JOIN
|
|
78
|
+
// Detect if LATERAL JOIN is needed
|
|
79
79
|
if (this.needsLateral(join)) {
|
|
80
|
-
// from
|
|
81
|
-
//
|
|
80
|
+
// If from is an array (UNION ALL), use renderFrom(join.from),
|
|
81
|
+
// otherwise (orderBy, top, select, etc.) use renderFrom(join) to generate subquery
|
|
82
82
|
const from = Array.isArray(join.from) ? this.renderFrom(join.from) : this.renderFrom(join);
|
|
83
83
|
return ` LEFT OUTER JOIN LATERAL ${from} AS ${alias} ON TRUE`;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
//
|
|
86
|
+
// Normal JOIN
|
|
87
87
|
const from = this.renderFrom(join.from);
|
|
88
88
|
const where =
|
|
89
89
|
join.where != null && join.where.length > 0
|
|
@@ -128,7 +128,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
128
128
|
|
|
129
129
|
// LOCK
|
|
130
130
|
if (def.lock) {
|
|
131
|
-
// MySQL
|
|
131
|
+
// MySQL: SELECT ... FOR UPDATE (appended at the end)
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
// JOINs
|
|
@@ -171,7 +171,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
171
171
|
const columns = Object.keys(def.records[0]);
|
|
172
172
|
const colList = columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
173
173
|
|
|
174
|
-
// OUTPUT
|
|
174
|
+
// No OUTPUT needed: simple batch INSERT
|
|
175
175
|
if (def.output == null) {
|
|
176
176
|
const valuesList = def.records.map((record) => {
|
|
177
177
|
const values = columns.map((c) => this.expr.escapeValue(record[c]));
|
|
@@ -180,9 +180,9 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
180
180
|
return { sql: `INSERT INTO ${table} (${colList}) VALUES ${valuesList.join(", ")}` };
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
// OUTPUT
|
|
184
|
-
// Result
|
|
185
|
-
// → resultSetIndex=1, resultSetStride=2
|
|
183
|
+
// OUTPUT needed: execute INSERT + SELECT via multi-statement
|
|
184
|
+
// Result sets: [INSERT result, SELECT result, INSERT result, SELECT result, ...]
|
|
185
|
+
// → Extract only SELECT results with resultSetIndex=1, resultSetStride=2
|
|
186
186
|
const output = def.output;
|
|
187
187
|
const outputCols = output.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
188
188
|
const statements: string[] = [];
|
|
@@ -191,7 +191,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
191
191
|
const values = columns.map((c) => this.expr.escapeValue(record[c])).join(", ");
|
|
192
192
|
statements.push(`INSERT INTO ${table} (${colList}) VALUES (${values})`);
|
|
193
193
|
|
|
194
|
-
// PK
|
|
194
|
+
// SELECT by PK (uses LAST_INSERT_ID() for aiColName)
|
|
195
195
|
const whereForSelect = output.pkColNames.map((pk) => {
|
|
196
196
|
const wrappedPk = this.expr.wrap(pk);
|
|
197
197
|
if (pk === output.aiColName) {
|
|
@@ -216,23 +216,23 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
216
216
|
const colList = columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
217
217
|
const values = columns.map((c) => this.expr.escapeValue(def.record[c])).join(", ");
|
|
218
218
|
|
|
219
|
-
// existsSelectQuery
|
|
219
|
+
// Render existsSelectQuery as SELECT 1 AS _
|
|
220
220
|
const existsQuerySql = this.select({
|
|
221
221
|
...def.existsSelectQuery,
|
|
222
222
|
select: { _: { type: "value", value: 1 } },
|
|
223
223
|
}).sql;
|
|
224
224
|
|
|
225
|
-
// OUTPUT
|
|
225
|
+
// No OUTPUT needed: simple INSERT IF NOT EXISTS
|
|
226
226
|
if (def.output == null) {
|
|
227
227
|
const sql = `INSERT INTO ${table} (${colList}) SELECT ${values} WHERE NOT EXISTS (${existsQuerySql})`;
|
|
228
228
|
return { sql };
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
// OUTPUT
|
|
231
|
+
// OUTPUT needed: multi-statement (INSERT + SET @affected + SELECT)
|
|
232
232
|
const output = def.output;
|
|
233
233
|
const outputCols = output.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
234
234
|
|
|
235
|
-
//
|
|
235
|
+
// SELECT WHERE condition for OUTPUT
|
|
236
236
|
const whereForSelect = output.pkColNames.map((pk) => {
|
|
237
237
|
const wrappedPk = this.expr.wrap(pk);
|
|
238
238
|
if (pk === output.aiColName) {
|
|
@@ -241,14 +241,14 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
241
241
|
return `${wrappedPk} = ${this.expr.escapeValue(def.record[pk])}`;
|
|
242
242
|
});
|
|
243
243
|
|
|
244
|
-
// multi-statement: INSERT → SET @affected → SELECT (
|
|
244
|
+
// multi-statement: INSERT → SET @affected → SELECT (result only if inserted)
|
|
245
245
|
const statements = [
|
|
246
246
|
`INSERT INTO ${table} (${colList}) SELECT ${values} WHERE NOT EXISTS (${existsQuerySql})`,
|
|
247
247
|
`SET @sd_affected = ROW_COUNT()`,
|
|
248
248
|
`SELECT ${outputCols} FROM ${table} WHERE ${whereForSelect.join(" AND ")} AND @sd_affected > 0`,
|
|
249
249
|
];
|
|
250
250
|
|
|
251
|
-
// results[0]=INSERT, results[1]=SET(
|
|
251
|
+
// results[0]=INSERT, results[1]=SET(empty result), results[2]=SELECT
|
|
252
252
|
return { sql: statements.join(";\n"), resultSetIndex: 2 };
|
|
253
253
|
}
|
|
254
254
|
|
|
@@ -256,7 +256,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
256
256
|
const table = this.tableName(def.table);
|
|
257
257
|
const selectSql = this.select(def.recordsSelectQuery).sql;
|
|
258
258
|
|
|
259
|
-
// INSERT INTO SELECT
|
|
259
|
+
// Extract columns from INSERT INTO SELECT
|
|
260
260
|
const selectDef = def.recordsSelectQuery;
|
|
261
261
|
const colList =
|
|
262
262
|
selectDef.select != null
|
|
@@ -265,15 +265,15 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
265
265
|
.join(", ")
|
|
266
266
|
: "*";
|
|
267
267
|
|
|
268
|
-
// OUTPUT
|
|
268
|
+
// No OUTPUT needed: simple INSERT INTO SELECT
|
|
269
269
|
if (def.output == null) {
|
|
270
270
|
return { sql: `INSERT INTO ${table} (${colList}) ${selectSql}` };
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
-
// OUTPUT
|
|
273
|
+
// OUTPUT needed: multi-statement
|
|
274
274
|
const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
275
275
|
|
|
276
|
-
// PK
|
|
276
|
+
// When PK is AI: query range via LAST_INSERT_ID() + ROW_COUNT()
|
|
277
277
|
if (def.output.aiColName != null) {
|
|
278
278
|
const aiCol = this.expr.wrap(def.output.aiColName);
|
|
279
279
|
|
|
@@ -283,14 +283,14 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
283
283
|
`SELECT ${outputCols} FROM ${table} WHERE ${aiCol} >= @sd_first_id AND ${aiCol} < @sd_first_id + @sd_count`,
|
|
284
284
|
];
|
|
285
285
|
|
|
286
|
-
// results[0]=INSERT, results[1]=SET(
|
|
286
|
+
// results[0]=INSERT, results[1]=SET(empty result), results[2]=SELECT
|
|
287
287
|
return { sql: statements.join(";\n"), resultSetIndex: 2 };
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
-
// PK
|
|
290
|
+
// PK is not AI: save PKs to temp table then query
|
|
291
291
|
const tempTableName = this.expr.wrap("SD_TEMP_" + Uuid.generate().toString().replace(/-/g, ""));
|
|
292
292
|
|
|
293
|
-
//
|
|
293
|
+
// Generate SELECT extracting only PK columns from recordsSelectQuery
|
|
294
294
|
const pkSelectDef: SelectQueryDef = {
|
|
295
295
|
...def.recordsSelectQuery,
|
|
296
296
|
select: Object.fromEntries(
|
|
@@ -299,7 +299,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
299
299
|
};
|
|
300
300
|
const pkSelectSql = this.select(pkSelectDef).sql;
|
|
301
301
|
|
|
302
|
-
//
|
|
302
|
+
// SELECT from target using PK from temp table
|
|
303
303
|
const pkConditions = def.output.pkColNames.map((pk) => {
|
|
304
304
|
const wrappedPk = this.expr.wrap(pk);
|
|
305
305
|
return `${table}.${wrappedPk} = ${tempTableName}.${wrappedPk}`;
|
|
@@ -329,7 +329,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
329
329
|
([col, expr]) => `${alias}.${this.expr.wrap(col)} = ${this.expr.render(expr)}`,
|
|
330
330
|
);
|
|
331
331
|
|
|
332
|
-
// OUTPUT
|
|
332
|
+
// No OUTPUT needed: simple UPDATE
|
|
333
333
|
if (def.output == null) {
|
|
334
334
|
let sql = `UPDATE ${table} AS ${alias}`;
|
|
335
335
|
sql += this.renderJoins(def.joins);
|
|
@@ -341,11 +341,11 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
341
341
|
return { sql };
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
-
// OUTPUT
|
|
344
|
+
// OUTPUT needed: multi-statement (save PK to temp table + UPDATE + SELECT + DROP)
|
|
345
345
|
const outputCols = def.output.columns.map((c) => `${alias}.${this.expr.wrap(c)}`).join(", ");
|
|
346
346
|
const tempTableName = this.expr.wrap("SD_TEMP_" + Uuid.generate().toString().replace(/-/g, ""));
|
|
347
347
|
|
|
348
|
-
//
|
|
348
|
+
// Save target PKs to temp table (since WHERE condition may change after UPDATE)
|
|
349
349
|
const pkSelectCols = def.output.pkColNames
|
|
350
350
|
.map((pk) => `${alias}.${this.expr.wrap(pk)} AS ${this.expr.wrap(pk)}`)
|
|
351
351
|
.join(", ");
|
|
@@ -360,14 +360,14 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
360
360
|
updateSql += this.renderWhere(def.where);
|
|
361
361
|
if (def.top != null) updateSql += ` LIMIT ${def.top}`;
|
|
362
362
|
|
|
363
|
-
//
|
|
363
|
+
// SELECT using PK from temp table (query updated values)
|
|
364
364
|
const pkConditions = def.output.pkColNames.map((pk) => {
|
|
365
365
|
const wrappedPk = this.expr.wrap(pk);
|
|
366
366
|
return `${alias}.${wrappedPk} = ${tempTableName}.${wrappedPk}`;
|
|
367
367
|
});
|
|
368
368
|
const selectSql = `SELECT ${outputCols} FROM ${table} AS ${alias}, ${tempTableName} WHERE ${pkConditions.join(" AND ")}`;
|
|
369
369
|
|
|
370
|
-
//
|
|
370
|
+
// Drop temp table
|
|
371
371
|
const dropSql = `DROP TEMPORARY TABLE ${tempTableName}`;
|
|
372
372
|
|
|
373
373
|
const statements = [createTempSql, updateSql, selectSql, dropSql];
|
|
@@ -382,7 +382,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
382
382
|
const table = this.tableName(def.table);
|
|
383
383
|
const alias = this.expr.wrap(def.as);
|
|
384
384
|
|
|
385
|
-
// OUTPUT
|
|
385
|
+
// No OUTPUT needed: simple DELETE
|
|
386
386
|
if (def.output == null) {
|
|
387
387
|
let sql = `DELETE ${alias} FROM ${table} AS ${alias}`;
|
|
388
388
|
sql += this.renderJoins(def.joins);
|
|
@@ -393,25 +393,25 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
393
393
|
return { sql };
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
-
// OUTPUT
|
|
396
|
+
// OUTPUT needed: multi-statement (save to temp table before delete + DELETE + SELECT + DROP)
|
|
397
397
|
const outputCols = def.output.columns.map((c) => `${alias}.${this.expr.wrap(c)}`).join(", ");
|
|
398
398
|
const tempTableName = this.expr.wrap("SD_TEMP_" + Uuid.generate().toString().replace(/-/g, ""));
|
|
399
399
|
|
|
400
|
-
//
|
|
400
|
+
// Save to temp table before delete
|
|
401
401
|
let createTempSql = `CREATE TEMPORARY TABLE ${tempTableName} AS SELECT ${outputCols} FROM ${table} AS ${alias}`;
|
|
402
402
|
createTempSql += this.renderJoins(def.joins);
|
|
403
403
|
createTempSql += this.renderWhere(def.where);
|
|
404
404
|
|
|
405
|
-
// DELETE
|
|
405
|
+
// Execute DELETE
|
|
406
406
|
let deleteSql = `DELETE ${alias} FROM ${table} AS ${alias}`;
|
|
407
407
|
deleteSql += this.renderJoins(def.joins);
|
|
408
408
|
deleteSql += this.renderWhere(def.where);
|
|
409
409
|
if (def.top != null) deleteSql += ` LIMIT ${def.top}`;
|
|
410
410
|
|
|
411
|
-
//
|
|
411
|
+
// Return results from temp table
|
|
412
412
|
const selectSql = `SELECT * FROM ${tempTableName}`;
|
|
413
413
|
|
|
414
|
-
//
|
|
414
|
+
// Drop temp table
|
|
415
415
|
const dropSql = `DROP TEMPORARY TABLE ${tempTableName}`;
|
|
416
416
|
|
|
417
417
|
const statements = [createTempSql, deleteSql, selectSql, dropSql];
|
|
@@ -427,7 +427,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
427
427
|
const alias = this.expr.wrap(def.existsSelectQuery.as);
|
|
428
428
|
const existsQuerySql = this.select(def.existsSelectQuery).sql;
|
|
429
429
|
|
|
430
|
-
// UPDATE SET part (alias.column
|
|
430
|
+
// UPDATE SET part (alias.column format)
|
|
431
431
|
const updateSetParts = Object.entries(def.updateRecord).map(
|
|
432
432
|
([col, e]) => `${alias}.${this.expr.wrap(col)} = ${this.expr.render(e)}`,
|
|
433
433
|
);
|
|
@@ -437,16 +437,16 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
437
437
|
const insertColList = insertColumns.map((c) => this.expr.wrap(c)).join(", ");
|
|
438
438
|
const insertValues = insertColumns.map((c) => this.expr.render(def.insertRecord[c])).join(", ");
|
|
439
439
|
|
|
440
|
-
// WHERE condition
|
|
440
|
+
// Extract WHERE condition (from existsSelectQuery's where)
|
|
441
441
|
const whereCondition =
|
|
442
442
|
def.existsSelectQuery.where != null && def.existsSelectQuery.where.length > 0
|
|
443
443
|
? this.expr.renderWhere(def.existsSelectQuery.where)
|
|
444
444
|
: "1=1";
|
|
445
445
|
|
|
446
|
-
// OUTPUT
|
|
446
|
+
// No OUTPUT needed: multi-statement (UPDATE + INSERT WHERE NOT EXISTS)
|
|
447
447
|
if (def.output == null) {
|
|
448
|
-
// UPDATE:
|
|
449
|
-
// INSERT SELECT WHERE NOT EXISTS:
|
|
448
|
+
// UPDATE: updates if exists
|
|
449
|
+
// INSERT SELECT WHERE NOT EXISTS: inserts if not exists
|
|
450
450
|
const statements = [
|
|
451
451
|
`UPDATE ${table} AS ${alias} SET ${updateSetParts.join(", ")} WHERE ${whereCondition}`,
|
|
452
452
|
`INSERT INTO ${table} (${insertColList}) SELECT ${insertValues} WHERE NOT EXISTS (${existsQuerySql})`,
|
|
@@ -454,22 +454,22 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
454
454
|
return { sql: statements.join(";\n") };
|
|
455
455
|
}
|
|
456
456
|
|
|
457
|
-
// OUTPUT
|
|
457
|
+
// OUTPUT needed: multi-statement (CREATE TEMP + UPDATE + INSERT + SELECT + DROP)
|
|
458
458
|
const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
459
459
|
const tempTableName = this.expr.wrap("SD_TEMP_" + Uuid.generate().toString().replace(/-/g, ""));
|
|
460
460
|
|
|
461
|
-
//
|
|
461
|
+
// Save target PKs to temp table (since WHERE condition may change after UPDATE)
|
|
462
462
|
const pkSelectCols = def.output.pkColNames.map((pk) => this.expr.wrap(pk)).join(", ");
|
|
463
463
|
const createTempSql = `CREATE TEMPORARY TABLE ${tempTableName} AS SELECT ${pkSelectCols} FROM ${table} AS ${alias} WHERE ${whereCondition}`;
|
|
464
464
|
|
|
465
|
-
// UPDATE (
|
|
465
|
+
// UPDATE (update if exists)
|
|
466
466
|
const updateSql = `UPDATE ${table} AS ${alias} SET ${updateSetParts.join(", ")} WHERE ${whereCondition}`;
|
|
467
467
|
|
|
468
468
|
// INSERT (NOT EXISTS Pattern)
|
|
469
469
|
const insertSql = `INSERT INTO ${table} (${insertColList}) SELECT ${insertValues} WHERE NOT EXISTS (${existsQuerySql})`;
|
|
470
470
|
|
|
471
|
-
// SELECT: UPDATE result
|
|
472
|
-
// UPDATE
|
|
471
|
+
// SELECT: query UPDATE result or INSERT result (merged with UNION ALL)
|
|
472
|
+
// UPDATE case: query by PK from temp table
|
|
473
473
|
const output = def.output;
|
|
474
474
|
const updatePkConditions = output.pkColNames.map((pk) => {
|
|
475
475
|
const wrappedPk = this.expr.wrap(pk);
|
|
@@ -477,7 +477,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
477
477
|
});
|
|
478
478
|
const selectUpdateSql = `SELECT ${outputCols} FROM ${table} WHERE ${updatePkConditions.join(" AND ")}`;
|
|
479
479
|
|
|
480
|
-
// INSERT
|
|
480
|
+
// INSERT case: query by PK from insertRecord (LAST_INSERT_ID() for AI, only when temp table is empty)
|
|
481
481
|
const insertPkConditions = output.pkColNames.map((pk) => {
|
|
482
482
|
const wrappedPk = this.expr.wrap(pk);
|
|
483
483
|
if (pk === output.aiColName) {
|
|
@@ -544,7 +544,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
544
544
|
}
|
|
545
545
|
|
|
546
546
|
protected truncate(def: TruncateQueryDef): QueryBuildResult {
|
|
547
|
-
// MySQL: TRUNCATE
|
|
547
|
+
// MySQL: TRUNCATE automatically resets AUTO_INCREMENT
|
|
548
548
|
return { sql: `TRUNCATE TABLE ${this.tableName(def.table)}` };
|
|
549
549
|
}
|
|
550
550
|
|
|
@@ -608,7 +608,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
608
608
|
|
|
609
609
|
protected renameColumn(def: RenameColumnQueryDef): QueryBuildResult {
|
|
610
610
|
const table = this.tableName(def.table);
|
|
611
|
-
// MySQL 8.0+: RENAME COLUMN
|
|
611
|
+
// MySQL 8.0+: RENAME COLUMN supported
|
|
612
612
|
return {
|
|
613
613
|
sql: `ALTER TABLE ${table} RENAME COLUMN ${this.expr.wrap(def.column)} TO ${this.expr.wrap(def.newName)}`,
|
|
614
614
|
};
|
|
@@ -635,7 +635,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
635
635
|
const targetTable = this.tableName(fk.targetTable);
|
|
636
636
|
const targetCols = fk.targetPkColumns.map((c) => this.expr.wrap(c)).join(", ");
|
|
637
637
|
|
|
638
|
-
// MySQL
|
|
638
|
+
// MySQL automatically creates index when adding FK, so no separate index needed
|
|
639
639
|
return {
|
|
640
640
|
sql: `ALTER TABLE ${table} ADD CONSTRAINT ${this.expr.wrap(fk.name)} FOREIGN KEY (${fkCols}) REFERENCES ${targetTable} (${targetCols})`,
|
|
641
641
|
};
|
|
@@ -676,7 +676,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
676
676
|
protected createProc(def: CreateProcQueryDef): QueryBuildResult {
|
|
677
677
|
const proc = this.tableName(def.procedure);
|
|
678
678
|
|
|
679
|
-
// params
|
|
679
|
+
// Process params
|
|
680
680
|
const paramList =
|
|
681
681
|
def.params
|
|
682
682
|
?.map((p) => {
|
|
@@ -719,9 +719,9 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
|
|
|
719
719
|
//#region ========== Utils ==========
|
|
720
720
|
|
|
721
721
|
protected clearSchema(def: ClearSchemaQueryDef): QueryBuildResult {
|
|
722
|
-
// MySQL:
|
|
723
|
-
//
|
|
724
|
-
// SQL
|
|
722
|
+
// MySQL: DROP all tables (in MySQL, database and schema are synonymous)
|
|
723
|
+
// Query table list from information_schema then DROP
|
|
724
|
+
// SQL injection prevention: identifier validation
|
|
725
725
|
if (!/^[a-zA-Z0-9_]+$/.test(def.database)) {
|
|
726
726
|
throw new Error(`Invalid database name: ${def.database}`);
|
|
727
727
|
}
|
|
@@ -741,14 +741,14 @@ SET FOREIGN_KEY_CHECKS = 1`,
|
|
|
741
741
|
}
|
|
742
742
|
|
|
743
743
|
protected schemaExists(def: SchemaExistsQueryDef): QueryBuildResult {
|
|
744
|
-
// MySQL: database
|
|
744
|
+
// MySQL: database and schema are synonymous
|
|
745
745
|
const dbName = this.expr.escapeString(def.database);
|
|
746
746
|
return {
|
|
747
747
|
sql: `SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = '${dbName}'`,
|
|
748
748
|
};
|
|
749
749
|
}
|
|
750
750
|
|
|
751
|
-
/** MySQL
|
|
751
|
+
/** MySQL only supports global setting (table parameter is ignored) */
|
|
752
752
|
protected switchFk(def: SwitchFkQueryDef): QueryBuildResult {
|
|
753
753
|
return def.enabled
|
|
754
754
|
? { sql: "SET FOREIGN_KEY_CHECKS = 1" }
|
|
@@ -72,14 +72,14 @@ import { ExprRendererBase } from "../base/expr-renderer-base";
|
|
|
72
72
|
* PostgreSQL expression renderer
|
|
73
73
|
*/
|
|
74
74
|
export class PostgresqlExprRenderer extends ExprRendererBase {
|
|
75
|
-
//#region ==========
|
|
75
|
+
//#region ========== Utilities (public - also used by QueryBuilder) ==========
|
|
76
76
|
|
|
77
|
-
/**
|
|
77
|
+
/** Wrap identifier */
|
|
78
78
|
wrap(name: string): string {
|
|
79
79
|
return `"${name.replace(/"/g, '""')}"`;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
/** SQL
|
|
82
|
+
/** Escape for SQL string literals (returns without quotes) */
|
|
83
83
|
escapeString(value: string): string {
|
|
84
84
|
return value.replace(/'/g, "''");
|
|
85
85
|
}
|
|
@@ -176,7 +176,7 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
|
|
|
176
176
|
//#region ========== comparison (null-safe) ==========
|
|
177
177
|
|
|
178
178
|
protected eq(expr: ExprEq): string {
|
|
179
|
-
// PostgreSQL: null-safe equal (IS NOT DISTINCT FROM operator
|
|
179
|
+
// PostgreSQL: null-safe equal (uses IS NOT DISTINCT FROM operator)
|
|
180
180
|
const left = this.render(expr.source);
|
|
181
181
|
const right = this.render(expr.target);
|
|
182
182
|
return `${left} IS NOT DISTINCT FROM ${right}`;
|
|
@@ -217,7 +217,7 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
|
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
protected like(expr: ExprLike): string {
|
|
220
|
-
// ESCAPE '\'
|
|
220
|
+
// Always add ESCAPE '\'
|
|
221
221
|
return `${this.render(expr.source)} LIKE ${this.render(expr.pattern)} ESCAPE '\\'`;
|
|
222
222
|
}
|
|
223
223
|
|
|
@@ -228,7 +228,7 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
|
|
|
228
228
|
|
|
229
229
|
protected in(expr: ExprIn): string {
|
|
230
230
|
if (expr.values.length === 0) {
|
|
231
|
-
return "FALSE"; //
|
|
231
|
+
return "FALSE"; // empty IN is always false
|
|
232
232
|
}
|
|
233
233
|
const values = expr.values.map((v) => this.render(v)).join(", ");
|
|
234
234
|
return `${this.render(expr.source)} IN (${values})`;
|
|
@@ -239,7 +239,7 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
|
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
protected exists(expr: ExprExists): string {
|
|
242
|
-
// SELECT 1
|
|
242
|
+
// Render as SELECT 1
|
|
243
243
|
const subquery = this.buildSelect({
|
|
244
244
|
...expr.query,
|
|
245
245
|
select: { _: { type: "value", value: 1 } },
|
|
@@ -267,10 +267,10 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
|
|
|
267
267
|
|
|
268
268
|
//#endregion
|
|
269
269
|
|
|
270
|
-
//#region ==========
|
|
270
|
+
//#region ========== String (null handling) ==========
|
|
271
271
|
|
|
272
272
|
protected concat(expr: ExprConcat): string {
|
|
273
|
-
// PostgreSQL: ||
|
|
273
|
+
// PostgreSQL: uses || operator with COALESCE
|
|
274
274
|
const args = expr.args.map((a) => `COALESCE(${this.render(a)}, '')`);
|
|
275
275
|
return args.join(" || ");
|
|
276
276
|
}
|
|
@@ -304,12 +304,12 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
|
|
|
304
304
|
}
|
|
305
305
|
|
|
306
306
|
protected length(expr: ExprLength): string {
|
|
307
|
-
// PostgreSQL: LENGTH() (null
|
|
307
|
+
// PostgreSQL: LENGTH() (null handling)
|
|
308
308
|
return `LENGTH(COALESCE(${this.render(expr.arg)}, ''))`;
|
|
309
309
|
}
|
|
310
310
|
|
|
311
311
|
protected byteLength(expr: ExprByteLength): string {
|
|
312
|
-
// PostgreSQL: OCTET_LENGTH() (null
|
|
312
|
+
// PostgreSQL: OCTET_LENGTH() (null handling)
|
|
313
313
|
return `OCTET_LENGTH(COALESCE(${this.render(expr.arg)}, ''))`;
|
|
314
314
|
}
|
|
315
315
|
|
|
@@ -378,7 +378,7 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
|
|
|
378
378
|
|
|
379
379
|
protected isoWeekStartDate(expr: ExprIsoWeekStartDate): string {
|
|
380
380
|
const src = this.render(expr.arg);
|
|
381
|
-
// ISO
|
|
381
|
+
// ISO week start date (Monday)
|
|
382
382
|
return `DATE_TRUNC('week', ${src})::DATE`;
|
|
383
383
|
}
|
|
384
384
|
|
|
@@ -511,13 +511,13 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
|
|
|
511
511
|
|
|
512
512
|
protected greatest(expr: ExprGreatest): string {
|
|
513
513
|
if (expr.args.length === 0) throw new Error("greatest requires at least one argument.");
|
|
514
|
-
// PostgreSQL: GREATEST
|
|
514
|
+
// PostgreSQL: native GREATEST support
|
|
515
515
|
return `GREATEST(${expr.args.map((a) => this.render(a)).join(", ")})`;
|
|
516
516
|
}
|
|
517
517
|
|
|
518
518
|
protected least(expr: ExprLeast): string {
|
|
519
519
|
if (expr.args.length === 0) throw new Error("least requires at least one argument.");
|
|
520
|
-
// PostgreSQL: LEAST
|
|
520
|
+
// PostgreSQL: native LEAST support
|
|
521
521
|
return `LEAST(${expr.args.map((a) => this.render(a)).join(", ")})`;
|
|
522
522
|
}
|
|
523
523
|
|
|
@@ -541,7 +541,7 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
|
|
|
541
541
|
const fn = this.renderWindowFn(expr.fn);
|
|
542
542
|
let over = this.renderWindowSpec(expr.spec);
|
|
543
543
|
|
|
544
|
-
// LAST_VALUE
|
|
544
|
+
// LAST_VALUE default frame only sees up to CURRENT ROW, so full frame must be specified
|
|
545
545
|
if (expr.fn.type === "lastValue" && over.length > 0) {
|
|
546
546
|
over += " ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING";
|
|
547
547
|
}
|