@simplysm/orm-common 13.0.83 → 13.0.85
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/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/package.json +2 -2
- 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
|
@@ -68,12 +68,12 @@ import type {
|
|
|
68
68
|
import type { SelectQueryDef } from "../../types/query-def";
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
|
-
* Expr → SQL Render
|
|
71
|
+
* Expr → SQL Render abstract base class
|
|
72
72
|
*
|
|
73
73
|
* Base principles:
|
|
74
74
|
* - Implement only 100% identical logic across all dialects (dispatch)
|
|
75
75
|
* - If different at all, make it abstract
|
|
76
|
-
* - Method
|
|
76
|
+
* - Method names match expr.type (enables dynamic dispatch)
|
|
77
77
|
*/
|
|
78
78
|
export abstract class ExprRendererBase {
|
|
79
79
|
constructor(protected buildSelect: (def: SelectQueryDef) => string) {}
|
|
@@ -81,20 +81,20 @@ export abstract class ExprRendererBase {
|
|
|
81
81
|
//#region ========== Public Utilities ==========
|
|
82
82
|
|
|
83
83
|
/**
|
|
84
|
-
*
|
|
84
|
+
* Wrap identifier (table name, column name, etc.)
|
|
85
85
|
* MySQL: `name`, MSSQL: [name], PostgreSQL: "name"
|
|
86
86
|
*/
|
|
87
87
|
abstract wrap(name: string): string;
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
|
-
* SQL
|
|
91
|
-
*
|
|
92
|
-
*
|
|
90
|
+
* Escape for SQL string literals
|
|
91
|
+
* Called when used as a string value in dynamic SQL or system queries
|
|
92
|
+
* e.g.: WHERE schema_name = 'escaped_value'
|
|
93
93
|
*/
|
|
94
94
|
abstract escapeString(value: string): string;
|
|
95
95
|
|
|
96
96
|
/**
|
|
97
|
-
*
|
|
97
|
+
* Value escape (transform to appropriate SQL literal based on type)
|
|
98
98
|
*/
|
|
99
99
|
abstract escapeValue(value: unknown): string;
|
|
100
100
|
|
|
@@ -150,7 +150,7 @@ export abstract class ExprRendererBase {
|
|
|
150
150
|
|
|
151
151
|
//#endregion
|
|
152
152
|
|
|
153
|
-
//#region ========== Abstract -
|
|
153
|
+
//#region ========== Abstract - String (null processing required) ==========
|
|
154
154
|
|
|
155
155
|
protected abstract concat(expr: ExprConcat): string;
|
|
156
156
|
protected abstract left(expr: ExprLeft): string;
|
|
@@ -72,19 +72,19 @@ import { ExprRendererBase } from "../base/expr-renderer-base";
|
|
|
72
72
|
* MSSQL expression renderer
|
|
73
73
|
*/
|
|
74
74
|
export class MssqlExprRenderer 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
|
}
|
|
86
86
|
|
|
87
|
-
/**
|
|
87
|
+
/** Value escape */
|
|
88
88
|
escapeValue(value: unknown): string {
|
|
89
89
|
if (value == null) {
|
|
90
90
|
return "NULL";
|
|
@@ -217,18 +217,18 @@ export class MssqlExprRenderer 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
|
|
|
224
224
|
protected regexp(_expr: ExprRegexp): string {
|
|
225
|
-
// MSSQL
|
|
225
|
+
// MSSQL does not support REGEXP - needs LIKE pattern or CLR
|
|
226
226
|
throw new Error("MSSQL does not natively support REGEXP.");
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
protected in(expr: ExprIn): string {
|
|
230
230
|
if (expr.values.length === 0) {
|
|
231
|
-
return "1=0"; //
|
|
231
|
+
return "1=0"; // 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 MssqlExprRenderer 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 MssqlExprRenderer 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
|
-
// MSSQL 2012+: CONCAT
|
|
273
|
+
// MSSQL 2012+: CONCAT function automatically treats NULL as empty string
|
|
274
274
|
const args = expr.args.map((a) => this.render(a)).join(", ");
|
|
275
275
|
return `CONCAT(${args})`;
|
|
276
276
|
}
|
|
@@ -308,12 +308,12 @@ export class MssqlExprRenderer extends ExprRendererBase {
|
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
protected length(expr: ExprLength): string {
|
|
311
|
-
// MSSQL: LEN() (null
|
|
311
|
+
// MSSQL: LEN() (null handling)
|
|
312
312
|
return `LEN(ISNULL(${this.render(expr.arg)}, N''))`;
|
|
313
313
|
}
|
|
314
314
|
|
|
315
315
|
protected byteLength(expr: ExprByteLength): string {
|
|
316
|
-
// MSSQL: DATALENGTH() (null
|
|
316
|
+
// MSSQL: DATALENGTH() (null handling)
|
|
317
317
|
return `DATALENGTH(ISNULL(${this.render(expr.arg)}, N''))`;
|
|
318
318
|
}
|
|
319
319
|
|
|
@@ -321,7 +321,7 @@ export class MssqlExprRenderer extends ExprRendererBase {
|
|
|
321
321
|
if (expr.length != null) {
|
|
322
322
|
return `SUBSTRING(${this.render(expr.source)}, ${this.render(expr.start)}, ${this.render(expr.length)})`;
|
|
323
323
|
}
|
|
324
|
-
// MSSQL: length
|
|
324
|
+
// MSSQL: if no length, go to end
|
|
325
325
|
return `SUBSTRING(${this.render(expr.source)}, ${this.render(expr.start)}, LEN(${this.render(expr.source)}))`;
|
|
326
326
|
}
|
|
327
327
|
|
|
@@ -384,9 +384,9 @@ export class MssqlExprRenderer extends ExprRendererBase {
|
|
|
384
384
|
|
|
385
385
|
protected isoWeekStartDate(expr: ExprIsoWeekStartDate): string {
|
|
386
386
|
const src = this.render(expr.arg);
|
|
387
|
-
// ISO
|
|
388
|
-
//
|
|
389
|
-
// (
|
|
387
|
+
// ISO week start date (Monday) - always returns Monday regardless of @@DATEFIRST
|
|
388
|
+
// Principle: DATEDIFF(DAY, 0, date) is the number of days from 1900-01-01 (Monday)
|
|
389
|
+
// (days + 6) % 7 + 1 = 1(Mon), 2(Tue), ..., 7(Sun)
|
|
390
390
|
const weekDay = `((DATEDIFF(DAY, 0, ${src}) + 6) % 7 + 1)`;
|
|
391
391
|
return `DATEADD(DAY, 1 - ${weekDay}, CAST(${src} AS DATE))`;
|
|
392
392
|
}
|
|
@@ -434,7 +434,7 @@ export class MssqlExprRenderer extends ExprRendererBase {
|
|
|
434
434
|
}
|
|
435
435
|
|
|
436
436
|
private convertDateFormat(format: string): string {
|
|
437
|
-
// MSSQL FORMAT
|
|
437
|
+
// For MSSQL FORMAT function (uses the same format)
|
|
438
438
|
return format;
|
|
439
439
|
}
|
|
440
440
|
|
|
@@ -504,7 +504,7 @@ export class MssqlExprRenderer extends ExprRendererBase {
|
|
|
504
504
|
protected greatest(expr: ExprGreatest): string {
|
|
505
505
|
if (expr.args.length === 0) throw new Error("greatest requires at least one argument.");
|
|
506
506
|
if (expr.args.length === 1) return this.render(expr.args[0]);
|
|
507
|
-
// MSSQL 2012+: VALUES + MAX
|
|
507
|
+
// MSSQL 2012+: VALUES + MAX approach
|
|
508
508
|
const values = expr.args.map((a) => `(${this.render(a)})`).join(", ");
|
|
509
509
|
return `(SELECT MAX(v) FROM (VALUES ${values}) AS t(v))`;
|
|
510
510
|
}
|
|
@@ -512,7 +512,7 @@ export class MssqlExprRenderer extends ExprRendererBase {
|
|
|
512
512
|
protected least(expr: ExprLeast): string {
|
|
513
513
|
if (expr.args.length === 0) throw new Error("least requires at least one argument.");
|
|
514
514
|
if (expr.args.length === 1) return this.render(expr.args[0]);
|
|
515
|
-
// MSSQL 2012+: VALUES + MIN
|
|
515
|
+
// MSSQL 2012+: VALUES + MIN approach
|
|
516
516
|
const values = expr.args.map((a) => `(${this.render(a)})`).join(", ");
|
|
517
517
|
return `(SELECT MIN(v) FROM (VALUES ${values}) AS t(v))`;
|
|
518
518
|
}
|
|
@@ -537,7 +537,7 @@ export class MssqlExprRenderer extends ExprRendererBase {
|
|
|
537
537
|
const fn = this.renderWindowFn(expr.fn);
|
|
538
538
|
let over = this.renderWindowSpec(expr.spec);
|
|
539
539
|
|
|
540
|
-
// LAST_VALUE
|
|
540
|
+
// LAST_VALUE default frame only sees up to CURRENT ROW, so full frame must be specified
|
|
541
541
|
if (expr.fn.type === "lastValue" && over.length > 0) {
|
|
542
542
|
over += " ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING";
|
|
543
543
|
}
|
|
@@ -41,9 +41,9 @@ import { MssqlExprRenderer } from "./mssql-expr-renderer";
|
|
|
41
41
|
export class MssqlQueryBuilder extends QueryBuilderBase {
|
|
42
42
|
protected expr = new MssqlExprRenderer((def) => this.select(def).sql);
|
|
43
43
|
|
|
44
|
-
//#region ==========
|
|
44
|
+
//#region ========== Utilities ==========
|
|
45
45
|
|
|
46
|
-
/**
|
|
46
|
+
/** Render table name */
|
|
47
47
|
protected tableName(obj: QueryDefObjectName): string {
|
|
48
48
|
const parts: string[] = [];
|
|
49
49
|
if (obj.database != null) {
|
|
@@ -58,7 +58,7 @@ export class MssqlQueryBuilder extends QueryBuilderBase {
|
|
|
58
58
|
return parts.join(".");
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
/** OFFSET...FETCH
|
|
61
|
+
/** Render OFFSET...FETCH clause */
|
|
62
62
|
protected renderLimit(limit: [number, number] | undefined): string {
|
|
63
63
|
if (limit == null) return "";
|
|
64
64
|
const [offset, count] = limit;
|
|
@@ -68,15 +68,15 @@ export class MssqlQueryBuilder extends QueryBuilderBase {
|
|
|
68
68
|
protected renderJoin(join: SelectQueryDefJoin): string {
|
|
69
69
|
const alias = this.expr.wrap(join.as);
|
|
70
70
|
|
|
71
|
-
// LATERAL JOIN
|
|
71
|
+
// Detect if LATERAL JOIN is needed → MSSQL uses OUTER APPLY
|
|
72
72
|
if (this.needsLateral(join)) {
|
|
73
|
-
// from
|
|
74
|
-
//
|
|
73
|
+
// If from is an array (UNION ALL), use renderFrom(join.from),
|
|
74
|
+
// otherwise (orderBy, top, select, etc.) use renderFrom(join) to generate subquery
|
|
75
75
|
const from = Array.isArray(join.from) ? this.renderFrom(join.from) : this.renderFrom(join);
|
|
76
76
|
return ` OUTER APPLY ${from} AS ${alias}`;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
//
|
|
79
|
+
// Normal JOIN
|
|
80
80
|
const from = this.renderFrom(join.from);
|
|
81
81
|
const where =
|
|
82
82
|
join.where != null && join.where.length > 0
|
|
@@ -122,7 +122,7 @@ export class MssqlQueryBuilder extends QueryBuilderBase {
|
|
|
122
122
|
const from = this.renderFrom(def.from);
|
|
123
123
|
sql += ` FROM ${from} AS ${this.expr.wrap(def.as)}`;
|
|
124
124
|
|
|
125
|
-
// LOCK (
|
|
125
|
+
// LOCK (force row-level lock with ROWLOCK - same behavior as MySQL/PostgreSQL FOR UPDATE)
|
|
126
126
|
if (def.lock) {
|
|
127
127
|
sql += " WITH (UPDLOCK, ROWLOCK)";
|
|
128
128
|
}
|
|
@@ -165,14 +165,14 @@ export class MssqlQueryBuilder extends QueryBuilderBase {
|
|
|
165
165
|
|
|
166
166
|
let sql = "";
|
|
167
167
|
|
|
168
|
-
// IDENTITY_INSERT ON (
|
|
168
|
+
// IDENTITY_INSERT ON (when inserting explicit values into AI column)
|
|
169
169
|
if (def.overrideIdentity) {
|
|
170
170
|
sql += `SET IDENTITY_INSERT ${table} ON;\n`;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
sql += `INSERT INTO ${table} (${colList})`;
|
|
174
174
|
|
|
175
|
-
// OUTPUT (MSSQL
|
|
175
|
+
// OUTPUT (MSSQL native support)
|
|
176
176
|
if (def.output != null) {
|
|
177
177
|
const outputCols = def.output.columns.map((c) => `INSERTED.${this.expr.wrap(c)}`).join(", ");
|
|
178
178
|
sql += ` OUTPUT ${outputCols}`;
|
|
@@ -191,7 +191,7 @@ export class MssqlQueryBuilder extends QueryBuilderBase {
|
|
|
191
191
|
sql += `;\nSET IDENTITY_INSERT ${table} OFF;`;
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
// overrideIdentity
|
|
194
|
+
// With overrideIdentity: SET ON → results[0], INSERT → results[1], SET OFF → results[2]
|
|
195
195
|
return { sql, resultSetIndex: def.overrideIdentity ? 1 : undefined };
|
|
196
196
|
}
|
|
197
197
|
|
|
@@ -202,7 +202,7 @@ export class MssqlQueryBuilder extends QueryBuilderBase {
|
|
|
202
202
|
const colList = columns.map((c) => this.expr.wrap(c)).join(", ");
|
|
203
203
|
const values = columns.map((c) => this.expr.escapeValue(def.record[c])).join(", ");
|
|
204
204
|
|
|
205
|
-
// existsSelectQuery
|
|
205
|
+
// Render existsSelectQuery as SELECT 1 AS _
|
|
206
206
|
const existsQuerySql = this.select({
|
|
207
207
|
...def.existsSelectQuery,
|
|
208
208
|
select: { _: { type: "value", value: 1 } },
|
|
@@ -210,7 +210,7 @@ export class MssqlQueryBuilder extends QueryBuilderBase {
|
|
|
210
210
|
|
|
211
211
|
let sql = `INSERT INTO ${table} (${colList})`;
|
|
212
212
|
|
|
213
|
-
// OUTPUT (MSSQL: OUTPUT
|
|
213
|
+
// OUTPUT (MSSQL: OUTPUT must be placed before SELECT)
|
|
214
214
|
if (def.output != null) {
|
|
215
215
|
const outputCols = def.output.columns.map((c) => `INSERTED.${this.expr.wrap(c)}`).join(", ");
|
|
216
216
|
sql += ` OUTPUT ${outputCols}`;
|
|
@@ -225,7 +225,7 @@ export class MssqlQueryBuilder extends QueryBuilderBase {
|
|
|
225
225
|
const table = this.tableName(def.table);
|
|
226
226
|
const selectSql = this.select(def.recordsSelectQuery).sql;
|
|
227
227
|
|
|
228
|
-
// INSERT INTO SELECT
|
|
228
|
+
// Extract columns from INSERT INTO SELECT
|
|
229
229
|
const selectDef = def.recordsSelectQuery;
|
|
230
230
|
const colList =
|
|
231
231
|
selectDef.select != null
|
|
@@ -310,7 +310,7 @@ export class MssqlQueryBuilder extends QueryBuilderBase {
|
|
|
310
310
|
//#region ========== DML - UPSERT ==========
|
|
311
311
|
|
|
312
312
|
protected upsert(def: UpsertQueryDef): QueryBuildResult {
|
|
313
|
-
// MSSQL: MERGE
|
|
313
|
+
// MSSQL: Use MERGE
|
|
314
314
|
const table = this.tableName(def.table);
|
|
315
315
|
const alias = this.expr.wrap(def.existsSelectQuery.as);
|
|
316
316
|
const existsWhere = def.existsSelectQuery.where;
|
|
@@ -389,14 +389,14 @@ export class MssqlQueryBuilder extends QueryBuilderBase {
|
|
|
389
389
|
}
|
|
390
390
|
|
|
391
391
|
protected renameTable(def: RenameTableQueryDef): QueryBuildResult {
|
|
392
|
-
// MSSQL: sp_rename
|
|
392
|
+
// MSSQL: Use sp_rename
|
|
393
393
|
const tableName = this.expr.escapeString(this.tableName(def.table));
|
|
394
394
|
const newName = this.expr.escapeString(def.newName);
|
|
395
395
|
return { sql: `EXEC sp_rename '${tableName}', '${newName}'` };
|
|
396
396
|
}
|
|
397
397
|
|
|
398
398
|
protected truncate(def: TruncateQueryDef): QueryBuildResult {
|
|
399
|
-
// MSSQL: TRUNCATE
|
|
399
|
+
// MSSQL: TRUNCATE automatically resets IDENTITY
|
|
400
400
|
return { sql: `TRUNCATE TABLE ${this.tableName(def.table)}` };
|
|
401
401
|
}
|
|
402
402
|
|
|
@@ -447,13 +447,13 @@ export class MssqlQueryBuilder extends QueryBuilderBase {
|
|
|
447
447
|
colSql += " NOT NULL";
|
|
448
448
|
}
|
|
449
449
|
|
|
450
|
-
// MSSQL: ALTER COLUMN (IDENTITY
|
|
450
|
+
// MSSQL: ALTER COLUMN (IDENTITY and DEFAULT require separate handling)
|
|
451
451
|
return { sql: `ALTER TABLE ${table} ALTER COLUMN ${colSql}` };
|
|
452
452
|
}
|
|
453
453
|
|
|
454
454
|
protected renameColumn(def: RenameColumnQueryDef): QueryBuildResult {
|
|
455
455
|
const table = this.tableName(def.table);
|
|
456
|
-
// MSSQL: sp_rename
|
|
456
|
+
// MSSQL: Use sp_rename
|
|
457
457
|
const tableCol = this.expr.escapeString(`${table}.${def.column}`);
|
|
458
458
|
const newName = this.expr.escapeString(def.newName);
|
|
459
459
|
return { sql: `EXEC sp_rename '${tableCol}', '${newName}', 'COLUMN'` };
|
|
@@ -487,7 +487,7 @@ export class MssqlQueryBuilder extends QueryBuilderBase {
|
|
|
487
487
|
|
|
488
488
|
let sql = `ALTER TABLE ${table} ADD CONSTRAINT ${this.expr.wrap(fk.name)} FOREIGN KEY (${fkCols}) REFERENCES ${targetTable} (${targetCols})`;
|
|
489
489
|
|
|
490
|
-
// MSSQL/PostgreSQL:
|
|
490
|
+
// MSSQL/PostgreSQL: separate index generation needed for FK
|
|
491
491
|
const idxName = `IDX_${def.table.name}_${fk.name.replace(/^FK_/, "")}`;
|
|
492
492
|
sql += `;\nCREATE INDEX ${this.expr.wrap(idxName)} ON ${table} (${fkCols});`;
|
|
493
493
|
|
|
@@ -530,7 +530,7 @@ export class MssqlQueryBuilder extends QueryBuilderBase {
|
|
|
530
530
|
protected createProc(def: CreateProcQueryDef): QueryBuildResult {
|
|
531
531
|
const proc = this.tableName(def.procedure);
|
|
532
532
|
|
|
533
|
-
// params
|
|
533
|
+
// Process params
|
|
534
534
|
const paramList =
|
|
535
535
|
def.params
|
|
536
536
|
?.map((p) => {
|
|
@@ -574,7 +574,7 @@ export class MssqlQueryBuilder extends QueryBuilderBase {
|
|
|
574
574
|
//#region ========== Utils ==========
|
|
575
575
|
|
|
576
576
|
protected clearSchema(def: ClearSchemaQueryDef): QueryBuildResult {
|
|
577
|
-
// SQL
|
|
577
|
+
// SQL injection prevention: identifier validation
|
|
578
578
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(def.database)) {
|
|
579
579
|
throw new Error(`Invalid database name: ${def.database}`);
|
|
580
580
|
}
|
|
@@ -590,22 +590,22 @@ export class MssqlQueryBuilder extends QueryBuilderBase {
|
|
|
590
590
|
DECLARE @sql NVARCHAR(MAX);
|
|
591
591
|
SET @sql = N'';
|
|
592
592
|
|
|
593
|
-
-- FK
|
|
593
|
+
-- Drop FK constraints
|
|
594
594
|
SELECT @sql = @sql + N'ALTER TABLE ' + QUOTENAME(OBJECT_SCHEMA_NAME(parent_object_id)) + '.' + QUOTENAME(OBJECT_NAME(parent_object_id)) + N' DROP CONSTRAINT ' + QUOTENAME(name) + N';' + CHAR(13)
|
|
595
595
|
FROM ${db}.sys.foreign_keys
|
|
596
596
|
WHERE OBJECT_SCHEMA_NAME(parent_object_id) = '${schema}';
|
|
597
597
|
|
|
598
|
-
-- Drop
|
|
598
|
+
-- Drop tables
|
|
599
599
|
SELECT @sql = @sql + N'DROP TABLE ' + QUOTENAME(SCHEMA_NAME(schema_id)) + '.' + QUOTENAME(name) + N';' + CHAR(13)
|
|
600
600
|
FROM ${db}.sys.tables
|
|
601
601
|
WHERE SCHEMA_NAME(schema_id) = '${schema}';
|
|
602
602
|
|
|
603
|
-
-- Drop
|
|
603
|
+
-- Drop views
|
|
604
604
|
SELECT @sql = @sql + N'DROP VIEW ' + QUOTENAME(SCHEMA_NAME(schema_id)) + '.' + QUOTENAME(name) + N';' + CHAR(13)
|
|
605
605
|
FROM ${db}.sys.views
|
|
606
606
|
WHERE schema_id = SCHEMA_ID('${schema}');
|
|
607
607
|
|
|
608
|
-
--
|
|
608
|
+
-- Drop procedures
|
|
609
609
|
SELECT @sql = @sql + N'DROP PROCEDURE ' + QUOTENAME(SCHEMA_NAME(schema_id)) + '.' + QUOTENAME(name) + N';' + CHAR(13)
|
|
610
610
|
FROM ${db}.sys.procedures
|
|
611
611
|
WHERE SCHEMA_NAME(schema_id) = '${schema}';
|
|
@@ -615,7 +615,7 @@ EXEC sp_executesql @sql;`,
|
|
|
615
615
|
}
|
|
616
616
|
|
|
617
617
|
protected schemaExists(def: SchemaExistsQueryDef): QueryBuildResult {
|
|
618
|
-
// SQL
|
|
618
|
+
// SQL injection prevention: identifier validation
|
|
619
619
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(def.database)) {
|
|
620
620
|
throw new Error(`Invalid database name: ${def.database}`);
|
|
621
621
|
}
|
|
@@ -626,7 +626,7 @@ EXEC sp_executesql @sql;`,
|
|
|
626
626
|
|
|
627
627
|
const dbName = this.expr.escapeString(def.database);
|
|
628
628
|
const schema = this.expr.escapeString(schemaName);
|
|
629
|
-
// MSSQL: database
|
|
629
|
+
// MSSQL: check database existence then check schema (using dynamic SQL)
|
|
630
630
|
return {
|
|
631
631
|
sql: `DECLARE @result NVARCHAR(MAX) = NULL;
|
|
632
632
|
IF EXISTS (SELECT 1 FROM sys.databases WHERE name = '${dbName}')
|
|
@@ -72,22 +72,22 @@ import { ExprRendererBase } from "../base/expr-renderer-base";
|
|
|
72
72
|
* MySQL expression renderer
|
|
73
73
|
*/
|
|
74
74
|
export class MysqlExprRenderer 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
|
|
85
|
-
.replace(/\\/g, "\\\\") //
|
|
86
|
-
.replace(/'/g, "''") //
|
|
87
|
-
.replace(/\0/g, "\\0") // NULL
|
|
88
|
-
.replace(/\n/g, "\\n") //
|
|
89
|
-
.replace(/\r/g, "\\r") //
|
|
90
|
-
.replace(/\t/g, "\\t"); //
|
|
85
|
+
.replace(/\\/g, "\\\\") // backslash (highest priority)
|
|
86
|
+
.replace(/'/g, "''") // single quote
|
|
87
|
+
.replace(/\0/g, "\\0") // NULL byte
|
|
88
|
+
.replace(/\n/g, "\\n") // newline
|
|
89
|
+
.replace(/\r/g, "\\r") // carriage return
|
|
90
|
+
.replace(/\t/g, "\\t"); // tab
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/** value escape */
|
|
@@ -221,7 +221,7 @@ export class MysqlExprRenderer extends ExprRendererBase {
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
protected like(expr: ExprLike): string {
|
|
224
|
-
// ESCAPE '\'
|
|
224
|
+
// Always add ESCAPE '\'
|
|
225
225
|
return `${this.render(expr.source)} LIKE ${this.render(expr.pattern)} ESCAPE '\\\\'`;
|
|
226
226
|
}
|
|
227
227
|
|
|
@@ -231,7 +231,7 @@ export class MysqlExprRenderer extends ExprRendererBase {
|
|
|
231
231
|
|
|
232
232
|
protected in(expr: ExprIn): string {
|
|
233
233
|
if (expr.values.length === 0) {
|
|
234
|
-
return "1=0"; //
|
|
234
|
+
return "1=0"; // empty IN is always false
|
|
235
235
|
}
|
|
236
236
|
const values = expr.values.map((v) => this.render(v)).join(", ");
|
|
237
237
|
return `${this.render(expr.source)} IN (${values})`;
|
|
@@ -242,7 +242,7 @@ export class MysqlExprRenderer extends ExprRendererBase {
|
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
protected exists(expr: ExprExists): string {
|
|
245
|
-
// SELECT 1
|
|
245
|
+
// Render as SELECT 1
|
|
246
246
|
const subquery = this.buildSelect({
|
|
247
247
|
...expr.query,
|
|
248
248
|
select: { _: { type: "value", value: 1 } },
|
|
@@ -270,10 +270,10 @@ export class MysqlExprRenderer extends ExprRendererBase {
|
|
|
270
270
|
|
|
271
271
|
//#endregion
|
|
272
272
|
|
|
273
|
-
//#region ==========
|
|
273
|
+
//#region ========== String (null handling) ==========
|
|
274
274
|
|
|
275
275
|
protected concat(expr: ExprConcat): string {
|
|
276
|
-
// null
|
|
276
|
+
// null handling: IFNULL(arg, '')
|
|
277
277
|
const args = expr.args.map((a) => `IFNULL(${this.render(a)}, '')`);
|
|
278
278
|
return `CONCAT(${args.join(", ")})`;
|
|
279
279
|
}
|
|
@@ -307,12 +307,12 @@ export class MysqlExprRenderer extends ExprRendererBase {
|
|
|
307
307
|
}
|
|
308
308
|
|
|
309
309
|
protected length(expr: ExprLength): string {
|
|
310
|
-
// null
|
|
310
|
+
// null handling: IFNULL(arg, '')
|
|
311
311
|
return `CHAR_LENGTH(IFNULL(${this.render(expr.arg)}, ''))`;
|
|
312
312
|
}
|
|
313
313
|
|
|
314
314
|
protected byteLength(expr: ExprByteLength): string {
|
|
315
|
-
// null
|
|
315
|
+
// null handling: IFNULL(arg, '')
|
|
316
316
|
return `LENGTH(IFNULL(${this.render(expr.arg)}, ''))`;
|
|
317
317
|
}
|
|
318
318
|
|
|
@@ -380,7 +380,7 @@ export class MysqlExprRenderer extends ExprRendererBase {
|
|
|
380
380
|
}
|
|
381
381
|
|
|
382
382
|
protected isoWeekStartDate(expr: ExprIsoWeekStartDate): string {
|
|
383
|
-
// ISO
|
|
383
|
+
// ISO week start date (Monday)
|
|
384
384
|
return `DATE_SUB(${this.render(expr.arg)}, INTERVAL (WEEKDAY(${this.render(expr.arg)})) DAY)`;
|
|
385
385
|
}
|
|
386
386
|
|
|
@@ -438,7 +438,7 @@ export class MysqlExprRenderer extends ExprRendererBase {
|
|
|
438
438
|
}
|
|
439
439
|
|
|
440
440
|
private convertDateFormat(format: string): string {
|
|
441
|
-
//
|
|
441
|
+
// Simple transform (yyyy-MM-dd HH:mm:ss format)
|
|
442
442
|
return format
|
|
443
443
|
.replace(/yyyy/g, "%Y")
|
|
444
444
|
.replace(/MM/g, "%m")
|
|
@@ -455,7 +455,7 @@ export class MysqlExprRenderer extends ExprRendererBase {
|
|
|
455
455
|
protected coalesce(expr: ExprCoalesce): string {
|
|
456
456
|
if (expr.args.length === 0) return "NULL";
|
|
457
457
|
if (expr.args.length === 1) return this.render(expr.args[0]);
|
|
458
|
-
//
|
|
458
|
+
// Render as COALESCE (first non-null among multiple values)
|
|
459
459
|
return `COALESCE(${expr.args.map((a) => this.render(a)).join(", ")})`;
|
|
460
460
|
}
|
|
461
461
|
|
|
@@ -522,8 +522,8 @@ export class MysqlExprRenderer extends ExprRendererBase {
|
|
|
522
522
|
}
|
|
523
523
|
|
|
524
524
|
protected rowNum(_expr: ExprRowNum): string {
|
|
525
|
-
// MySQL
|
|
526
|
-
//
|
|
525
|
+
// MySQL: use variables or ROW_NUMBER() window function
|
|
526
|
+
// Implemented with ROW_NUMBER() here (MySQL 8.0+)
|
|
527
527
|
return "ROW_NUMBER() OVER ()";
|
|
528
528
|
}
|
|
529
529
|
|
|
@@ -543,7 +543,7 @@ export class MysqlExprRenderer extends ExprRendererBase {
|
|
|
543
543
|
const fn = this.renderWindowFn(expr.fn);
|
|
544
544
|
let over = this.renderWindowSpec(expr.spec);
|
|
545
545
|
|
|
546
|
-
// LAST_VALUE
|
|
546
|
+
// LAST_VALUE default frame only sees up to CURRENT ROW, so full frame must be specified
|
|
547
547
|
if (expr.fn.type === "lastValue" && over.length > 0) {
|
|
548
548
|
over += " ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING";
|
|
549
549
|
}
|