Repository navigation
feat(mysql): binary column support and backwards compatibility tests - #1137
mihir-datazip wants to merge 121 commits into
Conversation
9de32a7 to
d4795ee
Compare
…mpatibility-tests
…ersion 8 ReformatBytes passes byte slices through and turns any other value into the bytes of the text a string column would hold, using reformatString, which the String case now shares. A nil value returns ErrNullValue. fallbackType and fallbackIcebergType are now functions, so they read the state version a sync loads. An undeclared or unregistered type falls back to Binary from state version 8 and to String below it.
…olumn 07a8978 made undeclared and unregistered types fall back to Binary from state version 8. Null and Unknown are unregistered, so Mongo's always-null field and its Regex, MinKey and MaxKey fields (typed Unknown) became Iceberg binary columns and failed the Mongo integration tests. fallbackType is a String var again, now right after the DataType const block, and TestToIceberg and TestToNewParquet pin Null and Unknown. The Mongo suite now checks id_nil's destination type. Parquet writes an always-null column as untyped bytes, which Spark reads as binary, so TestHandler takes a ParquetTypeMapping that replaces TypeMapping for Parquet, and MongoTypeMapping becomes a function so MongoParquetTypeMapping can map null to binary on a fresh copy.
A Kafka sweep went red because the v0.7.0 baseline's own stateless load hung, before the upgrade side ever ran the candidate. The report still listed it as "a cdc side never finished case """ under a state version 6 headline, which points at the state handover. When the case a side never finished is the stateless load, the reason now adds that both sides were still on the baseline, before the upgrade, so no state version is implicated.
…mpatibility-tests
…mpatibility-tests
…mpatibility-tests
| if width, isFixed := types.FixedBinaryWidth(p.schema[column].Type()); isFixed && len(b) != width { | ||
| return fmt.Errorf("field[%s]: %s holds exactly %d bytes, got %d", column, p.schema[column].Type(), width, len(b)) | ||
| } |
There was a problem hiding this comment.
A legitimate width change has no evolution path, so this turns into a permanent failure for the rest of the sync run. The type-merge machinery cannot see the drift: FlattenAndCleanData's diff check (elem.ValueType() == detectedType) and Fields.Process (currentType.ValueType() != detectedType) both treat fixed_binary(n) and Binary as the same family, so a column declared binary(4) at discovery that receives 8-byte values after a mid-sync ALTER TABLE ... BINARY(8) (post-ALTER binlog/backfill row images are 8 bytes) never sets diffFound/typeChange, EvolveSchema never runs, and every subsequent record fails here as a WriteFailure; in-process retries keep failing since p.schema never widens. Recovery requires restarting the sync so discovery re-reads the new width. Consider widening the field to Binary on a width mismatch (fold fixed_binary(n)+Binary → Binary) and surfacing it through the existing schema-change flow so EvolveSchema reopens partition files with a byte_array leaf, keeping this guard only for values seen before the schema catches up.
| if err := run("build the iceberg writer jar", "make", "-C", worktree, "iceberg.jar"); err != nil { | ||
| return err | ||
| } | ||
| return run("build the image", "docker", "build", "--build-arg", "DRIVER_NAME="+cfg.Driver, "-t", imageTag, worktree) |
There was a problem hiding this comment.
This commit-worktree build runs a plain docker build without --platform, while every other path in the harness honors the platform pin: the make-based build passes --platform from drivers/platforms.conf (Makefile docker.%.build), ensureImagePresent pulls with --platform, and DockerRunArgs runs with --platform cfg.ImagePlatform. For a platform-pinned driver this is reachable: db2 pins linux/amd64 (tests/db2/db2_test.go) and registers a compatibility suite whose baselines resolve to commit IDs (OLAKE_COMPATIBILITY_TEST_BASELINE accepts commit specs; ResolveToCommit rewrites them in RunBackwardCompatibility). On a non-amd64 host the commit-built image has the host architecture and the subsequent docker run --platform linux/amd64 fails with an image/platform mismatch, breaking the test-a-previous-commit flow for exactly the drivers that need the pin. Pass the platform here like the pull and run paths do.
Suggestion:
| return run("build the image", "docker", "build", "--build-arg", "DRIVER_NAME="+cfg.Driver, "-t", imageTag, worktree) | |
| buildArgs := []string{"build", "--build-arg", "DRIVER_NAME=" + cfg.Driver, "-t", imageTag} | |
| if cfg.ImagePlatform != "" { | |
| buildArgs = append(buildArgs, "--platform", cfg.ImagePlatform) | |
| } | |
| return run("build the image", "docker", append(buildArgs, worktree)...) |
| require.Eventually(t, func() bool { | ||
| if ctx.Err() != nil { | ||
| return true | ||
| } |
There was a problem hiding this comment.
A cancelled/expired context is reported as "the sync made progress" without stats.json ever being observed, silently breaking this function's documented contract ("blocks until the running sync has reported its first records"). The only current caller (the kafka rebalance trigger in tests/kafka/kafka_util_test.go) compensates by re-checking rebalanceCtx.Err() right after, so no misbehavior today, but the helper is exported for driver suites to call per its doc comment -- a caller following the contract proceeds to its mid-sync mutation against a dead or never-started sync, producing flaky downstream verification failures that mask the real sync-startup problem. (Behavior carried over from the previous harness.) Fail loudly instead of returning true.
Suggestion:
| require.Eventually(t, func() bool { | |
| if ctx.Err() != nil { | |
| return true | |
| } | |
| require.Eventually(t, func() bool { | |
| if ctx.Err() != nil { | |
| // A dead context must not read as "sync made progress": report it, so a caller | |
| // does not run its mid-sync mutation against a sync that never started. | |
| t.Errorf("context ended before %s reported progress: %v", statsPath, ctx.Err()) | |
| return false | |
| } |
| th.icebergDestination = testutils.Ternary(useArrowWriter, icebergArrowDestinationFile, icebergDestinationFile).(string) | ||
|
|
||
| return testFunc(ctx, t, testTable) |
There was a problem hiding this comment.
th.icebergDestination is set for the duration of testFunc but never restored afterwards, so it keeps pointing at the last-selected writer config long after the scoped call returns. Today this is benign (every iceberg sync resolves destinationFile inside an IcebergWriter scope), but any future sync that goes through runSyncAndVerify/destinationFile outside this scope will silently target whichever writer ran last instead of the committed base config. Restore the previous value so the selection stays scoped to the call, matching what the per-useArrowWriter parameter implies.
Suggestion:
| th.icebergDestination = testutils.Ternary(useArrowWriter, icebergArrowDestinationFile, icebergDestinationFile).(string) | |
| return testFunc(ctx, t, testTable) | |
| previous := th.icebergDestination | |
| th.icebergDestination = testutils.Ternary(useArrowWriter, icebergArrowDestinationFile, icebergDestinationFile).(string) | |
| defer func() { th.icebergDestination = previous }() | |
| return testFunc(ctx, t, testTable) |
Description
MySQL
BINARY,VARBINARYandBLOBcolumns holding non-UTF-8 bytes could not sync. OLake cast them to string, and the Iceberg writer failed withstring field contains invalid UTF-8. They now stay bytes end to end.BINARY(n)lands as Icebergfixed[n], and the other types land asbinary. State version 8, pinned tov0.12.2, gates the change, so syncs on older state keepstring.This PR also adds the backward-compatibility suite and splits the test harness to host it. The suite syncs each scenario twice, once on a baseline image and once with a switch to this build after the initial load, then compares the destinations. Separately,
LoadedStateVersionread 0 in commands without a state file and is now set ininit().Fixes #1031
Type of change
How Has This Been Tested?
86711085(run 38717757700). The run shows red only because the performance matrix is empty on PRs.make test.unit,make test.build.alland both lint targets passed after merging the lateststaging.BINARY(16),BINARY(4),VARBINARY(64)andBLOBcolumns landed byte-exact in Iceberg. Sparkhex()matched MySQLHEX().staging, run in this PR's CI.Screenshots or Recordings
N/A
Documentation
Related PR's (If Any):