8000 Strict replication parity: review items 10, 12, 14 and the ClickHouse-only column scope (follow-up to #1437) by minguyen9988 · Pull Request #1559 · Altinity/clickhouse-sink-connector · GitHub
[go: up one dir, main page]

Skip to content

Strict replication parity: review items 10, 12, 14 and the ClickHouse-only column scope (follow-up to #1437) - #1559

Merged
minguyen9988 merged 17 commits into
2.11.0from
strict-parity-2.11.0
Oct 8, 2026
Merged

minguyen9988 merged 17 commits into
2.11.0from
strict-parity-2.11.0

Conversation

@minguyen9988
@minguyen9988 minguyen9988 commented Oct 7, 2026 •
Copy link
Copy Markdown
Collaborator

Follow-up to #1437 review items 10-14, under one rule: the connector behaves strictly as a replication program. A condition that would leave a source-matching column or a connector-managed table different from the source halts replication loudly, the offset never advances past an unapplied change, and a retry re-applies only what ClickHouse has not acknowledged. Columns that exist only on the ClickHouse side are tolerated (parity scope, stated in CONSTITUTION.md).

All five review items are in this PR. Commits (spec first, each fix with a test that fails without it):

  1. fix(sink): a failed ALIAS override reconciliation halts like a direct-override mismatch (Spec 08.05 section 3.3.2, FM-08.05-5). Item 12.
  2. fix(postgres): a schema-drift failure halts instead of logging and continuing (Spec 10.04 section 3.9). Item 10.
  3. fix(batch): a batch retry resends only what ClickHouse has not acknowledged; a replicated TRUNCATE is never re-run (Spec 03.06 section 3.5). Item 14.
  4. fix(python): the checksum tool tolerates ClickHouse-only columns: excluded from the comparison, noted at INFO, exit code unaffected; --allow_replica_only_columns is kept and now matches the default (Spec 11.02 sections 3.3, 3.9, FM-11.02-9, FM-11.02-10). Reverses the default from fix(python): compare the source table column set; detect replica-only columns and fail by default (Spec 11.02 sections 3.3, 3.9, FM-11.02-10) #1557.
  5. refactor(cdc): extract the version sequencer out of DebeziumChangeEventCapture into VersionSequencer (Spec 02.01-02.03 section 2). Item 13.
  6. test(java): pin that a ClickHouse-only column never gets an INSERT placeholder and is never written (Spec 04.03 FM-04.03-4).
  7. test(postgres): reference VersionAssignment at its new home VersionSequencer (fixes the testCompile break from combining 2 and 5).
  8. fix(postgres): a PostgreSQL DDL translation failure halts like the MySQL path, reusing ErrorListenerImpl; the lenient listener is removed and the five intentional skips are listed in Spec 10.04 section 3.10 (FM-10.04-9). Item 11.
  9. refactor(cdc): extract system-database connection retry into SystemDbConnectionRetry (Spec 01.01 section 3.5). Item 13.
  10. refactor(ddl): extract the MySQL DEFAULT-literal translator into DefaultLiteralTranslator (Spec 06.04 sections 3.2.1, 3.2.2). Item 13.
  11. refactor(ddl): extract the ReplacingMergeTree sorting-key/engine policy into ReplacingMergeTreeKeyPolicy (Spec 06.05 section 3.6). Item 13.
  12. fix(batch): the all-applied short-circuit from commit 3 still advances the durable offset watermark (Spec 03.06 section 3.5). Found in review of commit 3.
  13. fix(build): remove duplicate dependency declarations (connect-json in both modules; commons-lang3 and log4j-api in the lightweight module). The GitHub ubuntu-24.04 runner image 20261004 moved Maven from 3.9.16 to 3.10.0, which rejects duplicates; the last declaration, the one Maven 3.9 resolved, is kept, so the shaded jar carries the same log4j-api 2.23.1, commons-lang3 3.12.0 and connect-json as before.
  14. fix(build): declare maven-compiler-plugin once in the lightweight pom (same Maven 3.10 rule); the 3.11.0 declaration Maven 3.9 used is kept. This also unblocks CI for the 2.11.0 base, which has the same duplicates.

Behaviour change to note: a PostgreSQL DDL statement the grammar used to recover from partially now halts replication (commit 8). Remaining known residual, stated in Spec 03.06 section 3.5: the one chunk in flight when a failure surfaces may already be applied and is resent on retry.

Whole-Change-Verification: ran=scripts/validate_specs.py (PASSED, 83 specs, 1232 citations) and both Java unit suites offline (sink-connector-lightweight 790 tests, 0 failures; sink-connector 782 tests, 0 failures; the only errors are Testcontainers tests that need Docker, unchanged from the base); a scan of every pom.xml finds no duplicate dependency, plugin, execution or module; CI build job passes on the current head; result=pass

minguyen9988 and others added 6 commits October 7, 2026 00:06
…rect-override mismatch (Spec 08.05 s3.3.2)

ColumnTypeOverrideReconciler#reconcileAliasOverrides auto-applies a missing
or drifted column_type_override.alias.* entry via ALTER TABLE ... ADD/MODIFY
COLUMN ... ALIAS. Three ways this could fail left the table silently out of
sync with the declared override, unlike a direct override, which already
halts via ColumnTypeOverrideMismatchException:

  - a rejected ADD/MODIFY COLUMN ... ALIAS statement was only logged, and
    the drift was never retried;
  - an unreadable system.columns silently skipped both the direct and the
    ALIAS checks for the whole table;
  - the alias column's configured name already belonging to a non-ALIAS
    column produced no DDL and no log line at all.

All three now throw ColumnTypeOverrideMismatchException (reused, not a new
exception type; a new (String, Throwable) constructor preserves the
underlying SQLException as the cause). It carries no ClickHouse error code
and is not a terminal type for ClickHouseErrorClassifier, so it classifies
UNKNOWN and ClickHouseBatchRunnable retries the same batch with exponential
backoff -- the same mechanism a direct-override mismatch already relies on,
now applied symmetrically, satisfying Invariant I15 (bounded, declared
recovery) and I9 (no silent continuation past a condition that leaves
ClickHouse different from the source).

Per a scope ruling from the coordinator: parity is required only for columns
that match the source, not for columns a user added to ClickHouse by hand.
Both override kinds (this fix touches only the ALIAS path; the direct path
already had the equivalent check) are skipped silently -- no DDL, no halt --
when the override's source column is absent from the table being
reconciled. This keeps a wildcard override that legitimately does not apply
to a given table from halting it, and guarantees a ClickHouse-only column is
never inspected or altered, even one that happens to carry the exact name a
generated ALIAS column would have.

reconcile()/getExistingColumns()/reconcileAliasOverrides() no longer declare
a checked exception (every failure path in this class now throws the
unchecked ColumnTypeOverrideMismatchException), so ClickHouseAutoCreateTable
createNewTable()'s try/catch around the reconcile() call -- which rethrew
the mismatch exception but silently swallowed anything else via
catch (Exception e) { log.error(...) } -- is removed; there is nothing left
to catch, and removing it also closes that latent catch-all for any other
exception the reconciler could throw.

Spec: 08-schema-catalog/05-auto-table-creation.md section 3.3 gains a
cross-reference, new section 3.3.2 documents the halt and the scope guard,
and new FM-08.05-5 (Failure Modes & Recovery) is added and marked FIXED.
Verification Criteria lists the new/changed tests.

Tests (ColumnTypeOverrideReconcilerTest, all new except the first):
  - addColumnDdlFailureHalts(), modifyColumnDdlFailureHalts(),
    nonAliasColumnCollisionHalts(), existingColumnsReadFailureHalts() pin
    the four halt triggers.
  - wildcardOverrideForAbsentSourceColumnIsIgnored(),
    clickHouseOnlyAliasColumnForAbsentSourceColumnIsNeverTouched() pin the
    source-matching scope guard from the coordinator's ruling.
  - addColumnDdlSucceedsForSourceMatchingColumn() confirms the existing
    auto-add behaviour for a source-matching column is unaffected.
No existing test encoded the old log-and-continue behaviour, so none needed
to be rewritten; columnMetadataQueryBindsTheNames() (section 3.3.1,
pre-existing) is unchanged and still passes.

Mutation-checked: reverted the three main-source files (the test file and
spec left in place) and re-ran the suite -- the build failed to compile
(three of the new test methods call reconcile() directly, and the pre-fix
signature still declares "throws Exception"), confirming the new tests
cannot pass without the fix. Restored the fix; recompiled clean.

Verification (boxcar sandbox, docker.io/jenkins/jenkins:lts-jdk17, mvnw -o):
  mvn test -Dtest=ColumnTypeOverrideReconcilerTest
    Tests run: 8, Failures: 0, Errors: 0, Skipped: 0
  mvn test -Dtest=ClickHouseAuto
F438
CreateTableTest,ColumnTypeOverrideReconcilerTest
    Tests run: 9, Failures: 0, Errors: 0, Skipped: 0 -- in ClickHouseAutoCreateTableTest
    Tests run: 8, Failures: 0, Errors: 0, Skipped: 0 -- in ColumnTypeOverrideReconcilerTest
  python3 scripts/validate_specs.py -- Validation PASSED (83 specifications,
    1215 verification citations checked).

Destructive-Op-Check: sites=1; result=pass
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ntinuing (spec 10.04 s3.9)

Defect: a PostgreSQL source column that could not be confirmed present on
ClickHouse was a silent divergence, not a loud failure. Three places caught
and logged past exactly this condition instead of halting, breaking parity
with the MySQL path (whose SQL thread stops and does not advance its
position on an error):

- `ClickHouseConverter.extractDebeziumSchema` caught any exception while
  reading a record's own schema and returned `null` after a WARN, so the
  caller treated "I could not tell what columns this row has" the same as
  a legitimate no-row record.
- `PostgresSchemaChangeDetector.checkAndReconcile` wrapped its whole body
  in a catch-all that logged and returned for a null writer/connection, a
  failed `system.columns` fetch, and a failed `addMissingColumns` call.
  Worse, because `lastReconcileAttempt` was recorded unconditionally
  before the reconciliation call, a failure was also masked for the
  following 10s cooldown window: every record in that window passed
  through unchecked.
- `PostgresSchemaReconciler.addMissingColumns` logged each failed
  `ALTER TABLE ... ADD COLUMN` and returned normally, so the caller
  believed every missing column had been added.
- The call site in `DebeziumChangeEventCapture#processEveryChangeRecord`
  caught whatever the detector threw and logged
  "Schema drift detection threw unexpectedly; continuing replication".

Fix: let all of the above propagate. `addMissingColumns` still attempts
every missing column (one bad column must not block the others) but now
throws afterward naming every column that failed, with the first
rejection as cause. `checkAndReconcile` no longer catches; it still
returns normally only for the two legitimate non-failure outcomes (a
record extraction that correctly returns null, and a table confirmed
absent from ClickHouse, which the connector is expected to auto-create).
It now also tracks the last reconciliation failure per table so a
cooldown-window check re-throws the stored failure instead of passing
records through silently. The `processEveryChangeRecord` call site wraps
whatever `checkAndReconcile` throws in `RecordReplicationException` --
the same exception type and wrap-and-rethrow shape already used a few
lines below for a parser failure -- so it escapes the generic catch-all
and halts the engine the same way an unconvertible row does: offset not
committed, retry/terminal-exit policy applies on restart.

Parity scope (ruling applied here and stated in spec 10.04 s3.9): parity
is required only for columns that exist in the source and for tables the
connector manages. A column a user has added to a ClickHouse table that
does not exist in the source is never flagged, never reconciled, and
never halts the pipeline -- `findMissingColumns` inspects only the
Debezium-side field list, by construction.

Spec: specs/10-resilience-monitoring/04-loud-failure-guarantee.md section
3.9 rewritten (previously described the WARN-and-skip behaviour as
correct); FM-10.04-8 failure mode entry added; Verification Criteria
updated to the five tests below.

Tests (all new or changed to assert the halt instead of the skip):
- `ClickHouseConverterSchemaExtractionTest.unreadableSchemaPropagates()`
  (rewritten from `unreadableSchemaIsLoggedAtWarn`, which asserted
  `assertDoesNotThrow`); `nullValueSchemaReturnsNullWithoutThrowing()`
  pins the still-legitimate null case.
- `PostgresSchemaChangeDetectorTest`: `genuineRowWithUnreachableWriterThrows()`
  and `cdcInternalColumnsEnvelopeStillThrows()` (rewritten from two tests
  that asserted `assertDoesNotThrow` for the same inputs);
  `cooldownAfterAFailureStillThrows()` (new, pins the cooldown-masking
  fix via reflection-prepopulated cache state); `extraClickHouseOnlyColumnNeverHalts()`
  (new, pins the parity-scope ruling: a ClickHouse-only column never
  halts even with no writer wired).
- `PostgresSchemaReconcilerAddColumnFailureTest.failedAlterThrowsAfterAttemptingAllColumns()`
  (new): a rejecting JDBC connection proxy confirms all 3 missing columns
  are attempted and the thrown exception names all 3.
- `SchemaDriftFailureIsTerminalTest.schemaDriftFailurePropagatesThroughProcessEveryChangeRecord()`
  (new): exercises the real `processEveryChangeRecord` seam with a
  rejecting-connection-backed detector wired through `PostgresConnectorConfig`;
  confirms `RecordReplicationException` naming "schema-drift" escapes.

Mutation-check: each of the four production edits (ClickHouseConverter.java,
PostgresSchemaChangeDetector.java, PostgresSchemaReconciler.java,
DebeziumChangeEventCapture.java) was individually reverted in isolation
(git stash of that one file) and the corresponding test(s) re-run in an
isolated container sandbox; every one went red with the expected
failure/assertion message, then the file was restored and the suite
re-confirmed green.

Verification (isolated container sandbox, offline `-o` Maven reactor build
`-pl sink-connector,sink-connector-lightweight -am`):
  Tests run: 22, Failures: 0, Errors: 0, Skipped: 0
  (ClickHouseConverterSchemaExtractionTest: 2; PostgresSchemaChangeDetectorTest: 18;
  PostgresSchemaReconcilerAddColumnFailureTest: 1; SchemaDriftFailureIsTerminalTest: 1)
  BUILD SUCCESS.
`python3 scripts/validate_specs.py`: Validation PASSED (83 specifications,
1217 verification citations checked).

Not touched: PreparedStatementExecutor, ColumnTypeOverrideReconciler, and
the alias-override code in PostgresSchemaReconciler
(ensureAliasColumns/ensureAllAliasColumns/fetchColumnNames) -- owned by a
different work item. Docker-dependent integration tests were not run (no
Docker in the sandbox); this change only touches unit-testable code paths.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…knowledged, not the whole record list (Spec 03.06 section 3.5)

A chunked batch's failure was retried from scratch: when chunk k of a BatchChunker split threw, the worker retried the whole batch (spec 03.03 FM-03.03-2) with the same retained record list, and nothing distinguished a record whose write ClickHouse had already acknowledged from one it had not. Every retry therefore resent chunks 1..k-1 again, resent a flushStagedRows() flush that had already landed, re-ran an inline SCD2 UPDATE/DELETE history statement that had already run, and re-ran a TRUNCATE/bulk-close that had already applied.

Destructive-Op-Check: sites=4; result=pass
…stead of warning and failing (Spec CONSTITUTION I6 parity scope, Spec 11.02 sections 3.3, 3.9, FM-11.02-9, FM-11.02-10)

A later ruling narrows what the replication contract covers: parity is
required only for columns that exist on the MySQL source (or that the
connector itself creates and manages for replication bookkeeping), not for
every column a ClickHouse table happens to carry. A column added on the
ClickHouse replica alone, with no column of that name on the source table,
has no source value to conform to or diverge from, so it is tolerated. This
reverses the previous default, which named such a column at WARNING, failed
the scheduled job's "no WARNING in the output" contract, and exited 1 unless
--allow_replica_only_columns was passed.

- specs/CONSTITUTION.md: new "Parity scope" paragraph under Invariant I6,
  the single place this rule is stated; it does not relax Invariant I9 (an
  unapplied or lost source change is still a loud halt) because a
  ClickHouse-only column has no source value to apply or lose.
- clickhouse_table_checksum.py restrict_to_source_columns(): the replica-only
  branch now logs once per table at INFO ("... tolerated, not compared)")
  instead of WARNING. The opposite case -- a source column the replica lacks
  -- is unchanged: still a WARNING, still a real difference, since parity is
  in scope for it.
- top_level_table_checksum.py: record_replica_only() always logs the
  tolerated finding at INFO (no more flag-gated WARNING branch);
  relay_side_messages() now checks record_replica_only() ahead of, and
  independently of, the generic WARNING-marker branch, so the report is
  recognised regardless of which level the side emits it at;
  report_run_summary()'s end-of-run line is INFO and no longer sets exit
  code 1 for these findings. --allow_replica_only_columns stays an accepted
  flag (Invariant I11: no flag is removed) but is now a no-op; its --help
  text says so.

Tests: renamed/rewrote the three tests that pinned the old WARNING/exit-1
default to pin the new tolerated/INFO/exit-0 default instead --
TestReplicaOnlyColumns (test_checksum_fidelity.py),
TestSourceColumnSetReachesTheReplicaSide (test_checksum_verdicts.py),
TestScheduledJobLogContract (test_checksum_job_log_contract.py). The
"source column missing on the replica" tests in the same classes, which
exercise the still-in-scope case, are unchanged. Mutation-checked: reverting
just the two source files (git stash) turns 6 tests red --
test_replica_only_column_is_left_out_and_named_once_at_info,
test_a_name_with_a_comma_or_a_space_survives_the_driver_to_side_trip,
test_by_default_a_replica_only_column_is_tolerated_at_info,
test_the_allow_flag_is_a_no_op,
test_replica_only_column_is_tolerated_by_default,
test_allow_replica_only_columns_flag_is_a_no_op; restoring the fix turns
them green again.

Verification: python3 -m pytest -q db_compare/tests db_load/tests
db_dump/tests tests: 805 passed, 2 skipped, 30 subtests passed (unchanged
from the pre-change baseline -- the renamed tests replace the old ones
one-for-one, no existing test weakened or dropped). scripts/validate_specs.py:
PASSED (83 specs, 1212 citations).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…pture into VersionSequencer (Spec 02.01 section 2, Spec 02.02 section 2, Spec 02.03 section 2)

DebeziumChangeEventCapture.java had grown to ~4,100 lines even after commit
19c1bea extracted DdlTableNames. The `_version` sequencer -- the
`effectiveTs * 1_000_000 + counter` formula, its process-wide floor, its
intra-window counter, its source-log high-water mark, and the restart-floor
seeding -- was ~300 lines of self-contained, stateless-logic-over-shared-
statics with no dependency on the rest of the engine object. It moves
unchanged to the package-private VersionSequencer, next to its user:

- The 7 static fields (SEQUENCE_START, SEQUENCE_START_INITIAL,
  sequenceNumber, sequenceAnchorTs, sequenceHighWaterPosition,
  sequenceHighWaterEffectiveTs, sequenceMaxSourceTs) and their javadoc,
  verbatim.
- nextSequenceNumber, the VersionAssignment inner class, nextVersionAssignment,
  seedVersionFloor, raiseVersionFloor and addVersion, verbatim.

No logic changes, no renamed config keys, same log text, same exceptions.
addVersion's visibility is narrowed from public to package-private: it has
no caller outside the cdc package (confirmed by grep across both modules),
matching the all-package-private convention of DdlTableNames.
seedVersionFloorFromDurableMark and handleChangeEventBatch stay on
DebeziumChangeEventCapture (they are entangled with the engine's own
Properties/config lifecycle) and now call into VersionSequencer.

The statics remain process-wide mutable state shared across engine restarts
within one JVM -- this extraction only moves which class declares them, it
does not change their lifetime or visibility to the rest of the process
(still package-private to `cdc` via the class, but the fields themselves
stay `public static` so the existing test hooks that reset them via direct
field access keep working unchanged, just via a new qualifier).

Formal model: formal_specs/lean/Replication/VersionFloor.lean names
`DebeziumChangeEventCapture.nextSequenceNumber` /
`DebeziumChangeEventCapture.seedVersionFloor` in two doc comments; both now
read `VersionSequencer.*`. The Lean state machine itself is unchanged.

Specs updated to point Codebase Mapping / qualified references at the new
file (Primary Source and/or inline qualified mentions only -- specs that
mention the moved methods/fields in unqualified prose are left as-is, per
the precedent set by 19c1bea's own spec diff):
- specs/02-versioning/01-version-formula-encoding.md
- specs/02-versioning/02-commit-monotonicity-floor.md
- specs/02-versioning/03-sequence-counter-lifecycle.md
- specs/02-versioning/04-redelivery-stability.md
- specs/02-versioning/06-upgrade-compatibility.md
- specs/01-cdc-engine/04-transaction-boundaries.md
- specs/01-cdc-engine/02-binlog-coordinates.md
- specs/01-cdc-engine/08-compressed-transaction-payloads.md
- specs/09-offset-management/03-clickhouse-offset-store.md

Tests: the 17 test files that reset the statics or construct/invoke
VersionAssignment / nextSequenceNumber / nextVersionAssignment /
seedVersionFloor / raiseVersionFloor / addVersion via reflection or direct
package-private access are updated to the DebeziumChangeEventCapture.X ->
VersionSequencer.X qualifier; no assertion changed. No new regression test
is added -- this is a pure move, and the existing suite already pins every
behaviour of the sequencer; mutation check is therefore N/A in the usual
revert-the-fix sense (there is no fix, only a move), and is instead a
move check: `git diff` shows the moved bodies are byte-identical to the
pre-move text modulo the class qualifier at call sites, and the test run
below is unchanged in count from the pre-move baseline.

Verification:
- `python3 scripts/validate_specs.py` -> PASSED (83 specifications,
  1212 verification citations).
- `./mvnw -pl sink-connector-lightweight test`: before and after this
  change, Tests run: 784, Failures: 0, Errors: 76, Skipped: 28 --
  identical counts; all 76 errors are the known Docker-environment
  baseline (testcontainers *IT classes cannot reach a Docker daemon in
  this environment).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…en (Spec 04.03 FM-04.03-4, Spec 11.02 FM-04.03-4 Blast radius)

A later ruling narrows what the replication contract covers (Constitution I6,
parity scope): a column that exists on the ClickHouse replica alone, with no
column of that name on the MySQL source and not created or managed by the
connector for replication bookkeeping, is tolerated rather than a replication
gap. The Python checksum side of this ruling was already fixed in a prior
commit on this branch; this commit covers the Java connector's own write
path, per the follow-up audit: does the connector ever try to supply a value
for such a column, warn about it, or ALTER it?

Investigation (QueryFormatter.createColumns, PreparedStatementFieldMapper.
insertPreparedStatement, GroupInsertQueryWithBatchRecords, DbWriter,
DBMetadata.getColumnsDataTypesForTable) found the connector already does the
right thing, by the same mechanism the #1389 pre-ALTER fix put in place:

- A ClickHouse-only column can never be a member of any record's schema (no
  source column of that name ever produces one), so
  QueryFormatter.createColumns() never gives it a placeholde
8000
r in the
  generated INSERT -- in every batch, not just the first one after an ADD
  COLUMN, because there is no ADD COLUMN on the source side to catch up to.
- At bind time, PreparedStatementFieldMapper.insertPreparedStatement finds no
  entry for it in columnNameToIndexMap, finds it is not isUnboundByDesign,
  finds recordCarries(fields, colName) false, and takes the existing
  by-design branch: DEBUG log, continue. No value is bound, no exception is
  thrown, no ALTER is issued; ClickHouse keeps whatever value the column
  already holds.

This is the #1389 mechanism's Case B, applied to a column that is absent
*permanently* (no source counterpart will ever arrive) rather than
*transiently* (pending an ALTER) -- mechanically identical, different
rationale, so it gets its own named failure mode and its own test rather than
extending the pre-ALTER one.

No production code changes: this is a "pin existing-correct-behaviour"
change (outcome (a) of the audit), not a bug fix.

- specs/04-query-generation/03-field-membership-rules.md: added a paragraph
  to section 3.1 distinguishing Case B's transient (pre-ALTER) and permanent
  (replica-only) triggers; added FM-04.03-4 ("A column only ClickHouse has");
  added the two new tests to section 5 Verification Criteria; updated the
  failure-mode count (3 -> 4).
- specs/11-verification-tooling/02-db-compare-checksums.md: corrected
  FM-11.02-10's "Blast radius" sentence, which previously only pointed at
  spec 04.03 without saying what it found there, to state
  plainly that the connector's write path already excludes such a column
  from every generated INSERT, so nothing worse than an unjudgeable column
  happens.
- sink-connector/src/test/java/.../db/batch/ClickHouseOnlyColumnTest.java
  (new): two tests through the two real entry points --
  testClickHouseOnlyColumnIsOmittedFromGeneratedInsert (query-formatting,
  through GroupInsertQueryWithBatchRecords.updateQueryToRecordsMap) and
  testClickHouseOnlyColumnRecordWritesWithoutError (bind-time, through
  PreparedStatementFieldMapper.insertPreparedStatement, using a JDK dynamic
  proxy PreparedStatement stand-in -- this module has no mocking framework on
  its test classpath, following the pattern already used by
  NullValueColumnDropTest). The proxy records every touched index from any
  setXxx(int, ...) call rather than naming individual JDBC setter methods: a
  first version that matched only setNull/setString/setInt/setLong missed
  that an Int32 column binds via setObject (ClickHouseDataTypeMapper's
  isWiderIntegerTarget treats Int32 itself as a possible post-ALTER widening
  target, so it is not routed through setInt), which silently dropped the
  "id" column's own bind from the recorded indices and failed the test for a
  reason that had nothing to do with the behaviour under test.

Mutation-checked: since no production code changed, the check instead
confirms the two new tests actually exercise the mechanism being pinned.
Temporarily disabled, in the two production methods the javadoc names, only
the specific condition this change pins (QueryFormatter.createColumns's
record-field-membership check at the "omitted from INSERT" branch; and
PreparedStatementFieldMapper.insertPreparedStatement's `recordCarries`
branch), confirmed both new tests red:
  ClickHouseOnlyColumnTest.testClickHouseOnlyColumnIsOmittedFromGeneratedInsert:
    AssertionError -- ch_added_col got a placeholder in the generated INSERT.
  ClickHouseOnlyColumnTest.testClickHouseOnlyColumnRecordWritesWithoutError:
    StaleSchemaCacheException -- "Column ch_added_col is present in the
    ClickHouse table and carried by the record, but has no placeholder in
    the generated INSERT ... Failing the batch instead."
Restored both methods; both tests green again. No other test's behaviour
depends on this change, so no test was weakened, deleted, or had its
assertions loosened.

Verification (isolated container, JDK 17, offline Maven; Docker-backed
testcontainers/*IT tests cannot run in this environment, the known baseline):
  ./mvnw -o -pl sink-connector -am -Dtest=ClickHouseOnlyColumnTest,
    NullValueColumnDropTest,QueryFormatterPreAlterRecordTest,
    PreparedStatementFieldMapperRecordCarriesTest,
    PreparedStatementFieldMapperUnboundColumnTest test
  Tests run: 23, Failures: 0, Errors: 0, Skipped: 0 (2 new + 21 pre-existing,
  unchanged).
  ./mvnw -o -pl sink-connector -am test (full module)
  Tests run: 770, Failures: 0, Errors: 20, Skipped: 41 -- all 20 errors are
  DBMetadataTest/DbKafkaOffsetWriterTest/DbWriterTest/
  ClickHouseCreateDatabaseTest failing with "IllegalState: Previous attempts
  to find a Docker environment failed", the pre-existing testcontainers/
  Docker-unavailable baseline for this environment, unrelated to this change.
  python3 scripts/validate_specs.py: Validation PASSED (83 specifications,
  1214 verification citations).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@minguyen9988
Copy link
Copy Markdown
Collaborator Author

Runtime mutation check for commit d4310eb (item 12, ALIAS override reconciliation). The commit message reports a compile-time check only; this is the runtime check, recorded here because rewriting the commit would need a force-push. Each fix was disabled one at a time in ColumnTypeOverrideReconciler (method signatures unchanged) and ColumnTypeOverrideReconcilerTest was run:

  1. ADD COLUMN ALIAS failure turned into catch-and-log: addColumnDdlFailureHalts fails (expected ColumnTypeOverrideMismatchException, nothing thrown). 8 run, 1 failure.
  2. MODIFY COLUMN ALIAS failure turned into catch-and-log: modifyColumnDdlFailureHalts fails, same assertion. 8 run, 1 failure.
  3. Non-ALIAS name-collision throw removed: nonAliasColumnCollisionHalts fails. 8 run, 1 failure.
  4. Unreadable system.columns turned into catch-and-log returning an empty map: existingColumnsReadFailureHalts fails. 8 run, 1 failure.
  5. Parity-scope skip removed: wildcardOverrideForAbsentSourceColumnIsIgnored and clickHouseOnlyAliasColumnForAbsentSourceColumnIsNeverTouched fail (an ALTER TABLE was attempted). 8 run, 2 failures.
    With every fix restored: ColumnTypeOverrideReconcilerTest 8 run, 0 failures; ClickHouseAutoCreateTableTest 9 run, 0 failures.

minguyen9988 and others added 5 commits October 7, 2026 06:36
…quencer (fixes testCompile on the combined head)

SchemaDriftFailureIsTerminalTest was written against
DebeziumChangeEventCapture.VersionAssignment; the version-sequencer
extraction (4f3f809) moved the nested class to VersionSequencer, and the two
commits were combined without a compile in between. No behaviour change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…being swallowed (Spec 10.04 section 3.10, FM-10.04-9; precedent Spec 06.03 FM-06.03-1)

The PostgreSQL DDL parse path (PostgreSQLDDLParserService#parseSql and all five enter*() callbacks in PostgreSQLDDLParserListenerImpl) wrapped translation in try/catch and logged-and-continued on any error, including statements the grammar could not parse. MySQL has no such catch: a syntax error propagates via ErrorListenerImpl#syntaxError (a RuntimeException), is wrapped in DDLReplicationException, and halts replication with retry/backoff instead of advancing the offset past an untranslated change (Spec 06.03 FM-06.03-1).

Fix: reuse MySQL's own ErrorListenerImpl unchanged, registered on the PostgreSQL lexer and parser in a new PostgreSQLDDLParserService#runAntlrPipeline helper, and remove every try/catch from the five enter*() methods in PostgreSQLDDLParserListenerImpl (enterCreatestmt, enterAltertablestmt, enterRenamestmt, enterDropstmt, enterTruncatestmt). The bespoke LenientErrorListener that only logged is deleted; no second halting mechanism was invented, reusing MySQL's exact type, listener, and propagation path.

Five things remain intentionally skipped without throwing (spec 10.04 section 3.10's allowed-skip list, each a dedicated early-return or reporting call decided before translation, never a caught exception):

1. Statement kinds with no listener callback (CREATE INDEX, CREATE SEQUENCE, CREATE VIEW, COMMENT ON, ...) parse fine and produce an empty translation plus a WARN log, mirroring MySQL's own FM-06.03-3.

2. DROP of a non-TABLE object (early return in enterDropstmt).

3. An unrecognized alter_table_cmd keyword (default branch in translateAlterTableCmd, DEBUG log).

4. An untranslatable constraint clause, reported via reportUntranslatedConstraint (Spec 06.09 section 3.7), unchanged by this fix.

5. ClickHouse-only tables/columns, tolerated by design under Parity scope (Spec 10.04 section 3.9).

Spec: updated specs/10-resilience-monitoring/04-loud-failure-guarantee.md

- Codebase Mapping cites the PostgreSQL parse path and its reuse of ErrorListenerImpl.

- New section 3.10 'A PostgreSQL DDL translation failure halts, not merely warns': before/after behavior and the five-item allowed-skip list above.

- Verification Criteria cites the two new tests plus the three existing allowed-skip-kind tests and PostgreSQLConstraintClauseReportTest.

- New failure mode FM-10.04-9, following the sibling entries' Trigger/Behaviour/Detection/Blast radius/Recovery/RTO/Test structure.

- Summary count updated to 9 failure modes.

python3 scripts/validate_specs.py passes (83 specs, 1229 verification citations checked).

Tests: added unparseablePostgresDdlPropagatesInsteadOfBeingSwallowed and unparseablePostgresDdlPropagatesThroughAtomicBooleanOverload to PostgreSQLDDLParserServiceTest; both feed an unparseable statement to the two parseSql overloads and assert a RuntimeException containing 'Error parsing DDL' escapes with nothing appended to the output buffer.

Negative proof (reverted both source files to HEAD via git stash, reran the class in an isolated offline test environment, then restored the fix):

pre-fix: Tests run: 38, Failures: 2, Errors: 0 -- both new tests failed: Expected RuntimeException to be thrown, but nothing was thrown.

post-fix: both tests pass; log shows 'ERROR - Error parsing' for each unparseable statement; the other 36 tests in the class unaffected.

Full build (build.sh, RUN_TESTS=1, isolated offline test environment, sink-connector installed -DskipTests then both modules tested) versus the e9068e8 baseline:

sink-connector-lightweight: tests=790 (788+2 new) failures=0 errors=76 skipped=28

sink-connector: tests=780 failures=0 errors=20 skipped=41

Error counts match baseline; every failing/erroring testcase (verified via surefire XML) is a Testcontainers/Docker *IT class or container-startup failure, expected offline, not a regression. No existing test was weakened, skipped, or deleted.

Destructive-Op-Check: sites=4; result=pass

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…umChangeEventCapture into SystemDbConnectionRetry (Spec 01.01 section 3.5)

DebeziumChangeEventCapture.java was still ~3,800 lines after commits
19c1bea (DdlTableNames) and 4f3f809 (VersionSequencer). The initial
system/offset database connection retry -- SYSTEM_DB_CONNECT_ATTEMPTS,
SYSTEM_DB_CONNECT_RETRY_MS, createSystemDbConnectionWithRetry and
connectWithRetry -- was ~100 lines of self-contained, stateless retry
logic with no dependency on the rest of the engine object beyond the
credentials/config it is handed. It moves unchanged to the
package-private SystemDbConnectionRetry, next to its caller:

- SYSTEM_DB_CONNECT_ATTEMPTS and SYSTEM_DB_CONNECT_RETRY_MS, and their
  javadoc, verbatim.
- createSystemDbConnectionWithRetry and connectWithRetry, verbatim,
  including log levels/messages.

setSystemDbConnection on DebeziumChangeEventCapture now delegates to
SystemDbConnectionRetry.createSystemDbConnectionWithRetry instead of
calling the method on itself; no other behavior changes.

SystemDbConnectRetryTest moves its reflection/reference target from
DebeziumChangeEventCapture to SystemDbConnectionRetry (qualifier only;
no assertion changed).

Spec 01.01 (embedded engine lifecycle) gets a new Codebase Mapping
bullet, a new section 3.5 describing the retry behavior, and the
existing SystemDbConnectRetryTest citation is repointed at the new
class. python3 scripts/validate_specs.py passes (83 specs, 1227
verification citations).

Pure move: both the production code and the test were otherwise
byte-identical before and after save for the DebeziumChangeEventCapture
-> SystemDbConnectionRetry qualifier swap; no behavior change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…SqlDDLParserListenerImpl into DefaultLiteralTranslator (Spec 06.04 section 3.2.1, 3.2.2 rules 1-2)

MySqlDDLParserListenerImpl.java was still 3,226 lines after commits
19c1bea (DdlTableNames) and 4f3f809 (VersionSequencer). The literal
DEFAULT rewrite -- literalDefault, bytesLiteral, clickHouseString,
stringBody, isCurrentTimestampDefault and firstEnumMember, plus the
NUMERIC_CH_TYPE pattern they share -- is ~160 lines of stateless,
side-effect-free parse-tree logic with no dependency on the listener's
own state (config, database/table names, the DDL event timestamp, the
logger). It moves unchanged to the package-private
DefaultLiteralTranslator, next to its caller:

- NUMERIC_CH_TYPE and its javadoc, verbatim.
- literalDefault, bytesLiteral, clickHouseString, stringBody,
  isCurrentTimestampDefault and firstEnumMember, verbatim, including
  their javadoc. literalDefault, isCurrentTimestampDefault and
  firstEnumMember become package-private statics (resolveDefault calls
  them); bytesLiteral, clickHouseString and stringBody stay private,
  since only the moved methods call them.

resolveDefault on MySqlDDLParserListenerImpl now delegates to
DefaultLiteralTranslator.literalDefault/isCurrentTimestampDefault/
firstEnumMember instead of calling the methods on itself; no other
behavior changes. eventInstantLiteral and backfillNotReproducible stay
behind -- they need the listener's own ddlEventTimestampMs/
databaseName/cleanTableName/originalSql/log -- along with the
DATETIME64_SCALE/UTC_SECONDS constants eventInstantLiteral uses. The
no-longer-used java.math.BigInteger import is dropped from the old
file; DefaultLiteralTranslator has no logger, since none of the moved
code logs (matching the DdlTableNames precedent).

None of literalDefault/bytesLiteral/clickHouseString/stringBody/
isCurrentTimestampDefault/firstEnumMember/NUMERIC_CH_TYPE is referenced
by any test, directly or via reflection: every existing DEFAULT test in
MySqlDDLParserListenerImplTest exercises this logic through the public
translate(...) entry point, so no test file changes are needed.

Spec 06.04 (ALTER TABLE clause translation) gets a new Codebase Mapping
bullet naming DefaultLiteralTranslator and the methods/sections it
implements, and resolveDefault is added to the Primary Source bullet's
method list. python3 scripts/validate_specs.py passes (83 specs, 1227
verification citations).

Pure move: the production code is byte-identical before and after save
for the MySqlDDLParserListenerImpl -> DefaultLiteralTranslator
qualifier swap at the three call sites in resolveDefault; no behavior
change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…cy out of MySqlDDLParserListenerImpl into ReplacingMergeTreeKeyPolicy (Spec 06.05 section 3.6)

MySqlDDLParserListenerImpl.enterColumnCreateTable carried the full
sorting-key derivation for a CREATE TABLE -- declared PRIMARY KEY, then
a NOT NULL UNIQUE key, then every stored column as a last resort, with
the allow_nullable_key determination, the is_deleted/_is_deleted
collision check, the ReplacingMergeTree/ReplicatedReplacingMergeTree
engine-clause selection, and the ORDER BY index-prefix clean-up -- all
inline in one already-large method. This is stateless, logic
determining the table's identity and engine from the source key, not
column/type emission, so it moves to its own package-private class,
next to its caller:

- resolveSortingKey: the PRIMARY KEY / UNIQUE-key-fallback / all-columns
  -fallback derivation (moved verbatim, including every rationale
  comment and the INFO/WARN/ERROR logging and the DDLReplicationException
  refusal for a table with no stored column), now taking
  orderByColumns/uniqueKeyColumns/orderedColumnNames/notNullColumnNames/
  databaseName/tableName/overridePrimaryKey/originalSql as parameters
  and returning the nullableSortingKey flag.
- resolveIsDeletedColumnName: the is_deleted/_is_deleted disambiguation
  loop, moved verbatim.
- engineClause: the four-way ENGINE=... text selection, moved and
  restructured into one method returning the clause text (leading space
  preserved) for the caller to append.
- sanitizeOrderBy: the id_registro(10)-style suffix clean-up regex,
  moved verbatim into a function returning the sanitized string.
- splitIndexColumns: moved in full (not just visibility-widened), since
  grep confirms its only call site is inside the sorting-key derivation
  that is itself moving; its one internal call becomes
  MySqlDDLParserListenerImpl.stripBackticks(part).

stripBackticks loses its `private` modifier (package-private now) so
the new class can call it across the package boundary; no other change
to it. The now-unused KeylessTableWarning import is dropped from the
old file (the one call that used it moved to the new class). The
schema-override primary_key/overridePrimaryKey lookup stays in
enterColumnCreateTable -- it is passed into resolveSortingKey as a
parameter -- along with the column/engine text emission, the PARTITION
BY/TTL/SETTINGS emission and the appendOrderBy calls themselves, since
all of those need the listener's own query/config state.
parseCreateTable/parseColumnDefinitions are untouched: they are general
column/type emission entangled with the listener's query buffer, not
part of the key/engine policy.

None of the moved methods is referenced by any test, directly or via
reflection: every existing sorting-key/engine test in
MySqlDDLParserListenerImplTest (including
testCreateTableKeylessOrdersByAllColumns) exercises this logic through
the public translate(...)/listener entry point, so no test file changes
are needed.

Spec 06.05 gets a new Codebase Mapping bullet naming
ReplacingMergeTreeKeyPolicy and the methods/sections it implements.
python3 scripts/validate_specs.py passes (83 specs, 1227 verification
citations).

Pure move: production code is byte-identical before and after for the
MySqlDDLParserListenerImpl -> ReplacingMergeTreeKeyPolicy qualifier
swap at the four call sites in enterColumnCreateTable; no behavior
change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@minguyen9988
minguyen9988 marked this pull request as ready for review October 7, 2026 15:50
minguyen9988 and others added 6 commits October 7, 2026 10:56
…offset watermark (Spec 03.06 section 3.5)

Section 3.5 says the watermark used for the offset commit reflects every row
that is actually durable, whichever attempt wrote it, and the grouping path
honours that by folding an already-applied record's offset into
partitionToOffsetMap. The short-circuit in
ClickHouseBatchRunnable.processRecordsByTopic (every record of the topic is
already marked appliedToClickHouse) returned true before grouping, so it never
reached the durablyInsertedOffsets merge. If the attempt that wrote those rows
returned false before its own merge, the rows were durable in ClickHouse but
outside the committed offset, and a restart would redeliver and re-insert them.

Fix: the short-circuit merges each record's offset into durablyInsertedOffsets
(max per TopicPartition, the same merge the normal path uses) before returning
true. ClickHouseBatchWriter has no durable watermark, so its short-circuit is
unchanged.

Spec: 03.06 section 3.5 states the fold for the short-circuit; Verification
Criteria cites the new tests.

Tests: AllAppliedTopicWatermarkTest (no server; connections are null and the
short-circuit never uses one).
- anAllAppliedTopicAdvancesTheDurableWatermark: without the fix it fails with
  "partition 0 must advance from 5 to the highest durable offset, 9 ==>
  expected: <9> but was: <5>"; with the fix it passes.
- anAllAppliedTopicNeverMovesTheWatermarkBackwards: a lower offset never
  lowers the watermark.
ClickHouseBatchRunnableTest, WorkerFailureModesTest, ParkedBatchWrittenOnceTest,
PreparedStatementExecutorFailureModesTest and PreparedStatementExecutorTruncateTest
pass unchanged (their skipped tests are the existing @disabled ones).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rejects [spec-exempt: build metadata only; the resolved dependency set is unchanged]

The CI build broke on the GitHub runner image update from ubuntu24/20260927.320
to ubuntu24/20261004.327, which moves Maven from 3.9.16 to 3.10.0. The first
build step runs `mvn -q ... exec:exec` in sink-connector and writes its output
into GITHUB_ENV; Maven 3.10 now reports duplicate dependency declarations as an
ERROR that -q does not silence, so the step wrote
"[ERROR] 'dependencies.dependency.(groupId:artifactId:type:classifier)' must be
unique: org.apache.kafka:connect-json:jar -> duplicate declaration of version
${version.kafka} @ line 425" into GITHUB_ENV and failed with "Unable to process
file command 'env' successfully". Every job after it was skipped. The same
poms passed on the previous image (run on dcfb9e0: 15 of 15 green).

Duplicates removed (a scan of every pom.xml finds no others):
- sink-connector/pom.xml: org.apache.kafka:connect-json, declared twice, both
  compile scope.
- sink-connector-lightweight/pom.xml: org.apache.kafka:connect-json (test, then
  compile), org.apache.commons:commons-lang3 3.12.0 (test, then compile),
  org.apache.logging.log4j:log4j-api (2.17.1, then 2.23.1 compile).

In each pair the LAST declaration is kept, because that is what Maven 3.9
resolved: the jar built from the unchanged poms shades log4j-api 2.23.1,
commons-lang3 3.12.0 and org/apache/kafka/connect/json, i.e. the compile-scope
and later-version entries. Keeping them leaves the effective dependency set
exactly as it was.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… (Maven 3.10 rejects duplicate plugins) [spec-exempt: build metadata only; the plugin configuration the build uses is unchanged]

After the duplicate dependencies were removed, the same runner-image update
(Maven 3.9.16 -> 3.10.0) failed the next step that reads
sink-connector-lightweight/pom.xml (`mvn versions:set-property`):
"'build.plugins.plugin.(groupId:artifactId)' must be unique but found
duplicate declaration of plugin org.apache.maven.plugins:maven-compiler-plugin
@ line 629". Maven 3.10 promotes duplicate plugin declarations from a warning
to an error (release notes: "duplicated plugin entries will not be tolerated
anymore").

The pom declared maven-compiler-plugin twice: 3.15.0 with lombok 1.18.34 as the
annotation processor, then 3.11.0 with lombok 1.18.30. Maven 3.9 used the last
one: the earlier CI log on this branch shows
"maven-compiler-plugin:3.11.0:testCompile ... on project
clickhouse-debezium-embedded". The 3.11.0 declaration is kept and the unused
3.15.0 one removed, so the compiler and annotation-processor versions the
build runs with do not change.

A scan of every pom.xml (dependencies, plugins, plugin executions, modules)
now finds no duplicate declarations.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…kip, no XFail [spec-exempt: test-suite selection only, no product code]

The connector must replicate with source-to-destination parity, so the
end-to-end suite has to pass in every test, not only in the ones it chooses to
run. Until now the two suites the pull-request pipeline runs
(regression_auto.py, regression_auto_replicated.py) disabled a large part of
themselves: on the 2.11.0 base and on this branch alike each run reported
"91 features (74 ok, 14 skipped, 3 xfail)" (auto) and
"89 features (72 ok, 14 skipped, 3 xfail)" (replicated).

Disabled until now, now enabled:
- ffails Skip entries (both regressions, 33 entries each): the whole insert
  feature, partitions, schema only, cli, truncate of tables without a primary
  key, the multiple parallel add/modify/drop column alter, the
  no-primary-key variants of insert/delete/update, primary keys/no primary key,
  partition limits.
- inline xfail() in modules the pipeline loads:
  truncate.py complex primary key (2), insert.py DEFAULT-with-NULL (2),
  INSERT with a SELECT constant / SELECT from a table (2), one million
  datapoints, the no-primary-key examples of many-partitions-one-part /
  one-million / parallel (3), parallel_alters.py multiple parallel add and
  rename column.

Several of these reasons describe parity defects (for example "doesn't work
without primary key as only last row of insert is replicated"). This commit
only stops hiding them; the failures it surfaces are fixed in the commits that
follow on this branch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…aim [spec-exempt: end-to-end test harness only, no product code]

With every Skip/XFail removed (previous commit) the pipeline's testflows suites
failed the same set of tests in all four jobs. Each failure is in the test
harness: the statement the test sends is rejected by MySQL itself, or the test
races itself, before the connector is involved. Fixed without weakening any
assertion:

- insert/default with null, default with null and non null, use select
  constant as value, use select from table as value: the table's primary key
  `id` is INT NOT NULL with no default and the INSERT named only
  (col1,col2,col3), so MySQL answered "ERROR 1364 (HY000): Field 'id' doesn't
  have a default value" (quoted from the CI log). The INSERT now names id.
- insert/parallel (all four table shapes): the three concurrent inserts used
  overlapping ids (1, 1..100, 2..1001), so MySQL rejected the later ones with
  duplicate keys and the step retried for its whole 300 s; keyless tables are
  a single column `x` but were sent two values per row. The inserts now use
  disjoint ranges (1, 2..101, 102..1101) and one value per row for keyless
  tables (distinct values, since identical whole rows collapse under FINAL in
  a keyless ReplacingMergeTree, spec 08.05).
- truncate/complex primary key (both variants): create_mysql_table turned
  primary_key="id,k" into "id,k INT NOT NULL," - a column without a type, and
  k twice. It now defines only the key columns that the column list does not
  already define.
- truncate/no primary key (both variants): four values were inserted into the
  three-column (k, c, pad) table; keyless tables now get three.
- parallel alters/multiple parallel add modify drop column: ADD, MODIFY and
  DROP of the same column ran in parallel, so MODIFY/DROP could reach MySQL
  before ADD ("Unknown column"). They now run in order per column; columns
  still run in parallel with each other.
- schema only: the test inserted a row into a table created after the
  connector was already streaming and expected it not to replicate, but
  snapshot.mode=no_data only skips the initial snapshot. In the replicated suite
  it also wrote schema_only.yml to env/auto/configs, which that suite does not
  mount, so the connector never restarted. The scenario now stops the
  connector, creates a table that already holds a row, starts the connector in
  no_data mode with offset and schema-history tables of its own (so a new
  snapshot really happens), checks the table is created with no rows, checks a
  row written afterwards is replicated, and restarts the connector with the
  suite's configuration, restoring the shared in-memory config.
- cli (replicated suite) and multiple databases (replicated suite): cascades
  of the dead connector left by schema only. The client calls now assert exit
  code 0, so an unreachable connector fails at the call instead of as a later
  timeout.

CI: a failing testflows job now prints every failing test's steps and
messages (tfs transform nice-fails on the raw log); --output new-fails printed
details only for the first failure and a bare "AssertionError" for the rest.

Jinja-Render-Check: rendered=1; formats=yaml; result=pass
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ma only [spec-exempt: end-to-end test harness only, no product code]

The previous commit fixed truncate, parallel alters, cli and multiple
databases: they pass in all four testflows jobs of run 37722722453. Three
harness defects remained, now visible thanks to the per-failure log step:

- insert/default with null, default with null and non null, use select
  constant as value, use select from table as value: the rows now reach
  ClickHouse, but select() compared the test's comma-separated expectation
  (e.g. "\N,5,333") with `SELECT ... FINAL` in ClickHouse's default
  tab-separated format, so it could never match and timed out after 300 s.
  When a test supplies its expected output, the FINAL query now asks for
  FORMAT CSV, as the two other branches of select() already do. Comparisons
  against the MySQL client's own (tab-separated) output are unchanged.
- insert/parallel, keyless shapes: create_mysql_to_clickhouse_replicated_table
  gives every shape an `id` column, so keyless tables are (id, x), not (x);
  the one-value rows from the previous commit were rejected by MySQL. All
  shapes are back to two values per row, still with disjoint id ranges.
- schema only: errored at once with "'TestStep' object has no attribute
  'deepcopy'" because the star imports bind a test step named `copy` over the
  standard-library module. deepcopy is now imported after them under its own
  name.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@minguyen9988
Copy link
Copy Markdown
Collaborator Author

Additional end-to-end verification of head ab0733c (connector jar built from this head; Java code identical to 73c44dd), beyond the CI pipeline. Each suite ran in a fresh isolated container: MySQL 8.0 (GTID on unless noted, binlog_transaction_compression on, sql_generate_invisible_primary_key=1), ClickHouse 24.8, the embedded lightweight connector. All checks are value-level (CRC32 over canonical column renderings, MySQL vs ClickHouse FINAL) or structural.

  • main matrix (types, keyless, relocation, TRUNCATE, spatial): 8/8 pass
  • primary-key change / rebuild: 26/26 pass
  • SCD2 history + replication-log-only modes, three connectors side by side, kill -9 restart, single-threaded variants: 148/148 pass
  • upgrade/downgrade hops between the 2.11.0 base (db0330a) and this head, OLD->NEW->OLD->NEW plus kill -9: 101/101 pass with GTID on, 101/101 with GTID off
  • DDL x DML matrix (ADD/DROP/RENAME COLUMN, RENAME/DROP+CREATE TABLE, TRUNCATE, key relocation, NULL toggles, bulk, kill -9): 26/26 pass. Note: the matrix's settle step waits only for equal row counts, so two steps whose DML keeps the count (DROP+CREATE same name, sorting-key relocation) were compared before the change was applied; with a convergence poll (both converged within ~1 s) all 26 pass. The same two steps fail the same way on the 2.11.0 base without the poll, so this is a test timing issue, not a regression.
  • binlog transaction compression: 26/26 for GTID on/compression on, GTID off/compression on, GTID on/compression off; hard cases 49/49
  • DST fidelity (Spec 07.03 section 3.1.4): the table created by the CDC DDL path passes 15/15; the table copied by the initial snapshot has 2 of 15 fall-back-overlap rows stored one hour off (ids 2 and 31). This is identical on the 2.11.0 base and is the overlap-hour DEFECT already documented in Spec 07.03; not introduced here.

🤖 Generated with Claude Code

@minguyen9988
minguyen9988 merged commit f5b5992 into 2.11.0 Oct 8, 2026
15 checks passed
@minguyen9988
minguyen9988 deleted the strict-parity-2.11.0 branch October 8, 2026 11:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

0