enhancement
major
minor
defect
critical
major
minor
major
#30247
Migration auf PostgreSQL mit autosave=NEVER: Erwartete SQL-Fehler brechen die Transaktion ab
Problem
Mehrere Stellen der Engine prüfen etwas, indem sie ein SQL-Statement absetzen und einen Fehler als erwartetes Ergebnis werten (Tabelle existiert nicht, Foreign Key existiert nicht, ...). Die Exception wird gefangen, höchstens als Info geloggt, und die Verarbeitung läuft weiter.
Auf PostgreSQL bricht ein fehlgeschlagenes Statement die gesamte Transaktion ab. Alle weiteren Statements scheitern mit
ERROR: current transaction is aborted, commands ignored until end of transaction block (25P02)
und der nächste Commit wirkt als Rollback. In der Migration geht damit alles verloren, was seit dem letzten Commit (einer pro Migrationsversion, siehe DataMigration.executeSQLMigration) geschrieben wurde. Die folgenden Prozessoren melden nur noch Folgefehler, die nicht auf die Ursache verweisen.
Bisher fängt das der JDBC-Treiber mit autosave=ALWAYS (Vorgabe in db-postgresql.xml) ab: Er setzt vor jedem Statement einen Savepoint und rollt bei einem Fehler darauf zurück. In Projekten wird aus Performance-Gründen autosave=NEVER gesetzt, da jeder Savepoint auf dem Server eine Subtransaktion erzeugt. Dann greift der Schutz nicht mehr. PostgreSQLHelper.internalCheck loggt in diesem Fall nur einen Fehler beim Start.
Auch autosave=CONSERVATIVE schützt nicht: Der Treiber setzt zwar vor jedem Statement einen Savepoint, rollt aber nur bei Fehlern wie "cached statement cannot change return type" darauf zurück, nicht bei einer fehlenden Tabelle. PostgreSQLHelper.Driver.usesAutosave() wertet jedoch alles außer NEVER als sicher.
Die vorhandenen Aufrufe von DBHelper.setSavepoint / rollback / releaseSavepoint helfen nicht: Kein DBHelper implementiert Savepoints, DBHelper und OracleHelper liefern No-ops.
Betroffene Stellen
- DBSchemaUtils.exists(): SELECT * ... WHERE 1=0, Exception = Tabelle fehlt. Verwendet von AlignIndexesProcessor, AlignTableLayoutProcessor, Ticket23376UpdateReferenceIdColumns.
- DBProperties.tableExists(): dasselbe, ohne Savepoint. Verwendet von KBSchemaUtil.loadSchemaRaw und damit u.a. von MigrationContext und DeleteTableProcessor, sowie von MigrationService.
- DBSchemaUtils.tryResetTable() / tryDropForeignKey(): DROP versuchen, Fehler nur loggen (tryDropForeignKey() setzt nicht einmal einen Savepoint). Verwendet von DropTablesProcessor, RemoveApplicationTypesProcessor, DataMigration.recreateTables.
- Ticket23376UpdateReferenceIdColumns.migrateColumn(): Fehler wird als Warnung geloggt.
- RowLevelLockingSequenceManager (Laufzeit, nicht Migration) nutzt ebenfalls das Savepoint-Muster: Legen zwei Cluster-Knoten dieselbe Sequenz gleichzeitig an, scheitert der Retry nach dem Duplicate-Key-Fehler mit 25P02.
Lösung
- Existenzprüfungen über DatabaseMetaData (getTables, getImportedKeys, getColumns) statt über eine erwartete Exception: DBSchemaUtils.exists(), DBProperties.tableExists(); tryResetTable() und tryDropForeignKey() prüfen vorher, ob das Objekt existiert, und überspringen es sonst. Die Schreibweise der Namen im Katalog (PostgreSQL klein, Oracle groß) ist dabei zu beachten (Vorlage: DropOrphanedColumnProcessor.existingColumn()).
- PostgreSQLHelper implementiert setSavepoint / rollback / releaseSavepoint mit echten JDBC-Savepoints, wenn die Connection nicht autosave=ALWAYS nutzt (bei ALWAYS bleibt es beim No-op, um doppelte Savepoints zu vermeiden). Damit kostet ein Savepoint nur an den Stellen, die einen Fehler bewusst tolerieren.
- autosave=NEVER wird damit unterstützt: Die Fehlermeldung in PostgreSQLHelper.internalCheck entfällt bzw. wird zur Info.
Test
TestDBSchemaUtils.testToleratedFailuresKeepTransaction schreibt in einer Transaktion mehrere Zeilen und führt dazwischen aus:
- Existenzprüfung einer fehlenden Tabelle (DBSchemaUtils.exists()),
- Löschen einer fehlenden Tabelle (tryResetTable()) und eines Schemas mit fehlender Tabelle und fehlendem Foreign Key (resetTables(), wie es DropTablesProcessor verwendet),
- ein absichtlich fehlschlagendes Statement, abgesichert über DBHelper.setSavepoint / rollback.
Nach dem Commit müssen alle Zeilen vorhanden sein. Auf PostgreSQL läuft der Test nacheinander mit autosave NEVER, CONSERVATIVE und ALWAYS, gesetzt über PGConnection.setAutosave() an der Connection; ein normaler Testlauf deckt damit alle Modi ab, ohne die Datenbank umzukonfigurieren. Zusätzlich prüft der Test, dass ein vorhandener Foreign Key über die Metadaten gefunden und gelöscht wird.
Ohne die Korrektur schlägt der Test auf PostgreSQL fehl (Zeilen gehen verloren bzw. 25P02).