Code review comment: REMEMBER THAT: `Plan` and `Expression` in Nereids is **_IMMUTABLE_**. we should not mod scan's attribute. Instead, we need to generate a new `LogicalOlapScan` with pruned partitionIds.

Code under review:
public Rule build() {
        return logicalFilter(logicalOlapScan()).thenApply(ctx -> {
            LogicalFilter<LogicalOlapScan> filter = ctx.root;
            LogicalOlapScan scan = filter.child();
            Expression predicate = filter.getPredicates();
            List<Expression> expressionList = ExpressionUtils.extractConjunction(predicate);
            OlapTable table = scan.getTable();
            Set<String> partitionColumnNameSet = Utils.execWithReturnVal(table::getPartitionColumnNames);
            PartitionInfo partitionInfo = table.getPartitionInfo();
            
            
            if (partitionColumnNameSet.isEmpty() || !partitionInfo.getType().equals(PartitionType.RANGE)) {
                return ctx.root;
            }
            
            Map<String, ColumnRange> columnNameToRange = Maps.newHashMap();
            for (String colName : partitionColumnNameSet) {
                ColumnRange columnRange = createColumnRange(colName, expressionList);
                columnNameToRange.put(colName, columnRange);
            }

            Map<Long, PartitionItem> keyItemMap = partitionInfo.getIdToItem(false);
            PartitionPruner partitionPruner = new RangePartitionPrunerV2(keyItemMap,
                    partitionInfo.getPartitionColumns(), columnNameToRange);
            Collection<Long> selectedPartitionId =  Utils.execWithReturnVal(partitionPruner::prune);
            scan.getSelectedPartitionIds().retainAll(selectedPartitionId);
            return ctx.root;
        }).toRule(RuleType.PARTITION_PRUNE);
    }

Line commented on: return ctx.root;

Revised code:
public Rule build() {
        return logicalFilter(logicalOlapScan()).when(p -> !p.child().isPartitionPruned()).thenApply(ctx -> {
            LogicalFilter<LogicalOlapScan> filter = ctx.root;
            LogicalOlapScan scan = filter.child();
            Expression predicate = filter.getPredicates();
            OlapTable table = scan.getTable();
            Set<String> partitionColumnNameSet = Utils.execWithReturnVal(table::getPartitionColumnNames);
            PartitionInfo partitionInfo = table.getPartitionInfo();
            
            
            if (partitionColumnNameSet.isEmpty() || !partitionInfo.getType().equals(PartitionType.RANGE)) {
                return ctx.root;
            }
            List<Expression> expressionList = ExpressionUtils.extractConjunction(predicate);
            
            Map<String, ColumnRange> columnNameToRange = Maps.newHashMap();
            for (String colName : partitionColumnNameSet) {
                ColumnRange columnRange = createColumnRange(colName, expressionList);
                columnNameToRange.put(colName, columnRange);
            }

            Map<Long, PartitionItem> keyItemMap = partitionInfo.getIdToItem(false);
            PartitionPruner partitionPruner = new RangePartitionPrunerV2(keyItemMap,
                    partitionInfo.getPartitionColumns(), columnNameToRange);
            Collection<Long> selectedPartitionId = Utils.execWithReturnVal(partitionPruner::prune);
            LogicalOlapScan rewrittenScan =
                    scan.withSelectedPartitionId(new ArrayList<>(selectedPartitionId));
            return new LogicalFilter<>(filter.getPredicates(), rewrittenScan);
        }).toRule(RuleType.OLAP_SCAN_PARTITION_PRUNE);
    }