Bug Description
Every DDL operation in ooljet-db-table-operations.service.ts commits two independent transactions sequentially: one to the application database (TypeORM queryRunner) and one to the ToolJet Postgres schema ( jdbQueryRunner). If the first commit succeeds and the second fails, the system is permanently split: the metadata and the physical schema are out of sync with no automatic recovery.
Affected files
server/src/modules/tooljet-db/services/tooljet-db-table-operations.service.ts
createTable (lines 388-399):
s await queryRunner.commitTransaction(); // app DB: InternalTable row saved await tjdbQueryRunner.commitTransaction(); // TjDB: physical table - if this fails: // catch block: await queryRunner.rollbackTransaction(); // ERROR: already committed, rollback is a no-op await tjdbQueryRunner.rollbackTransaction();
After failure: InternalTable record exists (table appears in UI), but no physical table exists. All queries against it fail with cryptic Postgres errors.
dropTable (lines 452-457) has the symmetric problem: app DB record deleted, physical table survives.
The same pattern repeats at lines 693-700, 785-793, 851-858, 1275-1283, 1457-1473, and 1539-1547 for editTable, �ddColumn, dropColumn, editColumn, createForeignKey, updateForeignKey, and deleteForeignKey.
Failure scenario
- Admin creates a new ToolJet DB table.
- App DB commits the InternalTable record successfully.
- A network partition between app server and ToolJet Postgres host causes jdbQueryRunner.commitTransaction() to fail.
- The catch block calls queryRunner.rollbackTransaction() on an already-committed transaction - this fails or is a no-op.
- The table appears in the UI (metadata row committed) but is unusable (no physical table). Retry attempts fail because the InternalTable row already exists.
Fix
Apply a compensating-transaction pattern: catch the second commit's failure and attempt to undo the first commit's effect before re-throwing:
s await queryRunner.commitTransaction(); // commit app DB first try { await tjdbQueryRunner.commitTransaction(); } catch (err) { // Compensate: undo the app DB change await this.internalTableRepository.delete({ id: internalTable.id }); throw err; }
Environment
ToolJet main branch (2026-08-13), NestJS, TypeORM, PostgreSQL.
Bug Description
Every DDL operation in ooljet-db-table-operations.service.ts commits two independent transactions sequentially: one to the application database (TypeORM queryRunner) and one to the ToolJet Postgres schema ( jdbQueryRunner). If the first commit succeeds and the second fails, the system is permanently split: the metadata and the physical schema are out of sync with no automatic recovery.
Affected files
server/src/modules/tooljet-db/services/tooljet-db-table-operations.service.ts
createTable (lines 388-399):
s await queryRunner.commitTransaction(); // app DB: InternalTable row saved await tjdbQueryRunner.commitTransaction(); // TjDB: physical table - if this fails: // catch block: await queryRunner.rollbackTransaction(); // ERROR: already committed, rollback is a no-op await tjdbQueryRunner.rollbackTransaction();After failure: InternalTable record exists (table appears in UI), but no physical table exists. All queries against it fail with cryptic Postgres errors.
dropTable (lines 452-457) has the symmetric problem: app DB record deleted, physical table survives.
The same pattern repeats at lines 693-700, 785-793, 851-858, 1275-1283, 1457-1473, and 1539-1547 for editTable, �ddColumn, dropColumn, editColumn, createForeignKey, updateForeignKey, and deleteForeignKey.
Failure scenario
Fix
Apply a compensating-transaction pattern: catch the second commit's failure and attempt to undo the first commit's effect before re-throwing:
s await queryRunner.commitTransaction(); // commit app DB first try { await tjdbQueryRunner.commitTransaction(); } catch (err) { // Compensate: undo the app DB change await this.internalTableRepository.delete({ id: internalTable.id }); throw err; }Environment
ToolJet main branch (2026-08-13), NestJS, TypeORM, PostgreSQL.