{"record":{"id":"5c3545867645101f","repo":"apache/seatunnel","slug":"cannot-deserialize-tidb-delete-cdc-row-because-bot","errorCode":null,"errorMessage":"Cannot deserialize TiDB DELETE CDC row because both value and oldValue are empty, and table ${tableInfo.getName()}(${tableInfo.getId()}) is not a pk-handle table. key=${row.getKey()}, startTs=${row.getStartTs()}, commitTs=${row.getCommitTs()}","messagePattern":"Cannot deserialize TiDB DELETE CDC row because both value and oldValue are empty, and table (.+?)\\((.+?)\\) is not a pk-handle table\\. key=(.+?), startTs=(.+?), commitTs=(.+?)","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"seatunnel-connectors-v2/connector-cdc/connector-cdc-tidb/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/tidb/source/deserializer/SeaTunnelRowStreamingRecordDeserializer.java","lineNumber":104,"sourceCode":"    }\n\n    private Object[] decodeDeleteValues(Cdcpb.Event.Row row, long handle) {\n        ByteString oldValue = row.getOldValue();\n        if (oldValue != null && !oldValue.isEmpty()) {\n            return decodeObjects(oldValue.toByteArray(), handle, tableInfo);\n        }\n        ByteString value = row.getValue();\n        if (value != null && !value.isEmpty()) {\n            // Prefer an available row image before falling back to a PK-only delete row.\n            return decodeObjects(value.toByteArray(), handle, tableInfo);\n        }\n        return decodeDeleteValuesFromHandle(row, handle);\n    }\n\n    private Object[] decodeDeleteValuesFromHandle(Cdcpb.Event.Row row, long handle) {\n        TiColumnInfo handleColumn = tableInfo.getPKIsHandleColumn();\n        if (!tableInfo.isPkHandle() || handleColumn == null) {\n            throw new IllegalStateException(\n                    String.format(\n                            \"Cannot deserialize TiDB DELETE CDC row because both value and\"\n                                    + \" oldValue are empty, and table %s(%s) is not a pk-handle\"\n                                    + \" table. key=%s, startTs=%s, commitTs=%s\",\n                            tableInfo.getName(),\n                            tableInfo.getId(),\n                            row.getKey(),\n                            row.getStartTs(),\n                            row.getCommitTs()));\n        }\n        Object[] values = new Object[tableInfo.getColumns().size()];\n        values[handleColumn.getOffset()] = handle;\n        return values;\n    }\n}\n","sourceCodeStart":86,"sourceCodeEnd":120,"githubUrl":"https://github.com/apache/seatunnel/blob/cf67b549a7a6c35fa0beb12d83c62892427ea919/seatunnel-connectors-v2/connector-cdc/connector-cdc-tidb/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/tidb/source/deserializer/SeaTunnelRowStreamingRecordDeserializer.java#L86-L120","documentation":"Thrown by decodeDeleteValuesFromHandle when a DELETE CDC event has neither value nor oldValue bytes, and the table is not a pk-handle table, so the deleted row's values cannot be reconstructed from the row handle. Without value bytes or a handle-capable primary key, the connector cannot produce a meaningful delete record.","triggerScenarios":"A DELETE event arrives with empty value and empty oldValue while tableInfo.isPkHandle() is false (or getPKIsHandleColumn() is null) — i.e. the table's primary key is not an integer handle that can be decoded into column values.","commonSituations":"Tables with non-integer or composite primary keys receiving delete events where TiCDC/TiKV omits the old value (old-value capture disabled in TiCDC configuration); deletes on tables without handle-type PKs.","solutions":["Enable old-value capture in TiCDC (changefeed config enable-old-value=true) so DELETE events carry the row bytes","If old values cannot be enabled, use a table with an integer handle primary key (INT/BIGINT single-column PK)","Check TiKV client region/key decode: verify key and startTs/commitTs in the message to trace the event","Upgrade TiDB/TiCDC — newer versions populate old values for delete events"],"exampleFix":"// before: TiCDC changefeed without old values\ncdc cli changefeed create --sink-uri=... --config=...\n// after\n// tiCDC changefeed config: {\"enable-old-value\": true}\ncdc cli changefeed update --changefeed-id=... --config=cf.toml","handlingStrategy":"validation","validationCode":"// Verify TiCDC changefeed has old-value enabled before running CDC jobs\ncdc cli changefeed query --changefeed-id=<id> | jq '.config.enable_old_value'\n// must be true for tables without integer handle PKs","typeGuard":"boolean canDecodeDelete(Cdcpb.Event.Row row, TiTableInfo tableInfo) {\n    boolean hasValue = row.getValue() != null && !row.getValue().isEmpty();\n    boolean hasOld = row.getOldValue() != null && !row.getOldValue().isEmpty();\n    return hasValue || hasOld || (tableInfo.isPkHandle() && tableInfo.getPKIsHandleColumn() != null);\n}","tryCatchPattern":"try {\n    deserializer.deserialize(row, output);\n} catch (IllegalStateException e) {\n    if (e.getMessage().contains(\"not a pk-handle table\")) {\n        LOG.error(\"Enable TiCDC enable-old-value for table {}\", tableInfo.getName());\n    }\n    throw e;\n}","preventionTips":["Always enable enable-old-value=true in TiCDC changefeeds used by SeaTunnel","Prefer tables with single-column integer primary keys for CDC","Test delete-event handling on staging before production"],"tags":["cdc","tidb","delete-event","old-value","pk-handle"],"backgroundTag":"unsupported-operation","analyzedSha":"cf67b549a7a6c35fa0beb12d83c62892427ea919","analyzedAt":"2026-09-10T21:44:55.265Z","contentChangedAt":"2026-09-10T21:44:55.265Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}