github-actions[bot] commented on code in PR #69292: URL: https://github.com/apache/doris/pull/69292#discussion_r4335062586
########## fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/BindScoreToOlapScan.java: ########## @@ -0,0 +1,324 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.doris.nereids.rules.rewrite; + +import org.apache.doris.nereids.exceptions.AnalysisException; +import org.apache.doris.nereids.jobs.JobContext; +import org.apache.doris.nereids.properties.OrderKey; +import org.apache.doris.nereids.trees.expressions.Alias; +import org.apache.doris.nereids.trees.expressions.Cast; +import org.apache.doris.nereids.trees.expressions.ComparisonPredicate; +import org.apache.doris.nereids.trees.expressions.Expression; +import org.apache.doris.nereids.trees.expressions.GreaterThan; +import org.apache.doris.nereids.trees.expressions.GreaterThanEqual; +import org.apache.doris.nereids.trees.expressions.LessThan; +import org.apache.doris.nereids.trees.expressions.LessThanEqual; +import org.apache.doris.nereids.trees.expressions.Match; +import org.apache.doris.nereids.trees.expressions.NamedExpression; +import org.apache.doris.nereids.trees.expressions.Slot; +import org.apache.doris.nereids.trees.expressions.functions.scalar.Score; +import org.apache.doris.nereids.trees.expressions.functions.scalar.Search; +import org.apache.doris.nereids.trees.plans.Plan; +import org.apache.doris.nereids.trees.plans.logical.LogicalAggregate; +import org.apache.doris.nereids.trees.plans.logical.LogicalApply; +import org.apache.doris.nereids.trees.plans.logical.LogicalCTEAnchor; +import org.apache.doris.nereids.trees.plans.logical.LogicalCTEProducer; +import org.apache.doris.nereids.trees.plans.logical.LogicalFilter; +import org.apache.doris.nereids.trees.plans.logical.LogicalLimit; +import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan; +import org.apache.doris.nereids.trees.plans.logical.LogicalProject; +import org.apache.doris.nereids.trees.plans.logical.LogicalRecursiveUnionAnchor; +import org.apache.doris.nereids.trees.plans.logical.LogicalRecursiveUnionProducer; +import org.apache.doris.nereids.trees.plans.logical.LogicalRelation; +import org.apache.doris.nereids.trees.plans.logical.LogicalSetOperation; +import org.apache.doris.nereids.trees.plans.logical.LogicalSink; +import org.apache.doris.nereids.trees.plans.logical.LogicalSort; +import org.apache.doris.nereids.trees.plans.logical.LogicalSubQueryAlias; +import org.apache.doris.nereids.trees.plans.logical.LogicalView; +import org.apache.doris.nereids.trees.plans.visitor.CustomRewriter; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Bind score() to the olap scan that computes it. + * + * <p>score() is the relevance of a row for the MATCH and SEARCH predicates in the WHERE clause of + * its SELECT, and only the olap scan that evaluates those predicates computes it. So score() is + * supported in a SELECT that reads one olap table and has a MATCH or SEARCH predicate in its WHERE + * clause: + * <ul> + * <li>in the WHERE clause, as one minimum score: score() > x or score() >= x;</li> + * <li>in the select list, window functions and ORDER BY of a SELECT that does not aggregate.</li> + * </ul> + * A SELECT in a subquery, view, CTE or set operation has to ORDER BY score() with a LIMIT, which keeps + * the predicates of the outer query away from its scan. Any other use of score() is rejected. + * + * <p>This rule runs before subquery aliases are removed. It adds a score column to the scan, which + * scores every row the scan returns, and replaces the score() calls of the SELECT with it. + * PushDownScoreRangeIntoOlapScan and PushDownScoreTopNIntoOlapScan push the minimum score and a + * TopN on the score into the scan. + */ +public class BindScoreToOlapScan implements CustomRewriter { + public static final String UNSUPPORTED_SELECT_ERROR = "score() can only be used in a SELECT that reads one " + + "table and has a MATCH or SEARCH predicate in its WHERE clause"; + public static final String NESTED_SELECT_ERROR = "score() in a subquery, view, CTE or set operation " + + "requires ORDER BY score() with LIMIT"; + public static final String AGGREGATE_ERROR = "score() can only be used in the WHERE clause of a SELECT " + + "with GROUP BY, aggregate functions or DISTINCT"; + public static final String HAVING_OR_QUALIFY_ERROR = "score(), MATCH and SEARCH cannot be used in HAVING " + + "or QUALIFY of a SELECT that calls score()"; + public static final String ONE_RANGE_ERROR = "Only one score() range predicate is supported in WHERE clause"; + public static final String RANGE_FORM_ERROR = "score() predicate in WHERE clause must be in the form of " + + "'score() > literal' or 'score() >= literal'"; + public static final String TOP_LEVEL_RANGE_ERROR = "score() predicate must be a top-level AND condition " + + "in WHERE clause"; + + @Override + public Plan rewriteRoot(Plan plan, JobContext jobContext) { + if (!plan.anyMatch(node -> callsScore((Plan) node))) { + return plan; + } + return bindSelect(plan, true); + } + + /** The score column that this rule added to the scan. */ + public static Optional<Slot> findScoreSlot(LogicalOlapScan scan) { + return scan.getVirtualColumns().stream() + .filter(column -> column instanceof Alias && ((Alias) column).child() instanceof Score) + .map(NamedExpression::toSlot) + .findFirst(); + } + + private Plan bindSelect(Plan root, boolean outermost) { + Plan plan = bindNestedSelects(root); + if (!selectCallsScore(plan)) { + return plan; + } + List<Plan> path = pathToScan(plan); + if (path.isEmpty()) { + throw new AnalysisException(UNSUPPORTED_SELECT_ERROR); + } + check(path, outermost); + return bind(path); + } + + private Plan bindNestedSelects(Plan plan) { + ImmutableList.Builder<Plan> newChildren = ImmutableList.builderWithExpectedSize(plan.arity()); + boolean changed = false; + for (int i = 0; i < plan.arity(); i++) { + Plan child = plan.child(i); + Plan newChild = startsSelect(plan, i) ? bindSelect(child, false) : bindNestedSelects(child); + changed |= newChild != child; + newChildren.add(newChild); + } + return changed ? plan.withChildren(newChildren.build()) : plan; + } + + // Subqueries, views, CTEs and the inputs of set operations are SELECTs of their own, a table + // alias is not. + private static boolean startsSelect(Plan parent, int childIndex) { + if (parent instanceof LogicalSubQueryAlias) { + return !(parent.child(0) instanceof LogicalRelation); + } + return parent instanceof LogicalView || parent instanceof LogicalCTEProducer + || parent instanceof LogicalSetOperation || parent instanceof LogicalRecursiveUnionAnchor + || parent instanceof LogicalRecursiveUnionProducer + || parent instanceof LogicalCTEAnchor && childIndex == 0 + || parent instanceof LogicalApply && childIndex == 1; + } + + private static boolean selectCallsScore(Plan plan) { + if (callsScore(plan)) { + return true; + } + for (int i = 0; i < plan.arity(); i++) { + if (!startsSelect(plan, i) && selectCallsScore(plan.child(i))) { + return true; + } + } + return false; + } + + private static boolean callsScore(Plan plan) { + return plan.getExpressions().stream().anyMatch(expression -> expression.containsType(Score.class)); + } + + // The plan nodes from the root of the SELECT down to its olap scan; empty when the SELECT reads + // anything else than one olap table, e.g. a join, a subquery or a lateral view. + private static List<Plan> pathToScan(Plan root) { + List<Plan> path = new ArrayList<>(); + Plan node = root; + while (!(node instanceof LogicalOlapScan)) { + path.add(node); + if (node instanceof LogicalCTEAnchor) { + node = node.child(1); + } else if (node instanceof LogicalSink || node instanceof LogicalLimit || node instanceof LogicalSort + || node instanceof LogicalProject || node instanceof LogicalFilter + || node instanceof LogicalAggregate + || node instanceof LogicalSubQueryAlias && !startsSelect(node, 0)) { + node = node.child(0); + } else { + return ImmutableList.of(); + } + } + path.add(node); + return path; + } + + private static void check(List<Plan> path, boolean outermost) { + // The WHERE clause is the filters right above the scan and its table alias. + int clause = path.size() - 2; + if (path.get(clause) instanceof LogicalSubQueryAlias) { + clause--; + } + List<Expression> where = new ArrayList<>(); + for (; clause >= 0 && path.get(clause) instanceof LogicalFilter; clause--) { + where.addAll(((LogicalFilter<?>) path.get(clause)).getConjuncts()); + } + if (where.stream().noneMatch(conjunct -> conjunct.containsType(Match.class, Search.class))) { + throw new AnalysisException(UNSUPPORTED_SELECT_ERROR); + } + List<Expression> ranges = where.stream() + .filter(conjunct -> conjunct.containsType(Score.class)) + .collect(Collectors.toList()); + if (ranges.size() > 1) { + throw new AnalysisException(ONE_RANGE_ERROR); + } + ranges.forEach(BindScoreToOlapScan::checkRange); + + List<Plan> aboveWhere = path.subList(0, clause + 1); + boolean aggregates = aboveWhere.stream().anyMatch(LogicalAggregate.class::isInstance); + Set<Slot> scoreAliases = aboveWhere.stream() + .filter(LogicalProject.class::isInstance) + .flatMap(project -> ((LogicalProject<?>) project).getProjects().stream()) + .filter(project -> project.containsType(Score.class)) + .map(NamedExpression::toSlot) + .collect(ImmutableSet.toImmutableSet()); + for (Plan node : aboveWhere) { + if (node instanceof LogicalFilter && ((LogicalFilter<?>) node).getConjuncts().stream().anyMatch( + conjunct -> conjunct.containsType(Score.class, Match.class, Search.class) + || conjunct.getInputSlots().stream().anyMatch(scoreAliases::contains))) { + throw new AnalysisException(HAVING_OR_QUALIFY_ERROR); + } + if (aggregates && callsScore(node)) { + throw new AnalysisException(AGGREGATE_ERROR); + } + } + if (!outermost && !ordersByScoreWithLimit(aboveWhere)) { Review Comment: [P1] Reject correlated scored SELECTs whose isolation limit is removed A correlated EXISTS passes this check, for example an outer `(b.c1 MATCH 'red' OR b.c1 MATCH 'green')` with: ```sql EXISTS (SELECT 1 FROM test_score_filter_usage a WHERE a.c1 = b.c1 AND a.c1 MATCH 'apple' AND score() > 0.6 ORDER BY score() DESC LIMIT 1) ``` Its reduced RHS is `Limit -> Sort(score) -> Project -> Filter(correlation, MATCH apple, score > 0.6) -> Scan`. `SubExprAnalyzer` accepts correlated EXISTS with zero offset, but the subsequent `EliminateLimitUnderApply` and `EliminateSortUnderApply` remove the very barrier this check relies on. `ExistsApplyToJoin` then produces a semi join; `InferPredicates`/`InferPredicateByReplace.visitOr` copy the outer MATCH disjunction across the same-type c1 equality onto the scored scan. Those inferred MATCHs now add BM25 before the minimum is applied. With the usage fixture, both apple-only scores are 0.4700036 and should fail 0.6; the added red/green scores make them pass, changing EXISTS results. Reject correlated scored SELECTs or preserve their scoring isolation through decorrelation, and add this regression. This is a distinct correlated-subquery path from the existing HAVING/QUALIFY and score-key runtime-filter threads. ########## fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/BindScoreToOlapScan.java: ########## @@ -0,0 +1,324 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.doris.nereids.rules.rewrite; + +import org.apache.doris.nereids.exceptions.AnalysisException; +import org.apache.doris.nereids.jobs.JobContext; +import org.apache.doris.nereids.properties.OrderKey; +import org.apache.doris.nereids.trees.expressions.Alias; +import org.apache.doris.nereids.trees.expressions.Cast; +import org.apache.doris.nereids.trees.expressions.ComparisonPredicate; +import org.apache.doris.nereids.trees.expressions.Expression; +import org.apache.doris.nereids.trees.expressions.GreaterThan; +import org.apache.doris.nereids.trees.expressions.GreaterThanEqual; +import org.apache.doris.nereids.trees.expressions.LessThan; +import org.apache.doris.nereids.trees.expressions.LessThanEqual; +import org.apache.doris.nereids.trees.expressions.Match; +import org.apache.doris.nereids.trees.expressions.NamedExpression; +import org.apache.doris.nereids.trees.expressions.Slot; +import org.apache.doris.nereids.trees.expressions.functions.scalar.Score; +import org.apache.doris.nereids.trees.expressions.functions.scalar.Search; +import org.apache.doris.nereids.trees.plans.Plan; +import org.apache.doris.nereids.trees.plans.logical.LogicalAggregate; +import org.apache.doris.nereids.trees.plans.logical.LogicalApply; +import org.apache.doris.nereids.trees.plans.logical.LogicalCTEAnchor; +import org.apache.doris.nereids.trees.plans.logical.LogicalCTEProducer; +import org.apache.doris.nereids.trees.plans.logical.LogicalFilter; +import org.apache.doris.nereids.trees.plans.logical.LogicalLimit; +import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan; +import org.apache.doris.nereids.trees.plans.logical.LogicalProject; +import org.apache.doris.nereids.trees.plans.logical.LogicalRecursiveUnionAnchor; +import org.apache.doris.nereids.trees.plans.logical.LogicalRecursiveUnionProducer; +import org.apache.doris.nereids.trees.plans.logical.LogicalRelation; +import org.apache.doris.nereids.trees.plans.logical.LogicalSetOperation; +import org.apache.doris.nereids.trees.plans.logical.LogicalSink; +import org.apache.doris.nereids.trees.plans.logical.LogicalSort; +import org.apache.doris.nereids.trees.plans.logical.LogicalSubQueryAlias; +import org.apache.doris.nereids.trees.plans.logical.LogicalView; +import org.apache.doris.nereids.trees.plans.visitor.CustomRewriter; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Bind score() to the olap scan that computes it. + * + * <p>score() is the relevance of a row for the MATCH and SEARCH predicates in the WHERE clause of + * its SELECT, and only the olap scan that evaluates those predicates computes it. So score() is + * supported in a SELECT that reads one olap table and has a MATCH or SEARCH predicate in its WHERE + * clause: + * <ul> + * <li>in the WHERE clause, as one minimum score: score() > x or score() >= x;</li> + * <li>in the select list, window functions and ORDER BY of a SELECT that does not aggregate.</li> + * </ul> + * A SELECT in a subquery, view, CTE or set operation has to ORDER BY score() with a LIMIT, which keeps + * the predicates of the outer query away from its scan. Any other use of score() is rejected. + * + * <p>This rule runs before subquery aliases are removed. It adds a score column to the scan, which + * scores every row the scan returns, and replaces the score() calls of the SELECT with it. + * PushDownScoreRangeIntoOlapScan and PushDownScoreTopNIntoOlapScan push the minimum score and a + * TopN on the score into the scan. + */ +public class BindScoreToOlapScan implements CustomRewriter { + public static final String UNSUPPORTED_SELECT_ERROR = "score() can only be used in a SELECT that reads one " + + "table and has a MATCH or SEARCH predicate in its WHERE clause"; + public static final String NESTED_SELECT_ERROR = "score() in a subquery, view, CTE or set operation " + + "requires ORDER BY score() with LIMIT"; + public static final String AGGREGATE_ERROR = "score() can only be used in the WHERE clause of a SELECT " + + "with GROUP BY, aggregate functions or DISTINCT"; + public static final String HAVING_OR_QUALIFY_ERROR = "score(), MATCH and SEARCH cannot be used in HAVING " + + "or QUALIFY of a SELECT that calls score()"; + public static final String ONE_RANGE_ERROR = "Only one score() range predicate is supported in WHERE clause"; + public static final String RANGE_FORM_ERROR = "score() predicate in WHERE clause must be in the form of " + + "'score() > literal' or 'score() >= literal'"; + public static final String TOP_LEVEL_RANGE_ERROR = "score() predicate must be a top-level AND condition " + + "in WHERE clause"; + + @Override + public Plan rewriteRoot(Plan plan, JobContext jobContext) { + if (!plan.anyMatch(node -> callsScore((Plan) node))) { + return plan; + } + return bindSelect(plan, true); + } + + /** The score column that this rule added to the scan. */ + public static Optional<Slot> findScoreSlot(LogicalOlapScan scan) { + return scan.getVirtualColumns().stream() + .filter(column -> column instanceof Alias && ((Alias) column).child() instanceof Score) + .map(NamedExpression::toSlot) + .findFirst(); + } + + private Plan bindSelect(Plan root, boolean outermost) { + Plan plan = bindNestedSelects(root); + if (!selectCallsScore(plan)) { + return plan; + } + List<Plan> path = pathToScan(plan); + if (path.isEmpty()) { + throw new AnalysisException(UNSUPPORTED_SELECT_ERROR); + } + check(path, outermost); + return bind(path); + } + + private Plan bindNestedSelects(Plan plan) { + ImmutableList.Builder<Plan> newChildren = ImmutableList.builderWithExpectedSize(plan.arity()); + boolean changed = false; + for (int i = 0; i < plan.arity(); i++) { + Plan child = plan.child(i); + Plan newChild = startsSelect(plan, i) ? bindSelect(child, false) : bindNestedSelects(child); + changed |= newChild != child; + newChildren.add(newChild); + } + return changed ? plan.withChildren(newChildren.build()) : plan; + } + + // Subqueries, views, CTEs and the inputs of set operations are SELECTs of their own, a table + // alias is not. + private static boolean startsSelect(Plan parent, int childIndex) { + if (parent instanceof LogicalSubQueryAlias) { + return !(parent.child(0) instanceof LogicalRelation); + } + return parent instanceof LogicalView || parent instanceof LogicalCTEProducer + || parent instanceof LogicalSetOperation || parent instanceof LogicalRecursiveUnionAnchor + || parent instanceof LogicalRecursiveUnionProducer + || parent instanceof LogicalCTEAnchor && childIndex == 0 + || parent instanceof LogicalApply && childIndex == 1; + } + + private static boolean selectCallsScore(Plan plan) { + if (callsScore(plan)) { + return true; + } + for (int i = 0; i < plan.arity(); i++) { + if (!startsSelect(plan, i) && selectCallsScore(plan.child(i))) { + return true; + } + } + return false; + } + + private static boolean callsScore(Plan plan) { + return plan.getExpressions().stream().anyMatch(expression -> expression.containsType(Score.class)); + } + + // The plan nodes from the root of the SELECT down to its olap scan; empty when the SELECT reads + // anything else than one olap table, e.g. a join, a subquery or a lateral view. + private static List<Plan> pathToScan(Plan root) { + List<Plan> path = new ArrayList<>(); + Plan node = root; + while (!(node instanceof LogicalOlapScan)) { + path.add(node); + if (node instanceof LogicalCTEAnchor) { + node = node.child(1); + } else if (node instanceof LogicalSink || node instanceof LogicalLimit || node instanceof LogicalSort + || node instanceof LogicalProject || node instanceof LogicalFilter + || node instanceof LogicalAggregate + || node instanceof LogicalSubQueryAlias && !startsSelect(node, 0)) { + node = node.child(0); + } else { + return ImmutableList.of(); + } + } + path.add(node); + return path; + } + + private static void check(List<Plan> path, boolean outermost) { + // The WHERE clause is the filters right above the scan and its table alias. + int clause = path.size() - 2; + if (path.get(clause) instanceof LogicalSubQueryAlias) { + clause--; + } + List<Expression> where = new ArrayList<>(); + for (; clause >= 0 && path.get(clause) instanceof LogicalFilter; clause--) { + where.addAll(((LogicalFilter<?>) path.get(clause)).getConjuncts()); + } + if (where.stream().noneMatch(conjunct -> conjunct.containsType(Match.class, Search.class))) { + throw new AnalysisException(UNSUPPORTED_SELECT_ERROR); + } + List<Expression> ranges = where.stream() + .filter(conjunct -> conjunct.containsType(Score.class)) + .collect(Collectors.toList()); + if (ranges.size() > 1) { + throw new AnalysisException(ONE_RANGE_ERROR); + } + ranges.forEach(BindScoreToOlapScan::checkRange); + + List<Plan> aboveWhere = path.subList(0, clause + 1); + boolean aggregates = aboveWhere.stream().anyMatch(LogicalAggregate.class::isInstance); + Set<Slot> scoreAliases = aboveWhere.stream() + .filter(LogicalProject.class::isInstance) + .flatMap(project -> ((LogicalProject<?>) project).getProjects().stream()) + .filter(project -> project.containsType(Score.class)) + .map(NamedExpression::toSlot) + .collect(ImmutableSet.toImmutableSet()); + for (Plan node : aboveWhere) { + if (node instanceof LogicalFilter && ((LogicalFilter<?>) node).getConjuncts().stream().anyMatch( + conjunct -> conjunct.containsType(Score.class, Match.class, Search.class) + || conjunct.getInputSlots().stream().anyMatch(scoreAliases::contains))) { + throw new AnalysisException(HAVING_OR_QUALIFY_ERROR); + } + if (aggregates && callsScore(node)) { + throw new AnalysisException(AGGREGATE_ERROR); + } + } + if (!outermost && !ordersByScoreWithLimit(aboveWhere)) { + throw new AnalysisException(NESTED_SELECT_ERROR); + } + } + + // score() > x or score() >= x, also written x < score() or x <= score(). A cast of score() to + // FLOAT or DOUBLE keeps its value. + private static void checkRange(Expression conjunct) { + if (!(conjunct instanceof ComparisonPredicate)) { + throw new AnalysisException(TOP_LEVEL_RANGE_ERROR); + } + ComparisonPredicate comparison = (ComparisonPredicate) conjunct; + boolean scoreAbove = (comparison instanceof GreaterThan || comparison instanceof GreaterThanEqual) + && isScore(comparison.left()) && isConstant(comparison.right()); + boolean scoreBelow = (comparison instanceof LessThan || comparison instanceof LessThanEqual) + && isScore(comparison.right()) && isConstant(comparison.left()); + if (!scoreAbove && !scoreBelow) { + throw new AnalysisException(RANGE_FORM_ERROR); + } + } + + private static boolean isScore(Expression expression) { + if (expression instanceof Cast) { + return expression.getDataType().isFloatLikeType() && isScore(((Cast) expression).child()); + } + return expression instanceof Score; + } + + private static boolean isConstant(Expression expression) { + return expression.getInputSlots().isEmpty() && !expression.containsType(Score.class); + } + + // A TopN on the score above the SELECT, so that no outer predicate is pushed into the scan. + private static boolean ordersByScoreWithLimit(List<Plan> aboveWhere) { + for (int i = 0; i + 1 < aboveWhere.size(); i++) { + if (aboveWhere.get(i) instanceof LogicalLimit && aboveWhere.get(i + 1) instanceof LogicalSort) { Review Comment: [P2] Recognize a score TopN through the analysis cleanup projection This otherwise supported nested SELECT is rejected: ```sql SELECT * FROM ( SELECT id, score() AS s FROM test_score_filter_usage WHERE c1 MATCH 'apple' HAVING c2 = 'sweet' ORDER BY s DESC LIMIT 2 ) x; ``` Because c2 is absent from the select list, `FillUpMissingSlots.processWithSortHavingProject` appends it below HAVING and restores the original output with a projection above Sort. At this early rewrite stage the subtree is: ```text Limit(2) Project(id, s) Sort(s DESC) Filter(c2 = 'sweet') Project(id, score() AS s, c2) Filter(c1 MATCH 'apple') -> Scan ``` The ordinary HAVING predicate passes the other checks, and the score sort/limit still isolates the scan, but requiring adjacent Limit/Sort returns false and throws `NESTED_SELECT_ERROR`. Skip transparent output-restoring projections when recognizing the protective score TopN and cover this analyzed shape. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
