@mlagie/sql-connector 3.1.0 → 3.3.0
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/index.d.ts +13 -0
- package/index.js +1 -5
- package/package.json +3 -2
- package/src/db/dialects.js +639 -1
- package/src/migration/Migration.js +1 -2
- package/src/migration/SchemaDiffer.js +34 -29
- package/src/migration/SchemaInspector.js +1 -1
- package/src/migration/index.js +0 -3
- package/src/models/Model.js +6 -6
- package/src/utils/buildQuery/buildAggregationField.js +11 -0
- package/src/utils/buildQuery/buildQuery.js +8 -7
- package/src/migration/dialects/Dialect.js +0 -112
- package/src/migration/dialects/MySQLDialect.js +0 -205
- package/src/migration/dialects/PostgreSQLDialect.js +0 -233
- package/src/migration/dialects/index.js +0 -21
- package/src/utils/buildQuery/count.js +0 -11
package/src/db/dialects.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// src/db/dialects.js
|
|
2
2
|
const mysql = require('mysql2');
|
|
3
|
+
const { normalizeType } = require("../migration/utils");
|
|
4
|
+
const { getSafe } = require("../utils/security/safe");
|
|
3
5
|
|
|
4
6
|
// Ta regex de validation existante (ex: /^[a-zA-Z_][a-zA-Z0-9_]*$/)
|
|
5
7
|
const SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
@@ -55,15 +57,68 @@ function formatStringDefault(defaultValue, quote) {
|
|
|
55
57
|
return `DEFAULT ${quote}${escapedValue}${quote}`;
|
|
56
58
|
}
|
|
57
59
|
|
|
60
|
+
function formatPostgresDateFormat(format) {
|
|
61
|
+
return String(format)
|
|
62
|
+
.replace(/%Y/g, "YYYY")
|
|
63
|
+
.replace(/%m/g, "MM")
|
|
64
|
+
.replace(/%d/g, "DD")
|
|
65
|
+
.replace(/%H/g, "HH24")
|
|
66
|
+
.replace(/%i/g, "MI")
|
|
67
|
+
.replace(/%s/g, "SS");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const MYSQL_TYPE_MAP = {
|
|
71
|
+
String: "VARCHAR",
|
|
72
|
+
Char: "CHAR",
|
|
73
|
+
Number: "INT",
|
|
74
|
+
SmallInt: "SMALLINT",
|
|
75
|
+
BigInt: "BIGINT",
|
|
76
|
+
Decimal: "DECIMAL",
|
|
77
|
+
Boolean: "BOOLEAN",
|
|
78
|
+
Date: "DATETIME",
|
|
79
|
+
Object: "JSON",
|
|
80
|
+
Array: "VARCHAR",
|
|
81
|
+
Now: "NOW()",
|
|
82
|
+
Float: "FLOAT",
|
|
83
|
+
Double: "DOUBLE",
|
|
84
|
+
Text: "TEXT",
|
|
85
|
+
Blob: "BLOB",
|
|
86
|
+
Binary: "VARBINARY",
|
|
87
|
+
Uuid: "CHAR",
|
|
88
|
+
DateTime: "DATETIME",
|
|
89
|
+
Timestamp: "TIMESTAMP",
|
|
90
|
+
CurrentTimestamp: "CURRENT_TIMESTAMP"
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const POSTGRES_TYPE_MAP = {
|
|
94
|
+
...MYSQL_TYPE_MAP,
|
|
95
|
+
Date: "TIMESTAMP",
|
|
96
|
+
Object: "JSONB",
|
|
97
|
+
Now: "TIMESTAMP",
|
|
98
|
+
Float: "REAL",
|
|
99
|
+
Double: "DOUBLE PRECISION",
|
|
100
|
+
Blob: "BYTEA",
|
|
101
|
+
Binary: "BYTEA",
|
|
102
|
+
Uuid: "UUID",
|
|
103
|
+
DateTime: "TIMESTAMP",
|
|
104
|
+
Timestamp: "TIMESTAMP",
|
|
105
|
+
CurrentTimestamp: "TIMESTAMP"
|
|
106
|
+
};
|
|
107
|
+
|
|
58
108
|
const dialects = {
|
|
59
109
|
mysql: {
|
|
60
110
|
name: "mysql",
|
|
111
|
+
mapType: (fieldType) => getSafe(MYSQL_TYPE_MAP, fieldType),
|
|
61
112
|
// Sécurisé avec ta validation + mysql.escapeId natif
|
|
62
113
|
escape: (identifier) => secureEscape(identifier, (part) => mysql.escapeId(part)),
|
|
63
114
|
escapeValue: (value) => mysql.escape(value),
|
|
64
115
|
escapeIdentifierList: (identifiers) => escapeIdentifierList(identifiers),
|
|
65
116
|
getPlaceholder: () => "?",
|
|
66
117
|
tableSuffix: " ENGINE=InnoDB",
|
|
118
|
+
autoIncrement: () => "AUTO_INCREMENT",
|
|
119
|
+
columnType: (type, length) => `${type}${length ? `(${length})` : ""}`,
|
|
120
|
+
dateFormat: (column, format) => `DATE_FORMAT(${column}, ${dialects.mysql.escapeValue(format)})`,
|
|
121
|
+
caseInsensitiveLike: (column, placeholder, negate) => `${negate ? "NOT " : ""}LOWER(${column}) LIKE LOWER(${placeholder})`,
|
|
67
122
|
uuidQuery: "SELECT UUID();",
|
|
68
123
|
extractUuid: (rows) => rows?.[0]?.["UUID()"] ?? rows?.["UUID()"],
|
|
69
124
|
countKey: (row) => row["COUNT(*)"] ?? row["count"] ?? Object.values(row)[0],
|
|
@@ -92,6 +147,7 @@ const dialects = {
|
|
|
92
147
|
},
|
|
93
148
|
postgres: {
|
|
94
149
|
name: "postgres",
|
|
150
|
+
mapType: (fieldType) => getSafe(POSTGRES_TYPE_MAP, fieldType),
|
|
95
151
|
// Sécurisé avec ta validation + standard ANSI SQL (double-quotes doublées pour l'échappement)
|
|
96
152
|
escape: (identifier) => secureEscape(identifier, (part) => `"${part.replace(/"/g, '""')}"`),
|
|
97
153
|
escapeValue: (value) => {
|
|
@@ -103,6 +159,15 @@ const dialects = {
|
|
|
103
159
|
escapeIdentifierList: (identifiers) => escapeIdentifierList(identifiers),
|
|
104
160
|
getPlaceholder: (index) => `$${index + 1}`,
|
|
105
161
|
tableSuffix: "",
|
|
162
|
+
autoIncrement: (fieldType) => {
|
|
163
|
+
if (!["Number", "SmallInt", "BigInt"].includes(fieldType)) {
|
|
164
|
+
throw new Error(`PostgreSQL auto-increment is unsupported for type ${fieldType}.`);
|
|
165
|
+
}
|
|
166
|
+
return "GENERATED BY DEFAULT AS IDENTITY";
|
|
167
|
+
},
|
|
168
|
+
columnType: (type, length) => `${type}${type === "INT" ? "" : (length ? `(${length})` : "")}`,
|
|
169
|
+
dateFormat: (column, format) => `TO_CHAR(${column}, ${dialects.postgres.escapeValue(formatPostgresDateFormat(format))})`,
|
|
170
|
+
caseInsensitiveLike: (column, placeholder, negate) => `${column} ${negate ? "NOT " : ""}ILIKE ${placeholder}`,
|
|
106
171
|
uuidQuery: "SELECT gen_random_uuid() AS uuid;",
|
|
107
172
|
extractUuid: (rows) => rows?.[0]?.uuid || rows?.uuid,
|
|
108
173
|
countKey: (row) => row["count"] ?? Object.values(row)[0],
|
|
@@ -138,6 +203,559 @@ const dialects = {
|
|
|
138
203
|
}
|
|
139
204
|
};
|
|
140
205
|
|
|
206
|
+
// Migration dialects live in this module too, so SQL escaping, connection execution
|
|
207
|
+
// and schema migration share one source of truth.
|
|
208
|
+
|
|
209
|
+
const { quoteIdentifier } = require("../migration/utils");
|
|
210
|
+
|
|
211
|
+
class Dialect {
|
|
212
|
+
constructor(name, options = {}) {
|
|
213
|
+
this.name = name;
|
|
214
|
+
this.quote = options.quote;
|
|
215
|
+
this.capabilities = {
|
|
216
|
+
transactionalDDL: false,
|
|
217
|
+
renameColumn: true,
|
|
218
|
+
alterColumn: true,
|
|
219
|
+
...options.capabilities
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
escape(name) {
|
|
224
|
+
return quoteIdentifier(name, this.quote);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
placeholder() {
|
|
228
|
+
return "?";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
placeholders(count) {
|
|
232
|
+
return Array.from({ length: count }, (_, i) => this.placeholder(i)).join(", ");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
execute() {
|
|
236
|
+
throw new Error(`${this.name} dialect must implement execute().`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async begin() {}
|
|
240
|
+
async commit() {}
|
|
241
|
+
async rollback() {}
|
|
242
|
+
|
|
243
|
+
mapType() {
|
|
244
|
+
throw new Error(`${this.name} dialect must implement mapType().`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
columnDefinition() {
|
|
248
|
+
throw new Error(`${this.name} dialect must implement columnDefinition().`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
createTable() {
|
|
252
|
+
throw new Error(`${this.name} dialect must implement createTable().`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
addColumn(table, name, field) {
|
|
256
|
+
return `ALTER TABLE ${this.escape(table)} ADD COLUMN ${this.columnDefinition(name, field)};`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
renameColumn(table, oldName, newName) {
|
|
260
|
+
return `ALTER TABLE ${this.escape(table)} RENAME COLUMN ${this.escape(oldName)} TO ${this.escape(newName)};`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
alterColumn() {
|
|
264
|
+
throw new Error(`${this.name} does not implement alterColumn().`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
dropColumn(table, name) {
|
|
268
|
+
return `ALTER TABLE ${this.escape(table)} DROP COLUMN ${this.escape(name)};`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
dropTable(table) {
|
|
272
|
+
return `DROP TABLE ${this.escape(table)};`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
introspectionQueries() {
|
|
276
|
+
throw new Error(`${this.name} dialect must implement introspectionQueries().`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
normalizeDatabaseColumn(row) {
|
|
280
|
+
return row;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
normalizeDatabaseTable(row) {
|
|
284
|
+
return row;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
createMigrationTable() {
|
|
288
|
+
throw new Error(`${this.name} dialect must implement createMigrationTable().`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
insertMigration() {
|
|
292
|
+
throw new Error(`${this.name} dialect must implement insertMigration().`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
selectMigrations() {
|
|
296
|
+
throw new Error(`${this.name} dialect must implement selectMigrations().`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
deleteMigration() {
|
|
300
|
+
throw new Error(`${this.name} dialect must implement deleteMigration().`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async transaction(connection, callback) {
|
|
304
|
+
if (!this.capabilities.transactionalDDL) return callback();
|
|
305
|
+
|
|
306
|
+
await this.begin(connection);
|
|
307
|
+
try {
|
|
308
|
+
const result = await callback();
|
|
309
|
+
await this.commit(connection);
|
|
310
|
+
return result;
|
|
311
|
+
} catch (error) {
|
|
312
|
+
try {
|
|
313
|
+
await this.rollback(connection);
|
|
314
|
+
} catch {}
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
module.exports = { Dialect };
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class MySQLDialect extends Dialect {
|
|
327
|
+
constructor() {
|
|
328
|
+
super("mysql", {
|
|
329
|
+
quote: "`",
|
|
330
|
+
capabilities: {
|
|
331
|
+
transactionalDDL: false,
|
|
332
|
+
renameColumn: true,
|
|
333
|
+
alterColumn: true
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
placeholder() {
|
|
339
|
+
return "?";
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
execute(connection, sql, values = []) {
|
|
343
|
+
return connection.promise ? connection.promise().query(sql, values).then(([rows]) => rows)
|
|
344
|
+
: new Promise((resolve, reject) => {
|
|
345
|
+
connection.query(sql, values, (error, rows) => error ? reject(error) : resolve(rows));
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
mapType(field) {
|
|
350
|
+
const rawType = normalizeType(field);
|
|
351
|
+
const type = String(rawType || "").toUpperCase();
|
|
352
|
+
const length = field.length || 255;
|
|
353
|
+
|
|
354
|
+
switch (type) {
|
|
355
|
+
case "STRING":
|
|
356
|
+
case "VARCHAR":
|
|
357
|
+
return `VARCHAR(${length})`;
|
|
358
|
+
case "CHAR":
|
|
359
|
+
return `CHAR(${length})`;
|
|
360
|
+
case "TEXT":
|
|
361
|
+
return "TEXT";
|
|
362
|
+
case "NUMBER":
|
|
363
|
+
case "INT":
|
|
364
|
+
case "INTEGER":
|
|
365
|
+
return "INT";
|
|
366
|
+
case "SMALLINT":
|
|
367
|
+
return "SMALLINT";
|
|
368
|
+
case "MEDIUMINT":
|
|
369
|
+
return "MEDIUMINT";
|
|
370
|
+
case "BIGINT":
|
|
371
|
+
return "BIGINT";
|
|
372
|
+
case "DECIMAL":
|
|
373
|
+
case "NUMERIC":
|
|
374
|
+
return "DECIMAL";
|
|
375
|
+
case "FLOAT":
|
|
376
|
+
return "DOUBLE";
|
|
377
|
+
case "DOUBLE":
|
|
378
|
+
return "DOUBLE";
|
|
379
|
+
case "BOOLEAN":
|
|
380
|
+
case "BOOL":
|
|
381
|
+
case "TINYINT":
|
|
382
|
+
return "BOOLEAN";
|
|
383
|
+
case "DATE":
|
|
384
|
+
return "DATE";
|
|
385
|
+
case "DATETIME":
|
|
386
|
+
case "TIMESTAMP":
|
|
387
|
+
case "NOW":
|
|
388
|
+
case "CURRENTTIMESTAMP":
|
|
389
|
+
return "DATETIME";
|
|
390
|
+
case "OBJECT":
|
|
391
|
+
case "JSON":
|
|
392
|
+
return "JSON";
|
|
393
|
+
case "BLOB":
|
|
394
|
+
return "BLOB";
|
|
395
|
+
case "VARBINARY":
|
|
396
|
+
case "BINARY":
|
|
397
|
+
return "VARBINARY";
|
|
398
|
+
default:
|
|
399
|
+
throw new Error(`Unsupported MySQL type: ${rawType}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
defaultSql(value) {
|
|
404
|
+
if (value === undefined) return "";
|
|
405
|
+
if (value === null) return "DEFAULT NULL";
|
|
406
|
+
if (typeof value === "number") return `DEFAULT ${value}`;
|
|
407
|
+
if (typeof value === "boolean") return `DEFAULT ${value ? 1 : 0}`;
|
|
408
|
+
if (typeof value === "string") {
|
|
409
|
+
if (/^(CURRENT_TIMESTAMP|CURRENT_DATE|CURRENT_TIME)\b/i.test(value)) {
|
|
410
|
+
return `DEFAULT ${value}`;
|
|
411
|
+
}
|
|
412
|
+
return `DEFAULT '${value.replace(/'/g, "''")}'`;
|
|
413
|
+
}
|
|
414
|
+
return `DEFAULT '${String(value).replace(/'/g, "''")}'`;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
columnDefinition(name, field) {
|
|
418
|
+
if (field.primary_key && field.unique) {
|
|
419
|
+
throw new Error(`Field '${name}' cannot be both PRIMARY KEY and UNIQUE.`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
let sql = `${this.escape(name)} ${this.mapType(field)}`;
|
|
423
|
+
|
|
424
|
+
if (field.required || field.primary_key) sql += " NOT NULL";
|
|
425
|
+
if (field.default !== undefined) sql += ` ${this.defaultSql(field.default, field)}`;
|
|
426
|
+
if (field.unique) sql += " UNIQUE";
|
|
427
|
+
if (field.auto_increment) sql += " AUTO_INCREMENT";
|
|
428
|
+
if (field.primary_key) sql += " PRIMARY KEY";
|
|
429
|
+
if (typeof field.customize === "string" && field.customize.trim()) {
|
|
430
|
+
sql += ` ${field.customize.trim()}`;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return sql;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
createTable(table, schema) {
|
|
437
|
+
const columns = [];
|
|
438
|
+
const foreignKeys = [];
|
|
439
|
+
|
|
440
|
+
for (const [name, field] of Object.entries(schema)) {
|
|
441
|
+
columns.push(this.columnDefinition(name, field));
|
|
442
|
+
|
|
443
|
+
if (field.foreignKey) {
|
|
444
|
+
const match = /^([A-Za-z_][A-Za-z0-9_$]*)(?:\s*\(\s*([A-Za-z_][A-Za-z0-9_$]*)\s*\)|\s*\.\s*([A-Za-z_][A-Za-z0-9_$]*)\s*)$/.exec(field.foreignKey);
|
|
445
|
+
if (!match) throw new Error(`Invalid foreign key definition for ${name}.`);
|
|
446
|
+
const refColumn = match[2] || match[3];
|
|
447
|
+
foreignKeys.push(
|
|
448
|
+
`FOREIGN KEY (${this.escape(name)}) REFERENCES ${this.escape(match[1])} (${this.escape(refColumn)})`
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return `CREATE TABLE IF NOT EXISTS ${this.escape(table)} (${columns.concat(foreignKeys).join(", ")});`;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
alterColumn(table, name, field) {
|
|
457
|
+
return `ALTER TABLE ${this.escape(table)} MODIFY COLUMN ${this.columnDefinition(name, field)};`;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
introspectionQueries() {
|
|
461
|
+
return {
|
|
462
|
+
tables: `
|
|
463
|
+
SELECT TABLE_NAME AS table_name
|
|
464
|
+
FROM information_schema.tables
|
|
465
|
+
WHERE table_schema = DATABASE()
|
|
466
|
+
AND table_type = 'BASE TABLE'
|
|
467
|
+
`,
|
|
468
|
+
columns: `
|
|
469
|
+
SELECT
|
|
470
|
+
COLUMN_NAME AS column_name,
|
|
471
|
+
DATA_TYPE AS data_type,
|
|
472
|
+
COLUMN_TYPE AS column_type,
|
|
473
|
+
IS_NULLABLE AS is_nullable,
|
|
474
|
+
COLUMN_DEFAULT AS column_default,
|
|
475
|
+
COLUMN_KEY AS column_key,
|
|
476
|
+
EXTRA AS extra,
|
|
477
|
+
CHARACTER_MAXIMUM_LENGTH AS character_maximum_length
|
|
478
|
+
FROM information_schema.columns
|
|
479
|
+
WHERE table_schema = DATABASE()
|
|
480
|
+
AND table_name = ?
|
|
481
|
+
ORDER BY ORDINAL_POSITION
|
|
482
|
+
`
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
normalizeDatabaseColumn(row) {
|
|
487
|
+
const type = String(row.data_type || "").toLowerCase();
|
|
488
|
+
return {
|
|
489
|
+
type,
|
|
490
|
+
length: row.character_maximum_length ?? undefined,
|
|
491
|
+
required: row.is_nullable === "NO",
|
|
492
|
+
primary_key: row.column_key === "PRI",
|
|
493
|
+
unique: row.column_key === "UNI",
|
|
494
|
+
auto_increment: String(row.extra || "").toLowerCase().includes("auto_increment"),
|
|
495
|
+
default: row.column_default
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
createMigrationTable() {
|
|
500
|
+
return `
|
|
501
|
+
CREATE TABLE IF NOT EXISTS ${this.escape("sql_connector_migrations")} (
|
|
502
|
+
${this.escape("id")} VARCHAR(191) NOT NULL PRIMARY KEY,
|
|
503
|
+
${this.escape("checksum")} VARCHAR(64) NOT NULL,
|
|
504
|
+
${this.escape("applied_at")} DATETIME NOT NULL,
|
|
505
|
+
${this.escape("created_at")} DATETIME NOT NULL
|
|
506
|
+
);
|
|
507
|
+
`;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
insertMigration() {
|
|
511
|
+
return `
|
|
512
|
+
INSERT INTO ${this.escape("sql_connector_migrations")}
|
|
513
|
+
(${this.escape("id")}, ${this.escape("checksum")}, ${this.escape("applied_at")}, ${this.escape("created_at")})
|
|
514
|
+
VALUES (?, ?, ?, ?)
|
|
515
|
+
`;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
selectMigrations() {
|
|
519
|
+
return `SELECT ${this.escape("id")} AS id, ${this.escape("checksum")} AS checksum, ${this.escape("applied_at")} AS applied_at FROM ${this.escape("sql_connector_migrations")} ORDER BY ${this.escape("id")}`;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
deleteMigration() {
|
|
523
|
+
return `DELETE FROM ${this.escape("sql_connector_migrations")} WHERE ${this.escape("id")} = ?`;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
class PostgreSQLDialect extends Dialect {
|
|
531
|
+
constructor() {
|
|
532
|
+
super("postgres", {
|
|
533
|
+
quote: '"',
|
|
534
|
+
capabilities: {
|
|
535
|
+
transactionalDDL: true,
|
|
536
|
+
renameColumn: true,
|
|
537
|
+
alterColumn: true
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
placeholder(index) {
|
|
543
|
+
return `$${index + 1}`;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
execute(connection, sql, values = []) {
|
|
547
|
+
if (typeof connection.query === "function") {
|
|
548
|
+
return connection.query(sql, values).then(result => result.rows);
|
|
549
|
+
}
|
|
550
|
+
throw new Error("PostgreSQL connection does not expose query().");
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
mapType(field) {
|
|
554
|
+
const rawType = normalizeType(field);
|
|
555
|
+
const type = String(rawType || "").toUpperCase();
|
|
556
|
+
const length = field.length || 255;
|
|
557
|
+
|
|
558
|
+
switch (type) {
|
|
559
|
+
case "STRING":
|
|
560
|
+
case "VARCHAR":
|
|
561
|
+
case "CHARACTER VARYING":
|
|
562
|
+
return `VARCHAR(${length})`;
|
|
563
|
+
case "CHAR":
|
|
564
|
+
case "BPCHAR":
|
|
565
|
+
case "CHARACTER":
|
|
566
|
+
return `CHAR(${length})`;
|
|
567
|
+
case "TEXT":
|
|
568
|
+
return "TEXT";
|
|
569
|
+
case "NUMBER":
|
|
570
|
+
case "INT":
|
|
571
|
+
case "INT4":
|
|
572
|
+
case "INTEGER":
|
|
573
|
+
return "INTEGER";
|
|
574
|
+
case "SMALLINT":
|
|
575
|
+
case "INT2":
|
|
576
|
+
return "SMALLINT";
|
|
577
|
+
case "BIGINT":
|
|
578
|
+
case "INT8":
|
|
579
|
+
return "BIGINT";
|
|
580
|
+
case "DECIMAL":
|
|
581
|
+
case "NUMERIC":
|
|
582
|
+
return "DECIMAL";
|
|
583
|
+
case "FLOAT":
|
|
584
|
+
case "FLOAT4":
|
|
585
|
+
case "REAL":
|
|
586
|
+
return "REAL";
|
|
587
|
+
case "DOUBLE":
|
|
588
|
+
case "FLOAT8":
|
|
589
|
+
case "DOUBLE PRECISION":
|
|
590
|
+
return "DOUBLE PRECISION";
|
|
591
|
+
case "BOOLEAN":
|
|
592
|
+
case "BOOL":
|
|
593
|
+
return "BOOLEAN";
|
|
594
|
+
case "DATE":
|
|
595
|
+
return "DATE";
|
|
596
|
+
case "DATETIME":
|
|
597
|
+
case "TIMESTAMP":
|
|
598
|
+
case "NOW":
|
|
599
|
+
case "CURRENTTIMESTAMP":
|
|
600
|
+
return "TIMESTAMP";
|
|
601
|
+
case "TIMESTAMPTZ":
|
|
602
|
+
case "TIMESTAMP WITH TIME ZONE":
|
|
603
|
+
return "TIMESTAMPTZ";
|
|
604
|
+
case "OBJECT":
|
|
605
|
+
case "JSON":
|
|
606
|
+
case "JSONB":
|
|
607
|
+
return "JSONB";
|
|
608
|
+
case "UUID":
|
|
609
|
+
return "UUID";
|
|
610
|
+
default:
|
|
611
|
+
throw new Error(`Unsupported PostgreSQL type: ${rawType}`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
defaultSql(value) {
|
|
616
|
+
if (value === undefined) return "";
|
|
617
|
+
if (value === null) return "DEFAULT NULL";
|
|
618
|
+
if (typeof value === "number") return `DEFAULT ${value}`;
|
|
619
|
+
if (typeof value === "boolean") return `DEFAULT ${value ? "TRUE" : "FALSE"}`;
|
|
620
|
+
if (typeof value === "string") {
|
|
621
|
+
if (/^(CURRENT_TIMESTAMP|CURRENT_DATE|CURRENT_TIME)\b/i.test(value)) {
|
|
622
|
+
return `DEFAULT ${value}`;
|
|
623
|
+
}
|
|
624
|
+
return `DEFAULT '${value.replace(/'/g, "''")}'`;
|
|
625
|
+
}
|
|
626
|
+
return `DEFAULT '${String(value).replace(/'/g, "''")}'`;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
columnDefinition(name, field) {
|
|
630
|
+
if (field.primary_key && field.unique) {
|
|
631
|
+
throw new Error(`Field '${name}' cannot be both PRIMARY KEY and UNIQUE.`);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
let sql = `${this.escape(name)} ${this.mapType(field)}`;
|
|
635
|
+
|
|
636
|
+
if (field.required || field.primary_key) sql += " NOT NULL";
|
|
637
|
+
if (field.default !== undefined) sql += ` ${this.defaultSql(field.default)}`;
|
|
638
|
+
if (field.unique) sql += " UNIQUE";
|
|
639
|
+
if (field.primary_key) sql += " PRIMARY KEY";
|
|
640
|
+
if (field.auto_increment) {
|
|
641
|
+
// Explicit auto-increment is represented with identity syntax.
|
|
642
|
+
sql = sql.replace(/\s+(INTEGER|BIGINT|SMALLINT)(?=\s|$)/i, " $1 GENERATED BY DEFAULT AS IDENTITY");
|
|
643
|
+
}
|
|
644
|
+
if (typeof field.customize === "string" && field.customize.trim()) {
|
|
645
|
+
sql += ` ${field.customize.trim()}`;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
return sql;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
createTable(table, schema) {
|
|
652
|
+
const columns = [];
|
|
653
|
+
const foreignKeys = [];
|
|
654
|
+
|
|
655
|
+
for (const [name, field] of Object.entries(schema)) {
|
|
656
|
+
columns.push(this.columnDefinition(name, field));
|
|
657
|
+
|
|
658
|
+
if (field.foreignKey) {
|
|
659
|
+
const match = /^([A-Za-z_][A-Za-z0-9_$]*)(?:\s*\(\s*([A-Za-z_][A-Za-z0-9_$]*)\s*\)|\s*\.\s*([A-Za-z_][A-Za-z0-9_$]*)\s*)$/.exec(field.foreignKey);
|
|
660
|
+
if (!match) throw new Error(`Invalid foreign key definition for ${name}.`);
|
|
661
|
+
const refColumn = match[2] || match[3];
|
|
662
|
+
foreignKeys.push(
|
|
663
|
+
`FOREIGN KEY (${this.escape(name)}) REFERENCES ${this.escape(match[1])} (${this.escape(refColumn)})`
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return `CREATE TABLE IF NOT EXISTS ${this.escape(table)} (${columns.concat(foreignKeys).join(", ")});`;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
alterColumn(table, name, field) {
|
|
672
|
+
const clauses = [
|
|
673
|
+
`ALTER COLUMN ${this.escape(name)} TYPE ${this.mapType(field)}`
|
|
674
|
+
];
|
|
675
|
+
|
|
676
|
+
if (field.required || field.primary_key) {
|
|
677
|
+
clauses.push(`ALTER COLUMN ${this.escape(name)} SET NOT NULL`);
|
|
678
|
+
} else {
|
|
679
|
+
clauses.push(`ALTER COLUMN ${this.escape(name)} DROP NOT NULL`);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (field.default === undefined) {
|
|
683
|
+
clauses.push(`ALTER COLUMN ${this.escape(name)} DROP DEFAULT`);
|
|
684
|
+
} else {
|
|
685
|
+
clauses.push(`ALTER COLUMN ${this.escape(name)} SET ${this.defaultSql(field.default)}`);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return `ALTER TABLE ${this.escape(table)} ${clauses.join(", ")};`;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
introspectionQueries() {
|
|
692
|
+
return {
|
|
693
|
+
tables: `
|
|
694
|
+
SELECT table_name
|
|
695
|
+
FROM information_schema.tables
|
|
696
|
+
WHERE table_schema = current_schema()
|
|
697
|
+
AND table_type = 'BASE TABLE'
|
|
698
|
+
`,
|
|
699
|
+
columns: `
|
|
700
|
+
SELECT
|
|
701
|
+
column_name,
|
|
702
|
+
data_type,
|
|
703
|
+
udt_name,
|
|
704
|
+
is_nullable,
|
|
705
|
+
column_default,
|
|
706
|
+
character_maximum_length
|
|
707
|
+
FROM information_schema.columns
|
|
708
|
+
WHERE table_schema = current_schema()
|
|
709
|
+
AND table_name = $1
|
|
710
|
+
ORDER BY ordinal_position
|
|
711
|
+
`
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
normalizeDatabaseColumn(row) {
|
|
716
|
+
const type = String(row.udt_name || row.data_type || "").toLowerCase();
|
|
717
|
+
const isSerial = typeof row.column_default === "string" &&
|
|
718
|
+
row.column_default.includes("nextval(");
|
|
719
|
+
|
|
720
|
+
return {
|
|
721
|
+
type,
|
|
722
|
+
length: row.character_maximum_length ?? undefined,
|
|
723
|
+
required: row.is_nullable === "NO",
|
|
724
|
+
primary_key: false,
|
|
725
|
+
unique: false,
|
|
726
|
+
auto_increment: isSerial,
|
|
727
|
+
default: row.column_default
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
createMigrationTable() {
|
|
732
|
+
return `
|
|
733
|
+
CREATE TABLE IF NOT EXISTS ${this.escape("sql_connector_migrations")} (
|
|
734
|
+
${this.escape("id")} VARCHAR(255) NOT NULL PRIMARY KEY,
|
|
735
|
+
${this.escape("checksum")} VARCHAR(64) NOT NULL,
|
|
736
|
+
${this.escape("applied_at")} TIMESTAMPTZ NOT NULL,
|
|
737
|
+
${this.escape("created_at")} TIMESTAMPTZ NOT NULL
|
|
738
|
+
);
|
|
739
|
+
`;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
insertMigration() {
|
|
743
|
+
return `
|
|
744
|
+
INSERT INTO ${this.escape("sql_connector_migrations")}
|
|
745
|
+
(${this.escape("id")}, ${this.escape("checksum")}, ${this.escape("applied_at")}, ${this.escape("created_at")})
|
|
746
|
+
VALUES ($1, $2, $3, $4)
|
|
747
|
+
`;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
selectMigrations() {
|
|
751
|
+
return `SELECT ${this.escape("id")} AS id, ${this.escape("checksum")} AS checksum, ${this.escape("applied_at")} AS applied_at FROM ${this.escape("sql_connector_migrations")} ORDER BY ${this.escape("id")}`;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
deleteMigration() {
|
|
755
|
+
return `DELETE FROM ${this.escape("sql_connector_migrations")} WHERE ${this.escape("id")} = $1`;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
141
759
|
let currentDialectName = "mysql";
|
|
142
760
|
|
|
143
761
|
function setGlobalDialect(dialectName) {
|
|
@@ -156,4 +774,24 @@ function getDialect() {
|
|
|
156
774
|
}
|
|
157
775
|
}
|
|
158
776
|
|
|
159
|
-
|
|
777
|
+
function createDialect(name) {
|
|
778
|
+
switch (String(name || currentDialectName).toLowerCase()) {
|
|
779
|
+
case "mysql":
|
|
780
|
+
case "mariadb":
|
|
781
|
+
return new MySQLDialect();
|
|
782
|
+
case "postgres":
|
|
783
|
+
case "postgresql":
|
|
784
|
+
return new PostgreSQLDialect();
|
|
785
|
+
default:
|
|
786
|
+
throw new Error(`Unsupported migration dialect: ${name}`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
module.exports = {
|
|
791
|
+
getDialect,
|
|
792
|
+
setGlobalDialect,
|
|
793
|
+
createDialect,
|
|
794
|
+
Dialect,
|
|
795
|
+
MySQLDialect,
|
|
796
|
+
PostgreSQLDialect
|
|
797
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const { getConnexion } = require("../db/connexion");
|
|
2
|
-
const { getDialect: getConnectorDialect } = require("../db/dialects");
|
|
2
|
+
const { getDialect: getConnectorDialect, createDialect } = require("../db/dialects");
|
|
3
3
|
const { modelsToSnapshot } = require("./SchemaSnapshot");
|
|
4
4
|
const { SchemaInspector } = require("./SchemaInspector");
|
|
5
5
|
const { SchemaDiffer } = require("./SchemaDiffer");
|
|
@@ -7,7 +7,6 @@ const { MigrationPlan } = require("./MigrationPlan");
|
|
|
7
7
|
const { MigrationStore } = require("./MigrationStore");
|
|
8
8
|
const { BackupManager } = require("./BackupManager");
|
|
9
9
|
const { MigrationRunner } = require("./MigrationRunner");
|
|
10
|
-
const { createDialect } = require("./dialects");
|
|
11
10
|
const { migrationId, stableStringify, sha256 } = require("./utils");
|
|
12
11
|
|
|
13
12
|
function resolveDialect(name) {
|