diff --git a/src/driver/sqlite-abstract/AbstractSqliteQueryRunner.ts b/src/driver/sqlite-abstract/AbstractSqliteQueryRunner.ts index c5951eb094..ab9d519456 100644 --- a/src/driver/sqlite-abstract/AbstractSqliteQueryRunner.ts +++ b/src/driver/sqlite-abstract/AbstractSqliteQueryRunner.ts @@ -2005,6 +2005,14 @@ export abstract class AbstractSqliteQueryRunner const upQueries: Query[] = [] const downQueries: Query[] = [] + // query triggers from old table before recreation + const [, tableNameOldInitial] = this.splitTablePath(oldTable.name) + const triggerQuery = `SELECT name, sql FROM sqlite_master WHERE type = 'trigger' AND tbl_name = ?` + const triggers: { name: string; sql: string }[] = await this.query( + triggerQuery, + [tableNameOldInitial], + ) + // drop old table indices oldTable.indices.forEach((index) => { upQueries.push(this.dropIndexSql(index)) @@ -2112,6 +2120,14 @@ export abstract class AbstractSqliteQueryRunner downQueries.push(this.dropIndexSql(index)) }) + // recreate table triggers + triggers.forEach((trigger) => { + // upQueries: recreate triggers on the new table (forward migration) + upQueries.push(new Query(trigger.sql)) + // downQueries: recreate triggers on the old table (rollback) + downQueries.push(new Query(trigger.sql)) + }) + // update generated columns in "typeorm_metadata" table // Step 1: clear data for removed generated columns oldTable.columns diff --git a/test/functional/query-runner/drop-column.ts b/test/functional/query-runner/drop-column.ts index 14d9adcdf8..2057481f9b 100644 --- a/test/functional/query-runner/drop-column.ts +++ b/test/functional/query-runner/drop-column.ts @@ -111,6 +111,57 @@ describe("query runner > drop column", () => { )) }) + it("should preserve triggers when recreating table in sqlite", () => + Promise.all( + connections + // Only run this test for sqlite-based connections + .filter( + (connection) => + connection.options.type === "sqlite" || + connection.options.type === "sqlite-pooled", + ) + .map(async (connection) => { + const queryRunner = connection.createQueryRunner() + + // Get the post table + let table = await queryRunner.getTable("post") + expect(table).to.exist + + // Create a trigger on the table + const triggerName = "test_insert_trigger" + const triggerSql = `CREATE TRIGGER ${triggerName} AFTER INSERT ON "${table!.name}" BEGIN SELECT 1; END` + await queryRunner.query(triggerSql) + + // Verify trigger exists before column drop + const triggersBefore: { name: string; sql: string }[] = + await queryRunner.query( + `SELECT name, sql FROM sqlite_master WHERE type = 'trigger' AND tbl_name = ?`, + [table!.name], + ) + expect(triggersBefore).to.have.length(1) + expect(triggersBefore[0].name).to.equal(triggerName) + + // Drop a column, which triggers recreateTable in SQLite + await queryRunner.dropColumn(table!, "version") + + // Verify trigger still exists after table recreation + table = await queryRunner.getTable("post") + const triggersAfter: { name: string; sql: string }[] = + await queryRunner.query( + `SELECT name, sql FROM sqlite_master WHERE type = 'trigger' AND tbl_name = ?`, + [table!.name], + ) + expect(triggersAfter).to.have.length(1) + expect(triggersAfter[0].name).to.equal(triggerName) + expect(triggersAfter[0].sql).to.equal(triggerSql) + + // Clean up + await queryRunner.query(`DROP TRIGGER ${triggerName}`) + await queryRunner.executeMemoryDownSql() + await queryRunner.release() + }), + )) + it("should safely handle SQL injection in hasEnumType", () => Promise.all( connections