Skip to content

Array modification bug in QueryRunner drop methods causes elements to be skipped during iteration #11563

@taina0407

Description

@taina0407

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]:

  1. Iterator starts at index 0, drops column A
  2. Array becomes [B, C], but iterator moves to index 1
  3. Iterator now points to column C (skipping B)
  4. 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, CockroachQueryRunner
  • dropIndices() - SpannerQueryRunner, PostgresQueryRunner, CockroachQueryRunner
  • dropColumns() - 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() - Uses Promise.all() pattern
  • dropExclusionConstraints() - Uses Promise.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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions