8000 fix(writer): value fidelity -- NULL never defaulted, column-zone DateTime64, DST gap, geometry, loud clamps, case-mismatch, unbindable fields by minguyen9988 · Pull Request #1475 · Altinity/clickhouse-sink-connector · GitHub
[go: up one dir, main page]

Skip to content

fix(writer): value fidelity -- NULL never defaulted, column-zone DateTime64, DST gap, geometry, loud clamps, case-mismatch, unbindable fields - #1475

Merged
minguyen9988 merged 11 commits into
2.11.0from
fix-writer-value-fidelity
Sep 22, 2026
Merged

minguyen9988 merged 11 commits into
2.11.0from
fix-writer-value-fidelity

Conversation

@minguyen9988
Copy link
Copy Markdown
Collaborator

Base: 2.11.0 at 5780b62 (after 1470-1474). Part of the replication-correctness audit; every change is spec-first (spec updated in the same commit), with a failing-then-passing test and a mutation check per finding.

Each finding is a case where a ClickHouse row could differ from its MySQL row while row counts still matched.

  • input_format_null_as_default=0 on every ClickHouse connection: 1474 binds source NULLs, but the server default replaced an explicit NULL for a non-Nullable column by the column DEFAULT (empty string, 0, 1970-01-01). Spec 07.07 and 03.05.
  • DATETIME(0..3) digits are decoded as Debezium encoded them; a spring-forward gap wall time no longer shifts by one hour when source and session zones are the same DST zone (TimestampConverter now matches MicroTimestampConverter). Spec 07.03.
  • Temporal values are rendered in the column's declared zone; an empty source zone means the session zone. Auto-created and evolved DateTime64(n, 'UTC') columns no longer receive wall-clock text formatted in the server zone.
  • Out-of-range values are never saturated silently: clamp.out.of.range (default false) fails the batch; when enabled the clamp is logged.
  • No fabricated spatial, decimal or timestamp values: non-POLYGON spatial types (LINESTRING, MULTIPOLYGON, GEOMETRY, unparsable WKB) are String (WKB hex) instead of an empty polygon.
  • A ClickHouse column that differs from the source field only in letter case is bound instead of stalling the pipeline.
  • MicroTime is declared Nullable(String) in the DDL-string mapping, matching what is bound.
  • Canonical stored representation of strings, JSON, ENUM/SET, TIME, BOOL, FLOAT and binary values recorded in the specs.
  • An unbindable field fails the batch; parameters are cleared after every addBatch; raw JSON without schema defaults.
  • One report per table when an INT64 field has no propagated source type (BIGINT UNSIGNED hazard).
  • The record-schema type mapping agrees with the DDL path; engine-column name collisions are detected; no legacy engine on a metadata failure.

Verification: sink-connector unit suite and sink-connector-lightweight unit suite green (Docker-backed ITs not runnable in the build environment used, so they rely on CI). Spec validator passes. No Lean file changed.

Merge note: the six audit PRs are independent branches off 5780b62 and touch shared registration files (specs README, CONSTITUTION, Lean roots, validator module list). Suggested order: this one, then fix-writer-engine-semantics, fix-engine-lifecycle-no-silent-loss, fix-version-floor-restart-and-gtid-commit-order, fix-ddl-translation-identity-and-types, fix-db-compare-checksum-fidelity; after each merge, forward-merge 2.11.0 into the remaining branches (no force-push).

minguyen9988 and others added 11 commits September 20, 2026 21:40
…as_default=0

A source NULL bound for a non-Nullable ClickHouse column was silently replaced
by the column DEFAULT: the connector binds NULL explicitly (Spec 07.07 §3.2),
but the server-side setting input_format_null_as_default defaults to 1, so
ClickHouse swapped in 0 / '' / 1970-01-01 and reported the batch successful.
Measured with clickhouse local 24.8.14 on `v Int32 DEFAULT 7`: the row stored 7
under the server default and is rejected (Code 53 TYPE_MISMATCH) with the
setting at 0.

BaseDbWriter.customSettings() now produces the default list
input_format_null_as_default=0,allow_experimental_object_type=1,insert_allow_materialized_columns=1,
and appends input_format_null_as_default=0 to a user-provided
clickhouse.jdbc.settings list that does not mention the key (an explicit user
value is honoured). The failure is now loud and the batch is retried; the
remediation is on the ClickHouse side (make the column Nullable).

Spec: 07.07 §3.2.1 (new), §5; 03.05 §3.1.
Tests: JdbcCustomSettingsTest (3 tests; 2 fail on the pre-fix code, mutation
check flipping the default to =1 turns defaultSettingsDisableNullAsDefault red).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…o DST-gap shift

MySQL DATETIME is zone-less wall-clock digits, which Debezium encodes as a UTC
epoch (LocalDateTime.toInstant(UTC)). TimestampConverter (DATETIME(0..3))
decoded that epoch through TimeZone.getRawOffset/inDaylightTime of the source
zone and then formatted it in the session zone. When both zones are the same
DST zone this moved every spring-forward gap wall time back by an hour:
2026-03-08 02:30:00 in America/Chicago was written as 01:30:00, silently, with
row counts intact. MicroTimestampConverter (DATETIME(4..6)) already took the
same-zone shortcut (format the epoch as UTC), so the two converters disagreed
on the same digits.

TimestampConverter now takes the same shortcut when the source and session
zones are equal, and when they differ it interprets the digits as a source-zone
wall time through ZoneRules, using the pre-transition offset for a gap time
exactly as MicroTimestampConverter does.

Spec: 07.03 §3.1 / §3.1.1 (new), §5.
Test: DebeziumConverterTest.testTimestampConverterGapTimePreserved (pre-fix:
expected 02:30:00.000 but was 01:30:00.000; mutation check disabling the
shortcut turns it red again). All 17 DebeziumConverterTest tests pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…pty source zone means the session zone

Two related zone defects in the value path.

1. ClickHouse parses a DateTime literal in the COLUMN's zone, but the
   converters rendered every instant in the ClickHouse session zone. The
   auto-create path declares DateTime64(p, 'UTC') columns, so on a session
   whose zone is not UTC every TIMESTAMP (Debezium ZonedTimestamp) and every
   zone-shifted DATETIME was stored off by the session offset: a TIMESTAMP of
   16:00Z through an America/Chicago session landed as 10:00 UTC. Measured
   with clickhouse local 24.8.14: '2022-01-01 10:00:00' into
   DateTime64(3,'UTC') reads back 04:00:00 in America/Chicago.
   PreparedStatementFieldMapper now resolves the zone declared by the target
   column type (ClickHouseColumn.getTimeZone) and ClickHouseDataTypeMapper /
   DebeziumConverter render instants in it, falling back to the session zone
   only when the column declares none. The same-zone DATETIME digits decode
   is unaffected (digits are digits in any column zone).

2. An empty database.connectionTimeZone was taken as UTC while an empty
   clickhouse.datetime.timezone fell back to the server zone, so the default
   configuration on a non-UTC server ran the "different zones" branch with a
   source zone nobody configured: DATETIME '10:00:00' was written as
   '04:00:00' on an America/Chicago server. resolveSourceTimeZone() now
   resolves an empty source zone to the session zone (digits preserved; no
   basis for a shift without a declared source zone), logged once at INFO.
   A wall-clock shift happens only when the operator declares a source zone
   that differs from the session zone.

Spec: 07.03 §3 table, §3.1.2 and §3.1.3 (new), §5.
Tests: PreparedStatementFieldMapperColumnZoneTest (3 tests, all fail on the
pre-fix code: 04:00:00 / 10:00:00 where 10:00:00 / 16:00:00 are expected),
DebeziumConverterTest.testTimestampIntoUtcColumnWhenServerZoneIsChicago,
ClickHouseDataTypeMapperTimeZoneTest (4). Mutation checks: ignoring the
column zone turns the TIMESTAMP and explicit-shift tests red; resolving the
empty source zone to UTC turns the digits test red.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
….of.range (default false) fails the batch

