sqlmorpheus 0.2.7 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,788 +1,768 @@
1
- // src/lib/Logger.ts
1
+ import escapeString from "escape-sql-string";
2
+ import "polyfill-symbol-metadata";
3
+ //#region src/lib/Logger.ts
4
+ /**
5
+ * A simple LoggerClass which provides functionality for logging messages
6
+ * in different modes such as normal, debug, silent, and noLog.
7
+ * It allows controlling the verbosity of logs based on the mode.
8
+ */
2
9
  var LoggerClass = class {
3
- mode = "log";
4
- setMode(mode) {
5
- this.mode = mode ?? "log";
6
- }
7
- debug(text) {
8
- if (this.mode == "debug") {
9
- console.debug(`\x1B[90m${(/* @__PURE__ */ new Date()).toLocaleTimeString()}\x1B[0m ${text}`);
10
- }
11
- }
12
- log(text) {
13
- if (this.mode == "log" || this.mode == "debug") {
14
- console.log(`\x1B[90m${(/* @__PURE__ */ new Date()).toLocaleTimeString()}\x1B[0m ${text}`);
15
- }
16
- }
17
- warn(text) {
18
- if (this.mode == "warn" || this.mode == "log" || this.mode == "debug") {
19
- console.warn(`\x1B[90m${(/* @__PURE__ */ new Date()).toLocaleTimeString()}\x1B[0m ${text}`);
20
- }
21
- }
22
- error(text) {
23
- if (this.mode == "error" || this.mode == "warn" || this.mode == "log" || this.mode == "debug") {
24
- console.error(`\x1B[90m${(/* @__PURE__ */ new Date()).toLocaleTimeString()}\x1B[0m ${text}`);
25
- }
26
- }
10
+ mode = "log";
11
+ setMode(mode) {
12
+ this.mode = mode ?? "log";
13
+ }
14
+ debug(text) {
15
+ if (this.mode == "debug") console.debug(`\x1b[90m${(/* @__PURE__ */ new Date()).toLocaleTimeString()}\x1b[0m ${text}`);
16
+ }
17
+ log(text) {
18
+ if (this.mode == "log" || this.mode == "debug") console.log(`\x1b[90m${(/* @__PURE__ */ new Date()).toLocaleTimeString()}\x1b[0m ${text}`);
19
+ }
20
+ warn(text) {
21
+ if (this.mode == "warn" || this.mode == "log" || this.mode == "debug") console.warn(`\x1b[90m${(/* @__PURE__ */ new Date()).toLocaleTimeString()}\x1b[0m ${text}`);
22
+ }
23
+ error(text) {
24
+ if (this.mode == "error" || this.mode == "warn" || this.mode == "log" || this.mode == "debug") console.error(`\x1b[90m${(/* @__PURE__ */ new Date()).toLocaleTimeString()}\x1b[0m ${text}`);
25
+ }
27
26
  };
