-
-
Notifications
You must be signed in to change notification settings - Fork 6.5k
Description
Issue description
Multiple QueryRunner implementations have a critical bug where iterating over arrays (foreign keys, indices, columns, unique constraints) and modifying them during iteration causes elements to be skipped due to in-place array modification.
Expected Behavior
When methods like dropForeignKeys(), dropIndices(), dropColumns(), and dropUniqueConstraints() iterate over arrays using for (const item of array) and call individual drop methods, these individual methods internally remove items from the same array being iterated over. This causes the iterator to skip elements because the array indices shift during iteration, all items in the array should be dropped from the database table.
Actual Behavior
Due to array modification during iteration, only some of the items are actually dropped. Elements get skipped because the array indices shift when items are removed during the loop.
For example, when dropping columns [A, B, C]:
- Iterator starts at index 0, drops column A
- Array becomes
[B, C], but iterator moves to index 1 - Iterator now points to column C (skipping B)
- Only columns A and C are dropped, B remains
Steps to reproduce
import { DataSource } from 'typeorm';
// Create a table with multiple columns
const queryRunner = dataSource.createQueryRunner();
await queryRunner.createTable(new Table({
name: 'test_table',
columns: [
{ name: 'id', type: 'int', isPrimary: true },
{ name: 'col1', type: 'varchar', length: '255' },
{ name: 'col2', type: 'varchar', length: '255' },
{ name: 'col3', type: 'varchar', length: '255' },
{ name: 'col4', type: 'varchar', length: '255' }
]
}));
// Try to drop multiple columns at once
const table = await queryRunner.getTable('test_table');
const columnsToRemove = ['col1', 'col2', 'col3', 'col4'];
await queryRunner.dropColumns(table, columnsToRemove);
// Check remaining columns - some columns will still exist
const updatedTable = await queryRunner.getTable('test_table');
console.log(updatedTable.columns.map(c => c.name));
// Expected: ['id']
// Actual: ['id', 'col2', 'col4'] (col1 and col3 dropped, col2 and col4 remain)My Environment
| Dependency | Version |
|---|---|
| Operating System | Any |
| Node.js version | Any |
| Typescript version | Any |
| TypeORM version | 0.3.x (all versions with affected QueryRunners) |
Additional Context
Affected Methods and Files:
dropForeignKeys()- SpannerQueryRunner, PostgresQueryRunner, CockroachQueryRunnerdropIndices()- SpannerQueryRunner, PostgresQueryRunner, CockroachQueryRunnerdropColumns()- All major QueryRunners (Spanner, Postgres, MySQL, SQLServer, Oracle, CockroachDB, SAP, AuroraMySQL)dropUniqueConstraints()- PostgresQueryRunner, CockroachQueryRunner
Example of problematic code pattern:
async dropColumns(tableOrName: Table | string, columns: TableColumn[] | string[]): Promise<void> {
// BUG: Iterating over array while modifying it
for (const column of columns) {
await this.dropColumn(tableOrName, column) // This modifies the 'columns' array internally
}
}Proposed fix:
async dropColumns(tableOrName: Table | string, columns: TableColumn[] | string[]): Promise<void> {
// Create a copy of the array to avoid modification during iteration
for (const column of [...columns]) {
await this.dropColumn(tableOrName, column)
}
}Safe methods (no changes needed):
dropCheckConstraints()- UsesPromise.all()patterndropExclusionConstraints()- UsesPromise.all()pattern- SQLite-based QueryRunners - Use table cloning/recreation
- All
create*methods - Don't modify arrays during iteration
This is a systematic issue affecting multiple database drivers with potential for data loss and inconsistent database states.
Relevant Database Driver(s)
- aurora-mysql
- aurora-postgres
- better-sqlite3
- cockroachdb
- cordova
- expo
- mongodb
- mysql
- nativescript
- oracle
- postgres
- react-native
- sap
- spanner
- sqlite
- sqlite-abstract
- sqljs
- sqlserver
Are you willing to resolve this issue by submitting a Pull Request?
Yes, I have the time, and I know how to start.