131131import java .util .stream .Collectors ;
132132import java .util .stream .IntStream ;
133133
134+ import javax .annotation .Nullable ;
135+
134136public class OlapScanNode extends AbstractOlapTableScanNode {
135137 private static final Logger LOG = LogManager .getLogger (OlapScanNode .class );
136138
@@ -316,7 +318,12 @@ public List<Expr> getBucketExprs() {
316318
317319 @ Override
318320 public int getBucketNums () {
319- int bucketNum = olapTable .getDefaultDistributionInfo ().getBucketNum ();
321+ DistributionInfo distInfo = olapTable .getDefaultDistributionInfo ();
322+ if (distInfo .getType () == DistributionInfo .DistributionInfoType .RANGE ) {
323+ return getRangeDistributionBucketNums (distInfo );
324+ }
325+ // HASH path.
326+ int bucketNum = distInfo .getBucketNum ();
320327 if (getSelectedPartitionIds ().size () <= 1 ) {
321328 for (Long pid : getSelectedPartitionIds ()) {
322329 bucketNum = olapTable .getPartition (pid ).getDistributionInfo ().getBucketNum ();
@@ -325,6 +332,54 @@ public int getBucketNums() {
325332 return bucketNum ;
326333 }
327334
335+ private int getRangeDistributionBucketNums (DistributionInfo distInfo ) {
336+ RangeColocateScanDispatch dispatch = RangeColocateScanDispatch .forTable (olapTable );
337+ if (dispatch != null ) {
338+ // getBucketNums() is invoked from ExecutionFragment.getOrCreateColocatedAssignment
339+ // only when BackendSelectorFactory has chosen a colocate-dispatch path. Verify
340+ // alignment HERE, after the dispatch decision: a misaligned ColocateRangeMgr
341+ // would silently produce wrong join results under colocate dispatch.
342+ // Non-colocate scans go through NormalBackendSelector and never reach this point.
343+ dispatch .requireAligned (getSelectedPhysicalPartitions (), index .indexMetaId );
344+ return dispatch .bucketCount ();
345+ }
346+ // Range distribution without a colocate group: RangeDistributionInfo always
347+ // reports 1 (one tablet per partition by design).
348+ return distInfo .getBucketNum ();
349+ }
350+
351+ /**
352+ * Returns the physical partitions that actually contribute scan tablets.
353+ * Optimizer path: derived from {@link #partitionToScanTabletMap} (skip
354+ * empty entries — those were pruned out). Legacy path: every sub-partition
355+ * of every selected logical partition.
356+ */
357+ private List <PhysicalPartition > getSelectedPhysicalPartitions () {
358+ List <PhysicalPartition > result = new ArrayList <>();
359+ if (partitionToScanTabletMap != null ) {
360+ for (Map .Entry <Long , List <Long >> entry : partitionToScanTabletMap .entrySet ()) {
361+ if (entry .getValue () == null || entry .getValue ().isEmpty ()) {
362+ continue ;
363+ }
364+ PhysicalPartition physicalPartition = olapTable .getPhysicalPartition (entry .getKey ());
365+ if (physicalPartition != null ) {
366+ result .add (physicalPartition );
367+ }
368+ }
369+ return result ;
370+ }
371+ for (Long partitionId : selectedPartitionIds ) {
372+ Partition partition = olapTable .getPartition (partitionId );
373+ if (partition == null ) {
374+ continue ;
375+ }
376+ for (PhysicalPartition physicalPartition : partition .getSubPartitions ()) {
377+ result .add (physicalPartition );
378+ }
379+ }
380+ return result ;
381+ }
382+
328383 @ Override
329384 public Optional <List <BucketProperty >> getBucketProperties () throws StarRocksException {
330385 return Optional .empty ();
@@ -763,6 +818,11 @@ private void computeTabletInfo() throws StarRocksException {
763818 */
764819 Preconditions .checkState (scanBackendIds .size () == 0 );
765820 Preconditions .checkState (scanTabletIds .size () == 0 );
821+ DistributionInfo distInfo = olapTable .getDefaultDistributionInfo ();
822+ RangeColocateScanDispatch dispatch = null ;
823+ if (distInfo .getType () == DistributionInfo .DistributionInfoType .RANGE ) {
824+ dispatch = RangeColocateScanDispatch .forTable (olapTable );
825+ }
766826 for (Long partitionId : selectedPartitionIds ) {
767827 final Partition partition = olapTable .getPartition (partitionId );
768828
@@ -783,16 +843,37 @@ private void computeTabletInfo() throws StarRocksException {
783843 scanTabletIds .addAll (allTabletIds );
784844 }
785845
786- for (int i = 0 ; i < allTabletIds .size (); i ++) {
787- tabletId2BucketSeq .put (allTabletIds .get (i ), i );
788- }
846+ fillTabletId2BucketSeq (dispatch , selectedIndex , allTabletIds );
789847 totalTabletsNum += selectedIndex .getTablets ().size ();
790848 selectedTabletsNum += tablets .size ();
791849 addScanRangeLocations (partition , physicalPartition , selectedIndex , tablets , localBeId );
792850 }
793851 }
794852 }
795853
854+ /**
855+ * Populates {@link #tabletId2BucketSeq} for one {@link MaterializedIndex}.
856+ * Range-colocate scans use the bucket sequence supplied by the dispatch
857+ * facade when alignment holds; everything else (HASH, range non-colocate,
858+ * or transiently unaligned range colocate) falls back to position-based
859+ * bucketSeq. The dispatch-time alignment guard fires later in
860+ * {@link #getBucketNums()} for the colocate-dispatch path.
861+ */
862+ private void fillTabletId2BucketSeq (@ Nullable RangeColocateScanDispatch dispatch ,
863+ MaterializedIndex selectedIndex ,
864+ List <Long > allTabletIds ) {
865+ if (dispatch != null ) {
866+ Map <Long , Integer > rangeColocateMap = dispatch .computeBucketSeq (selectedIndex );
867+ if (rangeColocateMap != null ) {
868+ tabletId2BucketSeq .putAll (rangeColocateMap );
869+ return ;
870+ }
871+ }
872+ for (int i = 0 ; i < allTabletIds .size (); i ++) {
873+ tabletId2BucketSeq .put (allTabletIds .get (i ), i );
874+ }
875+ }
876+
796877 public void checkIfScanRangeNumSafe (long scanRangeSize ) {
797878 long totalPartitionNum = 0 ;
798879 long totalTabletsNum = 0 ;
0 commit comments