Repository navigation
Conversation
|
Thank you for your contribution to Apache Doris. Please clearly describe your PR:
|
|
run buildall |
|
/review |
There was a problem hiding this comment.
Static review complete for PR #68648 at head 908d784ad5076034117789eb966f653ae7b7f09a after two review rounds, an independent risk scan, and a final sweep of all seven changed files. I request changes for one correctness finding and include one test-coverage finding. There are no existing P0/P1 inline findings on this head.
- Goal and data correctness: The new non-IVM path derives base read predicates from the same mapping used for refresh snapshots. It works for the covered single-column range and empty-scope cases, but a valid multi-column LIST partition layout still reads an unmapped base partition because the predicate uses only the projected MV partition column (P1). A later partition drop can leave a stale row in an MV partition that is judged synchronized.
- Scope and parallel paths: The change is localized to refresh task planning, command predicate construction, EXPLAIN wiring, and tests.
BaseTableInfopreserves table identity across resolved objects. IVM refresh deliberately passes a null scope; EXPLAIN retains the MV key-range approximation; external PCT tables are unscoped. I found no separate defect in those paths. - Concurrency, lifecycle, and errors: A batch's map and snapshots come from one refresh context and the map is retained through retries. No new thread, lock order, or static lifecycle was introduced. If a mapped base partition disappears before live item lookup, the refresh fails explicitly under stale metadata; I did not treat that as a correctness defect.
- Configuration, compatibility, and writes: This adds no configuration key, persisted format, FE/BE field, or rolling-upgrade interface. It uses the existing
partition_sync_limit. Insert overwrite still targets the selected MV partitions and publishes task snapshots after a successful batch; the P1 case is a mismatch between what the scan reads and those recorded snapshots. No separate EditLog or crash-path regression was substantiated. - Tests, observability, and performance: The FE tests exercise range scope, empty scope, and multi-key LIST key position. The new regression checks base rows and MV counts, but on January 2-5 it passes even with the old predicate (P2), and it does not cover the P1 overlap. Existing logs and failure propagation remain in place; no new metric or independently substantiated performance defect was found. This was static inspection only: the review contract prohibited builds, tests, and source edits.
The user supplied no additional focus points. All candidates were accepted, dismissed with code evidence, or deduplicated; the second round returned NO_NEW_VALUABLE_FINDINGS from every required reviewer.
| // Built from the key at the position the MV's partition column has in this table, which is | ||
| // what the mapping is keyed by; a partition of a list partitioned table can hold more than | ||
| // one key, and the MV's column is not necessarily the first of them. | ||
| builder.put(table, constructPredicates(items, new UnboundSlot(colName), |
There was a problem hiding this comment.
[P1] Restrict the scan to the mapped base partitions, not just their projected partition-column values. For a valid LIST(d, region) table, let expired p_old contain (old, 'US') and retained p_kept contain (old, 'EU') and (new, 'EU'). With partition_sync_limit=2 and an MV partitioned by d, the mapping and snapshot name only p_kept, but this predicate becomes d IN (old, new) and still reads the p_old row. Dropping p_old later leaves that row in the MV while the snapshot comparison sees only unchanged p_kept, so rewrite can serve stale data. The old predicate had this hole too; the new scope does not close it. Use exact base partition IDs or full LIST tuples for the scoped read, and cover this overlap in a regression test.
There was a problem hiding this comment.
Fixed in 43be1b3. The scope is now built from the partitions themselves rather than from the value they take at one column: a list partition is pinned to the whole of each key it holds (each key a conjunction, IS NULL for a value a row does not have), and a range partition to its bounds, which is the same thing since a range partitioned base table has a single partition column.
The LIST(d, region) shape you describe is a regression case now. With the change the MV holds only the kept partition's rows; with the predicate reverted to the projected one it reads the expired partition's row, measured as RealRow: [2020-01-01, US, 1] against a golden that expects only the EU rows.
One shape is deliberately left as it was: a list partitioned table's default partition, which holds no key at all. What it holds cannot be said with a predicate on the partition columns, so it is read in full as before -- a read that can be seen to be too wide, rather than one that loses its rows quietly.
| // the window keeps rather than a fixed number of rows: when today is early enough in January that the | ||
| // kept days fall in the new year, the MV partition that covers the expired day does not exist and both | ||
| // the MV and the expectation lose it -- the case is then not exercised, but it is still asserted. | ||
| def today = java.time.LocalDate.now() |
There was a problem hiding this comment.
[P2] Keep the expired and retained days in one MV year for every test run. On January 2-5, expiredDay falls in the previous year while both retained days are in the current year. The sync limit therefore creates only a current-year MV partition, whose old key-range predicate already excludes expiredDay; both assertions pass even if this PR's scoped-read change is reverted. Please use a fixture or controlled clock that always leaves an expired base partition inside a retained MV partition's year, so this regression consistently fails without the fix.
There was a problem hiding this comment.
Fixed in 43be1b3. You are right, and the fixture turned out to rest on an assumption of mine rather than on a measurement: while fixing it I measured the property, and partition_sync_limit=1 DAY expires nothing at all (six days of partitions all kept), while limit=2 DAY puts its edge at today-2. So both the number of retained days and the identity of the expired one were guesses.
The two sides are now asserted separately through order_qt -- the expired day must be missing from the MV, the days the window kept must be there -- so a total cannot let them cancel out, and the case is skipped with assumeTrue where the calendar does not leave the expired day inside a retained MV partition. Skipped rather than asserted, so that a run which exercises the case is a run that can fail without the change.
TPC-H: Total hot run time: 27828 ms |
TPC-DS: Total hot run time: 152080 ms |
ClickBench: Total hot run time: 23.92 s |
FE Regression Coverage ReportIncrement line coverage |
|
run buildall |
|
/review |
There was a problem hiding this comment.
Static review complete for PR #68648 at head 43be1b3. Two full-review rounds, separate risk scans, and a final sweep covered all seven changed files. I request changes for two P1 correctness findings and include one P2 regression finding. This is static inspection only; the review contract prohibited builds, tests, and source edits.
Critical checkpoints:
-
Goal and tests: The change aims to read the same OLAP base partitions that a non-IVM MV refresh records in its snapshots. Ordinary range and full LIST tuple cases follow that mapping, and the new tests cover selected RANGE partitions, an empty scope, and one ordinary LIST overlap. The goal is not met for fractional DATETIMEV2 keys or default LIST partitions (P1 comments); no added test covers either.
-
Scope and clarity: The implementation is localized to refresh planning, predicate construction, EXPLAIN wiring, and tests. BaseTableInfo avoids an object-identity mismatch. The default-partition comment assumes an empty key list that Doris does not produce for normal defaults.
-
Concurrency: Refresh jobs and base DDL can overlap. The batch read set and its snapshots use one MTMVRefreshContext mapping, the same set is retained across retries, and missing mapped names fail rather than silently widening a scan. No new thread, lock-order, or independently substantiated race defect was found.
-
Lifecycle: COMPLETE and PARTITIONS non-IVM attempts reach the per-batch scope; overwrite completion precedes snapshot publication. I found no new static-initialization, ownership, or termination issue.
-
Configuration: No configuration item was added. The change uses the existing partition_sync_limit; no new dynamic-update behavior is required.
-
Compatibility: No persisted format, function symbol, or FE/BE protocol field changed. Existing valid fractional partition keys and default LIST layouts do regress under the new predicates (P1 comments). No separate rolling-upgrade defect was found.
-
Parallel paths: IVM deliberately keeps its stream and partition-ID selection, EXPLAIN COMPLETE keeps an approximate MV-range plan without an execution context, and external PCT tables remain unscoped. No additional parallel-path omission was substantiated.
-
Conditions and errors: An explicitly empty readable set becomes FALSE, which matches its snapshot set. NULL tuple members use IS NULL. The default LIST fallback checks for an empty key list, but normal defaults contain a synthetic MIN key (P1). Missing partition lookups fail explicitly.
-
Test coverage and results: The golden output matches the fixture's intended ordered results, but the new date guard throws a JUnit assumption that this Groovy runner counts as a failed suite on January 1-5 (P2), and it prevents the independent LIST case from running then. Fractional and default LIST/RANGE refresh cases need coverage. No tests were run in this review; author and CI statements were not treated as independent validation.
-
Observability: Existing task logs and failure propagation remain. The two missing-row cases can be recorded as synchronized, so ordinary logs do not expose their incorrect contents; this is covered by the P1 findings. No separate metric or logging defect was substantiated.
-
Transaction, persistence, and writes: Insert overwrite still targets the selected MV partitions and publishes snapshots after successful batches. No EditLog, commit, or crash-path change was added. The P1 defects instead make a successful overwrite persist an incomplete MV image beside a matching snapshot.
-
FE/BE variable passing: No new cross-process variable or thrift field was introduced.
-
Performance: Base-partition predicates can grow with the mapped partition count, but this review found no concrete performance failure or further actionable optimization defect.
-
Other issues: No additional substantiated issue remains after candidate-by-candidate verification and the final changed-file sweep.
Existing review context: Inline P1 #4134420545's ordinary LIST(d,region) overlap is addressed by the full-tuple predicate and regression fixture on this head; its original scenario is not a still-applicable blocking comment. The new default-LIST underread is a distinct failure despite the author's reply expecting a broad default read. Inline P2 #4134420556's former false-positive assertion was replaced, while the newly added assumption produces the separate suite failure reported here. There are no still-applicable existing P0/P1 inline IDs.
User focus: No additional focus points were supplied. All candidate findings were accepted, dismissed with code evidence, or deduplicated; every required reviewer returned NO_NEW_VALUABLE_FINDINGS in round two.
| } | ||
| keys.add(ExpressionUtils.and(oneKey)); | ||
| } | ||
| return keys.isEmpty() ? BooleanLiteral.TRUE : ExpressionUtils.or(keys); |
There was a problem hiding this comment.
[P1] Keep default LIST rows reachable when scoping by full tuples. With LIST(k1,k2), an explicit partition for (1,2), and a default partition containing (1,3), an MV partitioned by k1 previously read both rows for key 1. This predicate now reads only (1,2). Doris represents the default partition with a synthetic MIN key, so keys.isEmpty() is never true; its separate MIN MV partition also cannot supply (1,3). The MV loses a committed row while its mapped snapshots can report synchronized. Handle default partition coverage in the mapping/read together, or reject this shape until it can be represented; add a regression with a default row sharing an explicit MV key.
There was a problem hiding this comment.
Fixed in 5bf185b, and you were right about the representation: a default partition's item is not empty, it holds the sentinel key the rows no other partition claims are placed by, so the keys.isEmpty() I had written to read it in full was dead code and the partition was read as none of its rows. Measured on the shape in your comment -- LIST(k1, k2) with an explicit (1, 2) and a default partition holding (1, 3), an MV partitioned by k1 -- the MV held only (1, 2) before the change and holds both after it.
The two cannot be handled apart, as you say: which rows of a default partition belong to an MV partition is the MV partition's own question, since it takes the rows whose key falls in its range wherever the base table placed them, and that partition is not one the MV partition is recorded with. So a base table that has one is read the way an unscoped one is -- the MV partition's own key range -- which is a reading that can be seen to be too wide rather than one that drops rows belonging to the partition being refreshed. A regression case pins it (list_default in the read scope suite), and it fails when that fallback is removed.
|
|
||
| private static Expression convertPartitionKeyToLiteral(PartitionKey key, int keyPos) { | ||
| return Literal.fromLegacyLiteral(key.getKeys().get(keyPos), | ||
| Type.fromPrimitiveType(key.getTypes().get(keyPos))); |
There was a problem hiding this comment.
[P1] Preserve DATETIMEV2 scale in scoped base-partition predicates. Type.fromPrimitiveType(DATETIMEV2) supplies scale 0, so this conversion rounds fractional partition keys before building both LIST tuple equalities and RANGE bounds. For example, a RANGE(ts DATETIMEV2(3)) partition ending at 2024-02-01 00:00:00.123 in a year-rollup MV gets a new ts < ...00.000 filter, omitting rows that belong to the mapped partition; a secondary LIST key at .123 is similarly compared to .000. The snapshot still records that partition, so the MV can appear synchronized while missing rows. Convert with the base partition column's full Type and cover fractional keys in refresh tests.
There was a problem hiding this comment.
Fixed in 5bf185b. The key is converted with its partition column's full Type now, so a value written with milliseconds is compared at that scale; the callers that have the column pass its type and the ones that do not keep the primitive one they were given before.
Measured on a fractional key rather than assumed: LIST(ts) with a partition of ('2024-02-01 00:00:00.123') and an MV partitioned by ts left that MV partition empty before the change -- the predicate had been rounded to .000 -- and holds its row after it. A regression case pins it (fractional_key), failing when the type is taken from the primitive again.
One thing your example led me to check: a RANGE(ts DATETIME(3)) partition ending at a fractional value cannot back a year rollup MV at all -- MTMVPartitionExprDateTrunc refuses it at CREATE with "partition values not equal" -- so the range bound is not a way to reach this; the list key is.
| // The expired day is read by a refresh only while it falls inside a retained MV partition's range, and | ||
| // both days are inside the same year only from the third of January on. Skipped rather than asserted | ||
| // when they are not, so that a run that exercises the case is a run that can fail without the change. | ||
| org.junit.Assume.assumeTrue("the expired day and today are in the same year", |
There was a problem hiding this comment.
[P2] Avoid a JUnit assumption as this suite's calendar guard. From January 1 through 5, expiredDay is in the prior year and assumeTrue throws, but the Groovy regression runner catches every Throwable in SuiteContext.start and records it through onSuiteFailed; this makes the suite fail rather than skip, and the independent LIST case never runs. Use a calendar-independent fixture or a supported conditional that leaves the LIST case runnable.
There was a problem hiding this comment.
Fixed in 5bf185b. You are right about the runner: SuiteContext#start catches every Throwable from the suite body and records it through onSuiteFailed, so the assumption was a failure of the whole suite -- and being at the top of it, it took the calendar independent cases down with it for five days a year.
The calendar dependent part is now guarded by a condition, so it is the part that is left out rather than the suite, and the rest runs either way (verified by forcing the guard off: the same suite passes with the other cases alone). The days themselves stay relative because what the case is about -- a base partition the sync window left out, inside a retained MV partition's range -- cannot be written with fixed dates; the fixed-date shapes that can be are the list partition cases, which are calendar independent and always run.
TPC-H: Total hot run time: 27608 ms |
TPC-DS: Total hot run time: 152644 ms |
ClickBench: Total hot run time: 24.02 s |
|
run buildall |
|
/review |
TPC-H: Total hot run time: 28179 ms |
TPC-DS: Total hot run time: 152265 ms |
ClickBench: Total hot run time: 24.1 s |
There was a problem hiding this comment.
Review complete — changes requested. I reviewed all seven changed files at head 5bf185b in three static rounds. The final round's two full reviews and separate risk scan found no new valuable findings. This review adds three P1 and one P2 inline findings. Earlier P1 comment 4134420545 remains applicable only to the default-LIST fallback variant: its original no-default fixture is fixed, but an expired explicit partition can still be read without appearing in the recorded mapping when a default partition triggers the fallback. I did not repost that issue.
Critical checkpoints
- Goal and proof: The change aims to make non-IVM MV refresh reads match mapped base partitions and their snapshots. The RANGE, explicit LIST-tuple, and fractional-key cases have unit or regression coverage, but default LIST and multi-PCT cases still violate that goal (inline findings). The golden output is consistent with the checked positive cases; it does not prove later default-partition DML or the combined edge cases.
- Scope and clarity: The new mapping is passed only through the intended refresh path; IVM and EXPLAIN retain their existing unscoped behavior. The empty-set and default fallbacks introduce the correctness gaps identified inline. No unrelated source change was found.
- Concurrency and lifecycle: The refresh context builds a mapping, batches derive readable names, the overwrite reads base data, and partition snapshots are published afterward. Default-partition data can enter an MV partition without a matching dependency in its snapshot. Concurrent partition DDL can also mutate the newly traversed metadata map after its read lock has been released. No separate lock-order deadlock, static-initialization, or one-replacement false-synchronization case was substantiated.
- Configuration and compatibility: No new configuration, FE-BE field, function symbol, storage format, or edit-log schema is added. Existing partition_sync_limit and one-partition batch settings expose the residual over-read and repeated metadata-scan cost; no separate dynamic-config or rolling-upgrade issue was established.
- Parallel paths and conditions: Scoped RANGE and full LIST tuples, NULL keys, views/CTEs, retries, IVM, SELF_MANAGE, and EXPLAIN were traced. The default LIST fallback and the empty-mapping branch need the fixes described inline.
- Tests and results: The added FE assertions and ordered regression golden cover the positive RANGE, LIST, default-row, and fractional-key examples. They omit a later default-partition write, a multi-PCT default row, and a fractional key on a table with a default partition. This invocation did not run builds or tests, as the review prompt prohibits them; author replies and the golden are not independent runtime validation.
- Persistence, writes, visibility, and observability: The overwrite and journal formats are unchanged. The findings concern which committed rows the refresh reads and which base-partition versions its snapshot records, so they can affect MV contents and transparent rewrite despite ordinary visible-version handling. Existing task logging remains available; no separate metrics or logging defect was substantiated.
- Performance and other invariants: The new default-partition lookup is repeated for each batch and adds quadratic partition-map visits under the default batch size. No BE memory-safety or additional error-handling issue was established.
User focus: No additional focus points were supplied. Conclusions are static-only.
Existing P0/P1 findings confirmed for this head: #68648 (comment)
| builder.put(table, constructPredicates(mvItems, colName)); | ||
| continue; | ||
| } | ||
| if (readable.isEmpty()) { |
There was a problem hiding this comment.
[P1] Check default LIST coverage before treating an empty mapping as FALSE. In a supported two-PCT MV, A has an explicit LIST partition for key 1, B has an explicit partition for key 2 plus a default partition holding key 1, and the MV joins A and B on that key. The key-1 MV partition is created from A; B has no mapped name for it because its default projects the MIN sentinel. This branch injects FALSE into B's scan, making the join empty and dropping the committed key-1 row on the first refresh. The earlier default-partition thread covers the nonempty-map branch. Apply the MV-key fallback here too, with matching snapshot dependencies, and test the two-PCT case.
There was a problem hiding this comment.
Fixed in 800af51, in the mapping rather than in that branch, because that is where the read and the record have to agree.
A base table's default list partition is now named in the mapping of every MV partition that reads the table, not only in the mapping of the one its sentinel key maps to. Your shape is measured: with a_t holding key 1 explicitly and b_t holding key 2 explicitly plus a default partition with key 1, the MV partition for key 1 is recorded with b_default, so it is not read as nothing and the join holds the row (1 10 100), where before the change the MV was empty.
That also settles where the empty mapping comes from: with the mapping naming the default partition in every MV partition, "this table feeds nothing to these partitions" can no longer be true of a table that has one. The check for it is still made before the empty branch answers FALSE, and it is now read from the mapped partitions themselves, so a table's partitions are not scanned per batch.
| // such a partition is therefore read the way an unscoped one is. That read can be seen to be | ||
| // too wide -- it is the one this scope exists to narrow -- rather than one that drops rows | ||
| // which belong to the MV partition being refreshed. | ||
| builder.put(table, constructPredicates(mvItems, colName)); |
There was a problem hiding this comment.
[P1] Track default LIST partitions in every MV partition that reads them. In the new list_default fixture, this fallback puts the p_default row with d=2020-01-01 into the explicit 2020 MV partition, but its snapshot records only p_explicit because the default sentinel maps elsewhere. A later insert into p_default changes none of the versions checked for 2020, so automatic refresh skips it and transparent rewrite can serve stale rows. The existing default-partition thread covers the initial read; this is the missing dependency after that read. Add a post-refresh default insert test.
There was a problem hiding this comment.
Fixed in 800af51. The default list partition of a base table is now named in the mapping of every MV partition that reads it, which is the same set the read uses and the set the refresh records, so isSyncWithPartitions compares it: an insert into it changes a version the MV partition checks and the partition stops calling itself synchronized.
Pinned as a case in the read scope suite (list_default_tracked): the MV is refreshed, a row is inserted into the default partition afterwards, and SyncWithBaseTables is false for it, where before the change it stayed true.
| private static Expression convertListPartitionToIn(PartitionItem item, Slot col) { | ||
| List<Expression> inValues = ((ListPartitionItem) item).getItems().stream() | ||
| .map(UpdateMvByPartitionCommand::convertPartitionKeyToLiteral) | ||
| .map(key -> convertPartitionKeyToLiteral(key, 0, Optional.empty())) |
There was a problem hiding this comment.
[P1] Preserve DATETIMEV2 scale when the new default-LIST fallback reaches this conversion. A LIST(ts DATETIME(3)) base table with an explicit key 2024-02-01 00:00:00.123 and a default partition sends its PARTITION BY ts MV through constructPredicates(mvItems, colName). This Optional.empty() uses Type.fromPrimitiveType(DATETIMEV2) (scale 0), so the generated ts IN predicate compares against .000 and omits the committed .123 row. The earlier scale thread covered the scoped base-partition path; the fallback bypasses that fix. Add a combined fractional/default regression.
There was a problem hiding this comment.
Fixed in 800af51. The fallback that reads a table through the MV partition's own key range now converts keys with the partition column's full Type, like the scoped path does, so the scale is not lost on the way: a LIST(ts DATETIME(3)) table with an explicit 2024-02-01 00:00:00.123 and a default partition keeps that row in its PARTITION BY ts MV, which was empty before.
The regression case is the combined shape you asked for (fractional_default), and it fails when the type is taken from the primitive again.
| if (!(partitionInfo instanceof ListPartitionInfo)) { | ||
| return false; | ||
| } | ||
| return ((ListPartitionInfo) partitionInfo).getIdToItem(false).values().stream() |
There was a problem hiding this comment.
[P2] Resolve default-LIST presence once from stable partition metadata. getIdToItem(false) returns a mutable HashMap and requires the table read lock, but this call runs after buildRefreshContext released its locks; concurrent ADD/DROP PARTITION can make anyMatch throw an unretried ConcurrentModificationException. With no default, it also walks all N partitions for every refresh batch; the default one-partition batch size adds O(N²) metadata visits for N one-to-one MV partitions. Capture this fact under the table lock and reuse it across batches.
There was a problem hiding this comment.
Fixed in 800af51, in the direction you point at: the fact is no longer read from getIdToItem at all.
Whether a table is read this way is now taken from the mapped partitions a refresh already has in hand -- one of them being a default partition is the condition -- so there is no lock-free walk of the table's partition metadata, no per-batch O(N) scan, and nothing new to serialise against a concurrent ADD or DROP PARTITION. The underlying fact, which table has such a partition, is computed once per mapping in MTMV#calculatePartitionMappings and merged into every MV partition's entry there, which is also what makes it available across batches instead of per batch.
|
run buildall |
|
/review |
There was a problem hiding this comment.
Static review of PR #68648 at 800af51. Review complete after three rounds and a final sweep of all eight changed files. Request changes: one new P1 rewrite correctness finding and two new P2 test findings are inline below. No builds or tests were run, as required by the review task; the conclusions are code-path analysis, not runtime validation.
Critical checkpoints:
- Goal and proof: The PR aims to make MV refresh read exactly the recorded base partitions, including default LIST dependencies. Exact LIST/RANGE predicates improve that path, but the default mapping still has wrong-result cases, so the goal is not fully met. The new regression suite does not reliably prove its date-window or per-partition dependency claims (two P2 comments).
- Scope and clarity: The changes are concentrated in mapping, refresh predicate plumbing, and matching tests. The new fan-out also changes rewrite eligibility; that shared consumer needs to be handled explicitly.
- Concurrency: Refresh context, invalidation, and rewrite planning hold the relevant table locks during mapping. The unlocked metadata status probe catches analysis/runtime failures and can transiently report unsynchronized; no separate correctness or lock-order finding was substantiated. No new thread or global mutable state is introduced.
- Lifecycle: Manual named-partition refresh remains exact, and batch snapshots publish only refreshed partitions. That lifecycle is central to the new P1: another default-dependent MV partition can remain stale while rewrite checks a different, refreshed partition.
- Configuration: No new setting is introduced. The existing SET-able enable_materialized_view_union_rewrite=false path skips invalid-partition removal and exposes the P1 wrong result.
- Compatibility: No changed storage, serialization, FE/BE protocol, or function symbol was found in this PR; no rolling-upgrade finding was substantiated.
- Parallel paths: Scoped LIST/RANGE reads, default fallback, empty mapping, EXPLAIN, batch refresh, invalidation, and rewrite were traced. The new mapping must be represented faithfully in the rewrite inverse; it currently is not.
- Conditions: The default LIST and empty-map branches were checked against their upstream mappings and related cases. The still-applicable too-wide default read and missing real-key coverage are covered by existing P1 threads below.
- Tests: FE unit and regression changes cover several intended shapes, including LIST, RANGE, default, and fractional keys, but the date fixture can vary between the runner and FE calendars, and the whole-MV flag can pass without proving the 2020 partition dependency. The partial-refresh/default-only rewrite case needs coverage.
- Expected results: The affected golden entries are deterministic only under an unverified common calendar, and SyncWithBaseTables=false is too coarse to prove the claimed dependency. No other distinct golden mismatch was substantiated.
- Observability: Existing debug/status paths provide context; this PR adds no metric or logging requirement supported by a distinct issue.
- Transactions and persistence: No EditLog format or new persistent field is changed. The reviewed snapshot and partition-version relationships can nevertheless mark an incomplete or stale MV as rewrite-eligible in the cases reported.
- Writes and crashes: No new base-table write or crash-recovery mechanism is introduced. Refresh publication and exact partial scope were checked; no separate atomicity or crash finding was substantiated.
- FE/BE variables: No new variable or FE-to-BE transfer is added. The relevant union-rewrite session setting is handled in FE planner control flow.
- Performance and remaining issues: Default detection runs once per mapping rather than once per refresh batch. No further concrete performance defect or unresolved candidate remained after the final sweep.
Existing blocking findings still applicable on this head, without duplicate inline comments: r4134420545 (default LIST plus sync limit can read an expired explicit partition absent from the snapshot) and r4140476259 (a default row whose real MV key has no created partition remains unreachable while rewrite can regard the base default as covered). The earlier threads already describe these failure modes. User focus: none was provided.
Existing P0/P1 findings confirmed for this head: #68648 (comment), #68648 (comment)
| for (Entry<String, PartitionItem> entry : mvPartitionItems.entrySet()) { | ||
| res.put(entry.getKey(), | ||
| pctPartitionDescs.getOrDefault(entry.getValue().toPartitionKeyDesc(), Maps.newHashMap())); | ||
| res.put(entry.getKey(), withDefaultListPartitions( |
There was a problem hiding this comment.
[P1] Check every MV partition mapped from a default LIST base partition before allowing rewrite. This fan-out puts p_default in multiple MV mappings, but MTMVRewriteUtil.getPctToMv inverts them into one String and keeps only an arbitrary destination. With explicit LIST tuples (2020,2) and (2021,2) plus default rows for (2020,3) and (2021,3), an insert into p_default dirties both MV partitions. Refresh only the inverse-selected partition, then query the other default-only tuple: Filter(d=other AND k=3) -> Scan(base[p_default]) can rewrite to Filter(d=other AND k=3) -> Scan(MV[other stale partition]) because eligibility checks only the refreshed destination. Setting enable_materialized_view_union_rewrite=false skips the later invalid-partition removal, so this can return stale data. Preserve the full inverse set or reject ambiguous default mappings for rewrite.
There was a problem hiding this comment.
Fixed in 5ba21b7, by preserving the whole inverse set. MTMVRewriteUtil.getPctToMv answers each base partition with the set of MV partitions that read it now, and the rewrite's partition selection adds all of them, so a query that reads a default partition is held against every MV partition its rows can be in rather than against whichever one the map happened to keep. The UT covers the fan-out (t1_default read by mv_a and mv_b, answered with both).
That is the other half of the fan-out the previous commit introduced, and it is what the mapping's own contract says: the mapping is what the read and the record agree on, so anything inverting it has to keep all of it.
There was a problem hiding this comment.
Fixed in 0dbc408, on the side that cannot drop rows. Preserving the whole inverse set made every MV partition that reads a base partition a candidate, but the eligibility still answered a query when only some of those partitions were valid. With enable_materialized_view_union_rewrite off there is nothing to compensate the others: the MV alone answers the query and reads the partitions that are not valid as they are.
getMTMVCanRewritePartitions now returns no partition for a query unless every MV partition its base partitions are mapped from is valid, when the union rewrite is off. Read as a subset rather than as a count, since a partition inside its grace period is answered usable without having been compared and is not necessarily one the query reads. With the union rewrite on (the default) a partially valid answer is still a complete one, and the valid partitions still answer.
| // The MV partition is a year and the base table's are days, so the range it is read through is wider | ||
| // than what it is recorded with. Days are taken relative to today, and the two sides are asserted | ||
| // separately rather than as a total, so that where the window's edge falls does not decide the case. | ||
| def today = java.time.LocalDate.now() |
There was a problem hiding this comment.
[P2] Use the FE refresh calendar for this date-window fixture. LocalDate.now() uses the regression runner's default time zone, while partition_sync_limit computes its cutoff from DateTimeAcquire.now() in the FE session time zone. If the runner is on September 30 and FE is on October 1 (or setup crosses midnight), p_kept has an upper bound equal to FE's cutoff and is filtered out, so scope_kept_days returns 1 against the fixed golden value 2. Derive and hold the fixture date in the same calendar as the refresh, or recheck the boundary before asserting it.
There was a problem hiding this comment.
Fixed in 5ba21b7. The fixture's days now come from select curdate(), the FE session's own date, which is the calendar partition_sync_limit cuts its window from, instead of the runner's local date.
| // recorded with the partition they come from: a row inserted into it afterwards is a change the MV | ||
| // compares, rather than one it calls itself synchronized through. | ||
| sql """INSERT INTO list_default_base VALUES (\"2020-01-01\", 4, 7)""" | ||
| order_qt_list_default_tracked "select SyncWithBaseTables from mv_infos('database'='${dbName}') where Name='list_default_mv'" |
There was a problem hiding this comment.
[P2] Assert the default dependency on the 2020 MV partition itself. Before the fan-out change, p_default still mapped to the separate MIN MV partition. This insert makes MIN unsynchronized, so mv_infos.SyncWithBaseTables becomes false even while the 2020 partition still omits p_default from its snapshot. The new golden therefore passes with the tracking fix reverted. Check the 2020 partition's sync state before and after the insert, or refresh again and assert that its new row appears.
There was a problem hiding this comment.
You are right, and the case asserts the row now instead of the flag. list_default_tracked inserts into the default partition, refreshes the MV, and compares the rows: the new row has to be there, which is exactly what a refresh that judged the 2020 partition synchronized would leave out. The MV-level SyncWithBaseTables was not a check -- as you say, the sentinel partition moves it on its own.
The mapping fix cannot be reverted on its own to control this one, since reverting it fails the suite earlier (list_default, the read half); the row assertion is the part that pins the tracking.
|
run buildall |
|
/review |
There was a problem hiding this comment.
Static review of #68648 at dc1d04c. One new P2 finding: the aggregate-group fallback also suppresses safe union rewrites over disjoint RANGE partitions.
Two existing P1 findings remain applicable and are carried as existing_blocking_comment_ids rather than reposted: external PCT read/snapshot mismatch and absent IVM PCT input read outside recorded scope. Both were independently traced through current sources, including the relocated Paimon connector and IVM full/reset/delta paths. The existing aggregate-group finding 4356885832 is addressed by the new fallback for its stated LIST trigger.
Required checkpoint conclusions:
- Goal and evidence: the change aligns ordinary OLAP refresh reads with recorded base partitions, coalesces overlapping LIST components, and tightens rewrite eligibility. Added unit/regression fixtures cover these mechanisms; the two known P1 exemptions mean read/record agreement is incomplete.
- Scope and reuse: changes stay within partition mapping and its refresh, EXPLAIN, and rewrite consumers; grouping and mapped-scope helpers are reused. The aggregate fallback is broader than necessary (new P2).
- Concurrency and locks: task alignment/context construction use sorted base-table read locks; default metadata walks hold the table lock. Mapping and later predicate lookup are separate phases. The existing DDL/retry threads already cover that transient window; the new descriptor guard fails loudly but does not itself retry alignment. No distinct new concurrency defect was substantiated.
- Lifecycle: new maps and the undescribed-partition set are task/statement scoped. No new thread, ownership cycle, C++ static initializer, or release obligation is introduced.
- Configuration: no new setting. Existing sync-window, grace, and union settings control the paths; default LIST tables deliberately bypass the sync window.
- Compatibility: no new wire/storage format or journal member. Stored and generated direct LIST descriptors both use canonical ordering; legacy physical key order alone does not require replacement.
- Thrift/protobuf: no gensrc changes.
- Parallel paths: ordinary/default LIST, RANGE, fractional keys, SELF_MANAGE, EXPLAIN, aggregate-on-nonaggregate, self/multi-table joins, external PCT, and IVM paths were inspected. The last two retain the known P1 scope mismatches.
- Special conditions: NULL-aware full-key predicates, explicit empty scope, unmatched-descriptor guard, and post-version-read grace decisions are justified. The aggregate rejection needs narrowing to overlapping groups.
- Test coverage: added unit and regression cases cover transitive grouping, key-order collisions, pruned components, read scope, default dependencies, millisecond bounds, grace/preload, compensation slots, and aggregate-group fallback. The known P1 scenarios still need focused end-to-end coverage. No builds or tests were run, as required by the static-only contract.
- Changed results: inspected new expected rows against the fixtures. The three changed grace-big no-cache blocks match their base-query blocks. Actual execution and golden-generation provenance were not verified.
- Observability: existing task failure/snapshot logs and optimizer fail reasons carry useful MV/partition context; no additional critical observability defect found.
- Persistence and failover: existing snapshot publication and journal machinery remain in use; no new persisted state or commit point is added. Read/record correctness remains limited by the two known P1s.
- Writes and atomicity: refresh continues through the existing transactional INSERT OVERWRITE path and publishes snapshots after successful batches. An atomic write can still record the wrong dependency set in the known external/IVM cases.
- FE/BE propagation: no new transmitted variable or protocol field.
- Performance: key-indexed grouping, factored RANGE predicates, early RANGE pruning, and bulk cloud-version reads avoid the historical repeated-work paths. The new P2 unnecessarily removes MV acceleration for disjoint RANGE aggregate compensation.
- Further-issue recheck: reviewed wrapper/subtree aggregates, stale-only versus uncovered compensation, self aliases, bound slots, NULL/scale, descriptor identity, and DDL/default fanout. The apparent LIST roll-up ordering issue is unreachable through supported CREATE: the only partition-expression service rejects LIST inputs.
User focus: none supplied. Coverage includes all 30 changed paths and required upstream/downstream mechanisms. Limits are static inspection only: no runtime interleaving, connector integration, build, or test execution is claimed; no unresolved concrete candidate remains.
Review converged after 2 round(s).
Existing P0/P1 findings confirmed for this head: #68648 (comment), #68648 (comment)
| // uncovered partition also carries. The union is below the aggregate then, so the group is | ||
| // two rows rather than one. Answered from the base table instead of splitting it. | ||
| if (needBaseTableUnion && !uncoveredNarrowedBasePartitions.isEmpty() | ||
| && queryPlan.anyMatch(LogicalAggregate.class::isInstance)) { |
There was a problem hiding this comment.
[P2] Preserve safe RANGE aggregate union rewrites
This also rejects ordinary windowed RANGE MVs whose aggregate groups are disjoint. For example, let t have populated daily RANGE(d) partitions for 2020-01-01 and 2038-01-01, plus an unrelated 2040 partition. Refresh PARTITION BY(d) AS SELECT d,SUM(v) FROM t GROUP BY d with a 2 YEAR sync limit, then query those first two dates:
Aggregate(d, SUM(v))
Filter(d IN (2020-01-01, 2038-01-01))
Scan t[old, kept]
calcInvalidPartitions reports old in uncoveredNarrowedBasePartitions, so this guard rejects the candidate before canUnionRewrite. But UnionAll(Scan MV[kept], Aggregate(d,SUM(v)) -> Scan t[old]) is correct: its groups have different d values. The query now scans and aggregates the retained base data too, losing the normal TTL/union optimization (also expected by partition_union_rewrite/partition_mv_rewrite.groovy). Limit the fallback to cases where uncovered partitions can overlap retained aggregate groups, and preserve this disjoint RANGE case.
There was a problem hiding this comment.
Fixed in a3cdee32fd1.
The fallback you pointed at is now only the key-overlap question, and that set is filled from a ListPartitionInfo: overlappingUncoveredListPartitions returns empty for any other partitioning, because a RANGE partition is either covered by a retained MV partition -- then it is not uncovered -- or lies outside its range, where it answers groups of its own. Your windowed RANGE case keeps its UnionAll(Scan MV[kept], Aggregate(d,SUM(v)) -> Scan t[old]), and so does every ordinary TTL-shaped query.
What I took out of the base rule is the group-expression test, which is what over-declined here; it now lives in AbstractMaterializedViewAggregateRule#canUnionRewrite, which asks the grouping itself: the rewrite stands when the grouping holds every column the MV is partitioned on, each of them by itself, and is declined otherwise. A bare GROUP BY d therefore keeps the rewrite -- which is what nereids_rules_p0/mv/partition_union_rewrite and nereids_rules_p0/mv/union_rewrite_grace_big need. Those two suites were failing on the branch before this commit (the roll-up query of partition_mv_rewrite, which groups by date_trunc(l_shipdate,'day') and bare l_shipdate, was declined by the expression test); both pass now, along with union_rewrite, grouping_sets, agg_with_roll_up, agg_without_roll_up and date_trunc -- 14 suites, none failed.
One thing to flag rather than bury: the grouping test declines an expression over the partition column even when the expression happens not to coarsen. union_all_compensate's query5 groups by to_date(data_date) over daily RANGE partitions, where the branches do come out disjoint; telling that apart needs the expression evaluated per partition value, which is not something the grouping can answer, so that candidate is now declined and the suite's expectation moved from mv_rewrite_success to mv_rewrite_fail. Its rows are unchanged -- the .out compares the base-table answer against the same values -- only the plan differs.
The fallback for a compensated group that can split was applied to every uncovered partition, so an ordinary windowed RANGE MV whose uncovered partition answers groups of its own lost the union rewrite and scanned the retained base data again. Only a LIST partition can carry, on the MV's partition columns, a key a retained MV partition also holds; a RANGE partition outside the retained ranges answers its own groups. Key changes: - Record an uncovered partition only when its keys, projected onto the MV's partition column, meet the keys of the base partitions a retained MV partition is recorded with. Unit Test: - Regression: mtmv_p0/test_mtmv_base_partition_read_scope's group_split, nereids_rules_p0/mv/partition_union_rewrite - fe/fe-core: 70 mtmv / nereids-mv test classes, 971 tests
|
run buildall |
|
/review |
There was a problem hiding this comment.
Static review of #68648 at 1de7a0f. No additional inline findings; request changes for three previously reported P1 concerns independently confirmed against this head. Existing threads were treated as duplicate fences.
- 4271997436: external PCT reads remain wider than the recorded partition set for newly merged LIST components. The OLAP-only scope leaves the external scan on the MV key domain, while Paimon freshness compares only mapped per-partition timestamps; removing an unrecorded partition can leave its rows eligible through the MV.
- 4278938895: an IVM COMPLETE batch can read an entirely absent PCT input through an ordinary scan. No reset IDs or freshness dependency record that expired partition, and a subsequent DELETE can be skipped by the all-synchronized incremental exit.
- 4356885832: the exact GROUP BY d reproduction is fixed, but aggregate-group splitting remains for a many-to-one grouping expression. For LIST(d,k), use kept={(2020-01-02,1),(2038-01-01,1)}, bridge={(2038-01-01,2)}, old={(2020-01-01,2)}, and a 2 YEAR sync window. An MV partitioned/grouped by d records kept+bridge. With v=2 at kept's January 2 row and v=1 at old's January 1 row, query d IN ('2020-01-01','2020-01-02') grouped by date_trunc(d,'year'). The raw-key overlap helper sees distinct dates, but canUnionRewrite accepts the grouping expression containing d. The reduced static result is UnionAll(Aggregate(year(d),SUM(s))->MV[M], Aggregate(year(d),SUM(v))->base[old]): two year-2020 rows of 2 and 1 instead of one row of 3. The existing issue remains open for this variant; no duplicate inline is posted.
The safe disjoint RANGE case in 4359578727 is addressed: the helper exempts RANGE and preserves ordinary GROUP BY d union compensation.
Required checkpoint conclusions:
- Goal/correctness: non-IVM OLAP refresh reads and snapshots now share the mapping, with full LIST tuples and precise RANGE bounds; the three concerns above prevent a general correctness sign-off.
- Scope/clarity/reuse: changes are focused on MV mapping, refresh, rewrite and tests; EXPLAIN and refresh share the scope helper, and predicate construction is reused.
- Concurrency/locks: refresh workers, planners and DDL can overlap. Sorted context table locks and locked default walks are retained. The known post-context live partition lookup remains covered by existing thread 4220192710; no distinct new lock-order issue was established. The descriptor-miss check fails before overwrite, rather than automatically realigning in that call.
- Lifecycle: new contexts/scopes are statement/task/batch local; no new static initialization or resource lifecycle issue found.
- Configuration: no new settings. Existing sync-window behavior changes for default LIST tables; session union/grace decisions remain runtime reads.
- Compatibility: no new persisted format or wire contract; snapshot compatibility handling remains intact, and canonical LIST ordering is shared by current descriptors and stored items.
- Thrift/Protobuf: no changes; branch field-ID comparison is inapplicable.
- Parallel paths: checked OLAP/external/IVM, SELF_MANAGE, COMPLETE/partition refresh/EXPLAIN, LIST/RANGE/default/NULL/precision, aggregate-on-nonaggregate, multi-PCT and self-join compensation. Exceptions are identified above.
- Conditions/errors: missing full-key slots reject rewrite; unmatched descriptors and invalid key/type assumptions fail explicitly. Original exact-key overlap and cloud-grace preload cases are covered by current logic.
- Test coverage: inspected all eight changed Java test files and four regression scripts. Fixtures cover tuple scope, defaults, fractional bounds, component pruning, grace changes and exact-key aggregate fallback. The known default-insert fixture weakness (4142955465) remains: COMPLETE does not prove dependency-driven partition selection. The residual grouping-expression case lacks a regression.
- Expected results: inspected all three changed goldens against their fixtures; restored outer-join rows and fractional/default/group totals are consistent. Generation history and runtime results were not independently verified.
- Observability: existing task/query/snapshot logging and optimizer failure reasons cover the changed decisions; no additional essential metric identified.
- Persistence/failover: new data structures are transient; existing committed-batch snapshot publication and replay structures are reused. No new EditLog schema is introduced.
- Writes/atomicity: overwrite transaction/swap and partial-progress publication remain unchanged; freshness-record correctness is limited by the concerns above. Crash and concurrent schedules were not executed.
- FE/BE propagation: no new transmitted variable or protocol field.
- Performance: reviewed indexed component grouping, canonical ordering, early RANGE filtering, factored RANGE predicates and bulk cloud version reads; no additional verified performance defect found.
No additional user focus was supplied. This was a complete static review of the supplied changed-file list and authoritative diff, including related control flow and tests. No builds, tests, source changes or runtime/performance experiments were performed, as required by the runner contract. Runtime reproduction, concurrent schedules and golden provenance remain validation limits, not unclassified review candidates.
Review converged after 2 round(s).
Existing P0/P1 findings confirmed for this head: #68648 (comment), #68648 (comment), #68648 (comment)
A group expression over the partition column puts several of its values in one group, so an uncovered base partition and a retained MV partition answer the same group even when their keys differ: date_trunc(d,'year') puts January 1 beneath the MV's January 2, and the union of the two finished aggregates is two rows for the year. The query is answered from the base table instead. Key changes: - Detect a query group-by expression over a PCT column (not the bare column) and fall back for an uncovered-only compensation; the key-overlap fallback still covers the bare-column LIST case. Unit Test: - Regression: mtmv_p0/test_mtmv_base_partition_read_scope's group_split_expr - fe/fe-core: 70 mtmv / nereids-mv test classes, 971 tests
|
run buildall |
|
/review |
There was a problem hiding this comment.
Request changes at e57d08a. Three new verified findings are inline: a mixed-stale computed-group bypass and a grouping-sets bypass can return duplicate aggregate groups; overlap detection also rejects a safe LIST compensation by counting an MV partition already scheduled for removal.
Two existing P1s remain applicable and are retained without duplicate inline comments: external PCT read/record mismatch and absent IVM PCT input scope. The external read still uses the MV domain while recording only mapped partitions; an absent IVM PCT input still gets an ordinary scan and an empty partition record. The exact earlier raw LIST aggregate split and safe RANGE fallback examples are fixed on this head; the new inline cases exercise distinct remaining mechanisms.
Critical checkpoint conclusions:
- Goal/data correctness: ordinary non-IVM OLAP reads now follow their recorded mapping, including full LIST tuples, NULLs, defaults and fractional RANGE bounds. The new aggregate failures and two existing exceptions prevent an overall correctness endorsement.
- Scope/reuse: refresh, mapping, rewrite and canonical LIST descriptors support the same invariant; shared scope/literal/grouping helpers are reused. No unrelated production changes identified.
- Concurrency/locks: no new threads. Context construction uses existing sorted table locks; new default metadata walks use read locks. The post-context partition lookup window is existing thread context (#4220192710); no additional lock-order defect verified.
- Lifecycle/static initialization: new maps/sets and snapshot preparation are invocation-local. Cloud version reads precede the single grace decision; that same set drives comparison preloading. No new static initialization dependency.
- Configuration: none added. Existing sync-window, grace, batch-size and union settings are read per invocation; default-LIST window/rewrite behavior changes are explicit.
- Compatibility: no persisted format change. Canonical descriptors are computed consistently on both alignment sides; stored LIST keys and SQL serialization stay unchanged. Internal command/rule signatures are propagated.
- Thrift/protobuf: unchanged; no field-ID or enum review required.
- Parallel paths: COMPLETE/PARTITIONS/EXPLAIN, LIST/RANGE/default, OLAP/external/IVM, self joins, grace/force-consistent, union enabled/disabled and aggregate-on-nonaggregate paths were traced. External/IVM exceptions are the existing P1s above.
- Conditions/error handling: empty scoped input emits FALSE, absent scope keeps the MV domain, descriptor misses fail before overwrite, missing bound key slots refuse compensation. Aggregate safety conditions still have the inline defects.
- Test coverage: changed JUnit and regression artifacts cover grouping/canonical collisions, pruning, scope/precision/defaults, grace/preload and union behavior. The three new mixed-stale/grouping-sets/removed-overlap cases are missing. The default dependency regression still uses unconditional COMPLETE, so it cannot isolate freshness selection; this is already covered by the existing test thread. Deterministic metadata interleavings and merged external/IVM mutation cases also lack direct coverage.
- Expected results: changed scope/group totals and partition names agree with fixture data; repaired grace-big rows match base-only before blocks. Golden generation provenance was not independently established.
- Observability: descriptor errors name the MV/partition; rewrite fallbacks record plan/partition reasons and version/preload failures log warnings. No additional metric requirement identified.
- Persistence/transactions: existing overwrite and snapshot publication ordering remains; no new journal record or replay format.
- Writes/atomicity: refresh retains batch INSERT OVERWRITE and completed-group accounting; descriptor misses fail before erasing data. No new crash leak or deadlock verified statically.
- FE/BE propagation: predicates use existing expression analysis/translation; no new cross-process variable.
- Performance: bulk grace version reads, one descriptor per component, early RANGE filtering and factored RANGE predicates bound redundant work. The new removed-partition overlap finding loses a safe optimization; conservative full-key LIST grouping fallback is also existing safe-union-thread context.
- Omission sweep: all 30 changed paths and related invocation chains were reviewed. No additional unclassified candidate remains after the bounded reviewer passes.
No additional user focus was supplied. This was a static review only: no builds, tests, cluster runs or source modifications were performed. Runtime behavior, race reproduction and reported author test runs were not independently validated.
Review converged after 2 round(s).
Existing P0/P1 findings confirmed for this head: #68648 (comment), #68648 (comment)
| boolean splitsAggregateGroup = queryPlan.anyMatch(LogicalAggregate.class::isInstance) | ||
| && needBaseTableUnion | ||
| && (!uncoveredNarrowedBasePartitions.isEmpty() | ||
| || (!needRemoveMvPartitions && groupsByExpressionOverPctColumn(queryPlan, mtmv))); |
There was a problem hiding this comment.
[P1] Keep computed-group protection when only some MV partitions are removed
Removing a stale partition does not imply that every MV partition is removed. Extend the new group_split_expr fixture with a populated p_stale={(2037-01-01,3)}, refresh the MV, then write p_stale. Query d IN ('2020-01-01','2020-01-02','2037-01-01') GROUP BY date_trunc(d,'year'). The fresh merged component still holds Jan 2 with v=2; uncovered p_old holds Jan 1 with v=1, while the separate 2037 MV partition is stale.
calcInvalidPartitions removes only the 2037 partition and compensates {p_old,p_stale}. Raw dates do not overlap, so uncoveredNarrowedBasePartitions is empty; needRemoveMvPartitions=true then disables this expression check. canUnionRewrite accepts the PCT slot inside date_trunc, producing this reduced plan:
UnionAll
Aggregate(year(d),sum(s)) -> Scan MV[fresh merged component]
Aggregate(year(d),sum(v)) -> Filter(query AND keys(old,stale)) -> Scan t
The two branches emit separate year-2020 rows with 2 and 1, instead of one row with 3. Unlike #4356885832's fixed same-date case, this bypass needs computed groups and a simultaneous stale removal. Check overlap against the MV partitions that actually remain, even when another partition is removed, or merge aggregate states above the union.
There was a problem hiding this comment.
Fixed in a3cdee32fd1.
The bypass was the !needRemoveMvPartitions term, and the whole expression test has left the base rule: what the union of two finished aggregates needs is a question about the grouping, not about which partitions were dropped, and the aggregate rule is where the grouping is known. AbstractMaterializedViewAggregateRule#canUnionRewrite now requires every column the MV is partitioned on to appear in the grouping by itself, and AbstractMaterializedViewRule consults it for any compensation union -- needBaseTableUnion alone, with removal beside the point -- so a stale partition removed beside the compensation no longer switches the question off.
Your shape is the new group_split_stale_expr fixture of mtmv_p0/test_mtmv_base_partition_read_scope: p_kept={(2020-01-01,1),(2038-01-01,1)}, p_bridge={(2038-01-01,2)}, p_stale={(2037-01-01,3)}, p_old={(2020-01-01,2)}, 2 YEAR window, MV PARTITION BY (d) AS SELECT d, SUM(v) FROM t GROUP BY d; after the refresh, 100 more go into p_stale. The query over the three dates grouped by date_trunc(d,'year') removes only the 2037 MV partition and compensates {p_old, p_stale}, so raw dates do not overlap and the key-overlap set stays empty. Before the change the plan carried your UnionAll and year 2020 came out twice (2 and 1); the explain now asserts no UNION and the golden is one row per year, 2020-01-01 -> 3 and 2037-01-01 -> 104.
| List<? extends Expression> groupByExpressions = ExpressionUtils.shuttleExpressionWithLineage( | ||
| aggregate.get().getGroupByExpressions(), queryPlan); | ||
| for (Expression expression : groupByExpressions) { | ||
| if (expression instanceof SlotReference) { |
There was a problem hiding this comment.
[P1] Check each grouping set before treating slot grouping keys as disjoint
The normalized group-by list hides grouping sets that omit the partition column. With the new group_split_expr fixture unchanged and fresh, query SELECT d,SUM(v) FROM group_split_expr_base WHERE d IN ('2020-01-01','2020-01-02') GROUP BY GROUPING SETS ((d),()). The MV holds Jan 2 (v=2), and uncovered p_old holds Jan 1 (v=1). Their raw dates do not overlap.
NormalizeRepeat builds aggregate keys [d, grouping_id] and preserves sourceRepeat=[[d],[]]. Both keys remain SlotReferences through lineage, so this loop skips them and returns false. canUnionRewrite accepts d, while aggregateRewriteByView explicitly rewrites the repeat over the MV. The reduced result is:
UnionAll
Aggregate[sets(d),()] -> Repeat -> Scan MV // (Jan2,2), (NULL,2)
Aggregate[sets(d),()] -> Repeat -> Filter(keys(old)) -> Scan t
// (Jan1,1), (NULL,1)
There is no aggregate above this union, so it returns two grand totals instead of (NULL,3). This is separate from the stale-partition bypass: no partition removal or computed group expression is needed. Inspect sourceRepeat/each grouping set and reject compensation when a set can span both branches, or combine its aggregate states correctly.
There was a problem hiding this comment.
Fixed in a3cdee32fd1.
Done as you describe, and in the place that can ask it: the method at line 547 you pointed at is gone, and AbstractMaterializedViewAggregateRule#canUnionRewrite now takes the grouping apart itself. When the aggregate has a source Repeat it walks getGroupingSets() and requires, of every set, that each column the MV is partitioned on appears by itself; the normalized key list is no longer trusted to say which set a slot came from. Your [[d],[]] case fails on the empty set, and a set that groups only by an expression over the column fails on the same rule as the plain case.
group_split_sets in mtmv_p0/test_mtmv_base_partition_read_scope is the case: the group_split_expr_base fixture unchanged, SELECT d, SUM(v) ... WHERE d IN ('2020-01-01','2020-01-02') GROUP BY GROUPING SETS ((d),()). Before, the plan was the UnionAll of two Aggregate -> Repeat -> ... branches and the grand total came out as two rows; the explain now asserts no UNION, and the golden is the three rows with one total: \N 3, 2020-01-01 1, 2020-01-02 2.
nereids_rules_p0/mv/grouping_sets and mtmv_p0/test_multi_pct_rollup_mtmv / test_rollup_partition_mtmv / test_create_rollup_mtmv pass with it, so the tightening is limited to the shapes that can actually span the two branches.
| continue; | ||
| } | ||
| uncoveredNarrowedBasePartitions.addAll(overlappingUncoveredListPartitions( | ||
| entry.getKey(), pctInfo, entry.getValue(), rewrittenPlanUsePartitionNameSet, |
There was a problem hiding this comment.
[P2] Exclude removed MV partitions from the retained-key overlap check
This passes every selected MV partition, including those already scheduled for removal, as the helper's retainedMvPartitions. For LIST(d,k), use p_stale={(2020-01-01,1),(2037-01-01,1)}, p_bridge={(2037-01-01,2)}, p_old={(2020-01-01,2)}, and p_fresh={(2038-01-01,1)} with a 2 YEAR sync limit. Populate both retained components, refresh PARTITION BY(d) AS SELECT d,SUM(v) FROM t GROUP BY d, then write p_stale. Query dates 2020-01-01 and 2038-01-01, grouped by d.
The invalid merged {2020,2037} MV partition is removed; only the fresh 2038 partition remains. The base branch reads old plus the stale bucket (the query filter excludes bridge's 2037 rows), so the valid target is:
UnionAll
Scan MV[fresh 2038 aggregate]
Aggregate(d,sum(v)) -> Filter(d=2020 AND compensation keys) -> Scan t
Its groups are disjoint. But the helper sees 2020 in the stale MV partition, flags p_old, and the aggregate guard rejects the rewrite before removal. Unlike #4359578727's fixed RANGE case, this false overlap comes specifically from including a removed LIST component. Subtract the MV removal set before computing retained keys and add this partial-staleness case.
There was a problem hiding this comment.
Fixed in a3cdee32fd1.
calcInvalidPartitions now computes the set the overlap is projected against as the plan's MV partitions minus everything the removal pass has named:
Set<String> removedMvPartitions = Sets.newHashSet();
mvPartitionNeedRemoveNameMap.values().forEach(removedMvPartitions::addAll);
Set<String> retainedMvPartitions = Sets.difference(rewrittenPlanUsePartitionNameSet, removedMvPartitions);A partition scheduled for removal is read back through the compensation branch, so its keys are the compensation's own and cannot be the retained group an uncovered partition would meet.
partial_stale in mtmv_p0/test_mtmv_base_partition_read_scope is your case: p_stale={(2020-01-01,1),(2037-01-01,1)}, p_bridge={(2037-01-01,2)}, p_old={(2020-01-01,2)}, p_fresh={(2038-01-01,1)}, 2 YEAR window, both retained components populated before the refresh and p_stale written after it. The merged {2020,2037} MV partition is removed and only the fresh 2038 partition remains, so p_old is no longer flagged for a 2020 it shares with a partition that is going away; SELECT d, SUM(v) ... WHERE d IN ('2020-01-01','2038-01-01') GROUP BY d keeps the union, and its explain asserts both UNION and partial_stale_mv (rows 2020-01-01 103, 2038-01-01 0). Note it needs the bare-column grouping too: this is the case where the partition keys are the only thing separating the two branches' groups.
TPC-H: Total hot run time: 28720 ms |
TPC-DS: Total hot run time: 150332 ms |
ClickBench: Total hot run time: 24.37 s |
FE UT Coverage ReportIncrement line coverage |
FE Regression Coverage ReportIncrement line coverage |
TPC-H: Total hot run time: 28535 ms |
TPC-DS: Total hot run time: 150682 ms |
ClickBench: Total hot run time: 24.37 s |
FE UT Coverage ReportIncrement line coverage |
FE Regression Coverage ReportIncrement line coverage |
3 similar comments
FE Regression Coverage ReportIncrement line coverage |
FE Regression Coverage ReportIncrement line coverage |
FE Regression Coverage ReportIncrement line coverage |
The test that decides whether the compensation may union two finished aggregates was applied to the plan shape rather than to the grouping, and it was skipped whenever a stale MV partition was removed beside the compensation. Both let a group be answered twice: date_trunc(d,'year') puts the days of a year in one group, so the MV's part of it and the base branch's come out as two rows, and a grouping set that leaves the partition column out -- the () total of ROLLUP/CUBE -- puts every partition value in one group. Move the question into the aggregate rule's canUnionRewrite, where the grouping the branches are built on is asked directly and where partition removal is beside the point. Key changes: - canUnionRewrite requires every column the MV is partitioned on to appear in the grouping by itself, and requires that of each grouping set when the aggregate reads a source Repeat; an expression over a partition column no longer counts, since it can put several of its values in one group. - The base rule keeps only the LIST key-overlap fallback, so a windowed RANGE MV whose uncovered partition answers groups of its own keeps the union rewrite it had before. - The retained MV partitions the uncovered-key overlap is computed against exclude those the staleness pass has already scheduled for removal, whose keys the compensation branch reads anyway. Unit Test: - Regression: mtmv_p0/test_mtmv_base_partition_read_scope's group_split_sets, group_split_stale_expr, partial_stale; 24 suites of nereids_rules_p0/mv (partition_union_rewrite, union_all_compensate, union_rewrite, union_rewrite_grace_big, grouping_sets, agg_with_roll_up, agg_without_roll_up, date_trunc) and mtmv_p0 (multi-pct / rollup / partition-limit) -- all pass - union_all_compensate's query5 now expects the rewrite to be declined: it groups by to_date(data_date), which cannot be shown to keep the two branches' groups apart, and its rows are unchanged because the base table gives the same answer - fe/fe-core: 7 mv exploration test classes, 152 tests
|
run buildall |
|
/review |
There was a problem hiding this comment.
Reviewed exact head a3cdee3 against the supplied aggregate diff. Changes are requested because two existing P1 correctness findings still apply; neither is reposted. One new P2 performance regression is reported inline.
Existing blockers independently revalidated:
- 4271997436: external PCT inputs still read the MV key domain while recording only the sync-window mapping. The newly admitted mixed LIST component can materialize an unrecorded Paimon partition; removing it can leave the MV falsely synchronized.
- 4278938895: an IVM PCT table absent from a batch mapping still lacks an explicit empty reset scope. COMPLETE can read its excluded partition, while snapshot comparison and subsequent incremental scope omit it, preserving a row after DELETE.
The earlier full-tuple, default LIST, fractional precision, descriptor-miss, partition identity, grace eligibility, uncovered-input, aggregate-overlap, computed-group/stale-removal, and grouping-set findings were rechecked. Their reported failure paths are now fixed or refused by upstream rewrite eligibility. The new inline finding is distinct: equivalent PCT join columns are treated as independent grouping dimensions.
Applicable code-review skill checkpoints:
| Checkpoint | Conclusion |
|---|---|
| Goal and evidence | Non-IVM OLAP reads now share the recorded mapping and overlapping LIST descriptors form connected components. External/IVM exceptions remain the two correctness blockers. Added FE/regression tests supply static coverage; no runtime verification was performed. |
| Scope and clarity | Changes are confined to FE MTMV mapping, refresh, rewrite, and related tests. Shared descriptor and typed whole-key helpers avoid duplicated implementations. |
| Concurrency, threads, protected state | Scheduler refresh and partition DDL share metadata; alignment/context creation retain ID-sorted base-table read locks. New default-partition walks use table read locks with finally unlock. New maps are local; no thread or shared cache is added. |
| Work under locks and lock order | Added locked work is metadata traversal; cloud grace uses bulk version reads. No new lock-order inversion was substantiated. Existing external metadata planning under caller locks remains. |
| Lifecycle and static initialization | Contexts and batch scopes retain existing task/call lifetimes and cleanup. No C++ static initialization or ownership change. |
| Configuration and dynamic changes | No configuration added. Existing sync-window, grace, and union settings are read on refresh/query; narrowed-window behavior is covered. |
| Compatibility and rolling upgrades | No persisted field, storage format, wire schema, or exported function change. Single-PCT snapshot compatibility handling remains. |
| Thrift/protobuf IDs | No gensrc changes; inapplicable. |
| Parallel paths | OLAP/external, IVM/non-IVM, refresh/EXPLAIN, full/incremental, default LIST, and cloud/shared-nothing paths were traced. The two existing blockers are verified implementation defects, not unreviewed scope. |
| Special conditions and failure handling | Empty mapped OLAP reads become FALSE; undescribed MV partitions fail before overwrite, including mixed PCT batches. Default-bearing tables bypass the window consistently. Grace requires a written partition version. Missing bound compensation slots refuse rewrite. |
| Tests, negative cases, modularity | Unit cases cover component chains/order/pruning, typed/empty reads, grace/preload/coverage and compensation inputs. Regressions cover default dependencies, fractional bounds and aggregate/grouping-set cases. The inline safe-equijoin case and existing external/IVM failures need coverage. |
| Changed expected results | Fixture arithmetic supports the new ordered goldens; changed grace results restore rows present in base-result blocks. Static review cannot prove golden-generation provenance or test execution. |
| Observability | Existing task/query identifiers, partition-aware errors, and rewrite failure reasons cover changed decisions. No new service or critical metric surface was added. |
| Transactions, persistence, failover | Snapshot capture remains before reads and recording after successful overwrite; existing publication/journaling is retained. No new EditLog format. Dependency completeness still fails in the two existing P1s. |
| Writes, atomicity, crashes | Descriptor misses fail before writes; committed batch progress uses existing overwrite/snapshot lifecycle. No new transaction or crash/leak/deadlock path was substantiated. |
| FE-BE variables | No new variable or sending path. |
| Performance | Bulk cloud versions, single merged descriptor per component, early RANGE pruning, and factored RANGE predicates avoid known repeated work. The new allMatch grouping gate unnecessarily loses safe multi-PCT union rewrites, as reported inline. |
| Other invariants | Whole-key predicates retain NULL and full column types; version and snapshot identity checks were inspected. BE allocation accounting and MoW delete bitmap code are unchanged. |
No additional user focus was supplied. All 32 changed files and related invocation paths were covered by the main scan and independent reviewers. Stale-only multi-input compensation was rechecked against partition-key equality/whole-component guarantees; the speculative mixed-pair issue was dismissed. LIST rollup ordering is unreachable through currently supported expression DDL, which requires RANGE.
Limits: this was static-only review: no builds, tests, clusters, or source edits. Some result fixtures can also pass on base-only plans, so those results alone do not prove rewrite selection. The descriptor-miss guard safely fails the current task; despite its comment, it does not itself retry alignment inside that task. No unverified candidate is included inline.
Review converged after 2 round(s).
Existing P0/P1 findings confirmed for this head: #68648 (comment), #68648 (comment)
| List<BaseColInfo> pctInfos) { | ||
| List<? extends Expression> groupingShuttledExpressions = | ||
| ExpressionUtils.shuttleExpressionWithLineage(groupingExpressions, queryPlan); | ||
| return pctInfos.stream().allMatch(colInfo -> groupingShuttledExpressions.stream() |
There was a problem hiding this comment.
[P2] Accept equivalent PCT join keys when checking group disjointness
pctInfos lists the base columns that derive the same MV partition key: PartitionIncrementMaintainer follows A.d = B.d, so a two-PCT inner join records both A.d and B.d. Requiring every one as a grouping slot rejects the ordinary GROUP BY A.d; lineage substitution does not add the equivalent B.d.
For aligned daily RANGE partitions old, kept, and an unrelated third partition, refresh PARTITION BY(a_d) AS SELECT a.d AS a_d, SUM(a.v) AS s FROM A a JOIN B b ON a.d=b.d GROUP BY a.d, then write to old. Query only old and kept, grouping by a.d. The safe compensation is:
UnionAll
Scan MV[kept]
Aggregate(a.d, SUM(a.v))
Join(a.d=b.d)
Scan A[old]
Scan B[old]
Its groups are disjoint by a.d, but this allMatch fails on B.d, discarding the MV and scanning/aggregating the retained base data too. Unlike the earlier RANGE overlap guard, this rejection remains in the new aggregate helper. Check the actual partition dimension using join equivalence, retain the per-grouping-set check, and add a two-PCT partial-union regression grouped by one join side.
TPC-H: Total hot run time: 28271 ms |
TPC-DS: Total hot run time: 150603 ms |
ClickBench: Total hot run time: 24.4 s |
What problem does this PR solve?
Issue Number: N/A
Related PR: #68646
Trace issue: #65418
Problem Summary:
A refresh of one MV partition reads each base table through the key range of that MV partition, while the snapshot it records next to the read names the base partitions the MV partition's mapping keeps. The two sets are not the same one: the mapping is what
MTMV.calculatePartitionMappingsanswers, andpartition_sync_limitfilters it, so a refresh reads base partitions the snapshot does not describe -- and the rows they put in the MV partition stay there unaccounted for.That difference is what a silent base partition change hides behind.
DROP PARTITION,TRUNCATE PARTITION,REPLACE PARTITIONandRECOVER PARTITIONremove visible rows through metadata rather than through row binlog entries, so a refresh has to be told about them. An IVM MV is: its per-partition baseline is invalidated, which reaches every MV partition that reads the changed one. A plain MV is judged by its snapshot, and when the changed partition is one the snapshot never named, the base partition set is back to what the snapshot does name once the change is done: the MV partition is judged synchronized, the transparent rewrite serves the rows of a partition the base table no longer has, and no refresh plans that partition again.This PR makes the read and the record the same set.
MTMVTask#mappedBasePartitionsdecides which base partitions a batch may read, from the same mapping the snapshots taken next to that read are generated from, andUpdateMvByPartitionCommandbuilds each base table's read predicate from exactly those partitions:enable_insert_stricton by default, the refresh fails where it used to succeed. A scoped list table keeps the whole-key pin alone, since a list partition's key is a value the MV partition's key set was written from.BaseTableInfo, so a table is identified by the table it names rather than by which of the MV's partition info and the mapping's own two objects it was read from. Keying by the object would have made "the lookup missed" read as "this table may not be read at all", which would empty the MV quietly.ADD PARTITIONclaims a key without moving the rows the default partition already holds under it, so which side of a window a row is on is not something a predicate on the partition columns can say. The MV partition keeps the whole of what its key range covers and is recorded with every partition of it, plus the default one. Leaving the window is also what makes such a table's partitions meet at the MV's column -- one partition holding several keys of it, one of them shared with another partition -- soMTMVRelatedPartitionDescTransferGeneratormerges descs whose keys meet into the one partition whose keys are their union, which is the partition the windowed pipeline already produced for the shape; an MV cannot have two partitions repeating a key.Only the olap base tables of a non-IVM MV are scoped: the changes this answers are olap DDL, and an IVM MV already invalidates its baseline for them and scopes the reads of its delta in the IVM rewrite.
With the read equal to the record, a base partition that was read is one the snapshot names, so a later silent change to it is what the sync check compares against, and the partition is planned for a rebuild instead of being served stale.
The read is pinned where the predicates are written. The union compensation writes its whole-key predicate into a plan that is already bound and that nothing binds again, so it builds it from the slots that plan's relation reads; a partition column the relation does not read has no column to write the key against, and that case refuses the rewrite rather than narrowing it to a guess. The same path asks for a NULL list key as
IS NULL, through the conversion the scoped read uses, instead ofk IN (NULL), which is UNKNOWN even for a NULL row.EXPLAIN REFRESH ... COMPLETEbuilds its scope from the same mapping, through the helper the refresh also uses (MTMVPartitionUtil#mappedBasePartitions), so it describes the read the refresh makes rather than the MV partitions' own key ranges.Two places decide whether an MV may answer a query at all, and both are now closed against an MV that stands on less than the query reads:
enable_materialized_view_union_rewriteoff there is no compensation for the partitions that are not valid, so a non-empty subset of them was enough for the MV to answer. It now answers only when every MV partition the query's base partitions are mapped from is valid.Two readings that were taken twice are now taken once. The sync check compares exactly the MV partitions grace does not answer for, so the snapshots prepared for it are those of that same set -- the one grace is decided on, once and after the version read, rather than a second reading of the grace window. The read is not free to reorder it: on cloud it writes each partition's cached mtime back (0 where the meta service has none), so a partition whose timestamp was inside the window when it was selected can be outside it once the read is done and grace does not answer for it after all -- the second reading had already left it out of the preload, and the comparison then asked for a snapshot nobody had prepared. A partition inside its window that no refresh has written is in that same position, for the same reason. The check reports the unprepared snapshot as "not synchronized", which drops the partition and, with every partition in that state, the MV. And the check that a batch's MV partitions are described by some base partition is taken for every batch: an external table does not make the olap side of a mixed batch any less scoped, so its presence is no reason to skip the check.
Behaviour changed: Yes
partition_sync_limit, an MV partition no longer keeps the rows of base partitions the window no longer keeps: the refresh reads the partitions the window keeps rather than the whole key range of the MV partition, which can be wider. Queries that also read a base partition the window has expired are answered from the base table by the union rewrite, which is what that path is for and is on by default (enable_materialized_view_rewrite).ADD PARTITIONclaimed stays in the MV partition it was read into, and a change to any partition of that range reaches it.enable_materialized_view_union_rewriteoff, a query whose MV partitions are only partly valid is answered from the base table instead of reading the partitions that are not valid as they are.date_trunc(d, 'year'),to_date(d), which put several partitions' values in one group), by aROLLUP/CUBE/GROUPING SETSset that leaves a partition column out (the()total puts every partition value in one group), or over a view partitioned on several columns whose grouping holds fewer than all of them (two of its partitions can share one value of one of them).Release note
With
partition_sync_limit, a materialized view partition no longer keeps the rows of base partitions the sync window no longer keeps. A refresh reads exactly the base partitions the view's partition is recorded with, so what the partition holds and what it is reported as synchronized with agree; a query that also reads expired base partitions is answered from the base table by the union rewrite, as before. A base table that has a list partition's default partition is not windowed: the MV partition keeps every base partition its key range covers, so nothing read into it is left unrecorded. Withenable_materialized_view_union_rewritedisabled, a view no longer answers a query when part of the MV partitions the query's base partitions are mapped from are not valid. A materialized view over a base table whose list partitions include a default partition is no longer used to answer queries by transparent rewrite (those queries read the base table); the view itself is created, refreshed and queried directly as before. An aggregate query grouped by an expression over the view's partition column, or by a grouping set that leaves it out, is read from the base table instead of by the union compensation; its rows are unchanged.Check List (For Author)
UpdateMvByPartitionCommandTest(a scoped table is read from the partitions named, a table named with none is read as nothing, a partition predicate follows the key at the position the MV's partition column has, the read of a scoped range table ends at the MV partition's own upper bound, on the case of a DATETIME(3) partition ending at 2025-01-01 00:00:00.123 under a year MV, whose row at 2025-01-01 00:00:00.100 no partition of the batch can take, and a compensation filter is written with the relation's own slots),MTMVRewriteUtilTest(a partition in grace does not answer for a base partition the query reads that no MV partition is mapped from -- and the partition grace would have admitted is the one the coverage has to reject, so the case now gives it a written version and reaches grace at all; without the union rewrite every mapped partition has to be valid; an MV over a table with a default list partition offers no partition at all; a partition inside its window that no refresh has written has the snapshots prepared for the comparison the sync check then makes -- the case the second reading of the grace window had left out; and a partition the version read itself moves out of the window is compared with the snapshots of the same set),MTMVRelatedPartitionDescGeneratorTest(descs whose keys meet are one MV partition holding all of their keys -- a chain of three, and two tables of a multi-table MV -- naming each table's partitions; a key set is the same desc whether it was merged or not -- including two keys that hash alike (Aa,BB) written down in either order -- so ADD PARTITION over a covered key cannot re-derive the MV partition; a pruned query keeps the merged desc; disjoint ones keep their own),MTMVRelatedPartitionDescSyncLimitGeneratorTest(a table with a default list partition is kept out of the window, a range table is not),MTMVTaskTest,MTMVTest,MTMVPartitionUtilTest,ExplainRefreshMTMVCommandTest-- all pass. Each case was checked to fail with the change it covers reverted.test_mtmv_base_partition_read_scope(daily base partitions with a year MV partition and apartition_sync_limitwindow; a list partitioned base table whose partitions hold several keys of the MV's partition column; a default-partition table under a window, where a row the default partition holds under a key a laterADD PARTITIONclaimed stays in the MV partition, which is recorded with that partition; a default partition whose rows are read for the MV partition they belong to; a two-table join whose second table has a default partition; a key written with milliseconds; a key only a default partition holds, whose MV holds it not at all and whose query is answered whole from the base table; and the aggregate compensation's own declinations -- a group split by an uncovered partition whose keys meet a retained MV partition's, a grouping by an expression over the partition column, a grouping set that leaves it out, a stale partition re-read through the base branch beside an expression grouping, and a key that met only a partition the staleness pass had already scheduled for removal, which stays union-rewritten), themtmv_p0suites around list partitions, roll-up, partition limits, recreate and column changes, andmtmv_p0/ivm's twopartition_sync_limitsuites -- all pass, and the suites' other cases keep their goldens.test_mtmv_base_partition_read_scopecarries the read's own boundary as well: aDATETIME(3)range partition of[2024-01-01 00:00:00.123, 2025-01-01 00:00:00.123)under a year-granularity MV with a row at2025-01-01 00:00:00.100-- measured both ways, the refresh task fails withInsert has filtered data in strict mode / no partition for this tuplefor that row when the MV partition's own range is left out of the read. A second case,range_ms_direct, puts a direct range MV over that partition: it reads the same partition with the MV's own bound written at the base column's scale, and measured against a build that writes that bound from the partition key the refresh succeeds and the row at2024-02-01 00:00:00.100is not in the MV at all -- the golden names it.nereids_rules_p0/mv/union_rewrite_grace_bigfailed on a cloud cluster before the snapshots fix (Partition snapshot was not preparedout of the sync check, which took the MV out of the rewrite) and passes after it;nereids_rules_p0/mv/partition_union_rewrite/partition_mv_rewriteandmtmv_p0/test_iceberg_mtmv-- the refresh of a view over an external table, the shape the undescribed-partition check is about -- pass as well. Innereids_rules_p0/mv/union_all_compensate, query5_0 groups a daily-range view byto_date(data_date), the shape the compensation now declines, so its expectation moved frommv_rewrite_successtomv_rewrite_fail: the query is answered from the base table and its golden rows are unchanged.partition_sync_limitand for the incremental materialized view's refresh modes are tracked in the design document-change list, which records the exact locations and wording.