DebeziumConverter clamped every DATE / DATETIME / TIMESTAMP outside the
ClickHouse type's range to the bound with no log line at all, and
BigDecimalConverter clamped with a WARN naming neither table nor column. A
MySQL DATETIME '9999-12-31 23:59:59' (the customary open-ended sentinel)
landed as 2299-12-31 23:59:59 with the batch reported successful -- a value
MySQL never held.

New connector setting clamp.out.of.range (Boolean, default false):
- false: DebeziumConverter.RangePolicy throws ValueOutOfRangeException naming
  the column (db.table.column), the source value, the ClickHouse type, its
  bounds and the setting that would saturate. The batch fails and is retried;
  nothing is written.
- true: the value is saturated as before and every saturation logs a WARN
  naming the column, the source value and the stored value.
PreparedStatementFieldMapper builds the policy per bound column and
ClickHouseDataTypeMapper.convert threads it into TimestampConverter,
MicroTimestampConverter, DateConverter, ZonedTimestampConverter and
BigDecimalConverter. The pre-existing policy-less converter overloads keep
saturating but now WARN (the existing DebeziumConverterTest range tests pin
that behaviour). ZonedTimestampConverter bounds the parsed instant outside
the format-probing loop so a rejection is not swallowed as a format mismatch.
PostgreSQL infinity / -infinity keep their documented saturation.

