Repository navigation
fix(iceberg): one catalog-kind table, so every emitter agrees on location.catalog - #707
Merged
Merged
Conversation
…tion.catalog
The streaming sinks, dbt catalogs.yml, the Snowflake, AWS and Confluent IaC, the
native AWS planner, policy compile, fluid diff and fluid test each classified the
free-string location.catalog by hand, and disagreed: `catalog: lakekeeper`
streamed over REST while dbt wrote a Snowflake-managed table and the AWS IaC
created a metadata-less Glue table of the same name. One table in
providers/_iceberg_catalog.py now answers for every emitter (shape borrowed from
dbt-adapters' _V2_TO_V1_TYPE hook and Airbyte's typed Polaris preset; wire
values only from Iceberg's CatalogUtil set).
Also fixes the Kafka Connect Glue sink, which set both iceberg.catalog.type and
catalog-impl and so failed at startup ("both type and catalog-impl are set";
reproduced against cp-kafka-connect 7.9.10 + the Apache Iceberg sink 1.9.2).
- AWS: no Glue database/table/import/grant/LF resource for an Iceberg table in
another catalog; LF, column restrictions and row filters on one are refused by
catalog name; unknown catalog values are refused at apply. Absent or glue is
byte-identical.
- fluid apply stops with iceberg_catalog_move_blocked (and prints tofu state rm
commands) when state holds Glue resources an earlier release created for such
a table, instead of planning to destroy them.
- fluid validate: unknown kinds, sink/expose disagreement, uri+warehouse for
every REST catalog, type+impl overrides, snowflake volume collisions; a crash
in an Iceberg/Confluent gate now fails validation instead of passing silently.
- The Kafka Connect and embedded Debezium Server runners preflight the same
checks before they create anything.
- RFC-streaming-extension: the Apache sink creates namespaces on auto-create
since Iceberg 1.6.0 (apache/iceberg#10186); the deriver emits impl XOR type.
…orker Real Kafka Connect + Apache Iceberg sink 1.9.2 + Lakekeeper v0.13.6 (STS-vended credentials, no storage keys) in the admin-gated emulated-heavy lane: - forge's derived config for catalog: lakekeeper streams and commits records; - a config with both iceberg.catalog.type and catalog-impl fails on the worker (the startup crash every Glue sink hit); - forge's derived Glue config (impl only) gets past that gate. assert_lane_coverage --require keeps the job from passing with every test skipped. Local smoke: 3 passed in 158s.
warehouse_is_name and uri_suffix had no row that set them in this change, so their two sink checks could never fire. They return with the Lakekeeper-specific validation that sets them.
fas89
had a problem deploying
to
integration-emulated
October 6, 2026 21:41 — with
GitHub Actions
Failure
5 of 9 tasks
fas89
added a commit
that referenced
this pull request
Oct 7, 2026
…atalog-move error to its docs The upgrade note named rest/iceberg_rest/polaris/unity/nessie as having had a Snowflake EXTERNAL VOLUME; they never did (the pre-#707 external set). The kinds that lost one are lakekeeper, bigquery, the iceberg-rest spelling, hive, jdbc, hadoop and dynamodb, which is what the guard checks. iceberg_catalog_move_blocked was not in the error catalog, so the CLI printed no docs link for it; it now links the guard's section in cli/apply.
fas89
added a commit
that referenced
this pull request
Oct 7, 2026
…709) * fix(iceberg): close the gaps the post-merge inspection of #707 found - CLI output keeps square brackets: validate messages, CLIError context and suggestions, and FluidCLIError.format_for_user print literally (Rich ate exposes[orders]); printed tofu state rm commands stay on one line. - Runners check the sink they push: one iceberg_sink_plan gate shared by the validator, the preflight and both runners; hand-written configs are preflighted; each runner reads the build it executes, not builds[0]. - GCP: an Iceberg sink target with no location.catalog is refused (sink REST vs dbt-bigquery/GCP IaC BigLake); a sink.catalog/hand-written type that reaches BigLake is not a split. - catalog: bigquery on Kafka Connect warns (sink 1.9.2 has no bigquery type); runtime warnings follow the catalog type that reaches the worker. - Unknown catalog values: the native AWS planner refuses them like the IaC, without a plan_failed log line; a typo no longer also draws a Lake Formation refusal. - Catalog-move guard: a Glue database is flagged only when the moved expose's own table is in state; Snowflake external volumes are guarded too; probe failures log at WARNING; wiring proven by behaviour tests through apply_via_opentofu. - Agreement matrix extended to the planner, policy compiler, Confluent and dbt-BigQuery. * fix(iceberg): correct which Snowflake kinds lost a volume; link the catalog-move error to its docs The upgrade note named rest/iceberg_rest/polaris/unity/nessie as having had a Snowflake EXTERNAL VOLUME; they never did (the pre-#707 external set). The kinds that lost one are lakekeeper, bigquery, the iceberg-rest spelling, hive, jdbc, hadoop and dynamodb, which is what the guard checks. iceberg_catalog_move_blocked was not in the error catalog, so the CLI printed no docs link for it; it now links the guard's section in cli/apply. * style: one import style per module in the bracket tests; explicit concatenation in the catalog-move guidance * style: import the apply engine module directly in the bracket tests * fix(iceberg): four gaps the defect scan found in this PR's own checks - Catalog-move guard: a moved Iceberg expose that names no table had its v0.19.0 Glue database planned for destroy (the evidence rule needed a table); a database is now flagged when the expose's own table is in state OR the expose names no table. The shared-database false positive stays fixed. - Snowflake guard: the old volume is found by its contract-derived name, so an upgrade that also set location.warehouse to a catalog name or dropped iam_role_arn is still stopped. - GCP split gate: keyed on the catalog that reaches the worker, so a sink that reaches Glue/DynamoDB through catalog-impl (sink.catalog: glue, hand-written catalog-impl) is refused like a REST one. - A hand-written sink config or override whose type/catalog-impl selects a different catalog than the expose's is an error on every platform (following the GCP remedy while a hand-written config wrote REST used to pass). Catalog impl class names checked against apache/iceberg CatalogUtil.
This branch had an error being deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
binding.location.catalogis a free string, and every emitter classified it by hand. They disagreed:catalog: lakekeeper: streamed over REST, while dbt'scatalogs.ymlwrote a Snowflake-managed table and the AWS IaC created a second, metadata-less Glue table of the same name.rest.catalog: hiveon Snowflake: became a Snowflake-managed table.A separate defect: the Kafka Connect deriver set both
iceberg.catalog.typeandiceberg.catalog.catalog-implfor Glue. Apache Iceberg'sCatalogUtilrefuses that, so every Glue sink failed at startup:This PR adds one catalog-kind table in
fluid_build/providers/_iceberg_catalog.py, and every emitter now reads from it._V2_TO_V1_TYPEalias hook and Airbyte's typed Polaris preset.CatalogUtilset. No engine or SDK has a vendor type such aslakekeeper.catalogs.yml, the Snowflake/AWS/Confluent IaC, the native AWS planner,fluid policy compile,fluid diff,fluid testandfluid validate.Behaviour changes, all fail-closed:
catalog-implortype, never both (the Debezium twin already did).fluid validaterefuses overrides that would add the other key back.glueproduces byte-identical output.fluid apply(AWS): stops withiceberg_catalog_move_blockedwhen state holds Glue resources an earlier release created for such a table, and prints thetofu state rmcommands. Without the guard, the next plan would destroy them, and destroying a Glue database deletes every table in it. This mirrors the packaging ownership-transition guard.fluid validaterefuses:sink.catalogthat disagrees with the expose;uri/warehouseon any REST catalog;hive,jdbc,hadoop,dynamodb);catalog: snowflakeexposes that share one EXTERNAL VOLUME.fluid validategate crashes: a crash in an Iceberg or Confluent gate now fails validation instead of printing only with--verbose.See CHANGELOG
[Unreleased]for the upgrade note and every fixed behaviour.Type of Change
Contracts that were previously accepted and wrote to the wrong catalog now fail
fluid validateon purpose. Each case has a CHANGELOG line.Documentation
Checklist
pytest) — see Testing for 4 local-environment failures that fail identically onmainruff checkandblack(24.10.0) with no errorsTesting
Unit and contract tests. The new tests fail against the old code (negative controls run per slice):
tests/providers/test_iceberg_catalog_kinds.py: the cross-emitter agreement matrix. Every emitter runs over every kind, including absent and unknown, and each answer is checked against the kind's row.test_iac_aws_iceberg_catalog.pyandtest_iac_catalog_moves.py.Full suite:
20772 passed, 552 skippedon the rebased commit. Four failures also fail identically onmain(e0a8b258) with the same venv:tests/iac/test_iac_moto_e2e.py: three tests.tests/test_coding_agent_live.py: "NOT run in CI"; it drives a localclaudeCLI.Lint:
ruff check,black --check(24.10.0) andscripts/mypy_strict.pyare clean.Security review of
e0a8b258..aa940e36: no findings.tofu state rmcommands areshlex-quoted.Tested live
New CI lane:
lakekeeper-integrationin the emulated-heavy workflow. It runs a real Kafka Connect worker (cp-kafka-connect 7.9.10, Apache Iceberg sink 1.9.2) against a real Lakekeeper v0.13.6. Credentials are vended over STS from an S3-compatible store, so no static storage keys are used.test_derived_lakekeeper_config_streams_into_lakekeepercatalog: Lakekeeperstreams 5 records and commits a snapshot withtotal-records = 5. The sink auto-creates the namespace.test_both_type_and_catalog_impl_fail_on_a_real_workertest_derived_glue_config_gets_past_the_catalog_gateassert_lane_coverage.py --requirefails the job if every test skipped.Local smoke run:
Negative control: restoring the old "type always, plus impl" emission fails the Glue test and the unit matrix.