Skip to content

fix(iceberg): close the gaps the post-merge inspection of #707 found - #709

Merged
fas89 merged 5 commits into
mainfrom
fix/iceberg-catalog-followups
Oct 7, 2026
Merged

fas89 merged 5 commits into
mainfrom
fix/iceberg-catalog-followups

Conversation

@fas89

@fas89 fas89 commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Description

This fixes the P2 findings from the post-merge inspection of #707 (one catalog-kind table for every emitter). The inspection was a world-class review across docs, codebase/security, tests and architecture, with every finding checked by an adversarial verifier and the CLI UX walked by hand. All findings below were reproduced first and are covered by tests that fail without the fix.

Finding Before After
CLI output loses [...] Rich parsed messages as markup. fluid validate printed exposes declares governance.lakeFormation, bundle findings lost [error], and pip install 'pkg[gcp]' lost its extra. Validation lines, CLIError context and suggestions, and FluidCLIError.format_for_user print literally. tofu state rm <address> stays on one line.
Preflight skipped hand-written sinks Runners preflighted only a derived config. A hand-written sink_connector_config that validate refused was still deployed. One gate, iceberg_sink_plan, shared by the validator, the preflight and both runners. Hand-written configs are checked before anything is created.
Preflight and runner on different builds Preflight used build_id, but the runners read builds[0] properties. Runners read the build they execute (executing_build).
GCP absent catalog split The sink wrote through REST while dbt-bigquery and the GCP IaC created BigLake, and validate was silent. Validate error, and run error, unless the catalog type that reaches the worker is BigLake too.
catalog: bigquery on Kafka Connect Emitted type=bigquery, which sink 1.9.2 cannot load. Silent. Warning. The Nessie and BigQuery warnings follow the type that reaches the worker.
Catalog-move guard false positive A Glue database left by a removed parquet expose was flagged, and nothing could get past it. Flagged only when the moved expose's own Glue table is in state.
Guard wiring untested Only a source-text order test existed; disabling the call passed 180 tests. Behaviour tests through apply_via_opentofu for AWS and Snowflake. Probe failures log at WARNING.
No Snowflake upgrade path External-catalog Iceberg on Snowflake lost its EXTERNAL VOLUME, so the next apply would plan to drop it. Per-plugin CatalogMoveSpec. Snowflake volumes are guarded with tofu state rm remediation.
Native planner accepted unknown kinds catalog: glu planned buckets and no Glue table. Refused like the IaC, with a typed UnknownIcebergCatalogError and no plan_failed log line. A typo no longer also draws a Lake Formation refusal.
Matrix overclaimed coverage The docstring said "EVERY emitter". The agreement matrix now covers the planner, the policy compiler, Confluent and dbt-BigQuery.

P3 findings, plus the out-of-scope defects the reviewers found (the meltano, dlt and airbyte runners also read builds[0], among others), are filed on Trello.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would break existing functionality)
  • Documentation update
  • Refactor / code cleanup
  • New provider or provider enhancement

The breaking change: a GCP Iceberg expose that a streaming sink writes to must now name its catalog. It is called out in the CHANGELOG upgrade notes, with the remedy.

Documentation

Checklist

  • I have read the Contributing Guide
  • My code follows the project's coding standards
  • I have added tests that prove my fix/feature works
  • All new and existing tests pass (pytest) — 4 local-environment failures fail identically on main, see Testing
  • I have run ruff check and black (24.10.0) with no errors
  • New maintained Python files include license headers

Testing

  • Full suite (-n 8 --dist loadscope): 20905 passed, 556 skipped. Four tests fail locally, and they fail identically on main (9acc511d):
    • tests/iac/test_iac_moto_e2e.py (×3): Glue catalog id under OpenTofu 1.12 and a newer moto. A separate fix is in progress.
    • tests/test_coding_agent_live.py: local claude CLI; not run in CI.
  • Lint: ruff, black --check (24.10.0) and scripts/mypy_strict.py are clean.
  • Reviews:
    • Each slice was reviewed independently, with the finding's repro re-run plus a mutation.
    • A cross-slice seam review then found 5 boundary defects, all fixed here with tests:
      1. a GCP error on hand-written configs;
      2. the bigquery warning on hand-written configs;
      3. a contradictory disagreement error on GCP;
      4. a duplicated plan_failed line;
      5. a cascading Lake Formation refusal on a typo.
  • Security review of 9acc511d..b87fd3a7: no findings.
    • Redaction still runs on every printed line.
    • The preflight never echoes config values.
    • The unknown-kind governance skip is backed by the validate error and by the IaC refusal at apply.

