This is an automated email from the ASF dual-hosted git repository.
danny0405 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hudi.git
The following commit(s) were added to refs/heads/master by this push:
new a99e89ed2835 feat(agent): Add first executable mutable Flink SQL MOR
sink path to Hudi Architect (#20302)
a99e89ed2835 is described below
commit a99e89ed28351cb1e2f78bed7b48fbbd9e0c37c6
Author: fhan <[email protected]>
AuthorDate: Sat Oct 17 09:50:28 2026 +0800
feat(agent): Add first executable mutable Flink SQL MOR sink path to Hudi
Architect (#20302)
* feat(agent): Add the first executable mutable Flink SQL MOR sink path to
Hudi Architect
* fix(agent): harden Flink MOR contract validation
---------
Co-authored-by: fhan <[email protected]>
---
.github/workflows/agent_gateway_ci.yml | 3 +
.../fixtures/hudi-architect-flink-1.20/pom.xml | 6 +
.../architect/TestFlinkArchitectSqlFixtures.java | 82 ++++++
hudi-agent-gateway/skills/hudi-architect/README.md | 16 +-
.../skills/hudi-architect/RUNBOOK.md | 11 +-
hudi-agent-gateway/skills/hudi-architect/SKILL.md | 20 +-
.../flink-1.20-hudi-1.2.0-capabilities.md | 38 ++-
.../flink-1.20-hudi-1.2.0-capabilities.toml | 195 +++++++++++++-
.../references/flink-config-templates.md | 51 +++-
.../references/flink-decision-overrides.md | 41 ++-
.../references/flink-question-flow.md | 76 +++++-
.../hudi-architect/references/flink-warnings.md | 83 +++++-
.../hudi-architect/validate_flink_capabilities.py | 118 ++++++++-
.../skills/hudi-architect/validate_flink_design.py | 290 ++++++++++++++++++---
.../hudi_architect/flink_pr4/mutable_mor.json | 85 ++++++
.../hudi_architect/flink_pr4/mutable_mor.sql | 62 +++++
.../tests/test_hudi_architect_skill.py | 261 ++++++++++++++++++-
17 files changed, 1340 insertions(+), 98 deletions(-)
diff --git a/.github/workflows/agent_gateway_ci.yml
b/.github/workflows/agent_gateway_ci.yml
index aafccf8b6936..47e31b8f34df 100644
--- a/.github/workflows/agent_gateway_ci.yml
+++ b/.github/workflows/agent_gateway_ci.yml
@@ -74,6 +74,9 @@ jobs:
python skills/hudi-architect/validate_flink_design.py \
--input tests/fixtures/hudi_architect/flink_pr3/mutable_cow.json \
--emit-sql > /dev/null
+ python skills/hudi-architect/validate_flink_design.py \
+ --input tests/fixtures/hudi_architect/flink_pr4/mutable_mor.json \
+ --emit-sql > /dev/null
- name: Test
run: python -m pytest tests -q
- name: Lint
diff --git a/hudi-agent-gateway/fixtures/hudi-architect-flink-1.20/pom.xml
b/hudi-agent-gateway/fixtures/hudi-architect-flink-1.20/pom.xml
index 0714ab8d6fbf..919f08edf04c 100644
--- a/hudi-agent-gateway/fixtures/hudi-architect-flink-1.20/pom.xml
+++ b/hudi-agent-gateway/fixtures/hudi-architect-flink-1.20/pom.xml
@@ -88,6 +88,12 @@
<include>*.sql</include>
</includes>
</testResource>
+ <testResource>
+
<directory>${project.basedir}/../../tests/fixtures/hudi_architect/flink_pr4</directory>
+ <includes>
+ <include>*.sql</include>
+ </includes>
+ </testResource>
</testResources>
<plugins>
<plugin>
diff --git
a/hudi-agent-gateway/fixtures/hudi-architect-flink-1.20/src/test/java/org/apache/hudi/agent/architect/TestFlinkArchitectSqlFixtures.java
b/hudi-agent-gateway/fixtures/hudi-architect-flink-1.20/src/test/java/org/apache/hudi/agent/architect/TestFlinkArchitectSqlFixtures.java
index 89ef68792fb7..fe2a074bf238 100644
---
a/hudi-agent-gateway/fixtures/hudi-architect-flink-1.20/src/test/java/org/apache/hudi/agent/architect/TestFlinkArchitectSqlFixtures.java
+++
b/hudi-agent-gateway/fixtures/hudi-architect-flink-1.20/src/test/java/org/apache/hudi/agent/architect/TestFlinkArchitectSqlFixtures.java
@@ -143,12 +143,39 @@ class TestFlinkArchitectSqlFixtures {
assertTrue(plan.contains("stream_write:
default_database.orders_mutable_hudi"), plan);
assertTrue(plan.contains("changelogMode=[I,UA,D]"), plan);
assertTrue(plan.contains("index_bootstrap"), plan);
+ assertFalse(plan.contains("compact_plan_generate"), plan);
+ assertFalse(plan.contains("compact_task"), plan);
+ assertFalse(plan.contains("compact_commit"), plan);
assertEquals(
Set.of(RowKind.INSERT, RowKind.UPDATE_AFTER, RowKind.DELETE),
ChangelogModes.UPSERT.getContainedKinds());
assertFalse(ChangelogModes.UPSERT.contains(RowKind.UPDATE_BEFORE));
}
+ @Test
+ void testMutableMorSinkAndCompactionPlan() throws Exception {
+ TestContext context = newTestContext();
+ StreamTableEnvironment tableEnv = context.tableEnv;
+ registerMutableSource(context);
+
+ SqlFixture fixture = SqlFixture.load("mutable_mor.sql");
+ applyRuntimeStatements(tableEnv, fixture.runtimeSql);
+ tableEnv.executeSql(fixture.tableDdl);
+ String plan = tableEnv.explainSql(
+ fixture.insertSql,
+ ExplainDetail.CHANGELOG_MODE,
+ ExplainDetail.JSON_EXECUTION_PLAN);
+
+ assertPinnedArtifacts();
+ assertTrue(plan.contains("orders_mutable_mor_hudi"), plan);
+ assertTrue(plan.contains("stream_write:
default_database.orders_mutable_mor_hudi"), plan);
+ assertTrue(plan.contains("changelogMode=[I,UA,D]"), plan);
+ assertTrue(plan.contains("index_bootstrap"), plan);
+ assertTrue(plan.contains("compact_plan_generate"), plan);
+ assertTrue(plan.contains("compact_task"), plan);
+ assertTrue(plan.contains("compact_commit"), plan);
+ }
+
@Test
void testPinnedMutableOrderingConfiguration() {
Configuration conf = mutableConfiguration();
@@ -179,6 +206,48 @@ class TestFlinkArchitectSqlFixtures {
assertTrue(conf.get(FlinkOptions.INDEX_BOOTSTRAP_ENABLED));
}
+ @Test
+ void testPinnedMorCompactionConfiguration() {
+ Configuration conf = mutableMorConfiguration();
+
+ assertTrue(OptionsResolver.isMorTable(conf));
+ assertTrue(OptionsResolver.needsScheduleCompaction(conf));
+ assertTrue(OptionsResolver.needsAsyncCompaction(conf));
+ assertTrue(conf.get(FlinkOptions.COMPACTION_SCHEDULE_ENABLED));
+ assertTrue(conf.get(FlinkOptions.COMPACTION_ASYNC_ENABLED));
+
assertTrue(conf.get(FlinkOptions.COMPACTION_OPERATION_EXECUTE_ASYNC_ENABLED));
+ assertEquals(FlinkOptions.NUM_COMMITS,
conf.get(FlinkOptions.COMPACTION_TRIGGER_STRATEGY));
+ assertEquals(5, conf.get(FlinkOptions.COMPACTION_DELTA_COMMITS));
+ }
+
+ @Test
+ void testPinnedMorClusteringDisabled() {
+ Configuration conf = mutableMorConfiguration();
+
+ assertFalse(conf.get(FlinkOptions.CLUSTERING_SCHEDULE_ENABLED));
+ assertFalse(conf.get(FlinkOptions.CLUSTERING_ASYNC_ENABLED));
+ }
+
+ @Test
+ void testPinnedMorQuerySemantics() {
+ Configuration conf = mutableMorConfiguration();
+
+ assertEquals("snapshot", FlinkOptions.QUERY_TYPE_SNAPSHOT);
+ assertEquals("read_optimized", FlinkOptions.QUERY_TYPE_READ_OPTIMIZED);
+ assertEquals(FlinkOptions.QUERY_TYPE_SNAPSHOT,
conf.get(FlinkOptions.QUERY_TYPE));
+ conf.set(FlinkOptions.QUERY_TYPE, FlinkOptions.QUERY_TYPE_READ_OPTIMIZED);
+ assertEquals(FlinkOptions.QUERY_TYPE_READ_OPTIMIZED,
conf.get(FlinkOptions.QUERY_TYPE));
+ }
+
+ @Test
+ void testPinnedAppendOnlyMorDoesNotScheduleCompaction() {
+ Configuration conf = mutableMorConfiguration();
+ conf.set(FlinkOptions.OPERATION, "insert");
+
+ assertTrue(OptionsResolver.isAppendMode(conf));
+ assertFalse(OptionsResolver.needsScheduleCompaction(conf));
+ }
+
@Test
void testPinnedFactorySkipsMissingRecordKeyCheckInAppendMode() {
TestContext context = newTestContext();
@@ -432,6 +501,19 @@ class TestFlinkArchitectSqlFixtures {
return conf;
}
+ private static Configuration mutableMorConfiguration() {
+ Configuration conf = mutableConfiguration();
+ conf.set(FlinkOptions.TABLE_TYPE, FlinkOptions.TABLE_TYPE_MERGE_ON_READ);
+ conf.set(FlinkOptions.COMPACTION_SCHEDULE_ENABLED, true);
+ conf.set(FlinkOptions.COMPACTION_ASYNC_ENABLED, true);
+ conf.set(FlinkOptions.COMPACTION_OPERATION_EXECUTE_ASYNC_ENABLED, true);
+ conf.set(FlinkOptions.COMPACTION_TRIGGER_STRATEGY,
FlinkOptions.NUM_COMMITS);
+ conf.set(FlinkOptions.COMPACTION_DELTA_COMMITS, 5);
+ conf.set(FlinkOptions.CLUSTERING_SCHEDULE_ENABLED, false);
+ conf.set(FlinkOptions.CLUSTERING_ASYNC_ENABLED, false);
+ return conf;
+ }
+
private static TestContext newTestContext() {
StreamExecutionEnvironment environment =
StreamExecutionEnvironment.getExecutionEnvironment();
environment.enableCheckpointing(60_000L);
diff --git a/hudi-agent-gateway/skills/hudi-architect/README.md
b/hudi-agent-gateway/skills/hudi-architect/README.md
index e4796e0818bc..11a603043b69 100644
--- a/hudi-agent-gateway/skills/hudi-architect/README.md
+++ b/hudi-agent-gateway/skills/hudi-architect/README.md
@@ -35,11 +35,12 @@ Target Hudi version: **1.2.0**.
- **Spark and HoodieStreamer** retain the existing full design flow,
configuration bundle, and
runnable output.
-- **Flink** combines the routing and safety foundation with bounded
append-only and first mutable
- COW SQL sink paths for Hudi 1.2.0 and Flink 1.20 (1.20.1 fixtures). Mutable
eligibility requires
+- **Flink** combines the routing and safety foundation with bounded
append-only COW, mutable COW,
+ and mutable MOR SQL sink paths for Hudi 1.2.0 and Flink 1.20 (1.20.1
fixtures). Mutable eligibility requires
stable identity, event-time ordering, normalized UPSERT changelog, full-row
deletes when present, immutable
- partitions, and the fixed global FLINK_STATE/bootstrap/no-TTL contract.
Other Flink designs
- remain fail-closed.
+ partitions, and the fixed global FLINK_STATE/bootstrap/no-TTL contract. MOR
additionally fixes
+ same-writer asynchronous compaction, records snapshot/read-optimized
freshness, and disables
+ clustering. Other Flink designs remain fail-closed.
## How to invoke
@@ -123,6 +124,8 @@ planned in CI against the released Hudi 1.2.0 Flink bundle
and Flink 1.20.1.
The mutable schema-2 fixture is
`hudi-agent-gateway/tests/fixtures/hudi_architect/flink_pr3/mutable_cow.json`.
+The mutable MOR schema-3 fixture is
+`hudi-agent-gateway/tests/fixtures/hudi_architect/flink_pr4/mutable_mor.json`.
Credential-bearing evidence can be sanitized without executing or parsing it
as configuration:
@@ -144,8 +147,9 @@ This is **Milestone 1 of a longer arc** — meant to be
shareable and playable,
## What's out of scope in Milestone 1
-- Flink MOR, existing-table takeover, external catalogs, multiple writers,
alternative indexes,
- custom mergers, retract normalization, partition movement, key-only deletes,
full CDC history,
+- Flink append-only MOR, standalone compaction, detailed compaction tuning,
clustering,
+ existing-table takeover, external catalogs, multiple writers, alternative
indexes, custom
+ mergers, retract normalization, partition movement, key-only deletes, full
CDC history,
DataStream API, `HoodieFlinkStreamer`, source connector generation, and
submit/deploy commands.
Those paths do not reuse the bounded SQL output as if it were generally
applicable.
diff --git a/hudi-agent-gateway/skills/hudi-architect/RUNBOOK.md
b/hudi-agent-gateway/skills/hudi-architect/RUNBOOK.md
index 5ab8c8d4d05b..688c4e810794 100644
--- a/hudi-agent-gateway/skills/hudi-architect/RUNBOOK.md
+++ b/hudi-agent-gateway/skills/hudi-architect/RUNBOOK.md
@@ -25,7 +25,8 @@ A practical guide to running the Hudi Architect agent: what
it does, how to set
(ADR) with tradeoff tables and measurable revisit conditions, a ready-to-use
`hoodie.*` config
bundle, and a runnable submit command — all pinned to **Hudi 1.2.0**. The
Flink route is pinned to
**Hudi 1.2.0 / Flink 1.20** and remains fail-closed by default. It can emit
validated Flink SQL for
-bounded append-only and first mutable new-table, single-writer,
no-external-catalog COW paths.
+bounded append-only COW, mutable COW, and mutable MOR new-table, single-writer,
+no-external-catalog paths.
---
@@ -144,7 +145,8 @@ surface reaches `CONFIG_VALIDATED` only after the physical
target and source sch
source changelog, fixed write mode, and a checkpoint interval of at least 1000
ms pass deterministic
validation. Mutable input additionally requires a stable non-null key,
event-time ordering,
normalized UPSERT changelog, full-row deletes when present, immutable
partitions, and the fixed global
-FLINK_STATE/bootstrap/no-TTL contract. Its artifacts are runtime `SET`
+FLINK_STATE/bootstrap/no-TTL contract. Mutable MOR additionally requires
same-writer asynchronous
+compaction, explicit snapshot/read-optimized freshness expectations, and
disabled clustering. Its artifacts are runtime `SET`
statements, Hudi `CREATE TABLE`, and an explicit-column `INSERT INTO`;
deployment remains manual.
Then: land a first commit in a staging path, run your real read patterns
against it, and check the ADR's operational playbook section for what to
monitor from day one (commit duration, pending compactions, active timeline
size, small-file ratio).
@@ -153,8 +155,9 @@ Then: land a first commit in a staging path, run your real
read patterns against
Be aware of what the agent will *decline* to decide — it defers honestly
rather than guessing, but plan for these yourself:
-- **Flink executable coverage** — bounded append-only and first mutable
new-table, single-writer,
- no-external-catalog COW streaming SQL sinks are implemented. MOR,
existing-table takeover,
+- **Flink executable coverage** — bounded append-only COW, mutable COW, and
mutable MOR
+ new-table, single-writer, no-external-catalog streaming SQL sinks are
implemented. Append-only
+ MOR, standalone compaction, detailed compaction tuning, clustering,
existing-table takeover,
catalogs, multiple writers, alternative indexes, custom mergers, retract
normalization,
partition movement, key-only deletes, full CDC history, DataStream API,
`HoodieFlinkStreamer`,
source connector generation, and submit/deploy commands remain out of scope.
diff --git a/hudi-agent-gateway/skills/hudi-architect/SKILL.md
b/hudi-agent-gateway/skills/hudi-architect/SKILL.md
index bae7f9a110df..55654efc38e3 100644
--- a/hudi-agent-gateway/skills/hudi-architect/SKILL.md
+++ b/hudi-agent-gateway/skills/hudi-architect/SKILL.md
@@ -75,14 +75,16 @@ that manifest with options discovered from the enclosing
checkout. If the user e
another Hudi or Flink version, do not silently reuse the baseline: add a
`REVIEW_REQUIRED` finding
unless a separate verified capability reference exists.
-The Flink implementation includes the PR1 safety foundation, the PR2
append-only path, and the
-first bounded PR3 mutable COW path. Both require a new table, confirmed single
writer, no external
-catalog, streaming checkpointing, and `CONFIG_VALIDATED` from
`validate_flink_design.py`. The
-mutable path additionally requires stable non-null identity, one event-time
ordering field,
-normalized UPSERT changelog, full-row deletes when present, immutable
partitions, and the fixed global
-FLINK_STATE/bootstrap/no-TTL settings. MOR, existing-table takeover, catalog,
multi-writer,
-alternative indexes, custom mergers, retract normalization, and full CDC
history still fail
-closed.
+The Flink implementation includes the PR1 safety foundation and three bounded
executable paths:
+PR2 append-only COW, PR3 mutable COW, and PR4 mutable MOR. All require a new
table, confirmed
+single writer, no external catalog, streaming checkpointing, and
`CONFIG_VALIDATED` from
+`validate_flink_design.py`. Both mutable paths additionally require stable
non-null identity, one
+event-time ordering field, normalized UPSERT changelog, full-row deletes when
present, immutable
+partitions, and fixed global FLINK_STATE/bootstrap/no-TTL settings. The MOR
path further requires
+snapshot/read-optimized expectations, compaction owned by the same Flink
writer, fixed asynchronous
+five-delta-commit compaction, and clustering disabled. Append-only MOR,
standalone table services,
+detailed compaction tuning, existing-table takeover, catalog, multi-writer,
alternative indexes,
+custom mergers, retract normalization, and full CDC history still fail closed.
## Flow structure
@@ -91,7 +93,7 @@ The conversation has three parts:
1. **Tier gate** — a single scoping question to figure out which downstream
questions fire.
2. **Rounds 1-3** — workload questions, gated conditionally by tier.
3. **Output** — engine-specific output: the established Spark artifacts, a
non-executable Flink
- assessment, or bounded validator-rendered PR2/PR3 SQL artifacts.
+ assessment, or bounded validator-rendered PR2/PR3/PR4 SQL artifacts.
Load `references/question-flow.md` for the full round-by-round question list
with conditional gating.
diff --git
a/hudi-agent-gateway/skills/hudi-architect/references/flink-1.20-hudi-1.2.0-capabilities.md
b/hudi-agent-gateway/skills/hudi-architect/references/flink-1.20-hudi-1.2.0-capabilities.md
index 7aead7775916..e4d0ce50f81e 100644
---
a/hudi-agent-gateway/skills/hudi-architect/references/flink-1.20-hudi-1.2.0-capabilities.md
+++
b/hudi-agent-gateway/skills/hudi-architect/references/flink-1.20-hudi-1.2.0-capabilities.md
@@ -83,6 +83,7 @@ the current checkout contains it.
| Auto-generated-key durability warning | Supported |
| Replay-idempotence classification | Supported |
| First bounded mutable COW upsert/delete path | Supported after schema-2
validation |
+| First bounded mutable MOR upsert/delete path | Supported after schema-3
validation |
| Stable non-null mutable record key | Required; auto-key is `BLOCKED` |
| Mutable ordering | One non-null `BIGINT` or `TIMESTAMP(p<=6)` event-time
field |
| Mutable source changelog | Normalized UPSERT (`I`, `UA`, `D`, no `UB`) |
@@ -91,7 +92,12 @@ the current checkout contains it.
| Mutable index | Global `FLINK_STATE`, TTL 0, bootstrap enabled |
| Alternative indexes, expiring state, custom mergers, or retract
normalization | `BLOCKED`; deferred |
| Full changelog history / Hudi CDC query | Not claimed; output is
latest-state behavior |
-| MOR and compaction ownership | Deferred to PR4 |
+| COW/MOR derivation | Workload-derived for mutable paths; append-only remains
COW |
+| MOR compaction ownership | Same Flink writer only; standalone or unknown
ownership withholds SQL |
+| MOR compaction | Async schedule and execution, `num_commits`, five delta
commits |
+| MOR snapshot freshness | Base files plus log state at a visible completed
instant |
+| MOR read-optimized freshness | Compacted base-file state; current only after
compaction |
+| Detailed compaction tuning or clustering | `BLOCKED`; deferred to
operations/separate work |
| New-table, single-writer, append-only COW SQL | Supported after static
validation |
| Stable-key DDL | `PRIMARY KEY (...) NOT ENFORCED` |
| Explicitly accepted auto-generated keys | Supported with durable warning |
@@ -188,13 +194,41 @@ Bootstrap closes the cold-start gap for a table created
and operated under this
loss must not make existing keys look new. It does not expand the path into
arbitrary
existing-table takeover; that still needs the separate PR5 evidence and
compatibility flow.
+## Implemented PR4 mutable MOR contract
+
+Schema 3 reuses every schema-2 mutable safety invariant and changes only the
table type plus an
+explicit read/table-service contract. The skill derives MOR from sustained
mutable writes and
+reader needs; it does not ask the user to select a Hudi table type without
context. Low or
+occasional updates, simplest-operation priority, and strict read-optimized
freshness derive COW.
+Append-only MOR is rejected because Hudi 1.2.0 treats MOR insert as append
mode and does not enter
+the verified mutable compaction schedule path.
+
+The canonical MOR sink pins `MERGE_ON_READ` upsert, event-time ordering,
global `FLINK_STATE` with
+TTL 0 and bootstrap, and `changelog.enabled=false`. The same Flink writer owns
compaction;
+`compaction.schedule.enabled`, `compaction.async.enabled`, and
+`compaction.operation.execute.async.enabled` are true, with `num_commits` and
five delta commits.
+Both clustering controls remain false. Five is the Hudi 1.2.0 effective
default selected as a fixed
+bounded contract, not a workload-tuned SLA.
+
+Snapshot queries read base files plus log state at a visible completed
instant. Read-optimized
+queries read compacted base files and can lag delta commits until compaction.
The reader requirement
+is recorded in the design contract, but reader-side query options are not
emitted into sink DDL.
+A standalone compactor is treated as an independent writer and remains
review-required; unknown
+ownership also fails closed. Compaction parallelism, time triggers, target IO,
clustering,
+concurrency configuration, and live service execution are not claimed.
+
+The pinned Flink planner fixture consumes the exact schema-3 golden SQL and
verifies the mutable
+writer, index bootstrap, `compact_plan_generate`, `compact_task`, and
`compact_commit` operators.
+Focused release-bundle fixtures retain effective MOR, compaction, clustering,
and query-mode
+semantics without claiming that a live job ran.
+
## Status vocabulary
- `INCOMPLETE` — a required workload fact, schema, or lifecycle fact is
missing.
- `BLOCKED` — the requested path is known to be outside the currently
implemented Flink scope.
- `REVIEW_REQUIRED` — compatibility, writer topology, catalog behavior, or
another operational
risk requires human confirmation.
-- `CONFIG_VALIDATED` — every applicable gate and all load-bearing PR2 or PR3
design values passed the
+- `CONFIG_VALIDATED` — every applicable gate and all load-bearing PR2, PR3, or
PR4 design values passed the
pinned static validator. Canonical SQL was emitted without unresolved
placeholders.
No status claims that storage permissions, JAR deployment, source
availability, catalog
diff --git
a/hudi-agent-gateway/skills/hudi-architect/references/flink-1.20-hudi-1.2.0-capabilities.toml
b/hudi-agent-gateway/skills/hudi-architect/references/flink-1.20-hudi-1.2.0-capabilities.toml
index d193a36d97b6..b70a775e0c2b 100644
---
a/hudi-agent-gateway/skills/hudi-architect/references/flink-1.20-hudi-1.2.0-capabilities.toml
+++
b/hudi-agent-gateway/skills/hudi-architect/references/flink-1.20-hudi-1.2.0-capabilities.toml
@@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-schema_version = 3
+schema_version = 4
baseline_id = "hudi-1.2.0-flink-1.20"
validation_surface = "initial-flink-sql-sink"
@@ -62,6 +62,34 @@ delete_payload_when_deletes = "FULL_ROW"
delete_payload_without_deletes = "NOT_APPLICABLE"
partition_fields_mutable = false
+[mutable_mor_executable_contract]
+contract_schema = 3
+table_lifecycle = "new"
+writer_model = "single_writer"
+external_catalog = false
+mutability = "mutable"
+source_changelog_mode = "UPSERT"
+execution_mode = "STREAMING"
+require_checkpointing = true
+record_key_mode = "stable_key"
+allow_replay_behaviors = ["cannot_occur", "duplicates_acceptable",
"must_collapse"]
+ordering_mode = "event_time"
+ordering_field_types = ["BIGINT", "TIMESTAMP"]
+reserved_ordering_field_values = ["no_precombine"]
+allow_emits_deletes = [false, true]
+delete_payload_when_deletes = "FULL_ROW"
+delete_payload_without_deletes = "NOT_APPLICABLE"
+partition_fields_mutable = false
+table_type = "MERGE_ON_READ"
+operation = "upsert"
+compaction_owner = "flink_writer"
+compaction_mode = "async"
+compaction_trigger_strategy = "num_commits"
+compaction_delta_commits = 5
+query_modes = ["snapshot", "read_optimized"]
+read_optimized_freshness = "after_compaction"
+clustering_enabled = false
+
# The executable paths intentionally validate a bounded scalar physical-schema
surface. Complex,
# computed, metadata, and watermark columns remain unverified rather than
being guessed.
[physical_types]
@@ -142,7 +170,7 @@ purpose = "Writer-side prepending of Hudi metadata fields
to the Flink row type"
id = "options_resolver"
path =
"hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/configuration/OptionsResolver.java"
sha256 = "1a63ef88bbdf84cbcebb92f402557c21b6f84bdd0112726792ce22da635ad766"
-purpose = "Effective append-mode predicate and no-precombine ordering sentinel
resolution"
+purpose = "Effective append mode, MOR table type, compaction gates, and
no-precombine resolution"
[[source_anchors]]
id = "table_factory"
@@ -172,7 +200,19 @@ purpose = "Release behavior exercised by the existing
factory fixtures"
id = "table_sink"
path =
"hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/HoodieTableSink.java"
sha256 = "59c36ec6c97524386c4ce0fb7c70960816a6cfc9dee89e82de86f401f28e1087"
-purpose = "Sink changelog-mode negotiation for upsert writes"
+purpose = "Sink changelog-mode negotiation and mutable MOR compaction-pipeline
selection"
+
+[[source_anchors]]
+id = "table_source"
+path =
"hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/HoodieTableSource.java"
+sha256 = "94819175df0f9b3f76a641f45f70f6c1eee0e2da3976c3f4ff4883a831618504"
+purpose = "Snapshot and read-optimized Flink SQL source construction"
+
+[[source_anchors]]
+id = "hoodie_source"
+path =
"hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/source/HoodieSource.java"
+sha256 = "1100c640aa61e8bf4f3bcd584b00d720388cbb6a801d864028c5bf98591ec7dc"
+purpose = "Snapshot and read-optimized MOR file-slice selection"
[[source_anchors]]
id = "changelog_modes"
@@ -190,7 +230,7 @@ purpose = "FLINK_STATE backend and zero-TTL state behavior"
id = "pipelines"
path =
"hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/utils/Pipelines.java"
sha256 = "9278096f0894a0ad34be09206ee06d317fda4185101833018a314e87ade5e262"
-purpose = "Index bootstrap insertion into the Flink write pipeline"
+purpose = "Index bootstrap and compaction operators in the Flink write
pipeline"
[[source_anchors]]
id = "bootstrap_operator"
@@ -218,6 +258,48 @@ value_type = "boolean"
source_anchors = ["flink_options", "table_sink", "changelog_modes"]
source_symbol = "CHANGELOG_ENABLED"
+[[verified_options]]
+key = "clustering.async.enabled"
+value_type = "boolean"
+source_anchors = ["flink_options"]
+source_symbol = "CLUSTERING_ASYNC_ENABLED"
+
+[[verified_options]]
+key = "clustering.schedule.enabled"
+value_type = "boolean"
+source_anchors = ["flink_options"]
+source_symbol = "CLUSTERING_SCHEDULE_ENABLED"
+
+[[verified_options]]
+key = "compaction.async.enabled"
+value_type = "boolean"
+source_anchors = ["flink_options", "options_resolver", "table_sink",
"pipelines"]
+source_symbol = "COMPACTION_ASYNC_ENABLED"
+
+[[verified_options]]
+key = "compaction.delta_commits"
+value_type = "integer"
+source_anchors = ["flink_options"]
+source_symbol = "COMPACTION_DELTA_COMMITS"
+
+[[verified_options]]
+key = "compaction.operation.execute.async.enabled"
+value_type = "boolean"
+source_anchors = ["flink_options", "table_sink", "pipelines"]
+source_symbol = "COMPACTION_OPERATION_EXECUTE_ASYNC_ENABLED"
+
+[[verified_options]]
+key = "compaction.schedule.enabled"
+value_type = "boolean"
+source_anchors = ["flink_options", "options_resolver"]
+source_symbol = "COMPACTION_SCHEDULE_ENABLED"
+
+[[verified_options]]
+key = "compaction.trigger.strategy"
+value_type = "string"
+source_anchors = ["flink_options"]
+source_symbol = "COMPACTION_TRIGGER_STRATEGY"
+
[[verified_options]]
key = "hoodie.datasource.write.partitionpath.field"
value_type = "string"
@@ -345,6 +427,81 @@ key = "write.operation"
value = "upsert"
applies_to = "pr3-mutable-cow"
+[[required_effective_settings]]
+key = "changelog.enabled"
+value = false
+applies_to = "pr4-mutable-mor"
+
+[[required_effective_settings]]
+key = "clustering.async.enabled"
+value = false
+applies_to = "pr4-mutable-mor"
+
+[[required_effective_settings]]
+key = "clustering.schedule.enabled"
+value = false
+applies_to = "pr4-mutable-mor"
+
+[[required_effective_settings]]
+key = "compaction.async.enabled"
+value = true
+applies_to = "pr4-mutable-mor"
+
+[[required_effective_settings]]
+key = "compaction.delta_commits"
+value = 5
+applies_to = "pr4-mutable-mor"
+
+[[required_effective_settings]]
+key = "compaction.operation.execute.async.enabled"
+value = true
+applies_to = "pr4-mutable-mor"
+
+[[required_effective_settings]]
+key = "compaction.schedule.enabled"
+value = true
+applies_to = "pr4-mutable-mor"
+
+[[required_effective_settings]]
+key = "compaction.trigger.strategy"
+value = "num_commits"
+applies_to = "pr4-mutable-mor"
+
+[[required_effective_settings]]
+key = "hoodie.write.record.merge.mode"
+value = "EVENT_TIME_ORDERING"
+applies_to = "pr4-mutable-mor"
+
+[[required_effective_settings]]
+key = "index.bootstrap.enabled"
+value = true
+applies_to = "pr4-mutable-mor"
+
+[[required_effective_settings]]
+key = "index.global.enabled"
+value = true
+applies_to = "pr4-mutable-mor"
+
+[[required_effective_settings]]
+key = "index.state.ttl"
+value = 0.0
+applies_to = "pr4-mutable-mor"
+
+[[required_effective_settings]]
+key = "index.type"
+value = "FLINK_STATE"
+applies_to = "pr4-mutable-mor"
+
+[[required_effective_settings]]
+key = "table.type"
+value = "MERGE_ON_READ"
+applies_to = "pr4-mutable-mor"
+
+[[required_effective_settings]]
+key = "write.operation"
+value = "upsert"
+applies_to = "pr4-mutable-mor"
+
[[implemented_acceptance_checks]]
id = "FLINK_BINARY_ROUTING_FIELD_UNSUPPORTED"
requirement = "Reject binary record-key and partition fields whose Hudi 1.2.0
routing strings depend on Java object identity."
@@ -416,3 +573,33 @@ id = "FLINK_MUTABLE_INDEX_BOOTSTRAP_REQUIRED"
requirement = "Require index bootstrap so a cold restart does not treat
existing keys as new."
python_test = "test_pr3_requires_index_bootstrap"
java_test = "testPinnedMutableIndexBootstrapConfiguration"
+
+[[implemented_acceptance_checks]]
+id = "FLINK_MOR_CLUSTERING_UNSUPPORTED"
+requirement = "Keep clustering disabled on the first mutable MOR path."
+python_test = "test_pr4_rejects_clustering"
+java_test = "testPinnedMorClusteringDisabled"
+
+[[implemented_acceptance_checks]]
+id = "FLINK_MOR_APPEND_ONLY_UNSUPPORTED"
+requirement = "Keep append-only writes on COW because MOR insert enters append
mode and does not schedule compaction."
+python_test = "test_pr4_rejects_append_only_mor"
+java_test = "testPinnedAppendOnlyMorDoesNotScheduleCompaction"
+
+[[implemented_acceptance_checks]]
+id = "FLINK_MOR_COMPACTION_CONFIGURATION_UNSUPPORTED"
+requirement = "Pin scheduling and async compaction to the same Flink writer
with a five-delta-commit trigger."
+python_test = "test_pr4_rejects_incompatible_compaction_configuration"
+java_test = "testPinnedMorCompactionConfiguration"
+
+[[implemented_acceptance_checks]]
+id = "FLINK_MOR_READ_OPTIMIZED_FRESHNESS_UNSUPPORTED"
+requirement = "Record that read-optimized queries expose compacted base-file
state rather than every delta commit."
+python_test = "test_pr4_rejects_strict_read_optimized_freshness"
+java_test = "testPinnedMorQuerySemantics"
+
+[[implemented_acceptance_checks]]
+id = "FLINK_PR4_WRITE_PATH_UNSUPPORTED"
+requirement = "Plan the exact mutable MOR SQL through the released connector
and retain its writer plus compaction operators."
+python_test = "test_pr4_valid_mutable_mor_design_renders_exact_executable_sql"
+java_test = "testMutableMorSinkAndCompactionPlan"
diff --git
a/hudi-agent-gateway/skills/hudi-architect/references/flink-config-templates.md
b/hudi-agent-gateway/skills/hudi-architect/references/flink-config-templates.md
index 2759417f0934..f60212d613fb 100644
---
a/hudi-agent-gateway/skills/hudi-architect/references/flink-config-templates.md
+++
b/hudi-agent-gateway/skills/hudi-architect/references/flink-config-templates.md
@@ -30,8 +30,8 @@ Verified baseline: Apache Hudi 1.2.0 / Apache Flink 1.20
Baseline ID: hudi-1.2.0-flink-1.20
Hudi source revision: f05c83f2b97732de7a558ff9b26959e1139c05f5
Fixture version: Apache Flink 1.20.1
-Capability manifest schema: 3
-Design contract schema: <1 append-only | 2 mutable COW>
+Capability manifest schema: 4
+Design contract schema: <1 append-only COW | 2 mutable COW | 3 mutable MOR>
Confirmed facts:
- <facts established by explicit answers>
@@ -65,8 +65,8 @@ Verified baseline: Apache Hudi 1.2.0 / Apache Flink 1.20
Baseline ID: hudi-1.2.0-flink-1.20
Hudi source revision: f05c83f2b97732de7a558ff9b26959e1139c05f5
Fixture version: Apache Flink 1.20.1
-Capability manifest schema: 3
-Design contract schema: <1 append-only | 2 mutable COW>
+Capability manifest schema: 4
+Design contract schema: <1 append-only COW | 2 mutable COW | 3 mutable MOR>
Confirmed facts:
- New table; confirmed single writer; no current external-catalog requirement.
@@ -77,16 +77,18 @@ Confirmed facts:
Source contract:
- Existing table: <source identifier>
- Expected physical schema: <Avro-compatible field names, bounded types,
nullability>
-- Changelog: <INSERT_ONLY for schema 1 | normalized UPSERT (I/UA/D, no UB) for
schema 2>
+- Changelog: <INSERT_ONLY for schema 1 | normalized UPSERT (I/UA/D, no UB) for
schemas 2/3>
- The source connector and live source availability were not validated.
Target contract:
- Table: <target identifier>
- Path: <credential-free concrete path>
-- Table type: COPY_ON_WRITE
-- Operation: <insert for schema 1 | upsert for schema 2>
+- Table type: <COPY_ON_WRITE for schemas 1/2 | MERGE_ON_READ for schema 3>
+- Operation: <insert for schema 1 | upsert for schemas 2/3>
- Path-specific fixed settings: <insert clustering false | EVENT_TIME_ORDERING
plus global
FLINK_STATE, TTL 0, bootstrap true, and changelog false>
+- MOR only: compaction owned by this Flink writer, snapshot/read-optimized
freshness recorded,
+ fixed async five-delta-commit compaction, and clustering disabled.
Runtime contract:
- Execution: streaming
@@ -159,6 +161,37 @@ The mutable source must declare the same primary key as
the target and normalize
changelog (`I`, `UA`, `D`, no `UB`). Deletes, when emitted, must be full-row;
partition values are immutable.
This is a latest-state sink contract; it is not full changelog-history or
CDC-query support.
+For schema 3 mutable MOR, preserve every schema-2 identity, ordering, source,
index, and runtime
+invariant, replace only `table.type`, and append the bounded service settings:
+
+```sql
+WITH (
+ 'connector' = 'hudi',
+ 'path' = '<concrete credential-free target path>',
+ 'table.type' = 'MERGE_ON_READ',
+ 'write.operation' = 'upsert',
+ 'ordering.fields' = '<one non-null BIGINT or TIMESTAMP(p<=6) field>',
+ 'hoodie.write.record.merge.mode' = 'EVENT_TIME_ORDERING',
+ 'index.type' = 'FLINK_STATE',
+ 'index.global.enabled' = 'true',
+ 'index.state.ttl' = '0',
+ 'index.bootstrap.enabled' = 'true',
+ 'changelog.enabled' = 'false',
+ 'compaction.schedule.enabled' = 'true',
+ 'compaction.async.enabled' = 'true',
+ 'compaction.operation.execute.async.enabled' = 'true',
+ 'compaction.trigger.strategy' = 'num_commits',
+ 'compaction.delta_commits' = '5',
+ 'clustering.schedule.enabled' = 'false',
+ 'clustering.async.enabled' = 'false'
+)
+```
+
+Compaction is scheduled and executed by the same Flink writer job. Snapshot
readers include log
+state at a visible completed instant; read-optimized readers expose compacted
base files and are
+current only after compaction. Five delta commits is a trigger, not a time
SLA. Reader-side
+`hoodie.datasource.query.type` is intentionally absent from sink DDL.
+
## Deployment checks
Always retain these outside the static success claim:
@@ -176,8 +209,8 @@ Always retain these outside the static success claim:
- A complete Kafka, CDC, JDBC, or other source connector job.
- A Flink deployment or submit command.
- A Spark fallback.
-- OCC, NBCC, lock-provider, alternative index, catalog, MOR, compaction, or
clustering
- configuration.
+- OCC, NBCC, lock-provider, alternative index, catalog, append-only MOR,
standalone compaction,
+ detailed compaction tuning, or enabled clustering.
- Existing-table takeover, multi-writer operation, expiring state index,
key-only deletes,
retract-stream normalization, partition movement, custom mergers, or full
CDC history.
- A claim that static validation checked live storage, the deployed JAR, the
live source, active
diff --git
a/hudi-agent-gateway/skills/hudi-architect/references/flink-decision-overrides.md
b/hudi-agent-gateway/skills/hudi-architect/references/flink-decision-overrides.md
index 92dd6baae6cb..2faf994a2572 100644
---
a/hudi-agent-gateway/skills/hudi-architect/references/flink-decision-overrides.md
+++
b/hudi-agent-gateway/skills/hudi-architect/references/flink-decision-overrides.md
@@ -46,8 +46,8 @@ do not replace the shared table-design rules and do not form
a standalone planne
- Do not equate a stable record key with replay idempotence.
- Do not reuse this baseline for another Hudi or Flink version.
- Emit executable output only when `validate_flink_design.py` returns
`CONFIG_VALIDATED` for a
- complete version-1 append-only or version-2 mutable contract. Never
hand-write around a
- validator finding.
+ complete version-1 append-only COW, version-2 mutable COW, or version-3
mutable MOR contract.
+ Never hand-write around a validator finding.
## Gate outcomes
@@ -93,13 +93,21 @@ do not replace the shared table-design rules and do not
form a standalone planne
| Mutable index is not global `FLINK_STATE` with TTL 0 |
`FLINK_MUTABLE_INDEX_CONFIGURATION_UNSUPPORTED` | `BLOCKED` | No |
| FLINK_STATE index bootstrap is not enabled |
`FLINK_MUTABLE_INDEX_BOOTSTRAP_REQUIRED` | `BLOCKED` | No |
| Table type, operation, merge mode, changelog setting, or execution mode is
outside PR3 | `FLINK_PR3_WRITE_PATH_UNSUPPORTED` | `BLOCKED` | No |
+| Append-only input requests MOR | `FLINK_MOR_APPEND_ONLY_UNSUPPORTED` |
`BLOCKED` | No |
+| MOR compaction owner is missing | `FLINK_MOR_COMPACTION_OWNER_REQUIRED` |
`INCOMPLETE` | No |
+| MOR compaction schedule, execution, mode, trigger, or threshold differs from
the bounded contract | `FLINK_MOR_COMPACTION_CONFIGURATION_UNSUPPORTED` |
`BLOCKED` | No |
+| MOR request supplies detailed compaction tuning |
`FLINK_MOR_COMPACTION_TUNING_UNSUPPORTED` | `BLOCKED` | No |
+| MOR reader mode or freshness requirement is unresolved |
`FLINK_MOR_QUERY_REQUIREMENT_UNRESOLVED` | `INCOMPLETE` | No |
+| MOR read-optimized results must include every completed write |
`FLINK_MOR_READ_OPTIMIZED_FRESHNESS_UNSUPPORTED` | `BLOCKED` | No |
+| MOR enables clustering | `FLINK_MOR_CLUSTERING_UNSUPPORTED` | `BLOCKED` | No
|
+| Table type, operation, merge mode, changelog setting, compaction,
clustering, or execution mode is outside PR4 |
`FLINK_PR4_WRITE_PATH_UNSUPPORTED` | `BLOCKED` | No |
| Checkpointing is explicitly disabled | `FLINK_CHECKPOINTING_REQUIRED` |
`BLOCKED` | No |
| Checkpoint interval is absent or non-positive |
`FLINK_CHECKPOINT_INTERVAL_REQUIRED` | `INCOMPLETE` | No |
| Positive checkpoint interval is below the 1000 ms Architect safety floor |
`FLINK_CHECKPOINT_INTERVAL_UNSUPPORTED` | `BLOCKED` | No |
| Target table, target path, source table, or another load-bearing value is a
placeholder | `FLINK_LOAD_BEARING_VALUE_REQUIRED` | `INCOMPLETE` | No |
| Design-contract structure is malformed or contains an ignored field |
`FLINK_DESIGN_CONTRACT_INVALID` | `INCOMPLETE` | No |
| A connector option is outside the pinned allowlist |
`FLINK_OPTION_NOT_VERIFIED` | `REVIEW_REQUIRED` | No |
-| Complete PR2 or PR3 contract passes deterministic validation | — |
`CONFIG_VALIDATED` | Yes |
+| Complete PR2, PR3, or PR4 contract passes deterministic validation | — |
`CONFIG_VALIDATED` | Yes |
## Final-status precedence
@@ -124,9 +132,9 @@ only bounded static validation against the pinned release
contract.
| Mutability | Stable business key | Replay requirement | Result |
|---|---|---|---|
-| Mutable | Yes | Replays impossible | Continue into the bounded PR3 mutable
contract |
+| Mutable | Yes | Replays impossible | Continue to COW/MOR derivation, then
schema 2 or 3 |
| Mutable | Yes | Duplicates acceptable | Continue; record duplicate tolerance
|
-| Mutable | Yes | Copies must collapse | Continue into the bounded PR3 mutable
contract |
+| Mutable | Yes | Copies must collapse | Continue to COW/MOR derivation, then
schema 2 or 3 |
| Mutable | Yes | Replay behavior unknown | `REVIEW_REQUIRED` |
| Mutable | No | Any | `BLOCKED` — auto-key cannot express mutable identity |
| Mutable | Unknown | Any | `INCOMPLETE` |
@@ -171,7 +179,7 @@ and executable eligibility, not exact natural-language
wording.
| `F05_CATALOG` | External consumer requires catalog visibility |
`REVIEW_REQUIRED` | No |
| `F06_SCHEMA` | Physical schema is missing | `INCOMPLETE` | No |
| `F06_SCHEMA_POINTER` | Only a schema location is supplied; field names and
types are unavailable | `INCOMPLETE` | No |
-| `F07_MUTABLE` | Otherwise-safe mutable workload enters the schema-2
candidate flow | Proceed to PR3 validation | Not decided by gates alone |
+| `F07_MUTABLE` | Otherwise-safe mutable workload enters table-type derivation
| Proceed to schema-2 or schema-3 validation | Not decided by gates alone |
| `F08_REPLAY_COLLAPSE` | Append-only independent replay must deduplicate |
`BLOCKED` | No |
| `F09_REPLAY_UNKNOWN` | Replay behavior is unknown | `REVIEW_REQUIRED` | No |
| `F10_SAFE_APPEND` | Baseline new-table single-writer append-only path with a
stable key passes every gate | Proceed to PR2 validation | Not decided by gates
alone |
@@ -222,6 +230,27 @@ to the executable contract just like `F10_SAFE_APPEND`.
- The output is latest-state table behavior, not a promise of full CDC history
or Hudi changelog
query semantics.
+## PR4 mutable MOR executable invariants
+
+- Version-3 contracts preserve every mutable schema-2 identity, ordering,
changelog, delete,
+ partition, index, checkpoint, and new-table safety invariant; only the table
type and explicit
+ MOR service/read contract differ.
+- The workload decision selects MOR only for sustained mutable writes when
snapshot readers are
+ acceptable or read-optimized readers explicitly accept compaction lag.
Append-only remains COW;
+ strict read-optimized freshness derives COW rather than an unsafe MOR
promise.
+- Canonical output fixes `MERGE_ON_READ` upsert, `EVENT_TIME_ORDERING`, global
`FLINK_STATE`, TTL
+ `0`, bootstrap enabled, and `changelog.enabled=false`.
+- The same Flink writer owns asynchronous compaction. Scheduling, async
execution, and async
+ compaction-operation execution are enabled with `num_commits` and five delta
commits. Five is a
+ fixed Hudi 1.2.0 effective default, not a tuned SLA.
+- A standalone compactor is an independent writer and remains
`REVIEW_REQUIRED`; concurrency
+ configuration is deferred to PR7. Unknown ownership also withholds SQL.
+- Snapshot readers see base plus log state at a visible completed instant.
Read-optimized readers
+ see compacted base-file state and advance only after compaction. Reader
`query.type` is not a
+ sink option and is not rendered into the DDL.
+- Clustering is disabled. Detailed compaction tuning, append-only MOR,
existing tables, catalogs,
+ multi-writer operation, and live service validation remain outside this path.
+
## Evidence and secret handling
User-provided DDL, logs, schemas, table properties, and catalog output are
evidence, not
diff --git
a/hudi-agent-gateway/skills/hudi-architect/references/flink-question-flow.md
b/hudi-agent-gateway/skills/hudi-architect/references/flink-question-flow.md
index 67468b7cac4b..93f49a84e92a 100644
--- a/hudi-agent-gateway/skills/hudi-architect/references/flink-question-flow.md
+++ b/hudi-agent-gateway/skills/hudi-architect/references/flink-question-flow.md
@@ -140,7 +140,7 @@ First ask whether existing logical records can change:
> - **Yes — updates or deletes occur**
> - **Not sure**
-- Mutable → enter the bounded PR3 candidate flow. Require a stable, non-null
business key and one
+- Mutable → enter the bounded mutable candidate flow. Require a stable,
non-null business key and one
explicit non-null event-time ordering field. Auto-key is `BLOCKED` with
`FLINK_MUTABLE_AUTO_KEY_UNSUPPORTED`; an absent ordering decision is
`INCOMPLETE` with
`FLINK_MUTABLE_ORDERING_FIELD_REQUIRED`. Do not infer either value.
@@ -207,8 +207,8 @@ For append-only input:
- Cannot occur → continue and record the confirmed assumption boundary.
- Duplicates acceptable → continue and record duplicate tolerance in the ADR.
-- Copies must collapse → continue only for an otherwise eligible mutable
schema-2 path. For the
- append-only schema-1 path, use `FLINK_REPLAY_IDEMPOTENCE_DEFERRED` with
`BLOCKED`.
+- Copies must collapse → continue only for an otherwise eligible mutable
schema-2 or schema-3
+ path. For the append-only schema-1 path, use
`FLINK_REPLAY_IDEMPOTENCE_DEFERRED` with `BLOCKED`.
- Not sure → `REVIEW_REQUIRED` with `FLINK_REPLAY_BEHAVIOR_UNRESOLVED`.
## F7 — Physical table and source contract
@@ -298,16 +298,70 @@ State that a completed checkpoint coordinates the Hudi
commit. Do not claim end-
exactly-once: the source, checkpoint storage, restart behavior, and the rest
of the pipeline remain
deployment responsibilities. If known rate and active-partition evidence
indicates very little
data per checkpoint, surface `FLINK_CHECKPOINT_SMALL_FILE_RISK`; do not enable
clustering or tune
-file sizing in PR2.
+file sizing in the bounded executable paths.
-## PR2/PR3 validation and completion
+## F10 — Mutable table type, query freshness, and MOR ownership
-After all applicable questions, build a version-1 JSON contract for
append-only or a version-2
-JSON contract for mutable COW. Both contain confirmed safety facts, physical
schemas, identity,
-source, write, and runtime contracts. Version 2 additionally records
event-time ordering,
-normalized UPSERT, full-row delete posture, immutable partition fields, and
the fixed global
-FLINK_STATE/bootstrap/no-TTL contract. Do not insert placeholders. Record an
explicitly empty
-pass-through connector-option map; neither renderer accepts an ignored option.
+Ask this section only for an otherwise eligible mutable workload. Append-only
stays on schema 1
+COW and must not receive compaction questions.
+
+First establish write/read pressure in workload terms:
+
+> "Are updates sustained enough that rewriting whole data files is a concern,
and how will
+> consumers read the latest state?"
+>
+> - **Updates are low or occasional; prefer the simplest operations**
+> - **Sustained upserts; consumers use snapshot reads**
+> - **Sustained upserts; consumers use read-optimized scans and accept
compaction lag**
+> - **Read-optimized scans must include every completed write immediately**
+> - **Not sure**
+
+- Low/occasional updates or simplest operations → derive mutable COW and
schema 2.
+- Sustained upserts with snapshot reads → recommend MOR and continue the
schema-3 questions.
+- Sustained upserts with read-optimized consumers that accept compaction lag →
recommend MOR and
+ continue. Record `read_optimized_freshness=after_compaction`.
+- Read-optimized results required after every commit → recommend COW. If MOR
remains mandatory,
+ add `FLINK_MOR_READ_OPTIMIZED_FRESHNESS_UNSUPPORTED` with `BLOCKED`.
+- Unknown query/freshness requirement →
`FLINK_MOR_QUERY_REQUIREMENT_UNRESOLVED` with `INCOMPLETE`;
+ do not emit MOR SQL.
+
+For a MOR candidate, record whether readers require snapshot, read-optimized,
or both. Snapshot
+reads expose base files plus log state at a visible completed instant.
Read-optimized reads expose
+compacted base-file state and therefore advance only after compaction. Do not
render reader-side
+`hoodie.datasource.query.type` into sink DDL, and do not turn the fixed
five-delta-commit trigger
+into a wall-clock freshness SLA.
+
+Then establish compaction ownership:
+
+> "Who will schedule and execute compaction for this table?"
+>
+> - **The same Flink writer job**
+> - **A separately deployed compaction service**
+> - **Not sure**
+
+- Same Flink writer → schema 3 may continue with asynchronous schedule and
execution enabled,
+ `num_commits`, five delta commits, and clustering disabled.
+- Standalone service → `REVIEW_REQUIRED` with `FLINK_MULTI_WRITER_REVIEW`;
emit no SQL. PR7 owns
+ the concurrency contract.
+- Not sure → `REVIEW_REQUIRED` with `FLINK_WRITER_MODEL_UNRESOLVED`; do not
guess ownership.
+- Missing owner in a schema-3 contract → `INCOMPLETE` with
+ `FLINK_MOR_COMPACTION_OWNER_REQUIRED`.
+
+Do not ask for compaction tasks, seconds, target IO, strategy alternatives, or
other tuning. If
+the request supplies such tuning, add
`FLINK_MOR_COMPACTION_TUNING_UNSUPPORTED`; operational
+tuning is outside this bounded design path. Clustering must remain disabled or
+`FLINK_MOR_CLUSTERING_UNSUPPORTED` blocks output.
+
+## PR2/PR3/PR4 validation and completion
+
+After all applicable questions, build a version-1 JSON contract for
append-only COW, a version-2
+contract for mutable COW, or a version-3 contract for mutable MOR. All contain
confirmed safety
+facts, physical schemas, identity, source, write, and runtime contracts.
Versions 2 and 3 record
+event-time ordering, normalized UPSERT, full-row delete posture, immutable
partition fields, and the fixed global
+FLINK_STATE/bootstrap/no-TTL contract. Version 3 additionally records query
modes and
+read-optimized freshness, compaction ownership and fixed schedule/execution
settings, and disabled
+clustering. Do not insert placeholders. Record an explicitly empty
+pass-through connector-option map; no renderer accepts an ignored option.
Run:
diff --git
a/hudi-agent-gateway/skills/hudi-architect/references/flink-warnings.md
b/hudi-agent-gateway/skills/hudi-architect/references/flink-warnings.md
index ef66e6f7bc89..5e277fd6f6ef 100644
--- a/hudi-agent-gateway/skills/hudi-architect/references/flink-warnings.md
+++ b/hudi-agent-gateway/skills/hudi-architect/references/flink-warnings.md
@@ -123,10 +123,10 @@ ordering, and partition references can eventually be
checked.
**Trigger:** Existing logical records can be updated or deleted, but the
request is still using the
version-1 append contract.
-**Message:** Mutable COW requires the explicit version-2 identity, ordering,
changelog, delete,
-partition, and index safety contract.
+**Message:** Mutable input requires the explicit version-2 COW or version-3
MOR identity, ordering,
+changelog, delete, partition, and index safety contract after table-type
derivation.
-**Action:** `BLOCKED`; qualify the request for schema 2 or generate no
executable configuration.
+**Action:** `BLOCKED`; qualify the request for schema 2 or 3 or generate no
executable configuration.
## FLINK_MUTABILITY_REQUIRED
@@ -195,7 +195,7 @@ one table record.
**Message:** This requires stable record identity and an upsert-capable
design. The append-only
insert path cannot be presented as deduplicating.
-**Action:** `BLOCKED`; use the mutable schema-2 contract only if all of its
gates are satisfied.
+**Action:** `BLOCKED`; use a mutable schema-2 or schema-3 contract only if all
gates are satisfied.
## FLINK_REPLAY_BEHAVIOR_UNRESOLVED
@@ -317,7 +317,7 @@ the target field.
**Trigger:** The declared source changelog contains updates or deletes.
**Message:** Sink-factory construction does not prove the complete
source-to-sink statement is
-append-only. An eligible mutable request must use the separate schema-2
contract.
+append-only. An eligible mutable request must use a schema-2 COW or schema-3
MOR contract.
**Action:** `BLOCKED`; do not generate executable append SQL.
@@ -415,6 +415,79 @@ mutable configuration surface.
**Action:** `BLOCKED`; retain the request as a later capability rather than
guessing settings.
+## FLINK_MOR_APPEND_ONLY_UNSUPPORTED
+
+**Trigger:** An append-only contract requests a MOR table.
+
+**Message:** The bounded append route remains COW. Hudi 1.2.0 treats MOR
insert as append mode and
+does not schedule the mutable MOR compaction path that PR4 verifies.
+
+**Action:** `BLOCKED`; use schema-1 COW or reassess whether the workload is
actually mutable.
+
+## FLINK_MOR_COMPACTION_OWNER_REQUIRED
+
+**Trigger:** A schema-3 MOR contract omits compaction ownership.
+
+**Message:** The validator cannot assume that compaction runs inside the only
confirmed writer.
+
+**Action:** `INCOMPLETE`; identify the owner before emitting SQL.
+
+## FLINK_MOR_COMPACTION_CONFIGURATION_UNSUPPORTED
+
+**Trigger:** MOR schedule, async execution, operation execution, mode, trigger
strategy, or delta
+commit threshold differs from the bounded schema-3 contract.
+
+**Message:** PR4 verifies same-writer asynchronous compaction using
`num_commits` and five delta
+commits; it is not a general compaction planner.
+
+**Action:** `BLOCKED`; retain the fixed contract or defer to operational
design.
+
+## FLINK_MOR_COMPACTION_TUNING_UNSUPPORTED
+
+**Trigger:** A request supplies compaction task count, seconds, target IO,
timeout, or other
+detailed tuning.
+
+**Message:** These values depend on observed workload behavior and are outside
design-time static
+validation.
+
+**Action:** `BLOCKED`; defer tuning to operations rather than passing it
through silently.
+
+## FLINK_MOR_QUERY_REQUIREMENT_UNRESOLVED
+
+**Trigger:** Snapshot/read-optimized usage or the acceptable read-optimized
freshness is missing
+or unresolved.
+
+**Message:** MOR suitability depends on whether consumers read logs or can
accept compaction lag.
+
+**Action:** `INCOMPLETE`; obtain the reader contract before selecting schema 3.
+
+## FLINK_MOR_READ_OPTIMIZED_FRESHNESS_UNSUPPORTED
+
+**Trigger:** Read-optimized consumers require every completed delta commit
immediately.
+
+**Message:** Read-optimized queries expose compacted base files; the
five-delta-commit trigger is
+not an immediate or time-based freshness guarantee.
+
+**Action:** `BLOCKED`; derive COW or explicitly accept freshness only after
compaction.
+
+## FLINK_MOR_CLUSTERING_UNSUPPORTED
+
+**Trigger:** Clustering schedule or async execution is enabled for schema 3.
+
+**Message:** PR4 owns only the minimum MOR compaction service and does not
compose clustering.
+
+**Action:** `BLOCKED`; keep both clustering controls disabled.
+
+## FLINK_PR4_WRITE_PATH_UNSUPPORTED
+
+**Trigger:** Mutable MOR table type, operation, merge mode, changelog,
compaction, clustering, or
+runtime leaves the bounded schema-3 path.
+
+**Message:** PR4 implements one conservative mutable MOR route, not the
complete Hudi table-service
+surface.
+
+**Action:** `BLOCKED`; do not guess or pass through unsupported settings.
+
## FLINK_CHECKPOINTING_REQUIRED
**Trigger:** Checkpointing is explicitly disabled for the streaming sink.
diff --git
a/hudi-agent-gateway/skills/hudi-architect/validate_flink_capabilities.py
b/hudi-agent-gateway/skills/hudi-architect/validate_flink_capabilities.py
index db9cf70cbc32..8038b0abc303 100644
--- a/hudi-agent-gateway/skills/hudi-architect/validate_flink_capabilities.py
+++ b/hudi-agent-gateway/skills/hudi-architect/validate_flink_capabilities.py
@@ -40,7 +40,7 @@ DEFAULT_MANIFEST = (
SKILL_DIR / "references" / "flink-1.20-hudi-1.2.0-capabilities.toml"
)
-EXPECTED_SCHEMA_VERSION = 3
+EXPECTED_SCHEMA_VERSION = 4
EXPECTED_BASELINE_ID = "hudi-1.2.0-flink-1.20"
EXPECTED_HUDI_VERSION = "1.2.0"
EXPECTED_HUDI_SOURCE_REVISION = "f05c83f2b97732de7a558ff9b26959e1139c05f5"
@@ -102,6 +102,81 @@ EXPECTED_REQUIRED_SETTINGS = [
"value": "upsert",
"applies_to": "pr3-mutable-cow",
},
+ {
+ "key": "changelog.enabled",
+ "value": False,
+ "applies_to": "pr4-mutable-mor",
+ },
+ {
+ "key": "clustering.async.enabled",
+ "value": False,
+ "applies_to": "pr4-mutable-mor",
+ },
+ {
+ "key": "clustering.schedule.enabled",
+ "value": False,
+ "applies_to": "pr4-mutable-mor",
+ },
+ {
+ "key": "compaction.async.enabled",
+ "value": True,
+ "applies_to": "pr4-mutable-mor",
+ },
+ {
+ "key": "compaction.delta_commits",
+ "value": 5,
+ "applies_to": "pr4-mutable-mor",
+ },
+ {
+ "key": "compaction.operation.execute.async.enabled",
+ "value": True,
+ "applies_to": "pr4-mutable-mor",
+ },
+ {
+ "key": "compaction.schedule.enabled",
+ "value": True,
+ "applies_to": "pr4-mutable-mor",
+ },
+ {
+ "key": "compaction.trigger.strategy",
+ "value": "num_commits",
+ "applies_to": "pr4-mutable-mor",
+ },
+ {
+ "key": "hoodie.write.record.merge.mode",
+ "value": "EVENT_TIME_ORDERING",
+ "applies_to": "pr4-mutable-mor",
+ },
+ {
+ "key": "index.bootstrap.enabled",
+ "value": True,
+ "applies_to": "pr4-mutable-mor",
+ },
+ {
+ "key": "index.global.enabled",
+ "value": True,
+ "applies_to": "pr4-mutable-mor",
+ },
+ {
+ "key": "index.state.ttl",
+ "value": 0.0,
+ "applies_to": "pr4-mutable-mor",
+ },
+ {
+ "key": "index.type",
+ "value": "FLINK_STATE",
+ "applies_to": "pr4-mutable-mor",
+ },
+ {
+ "key": "table.type",
+ "value": "MERGE_ON_READ",
+ "applies_to": "pr4-mutable-mor",
+ },
+ {
+ "key": "write.operation",
+ "value": "upsert",
+ "applies_to": "pr4-mutable-mor",
+ },
]
EXPECTED_PHYSICAL_TYPES = {
"simple": [
@@ -196,6 +271,26 @@ EXPECTED_ACCEPTANCE_EVIDENCE = {
"test_pr3_requires_index_bootstrap",
"testPinnedMutableIndexBootstrapConfiguration",
),
+ "FLINK_MOR_CLUSTERING_UNSUPPORTED": (
+ "test_pr4_rejects_clustering",
+ "testPinnedMorClusteringDisabled",
+ ),
+ "FLINK_MOR_APPEND_ONLY_UNSUPPORTED": (
+ "test_pr4_rejects_append_only_mor",
+ "testPinnedAppendOnlyMorDoesNotScheduleCompaction",
+ ),
+ "FLINK_MOR_COMPACTION_CONFIGURATION_UNSUPPORTED": (
+ "test_pr4_rejects_incompatible_compaction_configuration",
+ "testPinnedMorCompactionConfiguration",
+ ),
+ "FLINK_MOR_READ_OPTIMIZED_FRESHNESS_UNSUPPORTED": (
+ "test_pr4_rejects_strict_read_optimized_freshness",
+ "testPinnedMorQuerySemantics",
+ ),
+ "FLINK_PR4_WRITE_PATH_UNSUPPORTED": (
+ "test_pr4_valid_mutable_mor_design_renders_exact_executable_sql",
+ "testMutableMorSinkAndCompactionPlan",
+ ),
}
SHA1_PATTERN = re.compile(r"[0-9a-f]{40}")
SHA256_PATTERN = re.compile(r"[0-9a-f]{64}")
@@ -331,6 +426,7 @@ def validate_manifest(manifest: dict[str, Any]) ->
list[str]:
if setting.get("applies_to") not in {
"pr2-append-only-cow",
"pr3-mutable-cow",
+ "pr4-mutable-mor",
}:
errors.append(
f"required_effective_settings[{index}].applies_to is
unsupported"
@@ -398,6 +494,25 @@ def validate_manifest(manifest: dict[str, Any]) ->
list[str]:
"mutable_executable_contract does not match the bounded PR3 path"
)
+ mutable_mor_executable_contract =
manifest.get("mutable_mor_executable_contract")
+ expected_mutable_mor_contract = {
+ **expected_mutable_contract,
+ "contract_schema": 3,
+ "table_type": "MERGE_ON_READ",
+ "operation": "upsert",
+ "compaction_owner": "flink_writer",
+ "compaction_mode": "async",
+ "compaction_trigger_strategy": "num_commits",
+ "compaction_delta_commits": 5,
+ "query_modes": ["snapshot", "read_optimized"],
+ "read_optimized_freshness": "after_compaction",
+ "clustering_enabled": False,
+ }
+ if mutable_mor_executable_contract != expected_mutable_mor_contract:
+ errors.append(
+ "mutable_mor_executable_contract does not match the bounded PR4
path"
+ )
+
physical_types = manifest.get("physical_types")
if not isinstance(physical_types, dict):
errors.append("physical_types must be a table")
@@ -487,6 +602,7 @@ def validation_evidence(
"supported_design_contract_schemas": [
manifest["executable_contract"]["contract_schema"],
manifest["mutable_executable_contract"]["contract_schema"],
+ manifest["mutable_mor_executable_contract"]["contract_schema"],
],
"flink_fixture_version": manifest["flink"]["fixture_version"],
"hudi_source_revision": manifest["hudi"]["source_revision"],
diff --git a/hudi-agent-gateway/skills/hudi-architect/validate_flink_design.py
b/hudi-agent-gateway/skills/hudi-architect/validate_flink_design.py
index 5a564fc08c2b..37504d1215ef 100644
--- a/hudi-agent-gateway/skills/hudi-architect/validate_flink_design.py
+++ b/hudi-agent-gateway/skills/hudi-architect/validate_flink_design.py
@@ -43,9 +43,11 @@ from validate_flink_capabilities import (
APPEND_CONTRACT_SCHEMA_VERSION = 1
MUTABLE_CONTRACT_SCHEMA_VERSION = 2
+MUTABLE_MOR_CONTRACT_SCHEMA_VERSION = 3
SUPPORTED_CONTRACT_SCHEMAS = {
APPEND_CONTRACT_SCHEMA_VERSION,
MUTABLE_CONTRACT_SCHEMA_VERSION,
+ MUTABLE_MOR_CONTRACT_SCHEMA_VERSION,
}
MAX_TYPE_LENGTH = 2_147_483_647
STATUS_PRIORITY = {"INCOMPLETE": 0, "REVIEW_REQUIRED": 1, "BLOCKED": 2}
@@ -92,6 +94,13 @@ FINDING_STATUS = {
"FLINK_MUTABLE_ORDERING_FIELD_REQUIRED": "INCOMPLETE",
"FLINK_MUTABLE_PARTITION_EVOLUTION_UNSUPPORTED": "BLOCKED",
"FLINK_MUTABLE_SOURCE_CHANGELOG_UNSUPPORTED": "BLOCKED",
+ "FLINK_MOR_APPEND_ONLY_UNSUPPORTED": "BLOCKED",
+ "FLINK_MOR_CLUSTERING_UNSUPPORTED": "BLOCKED",
+ "FLINK_MOR_COMPACTION_CONFIGURATION_UNSUPPORTED": "BLOCKED",
+ "FLINK_MOR_COMPACTION_OWNER_REQUIRED": "INCOMPLETE",
+ "FLINK_MOR_COMPACTION_TUNING_UNSUPPORTED": "BLOCKED",
+ "FLINK_MOR_QUERY_REQUIREMENT_UNRESOLVED": "INCOMPLETE",
+ "FLINK_MOR_READ_OPTIMIZED_FRESHNESS_UNSUPPORTED": "BLOCKED",
"FLINK_OPTION_NOT_VERIFIED": "REVIEW_REQUIRED",
"FLINK_PARTITION_FIELD_MISSING": "BLOCKED",
"FLINK_PHYSICAL_SCHEMA_REQUIRED": "INCOMPLETE",
@@ -111,6 +120,7 @@ FINDING_STATUS = {
"FLINK_WRITER_MODEL_UNRESOLVED": "REVIEW_REQUIRED",
"FLINK_PR2_WRITE_PATH_UNSUPPORTED": "BLOCKED",
"FLINK_PR3_WRITE_PATH_UNSUPPORTED": "BLOCKED",
+ "FLINK_PR4_WRITE_PATH_UNSUPPORTED": "BLOCKED",
"FLINK_SCHEMA_TYPE_UNVERIFIED": "REVIEW_REQUIRED",
}
@@ -369,7 +379,11 @@ def _render_sql(
source = contract["source"]
runtime = contract["runtime"]
identity = contract["identity"]
- mutable = contract["contract_schema"] == MUTABLE_CONTRACT_SCHEMA_VERSION
+ mutable = contract["contract_schema"] in {
+ MUTABLE_CONTRACT_SCHEMA_VERSION,
+ MUTABLE_MOR_CONTRACT_SCHEMA_VERSION,
+ }
+ mutable_mor = contract["contract_schema"] ==
MUTABLE_MOR_CONTRACT_SCHEMA_VERSION
definitions = [
f" {_quote_identifier(column['name'])} {column['type']}"
@@ -390,11 +404,12 @@ def _render_sql(
table_ddl += f"\nPARTITIONED BY ({partition_sql})"
if mutable:
ordering_field = contract["ordering"]["fields"][0]
+ table_type = "MERGE_ON_READ" if mutable_mor else "COPY_ON_WRITE"
table_ddl += (
"\nWITH (\n"
" 'connector' = 'hudi',\n"
f" 'path' = {_sql_string(table['path'])},\n"
- " 'table.type' = 'COPY_ON_WRITE',\n"
+ f" 'table.type' = '{table_type}',\n"
" 'write.operation' = 'upsert',\n"
f" 'ordering.fields' = {_sql_string(ordering_field)},\n"
" 'hoodie.write.record.merge.mode' = 'EVENT_TIME_ORDERING',\n"
@@ -402,9 +417,19 @@ def _render_sql(
" 'index.global.enabled' = 'true',\n"
" 'index.state.ttl' = '0',\n"
" 'index.bootstrap.enabled' = 'true',\n"
- " 'changelog.enabled' = 'false'\n"
- ");"
+ " 'changelog.enabled' = 'false'"
)
+ if mutable_mor:
+ table_ddl += (
+ ",\n 'compaction.schedule.enabled' = 'true',\n"
+ " 'compaction.async.enabled' = 'true',\n"
+ " 'compaction.operation.execute.async.enabled' = 'true',\n"
+ " 'compaction.trigger.strategy' = 'num_commits',\n"
+ " 'compaction.delta_commits' = '5',\n"
+ " 'clustering.schedule.enabled' = 'false',\n"
+ " 'clustering.async.enabled' = 'false'"
+ )
+ table_ddl += "\n);"
else:
table_ddl += (
"\nWITH (\n"
@@ -438,7 +463,7 @@ def _render_sql(
def assess_design(
contract: dict[str, Any], manifest: dict[str, Any]
) -> dict[str, Any]:
- """Validate one explicit append-only or mutable COW contract."""
+ """Validate one explicit append-only COW, mutable COW, or mutable MOR
contract."""
assessment = Assessment()
manifest_errors = validate_manifest(manifest)
@@ -466,34 +491,58 @@ def assess_design(
"hudi_version",
"identity",
"ordering",
+ "read",
"runtime",
"safety",
"source",
"table",
+ "table_services",
"write",
},
"contract",
assessment,
)
contract_schema = contract.get("contract_schema")
- if (
- not isinstance(contract_schema, int)
- or isinstance(contract_schema, bool)
- or contract_schema not in SUPPORTED_CONTRACT_SCHEMAS
- ):
+ supported_contract_schema = (
+ isinstance(contract_schema, int)
+ and not isinstance(contract_schema, bool)
+ and contract_schema in SUPPORTED_CONTRACT_SCHEMAS
+ )
+ if not supported_contract_schema:
assessment.add(
"FLINK_DESIGN_CONTRACT_INVALID",
- "contract_schema must be 1 (append-only) or 2 (mutable COW)",
+ "contract_schema must be 1 (append-only), 2 (mutable COW), or 3
(mutable MOR)",
)
- mutable_contract = contract_schema == MUTABLE_CONTRACT_SCHEMA_VERSION
- contract_profile = manifest[
- "mutable_executable_contract" if mutable_contract else
"executable_contract"
- ]
+ mutable_contract = supported_contract_schema and contract_schema in {
+ MUTABLE_CONTRACT_SCHEMA_VERSION,
+ MUTABLE_MOR_CONTRACT_SCHEMA_VERSION,
+ }
+ mutable_mor_contract = (
+ supported_contract_schema
+ and contract_schema == MUTABLE_MOR_CONTRACT_SCHEMA_VERSION
+ )
+ if mutable_mor_contract:
+ contract_profile = manifest["mutable_mor_executable_contract"]
+ elif mutable_contract:
+ contract_profile = manifest["mutable_executable_contract"]
+ else:
+ contract_profile = manifest["executable_contract"]
if not mutable_contract and "ordering" in contract:
assessment.add(
"FLINK_DESIGN_CONTRACT_INVALID",
- "ordering is supported only by contract_schema 2",
+ "ordering is supported only by mutable contract schemas 2 and 3",
)
+ if not mutable_mor_contract:
+ if "read" in contract:
+ assessment.add(
+ "FLINK_DESIGN_CONTRACT_INVALID",
+ "read is supported only by contract_schema 3",
+ )
+ if "table_services" in contract:
+ assessment.add(
+ "FLINK_DESIGN_CONTRACT_INVALID",
+ "table_services is supported only by contract_schema 3",
+ )
if contract.get("baseline_id") != manifest.get("baseline_id"):
assessment.add("FLINK_VERSION_UNVERIFIED", "The capability baseline
does not match")
if not _non_empty_string(contract.get("hudi_version")) or not
_non_empty_string(
@@ -549,12 +598,12 @@ def assess_design(
if mutability != "mutable":
assessment.add(
"FLINK_MUTABILITY_REQUIRED",
- "contract_schema 2 requires mutable input",
+ "Mutable contract schemas 2 and 3 require mutable input",
)
elif mutability == "mutable":
assessment.add(
"FLINK_MUTABLE_COW_DEFERRED",
- "Mutable COW requires the explicit contract_schema 2 safety
contract",
+ "Mutable input requires an explicit mutable safety contract",
)
elif mutability != "append_only":
assessment.add("FLINK_MUTABILITY_REQUIRED", "Append-only input must be
confirmed")
@@ -865,6 +914,8 @@ def assess_design(
write_keys = {"connector_options", "operation", "table_type"}
if mutable_contract:
write_keys.update({"changelog_enabled", "index", "record_merge_mode"})
+ if mutable_mor_contract:
+ write_keys.add("clustering")
else:
write_keys.add("insert_cluster")
_check_keys(
@@ -880,7 +931,14 @@ def assess_design(
"FLINK_DESIGN_CONTRACT_INVALID",
"write.table_type and write.operation must be explicit",
)
- elif mutable_contract and (
+ elif mutable_mor_contract and (
+ table_type != "MERGE_ON_READ" or operation != "upsert"
+ ):
+ assessment.add(
+ "FLINK_PR4_WRITE_PATH_UNSUPPORTED",
+ "The first mutable MOR path supports only MERGE_ON_READ upsert",
+ )
+ elif mutable_contract and not mutable_mor_contract and (
table_type != "COPY_ON_WRITE" or operation != "upsert"
):
assessment.add(
@@ -890,19 +948,33 @@ def assess_design(
elif not mutable_contract and (
table_type != "COPY_ON_WRITE" or operation != "insert"
):
- assessment.add(
- "FLINK_PR2_WRITE_PATH_UNSUPPORTED",
- "PR2 supports only COPY_ON_WRITE with write.operation=insert",
- )
+ if table_type == "MERGE_ON_READ":
+ assessment.add(
+ "FLINK_MOR_APPEND_ONLY_UNSUPPORTED",
+ "The bounded append-only path remains COPY_ON_WRITE",
+ )
+ else:
+ assessment.add(
+ "FLINK_PR2_WRITE_PATH_UNSUPPORTED",
+ "PR2 supports only COPY_ON_WRITE with write.operation=insert",
+ )
if mutable_contract:
if write.get("record_merge_mode") != "EVENT_TIME_ORDERING":
assessment.add(
- "FLINK_PR3_WRITE_PATH_UNSUPPORTED",
+ (
+ "FLINK_PR4_WRITE_PATH_UNSUPPORTED"
+ if mutable_mor_contract
+ else "FLINK_PR3_WRITE_PATH_UNSUPPORTED"
+ ),
"write.record_merge_mode must be EVENT_TIME_ORDERING",
)
if write.get("changelog_enabled") is not False:
assessment.add(
- "FLINK_PR3_WRITE_PATH_UNSUPPORTED",
+ (
+ "FLINK_PR4_WRITE_PATH_UNSUPPORTED"
+ if mutable_mor_contract
+ else "FLINK_PR3_WRITE_PATH_UNSUPPORTED"
+ ),
"write.changelog_enabled must be explicitly false",
)
index = _dict(write.get("index"), "write.index", assessment)
@@ -932,6 +1004,22 @@ def assess_design(
"FLINK_MUTABLE_INDEX_BOOTSTRAP_REQUIRED",
"FLINK_STATE must bootstrap existing keys after state loss or
cold restart",
)
+ if mutable_mor_contract:
+ clustering = _dict(write.get("clustering"), "write.clustering",
assessment)
+ _check_keys(
+ clustering,
+ {"async_enabled", "schedule_enabled"},
+ "write.clustering",
+ assessment,
+ )
+ if (
+ clustering.get("schedule_enabled") is not False
+ or clustering.get("async_enabled") is not False
+ ):
+ assessment.add(
+ "FLINK_MOR_CLUSTERING_UNSUPPORTED",
+ "The bounded MOR path requires clustering schedule and
execution disabled",
+ )
else:
insert_cluster = write.get("insert_cluster")
if insert_cluster is True:
@@ -969,21 +1057,45 @@ def assess_design(
raw_table_type = connector_options.get("table.type")
raw_operation = connector_options.get("write.operation")
expected_operation = "upsert" if mutable_contract else "insert"
+ expected_table_type = "MERGE_ON_READ" if mutable_mor_contract else
"COPY_ON_WRITE"
if (
"table.type" in connector_options
- and raw_table_type != "COPY_ON_WRITE"
+ and raw_table_type != expected_table_type
) or (
"write.operation" in connector_options
and raw_operation != expected_operation
):
assessment.add(
(
- "FLINK_PR3_WRITE_PATH_UNSUPPORTED"
- if mutable_contract
- else "FLINK_PR2_WRITE_PATH_UNSUPPORTED"
+ "FLINK_PR4_WRITE_PATH_UNSUPPORTED"
+ if mutable_mor_contract
+ else (
+ "FLINK_PR3_WRITE_PATH_UNSUPPORTED"
+ if mutable_contract
+ else "FLINK_PR2_WRITE_PATH_UNSUPPORTED"
+ )
),
- "A connector-option override leaves the bounded COW path",
+ "A connector-option override leaves the bounded write path",
)
+ if mutable_mor_contract:
+ if any(key.startswith("clustering.") for key in connector_options):
+ assessment.add(
+ "FLINK_MOR_CLUSTERING_UNSUPPORTED",
+ "Clustering connector overrides are not supported by the
bounded MOR path",
+ )
+ if any(
+ key in {
+ "compaction.tasks",
+ "compaction.delta_seconds",
+ "compaction.target.io",
+ "compaction.timeout.seconds",
+ }
+ for key in connector_options
+ ):
+ assessment.add(
+ "FLINK_MOR_COMPACTION_TUNING_UNSUPPORTED",
+ "Detailed compaction tuning is outside the bounded
Architect path",
+ )
verified_overrides = sorted(set(connector_options) -
set(unknown_options))
if verified_overrides:
assessment.add(
@@ -992,6 +1104,114 @@ def assess_design(
f"pass-through overrides are not rendered:
{verified_overrides}",
)
+ if mutable_mor_contract:
+ table_services = _dict(
+ contract.get("table_services"), "table_services", assessment
+ )
+ _check_keys(table_services, {"compaction"}, "table_services",
assessment)
+ compaction = _dict(
+ table_services.get("compaction"),
+ "table_services.compaction",
+ assessment,
+ )
+ compaction_keys = {
+ "async_enabled",
+ "delta_commits",
+ "mode",
+ "operation_execute_async_enabled",
+ "owner",
+ "schedule_enabled",
+ "trigger_strategy",
+ }
+ tuning_keys = {
+ "delta_seconds",
+ "target_io_mb",
+ "tasks",
+ "timeout_seconds",
+ }
+ unknown_compaction_keys = set(compaction) - compaction_keys
+ if unknown_compaction_keys & tuning_keys:
+ assessment.add(
+ "FLINK_MOR_COMPACTION_TUNING_UNSUPPORTED",
+ "Detailed compaction tuning is outside the bounded Architect
path: "
+ f"{sorted(unknown_compaction_keys & tuning_keys)}",
+ )
+ remaining_unknown = unknown_compaction_keys - tuning_keys
+ if remaining_unknown:
+ assessment.add(
+ "FLINK_DESIGN_CONTRACT_INVALID",
+ "table_services.compaction contains unsupported fields: "
+ f"{sorted(remaining_unknown)}",
+ )
+ owner = compaction.get("owner")
+ if owner is None:
+ assessment.add(
+ "FLINK_MOR_COMPACTION_OWNER_REQUIRED",
+ "The MOR compaction owner must be explicit",
+ )
+ elif owner == "standalone_service":
+ assessment.add(
+ "FLINK_MULTI_WRITER_REVIEW",
+ "A standalone compactor is an independent table-service
writer",
+ )
+ elif owner != "flink_writer":
+ assessment.add(
+ "FLINK_WRITER_MODEL_UNRESOLVED",
+ "The MOR compaction owner is unresolved",
+ )
+ expected_compaction = {
+ "async_enabled": True,
+ "delta_commits": 5,
+ "mode": "async",
+ "operation_execute_async_enabled": True,
+ "schedule_enabled": True,
+ "trigger_strategy": "num_commits",
+ }
+ mismatches = [
+ key
+ for key, expected_value in expected_compaction.items()
+ if compaction.get(key) != expected_value
+ ]
+ if mismatches:
+ assessment.add(
+ "FLINK_MOR_COMPACTION_CONFIGURATION_UNSUPPORTED",
+ "The bounded MOR compaction contract has incompatible fields: "
+ f"{mismatches}",
+ )
+
+ read = _dict(contract.get("read"), "read", assessment)
+ _check_keys(
+ read,
+ {"query_modes", "read_optimized_freshness"},
+ "read",
+ assessment,
+ )
+ query_modes = _string_list(read.get("query_modes"),
"read.query_modes", assessment)
+ allowed_query_modes = {"snapshot", "read_optimized"}
+ if not query_modes or not
set(query_modes).issubset(allowed_query_modes):
+ assessment.add(
+ "FLINK_MOR_QUERY_REQUIREMENT_UNRESOLVED",
+ "MOR readers must explicitly require snapshot and/or
read_optimized queries",
+ )
+ read_optimized_freshness = read.get("read_optimized_freshness")
+ if "read_optimized" in query_modes:
+ if read_optimized_freshness is None or read_optimized_freshness ==
"unknown":
+ assessment.add(
+ "FLINK_MOR_QUERY_REQUIREMENT_UNRESOLVED",
+ "The acceptable read-optimized freshness must be explicit",
+ )
+ elif read_optimized_freshness != "after_compaction":
+ assessment.add(
+ "FLINK_MOR_READ_OPTIMIZED_FRESHNESS_UNSUPPORTED",
+ "Read-optimized results can be current only after
compaction",
+ )
+ elif read_optimized_freshness != "NOT_APPLICABLE":
+ assessment.add(
+ "FLINK_DESIGN_CONTRACT_INVALID",
+ "read.read_optimized_freshness must be NOT_APPLICABLE without "
+ "read_optimized queries",
+ )
+
runtime = _dict(contract.get("runtime"), "runtime", assessment)
_check_keys(
runtime,
@@ -1012,9 +1232,13 @@ def assess_design(
elif execution_mode != "STREAMING":
assessment.add(
(
- "FLINK_PR3_WRITE_PATH_UNSUPPORTED"
- if mutable_contract
- else "FLINK_PR2_WRITE_PATH_UNSUPPORTED"
+ "FLINK_PR4_WRITE_PATH_UNSUPPORTED"
+ if mutable_mor_contract
+ else (
+ "FLINK_PR3_WRITE_PATH_UNSUPPORTED"
+ if mutable_contract
+ else "FLINK_PR2_WRITE_PATH_UNSUPPORTED"
+ )
),
"The executable path supports streaming execution",
)
diff --git
a/hudi-agent-gateway/tests/fixtures/hudi_architect/flink_pr4/mutable_mor.json
b/hudi-agent-gateway/tests/fixtures/hudi_architect/flink_pr4/mutable_mor.json
new file mode 100644
index 000000000000..6f655b40eac2
--- /dev/null
+++
b/hudi-agent-gateway/tests/fixtures/hudi_architect/flink_pr4/mutable_mor.json
@@ -0,0 +1,85 @@
+{
+ "contract_schema": 3,
+ "baseline_id": "hudi-1.2.0-flink-1.20",
+ "hudi_version": "1.2.0",
+ "flink_version": "1.20.1",
+ "safety": {
+ "table_lifecycle": "new",
+ "writer_model": "single_writer",
+ "external_catalog": false,
+ "mutability": "mutable",
+ "replay_behavior": "must_collapse"
+ },
+ "table": {
+ "name": "orders_mutable_mor_hudi",
+ "path": "file:///tmp/hudi-architect-mutable-mor-orders",
+ "columns": [
+ {"name": "id", "type": "STRING", "nullable": false},
+ {"name": "name", "type": "STRING", "nullable": true},
+ {"name": "event_version", "type": "BIGINT", "nullable": false},
+ {"name": "partition_date", "type": "DATE", "nullable": false}
+ ],
+ "partition_fields": ["partition_date"],
+ "partition_fields_mutable": false
+ },
+ "identity": {
+ "mode": "stable_key",
+ "record_key_fields": ["id"],
+ "record_key_option_fields": []
+ },
+ "ordering": {
+ "mode": "event_time",
+ "fields": ["event_version"]
+ },
+ "source": {
+ "table": "orders_mutable_source",
+ "columns": [
+ {"name": "id", "type": "STRING", "nullable": false},
+ {"name": "name", "type": "STRING", "nullable": true},
+ {"name": "event_version", "type": "BIGINT", "nullable": false},
+ {"name": "partition_date", "type": "DATE", "nullable": false}
+ ],
+ "primary_key_fields": ["id"],
+ "changelog_mode": "UPSERT",
+ "emits_deletes": true,
+ "delete_payload": "FULL_ROW"
+ },
+ "write": {
+ "table_type": "MERGE_ON_READ",
+ "operation": "upsert",
+ "record_merge_mode": "EVENT_TIME_ORDERING",
+ "index": {
+ "type": "FLINK_STATE",
+ "global": true,
+ "state_ttl_days": 0,
+ "bootstrap_enabled": true
+ },
+ "changelog_enabled": false,
+ "clustering": {
+ "schedule_enabled": false,
+ "async_enabled": false
+ },
+ "connector_options": {}
+ },
+ "table_services": {
+ "compaction": {
+ "owner": "flink_writer",
+ "mode": "async",
+ "schedule_enabled": true,
+ "async_enabled": true,
+ "operation_execute_async_enabled": true,
+ "trigger_strategy": "num_commits",
+ "delta_commits": 5
+ }
+ },
+ "read": {
+ "query_modes": ["snapshot", "read_optimized"],
+ "read_optimized_freshness": "after_compaction"
+ },
+ "runtime": {
+ "execution_mode": "STREAMING",
+ "checkpointing_enabled": true,
+ "checkpoint_interval_ms": 60000,
+ "target_commit_freshness_ms": 120000
+ }
+}
diff --git
a/hudi-agent-gateway/tests/fixtures/hudi_architect/flink_pr4/mutable_mor.sql
b/hudi-agent-gateway/tests/fixtures/hudi_architect/flink_pr4/mutable_mor.sql
new file mode 100644
index 000000000000..93d427b5ccb2
--- /dev/null
+++ b/hudi-agent-gateway/tests/fixtures/hudi_architect/flink_pr4/mutable_mor.sql
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+SET 'execution.runtime-mode' = 'streaming';
+SET 'execution.checkpointing.interval' = '60000 ms';
+
+CREATE TABLE `orders_mutable_mor_hudi` (
+ `id` STRING NOT NULL,
+ `name` STRING,
+ `event_version` BIGINT NOT NULL,
+ `partition_date` DATE NOT NULL,
+ PRIMARY KEY (`id`) NOT ENFORCED
+)
+PARTITIONED BY (`partition_date`)
+WITH (
+ 'connector' = 'hudi',
+ 'path' = 'file:///tmp/hudi-architect-mutable-mor-orders',
+ 'table.type' = 'MERGE_ON_READ',
+ 'write.operation' = 'upsert',
+ 'ordering.fields' = 'event_version',
+ 'hoodie.write.record.merge.mode' = 'EVENT_TIME_ORDERING',
+ 'index.type' = 'FLINK_STATE',
+ 'index.global.enabled' = 'true',
+ 'index.state.ttl' = '0',
+ 'index.bootstrap.enabled' = 'true',
+ 'changelog.enabled' = 'false',
+ 'compaction.schedule.enabled' = 'true',
+ 'compaction.async.enabled' = 'true',
+ 'compaction.operation.execute.async.enabled' = 'true',
+ 'compaction.trigger.strategy' = 'num_commits',
+ 'compaction.delta_commits' = '5',
+ 'clustering.schedule.enabled' = 'false',
+ 'clustering.async.enabled' = 'false'
+);
+
+INSERT INTO `orders_mutable_mor_hudi` (
+ `id`,
+ `name`,
+ `event_version`,
+ `partition_date`
+)
+SELECT
+ `id`,
+ `name`,
+ `event_version`,
+ `partition_date`
+FROM `orders_mutable_source`;
diff --git a/hudi-agent-gateway/tests/test_hudi_architect_skill.py
b/hudi-agent-gateway/tests/test_hudi_architect_skill.py
index b662da9e4a4d..39d885612bc2 100644
--- a/hudi-agent-gateway/tests/test_hudi_architect_skill.py
+++ b/hudi-agent-gateway/tests/test_hudi_architect_skill.py
@@ -41,6 +41,7 @@ FLINK_DESIGN_VALIDATOR = SKILL_DIR /
"validate_flink_design.py"
PINNED_HUDI_REVISION = "f05c83f2b97732de7a558ff9b26959e1139c05f5"
PR2_FIXTURE_DIR = GATEWAY_DIR / "tests" / "fixtures" / "hudi_architect" /
"flink_pr2"
PR3_FIXTURE_DIR = GATEWAY_DIR / "tests" / "fixtures" / "hudi_architect" /
"flink_pr3"
+PR4_FIXTURE_DIR = GATEWAY_DIR / "tests" / "fixtures" / "hudi_architect" /
"flink_pr4"
ASF_LICENSE_MARKER = "Licensed to the Apache Software Foundation (ASF) under
one"
PR2_JAVA_FIXTURE = (
GATEWAY_DIR
@@ -170,7 +171,7 @@ def
test_flink_baseline_has_bounded_append_and_mutable_contracts() -> None:
"fixture_version": "1.20.1",
}
assert PINNED_HUDI_REVISION in capability
- assert manifest["schema_version"] == 3
+ assert manifest["schema_version"] == 4
assert manifest["fixture_artifacts"] == {
"hudi_bundle": "org.apache.hudi:hudi-flink1.20-bundle:1.2.0",
"flink_version": "1.20.1",
@@ -215,11 +216,11 @@ def
test_flink_capability_validation_emits_pinned_evidence() -> None:
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"baseline_id": "hudi-1.2.0-flink-1.20",
- "capability_manifest_schema": 3,
+ "capability_manifest_schema": 4,
"design_contract_schema": 1,
"flink_fixture_version": "1.20.1",
"hudi_source_revision": PINNED_HUDI_REVISION,
- "supported_design_contract_schemas": [1, 2],
+ "supported_design_contract_schemas": [1, 2, 3],
}
@@ -348,6 +349,23 @@ def
test_flink_capability_manifest_records_executable_acceptance_contracts() ->
"table.type": "COPY_ON_WRITE",
"write.operation": "upsert",
},
+ "pr4-mutable-mor": {
+ "changelog.enabled": False,
+ "clustering.async.enabled": False,
+ "clustering.schedule.enabled": False,
+ "compaction.async.enabled": True,
+ "compaction.delta_commits": 5,
+ "compaction.operation.execute.async.enabled": True,
+ "compaction.schedule.enabled": True,
+ "compaction.trigger.strategy": "num_commits",
+ "hoodie.write.record.merge.mode": "EVENT_TIME_ORDERING",
+ "index.bootstrap.enabled": True,
+ "index.global.enabled": True,
+ "index.state.ttl": 0.0,
+ "index.type": "FLINK_STATE",
+ "table.type": "MERGE_ON_READ",
+ "write.operation": "upsert",
+ },
}
assert implemented_checks == {
"FLINK_APPEND_MODE_CLUSTERING_ENABLED": (
@@ -398,6 +416,26 @@ def
test_flink_capability_manifest_records_executable_acceptance_contracts() ->
"test_pr3_requires_index_bootstrap",
"testPinnedMutableIndexBootstrapConfiguration",
),
+ "FLINK_MOR_CLUSTERING_UNSUPPORTED": (
+ "test_pr4_rejects_clustering",
+ "testPinnedMorClusteringDisabled",
+ ),
+ "FLINK_MOR_APPEND_ONLY_UNSUPPORTED": (
+ "test_pr4_rejects_append_only_mor",
+ "testPinnedAppendOnlyMorDoesNotScheduleCompaction",
+ ),
+ "FLINK_MOR_COMPACTION_CONFIGURATION_UNSUPPORTED": (
+ "test_pr4_rejects_incompatible_compaction_configuration",
+ "testPinnedMorCompactionConfiguration",
+ ),
+ "FLINK_MOR_READ_OPTIMIZED_FRESHNESS_UNSUPPORTED": (
+ "test_pr4_rejects_strict_read_optimized_freshness",
+ "testPinnedMorQuerySemantics",
+ ),
+ "FLINK_PR4_WRITE_PATH_UNSUPPORTED": (
+ "test_pr4_valid_mutable_mor_design_renders_exact_executable_sql",
+ "testMutableMorSinkAndCompactionPlan",
+ ),
}
for check in manifest["implemented_acceptance_checks"]:
assert check["python_test"] in python_tests
@@ -509,11 +547,11 @@ def
test_pr2_valid_designs_render_exact_executable_sql(fixture_name: str) -> Non
).rstrip("\n")
assert assessment["validation_evidence"] == {
"baseline_id": "hudi-1.2.0-flink-1.20",
- "capability_manifest_schema": 3,
+ "capability_manifest_schema": 4,
"design_contract_schema": 1,
"flink_fixture_version": "1.20.1",
"hudi_source_revision": PINNED_HUDI_REVISION,
- "supported_design_contract_schemas": [1, 2],
+ "supported_design_contract_schemas": [1, 2, 3],
}
@@ -530,7 +568,212 @@ def
test_pr3_valid_mutable_design_renders_exact_executable_sql() -> None:
PR3_FIXTURE_DIR / "mutable_cow.sql"
).rstrip("\n")
assert assessment["validation_evidence"]["design_contract_schema"] == 2
- assert assessment["validation_evidence"]["capability_manifest_schema"] == 3
+ assert assessment["validation_evidence"]["capability_manifest_schema"] == 4
+
+
+def test_pr4_valid_mutable_mor_design_renders_exact_executable_sql() -> None:
+ result = _run_flink_design_validator(PR4_FIXTURE_DIR / "mutable_mor.json")
+
+ assert result.returncode == 0, result.stderr
+ assessment = json.loads(result.stdout)
+ assert assessment["status"] == "CONFIG_VALIDATED"
+ assert assessment["executable_eligible"] is True
+ assert assessment["finding_codes"] == []
+ assert assessment["advisory_codes"] == []
+ assert assessment["artifacts"]["combined_sql"] == _read_sql_golden(
+ PR4_FIXTURE_DIR / "mutable_mor.sql"
+ ).rstrip("\n")
+ assert assessment["validation_evidence"] == {
+ "baseline_id": "hudi-1.2.0-flink-1.20",
+ "capability_manifest_schema": 4,
+ "design_contract_schema": 3,
+ "flink_fixture_version": "1.20.1",
+ "hudi_source_revision": PINNED_HUDI_REVISION,
+ "supported_design_contract_schemas": [1, 2, 3],
+ }
+
+
[email protected](
+ ("field", "value"),
+ [
+ ("schedule_enabled", False),
+ ("async_enabled", False),
+ ("operation_execute_async_enabled", False),
+ ("mode", "inline"),
+ ("trigger_strategy", "time_elapsed"),
+ ("delta_commits", 6),
+ ],
+)
+def test_pr4_rejects_incompatible_compaction_configuration(
+ tmp_path: Path, field: str, value: object
+) -> None:
+ design = json.loads(_read(PR4_FIXTURE_DIR / "mutable_mor.json"))
+ design["table_services"]["compaction"][field] = value
+
+ result = _run_flink_design_validator(_write_design(tmp_path, design))
+
+ assert result.returncode == 1
+ assessment = json.loads(result.stdout)
+ assert "FLINK_MOR_COMPACTION_CONFIGURATION_UNSUPPORTED" in
assessment["finding_codes"]
+ assert assessment["status"] == "BLOCKED"
+ assert "artifacts" not in assessment
+
+
[email protected]("field", ["schedule_enabled", "async_enabled"])
+def test_pr4_rejects_clustering(tmp_path: Path, field: str) -> None:
+ design = json.loads(_read(PR4_FIXTURE_DIR / "mutable_mor.json"))
+ design["write"]["clustering"][field] = True
+
+ result = _run_flink_design_validator(_write_design(tmp_path, design))
+
+ assert result.returncode == 1
+ assessment = json.loads(result.stdout)
+ assert assessment["finding_codes"] == ["FLINK_MOR_CLUSTERING_UNSUPPORTED"]
+ assert assessment["status"] == "BLOCKED"
+ assert "artifacts" not in assessment
+
+
+def test_pr4_rejects_strict_read_optimized_freshness(tmp_path: Path) -> None:
+ design = json.loads(_read(PR4_FIXTURE_DIR / "mutable_mor.json"))
+ design["read"]["read_optimized_freshness"] = "after_every_commit"
+
+ result = _run_flink_design_validator(_write_design(tmp_path, design))
+
+ assert result.returncode == 1
+ assessment = json.loads(result.stdout)
+ assert assessment["finding_codes"] == [
+ "FLINK_MOR_READ_OPTIMIZED_FRESHNESS_UNSUPPORTED"
+ ]
+ assert assessment["status"] == "BLOCKED"
+ assert "artifacts" not in assessment
+
+
[email protected]("freshness", [None, "unknown"])
+def test_pr4_requires_resolved_read_optimized_freshness(
+ tmp_path: Path, freshness: object
+) -> None:
+ design = json.loads(_read(PR4_FIXTURE_DIR / "mutable_mor.json"))
+ if freshness is None:
+ design["read"].pop("read_optimized_freshness")
+ else:
+ design["read"]["read_optimized_freshness"] = freshness
+
+ result = _run_flink_design_validator(_write_design(tmp_path, design))
+
+ assert result.returncode == 1
+ assessment = json.loads(result.stdout)
+ assert assessment["finding_codes"] == [
+ "FLINK_MOR_QUERY_REQUIREMENT_UNRESOLVED"
+ ]
+ assert assessment["status"] == "INCOMPLETE"
+ assert "artifacts" not in assessment
+
+
[email protected](
+ ("owner", "expected_code", "expected_status"),
+ [
+ (None, "FLINK_MOR_COMPACTION_OWNER_REQUIRED", "INCOMPLETE"),
+ ("standalone_service", "FLINK_MULTI_WRITER_REVIEW", "REVIEW_REQUIRED"),
+ ("unknown", "FLINK_WRITER_MODEL_UNRESOLVED", "REVIEW_REQUIRED"),
+ ],
+)
+def test_pr4_requires_same_job_compaction_owner(
+ tmp_path: Path, owner: object, expected_code: str, expected_status: str
+) -> None:
+ design = json.loads(_read(PR4_FIXTURE_DIR / "mutable_mor.json"))
+ if owner is None:
+ design["table_services"]["compaction"].pop("owner")
+ else:
+ design["table_services"]["compaction"]["owner"] = owner
+
+ result = _run_flink_design_validator(_write_design(tmp_path, design))
+
+ assert result.returncode == 1
+ assessment = json.loads(result.stdout)
+ assert assessment["finding_codes"] == [expected_code]
+ assert assessment["status"] == expected_status
+ assert "artifacts" not in assessment
+
+
+def test_pr4_rejects_detailed_compaction_tuning(tmp_path: Path) -> None:
+ design = json.loads(_read(PR4_FIXTURE_DIR / "mutable_mor.json"))
+ design["table_services"]["compaction"]["tasks"] = 4
+
+ result = _run_flink_design_validator(_write_design(tmp_path, design))
+
+ assert result.returncode == 1
+ assessment = json.loads(result.stdout)
+ assert assessment["finding_codes"] ==
["FLINK_MOR_COMPACTION_TUNING_UNSUPPORTED"]
+ assert assessment["status"] == "BLOCKED"
+ assert "artifacts" not in assessment
+
+
+def test_pr4_preserves_combined_findings_and_withholds_artifacts(tmp_path:
Path) -> None:
+ design = json.loads(_read(PR4_FIXTURE_DIR / "mutable_mor.json"))
+ design["table_services"]["compaction"].pop("owner")
+ design["write"]["clustering"]["schedule_enabled"] = True
+ design["read"]["read_optimized_freshness"] = "after_every_commit"
+
+ result = _run_flink_design_validator(_write_design(tmp_path, design))
+
+ assert result.returncode == 1
+ assessment = json.loads(result.stdout)
+ assert assessment["finding_codes"] == [
+ "FLINK_MOR_CLUSTERING_UNSUPPORTED",
+ "FLINK_MOR_COMPACTION_OWNER_REQUIRED",
+ "FLINK_MOR_READ_OPTIMIZED_FRESHNESS_UNSUPPORTED",
+ ]
+ assert assessment["status"] == "BLOCKED"
+ assert "artifacts" not in assessment
+
+
+def test_pr4_snapshot_only_contract_does_not_invent_read_optimized_freshness(
+ tmp_path: Path,
+) -> None:
+ design = json.loads(_read(PR4_FIXTURE_DIR / "mutable_mor.json"))
+ design["read"] = {
+ "query_modes": ["snapshot"],
+ "read_optimized_freshness": "NOT_APPLICABLE",
+ }
+
+ result = _run_flink_design_validator(_write_design(tmp_path, design))
+
+ assert result.returncode == 0, result.stderr
+ assessment = json.loads(result.stdout)
+ assert assessment["status"] == "CONFIG_VALIDATED"
+ assert assessment["finding_codes"] == []
+
+
+def test_pr4_rejects_append_only_mor(tmp_path: Path) -> None:
+ design = json.loads(_read(PR2_FIXTURE_DIR / "stable_key.json"))
+ design["write"]["table_type"] = "MERGE_ON_READ"
+
+ result = _run_flink_design_validator(_write_design(tmp_path, design))
+
+ assert result.returncode == 1
+ assessment = json.loads(result.stdout)
+ assert assessment["finding_codes"] == ["FLINK_MOR_APPEND_ONLY_UNSUPPORTED"]
+ assert assessment["status"] == "BLOCKED"
+ assert "artifacts" not in assessment
+
+
[email protected](
+ ("field", "value"),
+ [("table_services", []), ("table_services", None), ("read", []), ("read",
None)],
+)
+def test_pr4_malformed_service_and_read_objects_fail_closed(
+ tmp_path: Path, field: str, value: object
+) -> None:
+ design = json.loads(_read(PR4_FIXTURE_DIR / "mutable_mor.json"))
+ design[field] = value
+
+ result = _run_flink_design_validator(_write_design(tmp_path, design))
+
+ assert result.returncode == 1
+ assessment = json.loads(result.stdout)
+ assert "FLINK_DESIGN_CONTRACT_INVALID" in assessment["finding_codes"]
+ assert assessment["executable_eligible"] is False
+ assert "artifacts" not in assessment
@pytest.mark.parametrize(
@@ -734,7 +977,7 @@ def
test_pr2_fails_closed_when_capability_manifest_is_malformed(tmp_path: Path)
assert "artifacts" not in assessment
[email protected]("contract_schema", [True, 1.0, 3])
[email protected]("contract_schema", [True, 1.0, 4, [], {}])
def test_flink_design_rejects_non_exact_contract_schema(
tmp_path: Path, contract_schema: object
) -> None:
@@ -745,6 +988,7 @@ def test_flink_design_rejects_non_exact_contract_schema(
assert result.returncode == 1
assessment = json.loads(result.stdout)
+ assert "Traceback" not in result.stderr
assert "FLINK_DESIGN_CONTRACT_INVALID" in assessment["finding_codes"]
assert assessment["executable_eligible"] is False
assert "artifacts" not in assessment
@@ -1316,7 +1560,8 @@ def test_safety_gate_order_is_stable() -> None:
"## F7 — Physical table and source contract",
"## F8 — Source changelog contract",
"## F9 — Streaming checkpoint contract",
- "## PR2/PR3 validation and completion",
+ "## F10 — Mutable table type, query freshness, and MOR ownership",
+ "## PR2/PR3/PR4 validation and completion",
]
offsets = [flow.index(heading) for heading in headings]
assert offsets == sorted(offsets)