mirror of
https://github.com/strapi/strapi.git
synced 2025-09-02 21:32:43 +00:00
fix(database): delete indexes before creating if they already exist
This commit is contained in:
commit
a95edaa158
@ -1,8 +1,10 @@
|
|||||||
import type { Database } from '..';
|
import type { Database } from '..';
|
||||||
import type { Schema } from '../schema';
|
import type { ForeignKey, Index, Schema } from '../schema';
|
||||||
|
|
||||||
export interface SchemaInspector {
|
export interface SchemaInspector {
|
||||||
getSchema(): Promise<Schema>;
|
getSchema(): Promise<Schema>;
|
||||||
|
getIndexes(tableName: string): Promise<Index[]>;
|
||||||
|
getForeignKeys(tableName: string): Promise<ForeignKey[]>;
|
||||||
getTables(): Promise<string[]>;
|
getTables(): Promise<string[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,6 +73,16 @@ export default (db: Database) => {
|
|||||||
const forceMigration = db.config.settings?.forceMigration;
|
const forceMigration = db.config.settings?.forceMigration;
|
||||||
|
|
||||||
await db.dialect.startSchemaUpdate();
|
await db.dialect.startSchemaUpdate();
|
||||||
|
|
||||||
|
// Pre-fetch metadata for all updated tables
|
||||||
|
const existingMetadata: Record<string, { indexes: Index[]; foreignKeys: ForeignKey[] }> = {};
|
||||||
|
for (const table of schemaDiff.tables.updated) {
|
||||||
|
existingMetadata[table.name] = {
|
||||||
|
indexes: await db.dialect.schemaInspector.getIndexes(table.name),
|
||||||
|
foreignKeys: await db.dialect.schemaInspector.getForeignKeys(table.name),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
await db.connection.transaction(async (trx) => {
|
await db.connection.transaction(async (trx) => {
|
||||||
await this.createTables(schemaDiff.tables.added, trx);
|
await this.createTables(schemaDiff.tables.added, trx);
|
||||||
|
|
||||||
@ -98,7 +108,8 @@ export default (db: Database) => {
|
|||||||
// alter table
|
// alter table
|
||||||
const schemaBuilder = this.getSchemaBuilder(trx);
|
const schemaBuilder = this.getSchemaBuilder(trx);
|
||||||
|
|
||||||
await helpers.alterTable(schemaBuilder, table);
|
const { indexes, foreignKeys } = existingMetadata[table.name];
|
||||||
|
await helpers.alterTable(schemaBuilder, table, { indexes, foreignKeys });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -111,9 +122,22 @@ const createHelpers = (db: Database) => {
|
|||||||
/**
|
/**
|
||||||
* Creates a foreign key on a table
|
* Creates a foreign key on a table
|
||||||
*/
|
*/
|
||||||
const createForeignKey = (tableBuilder: Knex.TableBuilder, foreignKey: ForeignKey) => {
|
const createForeignKey = (
|
||||||
|
tableBuilder: Knex.TableBuilder,
|
||||||
|
foreignKey: ForeignKey,
|
||||||
|
existingForeignKeys?: ForeignKey[]
|
||||||
|
) => {
|
||||||
const { name, columns, referencedColumns, referencedTable, onDelete, onUpdate } = foreignKey;
|
const { name, columns, referencedColumns, referencedTable, onDelete, onUpdate } = foreignKey;
|
||||||
|
|
||||||
|
// Check if it already exists, and if so drop it before creating
|
||||||
|
// Note that it is safe to drop multiple times with TableBuilder because it only uses it to define the schema
|
||||||
|
const existingForeignKey = existingForeignKeys?.find((fk) => fk.name === name);
|
||||||
|
const forceMigration = db.config.settings?.forceMigration;
|
||||||
|
if (existingForeignKey && forceMigration) {
|
||||||
|
debug(`Dropping existing foreign key ${name}`);
|
||||||
|
tableBuilder.dropForeign(existingForeignKey.columns, name);
|
||||||
|
}
|
||||||
|
|
||||||
const constraint = tableBuilder
|
const constraint = tableBuilder
|
||||||
.foreign(columns, name)
|
.foreign(columns, name)
|
||||||
.references(referencedColumns)
|
.references(referencedColumns)
|
||||||
@ -140,15 +164,27 @@ const createHelpers = (db: Database) => {
|
|||||||
/**
|
/**
|
||||||
* Creates an index on a table
|
* Creates an index on a table
|
||||||
*/
|
*/
|
||||||
const createIndex = (tableBuilder: Knex.TableBuilder, index: Index) => {
|
const createIndex = (
|
||||||
|
tableBuilder: Knex.TableBuilder,
|
||||||
|
index: Index,
|
||||||
|
existingIndexes?: Index[]
|
||||||
|
) => {
|
||||||
const { type, columns, name } = index;
|
const { type, columns, name } = index;
|
||||||
|
|
||||||
|
// Check if it already exists, and if so drop it before creating
|
||||||
|
// Note that it is safe to drop multiple times with TableBuilder because it only uses it to define the schema
|
||||||
|
const existingIndex = existingIndexes?.find((existing) => existing.name === name);
|
||||||
|
const forceMigration = db.config.settings?.forceMigration;
|
||||||
|
if (forceMigration && existingIndex) {
|
||||||
|
dropIndex(tableBuilder, index);
|
||||||
|
}
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'primary': {
|
case 'primary': {
|
||||||
return tableBuilder.primary(columns, name);
|
return tableBuilder.primary(columns, { constraintName: name });
|
||||||
}
|
}
|
||||||
case 'unique': {
|
case 'unique': {
|
||||||
return tableBuilder.unique(columns, name);
|
return tableBuilder.unique(columns, { indexName: name });
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return tableBuilder.index(columns, name, type);
|
return tableBuilder.index(columns, name, type);
|
||||||
@ -161,13 +197,19 @@ const createHelpers = (db: Database) => {
|
|||||||
* @param {Knex.TableBuilder} tableBuilder
|
* @param {Knex.TableBuilder} tableBuilder
|
||||||
* @param {Index} index
|
* @param {Index} index
|
||||||
*/
|
*/
|
||||||
const dropIndex = (tableBuilder: Knex.TableBuilder, index: Index) => {
|
const dropIndex = (tableBuilder: Knex.TableBuilder, index: Index, existingIndexes?: Index[]) => {
|
||||||
if (!db.config.settings?.forceMigration) {
|
if (!db.config.settings?.forceMigration) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { type, columns, name } = index;
|
const { type, columns, name } = index;
|
||||||
|
|
||||||
|
// Check if the index exists in existingIndexes, and return early if it doesn't
|
||||||
|
if (existingIndexes && !existingIndexes.some((existingIndex) => existingIndex?.name === name)) {
|
||||||
|
debug(`Index ${index.name} not found in existingIndexes. Skipping drop.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'primary': {
|
case 'primary': {
|
||||||
return tableBuilder.dropPrimary(name);
|
return tableBuilder.dropPrimary(name);
|
||||||
@ -244,45 +286,67 @@ const createHelpers = (db: Database) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const alterTable = async (schemaBuilder: Knex.SchemaBuilder, table: TableDiff['diff']) => {
|
/**
|
||||||
await schemaBuilder.alterTable(table.name, (tableBuilder) => {
|
* Alters a database table by applying a set of schema changes including updates to columns, indexes, and foreign keys.
|
||||||
// Delete indexes / fks / columns
|
* This function ensures proper ordering of operations to avoid conflicts (e.g., foreign key errors) and handles
|
||||||
|
* MySQL-specific quirks where dropping a foreign key can implicitly drop an associated index.
|
||||||
|
*
|
||||||
|
* @param {Knex.SchemaBuilder} schemaBuilder - Knex SchemaBuilder instance to perform schema operations.
|
||||||
|
* @param {TableDiff['diff']} table - A diff object representing the schema changes to be applied to the table.
|
||||||
|
* @param {{ indexes: Index[]; foreignKeys: ForeignKey[] }} existingMetadata - Metadata about existing indexes and
|
||||||
|
* foreign keys in the table. Used to ensure safe operations and avoid unnecessary modifications.
|
||||||
|
* - indexes: Array of existing index definitions.
|
||||||
|
* - foreignKeys: Array of existing foreign key definitions.
|
||||||
|
*/
|
||||||
|
const alterTable = async (
|
||||||
|
schemaBuilder: Knex.SchemaBuilder,
|
||||||
|
table: TableDiff['diff'],
|
||||||
|
existingMetadata: { indexes: Index[]; foreignKeys: ForeignKey[] } = {
|
||||||
|
indexes: [],
|
||||||
|
foreignKeys: [],
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
let existingIndexes = [...existingMetadata.indexes];
|
||||||
|
const existingForeignKeys = [...existingMetadata.foreignKeys];
|
||||||
|
|
||||||
|
// Track dropped foreign keys
|
||||||
|
const droppedForeignKeyNames: string[] = [];
|
||||||
|
|
||||||
|
await schemaBuilder.alterTable(table.name, async (tableBuilder) => {
|
||||||
// Drop foreign keys first to avoid foreign key errors in the following steps
|
// Drop foreign keys first to avoid foreign key errors in the following steps
|
||||||
for (const removedForeignKey of table.foreignKeys.removed) {
|
for (const removedForeignKey of table.foreignKeys.removed) {
|
||||||
debug(`Dropping foreign key ${removedForeignKey.name} on ${table.name}`);
|
debug(`Dropping foreign key ${removedForeignKey.name} on ${table.name}`);
|
||||||
dropForeignKey(tableBuilder, removedForeignKey);
|
dropForeignKey(tableBuilder, removedForeignKey);
|
||||||
|
|
||||||
|
droppedForeignKeyNames.push(removedForeignKey.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const updatedForeignKey of table.foreignKeys.updated) {
|
for (const updatedForeignKey of table.foreignKeys.updated) {
|
||||||
debug(`Dropping updated foreign key ${updatedForeignKey.name} on ${table.name}`);
|
debug(`Dropping updated foreign key ${updatedForeignKey.name} on ${table.name}`);
|
||||||
dropForeignKey(tableBuilder, updatedForeignKey.object);
|
dropForeignKey(tableBuilder, updatedForeignKey.object);
|
||||||
|
|
||||||
|
droppedForeignKeyNames.push(updatedForeignKey.object.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
// for mysql only, dropForeignKey also removes the index, so don't drop it twice
|
// In MySQL, dropping a foreign key can also implicitly drop an index with the same name
|
||||||
const isMySQL = db.config.connection.client === 'mysql';
|
// Remove dropped foreign keys from existingIndexes for MySQL
|
||||||
const ignoreForeignKeyNames = isMySQL
|
if (db.config.connection.client === 'mysql') {
|
||||||
? [
|
existingIndexes = existingIndexes.filter(
|
||||||
...table.foreignKeys.removed.map((fk) => fk.name),
|
(index) => !droppedForeignKeyNames.includes(index.name)
|
||||||
...table.foreignKeys.updated.map((fk) => fk.name),
|
);
|
||||||
]
|
}
|
||||||
: [];
|
|
||||||
|
|
||||||
for (const removedIndex of table.indexes.removed) {
|
for (const removedIndex of table.indexes.removed) {
|
||||||
if (!ignoreForeignKeyNames.includes(removedIndex.name)) {
|
|
||||||
debug(`Dropping index ${removedIndex.name} on ${table.name}`);
|
debug(`Dropping index ${removedIndex.name} on ${table.name}`);
|
||||||
dropIndex(tableBuilder, removedIndex);
|
dropIndex(tableBuilder, removedIndex, existingIndexes);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const updatedIndex of table.indexes.updated) {
|
for (const updatedIndex of table.indexes.updated) {
|
||||||
if (!ignoreForeignKeyNames.includes(updatedIndex.name)) {
|
|
||||||
debug(`Dropping updated index ${updatedIndex.name} on ${table.name}`);
|
debug(`Dropping updated index ${updatedIndex.name} on ${table.name}`);
|
||||||
dropIndex(tableBuilder, updatedIndex.object);
|
dropIndex(tableBuilder, updatedIndex.object, existingIndexes);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We drop columns after indexes to ensure that it doesn't cascade delete any indexes we expect to exist
|
// Drop columns after FKs have been removed to avoid FK errors
|
||||||
for (const removedColumn of table.columns.removed) {
|
for (const removedColumn of table.columns.removed) {
|
||||||
debug(`Dropping column ${removedColumn.name} on ${table.name}`);
|
debug(`Dropping column ${removedColumn.name} on ${table.name}`);
|
||||||
dropColumn(tableBuilder, removedColumn);
|
dropColumn(tableBuilder, removedColumn);
|
||||||
@ -316,22 +380,22 @@ const createHelpers = (db: Database) => {
|
|||||||
// once the columns have all been updated, we can create indexes again
|
// once the columns have all been updated, we can create indexes again
|
||||||
for (const updatedForeignKey of table.foreignKeys.updated) {
|
for (const updatedForeignKey of table.foreignKeys.updated) {
|
||||||
debug(`Recreating updated foreign key ${updatedForeignKey.name} on ${table.name}`);
|
debug(`Recreating updated foreign key ${updatedForeignKey.name} on ${table.name}`);
|
||||||
createForeignKey(tableBuilder, updatedForeignKey.object);
|
createForeignKey(tableBuilder, updatedForeignKey.object, existingForeignKeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const updatedIndex of table.indexes.updated) {
|
for (const updatedIndex of table.indexes.updated) {
|
||||||
debug(`Recreating updated index ${updatedIndex.name} on ${table.name}`);
|
debug(`Recreating updated index ${updatedIndex.name} on ${table.name}`);
|
||||||
createIndex(tableBuilder, updatedIndex.object);
|
createIndex(tableBuilder, updatedIndex.object, existingIndexes);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const addedForeignKey of table.foreignKeys.added) {
|
for (const addedForeignKey of table.foreignKeys.added) {
|
||||||
debug(`Creating foreign keys ${addedForeignKey.name} on ${table.name}`);
|
debug(`Creating foreign key ${addedForeignKey.name} on ${table.name}`);
|
||||||
createForeignKey(tableBuilder, addedForeignKey);
|
createForeignKey(tableBuilder, addedForeignKey, existingForeignKeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const addedIndex of table.indexes.added) {
|
for (const addedIndex of table.indexes.added) {
|
||||||
debug(`Creating index ${addedIndex.name} on ${table.name}`);
|
debug(`Creating index ${addedIndex.name} on ${table.name}`);
|
||||||
createIndex(tableBuilder, addedIndex);
|
createIndex(tableBuilder, addedIndex, existingIndexes);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user