Fixed before merge: defect scan of this PR

A defect scan of this branch (41 agents across static, dynamic and CLI-journey lanes, each finding checked by a skeptic) found four gaps in this PR's own checks. All four are fixed here, each with a reproduce-first test and a mutation-killed regression test:

  1. Table-less expose (a regression): the catalog-move guard planned to destroy the v0.19.0 Glue database of a moved Iceberg expose that names no table. A database is now flagged when the expose's own table is in state, or when the expose names no table.
  2. Snowflake guard after a storage change: the guard missed the old volume when the upgrade also changed location.warehouse or dropped iam_role_arn. It now finds the volume by its contract-derived name, which a test pins to the emitter's own key.
  3. GCP gate through catalog-impl: the gate missed sinks that reach their catalog through catalog-impl, such as sink.catalog: glue or a hand-written impl. It now keys on the catalog that reaches the worker.
  4. Hand-written config selecting a different catalog: a hand-written or override type/catalog-impl that selects a different catalog than the expose's is now an error on every platform. Following the GCP remedy while a hand-written config wrote REST used to pass. Glue's Iceberg REST endpoint users declare catalog: rest.

The catalog impl class names were checked against apache/iceberg CatalogUtil.

The other 21 confirmed defects, including one pre-existing High (a streaming sink always writes the first Iceberg expose), are filed on Trello.

Full suite after the fixes: 20947 passed, 555 skipped. The same 4 local-environment failures fail identically on main.

Tested live

The real fluid CLI built from this branch:

$ fluid validate A_typo.fluid.yaml        # location.catalog: lakekeper + governance.lakeFormation
 1. expose 'orders': binding.location.catalog 'lakekeper' is not a catalog kind FLUID knows. Use one of: ...
   (one error; no Lake Formation refusal naming the typo as a catalog)

$ fluid validate B_lk_with_lf.fluid.yaml  # catalog: lakekeeper + governance.lakeFormation
 1. exposes[orders] declares governance.lakeFormation, but its Iceberg table lives in the 'lakekeeper' catalog ...

$ fluid validate gcp_absent.yaml          # GCP Iceberg sink target, no location.catalog
 1. iceberg sink (build 'ingest_events'): the GCP Iceberg expose sets no binding.location.catalog, so it is read two ways: the sink would write through a REST catalog (the 'gcp' platform default) while dbt-bigquery and the GCP IaC ... create a BigLake metastore table. Set binding.location.catalog: bigquery, or the REST kind your catalog is (e.g. rest, lakekeeper)

$ fluid generate iac A_typo.fluid.yaml --provider aws
❌ unsupported_binding  [ERR_UNSUPPORTED_BINDING]
  kind: unknown-iceberg-catalog
  error: exposes[orders] names Iceberg catalog 'lakekeper' (location.catalog), which FLUID does not know, ...
   (no plan_failed JSON line before it)

- 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.
@fas89 fas89 added the ci:integration-emulated Maintainer vouch — run the admin-gated heavy-emulated lane (bigquery-emulator + LocalStack) label Oct 7, 2026
@fas89
fas89 deployed to integration-emulated October 7, 2026 10:56 — with GitHub Actions Active
@fas89
fas89 had a problem deploying to integration-emulated October 7, 2026 10:56 — with GitHub Actions Failure
@github-actions github-actions Bot added provider Changes or requests related to providers cli Changes to the CLI surface or implementation tests Test coverage or test infrastructure changes labels Oct 7, 2026
Comment thread tests/cli/test_cli_output_keeps_brackets.py Fixed
Comment thread tests/cli/test_cli_output_keeps_brackets.py Fixed
Comment thread tests/cli/test_cli_output_keeps_brackets.py Fixed
…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
fas89 deployed to integration-emulated October 7, 2026 11:13 — with GitHub Actions Active
@fas89
fas89 had a problem deploying to integration-emulated October 7, 2026 11:13 — with GitHub Actions Failure
@github-actions github-actions Bot removed the ci:integration-emulated Maintainer vouch — run the admin-gated heavy-emulated lane (bigquery-emulator + LocalStack) label Oct 7, 2026
Comment thread fluid_build/_error_catalog.py Fixed
Comment thread fluid_build/_error_catalog.py Fixed
Comment thread tests/cli/test_cli_output_keeps_brackets.py Fixed
fas89 added 2 commits October 7, 2026 14:18
- 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.
Comment thread tests/iac/test_iac_catalog_moves.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli Changes to the CLI surface or implementation provider Changes or requests related to providers tests Test coverage or test infrastructure changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant