@@ -445,17 +445,6 @@ private List<RefreshAttemptType> buildAttempts(RefreshRequest request, boolean c
445445 if (shouldUseCompleteForInitialIvmRefresh (containsOneRowRelation )) {
446446 return Lists .newArrayList (RefreshAttemptType .COMPLETE );
447447 }
448- // A base table the plan scans without a usable stream makes every other attempt fail: the
449- // incremental rewrite reads the stream of every table it scans, and a partition refresh reads
450- // those of the PCT tables it realigns as well. Only the COMPLETE attempt reconciles streams, so
451- // it is the one that has to run. Deciding it here keeps the attempt that cannot succeed -- and
452- // the baseline barrier a partition refresh writes before it starts -- out of the way altogether.
453- if (request .allowFallback && mtmv .isIvm () && hasUnusableIvmStream ()) {
454- ivmFallbackReason = IvmFailureReason .STREAM_UNSUPPORTED .name ();
455- LOG .warn ("IVM stream is unusable, mv={}, taskId={}. Continuing with COMPLETE refresh." ,
456- mtmv .getName (), getTaskId ());
457- return Lists .newArrayList (RefreshAttemptType .COMPLETE );
458- }
459448 List <RefreshAttemptType > attempts = Lists .newArrayList ();
460449 switch (request .refreshMode ) {
461450 case AUTO :
@@ -500,6 +489,24 @@ private List<RefreshAttemptType> buildAttempts(RefreshRequest request, boolean c
500489 default :
501490 throw new IllegalStateException ("Unsupported refresh mode: " + request .refreshMode );
502491 }
492+ // A base table the plan scans without a usable stream makes the incremental attempt fail: its
493+ // rewrite reads the stream of every table it scans. Only COMPLETE reconciles streams, so a request
494+ // that would try the incremental path first goes there directly, and the attempt that cannot
495+ // succeed -- with the baseline barrier it writes before it starts -- stays out of the way.
496+ //
497+ // Only that attempt is judged here. A partition refresh reads a narrower set, and how narrow
498+ // depends on the partitions it plans: a PCT table that no refreshed partition's mapping names
499+ // keeps its place in the plan as an ordinary scan, and an excluded trigger table is never read
500+ // through a stream. It checks its own scope once it has planned (see
501+ // hasUnusableIvmStreamForPartitions), so judging it here by the whole plan would send a partition
502+ // refresh that would have worked to COMPLETE.
503+ if (request .allowFallback && mtmv .isIvm () && attempts .contains (RefreshAttemptType .IVM )
504+ && hasUnusableIvmStream ()) {
505+ ivmFallbackReason = IvmFailureReason .STREAM_UNSUPPORTED .name ();
506+ LOG .warn ("IVM stream is unusable, mv={}, taskId={}. Continuing with COMPLETE refresh." ,
507+ mtmv .getName (), getTaskId ());
508+ return Lists .newArrayList (RefreshAttemptType .COMPLETE );
509+ }
503510 return attempts ;
504511 }
505512
@@ -810,6 +817,22 @@ private boolean executePartitionBasedRefresh(MTMVRefreshContext refreshContext,
810817 }
811818 this .needRefreshPartitions = partitionPlan .partitions ;
812819 this .refreshMode = generateRefreshMode (needRefreshPartitions );
820+ // This attempt now knows which partitions it refreshes, and with them which streams it reads.
821+ // Judged here rather than in buildAttempts because only the plan knows that scope, and judged
822+ // before the NOT_REFRESH return below so that a fallback-capable request still reaches the only
823+ // attempt that reconciles streams. Falling back here continues to COMPLETE, which is what repairs
824+ // them; a request that may not fall back fails instead of quietly refreshing less than it asked.
825+ if (mtmv .isIvm ()
826+ && hasUnusableIvmStreamForPartitions (partitionPlan .context , needRefreshPartitions )) {
827+ if (!request .allowFallback ) {
828+ throw new JobException ("IVM stream is unusable for the partitions of this refresh, mv="
829+ + mtmv .getName ());
830+ }
831+ ivmFallbackReason = IvmFailureReason .STREAM_UNSUPPORTED .name ();
832+ LOG .warn ("IVM stream is unusable for the partitions this refresh plans, mv={}, taskId={}. "
833+ + "Continuing with COMPLETE refresh." , mtmv .getName (), getTaskId ());
834+ return false ;
835+ }
813836 if (refreshMode == MTMVTaskRefreshMode .NOT_REFRESH ) {
814837 return true ;
815838 }
@@ -909,24 +932,90 @@ private boolean hasUnusableIvmStream() {
909932 // rewriter and the full refresh take the streams of the plan's scans -- so judging them would
910933 // rebuild an MV whose refresh had nothing wrong with it.
911934 for (BaseTableInfo baseTableInfo : relation .getBaseTablesOneLevelAndFromView ()) {
912- OlapTable baseTable ;
913- try {
914- baseTable = (OlapTable ) MTMVUtil .getTable (baseTableInfo );
915- } catch (Exception e ) {
916- LOG .warn ("Cannot resolve base table {} of mv={}" , baseTableInfo , mtmv .getName (), e );
935+ OlapTable baseTable = resolveIvmBaseTable (baseTableInfo );
936+ if (baseTable == null ) {
937+ continue ;
938+ }
939+ if (MTMVPartitionUtil .isTableExcluded (excluded ,
940+ new TableNameInfo (baseTable .getFullQualifiers ()))) {
941+ continue ;
942+ }
943+ if (usableIvmStream (mvDb , baseTable ) == null ) {
944+ return true ;
945+ }
946+ }
947+ return false ;
948+ }
949+
950+ /**
951+ * Whether a stream this partition refresh will read is missing or unusable.
952+ *
953+ * <p>The set is narrower than the plan, and which tables are in it depends on the partitions being
954+ * refreshed: a PCT table that no refreshed partition's mapping names keeps its place in the plan as
955+ * an ordinary scan, so its stream is never read, and a table outside the plan's own tables is not
956+ * scanned at all. Judging the whole plan instead sends a partition refresh that would have worked to
957+ * a full rebuild whenever such an unread stream is missing.
958+ */
959+ private boolean hasUnusableIvmStreamForPartitions (MTMVRefreshContext context ,
960+ List <String > mvPartitionNames ) {
961+ Database mvDb = (Database ) mtmv .getDatabase ();
962+ if (mvDb == null ) {
963+ // Nothing to look the streams up in, so there is nothing to decide here.
964+ return false ;
965+ }
966+ Set <TableNameInfo > excluded = mtmv .getExcludedTriggerTables ();
967+ Set <OlapTable > streamedTables = Sets .newLinkedHashSet ();
968+ Set <BaseTableInfo > pctTableInfos = Sets .newHashSet ();
969+ for (BaseColInfo pctInfo : mtmv .getMvPartitionInfo ().getPctInfos ()) {
970+ pctTableInfos .add (pctInfo .getTableInfo ());
971+ }
972+ // Every table of the plan that is not a PCT table is read through its stream: the partition
973+ // refresh gives those tables a read mode (IvmRewriteContext.full).
974+ for (BaseTableInfo baseTableInfo : relation .getBaseTablesOneLevelAndFromView ()) {
975+ if (pctTableInfos .contains (baseTableInfo )) {
917976 continue ;
918977 }
978+ OlapTable baseTable = resolveIvmBaseTable (baseTableInfo );
979+ if (baseTable != null ) {
980+ streamedTables .add (baseTable );
981+ }
982+ }
983+ // A PCT table is read through its stream only while the mapping of a refreshed partition names
984+ // it, which is exactly the mapping the reset read mode is built from.
985+ for (String mvPartitionName : mvPartitionNames ) {
986+ for (MTMVRelatedTableIf relatedTable : context .getByPartitionName (mvPartitionName ).keySet ()) {
987+ if (relatedTable instanceof OlapTable ) {
988+ streamedTables .add ((OlapTable ) relatedTable );
989+ }
990+ }
991+ }
992+ for (OlapTable baseTable : streamedTables ) {
919993 if (MTMVPartitionUtil .isTableExcluded (excluded ,
920994 new TableNameInfo (baseTable .getFullQualifiers ()))) {
921995 continue ;
922996 }
923997 if (usableIvmStream (mvDb , baseTable ) == null ) {
998+ LOG .warn ("IVM stream is unusable for the partitions this refresh plans, mv={}, baseTable={}" ,
999+ mtmv .getName (), baseTable .getName ());
9241000 return true ;
9251001 }
9261002 }
9271003 return false ;
9281004 }
9291005
1006+ /**
1007+ * A base table of the plan, or null when it cannot be resolved -- which says nothing about its stream,
1008+ * so a caller that is only judging streams skips it.
1009+ */
1010+ private OlapTable resolveIvmBaseTable (BaseTableInfo baseTableInfo ) {
1011+ try {
1012+ return (OlapTable ) MTMVUtil .getTable (baseTableInfo );
1013+ } catch (Exception e ) {
1014+ LOG .warn ("Cannot resolve base table {} of mv={}" , baseTableInfo , mtmv .getName (), e );
1015+ return null ;
1016+ }
1017+ }
1018+
9301019 /** The MV's stream for the base table, or null when it is missing or cannot be used. */
9311020 private BaseTableStream usableIvmStream (Database mvDb , OlapTable baseTable ) {
9321021 TableIf stream = mvDb .getTableNullable (
0 commit comments