apache/seatunnel · critical · SchemaEvolutionException
INVALID_SCHEMA_STRUCTURE
INVALID_SCHEMA_STRUCTURE
Error message
Failed to resolve PostgreSQL RELATION schema change for ${relationId}. Continuing could make the produced row schema diverge from the source relation. What it means
PostgresRelationSchemaChangeResolver.resolve wraps any unexpected exception (while converting a Debezium RELATION message into a SeaTunnel catalog table) into a SchemaEvolutionException with code INVALID_SCHEMA_STRUCTURE. It signals that the resolver could not derive the new schema for the relation and that continuing would let the produced rows' schema diverge from the actual PostgreSQL relation.
Solutions
- Inspect the wrapped cause (`getCause()`) to find the actual conversion failure (usually an unsupported type or null metadata).
- Check the table's column types and remove/alter unsupported ones (e.g. custom or unusual types) before the job consumes them.
- Restart the pipeline with a fresh replication slot so RELATION messages are re-emitted consistently, or restore from a clean snapshot.
- If the failure is persistent for schema-evolution-exempt cases, handle SchemaEvolutionException upstream per your evolution policy.
Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-check relation payload before resolution
if (relationChange == null || relationChange.id() == null) {
skipEvent();
} Try / catch
try {
resolver.resolve(...);
} catch (SchemaEvolutionException e) {
log.error("RELATION resolution failed for relation; cause=", e.getCause());
throw e; // do not continue with diverging schema
} Prevention
- Avoid capturing tables with exotic/custom PostgreSQL column types
- Use a fresh replication slot after WAL retention issues
- Keep PostgreSQL DDL windows outside job runtime where possible
- Enable checkpointing so schema state is consistent after restarts
When it happens
Trigger: Any exception during RELATION-event schema resolution — e.g. malformed/unsupported column type in the relation message, null `after` payload, or an internal conversion error; `before == null ? null : before.getTableId()` shows it also handles missing prior table info.
Common situations: PostgreSQL DDL introducing types the mapping layer cannot translate; corrupted or partially applied RELATION stream after a slot lsn jump; concurrent DDL during snapshot handoff.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- Interrupted while dispatching PostgreSQL relation change for
- PostgreSQL-CDC schema evolution requires…
- A slot marked as temporary or with an exported snapshot was…
- Cannot drop replication slot
- Cannot find table schema for table
AI-assisted analysis of apache/seatunnel@cf67b549a7 (2026-09-10).
Data as JSON: /api/errors/1120de8b557b0929.
Report an issue: GitHub.
Appendix: source
Thrown at seatunnel-connectors-v2/connector-cdc/connector-cdc-postgres/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/postgres/source/PostgresRelationSchemaChangeResolver.java:76
Table after = null;
CatalogTable before = null;
try {
after = extractTable(record);
before = findCatalogTable(after, catalogTables);
List<AlterTableColumnEvent> events = resolveAddedColumns(before, after);
if (events.isEmpty()) {
return null;
}
events.forEach(event -> event.setSourceDialectName(DatabaseIdentifier.POSTGRESQL));
AlterTableColumnsEvent result = new AlterTableColumnsEvent(before.getTableId(), events);
result.setSourceDialectName(DatabaseIdentifier.POSTGRESQL);
return result;
} catch (SchemaEvolutionException e) {
throw e;
} catch (Exception e) {
String relationId = after == null ? "unknown" : after.id().toString();
throw new SchemaEvolutionException(
SchemaEvolutionErrorCode.INVALID_SCHEMA_STRUCTURE,
"Failed to resolve PostgreSQL RELATION schema change for "
+ relationId
+ ". Continuing could make the produced row schema diverge from the source relation.",
before == null ? null : before.getTableId(),
null,
e);
}
}
private Table extractTable(SourceRecord record) {
Struct value = (Struct) record.value();
List<Struct> changes = value.getArray(HistoryRecord.Fields.TABLE_CHANGES);
if (changes == null || changes.isEmpty()) {
throw invalidRelationRecord("PostgreSQL relation record has no table change payload");
}
TableChanges tableChanges = new ConnectTableChangeSerializer().deserialize(changes, true);
return StreamSupport.stream(tableChanges.spliterator(), false)View on GitHub (pinned to cf67b549a7)