Spec: 07.03 §3.3 (new, with upgrade note), §5; doc/configuration.md row.
Tests: DebeziumConverterRangePolicyTest (6; the two mapper-level default
tests fail on the pre-fix code: "Expected RuntimeException to be thrown, but
nothing was thrown"), PreparedStatementFieldMapperOutOfRangeTest (1).
Mutation check: forcing clamp=true in RangePolicy.of turns the default
tests red.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…on-POLYGON spatial types are String (WKB hex)

ClickHouseDataTypeMapper wrote an EMPTY polygon ([]) whenever it could not
represent a Debezium Geometry value: a non-Polygon geometry (LINESTRING,
MULTI*, GEOMETRY) bound for a Polygon column, an unparseable WKB payload, a
missing wkb field, or a non-Struct carrier. A non-Struct Point became the
origin, a non-Struct variable-scale decimal became 0, and an unparseable
ZonedTimestamp became '' after an ERROR line. Every one of these stored a
value the source never held with the batch reported successful.

- Geometry / Point / VariableScaleDecimal / ZonedTimestamp: every such case now
  throws IllegalArgumentException naming the column (when known), the
  geometry type and the reason; the batch fails and is retried.
- A Geometry value bound for a String column is stored as its exact WKB
  bytes, hex-encoded (LOWER(HEX(ST_AsWKB(col))) on MySQL), or raw with
  persist.raw.bytes=true.
- Record-schema type mapping (auto-create and schema.evolution ADD COLUMN):
  a propagated source type of LINESTRING / MULTIPOINT / MULTILINESTRING /
  MULTIPOLYGON / GEOMETRYCOLLECTION / GEOMCOLLECTION / GEOMETRY maps to
  String (Nullable(String) when optional); POLYGON, and Geometry without a
  propagated type, stay Polygon; POINT stays Point.
- ClickHouseDataTypeMapper.canBeNullable centralises the "may be wrapped in
  Nullable" rule (not for Array/Map/Tuple/Nested or the geo types) and is
  used by getColumnNameToCHDataTypeMapping and by createTableSyntax /
  isNullableColumn, which previously emitted Nullable(Polygon) for an
  optional POLYGON column -- a type ClickHouse rejects.

Spec: 07.06 rewritten (§3.1 type mapping, §3.2 value binding, §3.3 other
substitutions, §5).
Tests: ClickHouseDataTypeMapperGeometryTest (10; 8 fail on the pre-fix code:
"[]" bound / nothing thrown / Nullable(Polygon)). Mutation check: restoring
the empty-polygon fallback for a non-Polygon geometry turns
nonPolygonIntoPolygonColumnThrows red.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…eld only in letter case

INSERT membership matches ClickHouse column names to record fields
case-insensitively (QueryFormatter.createColumns), but the bind-time value
read used the ClickHouse name verbatim (struct.getWithoutDefault(colName)),
which Kafka Connect resolves case-sensitively. A table created by hand as
`ID` Int32, `Amount` Float64 for source columns id, amount therefore threw
DataException on every row; PreparedStatementFieldMapper classified that as
a stale cache and the batch was retried forever against a cache that was
never stale -- a permanent stall with nothing written.

PreparedStatementFieldMapper.resolveSourceField(struct, colName) now resolves
the field against the record's schema (exact match, then case-insensitive)
and the value is read with getWithoutDefault(field.name()). The record's
schema is used rather than the modified-field list because that list omits
NULL-valued fields, and a NULL in a case-mismatched column must still be
bound as NULL. The private getFieldByColumnName(list) helper is replaced by
it. A name matching no field under either comparison still reaches the
stale-cache branch, which is the condition it was written for.

Spec: 04.03 §3.4 (new), §5.
Test: PreparedStatementFieldMapperColumnCaseTest (1; pre-fix:
StaleSchemaCacheException "Column ID is present in the ClickHouse table but
absent from the record's schema"). Mutation check: reverting the read to the
verbatim column name turns it red again.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…mapping, matching what is bound

ClickHouseDataTypeMapper.mapDebeziumSchemaToDDL (used by the PostgreSQL
schema reconciler for ADD COLUMN) declared io.debezium.time.MicroTime as
Nullable(Int64) ("microseconds since midnight"), while the value path binds
MicroTime as the formatted text [-]HH:mm:ss.ffffff (Spec 07.03 §3.2). Every
insert into such a reconciled TIME column therefore failed to parse. The
declared type and the bound representation must agree: Nullable(String).

Both existing tests pinned the mismatch (ClickHouseDataTypeMapperDDLTest and
PostgresSchemaReconcilerTest asserted Nullable(Int64)); they are flipped
deliberately, with the reason in the fixture.

Spec: 07.03 §3.2, §5.
Tests: ClickHouseDataTypeMapperDDLTest.testDebeziumLogicalTypes (MicroTime
row; pre-fix: expected Nullable(String) but was Nullable(Int64)),
PostgresSchemaReconcilerTest.testDebeziumLogicalTypes. Mutation check:
reverting the mapping to Nullable(Int64) turns both red.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…SON, ENUM/SET, TIME, BOOL, FLOAT and binary values

Spec-only. The type-system specs described target types but not the exact
text the writer stores, which is what any value-level comparison between
MySQL and ClickHouse must expect:

- 07.02 §3.1: MySQL FLOAT is delivered by Debezium as FLOAT64 and stored in
  Float64 on both paths -- a widening that renders 1.1 as 1.100000023841858;
  REAL is Float32 on the DDL path; comparisons must use toFloat32 or a
  common rendering.
- 07.04 §3.1 (new): verbatim strings, ENUM labels, comma-joined SET labels,
  Debezium's canonical JSON rendering, TIME always with six fraction digits,
  BIT(1) as Bool, BOOL/TINYINT(1) delivered as INT16 (0/1), binary as hex.
- 07.05 §3.2 (new): the stored text of a binary value by
  binary.handling.mode x persist.raw.bytes -- lower-case hex (default), raw
  bytes, base64 text (the shipped deployment templates set base64, so
  persist.raw.bytes has no effect there), or Debezium's upper-case hex.

No code changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…y addBatch, raw JSON without schema defaults

The V2 JDBC driver's PreparedStatementImpl.addBatch() substitutes the bound
values into the SQL template and keeps them (verified from the 0.9.8
bytecode: only clearParameters() resets values[]). Two consequences in the
writer:

1. PreparedStatementFieldMapper logged "DATA TYPE NOT HANDLED" and continued
   when ClickHouseDataTypeMapper.convert had no handler for a field (e.g. a
   MAP), leaving the parameter unbound. On the first row the driver failed
   the batch; on every later row the PREVIOUS row's value was written at that
   index, silently. It now throws DataException naming the type, the column
   and the table.
2. PreparedStatementExecutor now calls ps.clearParameters() immediately after
   every ps.addBatch() (ordinary row and relocation tombstone), so no bind
   state survives from one row to the next on either driver.

Also, TableMetaDataWriter.convertRecordToJSON (the store.raw.data copy of the
row) read fields with Struct.get, which substitutes the Connect-schema
default for a source NULL; it now reads with getWithoutDefault (NULL fields
stay omitted, as before).

Spec: 07.07 §3.2.2 (new), §5; 03.06 §3.2, §5.
Tests: PreparedStatementFieldMapperUnhandledTypeTest (pre-fix: nothing
thrown), PreparedStatementExecutorClearParametersTest (pre-fix call sequence
[setObject, setString, addBatch, setObject, setString, addBatch,
executeBatch, close] -- no clearParameters),
TableMetaDataWriterTest.testConvertRecordToJSONDoesNotSubstituteSchemaDefault
(pre-fix: {"id":1,"status":"new"}). Mutation checks: removing the
clearParameters calls, the throw, and the getWithoutDefault read each turn
the respective test red.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ted source type (BIGINT UNSIGNED hazard)

In Kafka Connect mode nothing forces column.propagate.source.type, so the
record-schema auto-create path sees a BIGINT UNSIGNED as a bare INT64 field,
declares Int64, and a value at or above 2^63 is stored as the wrapped
negative number (the UInt64 restore of Spec 07.01 §3.1 cannot apply to an
Int64 column). The connector cannot resolve the ambiguity without source
metadata; it now says so.

getColumnNameToCHDataTypeMapping logs one ERROR per table (static, per JVM)
naming the INT64 column(s) that carry no __debezium.source.column.type and
the remedy (column.propagate.source.type=.* or a hand-created table).
ClickHouseAlterTable.alterTable passes the table name through so the ADD
COLUMN path is attributable too; overrides still require both names and stay
skipped there, as before.

Spec: 07.01 §3.2 (new), §5.
Test: ClickHouseTableOperationsBaseUntypedInt64Test (1; pre-fix: no ERROR
logged). Mutation check: dropping the report turns it red.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ath; engine column collision; no legacy engine on a metadata failure

The Kafka-mode auto-create and schema.evolution ADD COLUMN path declared
ClickHouse types that differed from what the DDL translator declares for the
same MySQL column, so a table created by one path and evolved by the other
carried two types for one source type, and value-level comparisons rendered
the same instant differently.

Record-schema path (ClickHouseTableOperationsBase.getColumnNameToCHDataTypeMapping),
using Debezium's propagated source metadata where present:
- signed TINYINT -> Int8 (was Int16; DDL path says Int8);
- unsigned integers are Nullable(UIntN) when optional (the unsigned branch
  skipped the Nullable wrap, so ADD COLUMN declared a non-Nullable unsigned
  column for a nullable source column and the first NULL failed the batch);
- DECIMAL: Decimal(p, s) from the parameters, a missing precision is
  max(10, s), a missing scale 0 -- a dimensionless DECIMAL is Decimal(10,0)
  as MySQL defines it and the DDL path declares (was Decimal(10,2));
- DATETIME(p)/TIMESTAMP(p): DateTime64(p, 'UTC') with p from
  __debezium.source.column.length (a propagated DATETIME/TIMESTAMP without a
  length is p = 0); without a propagated type the previous 3/6 defaults.
The shared helpers (getSignedTinyIntType, decimalType, temporalPrecision)
live in ClickHouseDataTypeMapper.

createTableSyntax renames the engine delete-marker column to _is_deleted
when the source table has its own is_deleted column (as the DDL translator
does) instead of declaring the name twice, which ClickHouse rejects; the
source column keeps its name and nullability.

DbWriter.isNewReplacingMergeTreeEngine no longer returns false when
SELECT VERSION() fails: the failure propagates and no table is created in
that initialisation. It previously created the legacy
ReplacingMergeTree(_version) + _sign layout permanently after a transient
metadata failure.

Spec: 08.05 §3.1 / §3.1.1 (new, incl. the residual DDL-path differences),
§5; 06.05 §3.4.1 (new), §5.
Tests: ClickHouseTableOperationsBaseTest (Decimal(10,2) assertion flipped to
Decimal(10,0); new getColumnNameToCHDataTypeMappingSourceTypeParityTest),
ClickHouseAutoCreateTableTest.testSourceIsDeletedColumnRenamesEngineColumn
(pre-fix DDL declares `is_deleted` twice), DbWriterEngineSelectionTest
(pre-fix: nothing thrown), RecordSchemaVsDdlTypeAgreementTest in the
lightweight module (table-driven; pre-fix disagreements: TINYINT Int16 vs
Int8, DATETIME DateTime64(3) vs (0), TIMESTAMP DateTime64(6) vs (0)).
Mutation check (one run, three reversions): reverting the TINYINT mapping,
the engine-column rename and the version-failure propagation turn the
parity test, testSourceIsDeletedColumnRenamesEngineColumn and
DbWriterEngineSelectionTest red respectively (3 failures out of 14).
The DDL path's zone-less DateTime64(p, 0) is left as is: clickhouse local
24.8.14 stores DateTime64(3, 0) as DateTime64(3).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@minguyen9988
minguyen9988 merged commit cb523f1 into 2.11.0 Sep 22, 2026
6 of 11 checks passed
@minguyen9988
minguyen9988 deleted the fix-writer-value-fidelity branch September 22, 2026 05:09
minguyen9988 added a commit that referenced this pull request Sep 22, 2026
…-commit-order

Resolves eight conflicts of merging the current 2.11.0 head (now containing #1475/#1476/#1477) into the version-floor branch. Additive/kept-both in the Lean and spec files: both BatchOrder (#1476) and VersionFloor (this branch) listed in lakefile.lean, formal_specs/lean/README.md, CONSTITUTION.md, and spec 11.03 (numbered proof list renumbered so BatchOrder is 13 and VersionFloor 14-16); spec 04.04 keeps #1476's collapsing/grouping bullets and this branch's composite-PK bullet; spec 01.06 keeps #1477's terminal classifier plus this branch's heartbeat-sequence-state test.

The one semantic conflict is the version-assignment hot loop of DebeziumChangeEventCapture. Both behaviours are kept: #1478's VersionAssignment machinery (nextVersionAssignment + coverAssignedVersion, control records excluded from the sequence, chStruct versioned from assignment.sequenceNumber/effectiveTs) AND #1477's loud-failure path (rejectUnrepresentableValue before the sequence; parse() wrapped so a parser throw becomes RecordReplicationException; a null-parsed ROW record is terminal, not a WARN skip; the RecordReplicationException/DDLReplicationException rethrows ahead of the catch-all). NullParsedRowRecordIsTerminalTest keeps its terminal assertion, adapted to the VersionAssignment reflective signature.

Verification: lightweight compile clean; NullParsedRowRecordIsTerminalTest + ControlRecordLogLevelTest + DebeziumChangeEventCaptureTest + UnparseableRowRecordIsTerminalTest 38/38; spec validator PASSED (59 specs, 519 citations); lake build completed successfully (BatchOrder + VersionFloor both built, 0 sorry); full sink-connector unit suite 547 run, 0 failures, 20 errors all Docker-environment (identical to base). config.yml.j2 rendered (minimal + full variants) and YAML-linted with duplicate-key detection: no unrendered markers, no duplicate keys.

All 42 destructive sites in this merge are #1475/#1476/#1477 code already carrying DESTRUCTIVE: markers or test-code literals (statement text parsed/classified/logged, no execution against any live database), unchanged by this merge.

Jinja-Render-Check: rendered=2; formats=yaml; result=pass

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

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
minguyen9988 added a commit that referenced this pull request Sep 22, 2026
…ypes; fix inherited DdlIgnoreRulesTest signature

Resolves nine conflicts of merging the current 2.11.0 head (now containing #1475/#1476/#1477/#1478) into the ddl-translation branch. MySqlDDLParserListenerImpl.java: both sides added independent fields (this branch's PK-change/ddlEventTimestamp fields and #1477's dropOrTruncateStatement field + isDropOrTruncateStatement method) - kept both. Lean lakefile/README, CONSTITUTION and spec 11.03 list all of BatchOrder (#1476), VersionFloor (#1478) and this branch's CreateTable. spec 07.01 keeps this branch's DDL-path unsigned rule (§3.2) and #1478's record-path untyped-Int64 report (renumbered §3.3). spec 07.02 keeps this branch's IEEE section plus #1475's value-comparison caveat. spec 06.05 keeps both verification sets including #1478's RecordSchemaVsDdlTypeAgreementTest bullet. spec 07.06 (spatial) is a genuine two-path merge: the record-schema path keeps POINT/POLYGON native (#1475), the DDL path types every spatial column String/WKB-hex (this branch, new §3.4); they agree on every non-polygon type and the RecordSchemaVsDdlTypeAgreementTest does not cover spatial, so the code coexists.

CI fix: 2.11.0 itself is internally inconsistent - #1478 changed processEveryChangeRecord's last parameter to VersionAssignment but DdlIgnoreRulesTest (added by #1477) still reflected long.class, so disableDropTruncateIsScopedAndLive and disableDropTruncateExemptsSnapshotDdl threw NoSuchMethodException. This is the java-tests-lightweight failure the branch inherits through the merge. Fixed the reflective lookup and call to use VersionAssignment (mirroring NullParsedRowRecordIsTerminalTest). The same one-line fix should be back-ported to 2.11.0 directly.

Verification: spec validator PASSED (59 specs, 534 citations); lightweight compiles; DdlIgnoreRulesTest + MySqlDDLParserListenerImplTest + DataTypeConverterTest + CreateTableNoKeySortKeyTest + CreateTableUniqueKeySortKeyTest + NullParsedRowRecordIsTerminalTest 175/175 (1 Docker skip); full lightweight unit suite 556 run, 0 failures, 62 errors all Testcontainers/Docker (no daemon here). config.yml.j2 rendered (minimal + full) and YAML-linted, no duplicate keys.

All destructive sites in this merge are #1475/#1476/#1477/#1478 code with DESTRUCTIVE: markers or test-code literals, unchanged by this merge.

Jinja-Render-Check: rendered=2; formats=yaml; result=pass

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

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
minguyen9988 added a commit that referenced this pull request Sep 22, 2026
Rebases the CollapsingMergeTree clear-parameters fix onto the current 2.11.0 head, which now contains #1475-#1480. #1476 was squash-merged, so its commits on this branch conflicted with the same content already in 2.11.0; every such conflict (Lean lakefile/README, CONSTITUTION, specs 04.04/10.04/11.03, DbWriter) is resolved by taking the 2.11.0 side, which carries the accumulated #1476-#1480 content that subsumes this branch's #1476 commits. The only net change this branch adds over 2.11.0 is the one-file CollapsingMergeTree fix in PreparedStatementExecutor.java, re-applied on top of 2.11.0's version.

The fix: the CollapsingMergeTree UPDATE branch stages the -1 cancel row with ps.addBatch() but did not call ps.clearParameters() afterward, unlike the ReplacingMergeTree relocation-tombstone branch. The V2 JDBC driver retains bound values across addBatch(), so a parameter the after image does not rebind could inherit the cancel row's sign=-1 and lose the UPDATE. Defensive-consistency fix (Gemini panel finding on #1476, demoted-unproven because the sign is always rebound in practice); matches the RMT tombstone branch and the general per-row path (Spec 07.07 s3.2.2, Spec 05.04 s3.1).

Verification: net PR diff vs 2.11.0 is exactly PreparedStatementExecutor.java; sink-connector compiles; PreparedStatementExecutorCollapsingSignTest + ClearParametersTest + SortingKeyTombstoneTest + TruncateTest 17/17; spec validator PASSED (59 specs, 534 citations); config.yml.j2 rendered (minimal + full) and YAML-linted, no duplicate keys.

All 25 destructive sites in this merge are #1475-#1480 code with DESTRUCTIVE: markers or test-code literals, unchanged by this merge.

Jinja-Render-Check: rendered=2; formats=yaml; result=pass

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

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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