28
- var Logger = new LoggerClass();
29
-
30
- // src/lib/tableInfo/TableObjHelper.ts
31
- var TableObjHelper = class _TableObjHelper {
32
- static isTableObj(obj) {
33
- return !!obj?.IS_TABLE_OBJ;
34
- }
35
- static getEmptyColumnEntry(key) {
36
- return {
37
- name: key,
38
- sqlType: "",
39
- defaultValue: "",
40
- isPrimaryKey: false
41
- };
42
- }
43
- static getColumnEntry(key, value) {
44
- let dataType = typeof value;
45
- switch (typeof value) {
46
- case "function":
47
- return null;
48
- case "object":
49
- if (value instanceof Date) {
50
- dataType = "date";
51
- break;
52
- } else if (value !== null) {
53
- Logger.warn(`${key} was skipped because its type (${typeof value}) is not supported`);
54
- return null;
55
- }
56
- dataType = "string";
57
- break;
58
- case "undefined":
59
- case "symbol":
60
- Logger.warn(`${key} was skipped because its type (${typeof value}) is not supported`);
61
- return null;
62
- }
63
- return {
64
- ..._TableObjHelper.getEmptyColumnEntry(key),
65
- inputType: dataType,
66
- inputDefaultValue: value
67
- };
68
- }
27
+ const Logger = new LoggerClass();
28
+ //#endregion
29
+ //#region src/lib/tableInfo/TableObjHelper.ts
30
+ var TableObjHelper = class TableObjHelper {
31
+ static isTableObj(obj) {
32
+ return !!obj?.IS_TABLE_OBJ;
33
+ }
34
+ static getEmptyColumnEntry(key) {
35
+ return {
36
+ name: key,
37
+ sqlType: "",
38
+ defaultValue: "",
39
+ isPrimaryKey: false
40
+ };
41
+ }
42
+ static getColumnEntry(key, value) {
43
+ let dataType = typeof value;
44
+ switch (typeof value) {
45
+ case "function": return null;
46
+ case "object":
47
+ if (value instanceof Date) {
48
+ dataType = "date";
49
+ break;
50
+ } else if (value !== null) {
51
+ Logger.warn(`${key} was skipped because its type (${typeof value}) is not supported`);
52
+ return null;
53
+ }
54
+ dataType = "string";
55
+ break;
56
+ case "undefined":
57
+ case "symbol":
58
+ Logger.warn(`${key} was skipped because its type (${typeof value}) is not supported`);
59
+ return null;
60
+ }
61
+ return {
62
+ ...TableObjHelper.getEmptyColumnEntry(key),
63
+ inputType: dataType,
64
+ inputDefaultValue: value
65
+ };
66
+ }
69
67
  };
70
-
71
- // src/lib/tableInfo/getTableStructure.ts
72
- var TABLE_INFO_PROPERTY_NAME = Symbol("tableInfo");
68
+ //#endregion
69
+ //#region src/lib/tableInfo/getTableStructure.ts
70
+ const TABLE_INFO_PROPERTY_NAME = Symbol("tableInfo");
73
71
  function getInputColumnInfo(table, context) {
74
- const metadata = context?.metadata ? getTableStructureFromMetadata(context.metadata) : getTableStructure(table);
75
- const key = context.name ?? context;
76
- if (!metadata.columns.hasOwnProperty(key))
77
- metadata.columns[key] = TableObjHelper.getEmptyColumnEntry(key);
78
- return metadata.columns[key];
72
+ const metadata = context?.metadata ? getTableStructureFromMetadata(context.metadata) : getTableStructure(table);
73
+ const key = context.name ?? context;
74
+ if (!metadata.columns.hasOwnProperty(key)) metadata.columns[key] = TableObjHelper.getEmptyColumnEntry(key);
75
+ return metadata.columns[key];
79
76
  }
77
+ /**
78
+ * Retrieves table information based on the provided class and their decorators.
79
+ * If the table metadata property does not exist in the metadata,
80
+ * it initializes a default structure.
81
+ *
82
+ * @param table - The class representing the table.
83
+ * @return The table information as a `TableInfo` object.
84
+ */
80
85
  function getTableStructure(table) {
81
- let metadata = table[Symbol.metadata] ?? table.prototype?.[Symbol.metadata];
82
- if (!metadata) {
83
- metadata = {};
84
- table[Symbol.metadata] = metadata;
85
- }
86
- return getTableStructureFromMetadata(metadata);
86
+ let metadata = table[Symbol.metadata] ?? table.prototype?.[Symbol.metadata];
87
+ if (!metadata) {
88
+ metadata = {};
89
+ table[Symbol.metadata] = metadata;
90
+ }
91
+ return getTableStructureFromMetadata(metadata);
87
92
  }
93
+ /**
94
+ * Extracts and returns table information from the provided metadata object.
95
+ * If the specified table information property does not exist in the metadata,
96
+ * it initializes a default structure.
97
+ *
98
+ * @param metadata - The metadata object which potentially contains table information.
99
+ * @return {InputTableStructure} The table information retrieved or initialized from the metadata.
100
+ */
88
101
  function getTableStructureFromMetadata(metadata) {
89
- if (!metadata[TABLE_INFO_PROPERTY_NAME])
90
- metadata[TABLE_INFO_PROPERTY_NAME] = { table: "", primaryKey: "", foreignKeys: [], columns: {} };
91
- return metadata[TABLE_INFO_PROPERTY_NAME];
102
+ if (!metadata[TABLE_INFO_PROPERTY_NAME]) metadata[TABLE_INFO_PROPERTY_NAME] = {
103
+ table: "",
104
+ primaryKey: "",
105
+ foreignKeys: [],
106
+ columns: {}
107
+ };
108
+ return metadata[TABLE_INFO_PROPERTY_NAME];
92
109
  }
93
-
94
- // src/lib/TableStructureGenerator.ts
110
+ //#endregion
111
+ //#region src/lib/TableStructureGenerator.ts
112
+ /**
113
+ * Represents a generator for creating table structures based on specified database instructions
114
+ */
95
115
  var TableStructureGenerator = class {
96
- dialect;
97
- dbInstructions;
98
- constructor(dbInstructions, dialect) {
99
- this.dialect = dialect;
100
- this.dbInstructions = dbInstructions;
101
- }
102
- /**
103
- * Generates the structure of tables based on database instructions.
104
- *
105
- * @return An object where keys are table names, and values are their corresponding table structures.
106
- */
107
- generateTableStructure() {
108
- const tables = {};
109
- for (const table of this.dbInstructions.tables) {
110
- let structure;
111
- if (TableObjHelper.isTableObj(table)) {
112
- structure = table.tableStructure;
113
- tables[table.tableName] = table.tableStructure;
114
- } else {
115
- const classTable = table;
116
- structure = getTableStructure(classTable);
117
- tables[classTable.name] = structure;
118
- }
119
- for (const key in structure.columns) {
120
- const entry = structure.columns[key];
121
- entry.defaultValue = entry.inputDefaultValue === null ? this.dialect.nullType : this.dialect.formatValueToSql(entry.inputDefaultValue, entry.inputType);
122
- entry.sqlType = this.dialect.getSqlType(entry.inputType, entry);
123
- }
124
- }
125
- return tables;
126
- }
116
+ dialect;
117
+ dbInstructions;
118
+ constructor(dbInstructions, dialect) {
119
+ this.dialect = dialect;
120
+ this.dbInstructions = dbInstructions;
121
+ }
122
+ /**
123
+ * Generates the structure of tables based on database instructions.
124
+ *
125
+ * @return An object where keys are table names, and values are their corresponding table structures.
126
+ */
127
+ generateTableStructure() {
128
+ const tables = {};
129
+ for (const table of this.dbInstructions.tables) {
130
+ let structure;
131
+ if (TableObjHelper.isTableObj(table)) {
132
+ structure = table.tableStructure;
133
+ tables[table.tableName] = table.tableStructure;
134
+ } else {
135
+ const classTable = table;
136
+ structure = getTableStructure(classTable);
137
+ tables[classTable.name] = structure;
138
+ }
139
+ for (const key in structure.columns) {
140
+ const entry = structure.columns[key];
141
+ entry.defaultValue = entry.inputDefaultValue === null ? this.dialect.nullType : this.dialect.formatValueToSql(entry.inputDefaultValue, entry.inputType);
142
+ entry.sqlType = this.dialect.getSqlType(entry.inputType, entry);
143
+ }
144
+ }
145
+ return tables;
146
+ }
127
147
  };
128
-
129
- // src/lib/typings/AllowedMigrations.ts
130
- var ALLOWED = "allowed";
131
- var USED = "allowedAndUsed";
132
- var NO_COLUMN = "noColumn";
133
-
134
- // src/lib/exceptions/NotAllowedException.ts
148
+ //#endregion
149
+ //#region src/lib/typings/AllowedMigrations.ts
150
+ const ALLOWED = "allowed";
151
+ const USED = "allowedAndUsed";
152
+ const NO_COLUMN = "noColumn";
153
+ //#endregion
154
+ //#region src/lib/exceptions/NotAllowedException.ts
135
155
  var NotAllowedException = class extends Error {
136
- constructor(entries) {
137
- let message = "Some destructive changes need to be confirmed. To confirm, add to preMigration():\n";
138
- for (const entry of entries) {
139
- message += ` migrations.allowMigration(${entry.version}, "${entry.tableName}", "${entry.type}"${entry.column != NO_COLUMN ? `, "${entry.column}"` : ""});
140
- `;
141
- }
142
- super(message);
143
- }
156
+ constructor(entries) {
157
+ let message = "Some destructive changes need to be confirmed. To confirm, add to preMigration():\n";
158
+ for (const entry of entries) message += `\t\tmigrations.allowMigration(${entry.version}, "${entry.tableName}", "${entry.type.toString()}"${entry.column != "noColumn" ? `, "${entry.column}"` : ""});\n`;
159
+ super(message);
160
+ }
144
161
  };
145
-
146
- // src/lib/Migrations.ts
162
+ //#endregion
163
+ //#region src/lib/Migrations.ts
164
+ /**
165
+ * The Migrations class is responsible for managing manual database schema migrations.
166
+ */
147
167
  var Migrations = class {
148
- fromVersion = 0;
149
- toVersion = 0;
150
- lastUsedVersion = 0;
151
- migrationData = {};
152
- migrationDataForOldTableNames = {};
153
- alwaysAllowed = [];
154
- notAllowedChanges = [];
155
- /**
156
- * Resets the migration state to its initial values.
157
- * Only used internally.
158
- *
159
- * @param dbInstructions - The instructions containing database metadata and migration details.
160
- * @param fromVersion - The version of the actual database.
161
- */
162
- reset(dbInstructions, fromVersion) {
163
- this.fromVersion = fromVersion;
164
- this.toVersion = dbInstructions.version;
165
- this.migrationData = {};
166
- this.migrationDataForOldTableNames = {};
167
- this.alwaysAllowed = dbInstructions.alwaysAllowedMigrations ?? [];
168
- this.notAllowedChanges = [];
169
- }
170
- /**
171
- * Retrieves the migration instructions for the specified table.
172
- *
173
- * @param tableName - The name of the table for which the entry is to be retrieved or created.
174
- * @return The migration instructions corresponding to the given table input.
175
- */
176
- getEntryFromTableName(tableName) {
177
- if (!this.migrationData.hasOwnProperty(tableName)) {
178
- this.migrationData[tableName] = {
179
- recreate: false,
180
- renamedColumns: [],
181
- allowedMigrations: {}
182
- };
183
- }
184
- return this.migrationData[tableName];
185
- }
186
- /**
187
- * Checks whether a provided version is relevant based on database version ({@link fromVersion}) and target version ({@link toVersion}).
188
- * Also makes sure that version checks happen in order.
189
- *
190
- * @param version - The version to check for relevance.
191
- * @return Returns true if the version is relevant, otherwise false.
192
- * @throws Error if versions are not properly ordered
193
- */
194
- versionIsRelevant(version) {
195
- if (version <= this.fromVersion || version > this.toVersion) {
196
- return false;
197
- }
198
- if (version < this.lastUsedVersion) {
199
- throw new Error("Migrations in preMigration() have to be ordered by version (beginning with the lowest version).");
200
- }
201
- this.lastUsedVersion = version;
202
- return true;
203
- }
204
- /**
205
- * Verifies the renaming tasks specified in {@link DatabaseInstructions.preMigration} that were considered are valid.
206
- * Called after all SQL queries have been generated.
207
- * Only used internally.
208
- *
209
- * @param newTables - The table structures extracted from {@link DatabaseInstructions.tables}.
210
- * @throws Error if a renaming task is invalid.
211
- */
212
- verifyRenamingTasks(newTables) {
213
- for (const tableName in this.migrationData) {
214
- const migrationEntry = this.migrationData[tableName];
215
- if (migrationEntry.tableRenaming) {
216
- if (migrationEntry.tableRenaming.oldName == tableName) {
217
- return new Error(`You set table "${tableName}" to be renamed to itself!`);
218
- }
219
- if (migrationEntry.tableRenaming.newName != tableName) {
220
- return new Error(`You set table "${tableName}" to be renamed to a different name than the one in your structure (${migrationEntry.tableRenaming.newName})!`);
221
- } else if (!newTables[tableName]) {
222
- return new Error(`You set migrations for "${tableName}". But "${tableName}" does not exist in your structure.`);
223
- }
224
- }
225
- for (const renamingData of migrationEntry.renamedColumns) {
226
- const oldColumnName = renamingData.oldName;
227
- const newColumnName = renamingData.newName;
228
- if (oldColumnName == newColumnName) {
229
- return new Error(`You set column ${tableName}.${oldColumnName} to be renamed to itself!`);
230
- } else if (!newTables[tableName]?.columns[newColumnName]) {
231
- return new Error(`You set column ${tableName}.${oldColumnName} to be renamed to ${tableName}.${newColumnName}. But ${tableName}.${newColumnName} does not exist in your structure.`);
232
- }
233
- }
234
- }
235
- }
236
- /**
237
- * Making sure all considered calls to {@link allowMigration()} where necessary to prevent input mistakes.
238
- * Called after all SQL queries have been generated.
239
- * Only used internally.
240
- *
241
- * @throws Error if unnecessary calls were discovered. All discovered calls are collected and thrown as one Error.
242
- */
243
- verifyAllowedMigrations() {
244
- if (this.notAllowedChanges.length) {
245
- return new NotAllowedException(this.notAllowedChanges);
246
- }
247
- let errorMsg = "";
248
- for (const tableName in this.migrationData) {
249
- const migration = this.migrationData[tableName];
250
- for (const type in migration.allowedMigrations) {
251
- const migrationType = migration.allowedMigrations[type];
252
- for (const column in migrationType) {
253
- if (migrationType[column] != USED) {
254
- errorMsg += `Migration "${type}" for ${tableName}${column != NO_COLUMN ? `.${column}` : ""} was allowed but not needed.
255
- `;
256
- }
257
- }
258
- }
259
- }
260
- if (errorMsg) {
261
- return new Error(errorMsg);
262
- }
263
- }
264
- /**
265
- * Called by {@link MigrationManager} to check if a specific destructive query is allowed.
266
- * Only used internally.
267
- *
268
- * @see allowMigration()
269
- * @param tableName - The name of the table to check the migration against.
270
- * @param type - The type of destructive migration being checked.
271
- * @param column - The column name for which the migration is allowed. Only needed for "dropColumn", "removeForeignKey" and "alterForeignKey".
272
- */
273
- compareWithAllowedMigration(tableName, type, column = NO_COLUMN) {
274
- const migrationEntry = this.migrationData[tableName];
275
- const allowedMigrations = migrationEntry?.allowedMigrations[type];
276
- if (!allowedMigrations || !allowedMigrations[column]) {
277
- if (!this.alwaysAllowed.includes(type)) {
278
- this.notAllowedChanges.push({ version: this.toVersion, tableName, column, type });
279
- }
280
- } else if (migrationEntry) {
281
- allowedMigrations[column] = USED;
282
- }
283
- }
284
- /**
285
- * Retrieves the migration data containing instructions for data migration.
286
- * Only used internally.
287
- *
288
- * @return An object where each key corresponds to the table name,
289
- * and the value is the associated migration instructions.
290
- */
291
- getMigrationData() {
292
- return this.migrationData;
293
- }
294
- /**
295
- * Iterates over renamed columns for the specified table and executes a callback for each pair of old and new column names.
296
- * Only used internally.
297
- *
298
- * @param tableName - Name of the table whose renamed columns should be processed.
299
- * @param callback - A function to execute for each renamed column, receiving the old and new column names as arguments.
300
- */
301
- loopRenamedColumns(tableName, callback) {
302
- const migrationEntry = this.migrationData[tableName];
303
- for (const renamingData of migrationEntry.renamedColumns) {
304
- callback(renamingData.oldName, renamingData.newName);
305
- }
306
- }
307
- /**
308
- * Considers the renaming of a table and returns its (newest) name after the migration or
309
- * the original name if no renaming is queued.
310
- * Only used internally.
311
- *
312
- * @see getOldTableName()
313
- * @param tableName - The current table name in the database.
314
- * @return The newest table name if available, otherwise the current table name.
315
- */
316
- getNewestTableName(tableName) {
317
- return this.migrationDataForOldTableNames[tableName]?.tableRenaming?.newName ?? tableName;
318
- }
319
- /**
320
- * Retrieves the current table name in the database.
321
- * Only used internally.
322
- *
323
- * @see getNewestTableName()
324
- * @param tableName - The name of the newest table name.
325
- * @return The current table name in the database.
326
- */
327
- getOldTableName(tableName) {
328
- return this.migrationData[tableName]?.tableRenaming?.oldName ?? tableName;
329
- }
330
- /**
331
- * Considers the renaming of a column in a table and returns its (newest) name after the migration or
332
- * the original name if no renaming is queued.
333
- * Only used internally.
334
- *
335
- * @param tableName - The name of the table containing the column.
336
- * @param oldColumnName - The current name of the column in the database.
337
- * @return The newest column name if available, otherwise the original column name.
338
- */
339
- getNewestColumnName(tableName, oldColumnName) {
340
- const columnNames = this.migrationData[tableName]?.renamedColumns.find((columns) => columns.oldName == oldColumnName);
341
- return columnNames ? columnNames.newName : oldColumnName;
342
- }
343
- /**
344
- * Determines if the specified table is marked to be recreated.
345
- * Only used internally.
346
- *
347
- * @param tableName The name of the table to check.
348
- * @return Returns true if the table will be recreated, false otherwise.
349
- */
350
- willBeRecreated(tableName) {
351
- return this.migrationData[tableName]?.recreate;
352
- }
353
- /**
354
- * Marks a table to be recreated and logs the recreation reason for the user.
355
- * Only used internally.
356
- *
357
- * @param tableName - The name of the table to be marked for recreation.
358
- * @param reason - The reason why the table is being marked for recreation.
359
- */
360
- internalRecreate(tableName, reason) {
361
- const entry = this.getEntryFromTableName(tableName);
362
- entry.recreate = true;
363
- Logger.log(`Table ${tableName} will be recreated (${reason})!`);
364
- }
365
- /**
366
- * SqlMorpheus only allows destructive statements if it was specifically allowed in {@link DatabaseInstructions.preMigration()}.
367
- * Use this method to allow a destructive statement for a specific version.
368
- * This method will be ignored if {@link version} is smaller than the current database version or higher than the target version of {@link DatabaseInstructions}.
369
- *
370
- * @param version - The target version for which the specific migration is allowed. Must be provided to make sure this method will only have an effect when specified.
371
- * @param tableName - The current table name in the database at the time of the migration version.
372
- * @param allowedMigration - The keys representing the migrations to be allowed.
373
- * @param column - The column name for which the migration is allowed. Only needed for "dropColumn", "removeForeignKey" and "alterForeignKey".
374
- *
375
- * @throws Error if the provided column name is not valid for the specified migration.
376
- */
377
- allowMigration(version, tableName, allowedMigration, column) {
378
- if (!this.versionIsRelevant(version)) {
379
- return;
380
- }
381
- const entry = this.getEntryFromTableName(tableName);
382
- if (!entry.allowedMigrations[allowedMigration]) {
383
- entry.allowedMigrations[allowedMigration] = {};
384
- }
385
- entry.allowedMigrations[allowedMigration][column ?? NO_COLUMN] = ALLOWED;
386
- if (column) {
387
- switch (allowedMigration) {
388
- case "recreateTable":
389
- case "dropTable":
390
- case "alterPrimaryKey":
391
- throw new Error(`You cannot use the ${allowedMigration} migration with a column name. Please remove the column name.`);
392
- }
393
- } else {
394
- switch (allowedMigration) {
395
- case "dropColumn":
396
- case "removeForeignKey":
397
- case "alterForeignKey":
398
- throw new Error(`The ${allowedMigration} migration requires a column name. Please provide one.`);
399
- }
400
- }
401
- }
402
- /**
403
- * When you change the name of a table, SqlMorpheus will assume the original table was meant to be dropped
404
- * and the new table was meant to be added, which would lead to data loss.
405
- * Use this method to mark a table to be renamed instead.
406
- * This method will be ignored if {@link version} is smaller than the current database version or higher than the target version of {@link DatabaseInstructions}.
407
- *
408
- * @param version - The target version for which the table should be renamed. Must be provided to make sure this method will only have an effect when specified.
409
- * @param oldTableName - The current name in the database at the time of the migration version.
410
- * @param newTableName - The new table name at the time of the migration version.
411
- */
412
- renameTable(version, oldTableName, newTableName) {
413
- if (!this.versionIsRelevant(version)) {
414
- return;
415
- }
416
- if (this.migrationData.hasOwnProperty(oldTableName)) {
417
- this.migrationData[newTableName] = this.migrationData[oldTableName];
418
- delete this.migrationData[oldTableName];
419
- }
420
- const entry = this.getEntryFromTableName(newTableName);
421
- this.migrationDataForOldTableNames[oldTableName] = entry;
422
- if (!entry.tableRenaming) {
423
- entry.tableRenaming = { oldName: oldTableName, newName: newTableName };
424
- } else
425
- entry.tableRenaming.newName = newTableName;
426
- }
427
- /**
428
- * When you change the name of a column, SqlMorpheus will assume the original column was meant to be removed
429
- * and the new column was meant to be added, which would lead to data loss.
430
- * Use this method to mark a column to be renamed instead.
431
- * This method will be ignored if {@link version} is smaller than the current database version or higher than the target version of {@link DatabaseInstructions}.
432
- *
433
- * @param version - The target version for which the table should be renamed. Must be provided to make sure this method will only have an effect when specified.
434
- * @param tableName - The current table name in the database at the time of the migration version.
435
- * @param oldColumn The current name in the database at the time of the migration.
436
- * @param newColumn The new name to assign to the column.
437
- */
438
- renameColumn(version, tableName, oldColumn, newColumn) {
439
- if (!this.versionIsRelevant(version)) {
440
- return;
441
- }
442
- const entry = this.getEntryFromTableName(tableName);
443
- const existingColumnEntry = entry.renamedColumns.find((entry2) => entry2.newName == oldColumn);
444
- if (existingColumnEntry) {
445
- existingColumnEntry.newName = newColumn;
446
- } else {
447
- entry.renamedColumns.push({ oldName: oldColumn, newName: newColumn });
448
- }
449
- }
450
- /**
451
- * Should not be needed most of the time.
452
- * Marks a table to be recreated
453
- * (a copy of the table will be created, all data will be moved over, the original table will be dropped and the new table will be renamed to the original name).
454
- * This method will be ignored if {@link version} is smaller than the current database version or higher than the target version of {@link DatabaseInstructions}.
455
- *
456
- * @param version - The target version for which the table should be renamed. Must be provided to make sure this method will only have an effect when specified.
457
- * @param tableName - The current table name in the database at the time of the migration version.
458
- */
459
- recreateTable(version, tableName) {
460
- if (!this.versionIsRelevant(version)) {
461
- return;
462
- }
463
- const entry = this.getEntryFromTableName(tableName);
464
- entry.recreate = true;
465
- Logger.log(`Table ${tableName} will be recreated!`);
466
- this.allowMigration(version, tableName, "recreateTable");
467
- }
168
+ fromVersion = 0;
169
+ toVersion = 0;
170
+ lastUsedVersion = 0;
171
+ migrationData = {};
172
+ migrationDataForOldTableNames = {};
173
+ alwaysAllowed = [];
174
+ notAllowedChanges = [];
175
+ /**
176
+ * Resets the migration state to its initial values.
177
+ * Only used internally.
178
+ *
179
+ * @param dbInstructions - The instructions containing database metadata and migration details.
180
+ * @param fromVersion - The version of the actual database.
181
+ */
182
+ reset(dbInstructions, fromVersion) {
183
+ this.fromVersion = fromVersion;
184
+ this.toVersion = dbInstructions.version;
185
+ this.migrationData = {};
186
+ this.migrationDataForOldTableNames = {};
187
+ this.alwaysAllowed = dbInstructions.alwaysAllowedMigrations ?? [];
188
+ this.notAllowedChanges = [];
189
+ }
190
+ /**
191
+ * Retrieves the migration instructions for the specified table.
192
+ *
193
+ * @param tableName - The name of the table for which the entry is to be retrieved or created.
194
+ * @return The migration instructions corresponding to the given table input.
195
+ */
196
+ getEntryFromTableName(tableName) {
197
+ if (!this.migrationData.hasOwnProperty(tableName)) this.migrationData[tableName] = {
198
+ recreate: false,
199
+ renamedColumns: [],
200
+ allowedMigrations: {}
201
+ };
202
+ return this.migrationData[tableName];
203
+ }
204
+ /**
205
+ * Checks whether a provided version is relevant based on database version ({@link fromVersion}) and target version ({@link toVersion}).
206
+ * Also makes sure that version checks happen in order.
207
+ *
208
+ * @param version - The version to check for relevance.
209
+ * @return Returns true if the version is relevant, otherwise false.
210
+ * @throws Error if versions are not properly ordered
211
+ */
212
+ versionIsRelevant(version) {
213
+ if (version <= this.fromVersion || version > this.toVersion) return false;
214
+ if (version < this.lastUsedVersion) throw new Error("Migrations in preMigration() have to be ordered by version (beginning with the lowest version).");
215
+ this.lastUsedVersion = version;
216
+ return true;
217
+ }
218
+ /**
219
+ * Verifies the renaming tasks specified in {@link DatabaseInstructions.preMigration} that were considered are valid.
220
+ * Called after all SQL queries have been generated.
221
+ * Only used internally.
222
+ *
223
+ * @param newTables - The table structures extracted from {@link DatabaseInstructions.tables}.
224
+ * @throws Error if a renaming task is invalid.
225
+ */
226
+ verifyRenamingTasks(newTables) {
227
+ for (const tableName in this.migrationData) {
228
+ const migrationEntry = this.migrationData[tableName];
229
+ if (migrationEntry.tableRenaming) {
230
+ if (migrationEntry.tableRenaming.oldName == tableName) return /* @__PURE__ */ new Error(`You set table "${tableName}" to be renamed to itself!`);
231
+ if (migrationEntry.tableRenaming.newName != tableName) return /* @__PURE__ */ new Error(`You set table "${tableName}" to be renamed to a different name than the one in your structure (${migrationEntry.tableRenaming.newName})!`);
232
+ else if (!newTables[tableName]) return /* @__PURE__ */ new Error(`You set migrations for "${tableName}". But "${tableName}" does not exist in your structure.`);
233
+ }
234
+ for (const renamingData of migrationEntry.renamedColumns) {
235
+ const oldColumnName = renamingData.oldName;
236
+ const newColumnName = renamingData.newName;
237
+ if (oldColumnName == newColumnName) return /* @__PURE__ */ new Error(`You set column ${tableName}.${oldColumnName} to be renamed to itself!`);
238
+ else if (!newTables[tableName]?.columns[newColumnName]) return /* @__PURE__ */ new Error(`You set column ${tableName}.${oldColumnName} to be renamed to ${tableName}.${newColumnName}. But ${tableName}.${newColumnName} does not exist in your structure.`);
239
+ }
240
+ }
241
+ }
242
+ /**
243
+ * Making sure all considered calls to {@link allowMigration()} where necessary to prevent input mistakes.
244
+ * Called after all SQL queries have been generated.
245
+ * Only used internally.
246
+ *
247
+ * @throws Error if unnecessary calls were discovered. All discovered calls are collected and thrown as one Error.
248
+ */
249
+ verifyAllowedMigrations() {
250
+ if (this.notAllowedChanges.length) return new NotAllowedException(this.notAllowedChanges);
251
+ let errorMsg = "";
252
+ for (const tableName in this.migrationData) {
253
+ const migration = this.migrationData[tableName];
254
+ for (const type in migration.allowedMigrations) {
255
+ const migrationType = migration.allowedMigrations[type];
256
+ for (const column in migrationType) if (migrationType[column] != "allowedAndUsed") errorMsg += `Migration \"${type}\" for ${tableName}${column != "noColumn" ? `.${column}` : ""} was allowed but not needed.\n`;
257
+ }
258
+ }
259
+ if (errorMsg) return new Error(errorMsg);
260
+ }
261
+ /**
262
+ * Called by {@link MigrationManager} to check if a specific destructive query is allowed.
263
+ * Only used internally.
264
+ *
265
+ * @see allowMigration()
266
+ * @param tableName - The name of the table to check the migration against.
267
+ * @param type - The type of destructive migration being checked.
268
+ * @param column - The column name for which the migration is allowed. Only needed for "dropColumn", "removeForeignKey" and "alterForeignKey".
269
+ */
270
+ compareWithAllowedMigration(tableName, type, column = NO_COLUMN) {
271
+ const migrationEntry = this.migrationData[tableName];
272
+ const allowedMigrations = migrationEntry?.allowedMigrations[type];
273
+ if (!allowedMigrations || !allowedMigrations[column]) {
274
+ if (!this.alwaysAllowed.includes(type)) this.notAllowedChanges.push({
275
+ version: this.toVersion,
276
+ tableName,
277
+ column,
278
+ type
279
+ });
280
+ } else if (migrationEntry) allowedMigrations[column] = USED;
281
+ }
282
+ /**
283
+ * Retrieves the migration data containing instructions for data migration.
284
+ * Only used internally.
285
+ *
286
+ * @return An object where each key corresponds to the table name,
287
+ * and the value is the associated migration instructions.
288
+ */
289
+ getMigrationData() {
290
+ return this.migrationData;
291
+ }
292
+ /**
293
+ * Iterates over renamed columns for the specified table and executes a callback for each pair of old and new column names.
294
+ * Only used internally.
295
+ *
296
+ * @param tableName - Name of the table whose renamed columns should be processed.
297
+ * @param callback - A function to execute for each renamed column, receiving the old and new column names as arguments.
298
+ */
299
+ loopRenamedColumns(tableName, callback) {
300
+ const migrationEntry = this.migrationData[tableName];
301
+ for (const renamingData of migrationEntry.renamedColumns) callback(renamingData.oldName, renamingData.newName);
302
+ }
303
+ /**
304
+ * Considers the renaming of a table and returns its (newest) name after the migration or
305
+ * the original name if no renaming is queued.
306
+ * Only used internally.
307
+ *
308
+ * @see getOldTableName()
309
+ * @param tableName - The current table name in the database.
310
+ * @return The newest table name if available, otherwise the current table name.
311
+ */
312
+ getNewestTableName(tableName) {
313
+ return this.migrationDataForOldTableNames[tableName]?.tableRenaming?.newName ?? tableName;
314
+ }
315
+ /**
316
+ * Retrieves the current table name in the database.
317
+ * Only used internally.
318
+ *
319
+ * @see getNewestTableName()
320
+ * @param tableName - The name of the newest table name.
321
+ * @return The current table name in the database.
322
+ */
323
+ getOldTableName(tableName) {
324
+ return this.migrationData[tableName]?.tableRenaming?.oldName ?? tableName;
325
+ }
326
+ /**
327
+ * Considers the renaming of a column in a table and returns its (newest) name after the migration or
328
+ * the original name if no renaming is queued.
329
+ * Only used internally.
330
+ *
331
+ * @param tableName - The name of the table containing the column.
332
+ * @param oldColumnName - The current name of the column in the database.
333
+ * @return The newest column name if available, otherwise the original column name.
334
+ */
335
+ getNewestColumnName(tableName, oldColumnName) {
336
+ const columnNames = this.migrationData[tableName]?.renamedColumns.find((columns) => columns.oldName == oldColumnName);
337
+ return columnNames ? columnNames.newName : oldColumnName;
338
+ }
339
+ /**
340
+ * Determines if the specified table is marked to be recreated.
341
+ * Only used internally.
342
+ *
343
+ * @param tableName The name of the table to check.
344
+ * @return Returns true if the table will be recreated, false otherwise.
345
+ */
346
+ willBeRecreated(tableName) {
347
+ return this.migrationData[tableName]?.recreate;
348
+ }
349
+ /**
350
+ * Marks a table to be recreated and logs the recreation reason for the user.
351
+ * Only used internally.
352
+ *
353
+ * @param tableName - The name of the table to be marked for recreation.
354
+ * @param reason - The reason why the table is being marked for recreation.
355
+ */
356
+ internalRecreate(tableName, reason) {
357
+ const entry = this.getEntryFromTableName(tableName);
358
+ entry.recreate = true;
359
+ Logger.log(`Table ${tableName} will be recreated (${reason})!`);
360
+ }
361
+ /**
362
+ * SqlMorpheus only allows destructive statements if it was specifically allowed in {@link DatabaseInstructions.preMigration()}.
363
+ * Use this method to allow a destructive statement for a specific version.
364
+ * This method will be ignored if {@link version} is smaller than the current database version or higher than the target version of {@link DatabaseInstructions}.
365
+ *
366
+ * @param version - The target version for which the specific migration is allowed. Must be provided to make sure this method will only have an effect when specified.
367
+ * @param tableName - The current table name in the database at the time of the migration version.
368
+ * @param allowedMigration - The keys representing the migrations to be allowed.
369
+ * @param column - The column name for which the migration is allowed. Only needed for "dropColumn", "removeForeignKey" and "alterForeignKey".
370
+ *
371
+ * @throws Error if the provided column name is not valid for the specified migration.
372
+ */
373
+ allowMigration(version, tableName, allowedMigration, column) {
374
+ if (!this.versionIsRelevant(version)) return;
375
+ const entry = this.getEntryFromTableName(tableName);
376
+ if (!entry.allowedMigrations[allowedMigration]) entry.allowedMigrations[allowedMigration] = {};
377
+ entry.allowedMigrations[allowedMigration][column ?? "noColumn"] = ALLOWED;
378
+ if (column) switch (allowedMigration) {
379
+ case "recreateTable":
380
+ case "dropTable":
381
+ case "alterPrimaryKey": throw new Error(`You cannot use the ${allowedMigration} migration with a column name. Please remove the column name.`);
382
+ }
383
+ else switch (allowedMigration) {
384
+ case "dropColumn":
385
+ case "removeForeignKey":
386
+ case "alterForeignKey": throw new Error(`The ${allowedMigration} migration requires a column name. Please provide one.`);
387
+ }
388
+ }
389
+ /**
390
+ * When you change the name of a table, SqlMorpheus will assume the original table was meant to be dropped
391
+ * and the new table was meant to be added, which would lead to data loss.
392
+ * Use this method to mark a table to be renamed instead.
393
+ * This method will be ignored if {@link version} is smaller than the current database version or higher than the target version of {@link DatabaseInstructions}.
394
+ *
395
+ * @param version - The target version for which the table should be renamed. Must be provided to make sure this method will only have an effect when specified.
396
+ * @param oldTableName - The current name in the database at the time of the migration version.
397
+ * @param newTableName - The new table name at the time of the migration version.
398
+ */
399
+ renameTable(version, oldTableName, newTableName) {
400
+ if (!this.versionIsRelevant(version)) return;
401
+ if (this.migrationData.hasOwnProperty(oldTableName)) {
402
+ this.migrationData[newTableName] = this.migrationData[oldTableName];
403
+ delete this.migrationData[oldTableName];
404
+ }
405
+ const entry = this.getEntryFromTableName(newTableName);
406
+ this.migrationDataForOldTableNames[oldTableName] = entry;
407
+ if (!entry.tableRenaming) entry.tableRenaming = {
408
+ oldName: oldTableName,
409
+ newName: newTableName
410
+ };
411
+ else entry.tableRenaming.newName = newTableName;
412
+ }
413
+ /**
414
+ * When you change the name of a column, SqlMorpheus will assume the original column was meant to be removed
415
+ * and the new column was meant to be added, which would lead to data loss.
416
+ * Use this method to mark a column to be renamed instead.
417
+ * This method will be ignored if {@link version} is smaller than the current database version or higher than the target version of {@link DatabaseInstructions}.
418
+ *
419
+ * @param version - The target version for which the table should be renamed. Must be provided to make sure this method will only have an effect when specified.
420
+ * @param tableName - The current table name in the database at the time of the migration version.
421
+ * @param oldColumn The current name in the database at the time of the migration.
422
+ * @param newColumn The new name to assign to the column.
423
+ */
424
+ renameColumn(version, tableName, oldColumn, newColumn) {
425
+ if (!this.versionIsRelevant(version)) return;
426
+ const entry = this.getEntryFromTableName(tableName);
427
+ const existingColumnEntry = entry.renamedColumns.find((entry) => entry.newName == oldColumn);
428
+ if (existingColumnEntry) existingColumnEntry.newName = newColumn;
429
+ else entry.renamedColumns.push({
430
+ oldName: oldColumn,
431
+ newName: newColumn
432
+ });
433
+ }
434
+ /**
435
+ * Should not be needed most of the time.
436
+ * Marks a table to be recreated
437
+ * (a copy of the table will be created, all data will be moved over, the original table will be dropped and the new table will be renamed to the original name).
438
+ * This method will be ignored if {@link version} is smaller than the current database version or higher than the target version of {@link DatabaseInstructions}.
439
+ *
440
+ * @param version - The target version for which the table should be renamed. Must be provided to make sure this method will only have an effect when specified.
441
+ * @param tableName - The current table name in the database at the time of the migration version.
442
+ */
443
+ recreateTable(version, tableName) {
444
+ if (!this.versionIsRelevant(version)) return;
445
+ const entry = this.getEntryFromTableName(tableName);
446
+ entry.recreate = true;
447
+ Logger.log(`Table ${tableName} will be recreated!`);
448
+ this.allowMigration(version, tableName, "recreateTable");
449
+ }
468
450
  };
469
-
470
- // src/lib/dialects/DefaultSql.ts
471
- import escapeString from "escape-sql-string";
472
- var MIGRATION_DATA_TABLE_NAME = "__sqlmorpheus_migrations";
451
+ //#endregion
452
+ //#region src/lib/dialects/DefaultSql.ts
453
+ const MIGRATION_DATA_TABLE_NAME = "__sqlmorpheus_migrations";
454
+ /**
455
+ * Provides syntax for SQL queries. Specified dialects extend from this class.
456
+ * Needs a {@link DatabaseAccess} object for database operations
457
+ */
473
458
  var DefaultSql = class {
474
- db;
475
- canRecreateTable = false;
476
- canAlterColumnStructure = false;
477
- canAlterForeignKeys = false;
478
- canAlterPrimaryKey = false;
479
- types = {
480
- text: "TEXT",
481
- string: "TEXT",
482
- number: "INTEGER",
483
- bigint: "BIGINT",
484
- boolean: "BOOLEAN",
485
- date: "DATE",
486
- time: "TIME",
487
- dateTime: "DATETIME"
488
- };
489
- nullType = "NULL";
490
- constructor(db) {
491
- this.db = db;
492
- }
493
- /**
494
- * Formats a given value to a SQL-equivalent string based on the provided data type.
495
- *
496
- * @param value - The value to be formatted.
497
- * @param type - The data type of the value. Either determined through typeof or {@link TableStructure} setting
498
- * @return A string representation of the value formatted for SQL.
499
- */
500
- formatValueToSql(value, type) {
501
- function toDate(value2) {
502
- return value2 instanceof Date ? value2 : new Date(value2);
503
- }
504
- switch (type) {
505
- case "text":
506
- case "string":
507
- return `'${value}'`;
508
- case "time":
509
- return `'${toDate(value).toISOString().slice(11, 19)}'`;
510
- case "date":
511
- return `'${toDate(value).toISOString().slice(0, 10)}'`;
512
- case "dateTime":
513
- return `'${toDate(value).toISOString().slice(0, 19).replace("T", " ")}'`;
514
- default:
515
- return value.toString();
516
- }
517
- }
518
- formatIdentifier(identifier) {
519
- return identifier;
520
- }
521
- getSqlType(dataType, _) {
522
- return this.types[dataType];
523
- }
524
- /**
525
- * Changes the column type and the default value of a column in a specified table.
526
- * Is only used if {@link canAlterColumnStructure} is true.
527
- *
528
- * @param tableName - The name of the table containing the column to be altered.
529
- * @param columnName - The name of the column to be altered.
530
- * @param sqlType - The new data type (specific to the SQL dialect) for the column.
531
- * @param defaultValue - The new default value for the column.
532
- * @return The SQL statement as a string.
533
- */
534
- alterColumnStructure(tableName, columnName, sqlType, defaultValue) {
535
- throw new Error("Altering column structure is not supported. Recreate the table instead!");
536
- }
537
- /**
538
- * Statement to add a foreign key to a specified table.
539
- * Is only used if {@link canAlterForeignKeys} is true.
540
- *
541
- * @param fromTableName - The name of the table to which the foreign key will be added.
542
- * @param foreignKey - The foreign key definition to be added, see {@link foreignKey()}.
543
- * @return The SQL statement as a string.
544
- */
545
- addForeignKey(fromTableName, foreignKey) {
546
- throw new Error("Adding foreign keys is not supported!");
547
- }
548
- /**
549
- * Statement to remove a foreign key from a specified table.
550
- * Is only used if {@link canAlterForeignKeys} is true.
551
- *
552
- * @param tableName - The name of the table to which the foreign key will be added.
553
- * @param foreignKeyName - The name of the foreign key definition.
554
- * @return The SQL statement as a string.
555
- */
556
- removeForeignKey(tableName, foreignKeyName) {
557
- throw new Error("Foreign key removal is not supported!");
558
- }
559
- /**
560
- * Statement to add a primary key to the specified table.
561
- * Is only used if {@link canAlterPrimaryKey} is true.
562
- *
563
- * @param tableName - The name of the table to which the primary key will be added.
564
- * @param columnName - The name of the column that will be set as the primary key.
565
- * @return The SQL statement as a string.
566
- */
567
- addPrimaryKey(tableName, columnName) {
568
- throw new Error("Adding foreign keys is not supported!");
569
- }
570
- /**
571
- * Statement to remove a primary key from the specified table.
572
- * Is only used if {@link canAlterPrimaryKey} is true.
573
- *
574
- * @param tableName - The name of the table to which the primary key will be added.
575
- * @param columnName - The name of the column that will be set as the primary key.
576
- * @return The SQL statement as a string.
577
- */
578
- removePrimaryKey(tableName, columnName) {
579
- throw new Error("Foreign key removal is not supported!");
580
- }
581
- /**
582
- * Constructs a SQL FOREIGN KEY constraint clause.
583
- * Used in combination with {@link addForeignKey} or {@link createTable}
584
- *
585
- * @param column - The name of the column in the current table to apply the foreign key constraint.
586
- * @param foreignTable - The name of the referenced table.
587
- * @param foreignColumn - The name of the referenced column in the foreign table.
588
- * @param onUpdate - Optional action to specify the behavior on update (e.g., CASCADE, SET NULL).
589
- * @param onDelete - Optional action to specify the behavior on delete (e.g., CASCADE, SET NULL).
590
- * @return The SQL string representing the foreign key constraint.
591
- */
592
- foreignKey(column, foreignTable, foreignColumn, onUpdate, onDelete) {
593
- let query = `FOREIGN KEY (${this.formatIdentifier(column)}) REFERENCES ${this.formatIdentifier(foreignTable)} (${this.formatIdentifier(foreignColumn)})`;
594
- if (onUpdate)
595
- query += ` ON UPDATE ${onUpdate}`;
596
- if (onDelete)
597
- query += ` ON DELETE ${onDelete}`;
598
- return query;
599
- }
600
- /**
601
- * Statement to create a table if it does not already exist.
602
- *
603
- * @param tableName - The name of the table to be created.
604
- * @param entries - An array containing the column definitions for the table.
605
- * @return The SQL statement as a string.
606
- */
607
- createTable(tableName, entries) {
608
- return `CREATE TABLE IF NOT EXISTS ${this.formatIdentifier(tableName)} (
609
- ${entries.join(",\n ")}
610
- );`;
611
- }
612
- /**
613
- * Statement to rename a table.
614
- *
615
- * @param tableName - The name of the existing table to be renamed.
616
- * @param newTableName - The new name for the table.
617
- * @return The SQL statement as a string.
618
- */
619
- renameTable(tableName, newTableName) {
620
- return `ALTER TABLE ${this.formatIdentifier(tableName)} RENAME TO ${this.formatIdentifier(newTableName)};`;
621
- }
622
- /**
623
- * Statement to drop a table if it exists.
624
- *
625
- * @param tableName - The name of the table to be dropped.
626
- * @return The SQL statement as a string.
627
- */
628
- dropTable(tableName) {
629
- return `DROP TABLE IF EXISTS ${this.formatIdentifier(tableName)};`;
630
- }
631
- recreateTable(tableName, entries) {
632
- return `DROP TABLE IF EXISTS ${this.formatIdentifier(tableName)}; ${this.createTable(tableName, entries)}`;
633
- }
634
- /**
635
- * Constructs a SQL column definition based on the provided parameters.
636
- *
637
- * @param columnName - The name of the column.
638
- * @param type - The data type of the column.
639
- * @param defaultValue - The default value for the column.
640
- * @param isPrimaryKey - Indicates whether the column is a primary key.
641
- * @return The constructed SQL column definition string.
642
- */
643
- columnDefinition(columnName, type, isPrimaryKey, defaultValue) {
644
- const query = `${this.formatIdentifier(columnName)} ${type}${defaultValue ? ` DEFAULT ${defaultValue}` : ""}`;
645
- return isPrimaryKey ? `${query} PRIMARY KEY` : query;
646
- }
647
- /**
648
- * Statement to add a new column to a specified table.
649
- *
650
- * @param columnTable - The name of the table to which the column will be added.
651
- * @param entry - The column definition including the column name and its data type. See {@link columnDefinition()}
652
- * @return The SQL statement as a string.
653
- */
654
- createColumn(columnTable, entry) {
655
- return `ALTER TABLE ${this.formatIdentifier(columnTable)} ADD ${entry};`;
656
- }
657
- /**
658
- * Statement to rename a column in an existing table.
659
- *
660
- * @param tableName - The name of the table containing the column to rename.
661
- * @param oldColumnName - The current name of the column to be renamed.
662
- * @param newColumnName - The new name for the column.
663
- * @return The SQL statement as a string.
664
- */
665
- renameColumn(tableName, oldColumnName, newColumnName) {
666
- return `ALTER TABLE ${this.formatIdentifier(tableName)} RENAME COLUMN ${this.formatIdentifier(oldColumnName)} TO ${this.formatIdentifier(newColumnName)};`;
667
- }
668
- /**
669
- * Statement to drop a column from a specified table.
670
- *
671
- * @param tableName - The name of the table from which the column will be dropped.
672
- * @param columnName - The name of the column to be dropped.
673
- * @return The SQL statement as a string.
674
- */
675
- dropColumn(tableName, columnName) {
676
- return `ALTER TABLE ${this.formatIdentifier(tableName)} DROP COLUMN ${this.formatIdentifier(columnName)};`;
677
- }
678
- /**
679
- * Statement to return data from a table.
680
- *
681
- * @param tableName - The name of the database table to select data from.
682
- * @param select - An array of column names to include in the SELECT clause.
683
- * @param where - An optional condition.
684
- * @param orderBy - Optional. the column to sort by
685
- * @return The SQL statement as a string.
686
- */
687
- select(tableName, select, where, orderBy) {
688
- let query = `SELECT ${select.map((k) => this.formatIdentifier(k)).join(",")} FROM ${this.formatIdentifier(tableName)}`;
689
- if (where) {
690
- query += ` WHERE ${where}`;
691
- }
692
- if (orderBy) {
693
- query += ` ORDER BY ${orderBy}`;
694
- }
695
- return `${query};`;
696
- }
697
- /**
698
- * Statement to insert data into a table.
699
- *
700
- * @param tableName - The name of the table where the data will be inserted.
701
- * @param content - The value query. See {@link insertValues()}.
702
- * @return The SQL statement as a string.
703
- */
704
- insert(tableName, content) {
705
- return `INSERT INTO ${this.formatIdentifier(tableName)} ${content};`;
706
- }
707
- /**
708
- * A SQL insert query string. See {@link insert()}.
709
- *
710
- * @param keys - An array of column names to be used in the query.
711
- * @param [valueString] - An optional preformatted value string to be included in the query.
712
- * If not provided, the method generates a default placeholder string with "?" for each key.
713
- * @return The SQL statement as a string.
714
- */
715
- insertValues(keys, valueString) {
716
- return `(${keys.map((k) => this.formatIdentifier(k)).join(",")}) ${valueString ?? `VALUES (${keys.map(() => "?").join(",")})`}`;
717
- }
718
- /**
719
- * Returns a SQL query string to create a migration table.
720
- * The table is used to store migration data, specifically the version of the migration.
721
- *
722
- * @return The SQL query string for creating the migration table.
723
- */
724
- migrationTableQuery() {
725
- return this.createTable(MIGRATION_DATA_TABLE_NAME, [
726
- this.columnDefinition("fromVersion", this.getSqlType("number"), false, "0"),
727
- this.columnDefinition("toVersion", this.getSqlType("number"), true, "0"),
728
- this.columnDefinition("upChanges", this.getSqlType("text"), false),
729
- this.columnDefinition("downChanges", this.getSqlType("text"), false)
730
- ]);
731
- }
732
- /**
733
- * Ensures that the migration table exists in the database.
734
- * If the table does not exist, it creates the table using the defined migration table query.
735
- *
736
- * @return A promise that resolves when the migration table is created successfully.
737
- */
738
- async createMigrationTableIfNeeded() {
739
- await this.db.runTransaction(this.migrationTableQuery());
740
- }
741
- /**
742
- * Retrieves the current version from the migration data table.
743
- *
744
- * @return A promise that resolves to the current version number. Returns 0 if no version is found.
745
- */
746
- async getVersion() {
747
- await this.createMigrationTableIfNeeded();
748
- const query = this.select(MIGRATION_DATA_TABLE_NAME, ["toVersion"], void 0, `${this.formatIdentifier("toVersion")} DESC`);
749
- const data = await this.db.runReadStatement(query);
750
- return data.length ? data[0].toVersion : 0;
751
- }
752
- /**
753
- * Updates the migration version in the database. If the version entry does not exist, it inserts a new one;
754
- * if it exists, it updates the version to the specified value.
755
- *
756
- * @param toVersion - The new migration version to set in the database.
757
- * @return A promise that resolves when the operation is complete.
758
- */
759
- async rollbackHistory(toVersion) {
760
- const query = `${this.migrationTableQuery()}; DELETE FROM ${MIGRATION_DATA_TABLE_NAME} WHERE toVersion > ${toVersion};`;
761
- await this.db.runTransaction(query);
762
- }
763
- async getChanges(version) {
764
- await this.createMigrationTableIfNeeded();
765
- const query = this.select(MIGRATION_DATA_TABLE_NAME, ["fromVersion", "upChanges", "downChanges"], `toVersion = ${version}`);
766
- const data = await this.db.runReadStatement(query);
767
- return data ? { up: data[0].upChanges, down: data[0].downChanges, fromVersion: data[0].fromVersion } : null;
768
- }
769
- async hasChanges(version) {
770
- await this.createMigrationTableIfNeeded();
771
- const query = this.select(MIGRATION_DATA_TABLE_NAME, ["toVersion"], `${this.formatIdentifier("toVersion")} = ${version}`);
772
- const data = await this.db.runReadStatement(query);
773
- return !!data.length;
774
- }
775
- /**
776
- * Saves the change history to the SQLMorpheus table in the database
777
- * Since we save SQL queries in the database, we need to make sure that they are properly escaped.
778
- * @param fromVersion
779
- * @param toVersion
780
- * @param changes
781
- */
782
- async setChanges(fromVersion, toVersion, changes) {
783
- const downChanges = escapeString(changes.down);
784
- const upChanges = escapeString(changes.up);
785
- const query = `${this.migrationTableQuery()} ${await this.hasChanges(toVersion) ? `UPDATE ${MIGRATION_DATA_TABLE_NAME}
459
+ db;
460
+ canRecreateTable = false;
461
+ canAlterColumnStructure = false;
462
+ canAlterForeignKeys = false;
463
+ canAlterPrimaryKey = false;
464
+ types = {
465
+ text: "TEXT",
466
+ string: "TEXT",
467
+ number: "INTEGER",
468
+ bigint: "BIGINT",
469
+ boolean: "BOOLEAN",
470
+ date: "DATE",
471
+ time: "TIME",
472
+ dateTime: "DATETIME"
473
+ };
474
+ nullType = "NULL";
475
+ constructor(db) {
476
+ this.db = db;
477
+ }
478
+ /**
479
+ * Formats a given value to a SQL-equivalent string based on the provided data type.
480
+ *
481
+ * @param value - The value to be formatted.
482
+ * @param type - The data type of the value. Either determined through typeof or {@link TableStructure} setting
483
+ * @return A string representation of the value formatted for SQL.
484
+ */
485
+ formatValueToSql(value, type) {
486
+ function toDate(value) {
487
+ return value instanceof Date ? value : new Date(value);
488
+ }
489
+ switch (type) {
490
+ case "text":
491
+ case "string": return `'${value}'`;
492
+ case "time": return `'${toDate(value).toISOString().slice(11, 19)}'`;
493
+ case "date": return `'${toDate(value).toISOString().slice(0, 10)}'`;
494
+ case "dateTime": return `'${toDate(value).toISOString().slice(0, 19).replace("T", " ")}'`;
495
+ default: return value.toString();
496
+ }
497
+ }
498
+ formatIdentifier(identifier) {
499
+ return identifier;
500
+ }
501
+ getSqlType(dataType, _) {
502
+ return this.types[dataType];
503
+ }
504
+ /**
505
+ * Changes the column type and the default value of a column in a specified table.
506
+ * Is only used if {@link canAlterColumnStructure} is true.
507
+ *
508
+ * @param tableName - The name of the table containing the column to be altered.
509
+ * @param columnName - The name of the column to be altered.
510
+ * @param sqlType - The new data type (specific to the SQL dialect) for the column.
511
+ * @param defaultValue - The new default value for the column.
512
+ * @return The SQL statement as a string.
513
+ */
514
+ alterColumnStructure(tableName, columnName, sqlType, defaultValue) {
515
+ throw new Error("Altering column structure is not supported. Recreate the table instead!");
516
+ }
517
+ /**
518
+ * Statement to add a foreign key to a specified table.
519
+ * Is only used if {@link canAlterForeignKeys} is true.
520
+ *
521
+ * @param fromTableName - The name of the table to which the foreign key will be added.
522
+ * @param foreignKey - The foreign key definition to be added, see {@link foreignKey()}.
523
+ * @return The SQL statement as a string.
524
+ */
525
+ addForeignKey(fromTableName, foreignKey) {
526
+ throw new Error("Adding foreign keys is not supported!");
527
+ }
528
+ /**
529
+ * Statement to remove a foreign key from a specified table.
530
+ * Is only used if {@link canAlterForeignKeys} is true.
531
+ *
532
+ * @param tableName - The name of the table to which the foreign key will be added.
533
+ * @param foreignKeyName - The name of the foreign key definition.
534
+ * @return The SQL statement as a string.
535
+ */
536
+ removeForeignKey(tableName, foreignKeyName) {
537
+ throw new Error("Foreign key removal is not supported!");
538
+ }
539
+ /**
540
+ * Statement to add a primary key to the specified table.
541
+ * Is only used if {@link canAlterPrimaryKey} is true.
542
+ *
543
+ * @param tableName - The name of the table to which the primary key will be added.
544
+ * @param columnName - The name of the column that will be set as the primary key.
545
+ * @return The SQL statement as a string.
546
+ */
547
+ addPrimaryKey(tableName, columnName) {
548
+ throw new Error("Adding foreign keys is not supported!");
549
+ }
550
+ /**
551
+ * Statement to remove a primary key from the specified table.
552
+ * Is only used if {@link canAlterPrimaryKey} is true.
553
+ *
554
+ * @param tableName - The name of the table to which the primary key will be added.
555
+ * @param columnName - The name of the column that will be set as the primary key.
556
+ * @return The SQL statement as a string.
557
+ */
558
+ removePrimaryKey(tableName, columnName) {
559
+ throw new Error("Foreign key removal is not supported!");
560
+ }
561
+ /**
562
+ * Constructs a SQL FOREIGN KEY constraint clause.
563
+ * Used in combination with {@link addForeignKey} or {@link createTable}
564
+ *
565
+ * @param column - The name of the column in the current table to apply the foreign key constraint.
566
+ * @param foreignTable - The name of the referenced table.
567
+ * @param foreignColumn - The name of the referenced column in the foreign table.
568
+ * @param onUpdate - Optional action to specify the behavior on update (e.g., CASCADE, SET NULL).
569
+ * @param onDelete - Optional action to specify the behavior on delete (e.g., CASCADE, SET NULL).
570
+ * @return The SQL string representing the foreign key constraint.
571
+ */
572
+ foreignKey(column, foreignTable, foreignColumn, onUpdate, onDelete) {
573
+ let query = `FOREIGN KEY (${this.formatIdentifier(column)}) REFERENCES ${this.formatIdentifier(foreignTable)} (${this.formatIdentifier(foreignColumn)})`;
574
+ if (onUpdate) query += ` ON UPDATE ${onUpdate}`;
575
+ if (onDelete) query += ` ON DELETE ${onDelete}`;
576
+ return query;
577
+ }
578
+ /**
579
+ * Statement to create a table if it does not already exist.
580
+ *
581
+ * @param tableName - The name of the table to be created.
582
+ * @param entries - An array containing the column definitions for the table.
583
+ * @return The SQL statement as a string.
584
+ */
585
+ createTable(tableName, entries) {
586
+ return `CREATE TABLE IF NOT EXISTS ${this.formatIdentifier(tableName)} (\n\t${entries.join(",\n ")}\n);`;
587
+ }
588
+ /**
589
+ * Statement to rename a table.
590
+ *
591
+ * @param tableName - The name of the existing table to be renamed.
592
+ * @param newTableName - The new name for the table.
593
+ * @return The SQL statement as a string.
594
+ */
595
+ renameTable(tableName, newTableName) {
596
+ return `ALTER TABLE ${this.formatIdentifier(tableName)} RENAME TO ${this.formatIdentifier(newTableName)};`;
597
+ }
598
+ /**
599
+ * Statement to drop a table if it exists.
600
+ *
601
+ * @param tableName - The name of the table to be dropped.
602
+ * @return The SQL statement as a string.
603
+ */
604
+ dropTable(tableName) {
605
+ return `DROP TABLE IF EXISTS ${this.formatIdentifier(tableName)};`;
606
+ }
607
+ recreateTable(tableName, entries) {
608
+ return `DROP TABLE IF EXISTS ${this.formatIdentifier(tableName)}; ${this.createTable(tableName, entries)}`;
609
+ }
610
+ /**
611
+ * Constructs a SQL column definition based on the provided parameters.
612
+ *
613
+ * @param columnName - The name of the column.
614
+ * @param type - The data type of the column.
615
+ * @param defaultValue - The default value for the column.
616
+ * @param isPrimaryKey - Indicates whether the column is a primary key.
617
+ * @return The constructed SQL column definition string.
618
+ */
619
+ columnDefinition(columnName, type, isPrimaryKey, defaultValue) {
620
+ const query = `${this.formatIdentifier(columnName)} ${type}${defaultValue ? ` DEFAULT ${defaultValue}` : ""}`;
621
+ return isPrimaryKey ? `${query} PRIMARY KEY` : query;
622
+ }
623
+ /**
624
+ * Statement to add a new column to a specified table.
625
+ *
626
+ * @param columnTable - The name of the table to which the column will be added.
627
+ * @param entry - The column definition including the column name and its data type. See {@link columnDefinition()}
628
+ * @return The SQL statement as a string.
629
+ */
630
+ createColumn(columnTable, entry) {
631
+ return `ALTER TABLE ${this.formatIdentifier(columnTable)} ADD ${entry};`;
632
+ }
633
+ /**
634
+ * Statement to rename a column in an existing table.
635
+ *
636
+ * @param tableName - The name of the table containing the column to rename.
637
+ * @param oldColumnName - The current name of the column to be renamed.
638
+ * @param newColumnName - The new name for the column.
639
+ * @return The SQL statement as a string.
640
+ */
641
+ renameColumn(tableName, oldColumnName, newColumnName) {
642
+ return `ALTER TABLE ${this.formatIdentifier(tableName)} RENAME COLUMN ${this.formatIdentifier(oldColumnName)} TO ${this.formatIdentifier(newColumnName)};`;
643
+ }
644
+ /**
645
+ * Statement to drop a column from a specified table.
646
+ *
647
+ * @param tableName - The name of the table from which the column will be dropped.
648
+ * @param columnName - The name of the column to be dropped.
649
+ * @return The SQL statement as a string.
650
+ */
651
+ dropColumn(tableName, columnName) {
652
+ return `ALTER TABLE ${this.formatIdentifier(tableName)} DROP COLUMN ${this.formatIdentifier(columnName)};`;
653
+ }
654
+ /**
655
+ * Statement to return data from a table.
656
+ *
657
+ * @param tableName - The name of the database table to select data from.
658
+ * @param select - An array of column names to include in the SELECT clause.
659
+ * @param where - An optional condition.
660
+ * @param orderBy - Optional. the column to sort by
661
+ * @return The SQL statement as a string.
662
+ */
663
+ select(tableName, select, where, orderBy) {
664
+ let query = `SELECT ${select.map((k) => this.formatIdentifier(k)).join(",")} FROM ${this.formatIdentifier(tableName)}`;
665
+ if (where) query += ` WHERE ${where}`;
666
+ if (orderBy) query += ` ORDER BY ${orderBy}`;
667
+ return `${query};`;
668
+ }
669
+ /**
670
+ * Statement to insert data into a table.
671
+ *
672
+ * @param tableName - The name of the table where the data will be inserted.
673
+ * @param content - The value query. See {@link insertValues()}.
674
+ * @return The SQL statement as a string.
675
+ */
676
+ insert(tableName, content) {
677
+ return `INSERT INTO ${this.formatIdentifier(tableName)} ${content};`;
678
+ }
679
+ /**
680
+ * A SQL insert query string. See {@link insert()}.
681
+ *
682
+ * @param keys - An array of column names to be used in the query.
683
+ * @param [valueString] - An optional preformatted value string to be included in the query.
684
+ * If not provided, the method generates a default placeholder string with "?" for each key.
685
+ * @return The SQL statement as a string.
686
+ */
687
+ insertValues(keys, valueString) {
688
+ return `(${keys.map((k) => this.formatIdentifier(k)).join(",")}) ${valueString ?? `VALUES (${keys.map(() => "?").join(",")})`}`;
689
+ }
690
+ /**
691
+ * Returns a SQL query string to create a migration table.
692
+ * The table is used to store migration data, specifically the version of the migration.
693
+ *
694
+ * @return The SQL query string for creating the migration table.
695
+ */
696
+ migrationTableQuery() {
697
+ return this.createTable(MIGRATION_DATA_TABLE_NAME, [
698
+ this.columnDefinition("fromVersion", this.getSqlType("number"), false, "0"),
699
+ this.columnDefinition("toVersion", this.getSqlType("number"), true, "0"),
700
+ this.columnDefinition("upChanges", this.getSqlType("text"), false),
701
+ this.columnDefinition("downChanges", this.getSqlType("text"), false)
702
+ ]);
703
+ }
704
+ /**
705
+ * Ensures that the migration table exists in the database.
706
+ * If the table does not exist, it creates the table using the defined migration table query.
707
+ *
708
+ * @return A promise that resolves when the migration table is created successfully.
709
+ */
710
+ async createMigrationTableIfNeeded() {
711
+ await this.db.runTransaction(this.migrationTableQuery());
712
+ }
713
+ /**
714
+ * Retrieves the current version from the migration data table.
715
+ *
716
+ * @return A promise that resolves to the current version number. Returns 0 if no version is found.
717
+ */
718
+ async getVersion() {
719
+ await this.createMigrationTableIfNeeded();
720
+ const query = this.select(MIGRATION_DATA_TABLE_NAME, ["toVersion"], void 0, `${this.formatIdentifier("toVersion")} DESC`);
721
+ const data = await this.db.runReadStatement(query);
722
+ return data.length ? data[0].toVersion : 0;
723
+ }
724
+ /**
725
+ * Updates the migration version in the database. If the version entry does not exist, it inserts a new one;
726
+ * if it exists, it updates the version to the specified value.
727
+ *
728
+ * @param toVersion - The new migration version to set in the database.
729
+ * @return A promise that resolves when the operation is complete.
730
+ */
731
+ async rollbackHistory(toVersion) {
732
+ const query = `${this.migrationTableQuery()}; DELETE FROM ${MIGRATION_DATA_TABLE_NAME} WHERE toVersion > ${toVersion};`;
733
+ await this.db.runTransaction(query);
734
+ }
735
+ async getChanges(version) {
736
+ await this.createMigrationTableIfNeeded();
737
+ const query = this.select(MIGRATION_DATA_TABLE_NAME, [
738
+ "fromVersion",
739
+ "upChanges",
740
+ "downChanges"
741
+ ], `toVersion = ${version}`);
742
+ const data = await this.db.runReadStatement(query);
743
+ return data ? {
744
+ up: data[0].upChanges,
745
+ down: data[0].downChanges,
746
+ fromVersion: data[0].fromVersion
747
+ } : null;
748
+ }
749
+ async hasChanges(version) {
750
+ await this.createMigrationTableIfNeeded();
751
+ const query = this.select(MIGRATION_DATA_TABLE_NAME, ["toVersion"], `${this.formatIdentifier("toVersion")} = ${version}`);
752
+ return !!(await this.db.runReadStatement(query)).length;
753
+ }
754
+ /**
755
+ * Saves the change history to the SQLMorpheus table in the database
756
+ * Since we save SQL queries in the database, we need to make sure that they are properly escaped.
757
+ * @param fromVersion
758
+ * @param toVersion
759
+ * @param changes
760
+ */
761
+ async setChanges(fromVersion, toVersion, changes) {
762
+ const escapeStringFu = escapeString.default ?? escapeString;
763
+ const downChanges = escapeStringFu(changes.down);
764
+ const upChanges = escapeStringFu(changes.up);
765
+ const query = `${this.migrationTableQuery()} ${await this.hasChanges(toVersion) ? `UPDATE ${MIGRATION_DATA_TABLE_NAME}
786
766
  SET
787
767
  ${this.formatIdentifier("fromVersion")} = '${fromVersion}',
788
768
  ${this.formatIdentifier("upChanges")} = ${upChanges},
@@ -794,725 +774,656 @@ var DefaultSql = class {
794
774
  ${this.formatIdentifier("downChanges")}
795
775
  )
796
776
  VALUES ('${fromVersion}', '${toVersion}', ${upChanges}, ${downChanges})`}`;
797
- await this.db.runTransaction(query);
798
- }
777
+ await this.db.runTransaction(query);
778
+ }
799
779
  };
800
-
801
- // src/lib/MigrationManager.ts
780
+ //#endregion
781
+ //#region src/lib/MigrationManager.ts
782
+ /**
783
+ * The MigrationManager class is responsible for managing database schema migrations.
784
+ * It handles tasks such as generating SQL for migrations, creating migration histories,
785
+ * and synchronizing database schema changes between different versions.
786
+ */
802
787
  var MigrationManager = class {
803
- migrations = new Migrations();
804
- newTables = {};
805
- /**
806
- * Table names that currently exist in the database
807
- * Note: If tables are set to be renamed, then names in this array will be corrected to their future name.
808
- */
809
- existingTables = [];
810
- dialect;
811
- constructor(dialect) {
812
- this.dialect = dialect;
813
- }
814
- /**
815
- * Generates the SQL changes required to migrate a database from its current version
816
- * to the target version defined in the database instructions. Handles the creation
817
- * and deletion of tables, migration of foreign keys and columns, renaming of columns,
818
- * and recreating tables, along with executing any pre- and post-migration custom logic.
819
- *
820
- * @return A promise that resolves to an object containing the SQL
821
- * changes for the migration (`up` and `down` changes) or null if no migration is required.
822
- * Throws an error in scenarios such as attempting to migrate to a lower version
823
- * or an invalid starting version.
824
- */
825
- async generateSqlChanges(dbInstructions) {
826
- const fromVersion = await this.dialect.getVersion();
827
- const toVersion = dbInstructions.version;
828
- if (toVersion <= 0)
829
- throw new Error("Cannot migrate to version 0 or lower");
830
- if (fromVersion == toVersion) {
831
- Logger.log("Version has not changed. No migrations needed.");
832
- return null;
833
- }
834
- if (fromVersion > toVersion)
835
- throw new Error(`You cannot create new migrations with a lower version (from ${fromVersion} to ${toVersion})`);
836
- const changes = {
837
- up: "",
838
- down: ""
839
- };
840
- this.migrations.reset(dbInstructions, fromVersion);
841
- const preSQL = dbInstructions.preMigration?.(
842
- this.migrations,
843
- this.dialect.db,
844
- fromVersion,
845
- toVersion
846
- );
847
- if (preSQL) {
848
- changes.up += `
849
-
850
- -- Custom SQL from preMigration()
851
- ${preSQL.up}`;
852
- changes.down += `
853
-
854
- -- Custom SQL from preMigration()
855
- ${preSQL.down}`;
856
- }
857
- const tableStructureGenerator = new TableStructureGenerator(dbInstructions, this.dialect);
858
- this.newTables = tableStructureGenerator.generateTableStructure();
859
- if (fromVersion == 0) {
860
- Logger.log(`Creating initial migration to version ${dbInstructions.version}`);
861
- const initialChanges = await this.createAndDropTables();
862
- changes.up += initialChanges.up;
863
- changes.down += initialChanges.down;
864
- } else {
865
- Logger.log(`Creating migration SQL from version ${fromVersion} to ${toVersion}`);
866
- this.existingTables = (await this.dialect.getTableNames()).filter((oldTableName) => oldTableName != MIGRATION_DATA_TABLE_NAME).map((oldTableName) => this.migrations.getNewestTableName(oldTableName));
867
- [
868
- this.renameTables(),
869
- await this.createAndDropTables(),
870
- await this.migrateForeignKeys(),
871
- await this.migrateColumns(),
872
- this.renameColumns(),
873
- await this.recreateTables()
874
- ].forEach((entry) => {
875
- changes.up += entry.up;
876
- changes.down += entry.down;
877
- });
878
- }
879
- const postSql = dbInstructions.postMigration?.(this.dialect.db, fromVersion, toVersion);
880
- if (postSql) {
881
- changes.up += `
882
-
883
- -- Custom SQL from postMigration()
884
- ${postSql.up}`;
885
- changes.down += `
886
-
887
- -- Custom SQL from postMigration()
888
- ${postSql.down}`;
889
- }
890
- if (fromVersion != 0) {
891
- const exception = this.migrations.verifyRenamingTasks(this.newTables) ?? this.migrations.verifyAllowedMigrations();
892
- if (exception) {
893
- Logger.debug(`
894
- Canceled changes:
895
- ${changes.up}
896
- `);
897
- if (!dbInstructions.doNotThrowIfNotAllowed)
898
- throw exception;
899
- else {
900
- console.error(exception.message);
901
- return null;
902
- }
903
- }
904
- }
905
- return {
906
- fromVersion,
907
- toVersion,
908
- changes
909
- };
910
- }
911
- renameTables() {
912
- const changes = {
913
- up: "\n\n-- Rename tables\n",
914
- down: "\n\n-- Rename tables\n"
915
- };
916
- const data = this.migrations.getMigrationData();
917
- for (const tableName in data) {
918
- const migrationEntry = data[tableName];
919
- if (migrationEntry.tableRenaming) {
920
- Logger.log(`Table "${migrationEntry.tableRenaming.oldName}" will be renamed to "${tableName}"!`);
921
- changes.up += this.dialect.renameTable(migrationEntry.tableRenaming.oldName, tableName);
922
- changes.down += this.dialect.renameTable(tableName, migrationEntry.tableRenaming.oldName);
923
- }
924
- }
925
- return changes;
926
- }
927
- /**
928
- * Generates SQL change statements for creating and dropping tables based on the current table structure
929
- * and existing table configurations in the database. The `up` changes include SQL statements for creating
930
- * new tables, while the `down` changes include SQL statements for dropping the corresponding tables or
931
- * recreating them if necessary.
932
- *
933
- * @return {Promise<SqlChanges>} A promise that resolves to an object containing `up` and `down` SQL
934
- * change statements for migrating the database schema.
935
- */
936
- async createAndDropTables() {
937
- let changes = {
938
- up: "\n\n-- Create and drop tables\n",
939
- down: "\n\n-- Create and drop tables\n"
940
- };
941
- for (const tableName in this.newTables) {
942
- if (this.tableDoesNotExists(tableName)) {
943
- const structure = this.newTables[tableName];
944
- changes.up += `${this.createTableSql(tableName, structure.columns, structure.foreignKeys)}
945
- `;
946
- changes.down += `${this.dialect.dropTable(tableName)}
947
- `;
948
- }
949
- }
950
- for (const tableName of this.existingTables) {
951
- if (!this.newTables[tableName]) {
952
- this.migrations.compareWithAllowedMigration(tableName, "dropTable");
953
- const dbTableName = this.migrations.getOldTableName(tableName);
954
- changes.up += `${this.dialect.dropTable(tableName)}
955
- `;
956
- changes.down += this.createTableSql(tableName, await this.dialect.getColumnInformation(dbTableName), await this.dialect.getForeignKeys(dbTableName));
957
- }
958
- }
959
- return changes;
960
- }
961
- /**
962
- * Generates an SQL string to create a table with the specified schema.
963
- *
964
- * @param tableName - The name of the table to be created.
965
- * @param columns - An array of objects defining the columns of the table, including their names, types
966
- * default values, and whether they are primary keys.
967
- * @param foreignKeys - An optional array of objects defining the foreign key constraints for the table,
968
- * including the source column, target table, target column, action on update, and action on delete.
969
- * @return The complete SQL string to create the table with the specified structure and constraints.
970
- */
971
- createTableSql(tableName, columns, foreignKeys) {
972
- const queryLines = [];
973
- for (const key in columns) {
974
- const columnInfo = columns[key];
975
- queryLines.push(this.dialect.columnDefinition(columnInfo.name, columnInfo.sqlType, columnInfo.isPrimaryKey, columnInfo.defaultValue));
976
- }
977
- if (foreignKeys) {
978
- for (const foreignKeyInfo of foreignKeys) {
979
- queryLines.push(this.dialect.foreignKey(foreignKeyInfo.fromColumn, foreignKeyInfo.toTable, foreignKeyInfo.toColumn.toString(), foreignKeyInfo.onUpdate, foreignKeyInfo.onDelete));
980
- }
981
- }
982
- return this.dialect.createTable(tableName, queryLines);
983
- }
984
- /**
985
- * Checks if the specified table does not exist in the list of existing tables.
986
- *
987
- * @param tableName - The name of the table to check.
988
- * @return Returns true if the table does not exist, otherwise false.
989
- */
990
- tableDoesNotExists(tableName) {
991
- return this.existingTables.indexOf(tableName) == -1;
992
- }
993
- /**
994
- * Recreates database tables that have been flagged to be recreated in migration data.
995
- * This method generated the necessary SQL code for backing up the current table data, dropping and recreating
996
- * the tables, and reinserting the preserved data into the recreated tables.
997
- */
998
- async recreateTables() {
999
- const changes = {
1000
- up: "\n\n-- Recreate tables\n",
1001
- down: "\n\n-- Recreate tables\n"
1002
- };
1003
- const migrationData = this.migrations.getMigrationData();
1004
- for (const tableName in migrationData) {
1005
- const migrationEntry = migrationData[tableName];
1006
- if (!migrationEntry.recreate) {
1007
- continue;
1008
- }
1009
- if (!this.dialect.canRecreateTable) {
1010
- throw new Error("SQL dialect does not support recreating tables!");
1011
- }
1012
- this.migrations.compareWithAllowedMigration(tableName, "recreateTable");
1013
- const oldColumnList = await this.dialect.getColumnInformation(this.migrations.getOldTableName(tableName));
1014
- const newColumnList = this.newTables[tableName].columns;
1015
- const moveableColumns = [];
1016
- for (const key in newColumnList) {
1017
- if (oldColumnList.hasOwnProperty(key))
1018
- moveableColumns.push(newColumnList[key].name);
1019
- }
1020
- const backupTableName = `${tableName}__backup`;
1021
- const insertQuery = this.dialect.insert(
1022
- backupTableName,
1023
- this.dialect.insertValues(moveableColumns, this.dialect.select(tableName, moveableColumns))
1024
- );
1025
- const moveDataQuery = insertQuery + "\n" + this.dialect.dropTable(tableName) + "\n" + this.dialect.renameTable(backupTableName, tableName);
1026
- const structure = this.newTables[tableName];
1027
- changes.up += this.createTableSql(backupTableName, structure.columns, structure.foreignKeys) + "\n" + moveDataQuery + "\n\n";
1028
- const dbTableName = this.migrations.getOldTableName(tableName);
1029
- changes.down += this.createTableSql(backupTableName, await this.dialect.getColumnInformation(dbTableName), await this.dialect.getForeignKeys(dbTableName)) + "\n" + moveDataQuery;
1030
- }
1031
- return changes;
1032
- }
1033
- /**
1034
- * Migrates foreign key constraints for all defined tables.
1035
- */
1036
- async migrateForeignKeys() {
1037
- let changes = {
1038
- up: "\n\n-- Foreign keys\n",
1039
- down: "\n\n-- Foreign keys\n"
1040
- };
1041
- for (const tableName in this.newTables) {
1042
- if (this.migrations.willBeRecreated(tableName) || this.tableDoesNotExists(tableName))
1043
- continue;
1044
- const structure = this.newTables[tableName];
1045
- const newForeignKeys = structure.foreignKeys ?? [];
1046
- const oldForeignKeys = await this.dialect.getForeignKeys(this.migrations.getOldTableName(tableName));
1047
- let checkForNewForeignKeys = true;
1048
- for (const oldForeignKey of oldForeignKeys) {
1049
- const newForeignKey = newForeignKeys.find((entry) => entry.fromColumn == oldForeignKey.fromColumn);
1050
- if (!newForeignKey) {
1051
- Logger.log(`Foreign key ${oldForeignKey.fromTable}.${oldForeignKey.fromColumn} to ${oldForeignKey.toTable}.${oldForeignKey.toColumn} will be removed!`);
1052
- this.migrations.compareWithAllowedMigration(tableName, "removeForeignKey", oldForeignKey.fromColumn);
1053
- if (this.dialect.canAlterForeignKeys) {
1054
- changes.up += this.dialect.removeForeignKey(oldForeignKey.fromTable, oldForeignKey.fromColumn);
1055
- changes.down += this.dialect.addForeignKey(
1056
- oldForeignKey.fromTable,
1057
- this.dialect.foreignKey(oldForeignKey.fromColumn, oldForeignKey.toTable, oldForeignKey.toColumn, oldForeignKey.onUpdate, oldForeignKey.onDelete)
1058
- );
1059
- } else {
1060
- this.migrations.internalRecreate(structure.table, "Dialect does not support altering foreign keys");
1061
- checkForNewForeignKeys = false;
1062
- }
1063
- } else {
1064
- if (!newForeignKey || oldForeignKey.toColumn != newForeignKey.toColumn || (oldForeignKey.onUpdate ?? "NO ACTION") != (newForeignKey.onUpdate ?? "NO ACTION") || (oldForeignKey.onDelete ?? "NO ACTION") != (newForeignKey.onDelete ?? "NO ACTION")) {
1065
- Logger.log(`Foreign key ${oldForeignKey.fromTable}.${oldForeignKey.fromColumn} to ${oldForeignKey.toTable}.${oldForeignKey.toColumn} will be changed!`);
1066
- this.migrations.compareWithAllowedMigration(tableName, "alterForeignKey", oldForeignKey.fromColumn);
1067
- if (this.dialect.canAlterForeignKeys) {
1068
- changes.up += this.dialect.removeForeignKey(oldForeignKey.fromTable, oldForeignKey.fromColumn) + this.dialect.addForeignKey(
1069
- newForeignKey.fromTable,
1070
- this.dialect.foreignKey(newForeignKey.fromColumn, newForeignKey.toTable, newForeignKey.toColumn, newForeignKey.onUpdate, newForeignKey.onDelete)
1071
- );
1072
- changes.down += this.dialect.addForeignKey(
1073
- oldForeignKey.fromTable,
1074
- this.dialect.foreignKey(oldForeignKey.fromColumn, oldForeignKey.toTable, oldForeignKey.toColumn, oldForeignKey.onUpdate, oldForeignKey.onDelete)
1075
- ) + this.dialect.removeForeignKey(newForeignKey.fromTable, newForeignKey.fromColumn);
1076
- } else {
1077
- this.migrations.internalRecreate(structure.table, "Dialect does not support altering foreign keys");
1078
- checkForNewForeignKeys = false;
1079
- }
1080
- }
1081
- }
1082
- }
1083
- if (checkForNewForeignKeys) {
1084
- for (const newForeignKey of newForeignKeys) {
1085
- const oldForeignKey = oldForeignKeys.find((entry) => entry.fromColumn == newForeignKey.fromColumn);
1086
- if (oldForeignKey)
1087
- continue;
1088
- Logger.log(`Foreign key ${newForeignKey.fromTable}.${newForeignKey.fromColumn} to ${newForeignKey.toTable}.${newForeignKey.toColumn} will be added!`);
1089
- if (this.dialect.canAlterForeignKeys) {
1090
- changes.up += this.dialect.addForeignKey(
1091
- newForeignKey.toTable,
1092
- this.dialect.foreignKey(newForeignKey.fromColumn, newForeignKey.toTable, newForeignKey.toColumn, newForeignKey.onUpdate, newForeignKey.onDelete)
1093
- );
1094
- changes.down += this.dialect.removeForeignKey(newForeignKey.fromTable, newForeignKey.fromColumn);
1095
- } else {
1096
- this.migrations.internalRecreate(structure.table, "Dialect does not support altering foreign keys");
1097
- break;
1098
- }
1099
- }
1100
- }
1101
- }
1102
- return changes;
1103
- }
1104
- /**
1105
- * Checks all columns of all tables, creates them if they do not exist in the database and modifies them if needed.
1106
- * If types, default values or primary key change, the table is recreated
1107
- */
1108
- async migrateColumns() {
1109
- const changes = {
1110
- up: "\n\n-- Migrate columns\n",
1111
- down: "\n\n-- Migrate columns\n"
1112
- };
1113
- for (const tableName in this.newTables) {
1114
- if (this.migrations.willBeRecreated(tableName) || this.tableDoesNotExists(tableName))
1115
- continue;
1116
- const newTableDefinition = this.newTables[tableName];
1117
- const oldColumnList = await this.dialect.getColumnInformation(this.migrations.getOldTableName(tableName));
1118
- const oldPrimaryKey = this.getPrimaryKeyColumn(oldColumnList);
1119
- const oldColumnNewNameIndex = {};
1120
- for (const key in oldColumnList) {
1121
- oldColumnNewNameIndex[this.migrations.getNewestColumnName(tableName, key)] = oldColumnList[key];
1122
- }
1123
- const newColumnList = newTableDefinition.columns;
1124
- const newPrimaryKey = newTableDefinition.primaryKey;
1125
- if (oldPrimaryKey != newPrimaryKey) {
1126
- Logger.log(`Primary key in ${tableName} will be changed from ${oldPrimaryKey} to ${newPrimaryKey}!`);
1127
- this.migrations.compareWithAllowedMigration(tableName, "alterPrimaryKey");
1128
- if (this.dialect.canAlterPrimaryKey) {
1129
- if (oldPrimaryKey) {
1130
- changes.up += this.dialect.removePrimaryKey(tableName, oldPrimaryKey);
1131
- changes.down += this.dialect.addPrimaryKey(tableName, oldPrimaryKey);
1132
- }
1133
- if (newPrimaryKey) {
1134
- changes.up += this.dialect.addPrimaryKey(tableName, newPrimaryKey.toString());
1135
- changes.down += this.dialect.removePrimaryKey(tableName, newPrimaryKey.toString());
1136
- }
1137
- } else
1138
- this.migrations.internalRecreate(newTableDefinition.table, "Dialect does not support altering primary keys");
1139
- continue;
1140
- }
1141
- const renamedColumns = this.migrations.getMigrationData()[tableName]?.renamedColumns;
1142
- for (const key in newColumnList) {
1143
- const newColumn = newColumnList[key];
1144
- const oldColumn = oldColumnNewNameIndex[newColumn.name];
1145
- const willBeRenamed = renamedColumns?.find((entry) => entry.newName);
1146
- if (willBeRenamed)
1147
- continue;
1148
- if (oldColumn == void 0) {
1149
- changes.up += this.dialect.createColumn(tableName, this.dialect.columnDefinition(newColumn.name, newColumn.sqlType, newColumn.isPrimaryKey, newColumn.defaultValue)) + "\n";
1150
- changes.down += this.dialect.dropColumn(tableName, newColumn.name) + "\n";
1151
- } else if (this.dialect.canAlterColumnStructure) {
1152
- changes.up += this.dialect.alterColumnStructure(tableName, newColumn.name, newColumn.sqlType, newColumn.defaultValue);
1153
- changes.down += this.dialect.alterColumnStructure(tableName, oldColumn.name, oldColumn.sqlType, oldColumn.defaultValue);
1154
- } else if (newColumn.sqlType != oldColumn.sqlType || newColumn.defaultValue != oldColumn.defaultValue) {
1155
- this.migrations.internalRecreate(newTableDefinition.table, "Table structure was changed");
1156
- }
1157
- }
1158
- for (const key in oldColumnList) {
1159
- const oldColumn = oldColumnList[key];
1160
- const newColumn = newColumnList[this.migrations.getNewestColumnName(tableName, oldColumn.name)];
1161
- if (newColumn == void 0) {
1162
- this.migrations.compareWithAllowedMigration(tableName, "dropColumn", oldColumn.name);
1163
- changes.up += this.dialect.dropColumn(tableName, oldColumn.name) + "\n";
1164
- changes.down += this.dialect.createColumn(tableName, this.dialect.columnDefinition(oldColumn.name, oldColumn.sqlType, oldColumn.isPrimaryKey, oldColumn.defaultValue)) + "\n";
1165
- }
1166
- }
1167
- }
1168
- return changes;
1169
- }
1170
- /**
1171
- * Retrieves the name of the primary key column from a list of column information.
1172
- *
1173
- * @param columnInfoEntries - An array of ColumnInfo objects containing details about each column.
1174
- * @return The name of the primary key column if found; otherwise, undefined.
1175
- */
1176
- getPrimaryKeyColumn(columnInfoEntries) {
1177
- for (const key in columnInfoEntries) {
1178
- const columnInfo = columnInfoEntries[key];
1179
- if (columnInfo.isPrimaryKey)
1180
- return columnInfo.name;
1181
- }
1182
- }
1183
- /**
1184
- * Handles the renaming of columns in database tables by generating SQL changes for both applying and reverting the renames.
1185
- * Skips tables that are marked for recreation.
1186
- */
1187
- renameColumns() {
1188
- const changes = {
1189
- up: "\n\n-- Rename columns\n",
1190
- down: "\n\n-- Rename columns\n"
1191
- };
1192
- const migrationData = this.migrations.getMigrationData();
1193
- for (const tableName in migrationData) {
1194
- if (migrationData[tableName].recreate)
1195
- continue;
1196
- this.migrations.loopRenamedColumns(tableName, (oldColumnName, newColumnName) => {
1197
- Logger.log(`Column ${tableName}.${oldColumnName} will be renamed to ${tableName}.${newColumnName}!`);
1198
- changes.up += this.dialect.renameColumn(tableName, oldColumnName, newColumnName);
1199
- changes.down += this.dialect.renameColumn(tableName, newColumnName, oldColumnName);
1200
- });
1201
- }
1202
- return changes;
1203
- }
788
+ migrations = new Migrations();
789
+ newTables = {};
790
+ /**
791
+ * Table names that currently exist in the database
792
+ * Note: If tables are set to be renamed, then names in this array will be corrected to their future name.
793
+ */
794
+ existingTables = [];
795
+ dialect;
796
+ constructor(dialect) {
797
+ this.dialect = dialect;
798
+ }
799
+ /**
800
+ * Generates the SQL changes required to migrate a database from its current version
801
+ * to the target version defined in the database instructions. Handles the creation
802
+ * and deletion of tables, migration of foreign keys and columns, renaming of columns,
803
+ * and recreating tables, along with executing any pre- and post-migration custom logic.
804
+ *
805
+ * @return A promise that resolves to an object containing the SQL
806
+ * changes for the migration (`up` and `down` changes) or null if no migration is required.
807
+ * Throws an error in scenarios such as attempting to migrate to a lower version
808
+ * or an invalid starting version.
809
+ */
810
+ async generateSqlChanges(dbInstructions) {
811
+ const fromVersion = await this.dialect.getVersion();
812
+ const toVersion = dbInstructions.version;
813
+ if (toVersion <= 0) throw new Error("Cannot migrate to version 0 or lower");
814
+ if (fromVersion == toVersion) {
815
+ Logger.log("Version has not changed. No migrations needed.");
816
+ return null;
817
+ }
818
+ if (fromVersion > toVersion) throw new Error(`You cannot create new migrations with a lower version (from ${fromVersion} to ${toVersion})`);
819
+ const changes = {
820
+ up: "",
821
+ down: ""
822
+ };
823
+ this.migrations.reset(dbInstructions, fromVersion);
824
+ const preSQL = dbInstructions.preMigration?.(this.migrations, this.dialect.db, fromVersion, toVersion);
825
+ if (preSQL) {
826
+ changes.up += `\n\n-- Custom SQL from preMigration()\n${preSQL.up}`;
827
+ changes.down += `\n\n-- Custom SQL from preMigration()\n${preSQL.down}`;
828
+ }
829
+ const tableStructureGenerator = new TableStructureGenerator(dbInstructions, this.dialect);
830
+ this.newTables = tableStructureGenerator.generateTableStructure();
831
+ if (fromVersion == 0) {
832
+ Logger.log(`Creating initial migration to version ${dbInstructions.version}`);
833
+ const initialChanges = await this.createAndDropTables();
834
+ changes.up += initialChanges.up;
835
+ changes.down += initialChanges.down;
836
+ } else {
837
+ Logger.log(`Creating migration SQL from version ${fromVersion} to ${toVersion}`);
838
+ this.existingTables = (await this.dialect.getTableNames()).filter((oldTableName) => oldTableName != MIGRATION_DATA_TABLE_NAME).map((oldTableName) => this.migrations.getNewestTableName(oldTableName));
839
+ [
840
+ this.renameTables(),
841
+ await this.createAndDropTables(),
842
+ await this.migrateForeignKeys(),
843
+ await this.migrateColumns(),
844
+ this.renameColumns(),
845
+ await this.recreateTables()
846
+ ].forEach((entry) => {
847
+ changes.up += entry.up;
848
+ changes.down += entry.down;
849
+ });
850
+ }
851
+ const postSql = dbInstructions.postMigration?.(this.dialect.db, fromVersion, toVersion);
852
+ if (postSql) {
853
+ changes.up += `\n\n-- Custom SQL from postMigration()\n${postSql.up}`;
854
+ changes.down += `\n\n-- Custom SQL from postMigration()\n${postSql.down}`;
855
+ }
856
+ if (fromVersion != 0) {
857
+ const exception = this.migrations.verifyRenamingTasks(this.newTables) ?? this.migrations.verifyAllowedMigrations();
858
+ if (exception) {
859
+ Logger.debug(`\nCanceled changes:\n${changes.up}\n`);
860
+ if (!dbInstructions.doNotThrowIfNotAllowed) throw exception;
861
+ else {
862
+ console.error(exception.message);
863
+ return null;
864
+ }
865
+ }
866
+ }
867
+ return {
868
+ fromVersion,
869
+ toVersion,
870
+ changes
871
+ };
872
+ }
873
+ renameTables() {
874
+ const changes = {
875
+ up: "\n\n-- Rename tables\n",
876
+ down: "\n\n-- Rename tables\n"
877
+ };
878
+ const data = this.migrations.getMigrationData();
879
+ for (const tableName in data) {
880
+ const migrationEntry = data[tableName];
881
+ if (migrationEntry.tableRenaming) {
882
+ Logger.log(`Table "${migrationEntry.tableRenaming.oldName}" will be renamed to "${tableName}"!`);
883
+ changes.up += this.dialect.renameTable(migrationEntry.tableRenaming.oldName, tableName);
884
+ changes.down += this.dialect.renameTable(tableName, migrationEntry.tableRenaming.oldName);
885
+ }
886
+ }
887
+ return changes;
888
+ }
889
+ /**
890
+ * Generates SQL change statements for creating and dropping tables based on the current table structure
891
+ * and existing table configurations in the database. The `up` changes include SQL statements for creating
892
+ * new tables, while the `down` changes include SQL statements for dropping the corresponding tables or
893
+ * recreating them if necessary.
894
+ *
895
+ * @return {Promise<SqlChanges>} A promise that resolves to an object containing `up` and `down` SQL
896
+ * change statements for migrating the database schema.
897
+ */
898
+ async createAndDropTables() {
899
+ let changes = {
900
+ up: "\n\n-- Create and drop tables\n",
901
+ down: "\n\n-- Create and drop tables\n"
902
+ };
903
+ for (const tableName in this.newTables) if (this.tableDoesNotExists(tableName)) {
904
+ const structure = this.newTables[tableName];
905
+ changes.up += `${this.createTableSql(tableName, structure.columns, structure.foreignKeys)}\n`;
906
+ changes.down += `${this.dialect.dropTable(tableName)}\n`;
907
+ }
908
+ for (const tableName of this.existingTables) if (!this.newTables[tableName]) {
909
+ this.migrations.compareWithAllowedMigration(tableName, "dropTable");
910
+ const dbTableName = this.migrations.getOldTableName(tableName);
911
+ changes.up += `${this.dialect.dropTable(tableName)}\n`;
912
+ changes.down += this.createTableSql(tableName, await this.dialect.getColumnInformation(dbTableName), await this.dialect.getForeignKeys(dbTableName));
913
+ }
914
+ return changes;
915
+ }
916
+ /**
917
+ * Generates an SQL string to create a table with the specified schema.
918
+ *
919
+ * @param tableName - The name of the table to be created.
920
+ * @param columns - An array of objects defining the columns of the table, including their names, types
921
+ * default values, and whether they are primary keys.
922
+ * @param foreignKeys - An optional array of objects defining the foreign key constraints for the table,
923
+ * including the source column, target table, target column, action on update, and action on delete.
924
+ * @return The complete SQL string to create the table with the specified structure and constraints.
925
+ */
926
+ createTableSql(tableName, columns, foreignKeys) {
927
+ const queryLines = [];
928
+ for (const key in columns) {
929
+ const columnInfo = columns[key];
930
+ queryLines.push(this.dialect.columnDefinition(columnInfo.name, columnInfo.sqlType, columnInfo.isPrimaryKey, columnInfo.defaultValue));
931
+ }
932
+ if (foreignKeys) for (const foreignKeyInfo of foreignKeys) queryLines.push(this.dialect.foreignKey(foreignKeyInfo.fromColumn, foreignKeyInfo.toTable, foreignKeyInfo.toColumn.toString(), foreignKeyInfo.onUpdate, foreignKeyInfo.onDelete));
933
+ return this.dialect.createTable(tableName, queryLines);
934
+ }
935
+ /**
936
+ * Checks if the specified table does not exist in the list of existing tables.
937
+ *
938
+ * @param tableName - The name of the table to check.
939
+ * @return Returns true if the table does not exist, otherwise false.
940
+ */
941
+ tableDoesNotExists(tableName) {
942
+ return this.existingTables.indexOf(tableName) == -1;
943
+ }
944
+ /**
945
+ * Recreates database tables that have been flagged to be recreated in migration data.
946
+ * This method generated the necessary SQL code for backing up the current table data, dropping and recreating
947
+ * the tables, and reinserting the preserved data into the recreated tables.
948
+ */
949
+ async recreateTables() {
950
+ const changes = {
951
+ up: "\n\n-- Recreate tables\n",
952
+ down: "\n\n-- Recreate tables\n"
953
+ };
954
+ const migrationData = this.migrations.getMigrationData();
955
+ for (const tableName in migrationData) {
956
+ if (!migrationData[tableName].recreate) continue;
957
+ if (!this.dialect.canRecreateTable) throw new Error("SQL dialect does not support recreating tables!");
958
+ this.migrations.compareWithAllowedMigration(tableName, "recreateTable");
959
+ const oldColumnList = await this.dialect.getColumnInformation(this.migrations.getOldTableName(tableName));
960
+ const newColumnList = this.newTables[tableName].columns;
961
+ const moveableColumns = [];
962
+ for (const key in newColumnList) if (oldColumnList.hasOwnProperty(key)) moveableColumns.push(newColumnList[key].name);
963
+ const backupTableName = `${tableName}__backup`;
964
+ const moveDataQuery = this.dialect.insert(backupTableName, this.dialect.insertValues(moveableColumns, this.dialect.select(tableName, moveableColumns))) + "\n" + this.dialect.dropTable(tableName) + "\n" + this.dialect.renameTable(backupTableName, tableName);
965
+ const structure = this.newTables[tableName];
966
+ changes.up += this.createTableSql(backupTableName, structure.columns, structure.foreignKeys) + "\n" + moveDataQuery + "\n\n";
967
+ const dbTableName = this.migrations.getOldTableName(tableName);
968
+ changes.down += this.createTableSql(backupTableName, await this.dialect.getColumnInformation(dbTableName), await this.dialect.getForeignKeys(dbTableName)) + "\n" + moveDataQuery;
969
+ }
970
+ return changes;
971
+ }
972
+ /**
973
+ * Migrates foreign key constraints for all defined tables.
974
+ */
975
+ async migrateForeignKeys() {
976
+ let changes = {
977
+ up: "\n\n-- Foreign keys\n",
978
+ down: "\n\n-- Foreign keys\n"
979
+ };
980
+ for (const tableName in this.newTables) {
981
+ if (this.migrations.willBeRecreated(tableName) || this.tableDoesNotExists(tableName)) continue;
982
+ const structure = this.newTables[tableName];
983
+ const newForeignKeys = structure.foreignKeys ?? [];
984
+ const oldForeignKeys = await this.dialect.getForeignKeys(this.migrations.getOldTableName(tableName));
985
+ let checkForNewForeignKeys = true;
986
+ for (const oldForeignKey of oldForeignKeys) {
987
+ const newForeignKey = newForeignKeys.find((entry) => entry.fromColumn == oldForeignKey.fromColumn);
988
+ if (!newForeignKey) {
989
+ Logger.log(`Foreign key ${oldForeignKey.fromTable}.${oldForeignKey.fromColumn} to ${oldForeignKey.toTable}.${oldForeignKey.toColumn} will be removed!`);
990
+ this.migrations.compareWithAllowedMigration(tableName, "removeForeignKey", oldForeignKey.fromColumn);
991
+ if (this.dialect.canAlterForeignKeys) {
992
+ changes.up += this.dialect.removeForeignKey(oldForeignKey.fromTable, oldForeignKey.fromColumn);
993
+ changes.down += this.dialect.addForeignKey(oldForeignKey.fromTable, this.dialect.foreignKey(oldForeignKey.fromColumn, oldForeignKey.toTable, oldForeignKey.toColumn, oldForeignKey.onUpdate, oldForeignKey.onDelete));
994
+ } else {
995
+ this.migrations.internalRecreate(structure.table, "Dialect does not support altering foreign keys");
996
+ checkForNewForeignKeys = false;
997
+ }
998
+ } else if (!newForeignKey || oldForeignKey.toColumn != newForeignKey.toColumn || (oldForeignKey.onUpdate ?? "NO ACTION") != (newForeignKey.onUpdate ?? "NO ACTION") || (oldForeignKey.onDelete ?? "NO ACTION") != (newForeignKey.onDelete ?? "NO ACTION")) {
999
+ Logger.log(`Foreign key ${oldForeignKey.fromTable}.${oldForeignKey.fromColumn} to ${oldForeignKey.toTable}.${oldForeignKey.toColumn} will be changed!`);
1000
+ this.migrations.compareWithAllowedMigration(tableName, "alterForeignKey", oldForeignKey.fromColumn);
1001
+ if (this.dialect.canAlterForeignKeys) {
1002
+ changes.up += this.dialect.removeForeignKey(oldForeignKey.fromTable, oldForeignKey.fromColumn) + this.dialect.addForeignKey(newForeignKey.fromTable, this.dialect.foreignKey(newForeignKey.fromColumn, newForeignKey.toTable, newForeignKey.toColumn, newForeignKey.onUpdate, newForeignKey.onDelete));
1003
+ changes.down += this.dialect.addForeignKey(oldForeignKey.fromTable, this.dialect.foreignKey(oldForeignKey.fromColumn, oldForeignKey.toTable, oldForeignKey.toColumn, oldForeignKey.onUpdate, oldForeignKey.onDelete)) + this.dialect.removeForeignKey(newForeignKey.fromTable, newForeignKey.fromColumn);
1004
+ } else {
1005
+ this.migrations.internalRecreate(structure.table, "Dialect does not support altering foreign keys");
1006
+ checkForNewForeignKeys = false;
1007
+ }
1008
+ }
1009
+ }
1010
+ if (checkForNewForeignKeys) for (const newForeignKey of newForeignKeys) {
1011
+ if (oldForeignKeys.find((entry) => entry.fromColumn == newForeignKey.fromColumn)) continue;
1012
+ Logger.log(`Foreign key ${newForeignKey.fromTable}.${newForeignKey.fromColumn} to ${newForeignKey.toTable}.${newForeignKey.toColumn} will be added!`);
1013
+ if (this.dialect.canAlterForeignKeys) {
1014
+ changes.up += this.dialect.addForeignKey(newForeignKey.toTable, this.dialect.foreignKey(newForeignKey.fromColumn, newForeignKey.toTable, newForeignKey.toColumn, newForeignKey.onUpdate, newForeignKey.onDelete));
1015
+ changes.down += this.dialect.removeForeignKey(newForeignKey.fromTable, newForeignKey.fromColumn);
1016
+ } else {
1017
+ this.migrations.internalRecreate(structure.table, "Dialect does not support altering foreign keys");
1018
+ break;
1019
+ }
1020
+ }
1021
+ }
1022
+ return changes;
1023
+ }
1024
+ /**
1025
+ * Checks all columns of all tables, creates them if they do not exist in the database and modifies them if needed.
1026
+ * If types, default values or primary key change, the table is recreated
1027
+ */
1028
+ async migrateColumns() {
1029
+ const changes = {
1030
+ up: "\n\n-- Migrate columns\n",
1031
+ down: "\n\n-- Migrate columns\n"
1032
+ };
1033
+ for (const tableName in this.newTables) {
1034
+ if (this.migrations.willBeRecreated(tableName) || this.tableDoesNotExists(tableName)) continue;
1035
+ const newTableDefinition = this.newTables[tableName];
1036
+ const oldColumnList = await this.dialect.getColumnInformation(this.migrations.getOldTableName(tableName));
1037
+ const oldPrimaryKey = this.getPrimaryKeyColumn(oldColumnList);
1038
+ const oldColumnNewNameIndex = {};
1039
+ for (const key in oldColumnList) oldColumnNewNameIndex[this.migrations.getNewestColumnName(tableName, key)] = oldColumnList[key];
1040
+ const newColumnList = newTableDefinition.columns;
1041
+ const newPrimaryKey = newTableDefinition.primaryKey;
1042
+ if (oldPrimaryKey != newPrimaryKey) {
1043
+ Logger.log(`Primary key in ${tableName} will be changed from ${oldPrimaryKey} to ${newPrimaryKey}!`);
1044
+ this.migrations.compareWithAllowedMigration(tableName, "alterPrimaryKey");
1045
+ if (this.dialect.canAlterPrimaryKey) {
1046
+ if (oldPrimaryKey) {
1047
+ changes.up += this.dialect.removePrimaryKey(tableName, oldPrimaryKey);
1048
+ changes.down += this.dialect.addPrimaryKey(tableName, oldPrimaryKey);
1049
+ }
1050
+ if (newPrimaryKey) {
1051
+ changes.up += this.dialect.addPrimaryKey(tableName, newPrimaryKey.toString());
1052
+ changes.down += this.dialect.removePrimaryKey(tableName, newPrimaryKey.toString());
1053
+ }
1054
+ } else this.migrations.internalRecreate(newTableDefinition.table, "Dialect does not support altering primary keys");
1055
+ continue;
1056
+ }
1057
+ const renamedColumns = this.migrations.getMigrationData()[tableName]?.renamedColumns;
1058
+ for (const key in newColumnList) {
1059
+ const newColumn = newColumnList[key];
1060
+ const oldColumn = oldColumnNewNameIndex[newColumn.name];
1061
+ if (renamedColumns?.find((entry) => entry.newName)) continue;
1062
+ if (oldColumn == void 0) {
1063
+ changes.up += this.dialect.createColumn(tableName, this.dialect.columnDefinition(newColumn.name, newColumn.sqlType, newColumn.isPrimaryKey, newColumn.defaultValue)) + "\n";
1064
+ changes.down += this.dialect.dropColumn(tableName, newColumn.name) + "\n";
1065
+ } else if (this.dialect.canAlterColumnStructure) {
1066
+ changes.up += this.dialect.alterColumnStructure(tableName, newColumn.name, newColumn.sqlType, newColumn.defaultValue);
1067
+ changes.down += this.dialect.alterColumnStructure(tableName, oldColumn.name, oldColumn.sqlType, oldColumn.defaultValue);
1068
+ } else if (newColumn.sqlType != oldColumn.sqlType || newColumn.defaultValue != oldColumn.defaultValue) this.migrations.internalRecreate(newTableDefinition.table, "Table structure was changed");
1069
+ }
1070
+ for (const key in oldColumnList) {
1071
+ const oldColumn = oldColumnList[key];
1072
+ if (newColumnList[this.migrations.getNewestColumnName(tableName, oldColumn.name)] == void 0) {
1073
+ this.migrations.compareWithAllowedMigration(tableName, "dropColumn", oldColumn.name);
1074
+ changes.up += this.dialect.dropColumn(tableName, oldColumn.name) + "\n";
1075
+ changes.down += this.dialect.createColumn(tableName, this.dialect.columnDefinition(oldColumn.name, oldColumn.sqlType, oldColumn.isPrimaryKey, oldColumn.defaultValue)) + "\n";
1076
+ }
1077
+ }
1078
+ }
1079
+ return changes;
1080
+ }
1081
+ /**
1082
+ * Retrieves the name of the primary key column from a list of column information.
1083
+ *
1084
+ * @param columnInfoEntries - An array of ColumnInfo objects containing details about each column.
1085
+ * @return The name of the primary key column if found; otherwise, undefined.
1086
+ */
1087
+ getPrimaryKeyColumn(columnInfoEntries) {
1088
+ for (const key in columnInfoEntries) {
1089
+ const columnInfo = columnInfoEntries[key];
1090
+ if (columnInfo.isPrimaryKey) return columnInfo.name;
1091
+ }
1092
+ }
1093
+ /**
1094
+ * Handles the renaming of columns in database tables by generating SQL changes for both applying and reverting the renames.
1095
+ * Skips tables that are marked for recreation.
1096
+ */
1097
+ renameColumns() {
1098
+ const changes = {
1099
+ up: "\n\n-- Rename columns\n",
1100
+ down: "\n\n-- Rename columns\n"
1101
+ };
1102
+ const migrationData = this.migrations.getMigrationData();
1103
+ for (const tableName in migrationData) {
1104
+ if (migrationData[tableName].recreate) continue;
1105
+ this.migrations.loopRenamedColumns(tableName, (oldColumnName, newColumnName) => {
1106
+ Logger.log(`Column ${tableName}.${oldColumnName} will be renamed to ${tableName}.${newColumnName}!`);
1107
+ changes.up += this.dialect.renameColumn(tableName, oldColumnName, newColumnName);
1108
+ changes.down += this.dialect.renameColumn(tableName, newColumnName, oldColumnName);
1109
+ });
1110
+ }
1111
+ return changes;
1112
+ }
1204
1113
  };
1205
-
1206
- // src/lib/tableInfo/decorators/TableClass.ts
1207
- import "polyfill-symbol-metadata";
1114
+ //#endregion
1115
+ //#region src/lib/tableInfo/decorators/TableClass.ts
1116
+ /**
1117
+ * Decorator function to configure a database table with its name and primary key.
1118
+ *
1119
+ * @param tableName - The name of the database table used in SQL.
1120
+ * @param primaryKey - The property name representing the primary property of the table.
1121
+ */
1208
1122
  function TableClass(tableName, primaryKey) {
1209
- return (table, context) => {
1210
- const tableStructure = context?.metadata ? getTableStructureFromMetadata(context.metadata) : getTableStructure(table);
1211
- tableStructure.primaryKey = primaryKey?.toString();
1212
- tableStructure.table = tableName;
1213
- if (tableStructure.foreignKeys) {
1214
- for (const foreignKey of tableStructure.foreignKeys) {
1215
- foreignKey.fromTable = tableName;
1216
- if (foreignKey.toTable == table.name)
1217
- foreignKey.toTable = tableName;
1218
- }
1219
- }
1220
- const obj = new table();
1221
- for (const key in obj) {
1222
- const value = obj[key];
1223
- const existingEntry = tableStructure.columns[key];
1224
- const defaultEntry = TableObjHelper.getColumnEntry(key, value);
1225
- if (!defaultEntry)
1226
- continue;
1227
- if (key == primaryKey)
1228
- defaultEntry.isPrimaryKey = true;
1229
- tableStructure.columns[key] = {
1230
- ...defaultEntry,
1231
- ...existingEntry
1232
- };
1233
- }
1234
- Object.defineProperty(table, "name", { value: tableName, writable: false });
1235
- };
1123
+ return (table, context) => {
1124
+ const tableStructure = context?.metadata ? getTableStructureFromMetadata(context.metadata) : getTableStructure(table);
1125
+ tableStructure.primaryKey = primaryKey?.toString();
1126
+ tableStructure.table = tableName;
1127
+ if (tableStructure.foreignKeys) for (const foreignKey of tableStructure.foreignKeys) {
1128
+ foreignKey.fromTable = tableName;
1129
+ if (foreignKey.toTable == table.name) foreignKey.toTable = tableName;
1130
+ }
1131
+ const obj = new table();
1132
+ for (const key in obj) {
1133
+ const value = obj[key];
1134
+ const existingEntry = tableStructure.columns[key];
1135
+ const defaultEntry = TableObjHelper.getColumnEntry(key, value);
1136
+ if (!defaultEntry) continue;
1137
+ if (key == primaryKey) defaultEntry.isPrimaryKey = true;
1138
+ tableStructure.columns[key] = {
1139
+ ...defaultEntry,
1140
+ ...existingEntry
1141
+ };
1142
+ }
1143
+ Object.defineProperty(table, "name", {
1144
+ value: tableName,
1145
+ writable: false
1146
+ });
1147
+ };
1236
1148
  }
1237
-
1238
- // src/lib/tableInfo/TableObj.ts
1239
- var TableObj = class _TableObj {
1240
- IS_TABLE_OBJ = true;
1241
- tableName;
1242
- tableStructure = {
1243
- table: "",
1244
- primaryKey: void 0,
1245
- columns: {},
1246
- foreignKeys: []
1247
- };
1248
- constructor(tableName, defaultValues) {
1249
- const addColumnEntry = (key, value) => {
1250
- const columnEntry = TableObjHelper.getColumnEntry(key, value);
1251
- if (!columnEntry) {
1252
- return;
1253
- }
1254
- this.tableStructure.columns[key] = columnEntry;
1255
- return columnEntry;
1256
- };
1257
- this.tableName = tableName;
1258
- this.tableStructure.table = tableName;
1259
- for (const key in defaultValues) {
1260
- const content = defaultValues[key];
1261
- if (Array.isArray(content)) {
1262
- const [value, options] = content;
1263
- const columnEntry = addColumnEntry(key, value);
1264
- if (columnEntry) {
1265
- this.applyOptions(columnEntry, options);
1266
- }
1267
- } else {
1268
- addColumnEntry(key, content);
1269
- }
1270
- }
1271
- }
1272
- applyOptions(entry, options) {
1273
- for (const keyString in options) {
1274
- const key = keyString;
1275
- switch (key) {
1276
- case "primaryKey":
1277
- if (this.tableStructure.primaryKey) {
1278
- this.tableStructure.columns[this.tableStructure.primaryKey].isPrimaryKey = false;
1279
- }
1280
- this.tableStructure.primaryKey = entry.name;
1281
- entry.isPrimaryKey = true;
1282
- break;
1283
- case "dataType":
1284
- entry.inputType = options[key];
1285
- break;
1286
- case "maxCharacterLength":
1287
- entry.maxLength = options[key];
1288
- break;
1289
- default:
1290
- throw new Error(`Unsupported option "${key}" for ${this.tableName}.${entry.name}`);
1291
- }
1292
- }
1293
- }
1294
- /**
1295
- * Creates a foreign key relationship between the current table and a target table.
1296
- *
1297
- * @param fromColumn - The column in the current table that will serve as the foreign key.
1298
- * @param toTable - The target table object that the foreign key will reference.
1299
- * @param toColumn - The column in the target table that the foreign key will reference.
1300
- * @param options - Optional configurations for the foreign key, specifying actions on update or delete.
1301
- */
1302
- foreignKey(fromColumn, toTable, toColumn, options) {
1303
- this.tableStructure.foreignKeys.push({
1304
- fromTable: this.tableName,
1305
- fromColumn: fromColumn.toString(),
1306
- toTable: toTable.tableName,
1307
- toColumn: toColumn.toString(),
1308
- onUpdate: options?.onUpdate,
1309
- onDelete: options?.onDelete
1310
- });
1311
- return this;
1312
- }
1313
- /**
1314
- * An optional helper function to deal with `TS2589: Type instantiation is excessively deep and possibly infinite. when used as method parameter`
1315
- * Does nothing in runtime
1316
- */
1317
- sealType() {
1318
- return this;
1319
- }
1320
- static create(tableName, columns) {
1321
- return new _TableObj(tableName, columns);
1322
- }
1149
+ //#endregion
1150
+ //#region src/lib/tableInfo/TableObj.ts
1151
+ /**
1152
+ * Use this class to define tables for DatabaseInstructions
1153
+ * (as an alternative for Class decorators)
1154
+ *
1155
+ */
1156
+ var TableObj = class TableObj {
1157
+ IS_TABLE_OBJ = true;
1158
+ tableName;
1159
+ tableStructure = {
1160
+ table: "",
1161
+ primaryKey: void 0,
1162
+ columns: {},
1163
+ foreignKeys: []
1164
+ };
1165
+ constructor(tableName, defaultValues) {
1166
+ const addColumnEntry = (key, value) => {
1167
+ const columnEntry = TableObjHelper.getColumnEntry(key, value);
1168
+ if (!columnEntry) return;
1169
+ this.tableStructure.columns[key] = columnEntry;
1170
+ return columnEntry;
1171
+ };
1172
+ this.tableName = tableName;
1173
+ this.tableStructure.table = tableName;
1174
+ for (const key in defaultValues) {
1175
+ const content = defaultValues[key];
1176
+ if (Array.isArray(content)) {
1177
+ const [value, options] = content;
1178
+ const columnEntry = addColumnEntry(key, value);
1179
+ if (columnEntry) this.applyOptions(columnEntry, options);
1180
+ } else addColumnEntry(key, content);
1181
+ }
1182
+ }
1183
+ applyOptions(entry, options) {
1184
+ for (const keyString in options) {
1185
+ const key = keyString;
1186
+ switch (key) {
1187
+ case "primaryKey":
1188
+ if (this.tableStructure.primaryKey) this.tableStructure.columns[this.tableStructure.primaryKey].isPrimaryKey = false;
1189
+ this.tableStructure.primaryKey = entry.name;
1190
+ entry.isPrimaryKey = true;
1191
+ break;
1192
+ case "dataType":
1193
+ entry.inputType = options[key];
1194
+ break;
1195
+ case "maxCharacterLength":
1196
+ entry.maxLength = options[key];
1197
+ break;
1198
+ default: throw new Error(`Unsupported option "${key}" for ${this.tableName}.${entry.name}`);
1199
+ }
1200
+ }
1201
+ }
1202
+ /**
1203
+ * Creates a foreign key relationship between the current table and a target table.
1204
+ *
1205
+ * @param fromColumn - The column in the current table that will serve as the foreign key.
1206
+ * @param toTable - The target table object that the foreign key will reference.
1207
+ * @param toColumn - The column in the target table that the foreign key will reference.
1208
+ * @param options - Optional configurations for the foreign key, specifying actions on update or delete.
1209
+ */
1210
+ foreignKey(fromColumn, toTable, toColumn, options) {
1211
+ this.tableStructure.foreignKeys.push({
1212
+ fromTable: this.tableName,
1213
+ fromColumn: fromColumn.toString(),
1214
+ toTable: toTable.tableName,
1215
+ toColumn: toColumn.toString(),
1216
+ onUpdate: options?.onUpdate,
1217
+ onDelete: options?.onDelete
1218
+ });
1219
+ return this;
1220
+ }
1221
+ /**
1222
+ * An optional helper function to deal with `TS2589: Type instantiation is excessively deep and possibly infinite. when used as method parameter`
1223
+ * Does nothing in runtime
1224
+ */
1225
+ sealType() {
1226
+ return this;
1227
+ }
1228
+ static create(tableName, columns) {
1229
+ return new TableObj(tableName, columns);
1230
+ }
1323
1231
  };
1324
-
1325
- // src/lib/tableInfo/decorators/DataType.ts
1326
- import "polyfill-symbol-metadata";
1232
+ //#endregion
1233
+ //#region src/lib/tableInfo/decorators/DataType.ts
1234
+ /**
1235
+ * Decorator function to assign a specific data type to a column.
1236
+ * Should be used if you want to use a data type that does not exist in JavaScript or
1237
+ * if the default value should be NULL
1238
+ *
1239
+ * @param type - The data type to be assigned.
1240
+ */
1327
1241
  function DataType(type) {
1328
- return (table, context) => {
1329
- const entry = getInputColumnInfo(table, context);
1330
- entry.inputType = type;
1331
- };
1242
+ return (table, context) => {
1243
+ const entry = getInputColumnInfo(table, context);
1244
+ entry.inputType = type;
1245
+ };
1332
1246
  }
1333
-
1334
- // src/lib/tableInfo/decorators/ForeignKey.ts
1335
- import "polyfill-symbol-metadata";
1247
+ //#endregion
1248
+ //#region src/lib/tableInfo/decorators/ForeignKey.ts
1249
+ /**
1250
+ * Decorator function to define a foreign key relationship for a database table.
1251
+ *
1252
+ * @param toTable - The target table class to which the foreign key points.
1253
+ * @param toColumn - The column name in the target table to which the foreign key points.
1254
+ * @param options - Additional options.
1255
+ */
1336
1256
  function ForeignKey(toTable, toColumn, options) {
1337
- return (table, context) => {
1338
- const metadata = context?.metadata ? getTableStructureFromMetadata(context.metadata) : getTableStructure(table);
1339
- if (!metadata.foreignKeys)
1340
- metadata.foreignKeys = [];
1341
- metadata.foreignKeys.push({
1342
- fromTable: "",
1343
- fromColumn: context.name ?? context,
1344
- //to stay compatible with old decorator structure (see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#decorator-metadata)
1345
- toTable: toTable.name,
1346
- toColumn: toColumn.toString(),
1347
- onDelete: options?.onDelete,
1348
- onUpdate: options?.onUpdate
1349
- });
1350
- };
1257
+ return (table, context) => {
1258
+ const metadata = context?.metadata ? getTableStructureFromMetadata(context.metadata) : getTableStructure(table);
1259
+ if (!metadata.foreignKeys) metadata.foreignKeys = [];
1260
+ metadata.foreignKeys.push({
1261
+ fromTable: "",
1262
+ fromColumn: context.name ?? context,
1263
+ toTable: toTable.name,
1264
+ toColumn: toColumn.toString(),
1265
+ onDelete: options?.onDelete,
1266
+ onUpdate: options?.onUpdate
1267
+ });
1268
+ };
1351
1269
  }
1352
-
1353
- // src/lib/dialects/SqliteDialect.ts
1270
+ //#endregion
1271
+ //#region src/lib/dialects/SqliteDialect.ts
1272
+ /**
1273
+ * SQLite-specific syntax for SQL queries
1274
+ */
1354
1275
  var SqliteDialect = class extends DefaultSql {
1355
- types = {
1356
- text: "TEXT",
1357
- string: "TEXT",
1358
- number: "INTEGER",
1359
- bigint: "INTEGER",
1360
- boolean: "INTEGER",
1361
- date: "INTEGER",
1362
- time: "INTEGER",
1363
- dateTime: "INTEGER"
1364
- };
1365
- canRecreateTable = true;
1366
- canAlterColumnStructure = false;
1367
- canAlterForeignKeys = false;
1368
- canAlterPrimaryKey = false;
1369
- async runTransactionWithoutForeignKeys(query) {
1370
- await this.db.runWriteStatement(`PRAGMA foreign_keys = OFF;`);
1371
- await this.db.runWriteStatement(`PRAGMA defer_foreign_keys = ON;`);
1372
- await this.db.runTransaction(query);
1373
- await this.db.runWriteStatement(`PRAGMA foreign_keys = ON;`);
1374
- await this.db.runWriteStatement(`PRAGMA foreign_key_check;`);
1375
- await this.db.runWriteStatement(`PRAGMA defer_foreign_keys = OFF;`);
1376
- }
1377
- async getTableNames() {
1378
- return (await this.db.runReadStatement("SELECT name FROM sqlite_master WHERE type = 'table'")).map((obj) => obj.name);
1379
- }
1380
- async getColumnInformation(tableName) {
1381
- const data = await this.db.runReadStatement(`PRAGMA table_info(${tableName})`);
1382
- const output = {};
1383
- for (const entry of data) {
1384
- output[entry["name"]] = {
1385
- name: entry["name"],
1386
- sqlType: entry["type"],
1387
- defaultValue: entry["dflt_value"] ?? this.nullType,
1388
- isPrimaryKey: entry["pk"] == "1"
1389
- };
1390
- }
1391
- return output;
1392
- }
1393
- formatValueToSql(value, type) {
1394
- function toDate(value2) {
1395
- return value2 instanceof Date ? value2 : new Date(value2);
1396
- }
1397
- switch (type) {
1398
- case "boolean":
1399
- return value ? "1" : "0";
1400
- case "date":
1401
- case "time":
1402
- case "dateTime":
1403
- return toDate(value).getTime().toString();
1404
- default:
1405
- return super.formatValueToSql(value, type);
1406
- }
1407
- }
1408
- async getForeignKeys(tableName) {
1409
- const data = await this.db.runReadStatement(`PRAGMA foreign_key_list(${tableName});`);
1410
- return data.map((entry) => {
1411
- return {
1412
- fromTable: tableName,
1413
- fromColumn: entry["from"],
1414
- toTable: entry["table"],
1415
- toColumn: entry["to"],
1416
- onUpdate: entry["on_update"] ?? "NO ACTION",
1417
- onDelete: entry["on_delete"] ?? "NO ACTION"
1418
- };
1419
- });
1420
- }
1421
- async setChanges(fromVersion, toVersion, changes) {
1422
- await this.db.runTransaction(`PRAGMA user_version = ${toVersion};`);
1423
- await super.setChanges(fromVersion, toVersion, changes);
1424
- }
1425
- async getVersion() {
1426
- const output = await this.db.runReadStatement(`PRAGMA user_version;`);
1427
- return output[0].user_version;
1428
- }
1429
- async rollbackHistory(toVersion) {
1430
- await this.db.runTransaction(`PRAGMA user_version = ${toVersion};`);
1431
- await super.rollbackHistory(toVersion);
1432
- }
1276
+ types = {
1277
+ text: "TEXT",
1278
+ string: "TEXT",
1279
+ number: "INTEGER",
1280
+ bigint: "INTEGER",
1281
+ boolean: "INTEGER",
1282
+ date: "INTEGER",
1283
+ time: "INTEGER",
1284
+ dateTime: "INTEGER"
1285
+ };
1286
+ canRecreateTable = true;
1287
+ canAlterColumnStructure = false;
1288
+ canAlterForeignKeys = false;
1289
+ canAlterPrimaryKey = false;
1290
+ formatIdentifier(identifier) {
1291
+ return `"${identifier}"`;
1292
+ }
1293
+ async runTransactionWithoutForeignKeys(query) {
1294
+ await this.db.runWriteStatement(`PRAGMA foreign_keys = OFF;`);
1295
+ await this.db.runWriteStatement(`PRAGMA defer_foreign_keys = ON;`);
1296
+ await this.db.runTransaction(query);
1297
+ await this.db.runWriteStatement(`PRAGMA foreign_keys = ON;`);
1298
+ await this.db.runWriteStatement(`PRAGMA foreign_key_check;`);
1299
+ await this.db.runWriteStatement(`PRAGMA defer_foreign_keys = OFF;`);
1300
+ }
1301
+ async getTableNames() {
1302
+ return (await this.db.runReadStatement("SELECT name FROM sqlite_master WHERE type = 'table'")).map((obj) => obj.name);
1303
+ }
1304
+ async getColumnInformation(tableName) {
1305
+ const data = await this.db.runReadStatement(`PRAGMA table_info(${tableName})`);
1306
+ const output = {};
1307
+ for (const entry of data) output[entry["name"]] = {
1308
+ name: entry["name"],
1309
+ sqlType: entry["type"],
1310
+ defaultValue: entry["dflt_value"] ?? this.nullType,
1311
+ isPrimaryKey: entry["pk"] == "1"
1312
+ };
1313
+ return output;
1314
+ }
1315
+ formatValueToSql(value, type) {
1316
+ function toDate(value) {
1317
+ return value instanceof Date ? value : new Date(value);
1318
+ }
1319
+ switch (type) {
1320
+ case "boolean": return value ? "1" : "0";
1321
+ case "date":
1322
+ case "time":
1323
+ case "dateTime": return toDate(value).getTime().toString();
1324
+ default: return super.formatValueToSql(value, type);
1325
+ }
1326
+ }
1327
+ async getForeignKeys(tableName) {
1328
+ return (await this.db.runReadStatement(`PRAGMA foreign_key_list(${tableName});`)).map((entry) => {
1329
+ return {
1330
+ fromTable: tableName,
1331
+ fromColumn: entry["from"],
1332
+ toTable: entry["table"],
1333
+ toColumn: entry["to"],
1334
+ onUpdate: entry["on_update"] ?? "NO ACTION",
1335
+ onDelete: entry["on_delete"] ?? "NO ACTION"
1336
+ };
1337
+ });
1338
+ }
1339
+ async setChanges(fromVersion, toVersion, changes) {
1340
+ await this.db.runTransaction(`PRAGMA user_version = ${toVersion};`);
1341
+ await super.setChanges(fromVersion, toVersion, changes);
1342
+ }
1343
+ async getVersion() {
1344
+ return (await this.db.runReadStatement(`PRAGMA user_version;`))[0].user_version;
1345
+ }
1346
+ async rollbackHistory(toVersion) {
1347
+ await this.db.runTransaction(`PRAGMA user_version = ${toVersion};`);
1348
+ await super.rollbackHistory(toVersion);
1349
+ }
1433
1350
  };
1434
-
1435
- // src/lib/dialects/PostgresDialect.ts
1351
+ //#endregion
1352
+ //#region src/lib/dialects/PostgresDialect.ts
1436
1353
  var PostgresDialect = class extends DefaultSql {
1437
- canRecreateTable = false;
1438
- canAlterColumnStructure = true;
1439
- canAlterForeignKeys = true;
1440
- canAlterPrimaryKey = true;
1441
- types = {
1442
- text: "TEXT",
1443
- string: "VARCHAR",
1444
- number: "NUMERIC",
1445
- bigint: "BIGINT",
1446
- boolean: "BOOLEAN",
1447
- date: "DATE",
1448
- time: "TIME",
1449
- dateTime: "TIMESTAMP"
1450
- };
1451
- formatIdentifier(identifier) {
1452
- return `"${identifier}"`;
1453
- }
1454
- alterColumnStructure(tableName, columnName, sqlType, defaultValue) {
1455
- return `ALTER TABLE ${tableName} ALTER COLUMN ${columnName} TYPE ${sqlType}; ALTER TABLE ${tableName} ALTER COLUMN ${columnName} ${defaultValue ? `SET DEFAULT ${defaultValue}` : "DROP DEFAULT"};`;
1456
- }
1457
- async runTransactionWithoutForeignKeys(query) {
1458
- await this.db.runTransaction(`SET session_replication_role = 'replica'; ${query}; SET session_replication_role = 'origin';`);
1459
- }
1460
- addForeignKey(fromTableName, foreignKey) {
1461
- return `ALTER TABLE ${this.formatIdentifier(fromTableName)} ADD CONSTRAINT ${this.formatIdentifier(fromTableName)} ${foreignKey};`;
1462
- }
1463
- removeForeignKey(tableName, foreignKeyName) {
1464
- return `ALTER TABLE ${this.formatIdentifier(tableName)} DROP CONSTRAINT ${foreignKeyName};`;
1465
- }
1466
- async getTableNames() {
1467
- const result = await this.db.runReadStatement("SELECT table_name FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE';");
1468
- return result.map((row) => row.table_name);
1469
- }
1470
- async getColumnInformation(tableName) {
1471
- const data = await this.db.runReadStatement(`SELECT c.column_name as column_name, data_type, column_default, constraint_type FROM information_schema.columns c LEFT JOIN information_schema.key_column_usage cu ON cu.table_name = c.table_name AND cu.column_name = c.column_name LEFT JOIN information_schema.table_constraints tc ON tc.constraint_name = cu.constraint_name WHERE c.table_schema = 'public' AND c.table_name = '${tableName}';`);
1472
- const output = {};
1473
- for (const entry of data) {
1474
- const defaultValue = entry["column_default"]?.match(/^'(.+)'::.+$/);
1475
- let dataType = entry["data_type"].toUpperCase();
1476
- switch (dataType) {
1477
- case "INTEGER":
1478
- dataType = this.types.number;
1479
- break;
1480
- case "CHARACTER VARYING":
1481
- dataType = this.types.string;
1482
- break;
1483
- case "TIMESTAMP WITHOUT TIME ZONE":
1484
- case "TIMESTAMP WITH TIME ZONE":
1485
- dataType = this.types.dateTime;
1486
- break;
1487
- case "TIME WITHOUT TIME ZONE":
1488
- case "TIME WITH TIME ZONE":
1489
- dataType = this.types.time;
1490
- break;
1491
- case "DATE WITHOUT TIME ZONE":
1492
- case "DATE WITH TIME ZONE":
1493
- dataType = this.types.date;
1494
- break;
1495
- }
1496
- output[entry["column_name"]] = {
1497
- name: entry["column_name"],
1498
- sqlType: dataType,
1499
- defaultValue: defaultValue ? `'${defaultValue[1]}'` : entry["column_default"] ?? this.nullType,
1500
- isPrimaryKey: entry["constraint_type"] == "PRIMARY KEY"
1501
- };
1502
- }
1503
- return output;
1504
- }
1505
- addPrimaryKey(tableName, columnName) {
1506
- return `ALTER TABLE ${tableName} ADD PRIMARY KEY (${columnName});
1507
- `;
1508
- }
1509
- removePrimaryKey(tableName, columnName) {
1510
- return `ALTER TABLE ${tableName} DROP CONSTRAINT ${columnName};
1511
- `;
1512
- }
1513
- async getForeignKeys(tableName) {
1514
- const data = await this.db.runReadStatement(
1515
- `SELECT
1354
+ canRecreateTable = false;
1355
+ canAlterColumnStructure = true;
1356
+ canAlterForeignKeys = true;
1357
+ canAlterPrimaryKey = true;
1358
+ types = {
1359
+ text: "TEXT",
1360
+ string: "VARCHAR",
1361
+ number: "NUMERIC",
1362
+ bigint: "BIGINT",
1363
+ boolean: "BOOLEAN",
1364
+ date: "DATE",
1365
+ time: "TIME",
1366
+ dateTime: "TIMESTAMP"
1367
+ };
1368
+ formatIdentifier(identifier) {
1369
+ return `"${identifier}"`;
1370
+ }
1371
+ alterColumnStructure(tableName, columnName, sqlType, defaultValue) {
1372
+ return `ALTER TABLE ${tableName} ALTER COLUMN ${columnName} TYPE ${sqlType}; ALTER TABLE ${tableName} ALTER COLUMN ${columnName} ${defaultValue ? `SET DEFAULT ${defaultValue}` : "DROP DEFAULT"};`;
1373
+ }
1374
+ async runTransactionWithoutForeignKeys(query) {
1375
+ await this.db.runTransaction(`SET session_replication_role = 'replica'; ${query}; SET session_replication_role = 'origin';`);
1376
+ }
1377
+ addForeignKey(fromTableName, foreignKey) {
1378
+ return `ALTER TABLE ${this.formatIdentifier(fromTableName)} ADD CONSTRAINT ${this.formatIdentifier(fromTableName)} ${foreignKey};`;
1379
+ }
1380
+ removeForeignKey(tableName, foreignKeyName) {
1381
+ return `ALTER TABLE ${this.formatIdentifier(tableName)} DROP CONSTRAINT ${foreignKeyName};`;
1382
+ }
1383
+ async getTableNames() {
1384
+ return (await this.db.runReadStatement("SELECT table_name FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE';")).map((row) => row.table_name);
1385
+ }
1386
+ async getColumnInformation(tableName) {
1387
+ const data = await this.db.runReadStatement(`SELECT c.column_name as column_name, data_type, column_default, constraint_type FROM information_schema.columns c LEFT JOIN information_schema.key_column_usage cu ON cu.table_name = c.table_name AND cu.column_name = c.column_name LEFT JOIN information_schema.table_constraints tc ON tc.constraint_name = cu.constraint_name WHERE c.table_schema = 'public' AND c.table_name = '${tableName}';`);
1388
+ const output = {};
1389
+ for (const entry of data) {
1390
+ const defaultValue = entry["column_default"]?.match(/^'(.+)'::.+$/);
1391
+ let dataType = entry["data_type"].toUpperCase();
1392
+ switch (dataType) {
1393
+ case "INTEGER":
1394
+ dataType = this.types.number;
1395
+ break;
1396
+ case "CHARACTER VARYING":
1397
+ dataType = this.types.string;
1398
+ break;
1399
+ case "TIMESTAMP WITHOUT TIME ZONE":
1400
+ case "TIMESTAMP WITH TIME ZONE":
1401
+ dataType = this.types.dateTime;
1402
+ break;
1403
+ case "TIME WITHOUT TIME ZONE":
1404
+ case "TIME WITH TIME ZONE":
1405
+ dataType = this.types.time;
1406
+ break;
1407
+ case "DATE WITHOUT TIME ZONE":
1408
+ case "DATE WITH TIME ZONE": dataType = this.types.date;
1409
+ }
1410
+ output[entry["column_name"]] = {
1411
+ name: entry["column_name"],
1412
+ sqlType: dataType,
1413
+ defaultValue: defaultValue ? `'${defaultValue[1]}'` : entry["column_default"] ?? this.nullType,
1414
+ isPrimaryKey: entry["constraint_type"] == "PRIMARY KEY"
1415
+ };
1416
+ }
1417
+ return output;
1418
+ }
1419
+ addPrimaryKey(tableName, columnName) {
1420
+ return `ALTER TABLE ${tableName} ADD PRIMARY KEY (${columnName});\n`;
1421
+ }
1422
+ removePrimaryKey(tableName, columnName) {
1423
+ return `ALTER TABLE ${tableName} DROP CONSTRAINT ${columnName};\n`;
1424
+ }
1425
+ async getForeignKeys(tableName) {
1426
+ return (await this.db.runReadStatement(`SELECT
1516
1427
  tc.table_name AS child_table,
1517
1428
  kcu.column_name AS child_column,
1518
1429
  ccu.table_name AS parent_table,
@@ -1526,177 +1437,157 @@ var PostgresDialect = class extends DefaultSql {
1526
1437
  ON tc.constraint_name = ccu.constraint_name
1527
1438
  JOIN information_schema.referential_constraints AS rc
1528
1439
  ON tc.constraint_name=rc.constraint_name
1529
- WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name = '${tableName}';`
1530
- );
1531
- return data.map((entry) => {
1532
- return {
1533
- fromTable: tableName,
1534
- fromColumn: entry["child_column"],
1535
- toTable: entry["parent_table"],
1536
- toColumn: entry["parent_column"],
1537
- onUpdate: entry["update_rule"],
1538
- onDelete: entry["delete_rule"]
1539
- };
1540
- });
1541
- }
1440
+ WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name = '${tableName}';`)).map((entry) => {
1441
+ return {
1442
+ fromTable: tableName,
1443
+ fromColumn: entry["child_column"],
1444
+ toTable: entry["parent_table"],
1445
+ toColumn: entry["parent_column"],
1446
+ onUpdate: entry["update_rule"],
1447
+ onDelete: entry["delete_rule"]
1448
+ };
1449
+ });
1450
+ }
1542
1451
  };
1543
-
1544
- // src/lib/dialects/MySqlDialect.ts
1452
+ //#endregion
1453
+ //#region src/lib/dialects/MySqlDialect.ts
1545
1454
  var MySqlDialect = class extends DefaultSql {
1546
- canRecreateTable = true;
1547
- canAlterColumnStructure = true;
1548
- canAlterForeignKeys = true;
1549
- canAlterPrimaryKey = true;
1550
- types = {
1551
- text: "TEXT",
1552
- string: "VARCHAR",
1553
- number: "INT",
1554
- bigint: "BIGINT",
1555
- boolean: "TINYINT(1)",
1556
- date: "DATE",
1557
- time: "TIME",
1558
- dateTime: "DATETIME"
1559
- };
1560
- formatValueToSql(value, type) {
1561
- switch (type) {
1562
- case "boolean":
1563
- return value ? "1" : "0";
1564
- default:
1565
- return super.formatValueToSql(value, type);
1566
- }
1567
- }
1568
- getSqlType(dataType, columnInfo) {
1569
- switch (dataType) {
1570
- case "string":
1571
- return this.types[dataType] + `(${columnInfo?.maxLength ?? 100})`;
1572
- default:
1573
- return super.getSqlType(dataType, columnInfo);
1574
- }
1575
- }
1576
- alterColumnStructure(tableName, columnName, sqlType, defaultValue) {
1577
- const typeQuery = `ALTER TABLE ${tableName} MODIFY COLUMN ${columnName} ${sqlType}`;
1578
- return defaultValue ? `${typeQuery} DEFAULT ${defaultValue}` : `${typeQuery}; ALTER TABLE ${tableName} ALTER ${columnName} DROP DEFAULT;`;
1579
- }
1580
- async runTransactionWithoutForeignKeys(query) {
1581
- await this.db.runWriteStatement("SET FOREIGN_KEY_CHECKS = 0;");
1582
- await this.db.runTransaction(query);
1583
- await this.db.runWriteStatement("SET FOREIGN_KEY_CHECKS = 1;");
1584
- }
1585
- addForeignKey(fromTableName, foreignKey) {
1586
- return `ALTER TABLE ${fromTableName} ADD ${foreignKey};`;
1587
- }
1588
- removeForeignKey(tableName, foreignKeyName) {
1589
- return `ALTER TABLE ${tableName} DROP FOREIGN KEY ${foreignKeyName};`;
1590
- }
1591
- addPrimaryKey(tableName, columnName) {
1592
- return `ALTER TABLE ${tableName} PRIMARY KEY (${columnName});`;
1593
- }
1594
- removePrimaryKey(tableName, _columnName) {
1595
- return `ALTER TABLE ${tableName} PRIMARY KEY;`;
1596
- }
1597
- async getForeignKeys(tableName) {
1598
- const data = await this.db.runReadStatement(
1599
- `SELECT
1455
+ canRecreateTable = true;
1456
+ canAlterColumnStructure = true;
1457
+ canAlterForeignKeys = true;
1458
+ canAlterPrimaryKey = true;
1459
+ types = {
1460
+ text: "TEXT",
1461
+ string: "VARCHAR",
1462
+ number: "INT",
1463
+ bigint: "BIGINT",
1464
+ boolean: "TINYINT(1)",
1465
+ date: "DATE",
1466
+ time: "TIME",
1467
+ dateTime: "DATETIME"
1468
+ };
1469
+ formatValueToSql(value, type) {
1470
+ switch (type) {
1471
+ case "boolean": return value ? "1" : "0";
1472
+ default: return super.formatValueToSql(value, type);
1473
+ }
1474
+ }
1475
+ getSqlType(dataType, columnInfo) {
1476
+ switch (dataType) {
1477
+ case "string": return this.types[dataType] + `(${columnInfo?.maxLength ?? 100})`;
1478
+ default: return super.getSqlType(dataType, columnInfo);
1479
+ }
1480
+ }
1481
+ alterColumnStructure(tableName, columnName, sqlType, defaultValue) {
1482
+ const typeQuery = `ALTER TABLE ${tableName} MODIFY COLUMN ${columnName} ${sqlType}`;
1483
+ return defaultValue ? `${typeQuery} DEFAULT ${defaultValue}` : `${typeQuery}; ALTER TABLE ${tableName} ALTER ${columnName} DROP DEFAULT;`;
1484
+ }
1485
+ async runTransactionWithoutForeignKeys(query) {
1486
+ await this.db.runWriteStatement("SET FOREIGN_KEY_CHECKS = 0;");
1487
+ await this.db.runTransaction(query);
1488
+ await this.db.runWriteStatement("SET FOREIGN_KEY_CHECKS = 1;");
1489
+ }
1490
+ addForeignKey(fromTableName, foreignKey) {
1491
+ return `ALTER TABLE ${fromTableName} ADD ${foreignKey};`;
1492
+ }
1493
+ removeForeignKey(tableName, foreignKeyName) {
1494
+ return `ALTER TABLE ${tableName} DROP FOREIGN KEY ${foreignKeyName};`;
1495
+ }
1496
+ addPrimaryKey(tableName, columnName) {
1497
+ return `ALTER TABLE ${tableName} PRIMARY KEY (${columnName});`;
1498
+ }
1499
+ removePrimaryKey(tableName, _columnName) {
1500
+ return `ALTER TABLE ${tableName} PRIMARY KEY;`;
1501
+ }
1502
+ async getForeignKeys(tableName) {
1503
+ return (await this.db.runReadStatement(`SELECT
1600
1504
  info.TABLE_NAME, info.COLUMN_NAME, info.REFERENCED_TABLE_NAME, info.REFERENCED_COLUMN_NAME, constr.UPDATE_RULE, constr.DELETE_RULE
1601
1505
  FROM information_schema.KEY_COLUMN_USAGE AS info INNER JOIN information_schema.REFERENTIAL_CONSTRAINTS AS constr ON info.CONSTRAINT_NAME = constr.CONSTRAINT_NAME
1602
- WHERE info.REFERENCED_TABLE_SCHEMA = (SELECT DATABASE()) AND info.TABLE_NAME = '${tableName}';`
1603
- );
1604
- return data.map((entry) => {
1605
- return {
1606
- fromTable: tableName,
1607
- fromColumn: entry["COLUMN_NAME"],
1608
- toTable: entry["REFERENCED_TABLE_NAME"],
1609
- toColumn: entry["REFERENCED_COLUMN_NAME"],
1610
- onUpdate: entry["UPDATE_RULE"],
1611
- onDelete: entry["DELETE_RULE"]
1612
- };
1613
- });
1614
- }
1615
- async getTableNames() {
1616
- const entries = await this.db.runReadStatement("SHOW TABLES");
1617
- return entries.map((entry) => Object.values(entry)[0]);
1618
- }
1619
- async getColumnInformation(tableName) {
1620
- const data = await this.db.runReadStatement(`SHOW COLUMNS FROM ${tableName};`);
1621
- const output = {};
1622
- for (const entry of data) {
1623
- const sqlType = entry["Type"].toUpperCase();
1624
- const info = {
1625
- name: entry["Field"],
1626
- sqlType,
1627
- isPrimaryKey: entry["Key"] == "PRI"
1628
- };
1629
- switch (sqlType.substring(0, 4)) {
1630
- case "TEXT":
1631
- case "VARC":
1632
- case "DATE":
1633
- case "TIME":
1634
- info.defaultValue = entry["Default"] ? `'${entry["Default"]}'` : this.nullType;
1635
- break;
1636
- default:
1637
- info.defaultValue = entry["Default"] ?? this.nullType;
1638
- }
1639
- output[entry["Field"]] = info;
1640
- }
1641
- return output;
1642
- }
1506
+ WHERE info.REFERENCED_TABLE_SCHEMA = (SELECT DATABASE()) AND info.TABLE_NAME = '${tableName}';`)).map((entry) => {
1507
+ return {
1508
+ fromTable: tableName,
1509
+ fromColumn: entry["COLUMN_NAME"],
1510
+ toTable: entry["REFERENCED_TABLE_NAME"],
1511
+ toColumn: entry["REFERENCED_COLUMN_NAME"],
1512
+ onUpdate: entry["UPDATE_RULE"],
1513
+ onDelete: entry["DELETE_RULE"]
1514
+ };
1515
+ });
1516
+ }
1517
+ async getTableNames() {
1518
+ return (await this.db.runReadStatement("SHOW TABLES")).map((entry) => Object.values(entry)[0]);
1519
+ }
1520
+ async getColumnInformation(tableName) {
1521
+ const data = await this.db.runReadStatement(`SHOW COLUMNS FROM ${tableName};`);
1522
+ const output = {};
1523
+ for (const entry of data) {
1524
+ const sqlType = entry["Type"].toUpperCase();
1525
+ const info = {
1526
+ name: entry["Field"],
1527
+ sqlType,
1528
+ isPrimaryKey: entry["Key"] == "PRI"
1529
+ };
1530
+ switch (sqlType.substring(0, 4)) {
1531
+ case "TEXT":
1532
+ case "VARC":
1533
+ case "DATE":
1534
+ case "TIME":
1535
+ info.defaultValue = entry["Default"] ? `'${entry["Default"]}'` : this.nullType;
1536
+ break;
1537
+ default: info.defaultValue = entry["Default"] ?? this.nullType;
1538
+ }
1539
+ output[entry["Field"]] = info;
1540
+ }
1541
+ return output;
1542
+ }
1643
1543
  };
1644
-
1645
- // src/lib/getDialect.ts
1544
+ //#endregion
1545
+ //#region src/lib/getDialect.ts
1646
1546
  function getDialect(db, dialect) {
1647
- switch (dialect) {
1648
- case "Sqlite":
1649
- return new SqliteDialect(db);
1650
- case "Postgres":
1651
- return new PostgresDialect(db);
1652
- case "MySql":
1653
- return new MySqlDialect(db);
1654
- default:
1655
- throw new Error(`Unknown dialect ${dialect}`);
1656
- }
1547
+ switch (dialect) {
1548
+ case "Sqlite": return new SqliteDialect(db);
1549
+ case "Postgres": return new PostgresDialect(db);
1550
+ case "MySql": return new MySqlDialect(db);
1551
+ default: throw new Error(`Unknown dialect ${dialect}`);
1552
+ }
1657
1553
  }
1658
-
1659
- // src/index.ts
1554
+ //#endregion
1555
+ //#region src/index.ts
1660
1556
  async function runMigration(db, dbInstructions) {
1661
- Logger.setMode(dbInstructions.loggerMode);
1662
- const dialect = getDialect(db, dbInstructions.dialect);
1663
- const mm = new MigrationManager(dialect);
1664
- const migration = await mm.generateSqlChanges(dbInstructions);
1665
- if (migration) {
1666
- await dialect.setChanges(migration.fromVersion, dbInstructions.version, migration.changes);
1667
- await db.createBackup?.(`from_${migration.fromVersion}_to_${dbInstructions.version}`);
1668
- await dialect.runTransactionWithoutForeignKeys(migration.changes.up);
1669
- }
1557
+ Logger.setMode(dbInstructions.loggerMode);
1558
+ const dialect = getDialect(db, dbInstructions.dialect);
1559
+ const migration = await new MigrationManager(dialect).generateSqlChanges(dbInstructions);
1560
+ if (migration) {
1561
+ await db.createBackup?.(`from_${migration.fromVersion}_to_${dbInstructions.version}`);
1562
+ await dialect.runTransactionWithoutForeignKeys(migration.changes.up);
1563
+ await dialect.setChanges(migration.fromVersion, dbInstructions.version, migration.changes);
1564
+ }
1670
1565
  }
1566
+ /**
1567
+ * Rolls back the database to the provided toVersion
1568
+ * @param dbAccess - Access to the database.
1569
+ * @param toVersion - the version to which the database should be upgraded to.
1570
+ * @param dialect - the dialect that should be used for the database. Usually is the value defined in DatabaseInstructions
1571
+ * @param loggerMode - defines how much should be logged to the console.
1572
+ */
1671
1573
  async function rollback(dbAccess, toVersion, dialect, loggerMode) {
1672
- Logger.setMode(loggerMode);
1673
- const sql = getDialect(dbAccess, dialect);
1674
- const fromVersion = await sql.getVersion();
1675
- await dbAccess.createBackup?.(`from_${fromVersion}_to_${toVersion}`);
1676
- Logger.log(`Rolling back from ${fromVersion} to ${toVersion}`);
1677
- let version = fromVersion;
1678
- while (version > toVersion) {
1679
- const changes = await sql.getChanges(version);
1680
- if (!changes) {
1681
- throw new Error(`No changes found for version ${version}`);
1682
- } else if (changes.fromVersion == version) {
1683
- throw new Error(`Saved changes do not change the database version (${version})`);
1684
- }
1685
- Logger.debug(changes.down);
1686
- await sql.runTransactionWithoutForeignKeys(changes.down);
1687
- version = changes.fromVersion;
1688
- }
1689
- if (version < toVersion) {
1690
- Logger.warn(`There was no entry for version ${toVersion}. Rolled back to version ${version}`);
1691
- }
1692
- await sql.rollbackHistory(version);
1693
- return version;
1574
+ Logger.setMode(loggerMode);
1575
+ const sql = getDialect(dbAccess, dialect);
1576
+ const fromVersion = await sql.getVersion();
1577
+ await dbAccess.createBackup?.(`from_${fromVersion}_to_${toVersion}`);
1578
+ Logger.log(`Rolling back from ${fromVersion} to ${toVersion}`);
1579
+ let version = fromVersion;
1580
+ while (version > toVersion) {
1581
+ const changes = await sql.getChanges(version);
1582
+ if (!changes) throw new Error(`Unexpected error! No changes were found from version ${version} (fromVersion: ${fromVersion}, toVersion: ${toVersion})`);
1583
+ else if (changes.fromVersion == version) throw new Error(`Saved changes do not change the database version (${version})`);
1584
+ Logger.debug(changes.down);
1585
+ await sql.runTransactionWithoutForeignKeys(changes.down);
1586
+ version = changes.fromVersion;
1587
+ }
1588
+ if (version < toVersion) Logger.warn(`There was no entry for version ${toVersion}. Rolled back to version ${version}`);
1589
+ await sql.rollbackHistory(version);
1590
+ return version;
1694
1591
  }
1695
- export {
1696
- DataType,
1697
- ForeignKey,
1698
- TableClass,
1699
- TableObj,
1700
- rollback,
1701
- runMigration
1702
- };
1592
+ //#endregion
1593
+ export { DataType, ForeignKey, TableClass, TableObj, rollback, runMigration };