sqlmorpheus 0.1.7 → 0.1.9

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/index.js CHANGED
@@ -72,31 +72,36 @@ function getTableInfoFromMetadata(metadata) {
72
72
  // src/lib/TableStructureGenerator.ts
73
73
  var TableStructureGenerator = class {
74
74
  dialect;
75
- tables = {};
76
- constructor(dbContext, dialect) {
75
+ dbInstructions;
76
+ constructor(dbInstructions, dialect) {
77
77
  this.dialect = dialect;
78
- if (dbContext.tables.length) {
78
+ this.dbInstructions = dbInstructions;
79
+ }
80
+ generateTableStructure() {
81
+ if (this.dbInstructions.tables.length) {
79
82
  const tableObjects = {};
80
- for (const table of dbContext.tables) {
83
+ for (const table of this.dbInstructions.tables) {
81
84
  tableObjects[table.name] = { columns: new table(), tableInfo: getTableInfo(table) };
82
85
  }
83
- this.getExpectedStructure(tableObjects);
86
+ return this.getExpectedStructure(tableObjects);
84
87
  } else {
85
- const tableObjects = dbContext.tables;
86
- this.getExpectedStructure(tableObjects);
88
+ const tableObjects = this.dbInstructions.tables;
89
+ return this.getExpectedStructure(tableObjects);
87
90
  }
88
91
  }
89
92
  getExpectedStructure(tableObjects) {
93
+ const tables = {};
90
94
  for (const tableName in tableObjects) {
91
95
  const obj = tableObjects[tableName];
92
96
  const tableInfo = obj.tableInfo;
93
- this.tables[tableName] = {
97
+ tables[tableName] = {
94
98
  table: tableName,
95
99
  primaryKey: tableInfo?.primaryKey,
96
100
  columns: this.getColumns(obj.columns, tableInfo?.primaryKey),
97
101
  foreignKeys: tableInfo?.foreignKeys
98
102
  };
99
103
  }
104
+ return tables;
100
105
  }
101
106
  getColumns(obj, primaryKey) {
102
107
  const columns = [];
@@ -114,6 +119,10 @@ var TableStructureGenerator = class {
114
119
  columnData.type = this.dialect.typeString;
115
120
  columnData.defaultValue = value === null ? this.dialect.typeNull : `"${this.dialect.formatValueToSql(value)}"`;
116
121
  break;
122
+ case "bigint":
123
+ columnData.type = this.dialect.typeBigInt;
124
+ columnData.defaultValue = value === null ? this.dialect.typeNull : this.dialect.formatValueToSql(value);
125
+ break;
117
126
  case "number":
118
127
  columnData.type = this.dialect.typeInt;
119
128
  columnData.defaultValue = value === null ? this.dialect.typeNull : this.dialect.formatValueToSql(value);
@@ -136,38 +145,30 @@ var TableStructureGenerator = class {
136
145
 
137
146
  // src/lib/exceptions/NotAllowedException.ts
138
147
  var NotAllowedException = class extends Error {
139
- constructor(version, tableName, type) {
140
- const information = `You have to enable "${type}" for version "${version}" and table "${tableName}"`;
141
- switch (type) {
142
- case "continueWithoutRollback":
143
- super(`Some changes can not be rolled back automatically. ${information}`);
144
- break;
145
- case "dropColumn":
146
- super(`Not allowed to drop column. ${information}`);
147
- break;
148
- case "dropTable":
149
- super(`Not allowed to drop table. ${information}`);
150
- break;
151
- case "recreateTable":
152
- super(`Not allowed to recreate table. ${information}`);
153
- break;
154
- case "alterForeignKey":
155
- super(`Not allowed to alter existing foreign key. ${information}`);
156
- break;
157
- case "alterPrimaryKey":
158
- super(`Not allowed to alter primary key. ${information}`);
159
- break;
160
- case "removeForeignKey":
161
- super(`Not allowed to remove existing foreign key. ${information}`);
162
- break;
148
+ constructor(entries) {
149
+ let message = "Some destructive changes need to be confirmed. In preMigrations() run:";
150
+ for (const entry of entries) {
151
+ message += `
152
+ migrations.allowMigration(${entry.version}, "${entry.tableName}", "${entry.type}");`;
163
153
  }
154
+ super(message);
164
155
  }
165
156
  };
166
157
 
167
158
  // src/lib/typings/Migrations.ts
168
159
  var Migrations = class {
169
- migrationData = {};
160
+ toVersion = 0;
170
161
  alwaysAllowed = {};
162
+ fromVersion = 0;
163
+ migrationData = {};
164
+ notAllowedChanges = [];
165
+ reset(dbInstructions, fromVersion) {
166
+ this.fromVersion = fromVersion;
167
+ this.migrationData = {};
168
+ this.toVersion = dbInstructions.version;
169
+ this.alwaysAllowed = dbInstructions.alwaysAllowedMigrations ?? {};
170
+ this.notAllowedChanges = [];
171
+ }
171
172
  getEntry(table) {
172
173
  const tableName = this.getTableName(table);
173
174
  if (!this.migrationData.hasOwnProperty(tableName)) {
@@ -182,30 +183,38 @@ var Migrations = class {
182
183
  getTableName(table) {
183
184
  return typeof table == "string" ? table : table.name;
184
185
  }
185
- alwaysAllow(...allowedMigrations) {
186
- for (const key of allowedMigrations) {
187
- this.alwaysAllowed[key] = true;
186
+ verifyMigration(tableName, type) {
187
+ if (!this.migrationData[tableName]?.allowedMigrations[type] && !this.alwaysAllowed[type])
188
+ this.notAllowedChanges.push({ version: this.toVersion, tableName, type });
189
+ }
190
+ checkIfAllowed(throwIfNotAllowed) {
191
+ const exception = new NotAllowedException(this.notAllowedChanges);
192
+ if (throwIfNotAllowed)
193
+ throw exception;
194
+ else {
195
+ console.error(exception.message);
188
196
  }
197
+ return !this.notAllowedChanges.length;
189
198
  }
190
199
  allowMigration(version, table, ...allowedMigrations) {
200
+ if (version < this.fromVersion || version > this.toVersion)
201
+ return;
191
202
  const entry = this.getEntry(table);
192
- if (!entry.allowedMigrations.hasOwnProperty(version))
193
- entry.allowedMigrations[version] = {};
194
203
  for (const key of allowedMigrations) {
195
- this.alwaysAllowed[key] = true;
204
+ entry.allowedMigrations[key] = true;
196
205
  }
197
206
  }
198
- throwIfNotAllowed(version, tableName, type) {
199
- if (!(this.migrationData[tableName]?.allowedMigrations[version] ?? this.alwaysAllowed)[type])
200
- throw new NotAllowedException(version, tableName, type);
201
- }
202
- renameTable(oldTableName, newTable) {
207
+ renameTable(version, oldTableName, newTable) {
208
+ if (version < this.fromVersion || version > this.toVersion)
209
+ return;
203
210
  const entry = this.getEntry(newTable);
204
211
  entry.recreate = true;
205
212
  if (!entry.oldTableName)
206
213
  entry.oldTableName = oldTableName;
207
214
  }
208
- renameColumn(table, oldColumn, newColumn) {
215
+ renameColumn(version, table, oldColumn, newColumn) {
216
+ if (version < this.fromVersion || version > this.toVersion)
217
+ return;
209
218
  const entry = this.getEntry(table);
210
219
  const existingColumnEntry = entry.renamedColumns.find((entry2) => entry2[entry2.length - 1] == oldColumn);
211
220
  if (existingColumnEntry)
@@ -213,10 +222,15 @@ var Migrations = class {
213
222
  else
214
223
  entry.renamedColumns.push([oldColumn, newColumn]);
215
224
  }
216
- recreateTable(table) {
225
+ recreateTableImp(table) {
217
226
  const entry = this.getEntry(table);
218
227
  entry.recreate = true;
219
228
  }
229
+ recreateTable(version, table) {
230
+ if (version < this.fromVersion || version > this.toVersion)
231
+ return;
232
+ this.recreateTableImp(table);
233
+ }
220
234
  getMigrationData() {
221
235
  return this.migrationData;
222
236
  }
@@ -231,7 +245,7 @@ var Migrations = class {
231
245
  }
232
246
  }
233
247
  getUpdatedColumnName(tableName, oldColumnName) {
234
- const columnNames = this.migrationData[tableName].renamedColumns.find((columns) => columns[0] == oldColumnName);
248
+ const columnNames = this.migrationData[tableName]?.renamedColumns.find((columns) => columns[0] == oldColumnName);
235
249
  return columnNames ? columnNames[columnNames.length - 1] : oldColumnName;
236
250
  }
237
251
  willBeRecreated(tableName) {
@@ -239,825 +253,836 @@ var Migrations = class {
239
253
  }
240
254
  };
241
255
 
242
- // src/lib/MigrationHistoryManager.ts
243
- var import_node_fs = require("fs");
244
- var import_fs = require("fs");
245
- var FILENAME_UP_PREFIX = "up_to_";
246
- var FILENAME_DOWN_PREFIX = "down_to_";
247
- var MigrationHistoryManager = class {
248
- configPath;
249
- migrationsPath;
250
- constructor(configPath) {
251
- this.configPath = configPath;
252
- this.migrationsPath = `${configPath}/migrations`;
253
- (0, import_node_fs.mkdirSync)(this.configPath, { recursive: true });
254
- (0, import_node_fs.mkdirSync)(this.migrationsPath, { recursive: true });
256
+ // src/lib/MigrationManager.ts
257
+ var MigrationManager = class {
258
+ migrations = new Migrations();
259
+ newTables = {};
260
+ existingTables = [];
261
+ dialect;
262
+ constructor(dialect) {
263
+ this.dialect = dialect;
255
264
  }
256
265
  /**
257
- * Creates migration history files for upgrading and downgrading database schemas.
266
+ * Generates the SQL changes required to migrate a database from its current version
267
+ * to the target version defined in the database instructions. Handles the creation
268
+ * and deletion of tables, migration of foreign keys and columns, renaming of columns,
269
+ * and recreating tables, along with executing any pre- and post-migration custom logic.
258
270
  *
259
- * @param fromVersion - The current version number of the database.
260
- * @param toVersion - The target version number for the migration.
261
- * @param changes - An object containing the SQL changes for upgrading (`changes.up`) and downgrading (`changes.down`).
262
- * @param overwriteExisting - A flag that determines if existing migration files should be overwritten. Defaults to `false`.
271
+ * @return A promise that resolves to an object containing the SQL
272
+ * changes for the migration (`up` and `down` changes) or null if no migration is required.
273
+ * Throws an error in scenarios such as attempting to migrate to a lower version
274
+ * or an invalid starting version.
263
275
  */
264
- createMigrationHistory(fromVersion, toVersion, changes, overwriteExisting) {
265
- const upPath = `${this.migrationsPath}/${FILENAME_UP_PREFIX}${toVersion}.sql`;
266
- const downPath = `${this.migrationsPath}/${FILENAME_DOWN_PREFIX}${fromVersion}.sql`;
267
- if ((0, import_node_fs.existsSync)(upPath) && !overwriteExisting)
268
- throw new Error(`Migration ${upPath} already exists!`);
269
- else if ((0, import_node_fs.existsSync)(downPath) && !overwriteExisting)
270
- throw new Error(`Migration ${downPath} already exists!`);
271
- Logger.log(`Save migrations to ${this.migrationsPath}`);
272
- (0, import_node_fs.writeFileSync)(`${this.migrationsPath}/${FILENAME_UP_PREFIX}${toVersion}.sql`, `-- From: Version ${fromVersion}
273
- -- To: Version ${toVersion}
274
- ${changes.up}`, { encoding: "utf-8" });
275
- (0, import_node_fs.writeFileSync)(`${this.migrationsPath}/${FILENAME_DOWN_PREFIX}${fromVersion}.sql`, `-- From: Version ${toVersion}
276
- -- To: Version ${fromVersion}
277
- ${changes.down}`, { encoding: "utf-8" });
276
+ async generateSqlChanges(dbInstructions) {
277
+ const fromVersion = await this.dialect.getVersion();
278
+ const toVersion = dbInstructions.version;
279
+ if (toVersion <= 0)
280
+ throw new Error("Cannot migrate to version 0 or lower");
281
+ if (fromVersion == toVersion) {
282
+ Logger.log("Version has not changed. No migrations needed.");
283
+ return null;
284
+ }
285
+ if (fromVersion > toVersion)
286
+ throw new Error(`You cannot create new migrations with a lower version (from ${fromVersion} to ${toVersion})`);
287
+ const foreignKeyOffQuery = this.dialect.changeForeignKeysState(false);
288
+ const changes = {
289
+ up: foreignKeyOffQuery,
290
+ down: foreignKeyOffQuery
291
+ };
292
+ this.migrations.reset(dbInstructions, fromVersion);
293
+ const preSQL = dbInstructions.preMigration?.(
294
+ this.migrations,
295
+ fromVersion,
296
+ toVersion
297
+ );
298
+ if (preSQL) {
299
+ changes.up += `
300
+
301
+ -- Custom SQL from preMigration()
302
+ ${preSQL.up}`;
303
+ changes.down += `
304
+
305
+ -- Custom SQL from preMigration()
306
+ ${preSQL.down}`;
307
+ }
308
+ const tableStructureGenerator = new TableStructureGenerator(dbInstructions, this.dialect);
309
+ this.newTables = tableStructureGenerator.generateTableStructure();
310
+ if (fromVersion == 0) {
311
+ Logger.log(`Creating initial migration to version ${dbInstructions.version}`);
312
+ const initialChanges = await this.createAndDropTables();
313
+ changes.up += initialChanges.up;
314
+ changes.down += initialChanges.down;
315
+ } else {
316
+ Logger.log(`Creating migration SQL from version ${fromVersion} to ${toVersion}`);
317
+ this.existingTables = await this.dialect.getTableNames();
318
+ [
319
+ await this.createAndDropTables(),
320
+ await this.migrateForeignKeys(),
321
+ await this.migrateColumns(),
322
+ this.renameColumns(),
323
+ await this.recreateTables()
324
+ ].forEach((entry) => {
325
+ changes.up += entry.up;
326
+ changes.down += entry.down;
327
+ });
328
+ }
329
+ const postSql = dbInstructions.postMigration?.(fromVersion, toVersion);
330
+ if (postSql) {
331
+ changes.up += `
332
+
333
+ -- Custom SQL from postMigration()
334
+ ${postSql.up}`;
335
+ changes.down += `
336
+
337
+ -- Custom SQL from postMigration()
338
+ ${postSql.down}`;
339
+ }
340
+ const foreignKeyOnQuery = this.dialect.changeForeignKeysState(true);
341
+ changes.up += `
342
+
343
+ ${foreignKeyOnQuery}`;
344
+ changes.down += `
345
+
346
+ ${foreignKeyOnQuery}`;
347
+ if (!this.migrations.checkIfAllowed(dbInstructions.throwIfNotAllowed)) {
348
+ return null;
349
+ }
350
+ return {
351
+ fromVersion,
352
+ toVersion,
353
+ changes
354
+ };
278
355
  }
279
356
  /**
280
- * Retrieves the SQL migration script for upgrading to the specified version.
281
- * If no version is provided, it retrieves the migration script for the latest version in the history.
357
+ * Generates SQL change statements for creating and dropping tables based on the current table structure
358
+ * and existing table configurations in the database. The `up` changes include SQL statements for creating
359
+ * new tables, while the `down` changes include SQL statements for dropping the corresponding tables or
360
+ * recreating them if necessary.
282
361
  *
283
- * @param toVersion - The version of the migration script to retrieve. If not specified, the latest version is used.
284
- * @return The content of the SQL migration script as a string.
362
+ * @return {Promise<SqlChanges>} A promise that resolves to an object containing `up` and `down` SQL
363
+ * change statements for migrating the database schema.
285
364
  */
286
- getUpMigration(toVersion) {
287
- return (0, import_fs.readFileSync)(`${this.migrationsPath}/${FILENAME_UP_PREFIX}${toVersion}.sql`, { encoding: "utf-8" });
365
+ async createAndDropTables() {
366
+ let changes = {
367
+ up: "\n\n-- Create tables\n",
368
+ down: "\n\n-- Create tables\n"
369
+ };
370
+ for (const tableName in this.newTables) {
371
+ if (this.tableDoesNotExists(tableName)) {
372
+ const structure = this.newTables[tableName];
373
+ changes.up += `${this.createTableSql(tableName, structure.columns, structure.foreignKeys)}
374
+ `;
375
+ changes.down += `${this.dialect.dropTable(tableName)}
376
+ `;
377
+ }
378
+ }
379
+ for (const tableName of this.existingTables) {
380
+ if (!this.newTables[tableName]) {
381
+ this.migrations.verifyMigration(tableName, "dropTable");
382
+ if (!this.dialect.canInspectForeignKeys || !this.dialect.canInspectPrimaryKey)
383
+ this.migrations.verifyMigration(tableName, "continueWithoutRollback");
384
+ changes.up += `${this.dialect.dropTable(tableName)}
385
+ `;
386
+ changes.down += this.dialect.canInspectForeignKeys && this.dialect.canInspectPrimaryKey ? this.createTableSql(tableName, await this.dialect.getColumnInformation(tableName), await this.dialect.getForeignKeys(tableName)) : "\n\n-- Cannot recreate foreign keys or load primary keys with this database! Table will not be recreated!\n";
387
+ }
388
+ }
389
+ return changes;
288
390
  }
289
391
  /**
290
- * Retrieves the SQL migration script for downgrading to the specified version.
392
+ * Generates an SQL string to create a table with the specified schema.
291
393
  *
292
- * @param version - The version number of the migration to retrieve.
293
- * @return The SQL script for the down migration.
394
+ * @param tableName - The name of the table to be created.
395
+ * @param columns - An array of objects defining the columns of the table, including their names, types
396
+ * default values, and whether they are primary keys.
397
+ * @param foreignKeys - An optional array of objects defining the foreign key constraints for the table,
398
+ * including the source column, target table, target column, action on update, and action on delete.
399
+ * @return The complete SQL string to create the table with the specified structure and constraints.
294
400
  */
295
- getDownMigration(version) {
296
- return (0, import_fs.readFileSync)(`${this.migrationsPath}/${FILENAME_DOWN_PREFIX}${version}.sql`, { encoding: "utf-8" });
297
- }
298
- };
299
-
300
- // src/lib/dialects/DefaultSql.ts
301
- var MIGRATION_DATA_TABLE_NAME = "__sqlmorpheus_migrations";
302
- var DefaultSql = class {
303
- canAlterForeignKeys = false;
304
- canAlterPrimaryKey = false;
305
- canInspectForeignKeys = false;
306
- canInspectPrimaryKey = false;
307
- typeString = "TEXT";
308
- typeInt = "INTEGER";
309
- typeBoolean = "BOOLEAN";
310
- typeNull = "NULL";
311
- formatValueToSql(value) {
312
- return value.toString();
313
- }
314
- changeForeignKeysState(enabled) {
315
- return "";
316
- }
317
- async getForeignKeys(tableName, db) {
318
- throw new Error("Inspecting foreign keys is not supported!");
319
- }
320
- addForeignKey(fromTableName, foreignKey) {
321
- throw new Error("Adding foreign keys is not supported!");
322
- }
323
- removeForeignKey(tableName, foreignKeyName) {
324
- throw new Error("Foreign key removal is not supported!");
325
- }
326
- addPrimaryKey(tableName, columnName) {
327
- throw new Error("Adding foreign keys is not supported!");
328
- }
329
- removePrimaryKey(tableName, columnName) {
330
- throw new Error("Foreign key removal is not supported!");
331
- }
332
- foreignKey(column, foreignTable, foreignColumn, onUpdate, onDelete) {
333
- let query = `FOREIGN KEY (${column}) REFERENCES ${foreignTable} (${foreignColumn})`;
334
- if (onUpdate)
335
- query += ` ON UPDATE ${onUpdate}`;
336
- if (onDelete)
337
- query += ` ON DELETE ${onDelete}`;
338
- return query;
339
- }
340
- createTable(tableName, entries) {
341
- return `CREATE TABLE IF NOT EXISTS ${tableName} (
342
- ${entries.join(",\n ")}
343
- );`;
344
- }
345
- renameTable(tableName, newTableName) {
346
- return `ALTER TABLE ${tableName} RENAME TO ${newTableName};`;
347
- }
348
- dropTable(tableName) {
349
- return `DROP TABLE IF EXISTS ${tableName};`;
350
- }
351
- columnDefinition(columnName, type, defaultValue, isPrimaryKey) {
352
- const query = `${columnName} ${type} DEFAULT ${defaultValue}`;
353
- return isPrimaryKey ? `${query} PRIMARY KEY` : query;
401
+ createTableSql(tableName, columns, foreignKeys) {
402
+ const queryLines = [];
403
+ for (const columnInfo of columns) {
404
+ queryLines.push(this.dialect.columnDefinition(columnInfo.name, columnInfo.type, columnInfo.defaultValue, columnInfo.isPrimaryKey));
405
+ }
406
+ if (foreignKeys) {
407
+ for (const foreignKeyInfo of foreignKeys) {
408
+ queryLines.push(this.dialect.foreignKey(foreignKeyInfo.fromColumn, foreignKeyInfo.toTable, foreignKeyInfo.toColumn.toString(), foreignKeyInfo.onUpdate, foreignKeyInfo.onDelete));
409
+ }
410
+ }
411
+ return this.dialect.createTable(tableName, queryLines);
354
412
  }
355
- createColumn(columnTable, entry) {
356
- return `ALTER TABLE ${columnTable} ADD ${entry};`;
413
+ /**
414
+ * Checks if the specified table does not exist in the list of existing tables.
415
+ *
416
+ * @param tableName - The name of the table to check.
417
+ * @return Returns true if the table does not exist, otherwise false.
418
+ */
419
+ tableDoesNotExists(tableName) {
420
+ return this.existingTables.indexOf(tableName) == -1;
357
421
  }
358
- renameColumn(tableName, oldColumnName, newColumnName) {
359
- return `ALTER TABLE ${tableName} RENAME COLUMN ${newColumnName} TO ${oldColumnName};`;
422
+ /**
423
+ * Recreates database tables that have been flagged to be recreated in migration data.
424
+ * This method generated the necessary SQL code for backing up the current table data, dropping and recreating
425
+ * the tables, and reinserting the preserved data into the recreated tables.
426
+ */
427
+ async recreateTables() {
428
+ const changes = {
429
+ up: "\n\n-- Recreate tables\n",
430
+ down: "\n\n-- Recreate tables\n"
431
+ };
432
+ const migrationData = this.migrations.getMigrationData();
433
+ for (const tableName in migrationData) {
434
+ const migrationEntry = migrationData[tableName];
435
+ if (!migrationEntry.recreate)
436
+ continue;
437
+ this.migrations.verifyMigration(tableName, "recreateTable");
438
+ if (!this.dialect.canInspectForeignKeys || !this.dialect.canInspectPrimaryKey)
439
+ this.migrations.verifyMigration(tableName, "continueWithoutRollback");
440
+ const oldColumnList = await this.dialect.getColumnInformation(tableName);
441
+ const newColumnList = this.newTables[tableName].columns;
442
+ const moveableColumns = newColumnList.filter((entry) => oldColumnList.find((oldEntry) => oldEntry.name == entry.name) != void 0).map((entry) => entry.name);
443
+ const backupTableName = `${tableName}__backup`;
444
+ const insertQuery = this.dialect.insert(
445
+ backupTableName,
446
+ this.dialect.insertValues(moveableColumns, this.dialect.select(tableName, moveableColumns))
447
+ );
448
+ const moveDataQuery = insertQuery + "\n" + this.dialect.dropTable(tableName) + "\n" + this.dialect.renameTable(backupTableName, tableName);
449
+ const structure = this.newTables[tableName];
450
+ changes.up += this.createTableSql(backupTableName, structure.columns, structure.foreignKeys) + "\n" + moveDataQuery;
451
+ changes.down += this.dialect.canInspectForeignKeys && this.dialect.canInspectPrimaryKey ? this.createTableSql(backupTableName, await this.dialect.getColumnInformation(tableName), await this.dialect.getForeignKeys(tableName)) + "\n" + moveDataQuery : "\n\n-- Cannot recreate foreign keys or load primary keys with this database! Table will not be recreated!\n";
452
+ }
453
+ return changes;
360
454
  }
361
- copyColumn(tableName, oldColumnName, newColumnName) {
362
- return `UPDATE ${tableName} SET ${newColumnName} = ${oldColumnName};`;
363
- }
364
- dropColumn(tableName, columnName) {
365
- return `ALTER TABLE ${tableName} DROP COLUMN ${columnName};`;
366
- }
367
- select(tableName, select, where) {
368
- let query = `SELECT ${select.join(",")} FROM ${tableName}`;
369
- if (where)
370
- query += ` WHERE ${where}`;
371
- return `${query};`;
372
- }
373
- insert(tableName, content) {
374
- return `INSERT INTO ${tableName} ${content};`;
375
- }
376
- insertValues(keys, valueString) {
377
- return `(${keys}) ${valueString ?? `VALUES (${keys.map(() => "?").join(",")})`}`;
378
- }
379
- migrationTableQuery() {
380
- return this.createTable(MIGRATION_DATA_TABLE_NAME, [
381
- this.columnDefinition("version", this.typeInt, "0", false)
382
- ]);
383
- }
384
- async createMigrationTableIfNeeded(db) {
385
- await db.runMultipleWriteStatements(this.migrationTableQuery());
386
- }
387
- //TODO: untested
388
- async getVersion(db) {
389
- await this.createMigrationTableIfNeeded(db);
390
- const query = this.select(MIGRATION_DATA_TABLE_NAME, ["version"]);
391
- const data = await db.runGetStatement(query);
392
- return data.length ? data[0].version : 0;
393
- }
394
- //TODO: untested
395
- async setVersion(db, newVersion) {
396
- const query = `${this.migrationTableQuery()};
397
- INSERT INTO ${MIGRATION_DATA_TABLE_NAME} (version) VALUES ${newVersion} WHERE NOT EXISTS (SELECT 1 FROM ${MIGRATION_DATA_TABLE_NAME})
398
- UNION ALL
399
- UPDATE ${MIGRATION_DATA_TABLE_NAME} SET version = ${newVersion} WHERE EXISTS (SELECT 1 FROM ${MIGRATION_DATA_TABLE_NAME});
400
- `;
401
- await db.runMultipleWriteStatements(query);
402
- }
403
- };
404
-
405
- // src/lib/dialects/SqliteDialect.ts
406
- var SqliteDialect = class extends DefaultSql {
407
- typeBoolean = "INTEGER";
408
- canAlterForeignKeys = false;
409
- canAlterPrimaryKey = false;
410
- canInspectForeignKeys = true;
411
- canInspectPrimaryKey = true;
412
- changeForeignKeysState(enabled) {
413
- return `PRAGMA foreign_keys = ${enabled ? "ON" : "OFF"};`;
414
- }
415
- async getForeignKeys(tableName, db) {
416
- const data = await db.runGetStatement(`PRAGMA foreign_key_list(${tableName});`);
417
- return data.map((entry) => {
418
- return {
419
- fromTable: tableName,
420
- fromColumn: entry["from"],
421
- toTable: entry["table"],
422
- toColumn: entry["to"],
423
- on_update: entry["on_update"],
424
- on_delete: entry["on_delete"]
425
- };
426
- });
427
- }
428
- async getTableNames(db) {
429
- return (await db.runGetStatement("SELECT name FROM sqlite_master WHERE type = 'table'")).map((obj) => obj.name);
430
- }
431
- async getColumnInformation(tableName, db) {
432
- const data = await db.runGetStatement(`PRAGMA table_info(${tableName})`);
433
- return data.map((entry) => {
434
- return {
435
- name: entry["name"],
436
- type: entry["type"],
437
- defaultValue: entry["dflt_value"],
438
- isPrimaryKey: entry["pk"] == "1"
439
- };
440
- });
441
- }
442
- formatValueToSql(value) {
443
- switch (typeof value) {
444
- case "boolean":
445
- return value ? "1" : "0";
446
- default:
447
- return value.toString();
455
+ /**
456
+ * Migrates foreign key constraints for all defined tables.
457
+ */
458
+ async migrateForeignKeys() {
459
+ if (!this.dialect.canAlterForeignKeys)
460
+ return { up: "", down: "" };
461
+ let changes = {
462
+ up: "\n\n-- Foreign keys\n",
463
+ down: "\n\n-- Foreign keys\n"
464
+ };
465
+ for (const tableName in this.newTables) {
466
+ if (this.migrations.willBeRecreated(tableName) || this.tableDoesNotExists(tableName))
467
+ continue;
468
+ const structure = this.newTables[tableName];
469
+ const newForeignKeys = structure.foreignKeys ?? [];
470
+ const oldForeignKeys = await this.dialect.getForeignKeys(tableName);
471
+ let checkForNewForeignKeys = true;
472
+ for (const oldForeignKey of oldForeignKeys) {
473
+ const newForeignKey = newForeignKeys.find((entry) => entry.fromColumn == oldForeignKey.fromColumn);
474
+ if (!newForeignKey) {
475
+ Logger.log(`Foreign key ${oldForeignKey.toTable}.${oldForeignKey.toColumn} to ${oldForeignKey.toTable}.${oldForeignKey.toColumn} was removed!`);
476
+ this.migrations.verifyMigration(tableName, "removeForeignKey");
477
+ if (this.dialect.canAlterForeignKeys) {
478
+ changes.up += this.dialect.removeForeignKey(oldForeignKey.fromTable, oldForeignKey.fromColumn);
479
+ changes.down += this.dialect.addForeignKey(
480
+ oldForeignKey.fromTable,
481
+ this.dialect.foreignKey(oldForeignKey.fromColumn, oldForeignKey.toTable, oldForeignKey.toColumn, oldForeignKey.onUpdate, oldForeignKey.onDelete)
482
+ );
483
+ } else {
484
+ this.migrations.recreateTableImp(structure.table);
485
+ checkForNewForeignKeys = false;
486
+ }
487
+ } else {
488
+ if (!newForeignKey || oldForeignKey.toColumn != newForeignKey.toColumn || (oldForeignKey.onUpdate ?? "NO ACTION") != (newForeignKey.onUpdate ?? "NO ACTION") || (oldForeignKey.onDelete ?? "NO ACTION") != (newForeignKey.onDelete ?? "NO ACTION")) {
489
+ Logger.log(`Foreign key ${oldForeignKey.toTable}.${oldForeignKey.toColumn} to ${oldForeignKey.toTable}.${oldForeignKey.toColumn} was changed!`);
490
+ this.migrations.verifyMigration(tableName, "alterForeignKey");
491
+ if (this.dialect.canAlterForeignKeys) {
492
+ changes.up += this.dialect.removeForeignKey(oldForeignKey.fromTable, oldForeignKey.fromColumn) + this.dialect.addForeignKey(
493
+ newForeignKey.fromTable,
494
+ this.dialect.foreignKey(newForeignKey.fromColumn, newForeignKey.toTable, newForeignKey.toColumn, newForeignKey.onUpdate, newForeignKey.onDelete)
495
+ );
496
+ changes.down += this.dialect.addForeignKey(
497
+ oldForeignKey.fromTable,
498
+ this.dialect.foreignKey(oldForeignKey.fromColumn, oldForeignKey.toTable, oldForeignKey.toColumn, oldForeignKey.onUpdate, oldForeignKey.onDelete)
499
+ ) + this.dialect.removeForeignKey(newForeignKey.fromTable, newForeignKey.fromColumn);
500
+ } else {
501
+ this.migrations.recreateTableImp(structure.table);
502
+ checkForNewForeignKeys = false;
503
+ }
504
+ }
505
+ }
506
+ }
507
+ if (checkForNewForeignKeys) {
508
+ for (const newForeignKey of newForeignKeys) {
509
+ const oldForeignKey = oldForeignKeys.find((entry) => entry.fromColumn == newForeignKey.fromColumn);
510
+ if (oldForeignKey)
511
+ continue;
512
+ Logger.log(`Foreign key ${newForeignKey.toTable}.${newForeignKey.toColumn} to ${newForeignKey.toTable}.${newForeignKey.toColumn} is new!`);
513
+ if (this.dialect.canAlterForeignKeys) {
514
+ changes.up += this.dialect.addForeignKey(
515
+ newForeignKey.toTable,
516
+ this.dialect.foreignKey(newForeignKey.fromColumn, newForeignKey.toTable, newForeignKey.toColumn, newForeignKey.onUpdate, newForeignKey.onDelete)
517
+ );
518
+ changes.down += this.dialect.removeForeignKey(newForeignKey.fromTable, newForeignKey.fromColumn);
519
+ } else {
520
+ this.migrations.recreateTableImp(structure.table);
521
+ break;
522
+ }
523
+ }
524
+ }
448
525
  }
449
- }
450
- async getVersion(db) {
451
- const output = await db.runGetStatement(`PRAGMA user_version;`);
452
- return output[0].user_version;
453
- }
454
- async setVersion(db, newVersion) {
455
- await db.runMultipleWriteStatements(`PRAGMA user_version = ${newVersion};`);
456
- }
457
- };
458
-
459
- // src/lib/dialects/PostgresDialect.ts
460
- var PostgresDialect = class extends DefaultSql {
461
- canAlterForeignKeys = true;
462
- canAlterPrimaryKey = true;
463
- canInspectForeignKeys = false;
464
- canInspectPrimaryKey = false;
465
- changeForeignKeysState(enabled) {
466
- return `SET session_replication_role = '${enabled ? "replica" : "origin"}';`;
467
- }
468
- addForeignKey(fromTableName, foreignKey) {
469
- return `ALTER TABLE ${fromTableName} ADD CONSTRAINT ${fromTableName} ${foreignKey};`;
470
- }
471
- removeForeignKey(tableName, foreignKeyName) {
472
- return `ALTER TABLE ${tableName} DROP CONSTRAINT ${foreignKeyName};`;
473
- }
474
- async getTableNames(db) {
475
- return await db.runGetStatement("SELECT table_name FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE';");
476
- }
477
- async getColumnInformation(tableName, db) {
478
- const data = await db.runGetStatement(`SELECT column_name, data_type, column_default FROM information_schema.columns WHERE table_schema = 'public' AND table_name = '${tableName}';`);
479
- return data.map((entry) => {
480
- return {
481
- name: entry["column_name"],
482
- type: entry["data_type"],
483
- defaultValue: entry["column_default"],
484
- isPrimaryKey: false
485
- };
486
- });
487
- }
488
- addPrimaryKey(tableName, columnName) {
489
- return `ALTER TABLE ${tableName} ADD PRIMARY KEY (${columnName});
490
- `;
491
- }
492
- removePrimaryKey(tableName, columnName) {
493
- return `ALTER TABLE ${tableName} DROP CONSTRAINT ${columnName};
494
- `;
495
- }
496
- };
497
-
498
- // src/lib/dialects/MsSqlDialect.ts
499
- var MsSqlDialect = class extends DefaultSql {
500
- canAlterForeignKeys = true;
501
- canAlterPrimaryKey = true;
502
- canInspectForeignKeys = true;
503
- canInspectPrimaryKey = false;
504
- changeForeignKeysState(enabled) {
505
- return `EXEC sp_MSforeachtable "${enabled ? "ALTER TABLE ? WITH CHECK CHECK CONSTRAINT all" : "ALTER TABLE ? NOCHECK CONSTRAINT all"}"`;
506
- }
507
- async getForeignKeys(tableName, db) {
508
- const data = await db.runGetStatement(`EXEC sp_fkeys @fktable_name = ${tableName};`);
509
- return data.map((entry) => {
510
- return {
511
- fromTable: entry[tableName],
512
- fromColumn: entry["FKCOLUMN_NAME"],
513
- toTable: entry["PKTABLE_NAME"],
514
- toColumn: entry["PKCOLUMN_NAME"]
515
- };
516
- });
517
- }
518
- addForeignKey(fromTableName, foreignKey) {
519
- return `ALTER TABLE ${fromTableName} ADD ${foreignKey};`;
520
- }
521
- removeForeignKey(tableName, foreignKeyName) {
522
- return `ALTER TABLE ${tableName} DROP CONSTRAINT ${foreignKeyName};`;
523
- }
524
- addPrimaryKey(tableName, columnName) {
525
- return `ALTER TABLE ${tableName} ADD CONSTRAINT ${tableName}_primary PRIMARY KEY (${columnName});`;
526
- }
527
- removePrimaryKey(tableName, columnName) {
528
- return `ALTER TABLE ${tableName} DROP CONSTRAINT ${columnName}_primary;`;
529
- }
530
- async getTableNames(db) {
531
- return await db.runGetStatement("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'");
532
- }
533
- async getColumnInformation(tableName, db) {
534
- const data = await db.runGetStatement(`SELECT COLUMN_NAME, DATA_TYPE, COLUMN_DEFAULT FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = N'${tableName};'`);
535
- return data.map((entry) => {
536
- return {
537
- name: entry["COLUMN_NAME"],
538
- type: entry["DATA_TYPE"],
539
- defaultValue: entry["COLUMN_DEFAULT"],
540
- isPrimaryKey: false
541
- };
542
- });
543
- }
544
- };
545
-
546
- // src/lib/dialects/MySqlDialect.ts
547
- var MySqlDialect = class extends DefaultSql {
548
- canAlterForeignKeys = true;
549
- canAlterPrimaryKey = true;
550
- canInspectForeignKeys = true;
551
- canInspectPrimaryKey = true;
552
- changeForeignKeysState(enabled) {
553
- return `SET FOREIGN_KEY_CHECKS ${enabled ? "1" : "0"}
554
- ;`;
555
- }
556
- async getForeignKeys(tableName, db) {
557
- const data = await db.runGetStatement(`SELECT TABLE_NAME,COLUMN_NAME,CONSTRAINT_NAME, REFERENCED_TABLE_NAME,REFERENCED_COLUMN_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE REFERENCED_TABLE_SCHEMA = (SELECT DATABASE()) AND REFERENCED_TABLE_NAME = '${tableName}';`);
558
- return data.map((entry) => {
559
- return {
560
- fromTable: tableName,
561
- fromColumn: entry["COLUMN_NAME"],
562
- toTable: entry["REFERENCED_TABLE_NAME"],
563
- toColumn: entry["REFERENCED_COLUMN_NAME"]
564
- };
565
- });
566
- }
567
- addForeignKey(fromTableName, foreignKey) {
568
- return `ALTER TABLE ${fromTableName} ADD ${foreignKey};`;
569
- }
570
- removeForeignKey(tableName, foreignKeyName) {
571
- return `ALTER TABLE ${tableName} DROP FOREIGN KEY ${foreignKeyName};`;
572
- }
573
- addPrimaryKey(tableName, columnName) {
574
- return `ALTER TABLE ${tableName} PRIMARY KEY (${columnName});`;
575
- }
576
- removePrimaryKey(tableName, _columnName) {
577
- return `ALTER TABLE ${tableName} PRIMARY KEY;`;
578
- }
579
- async getTableNames(db) {
580
- return await db.runGetStatement("SHOW TABLES;");
581
- }
582
- async getColumnInformation(tableName, db) {
583
- const data = await db.runGetStatement(`SHOW COLUMNS FROM ${tableName};`);
584
- return data.map((entry) => {
585
- return {
586
- name: entry["Field"],
587
- type: entry["Type"],
588
- defaultValue: entry["Default"],
589
- isPrimaryKey: entry["Key"] == "PRI"
590
- };
591
- });
592
- }
593
- };
594
-
595
- // src/lib/getDialect.ts
596
- function getDialect(dbInstructions) {
597
- switch (dbInstructions.dialect) {
598
- case "Sqlite":
599
- return new SqliteDialect();
600
- case "Postgres":
601
- return new PostgresDialect();
602
- case "MsSql":
603
- return new MsSqlDialect();
604
- case "MySql":
605
- return new MySqlDialect();
606
- default:
607
- throw new Error(`Unknown dialect ${dbInstructions.dialect}`);
608
- }
609
- }
610
-
611
- // src/lib/MigrationManager.ts
612
- var MigrationManager = class {
613
- migrations = new Migrations();
614
- tableStructureGenerator;
615
- existingTables = [];
616
- db;
617
- dbInstructions;
618
- migrationHistoryManager;
619
- dialect;
620
- constructor(db, dbInstructions) {
621
- this.dialect = getDialect(dbInstructions);
622
- this.db = db;
623
- this.dbInstructions = dbInstructions;
624
- this.tableStructureGenerator = new TableStructureGenerator(dbInstructions, this.dialect);
625
- this.migrationHistoryManager = new MigrationHistoryManager(this.dbInstructions.configPath);
526
+ return changes;
626
527
  }
627
528
  /**
628
- * Generates the SQL changes required to migrate a database from its current version
629
- * to the target version defined in the database instructions. Handles the creation
630
- * and deletion of tables, migration of foreign keys and columns, renaming of columns,
631
- * and recreating tables, along with executing any pre- and post-migration custom logic.
632
- *
633
- * @return A promise that resolves to an object containing the SQL
634
- * changes for the migration (`up` and `down` changes) or null if no migration is required.
635
- * Throws an error in scenarios such as attempting to migrate to a lower version
636
- * or an invalid starting version.
529
+ * Checks all columns of all tables, creates them if they do not exist in the database and modifies them if needed.
530
+ * If types, default values or primary key change, the table is recreated
637
531
  */
638
- async getMigrateSql() {
639
- if (this.dbInstructions.version <= 0)
640
- throw new Error("Cannot migrate to version 0 or lower");
641
- const fromVersion = await this.dialect.getVersion(this.db);
642
- if (fromVersion == 0) {
643
- Logger.log(`Creating initial migration to version ${this.dbInstructions.version}`);
644
- return this.createAndDropTables();
645
- }
646
- if (fromVersion == this.dbInstructions.version) {
647
- Logger.log("Version has not changed. No migrations needed.");
648
- return null;
649
- }
650
- if (fromVersion > this.dbInstructions.version)
651
- throw new Error(`You cannot create new migrations with a lower version (from ${fromVersion} to ${this.dbInstructions.version})`);
652
- Logger.log(`Creating migration SQL from version ${fromVersion} to ${this.dbInstructions.version}`);
653
- const db = this.db;
654
- await db.createBackup?.(`from_${fromVersion}_to_${this.dbInstructions.version}`);
655
- this.existingTables = await this.dialect.getTableNames(this.db);
656
- const foreignKeyOffQuery = this.dialect.changeForeignKeysState(false);
532
+ async migrateColumns() {
657
533
  const changes = {
658
- up: foreignKeyOffQuery,
659
- down: foreignKeyOffQuery
534
+ up: "\n\n-- Migrate columns\n",
535
+ down: "\n\n-- Migrate columns\n"
660
536
  };
661
- const preSQL = this.dbInstructions.preMigration?.(
662
- this.migrations,
663
- fromVersion,
664
- this.dbInstructions.version
665
- );
666
- if (preSQL) {
667
- changes.up += `
668
-
669
- -- Custom SQL from preMigration()
670
- ${preSQL.up}`;
671
- changes.down += `
672
-
673
- -- Custom SQL from preMigration()
674
- ${preSQL.down}`;
675
- }
676
- [
677
- await this.createAndDropTables(),
678
- await this.migrateForeignKeys(),
679
- await this.migrateColumns(),
680
- this.renameColumns(),
681
- await this.recreateTables()
682
- ].forEach((entry) => {
683
- changes.up += entry.up;
684
- changes.down += entry.down;
685
- });
686
- const postSql = this.dbInstructions.postMigration?.(fromVersion, this.dbInstructions.version);
687
- if (postSql) {
688
- changes.up += `
689
-
690
- -- Custom SQL from postMigration()
691
- ${postSql.up}`;
692
- changes.down += `
693
-
694
- -- Custom SQL from postMigration()
695
- ${postSql.down}`;
537
+ for (const tableName in this.newTables) {
538
+ if (this.migrations.willBeRecreated(tableName) || this.tableDoesNotExists(tableName))
539
+ continue;
540
+ const newTableDefinition = this.newTables[tableName];
541
+ const oldColumnList = await this.dialect.getColumnInformation(tableName);
542
+ const oldPrimaryKey = this.dialect.canInspectPrimaryKey ? this.getPrimaryKeyColumn(oldColumnList) : false;
543
+ const newColumnList = newTableDefinition.columns;
544
+ const newPrimaryKey = newTableDefinition.primaryKey;
545
+ if (this.dialect.canInspectPrimaryKey && oldPrimaryKey != newPrimaryKey) {
546
+ Logger.log(`Primary key in ${tableName} was changed from ${oldPrimaryKey} to ${newPrimaryKey}!`);
547
+ this.migrations.verifyMigration(tableName, "alterPrimaryKey");
548
+ if (this.dialect.canAlterPrimaryKey) {
549
+ if (oldPrimaryKey) {
550
+ changes.up += this.dialect.removePrimaryKey(tableName, oldPrimaryKey);
551
+ changes.down += this.dialect.addPrimaryKey(tableName, oldPrimaryKey);
552
+ }
553
+ if (newPrimaryKey) {
554
+ changes.up += this.dialect.addPrimaryKey(tableName, newPrimaryKey.toString());
555
+ changes.down += this.dialect.removePrimaryKey(tableName, newPrimaryKey.toString());
556
+ }
557
+ } else
558
+ this.migrations.recreateTableImp(newTableDefinition.table);
559
+ continue;
560
+ }
561
+ for (const newColumn of newColumnList) {
562
+ const oldColumn = oldColumnList.find(
563
+ (entry) => this.migrations.getUpdatedColumnName(tableName, entry.name) == newColumn.name
564
+ );
565
+ if (oldColumn == void 0) {
566
+ changes.up += this.dialect.createColumn(tableName, this.dialect.columnDefinition(newColumn.name, newColumn.type, newColumn.defaultValue, newColumn.isPrimaryKey)) + "\n";
567
+ changes.down += this.dialect.dropColumn(tableName, newColumn.name) + "\n";
568
+ } else if (newColumn.type != oldColumn.type || newColumn.defaultValue != oldColumn.defaultValue) {
569
+ this.migrations.recreateTableImp(newTableDefinition.table);
570
+ }
571
+ }
572
+ for (const oldColumn of oldColumnList) {
573
+ const newColumn = newColumnList.find(
574
+ (entry) => entry.name == this.migrations.getUpdatedColumnName(tableName, oldColumn.name)
575
+ );
576
+ if (newColumn == void 0) {
577
+ this.migrations.verifyMigration(tableName, "dropColumn");
578
+ changes.up += this.dialect.dropColumn(tableName, oldColumn.name) + "\n";
579
+ changes.down += this.dialect.createColumn(tableName, this.dialect.columnDefinition(oldColumn.name, oldColumn.type, oldColumn.defaultValue, oldColumn.isPrimaryKey)) + "\n";
580
+ }
581
+ }
696
582
  }
697
- const foreignKeyOnQuery = this.dialect.changeForeignKeysState(true);
698
- changes.up += `
699
-
700
- ${foreignKeyOnQuery}`;
701
- changes.down += `
702
-
703
- ${foreignKeyOnQuery}`;
704
583
  return changes;
705
584
  }
706
585
  /**
707
- * Prepares the migration by generating the SQL changes and creating a migration history entry.
708
- * If no changes are needed, the method exits early.
586
+ * Retrieves the name of the primary key column from a list of column information.
709
587
  *
710
- * @param overwriteExisting - Optional flag indicating if overwriting existing changes is allowed.
711
- * @return - A promise that resolves once the migration preparation is complete.
588
+ * @param columnInfoList - An array of ColumnInfo objects containing details about each column.
589
+ * @return The name of the primary key column if found; otherwise, undefined.
712
590
  */
713
- async prepareMigration(overwriteExisting) {
714
- const changes = await this.getMigrateSql();
715
- if (!changes)
716
- return;
717
- const fromVersion = await this.dialect.getVersion(this.db);
718
- this.migrationHistoryManager.createMigrationHistory(fromVersion, this.dbInstructions.version, changes, overwriteExisting);
591
+ getPrimaryKeyColumn(columnInfoList) {
592
+ for (const columnInfo of columnInfoList) {
593
+ if (columnInfo.isPrimaryKey)
594
+ return columnInfo.name;
595
+ }
719
596
  }
720
597
  /**
721
- * Generates SQL change statements for creating and dropping tables based on the current table structure
722
- * and existing table configurations in the database. The `up` changes include SQL statements for creating
723
- * new tables, while the `down` changes include SQL statements for dropping the corresponding tables or
724
- * recreating them if necessary.
725
- *
726
- * @return {Promise<SqlChanges>} A promise that resolves to an object containing `up` and `down` SQL
727
- * change statements for migrating the database schema.
598
+ * Handles the renaming of columns in database tables by generating SQL changes for both applying and reverting the renames.
599
+ * Skips tables that are marked for recreation.
728
600
  */
729
- async createAndDropTables() {
730
- let changes = {
731
- up: "\n\n-- Create tables\n",
732
- down: "\n\n-- Create tables\n"
601
+ renameColumns() {
602
+ const changes = {
603
+ up: "\n\n-- Rename columns\n",
604
+ down: "\n\n-- Rename columns\n"
733
605
  };
734
- for (const tableName in this.tableStructureGenerator.tables) {
735
- if (this.tableDoesNotExists(tableName)) {
736
- const structure = this.tableStructureGenerator.tables[tableName];
737
- changes.up += `${this.createTableSql(tableName, structure.columns, structure.foreignKeys)}
738
- `;
739
- changes.down += `${this.dialect.dropTable(tableName)}
740
- `;
741
- }
742
- }
743
- for (const tableName of this.existingTables) {
744
- if (!this.tableStructureGenerator.tables[tableName]) {
745
- this.migrations.throwIfNotAllowed(this.dbInstructions.version, tableName, "dropTable");
746
- if (!this.dialect.canInspectForeignKeys || !this.dialect.canInspectPrimaryKey)
747
- this.migrations.throwIfNotAllowed(this.dbInstructions.version, tableName, "continueWithoutRollback");
748
- changes.up += `${this.dialect.dropTable(tableName)}
749
- `;
750
- changes.down += this.dialect.canInspectForeignKeys && this.dialect.canInspectPrimaryKey ? this.createTableSql(tableName, await this.dialect.getColumnInformation(tableName, this.db), await this.dialect.getForeignKeys(tableName, this.db)) : "\n\n-- Cannot recreate foreign keys or load primary keys with this database! Table will not be recreated!\n";
751
- }
606
+ const migrationData = this.migrations.getMigrationData();
607
+ for (const tableName in migrationData) {
608
+ if (migrationData[tableName].recreate)
609
+ continue;
610
+ this.migrations.loopRenamedColumns(tableName, (oldColumnName, newColumnName) => {
611
+ changes.up += this.dialect.renameColumn(tableName, oldColumnName, newColumnName);
612
+ changes.down += this.dialect.renameColumn(tableName, newColumnName, oldColumnName);
613
+ });
752
614
  }
753
615
  return changes;
754
616
  }
617
+ };
618
+
619
+ // src/lib/MigrationHistoryManager.ts
620
+ var import_node_fs = require("fs");
621
+ var import_fs = require("fs");
622
+ var FILENAME_UP_PREFIX = "up_to_";
623
+ var FILENAME_DOWN_PREFIX = "down_to_";
624
+ var MigrationHistoryManager = class {
625
+ configPath;
626
+ migrationsPath;
627
+ constructor(configPath) {
628
+ this.configPath = configPath;
629
+ this.migrationsPath = `${configPath}/migrations`;
630
+ (0, import_node_fs.mkdirSync)(this.configPath, { recursive: true });
631
+ (0, import_node_fs.mkdirSync)(this.migrationsPath, { recursive: true });
632
+ }
755
633
  /**
756
- * Generates an SQL string to create a table with the specified schema.
634
+ * Creates migration history files for upgrading and downgrading database schemas.
757
635
  *
758
- * @param tableName - The name of the table to be created.
759
- * @param columns - An array of objects defining the columns of the table, including their names, types
760
- * default values, and whether they are primary keys.
761
- * @param foreignKeys - An optional array of objects defining the foreign key constraints for the table,
762
- * including the source column, target table, target column, action on update, and action on delete.
763
- * @return The complete SQL string to create the table with the specified structure and constraints.
636
+ * @param fromVersion - The current version number of the database.
637
+ * @param toVersion - The target version number for the migration.
638
+ * @param changes - An object containing the SQL changes for upgrading (`changes.up`) and downgrading (`changes.down`).
639
+ * @param overwriteExisting - A flag that determines if existing migration files should be overwritten. Defaults to `false`.
764
640
  */
765
- createTableSql(tableName, columns, foreignKeys) {
766
- const queryLines = [];
767
- for (const columnInfo of columns) {
768
- queryLines.push(this.dialect.columnDefinition(columnInfo.name, columnInfo.type, columnInfo.defaultValue, columnInfo.isPrimaryKey));
769
- }
770
- if (foreignKeys) {
771
- for (const foreignKeyInfo of foreignKeys) {
772
- queryLines.push(this.dialect.foreignKey(foreignKeyInfo.fromColumn, foreignKeyInfo.toTable, foreignKeyInfo.toColumn.toString(), foreignKeyInfo.onUpdate, foreignKeyInfo.onDelete));
773
- }
774
- }
775
- return this.dialect.createTable(tableName, queryLines);
641
+ createMigrationHistory(fromVersion, toVersion, changes, overwriteExisting) {
642
+ const upPath = `${this.migrationsPath}/${FILENAME_UP_PREFIX}${toVersion}.sql`;
643
+ const downPath = `${this.migrationsPath}/${FILENAME_DOWN_PREFIX}${fromVersion}.sql`;
644
+ if ((0, import_node_fs.existsSync)(upPath) && !overwriteExisting)
645
+ throw new Error(`Migration ${upPath} already exists!`);
646
+ else if ((0, import_node_fs.existsSync)(downPath) && !overwriteExisting)
647
+ throw new Error(`Migration ${downPath} already exists!`);
648
+ Logger.log(`Save migrations to ${this.migrationsPath}`);
649
+ (0, import_node_fs.writeFileSync)(`${this.migrationsPath}/${FILENAME_UP_PREFIX}${toVersion}.sql`, `-- From: Version ${fromVersion}
650
+ -- To: Version ${toVersion}
651
+ ${changes.up}`, { encoding: "utf-8" });
652
+ (0, import_node_fs.writeFileSync)(`${this.migrationsPath}/${FILENAME_DOWN_PREFIX}${fromVersion}.sql`, `-- From: Version ${toVersion}
653
+ -- To: Version ${fromVersion}
654
+ ${changes.down}`, { encoding: "utf-8" });
776
655
  }
777
656
  /**
778
- * Checks if the specified table does not exist in the list of existing tables.
657
+ * Retrieves the SQL migration script for upgrading to the specified version.
658
+ * If no version is provided, it retrieves the migration script for the latest version in the history.
779
659
  *
780
- * @param tableName - The name of the table to check.
781
- * @return Returns true if the table does not exist, otherwise false.
660
+ * @param toVersion - The version of the migration script to retrieve. If not specified, the latest version is used.
661
+ * @return The content of the SQL migration script as a string.
782
662
  */
783
- tableDoesNotExists(tableName) {
784
- return this.existingTables.indexOf(tableName) == -1;
663
+ getUpMigration(toVersion) {
664
+ return (0, import_fs.readFileSync)(`${this.migrationsPath}/${FILENAME_UP_PREFIX}${toVersion}.sql`, { encoding: "utf-8" });
785
665
  }
786
666
  /**
787
- * Recreates database tables that have been flagged to be recreated in migration data.
788
- * This method generated the necessary SQL code for backing up the current table data, dropping and recreating
789
- * the tables, and reinserting the preserved data into the recreated tables.
667
+ * Retrieves the SQL migration script for downgrading to the specified version.
668
+ *
669
+ * @param version - The version number of the migration to retrieve.
670
+ * @return The SQL script for the down migration.
790
671
  */
791
- async recreateTables() {
792
- const changes = {
793
- up: "\n\n-- Recreate tables\n",
794
- down: "\n\n-- Recreate tables\n"
795
- };
796
- const migrationData = this.migrations.getMigrationData();
797
- for (const tableName in migrationData) {
798
- const migrationEntry = migrationData[tableName];
799
- if (!migrationEntry.recreate)
800
- continue;
801
- this.migrations.throwIfNotAllowed(this.dbInstructions.version, tableName, "recreateTable");
802
- if (!this.dialect.canInspectForeignKeys || !this.dialect.canInspectPrimaryKey)
803
- this.migrations.throwIfNotAllowed(this.dbInstructions.version, tableName, "continueWithoutRollback");
804
- const oldColumnList = await this.dialect.getColumnInformation(tableName, this.db);
805
- const newColumnList = this.tableStructureGenerator.tables[tableName].columns;
806
- const moveableColumns = newColumnList.filter((entry) => oldColumnList.find((oldEntry) => oldEntry.name == entry.name) != void 0).map((entry) => entry.name);
807
- const backupTableName = `${tableName}__backup`;
808
- const insertQuery = this.dialect.insert(
809
- backupTableName,
810
- this.dialect.insertValues(moveableColumns, this.dialect.select(tableName, moveableColumns))
811
- );
812
- const moveDataQuery = insertQuery + "\n" + this.dialect.dropTable(tableName) + "\n" + this.dialect.renameTable(backupTableName, tableName);
813
- const structure = this.tableStructureGenerator.tables[tableName];
814
- changes.up += this.createTableSql(backupTableName, structure.columns, structure.foreignKeys) + "\n" + moveDataQuery;
815
- changes.down += this.dialect.canInspectForeignKeys && this.dialect.canInspectPrimaryKey ? this.createTableSql(backupTableName, await this.dialect.getColumnInformation(tableName, this.db), await this.dialect.getForeignKeys(tableName, this.db)) + "\n" + moveDataQuery : "\n\n-- Cannot recreate foreign keys or load primary keys with this database! Table will not be recreated!\n";
816
- }
817
- return changes;
672
+ getDownMigration(version) {
673
+ return (0, import_fs.readFileSync)(`${this.migrationsPath}/${FILENAME_DOWN_PREFIX}${version}.sql`, { encoding: "utf-8" });
818
674
  }
819
- /**
820
- * Migrates foreign key constraints for all defined tables.
821
- */
822
- async migrateForeignKeys() {
823
- if (!this.dialect.canAlterForeignKeys)
824
- return { up: "", down: "" };
825
- let changes = {
826
- up: "\n\n-- Foreign keys\n",
827
- down: "\n\n-- Foreign keys\n"
828
- };
829
- for (const tableName in this.tableStructureGenerator.tables) {
830
- if (this.migrations.willBeRecreated(tableName) || this.tableDoesNotExists(tableName))
831
- continue;
832
- const structure = this.tableStructureGenerator.tables[tableName];
833
- const newForeignKeys = structure.foreignKeys ?? [];
834
- const oldForeignKeys = await this.dialect.getForeignKeys(tableName, this.db);
835
- let checkForNewForeignKeys = true;
836
- for (const oldForeignKey of oldForeignKeys) {
837
- const newForeignKey = newForeignKeys.find((entry) => entry.fromColumn == oldForeignKey.fromColumn);
838
- if (!newForeignKey) {
839
- Logger.log(`Foreign key ${oldForeignKey.toTable}.${oldForeignKey.toColumn} to ${oldForeignKey.toTable}.${oldForeignKey.toColumn} was removed!`);
840
- this.migrations.throwIfNotAllowed(this.dbInstructions.version, tableName, "removeForeignKey");
841
- if (this.dialect.canAlterForeignKeys) {
842
- changes.up += this.dialect.removeForeignKey(oldForeignKey.fromTable, oldForeignKey.fromColumn);
843
- changes.down += this.dialect.addForeignKey(
844
- oldForeignKey.fromTable,
845
- this.dialect.foreignKey(oldForeignKey.fromColumn, oldForeignKey.toTable, oldForeignKey.toColumn, oldForeignKey.onUpdate, oldForeignKey.onDelete)
846
- );
847
- } else {
848
- this.migrations.recreateTable(structure.table);
849
- checkForNewForeignKeys = false;
850
- }
851
- } else {
852
- if (!newForeignKey || oldForeignKey.toColumn != newForeignKey.toColumn || (oldForeignKey.onUpdate ?? "NO ACTION") != (newForeignKey.onUpdate ?? "NO ACTION") || (oldForeignKey.onDelete ?? "NO ACTION") != (newForeignKey.onDelete ?? "NO ACTION")) {
853
- Logger.log(`Foreign key ${oldForeignKey.toTable}.${oldForeignKey.toColumn} to ${oldForeignKey.toTable}.${oldForeignKey.toColumn} was changed!`);
854
- this.migrations.throwIfNotAllowed(this.dbInstructions.version, tableName, "alterForeignKey");
855
- if (this.dialect.canAlterForeignKeys) {
856
- changes.up += this.dialect.removeForeignKey(oldForeignKey.fromTable, oldForeignKey.fromColumn) + this.dialect.addForeignKey(
857
- newForeignKey.fromTable,
858
- this.dialect.foreignKey(newForeignKey.fromColumn, newForeignKey.toTable, newForeignKey.toColumn, newForeignKey.onUpdate, newForeignKey.onDelete)
859
- );
860
- changes.down += this.dialect.addForeignKey(
861
- oldForeignKey.fromTable,
862
- this.dialect.foreignKey(oldForeignKey.fromColumn, oldForeignKey.toTable, oldForeignKey.toColumn, oldForeignKey.onUpdate, oldForeignKey.onDelete)
863
- ) + this.dialect.removeForeignKey(newForeignKey.fromTable, newForeignKey.fromColumn);
864
- } else {
865
- this.migrations.recreateTable(structure.table);
866
- checkForNewForeignKeys = false;
867
- }
868
- }
869
- }
870
- }
871
- if (checkForNewForeignKeys) {
872
- for (const newForeignKey of newForeignKeys) {
873
- const oldForeignKey = oldForeignKeys.find((entry) => entry.fromColumn == newForeignKey.fromColumn);
874
- if (oldForeignKey)
875
- continue;
876
- Logger.log(`Foreign key ${newForeignKey.toTable}.${newForeignKey.toColumn} to ${newForeignKey.toTable}.${newForeignKey.toColumn} is new!`);
877
- if (this.dialect.canAlterForeignKeys) {
878
- changes.up += this.dialect.addForeignKey(
879
- newForeignKey.toTable,
880
- this.dialect.foreignKey(newForeignKey.fromColumn, newForeignKey.toTable, newForeignKey.toColumn, newForeignKey.onUpdate, newForeignKey.onDelete)
881
- );
882
- changes.down += this.dialect.removeForeignKey(newForeignKey.fromTable, newForeignKey.fromColumn);
883
- } else {
884
- this.migrations.recreateTable(structure.table);
885
- break;
886
- }
887
- }
675
+ };
676
+
677
+ // src/lib/tableInfo/decorators/DbTable.ts
678
+ var import_polyfill_symbol_metadata = require("polyfill-symbol-metadata");
679
+ function DbTable(tableName, primaryKey) {
680
+ return (table, context) => {
681
+ Object.defineProperty(table, "name", { value: tableName, writable: false });
682
+ const tableInfo = context?.metadata ? getTableInfoFromMetadata(context.metadata) : getTableInfo(table);
683
+ tableInfo.primaryKey = primaryKey?.toString();
684
+ if (tableInfo.foreignKeys) {
685
+ for (const foreignKey of tableInfo.foreignKeys) {
686
+ foreignKey.fromTable = tableName;
888
687
  }
889
688
  }
890
- return changes;
689
+ };
690
+ }
691
+
692
+ // src/lib/tableInfo/decorators/ForeignKey.ts
693
+ var import_polyfill_symbol_metadata2 = require("polyfill-symbol-metadata");
694
+ function ForeignKey(toTable, toColumn, onDelete, onUpdate) {
695
+ return (table, context) => {
696
+ const metadata = context?.metadata ? getTableInfoFromMetadata(context.metadata) : getTableInfo(table);
697
+ if (!metadata.foreignKeys)
698
+ metadata.foreignKeys = [];
699
+ metadata.foreignKeys.push({
700
+ fromTable: "",
701
+ fromColumn: context.name ?? context,
702
+ //to stay compatible with old decorator structure (see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#decorator-metadata)
703
+ toTable: toTable.name,
704
+ toColumn: toColumn.toString(),
705
+ onDelete,
706
+ onUpdate
707
+ });
708
+ };
709
+ }
710
+
711
+ // src/lib/dialects/DefaultSql.ts
712
+ var MIGRATION_DATA_TABLE_NAME = "__sqlmorpheus_migrations";
713
+ var DefaultSql = class {
714
+ db;
715
+ canAlterForeignKeys = false;
716
+ canAlterPrimaryKey = false;
717
+ canInspectForeignKeys = false;
718
+ canInspectPrimaryKey = false;
719
+ typeString = "TEXT";
720
+ typeInt = "INTEGER";
721
+ typeBigInt = "BIGINT";
722
+ typeBoolean = "BOOLEAN";
723
+ typeNull = "NULL";
724
+ constructor(db) {
725
+ this.db = db;
726
+ }
727
+ formatValueToSql(value) {
728
+ return value.toString();
729
+ }
730
+ changeForeignKeysState(enabled) {
731
+ return "";
732
+ }
733
+ addForeignKey(fromTableName, foreignKey) {
734
+ throw new Error("Adding foreign keys is not supported!");
735
+ }
736
+ removeForeignKey(tableName, foreignKeyName) {
737
+ throw new Error("Foreign key removal is not supported!");
738
+ }
739
+ addPrimaryKey(tableName, columnName) {
740
+ throw new Error("Adding foreign keys is not supported!");
741
+ }
742
+ removePrimaryKey(tableName, columnName) {
743
+ throw new Error("Foreign key removal is not supported!");
744
+ }
745
+ foreignKey(column, foreignTable, foreignColumn, onUpdate, onDelete) {
746
+ let query = `FOREIGN KEY (${column}) REFERENCES ${foreignTable} (${foreignColumn})`;
747
+ if (onUpdate)
748
+ query += ` ON UPDATE ${onUpdate}`;
749
+ if (onDelete)
750
+ query += ` ON DELETE ${onDelete}`;
751
+ return query;
752
+ }
753
+ createTable(tableName, entries) {
754
+ return `CREATE TABLE IF NOT EXISTS ${tableName} (
755
+ ${entries.join(",\n ")}
756
+ );`;
757
+ }
758
+ renameTable(tableName, newTableName) {
759
+ return `ALTER TABLE ${tableName} RENAME TO ${newTableName};`;
760
+ }
761
+ dropTable(tableName) {
762
+ return `DROP TABLE IF EXISTS ${tableName};`;
763
+ }
764
+ columnDefinition(columnName, type, defaultValue, isPrimaryKey) {
765
+ const query = `${columnName} ${type} DEFAULT ${defaultValue}`;
766
+ return isPrimaryKey ? `${query} PRIMARY KEY` : query;
767
+ }
768
+ createColumn(columnTable, entry) {
769
+ return `ALTER TABLE ${columnTable} ADD ${entry};`;
770
+ }
771
+ renameColumn(tableName, oldColumnName, newColumnName) {
772
+ return `ALTER TABLE ${tableName} RENAME COLUMN ${oldColumnName} TO ${newColumnName};`;
773
+ }
774
+ copyColumn(tableName, oldColumnName, newColumnName) {
775
+ return `UPDATE ${tableName} SET ${newColumnName} = ${oldColumnName};`;
776
+ }
777
+ dropColumn(tableName, columnName) {
778
+ return `ALTER TABLE ${tableName} DROP COLUMN ${columnName};`;
779
+ }
780
+ select(tableName, select, where) {
781
+ let query = `SELECT ${select.join(",")} FROM ${tableName}`;
782
+ if (where)
783
+ query += ` WHERE ${where}`;
784
+ return `${query};`;
785
+ }
786
+ insert(tableName, content) {
787
+ return `INSERT INTO ${tableName} ${content};`;
788
+ }
789
+ insertValues(keys, valueString) {
790
+ return `(${keys}) ${valueString ?? `VALUES (${keys.map(() => "?").join(",")})`}`;
791
+ }
792
+ migrationTableQuery() {
793
+ return this.createTable(MIGRATION_DATA_TABLE_NAME, [
794
+ this.columnDefinition("version", this.typeInt, "0", false)
795
+ ]);
796
+ }
797
+ async getForeignKeys(tableName) {
798
+ throw new Error("Inspecting foreign keys is not supported!");
799
+ }
800
+ async createMigrationTableIfNeeded() {
801
+ await this.db.runMultipleWriteStatements(this.migrationTableQuery());
802
+ }
803
+ //TODO: untested
804
+ async getVersion() {
805
+ await this.createMigrationTableIfNeeded();
806
+ const query = this.select(MIGRATION_DATA_TABLE_NAME, ["version"]);
807
+ const data = await this.db.runGetStatement(query);
808
+ return data.length ? data[0].version : 0;
809
+ }
810
+ //TODO: untested
811
+ async setVersion(newVersion) {
812
+ const query = `${this.migrationTableQuery()};
813
+ INSERT INTO ${MIGRATION_DATA_TABLE_NAME} (version) VALUES ${newVersion} WHERE NOT EXISTS (SELECT 1 FROM ${MIGRATION_DATA_TABLE_NAME})
814
+ UNION ALL
815
+ UPDATE ${MIGRATION_DATA_TABLE_NAME} SET version = ${newVersion} WHERE EXISTS (SELECT 1 FROM ${MIGRATION_DATA_TABLE_NAME});
816
+ `;
817
+ await this.db.runMultipleWriteStatements(query);
818
+ }
819
+ };
820
+
821
+ // src/lib/dialects/SqliteDialect.ts
822
+ var SqliteDialect = class extends DefaultSql {
823
+ typeBoolean = "INTEGER";
824
+ typeBigInt = "INTEGER";
825
+ canAlterForeignKeys = false;
826
+ canAlterPrimaryKey = false;
827
+ canInspectForeignKeys = true;
828
+ canInspectPrimaryKey = true;
829
+ changeForeignKeysState(enabled) {
830
+ return `PRAGMA foreign_keys = ${enabled ? "ON" : "OFF"};`;
831
+ }
832
+ async getTableNames() {
833
+ return (await this.db.runGetStatement("SELECT name FROM sqlite_master WHERE type = 'table'")).map((obj) => obj.name);
834
+ }
835
+ async getColumnInformation(tableName) {
836
+ const data = await this.db.runGetStatement(`PRAGMA table_info(${tableName})`);
837
+ return data.map((entry) => {
838
+ return {
839
+ name: entry["name"],
840
+ type: entry["type"],
841
+ defaultValue: entry["dflt_value"],
842
+ isPrimaryKey: entry["pk"] == "1"
843
+ };
844
+ });
891
845
  }
892
- /**
893
- * Checks all columns of all tables, creates them if they do not exist in the database and modifies them if needed.
894
- * If types, default values or primary key change, the table is recreated
895
- */
896
- async migrateColumns() {
897
- const changes = {
898
- up: "\n\n-- Migrate columns\n",
899
- down: "\n\n-- Migrate columns\n"
900
- };
901
- for (const tableName in this.tableStructureGenerator.tables) {
902
- if (this.migrations.willBeRecreated(tableName) || this.tableDoesNotExists(tableName))
903
- continue;
904
- const newTableDefinition = this.tableStructureGenerator.tables[tableName];
905
- const oldColumnList = await this.dialect.getColumnInformation(tableName, this.db);
906
- const oldPrimaryKey = this.dialect.canInspectPrimaryKey ? this.getPrimaryKeyColumn(oldColumnList) : false;
907
- const newColumnList = newTableDefinition.columns;
908
- const newPrimaryKey = newTableDefinition.primaryKey;
909
- if (this.dialect.canInspectPrimaryKey && oldPrimaryKey != newPrimaryKey) {
910
- Logger.log(`Primary key in ${tableName} was changed from ${oldPrimaryKey} to ${newPrimaryKey}!`);
911
- this.migrations.throwIfNotAllowed(this.dbInstructions.version, tableName, "alterPrimaryKey");
912
- if (this.dialect.canAlterPrimaryKey) {
913
- if (oldPrimaryKey) {
914
- changes.up += this.dialect.removePrimaryKey(tableName, oldPrimaryKey);
915
- changes.down += this.dialect.addPrimaryKey(tableName, oldPrimaryKey);
916
- }
917
- if (newPrimaryKey) {
918
- changes.up += this.dialect.addPrimaryKey(tableName, newPrimaryKey.toString());
919
- changes.down += this.dialect.removePrimaryKey(tableName, newPrimaryKey.toString());
920
- }
921
- } else
922
- this.migrations.recreateTable(newTableDefinition.table);
923
- continue;
924
- }
925
- for (const newColumn of newColumnList) {
926
- const oldColumn = oldColumnList.find((entry) => entry.name == newColumn.name);
927
- if (oldColumn == void 0) {
928
- changes.up += this.dialect.createColumn(tableName, this.dialect.columnDefinition(newColumn.name, newColumn.type, newColumn.defaultValue, newColumn.isPrimaryKey)) + "\n";
929
- changes.down += this.dialect.dropColumn(tableName, newColumn.name) + "\n";
930
- } else if (newColumn.type != oldColumn.type || newColumn.defaultValue != oldColumn.defaultValue) {
931
- this.migrations.recreateTable(newTableDefinition.table);
932
- }
933
- }
934
- for (const oldColumn of oldColumnList) {
935
- const newColumn = newColumnList.find((entry) => entry.name == oldColumn.name);
936
- if (newColumn == void 0) {
937
- this.migrations.throwIfNotAllowed(this.dbInstructions.version, tableName, "dropColumn");
938
- changes.up += this.dialect.dropColumn(tableName, oldColumn.name) + "\n";
939
- changes.down += this.dialect.createColumn(tableName, this.dialect.columnDefinition(oldColumn.name, oldColumn.type, oldColumn.defaultValue, oldColumn.isPrimaryKey)) + "\n";
940
- }
941
- }
846
+ formatValueToSql(value) {
847
+ switch (typeof value) {
848
+ case "boolean":
849
+ return value ? "1" : "0";
850
+ default:
851
+ return value.toString();
942
852
  }
943
- return changes;
944
853
  }
945
- /**
946
- * Retrieves the name of the primary key column from a list of column information.
947
- *
948
- * @param columnInfoList - An array of ColumnInfo objects containing details about each column.
949
- * @return The name of the primary key column if found; otherwise, undefined.
950
- */
951
- getPrimaryKeyColumn(columnInfoList) {
952
- for (const columnInfo of columnInfoList) {
953
- if (columnInfo.isPrimaryKey)
954
- return columnInfo.name;
955
- }
854
+ async getForeignKeys(tableName) {
855
+ const data = await this.db.runGetStatement(`PRAGMA foreign_key_list(${tableName});`);
856
+ return data.map((entry) => {
857
+ return {
858
+ fromTable: tableName,
859
+ fromColumn: entry["from"],
860
+ toTable: entry["table"],
861
+ toColumn: entry["to"],
862
+ on_update: entry["on_update"],
863
+ on_delete: entry["on_delete"]
864
+ };
865
+ });
956
866
  }
957
- /**
958
- * Handles the renaming of columns in database tables by generating SQL changes for both applying and reverting the renames.
959
- * Skips tables that are marked for recreation.
960
- */
961
- renameColumns() {
962
- const changes = {
963
- up: "\n\n-- Rename columns\n",
964
- down: "\n\n-- Rename columns\n"
965
- };
966
- const migrationData = this.migrations.getMigrationData();
967
- for (const tableName in migrationData) {
968
- if (migrationData[tableName].recreate)
969
- continue;
970
- this.migrations.loopRenamedColumns(tableName, (oldColumnName, newColumnName) => {
971
- changes.up += this.dialect.renameColumn(tableName, oldColumnName, newColumnName);
972
- changes.down += this.dialect.renameColumn(tableName, newColumnName, oldColumnName);
973
- });
974
- }
975
- return changes;
867
+ async getVersion() {
868
+ const output = await this.db.runGetStatement(`PRAGMA user_version;`);
869
+ return output[0].user_version;
870
+ }
871
+ async setVersion(newVersion) {
872
+ await this.db.runMultipleWriteStatements(`PRAGMA user_version = ${newVersion};`);
976
873
  }
977
874
  };
978
875
 
979
- // src/lib/tableInfo/decorators/DbTable.ts
980
- var import_polyfill_symbol_metadata = require("polyfill-symbol-metadata");
981
- function DbTable(tableName, primaryKey) {
982
- return (table, context) => {
983
- Object.defineProperty(table, "name", { value: tableName, writable: false });
984
- const tableInfo = context?.metadata ? getTableInfoFromMetadata(context.metadata) : getTableInfo(table);
985
- tableInfo.primaryKey = primaryKey?.toString();
986
- if (tableInfo.foreignKeys) {
987
- for (const foreignKey of tableInfo.foreignKeys) {
988
- foreignKey.fromTable = tableName;
989
- }
990
- }
991
- };
992
- }
876
+ // src/lib/dialects/PostgresDialect.ts
877
+ var PostgresDialect = class extends DefaultSql {
878
+ canAlterForeignKeys = true;
879
+ canAlterPrimaryKey = true;
880
+ canInspectForeignKeys = false;
881
+ canInspectPrimaryKey = false;
882
+ changeForeignKeysState(enabled) {
883
+ return `SET session_replication_role = '${enabled ? "replica" : "origin"}';`;
884
+ }
885
+ addForeignKey(fromTableName, foreignKey) {
886
+ return `ALTER TABLE ${fromTableName} ADD CONSTRAINT ${fromTableName} ${foreignKey};`;
887
+ }
888
+ removeForeignKey(tableName, foreignKeyName) {
889
+ return `ALTER TABLE ${tableName} DROP CONSTRAINT ${foreignKeyName};`;
890
+ }
891
+ async getTableNames() {
892
+ return await this.db.runGetStatement("SELECT table_name FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE';");
893
+ }
894
+ async getColumnInformation(tableName) {
895
+ const data = await this.db.runGetStatement(`SELECT column_name, data_type, column_default FROM information_schema.columns WHERE table_schema = 'public' AND table_name = '${tableName}';`);
896
+ return data.map((entry) => {
897
+ return {
898
+ name: entry["column_name"],
899
+ type: entry["data_type"],
900
+ defaultValue: entry["column_default"],
901
+ isPrimaryKey: false
902
+ };
903
+ });
904
+ }
905
+ addPrimaryKey(tableName, columnName) {
906
+ return `ALTER TABLE ${tableName} ADD PRIMARY KEY (${columnName});
907
+ `;
908
+ }
909
+ removePrimaryKey(tableName, columnName) {
910
+ return `ALTER TABLE ${tableName} DROP CONSTRAINT ${columnName};
911
+ `;
912
+ }
913
+ };
993
914
 
994
- // src/lib/tableInfo/decorators/ForeignKey.ts
995
- var import_polyfill_symbol_metadata2 = require("polyfill-symbol-metadata");
996
- function ForeignKey(toTable, toColumn, onDelete, onUpdate) {
997
- return (table, context) => {
998
- const metadata = context?.metadata ? getTableInfoFromMetadata(context.metadata) : getTableInfo(table);
999
- if (!metadata.foreignKeys)
1000
- metadata.foreignKeys = [];
1001
- metadata.foreignKeys.push({
1002
- fromTable: "",
1003
- fromColumn: context.name ?? context,
1004
- //to stay compatible with old decorator structure (see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#decorator-metadata)
1005
- toTable: toTable.name,
1006
- toColumn: toColumn.toString(),
1007
- onDelete,
1008
- onUpdate
915
+ // src/lib/dialects/MsSqlDialect.ts
916
+ var MsSqlDialect = class extends DefaultSql {
917
+ canAlterForeignKeys = true;
918
+ canAlterPrimaryKey = true;
919
+ canInspectForeignKeys = true;
920
+ canInspectPrimaryKey = false;
921
+ changeForeignKeysState(enabled) {
922
+ return `EXEC sp_MSforeachtable "${enabled ? "ALTER TABLE ? WITH CHECK CHECK CONSTRAINT all" : "ALTER TABLE ? NOCHECK CONSTRAINT all"}"`;
923
+ }
924
+ addForeignKey(fromTableName, foreignKey) {
925
+ return `ALTER TABLE ${fromTableName} ADD ${foreignKey};`;
926
+ }
927
+ removeForeignKey(tableName, foreignKeyName) {
928
+ return `ALTER TABLE ${tableName} DROP CONSTRAINT ${foreignKeyName};`;
929
+ }
930
+ addPrimaryKey(tableName, columnName) {
931
+ return `ALTER TABLE ${tableName} ADD CONSTRAINT ${tableName}_primary PRIMARY KEY (${columnName});`;
932
+ }
933
+ removePrimaryKey(tableName, columnName) {
934
+ return `ALTER TABLE ${tableName} DROP CONSTRAINT ${columnName}_primary;`;
935
+ }
936
+ async getForeignKeys(tableName) {
937
+ const data = await this.db.runGetStatement(`EXEC sp_fkeys @fktable_name = ${tableName};`);
938
+ return data.map((entry) => {
939
+ return {
940
+ fromTable: entry[tableName],
941
+ fromColumn: entry["FKCOLUMN_NAME"],
942
+ toTable: entry["PKTABLE_NAME"],
943
+ toColumn: entry["PKCOLUMN_NAME"]
944
+ };
1009
945
  });
1010
- };
946
+ }
947
+ async getTableNames() {
948
+ return await this.db.runGetStatement("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'");
949
+ }
950
+ async getColumnInformation(tableName) {
951
+ const data = await this.db.runGetStatement(`SELECT COLUMN_NAME, DATA_TYPE, COLUMN_DEFAULT FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = N'${tableName};'`);
952
+ return data.map((entry) => {
953
+ return {
954
+ name: entry["COLUMN_NAME"],
955
+ type: entry["DATA_TYPE"],
956
+ defaultValue: entry["COLUMN_DEFAULT"],
957
+ isPrimaryKey: false
958
+ };
959
+ });
960
+ }
961
+ };
962
+
963
+ // src/lib/dialects/MySqlDialect.ts
964
+ var MySqlDialect = class extends DefaultSql {
965
+ canAlterForeignKeys = true;
966
+ canAlterPrimaryKey = true;
967
+ canInspectForeignKeys = true;
968
+ canInspectPrimaryKey = true;
969
+ changeForeignKeysState(enabled) {
970
+ return `SET FOREIGN_KEY_CHECKS ${enabled ? "1" : "0"}
971
+ ;`;
972
+ }
973
+ addForeignKey(fromTableName, foreignKey) {
974
+ return `ALTER TABLE ${fromTableName} ADD ${foreignKey};`;
975
+ }
976
+ removeForeignKey(tableName, foreignKeyName) {
977
+ return `ALTER TABLE ${tableName} DROP FOREIGN KEY ${foreignKeyName};`;
978
+ }
979
+ addPrimaryKey(tableName, columnName) {
980
+ return `ALTER TABLE ${tableName} PRIMARY KEY (${columnName});`;
981
+ }
982
+ removePrimaryKey(tableName, _columnName) {
983
+ return `ALTER TABLE ${tableName} PRIMARY KEY;`;
984
+ }
985
+ async getForeignKeys(tableName) {
986
+ const data = await this.db.runGetStatement(`SELECT TABLE_NAME,COLUMN_NAME,CONSTRAINT_NAME, REFERENCED_TABLE_NAME,REFERENCED_COLUMN_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE REFERENCED_TABLE_SCHEMA = (SELECT DATABASE()) AND REFERENCED_TABLE_NAME = '${tableName}';`);
987
+ return data.map((entry) => {
988
+ return {
989
+ fromTable: tableName,
990
+ fromColumn: entry["COLUMN_NAME"],
991
+ toTable: entry["REFERENCED_TABLE_NAME"],
992
+ toColumn: entry["REFERENCED_COLUMN_NAME"]
993
+ };
994
+ });
995
+ }
996
+ async getTableNames() {
997
+ return await this.db.runGetStatement("SHOW TABLES;");
998
+ }
999
+ async getColumnInformation(tableName) {
1000
+ const data = await this.db.runGetStatement(`SHOW COLUMNS FROM ${tableName};`);
1001
+ return data.map((entry) => {
1002
+ return {
1003
+ name: entry["Field"],
1004
+ type: entry["Type"],
1005
+ defaultValue: entry["Default"],
1006
+ isPrimaryKey: entry["Key"] == "PRI"
1007
+ };
1008
+ });
1009
+ }
1010
+ };
1011
+
1012
+ // src/lib/getDialect.ts
1013
+ function getDialect(db, dbInstructions) {
1014
+ switch (dbInstructions.dialect) {
1015
+ case "Sqlite":
1016
+ return new SqliteDialect(db);
1017
+ case "Postgres":
1018
+ return new PostgresDialect(db);
1019
+ case "MsSql":
1020
+ return new MsSqlDialect(db);
1021
+ case "MySql":
1022
+ return new MySqlDialect(db);
1023
+ default:
1024
+ throw new Error(`Unknown dialect ${dbInstructions.dialect}`);
1025
+ }
1011
1026
  }
1012
1027
 
1013
1028
  // src/index.ts
1014
- async function prepareMigration(db, instructions, overwriteExisting) {
1015
- Logger.setMode(instructions.loggerMode);
1016
- const mm = new MigrationManager(db, instructions);
1017
- await mm.prepareMigration(overwriteExisting);
1029
+ async function prepareMigration(db, dbInstructions, overwriteExisting) {
1030
+ Logger.setMode(dbInstructions.loggerMode);
1031
+ const dialect = getDialect(db, dbInstructions);
1032
+ const mm = new MigrationManager(dialect);
1033
+ const migration = await mm.generateSqlChanges(dbInstructions);
1034
+ if (!migration)
1035
+ return;
1036
+ const migrationHistoryManager = new MigrationHistoryManager(dbInstructions.configPath);
1037
+ migrationHistoryManager.createMigrationHistory(migration.fromVersion, dbInstructions.version, migration.changes, overwriteExisting);
1018
1038
  }
1019
- async function runPreparedMigrations(db, instructions) {
1020
- Logger.setMode(instructions.loggerMode);
1021
- const migrationHistoryManager = new MigrationHistoryManager(instructions.configPath);
1022
- const dialect = getDialect(instructions);
1023
- const fromVersion = await dialect.getVersion(db);
1024
- const toVersion = instructions.version;
1039
+ async function runPreparedMigrations(db, dbInstructions) {
1040
+ Logger.setMode(dbInstructions.loggerMode);
1041
+ const historyManager = new MigrationHistoryManager(dbInstructions.configPath);
1042
+ const dialect = getDialect(db, dbInstructions);
1043
+ const fromVersion = await dialect.getVersion();
1044
+ const toVersion = dbInstructions.version;
1025
1045
  if (fromVersion == toVersion)
1026
1046
  return;
1047
+ await db.createBackup?.(`from_${fromVersion}_to_${dbInstructions.version}`);
1027
1048
  Logger.log(`Running migrations from ${fromVersion} to ${toVersion}`);
1028
1049
  for (let i = fromVersion ? fromVersion + 1 : toVersion; i <= toVersion; ++i) {
1029
- const upChanges = migrationHistoryManager.getUpMigration(i);
1050
+ const upChanges = historyManager.getUpMigration(i);
1030
1051
  Logger.debug(upChanges);
1031
1052
  await db.runMultipleWriteStatements(upChanges);
1032
- instructions.version = i;
1053
+ dbInstructions.version = i;
1033
1054
  }
1034
- await dialect.setVersion(db, toVersion);
1055
+ await dialect.setVersion(toVersion);
1035
1056
  }
1036
1057
  async function prepareAndRunMigration(db, instructions, overwriteExisting) {
1037
1058
  Logger.setMode(instructions.loggerMode);
1038
1059
  await prepareMigration(db, instructions, overwriteExisting);
1039
1060
  await runPreparedMigrations(db, instructions);
1040
1061
  }
1041
- async function runMigrationWithoutHistory(db, instructions) {
1042
- Logger.setMode(instructions.loggerMode);
1043
- const mm = new MigrationManager(db, instructions);
1044
- const changes = await mm.getMigrateSql();
1045
- if (changes)
1046
- await db.runMultipleWriteStatements(changes.up);
1062
+ async function runMigrationWithoutHistory(db, dbInstructions) {
1063
+ Logger.setMode(dbInstructions.loggerMode);
1064
+ const dialect = getDialect(db, dbInstructions);
1065
+ const mm = new MigrationManager(dialect);
1066
+ const migration = await mm.generateSqlChanges(dbInstructions);
1067
+ if (migration) {
1068
+ await db.createBackup?.(`from_${migration.fromVersion}_to_${dbInstructions.version}`);
1069
+ await db.runMultipleWriteStatements(migration.changes.up);
1070
+ }
1047
1071
  }
1048
- async function rollback(db, instructions, toVersion) {
1049
- Logger.setMode(instructions.loggerMode);
1050
- const migrationHistoryManager = new MigrationHistoryManager(instructions.configPath);
1051
- const dialect = getDialect(instructions);
1052
- const fromVersion = await dialect.getVersion(db);
1072
+ async function rollback(db, dbInstructions, toVersion) {
1073
+ Logger.setMode(dbInstructions.loggerMode);
1074
+ const dialect = getDialect(db, dbInstructions);
1075
+ const fromVersion = await dialect.getVersion();
1076
+ const historyManager = new MigrationHistoryManager(dbInstructions.configPath);
1077
+ await db.createBackup?.(`from_${fromVersion}_to_${dbInstructions.version}`);
1053
1078
  Logger.log(`Rolling back from ${fromVersion} to ${toVersion}`);
1054
1079
  for (let i = fromVersion - 1; i >= toVersion; --i) {
1055
- const upChanges = migrationHistoryManager.getDownMigration(i);
1080
+ const upChanges = historyManager.getDownMigration(i);
1056
1081
  Logger.debug(upChanges);
1057
1082
  await db.runMultipleWriteStatements(upChanges);
1058
- instructions.version = i;
1083
+ dbInstructions.version = i;
1059
1084
  }
1060
- await dialect.setVersion(db, toVersion);
1085
+ await dialect.setVersion(toVersion);
1061
1086
  }
1062
1087
  // Annotate the CommonJS export names for ESM import in node:
1063
1088
  0 && (module.exports = {