diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/RewritingCostModel.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/RewritingCostModel.java index 8ebdc95851..355d3fe1e6 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/RewritingCostModel.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/RewritingCostModel.java @@ -24,19 +24,22 @@ import com.apple.foundationdb.annotation.SpotBugsSuppressWarnings; import com.apple.foundationdb.record.query.plan.RecordQueryPlannerConfiguration; import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpression; +import com.apple.foundationdb.record.query.plan.cascades.properties.PredicateCountByLevelProperty; import javax.annotation.Nonnull; import static com.apple.foundationdb.record.query.plan.cascades.properties.ExpressionCountProperty.selectCount; import static com.apple.foundationdb.record.query.plan.cascades.properties.ExpressionCountProperty.tableFunctionCount; import static com.apple.foundationdb.record.query.plan.cascades.properties.PredicateComplexityProperty.predicateComplexity; -import static com.apple.foundationdb.record.query.plan.cascades.properties.PredicateHeightProperty.predicateHeight; +import static com.apple.foundationdb.record.query.plan.cascades.properties.PredicateCountByLevelProperty.predicateCountByLevel; +import static com.apple.foundationdb.record.query.plan.cascades.properties.PredicateCountProperty.predicateCount; /** * Cost model for {@link PlannerPhase#REWRITING}. TODO To be fleshed out whe we have actual rules. */ @API(API.Status.EXPERIMENTAL) @SpotBugsSuppressWarnings("SE_COMPARATOR_SHOULD_BE_SERIALIZABLE") +@SuppressWarnings("PMD.TooManyStaticImports") public class RewritingCostModel implements CascadesCostModel { @Nonnull private final RecordQueryPlannerConfiguration configuration; @@ -72,12 +75,23 @@ public int compare(final RelationalExpression a, final RelationalExpression b) { } // - // Pick the expression where predicates have been pushed down as far as they can go + // Pick the expression where that has the least number of predicates overall // - int aPredicateHeight = predicateHeight().evaluate(a); - int bPredicateHeight = predicateHeight().evaluate(b); - if (aPredicateHeight != bPredicateHeight) { - return Integer.compare(aPredicateHeight, bPredicateHeight); + int aPredicateCount = predicateCount().evaluate(a); + int bPredicateCount = predicateCount().evaluate(b); + if (aPredicateCount != bPredicateCount) { + return Integer.compare(aPredicateCount, bPredicateCount); + } + + // + // Pick the expression where it has the higher number of predicates at a lower level + // + PredicateCountByLevelProperty.PredicateCountByLevelInfo aPredicateCountByLevel = predicateCountByLevel().evaluate(a); + PredicateCountByLevelProperty.PredicateCountByLevelInfo bPredicateCountByLevel = predicateCountByLevel().evaluate(b); + int levelByLevelComparison = PredicateCountByLevelProperty.PredicateCountByLevelInfo.compare(aPredicateCountByLevel, bPredicateCountByLevel); + if (levelByLevelComparison != 0) { + // Choose the expression that has more predicates at a lower level + return -levelByLevelComparison; } // diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/PredicateCountByLevelProperty.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/PredicateCountByLevelProperty.java new file mode 100644 index 0000000000..8a4e6baf9b --- /dev/null +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/PredicateCountByLevelProperty.java @@ -0,0 +1,146 @@ +/* + * PredicateCountByLevelProperty.java + * + * This source file is part of the FoundationDB open source project + * + * Copyright 2015-2025 Apple Inc. and the FoundationDB project authors + * + * Licensed 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 com.apple.foundationdb.record.query.plan.cascades.properties; + +import com.apple.foundationdb.record.query.plan.cascades.ExpressionProperty; +import com.apple.foundationdb.record.query.plan.cascades.Reference; +import com.apple.foundationdb.record.query.plan.cascades.SimpleExpressionVisitor; +import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpression; +import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpressionVisitor; +import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpressionWithPredicates; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; + +import javax.annotation.Nonnull; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; + +public class PredicateCountByLevelProperty implements ExpressionProperty { + private static final PredicateCountByLevelProperty PREDICATE_COUNT_BY_LEVEL = new PredicateCountByLevelProperty(); + + private PredicateCountByLevelProperty() { + // prevent outside instantiation + } + + @Nonnull + @Override + public RelationalExpressionVisitor createVisitor() { + return PredicateCountByLevelVisitor.VISITOR; + } + + public PredicateCountByLevelInfo evaluate(RelationalExpression expression) { + return createVisitor().visit(expression); + } + + @Nonnull + public static PredicateCountByLevelProperty predicateCountByLevel() { + return PREDICATE_COUNT_BY_LEVEL; + } + + public static final class PredicateCountByLevelInfo { + private final Map levelToPredicateCount; + private final int highestLevel; + + private PredicateCountByLevelInfo(Map levelToPredicateCount, int highestLevel) { + this.levelToPredicateCount = levelToPredicateCount; + this.highestLevel = highestLevel; + } + + public static PredicateCountByLevelInfo combine(Collection heightInfos, BiFunction combineFunc) { + final Map newLevelToComplexityMap = new HashMap<>(); + int heightLevel = 0; + for (PredicateCountByLevelInfo heightInfo : heightInfos) { + for (var entry : heightInfo.getLevelToPredicateCount().entrySet()) { + newLevelToComplexityMap.put(entry.getKey(), combineFunc.apply(newLevelToComplexityMap.getOrDefault(entry.getKey(), 0), entry.getValue())); + } + heightLevel = Integer.max(heightLevel, heightInfo.getHighestLevel()); + } + return new PredicateCountByLevelInfo(newLevelToComplexityMap, heightLevel); + } + + public Map getLevelToPredicateCount() { + return Collections.unmodifiableMap(levelToPredicateCount); + } + + public int getHighestLevel() { + return highestLevel; + } + + /** + * Compares two {@code PredicateHeightInfo} instances level by level. + * + * @param a the first {@code PredicateHeightInfo} to compare + * @param b the second {@code PredicateHeightInfo} to compare + * @return the value {@code 0} if {@code a} have the same number of predicates at each level as {@code b}; + * a value less than {@code 0} if a {@code a} has fewer predicates than {@code b} at a + * lower level or if {@code a} has a lower number of levels with predicates; and + * a value greater than {@code 0} if {@code b} has fewer predicates than {@code a} at a + * lower level or if {@code b} has a lower number of levels with predicates. + * @since 1.7 + */ + public static int compare(PredicateCountByLevelInfo a, PredicateCountByLevelInfo b) { + for (int currentLevel = 0; currentLevel < Integer.max(a.getHighestLevel(), b.getHighestLevel()); currentLevel++) { + var aLevelComplexity = a.getLevelToPredicateCount().getOrDefault(currentLevel, 0); + var bLevelComplexity = b.getLevelToPredicateCount().getOrDefault(currentLevel, 0); + var levelComparison = Integer.compare(aLevelComplexity, bLevelComplexity); + if (levelComparison != 0) { + return levelComparison; + } + } + return 0; + } + } + + private static final class PredicateCountByLevelVisitor implements SimpleExpressionVisitor { + private static final PredicateCountByLevelVisitor VISITOR = new PredicateCountByLevelVisitor(); + + @Nonnull + @Override + public PredicateCountByLevelInfo evaluateAtExpression(@Nonnull final RelationalExpression expression, @Nonnull final List childResults) { + var lowerLevels = PredicateCountByLevelInfo.combine(childResults, Integer::sum); + var currentLevel = childResults.stream().mapToInt(PredicateCountByLevelInfo::getHighestLevel).max().orElse(0) + 1; + var currentLevelPredicates = 0; + if (expression instanceof RelationalExpressionWithPredicates) { + currentLevelPredicates = ((RelationalExpressionWithPredicates)expression).getPredicates().size(); + } + return new PredicateCountByLevelInfo( + ImmutableMap.builder() + .putAll(lowerLevels.getLevelToPredicateCount()) + .put(currentLevel, currentLevelPredicates) + .build(), + currentLevel + ); + } + + @Nonnull + @Override + public PredicateCountByLevelInfo evaluateAtRef(@Nonnull final Reference ref, @Nonnull final List memberResults) { + if (memberResults.size() == 1) { + return Iterables.getOnlyElement(memberResults); + } + return PredicateCountByLevelInfo.combine(memberResults, Integer::max); + } + } +} diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/PredicateCountProperty.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/PredicateCountProperty.java new file mode 100644 index 0000000000..46a3917044 --- /dev/null +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/PredicateCountProperty.java @@ -0,0 +1,78 @@ +/* + * PredicateCountProperty.java + * + * This source file is part of the FoundationDB open source project + * + * Copyright 2015-2025 Apple Inc. and the FoundationDB project authors + * + * Licensed 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 com.apple.foundationdb.record.query.plan.cascades.properties; + +import com.apple.foundationdb.record.query.plan.cascades.ExpressionProperty; +import com.apple.foundationdb.record.query.plan.cascades.Reference; +import com.apple.foundationdb.record.query.plan.cascades.SimpleExpressionVisitor; +import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpression; +import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpressionVisitor; +import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpressionWithPredicates; +import com.google.common.collect.Iterables; + +import javax.annotation.Nonnull; +import java.util.List; + +public class PredicateCountProperty implements ExpressionProperty { + private static final PredicateCountProperty PREDICATE_COUNT = new PredicateCountProperty(); + + private PredicateCountProperty() { + // prevent outside instantiation + } + + @Nonnull + @Override + public RelationalExpressionVisitor createVisitor() { + return PredicateCountVisitor.VISITOR; + } + + public int evaluate(RelationalExpression expression) { + return createVisitor().visit(expression); + } + + @Nonnull + public static PredicateCountProperty predicateCount() { + return PREDICATE_COUNT; + } + + private static final class PredicateCountVisitor implements SimpleExpressionVisitor { + private static final PredicateCountVisitor VISITOR = new PredicateCountVisitor(); + + @Nonnull + @Override + public Integer evaluateAtExpression(@Nonnull final RelationalExpression expression, @Nonnull final List childResults) { + final var childPredicates = childResults.stream().mapToInt(Integer::intValue).sum(); + if (!(expression instanceof RelationalExpressionWithPredicates)) { + return childPredicates; + } + return childPredicates + ((RelationalExpressionWithPredicates)expression).getPredicates().size(); + } + + @Nonnull + @Override + public Integer evaluateAtRef(@Nonnull final Reference ref, @Nonnull final List memberResults) { + if (memberResults.size() == 1) { + return Iterables.getOnlyElement(memberResults); + } + return memberResults.stream().mapToInt(Integer::intValue).max().orElse(0); + } + } +} diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/PredicateHeightProperty.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/PredicateHeightProperty.java deleted file mode 100644 index 193253172a..0000000000 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/PredicateHeightProperty.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * PredicateHeightProperty.java - * - * This source file is part of the FoundationDB open source project - * - * Copyright 2015-2025 Apple Inc. and the FoundationDB project authors - * - * Licensed 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 com.apple.foundationdb.record.query.plan.cascades.properties; - -import com.apple.foundationdb.record.query.plan.cascades.ExpressionProperty; -import com.apple.foundationdb.record.query.plan.cascades.Reference; -import com.apple.foundationdb.record.query.plan.cascades.SimpleExpressionVisitor; -import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpression; -import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpressionVisitor; -import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpressionWithPredicates; -import com.google.common.collect.Iterables; - -import javax.annotation.Nonnull; -import java.util.Collection; -import java.util.List; - -public class PredicateHeightProperty implements ExpressionProperty { - private static final PredicateHeightProperty PREDICATE_HEIGHT = new PredicateHeightProperty(); - - private PredicateHeightProperty() { - // prevent outside instantiation - } - - @Nonnull - @Override - public RelationalExpressionVisitor createVisitor() { - return PredicateHeightVisitor.VISITOR; - } - - public int evaluate(RelationalExpression expression) { - return createVisitor().visit(expression).getPredicateHeight(); - } - - @Nonnull - public static PredicateHeightProperty predicateHeight() { - return PREDICATE_HEIGHT; - } - - public static final class PredicateHeightInfo { - private final int height; - private final int predicateHeight; - - private PredicateHeightInfo(int height, int predicateHeight) { - this.height = height; - this.predicateHeight = predicateHeight; - } - - public static PredicateHeightInfo combine(Collection heightInfos) { - int newHeight = 0; - int newPredicateHeight = 0; - for (PredicateHeightInfo heightInfo : heightInfos) { - newHeight = Math.max(newHeight, heightInfo.height); - newPredicateHeight = Math.max(newPredicateHeight, heightInfo.predicateHeight); - } - return new PredicateHeightInfo(newHeight, newPredicateHeight); - } - - public int getHeight() { - return height; - } - - public int getPredicateHeight() { - return predicateHeight; - } - } - - private static final class PredicateHeightVisitor implements SimpleExpressionVisitor { - private static final PredicateHeightVisitor VISITOR = new PredicateHeightVisitor(); - - @Nonnull - @Override - public PredicateHeightInfo evaluateAtExpression(@Nonnull final RelationalExpression expression, @Nonnull final List childResults) { - int newHeight = childResults.stream().mapToInt(PredicateHeightInfo::getHeight).max().orElse(0) + 1; - if (expression instanceof RelationalExpressionWithPredicates) { - var predicateExpression = (RelationalExpressionWithPredicates) expression; - if (!predicateExpression.getPredicates().isEmpty()) { - return new PredicateHeightInfo(newHeight, newHeight); - } - } - int newPredicateHeight = childResults.stream().mapToInt(PredicateHeightInfo::getPredicateHeight).max().orElse(0); - return new PredicateHeightInfo(newHeight, newPredicateHeight); - } - - @Nonnull - @Override - public PredicateHeightInfo evaluateAtRef(@Nonnull final Reference ref, @Nonnull final List memberResults) { - if (memberResults.size() == 1) { - return Iterables.getOnlyElement(memberResults); - } - return PredicateHeightInfo.combine(memberResults); - } - } -} diff --git a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/query/plan/cascades/RewritingCostModelTest.java b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/query/plan/cascades/RewritingCostModelTest.java new file mode 100644 index 0000000000..df73c8d231 --- /dev/null +++ b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/query/plan/cascades/RewritingCostModelTest.java @@ -0,0 +1,88 @@ +/* + * RewritingCostModelTest.java + * + * This source file is part of the FoundationDB open source project + * + * Copyright 2015-2025 Apple Inc. and the FoundationDB project authors + * + * Licensed 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 com.apple.foundationdb.record.query.plan.cascades; + +import com.apple.foundationdb.record.query.plan.RecordQueryPlannerConfiguration; +import com.apple.foundationdb.record.query.plan.cascades.expressions.SelectExpression; +import com.apple.foundationdb.record.query.plan.cascades.predicates.ExistsPredicate; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static com.apple.foundationdb.record.provider.foundationdb.query.FDBQueryGraphTestHelpers.column; +import static com.apple.foundationdb.record.provider.foundationdb.query.FDBQueryGraphTestHelpers.exists; +import static com.apple.foundationdb.record.provider.foundationdb.query.FDBQueryGraphTestHelpers.fieldPredicate; +import static com.apple.foundationdb.record.provider.foundationdb.query.FDBQueryGraphTestHelpers.forEach; +import static com.apple.foundationdb.record.provider.foundationdb.query.FDBQueryGraphTestHelpers.selectWithPredicates; +import static com.apple.foundationdb.record.query.plan.cascades.RuleTestHelper.EQUALS_42; +import static com.apple.foundationdb.record.query.plan.cascades.RuleTestHelper.baseT; +import static com.apple.foundationdb.record.query.plan.cascades.RuleTestHelper.baseTau; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class RewritingCostModelTest { + /** + * Test that simple rewrite of a single parameter predicate with the existence of other predicates is preferred. + * The following query: + *
{@code
+     * SELECT a FROM (SELECT a, b, d FROM T) WHERE a = 42 AND EXISTS (SELECT alpha FROM TAU)
+     * }
+ *

+ * should have a higher cost than the query: + *

+ *
{@code
+     * SELECT a FROM (SELECT a, b FROM T WHERE a = 42) WHERE EXISTS (SELECT alpha FROM TAU)
+     * }
+ */ + @Test + void costModelsPrefersPushedDownPredicates() { + Quantifier baseQun = baseT(); + + Quantifier lowerQun = forEach(selectWithPredicates( + baseQun, List.of("a", "b") + )); + Quantifier existentialQun = exists(selectWithPredicates( + baseTau(), List.of("alpha") + )); + + GraphExpansion.Builder builder = GraphExpansion.builder().addQuantifier(lowerQun).addQuantifier(existentialQun); + builder.addResultColumn(column(lowerQun, "b", "b")); + builder.addAllPredicates(List.of( + fieldPredicate(lowerQun, "a", EQUALS_42), + new ExistsPredicate(existentialQun.getAlias()) + )); + SelectExpression higher = builder.build().buildSelect(); + + + Quantifier newLowerQun = forEach(selectWithPredicates( + baseQun, List.of("a", "b"), + fieldPredicate(baseQun, "a", EQUALS_42) + )); + + GraphExpansion.Builder newBuilder = GraphExpansion.builder().addQuantifier(newLowerQun).addQuantifier(existentialQun); + newBuilder.addResultColumn(column(newLowerQun, "b", "b")); + newBuilder.addAllPredicates(List.of( + new ExistsPredicate(existentialQun.getAlias()) + )); + SelectExpression newHigher = newBuilder.build().buildSelect(); + + assertThat(PlannerPhase.REWRITING.createCostModel(RecordQueryPlannerConfiguration.defaultPlannerConfiguration()).compare(newHigher, higher)).isNegative(); + } +} diff --git a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/query/plan/cascades/rules/PredicatePushDownRuleTest.java b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/query/plan/cascades/rules/PredicatePushDownRuleTest.java index 2d40afaa52..89b5db2600 100644 --- a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/query/plan/cascades/rules/PredicatePushDownRuleTest.java +++ b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/query/plan/cascades/rules/PredicatePushDownRuleTest.java @@ -23,6 +23,7 @@ import com.apple.foundationdb.record.provider.foundationdb.query.FDBQueryGraphTestHelpers; import com.apple.foundationdb.record.query.expressions.Comparisons; import com.apple.foundationdb.record.query.plan.cascades.AliasMap; +import com.apple.foundationdb.record.query.plan.cascades.GraphExpansion; import com.apple.foundationdb.record.query.plan.cascades.OrderingPart; import com.apple.foundationdb.record.query.plan.cascades.PlannerStage; import com.apple.foundationdb.record.query.plan.cascades.Quantifier; @@ -128,6 +129,53 @@ void pushDownSimplePredicate() { testHelper.assertYields(higher, newHigher); } + /** + * Test a simple rewrite of a single parameter predicate with the existence of other predicates. It should go from: + *
{@code
+     * SELECT a FROM (SELECT a, b, d FROM T) WHERE a = 42 AND EXISTS (SELECT alpha FROM TAU)
+     * }
+ *

+ * And become: + *

+ *
{@code
+     * SELECT a FROM (SELECT a, b FROM T WHERE a = 42) WHERE EXISTS (SELECT alpha FROM TAU)
+     * }
+ */ + @Test + void pushDownOnePredicateOfMultiple() { + Quantifier baseQun = baseT(); + + Quantifier lowerQun = forEach(selectWithPredicates( + baseQun, List.of("a", "b") + )); + Quantifier existentialQun = exists(selectWithPredicates( + baseTau(), List.of("alpha") + )); + + GraphExpansion.Builder builder = GraphExpansion.builder().addQuantifier(lowerQun).addQuantifier(existentialQun); + builder.addResultColumn(column(lowerQun, "b", "b")); + builder.addAllPredicates(List.of( + fieldPredicate(lowerQun, "a", EQUALS_42), + new ExistsPredicate(existentialQun.getAlias()) + )); + SelectExpression higher = builder.build().buildSelect(); + + + Quantifier newLowerQun = forEach(selectWithPredicates( + baseQun, List.of("a", "b"), + fieldPredicate(baseQun, "a", EQUALS_42) + )); + + GraphExpansion.Builder newBuilder = GraphExpansion.builder().addQuantifier(newLowerQun).addQuantifier(existentialQun); + newBuilder.addResultColumn(column(newLowerQun, "b", "b")); + newBuilder.addAllPredicates(List.of( + new ExistsPredicate(existentialQun.getAlias()) + )); + SelectExpression newHigher = newBuilder.build().buildSelect(); + + testHelper.assertYields(higher, newHigher); + } + @Test void pushSimplePredicateIntoLogicalFilter() { Quantifier baseQun = baseT();