enhancement
major
minor
defect
critical
major
minor
major
#30247
Migrating to PostgreSQL with `autosave=NEVER`: Expected SQL errors abort the transaction
Problem
Several parts of the engine perform checks by executing an SQL statement and treating an error as the expected result (table does not exist, foreign key does not exist, etc.). The exception is caught, logged at most as information, and processing continues.
On PostgreSQL, a failed statement aborts the entire transaction. All subsequent statements fail with
ERROR: current transaction is aborted, commands ignored until end of transaction block (25P02)
and the next commit acts as a rollback. In the migration, this means that everything written since the last commit (one per migration version; see DataMigration.executeSQLMigration) is lost. The subsequent processors only report subsequent errors that do not indicate the root cause.
Until now, the JDBC driver has handled this with `autosave=ALWAYS ` (default in `db-postgresql.xml`): It sets a savepoint before each statement and rolls back to it in case of an error. In projects, autosave=NEVER is set for performance reasons, since every savepoint creates a subtransaction on the server. In that case, the protection no longer applies. PostgreSQLHelper.internalCheck logs only an error at startup in this scenario.
Even `autosave=CONSERVATIVE` does not provide protection: Although the driver sets a savepoint before every statement, it only rolls back to it in the event of errors such as "cached statement cannot change return type," not in the case of a missing table. However, PostgreSQLHelper.Driver.usesAutosave() treats everything except NEVER as safe.
The existing calls to DBHelper.setSavepoint / rollback / releaseSavepoint do not help: No DBHelper implements savepoints; DBHelper and OracleHelper return no-ops.
Affected locations
- DBSchemaUtils.exists(): SELECT * ... WHERE 1=0, Exception = table missing. Used by AlignIndexesProcessor, AlignTableLayoutProcessor, and Ticket23376UpdateReferenceIdColumns.
- DBProperties.tableExists(): same issue, without a savepoint. Used by KBSchemaUtil.loadSchemaRaw and thus, among others, by MigrationContext and DeleteTableProcessor, as well as by MigrationService.
- DBSchemaUtils.tryResetTable() / tryDropForeignKey(): Attempt DROP, log errors only (tryDropForeignKey() does not even set a savepoint). Used by DropTablesProcessor, RemoveApplicationTypesProcessor, and DataMigration.recreateTables.
- Ticket23376UpdateReferenceIdColumns.migrateColumn(): Errors are logged as warnings.
- RowLevelLockingSequenceManager (runtime, not migration) also uses the savepoint pattern: If two cluster nodes create the same sequence at the same time, the retry fails with error 25P02 after the duplicate key error.
Solution
- Check for existence using DatabaseMetaData (getTables, getImportedKeys, getColumns) instead of relying on an expected exception: DBSchemaUtils.exists(), DBProperties.tableExists(); tryResetTable() and tryDropForeignKey() first check whether the object exists and skip it otherwise. Note the naming convention in the catalog (PostgreSQL uses lowercase, Oracle uses uppercase) (see: DropOrphanedColumnProcessor.existingColumn()).
- PostgreSQLHelper implements setSavepoint / rollback / releaseSavepoint using true JDBC savepoints if the connection does not use autosave=ALWAYS (with ALWAYS, it remains a no-op to avoid duplicate savepoints). Thus, a savepoint incurs a cost only at those points that intentionally tolerate an error.
- autosave=NEVER is thus supported: The error message in PostgreSQLHelper.internalCheck is omitted or becomes informational.
Test
TestDBSchemaUtils.testToleratedFailuresKeepTransaction writes multiple rows within a transaction and performs the following in between:
- checks for the existence of a missing table (DBSchemaUtils.exists()),
- deleting a nonexistent table (tryResetTable()), and a schema with a missing table and a missing foreign key (resetTables(), as used by DropTablesProcessor ),
- an intentionally failing statement, safeguarded via DBHelper.setSavepoint / rollback.
After the commit, all rows must be present. On PostgreSQL, the test runs sequentially with autosave set to NEVER, CONSERVATIVE, and ALWAYS via PGConnection.setAutosave() on the connection; a normal test run thus covers all modes without reconfiguring the database. Additionally, the test verifies that an existing foreign key is found via the metadata and deleted.
Without this fix, the test fails on PostgreSQL (rows are lost or result in error 25P02).