diff --git a/src/main/java/org/qed/CodeGenerator.java b/src/main/java/org/qed/CodeGenerator.java index 7b5009e..d2f1d7d 100644 --- a/src/main/java/org/qed/CodeGenerator.java +++ b/src/main/java/org/qed/CodeGenerator.java @@ -16,7 +16,7 @@ default E unimplementedTransform(E env, Object object) { return env; } - E preMatch(); + E preMatch(String rulename); default E onMatch(E env, RelRN pattern) { return switch (pattern) { @@ -96,9 +96,9 @@ default String translate(String name, E onMatch, E transform) { default String generate(RRule rule) { System.out.printf("Generating Rule: %s\n", rule.name()); - var onMatch = postMatch(onMatch(preMatch(), rule.before())); + var onMatch = postMatch(onMatch(preMatch(rule.name()), rule.before())); var transform = postTransform(transform(preTransform(onMatch), rule.after())); - return translate(rule.getClass().getSimpleName(), onMatch, transform); + return translate(rule.name(), onMatch, transform); } default E onMatchScan(E env, RelRN.Scan scan) { diff --git a/src/main/java/org/qed/Generated/CalciteGenerator.java b/src/main/java/org/qed/Generated/CalciteGenerator.java index 4a74dc1..c366877 100644 --- a/src/main/java/org/qed/Generated/CalciteGenerator.java +++ b/src/main/java/org/qed/Generated/CalciteGenerator.java @@ -16,8 +16,8 @@ public class CalciteGenerator implements CodeGenerator { @Override - public Env preMatch() { - return Env.empty(); + public Env preMatch(String rulename) { + return Env.empty(rulename); } @Override @@ -36,13 +36,56 @@ public String translate(String name, Env onMatch, Env transform) { var builder = new StringBuilder("package org.qed.Generated;\n\n"); builder.append("import org.apache.calcite.plan.RelOptRuleCall;\n"); builder.append("import org.apache.calcite.plan.RelRule;\n"); + builder.append("import org.apache.calcite.plan.RelOptUtil;\n"); + builder.append("import java.util.List;\n"); builder.append("import org.apache.calcite.rel.RelNode;\n"); builder.append("import org.apache.calcite.rel.core.JoinRelType;\n"); - builder.append("import org.apache.calcite.rel.logical.*;\n\n"); + builder.append("import org.apache.calcite.rel.logical.*;\n"); + if (name.equals("ProjectFilterTranspose")) { + builder.append("import org.apache.calcite.rex.RexInputRef;\n"); + builder.append("import org.apache.calcite.rex.RexShuttle;\n"); + builder.append("import java.util.HashMap;\n"); + } + builder.append("\n"); builder.append("public class " + name + " extends RelRule<" + name + ".Config> {\n"); builder.append("\tprotected " + name + "(Config config) {\n"); builder.append("\t\tsuper(config);\n"); builder.append("\t}\n\n"); + + if(name.equals("ProjectFilterTranspose")) { + builder.append( + """ + \tprivate static org.apache.calcite.rex.RexNode mapFilterToProjectedColumns(RelOptRuleCall call) { + \t\tvar filter = (LogicalFilter) call.rel(1); + \t\tvar project = (LogicalProject) call.rel(0); + \t\tvar rexBuilder = project.getCluster().getRexBuilder(); + \t\t + \t\t// Create mapping from table column index to projected position + \t\tvar tableToProjectMapping = new HashMap(); + \t\tfor (int projectedPos = 0; projectedPos < project.getProjects().size(); projectedPos++) { + \t\t\tvar projectExpr = project.getProjects().get(projectedPos); + \t\t\tif (projectExpr instanceof RexInputRef inputRef) { + \t\t\t\ttableToProjectMapping.put(inputRef.getIndex(), projectedPos); + \t\t\t} + \t\t} + \t\t + \t\t// Rewrite filter condition to use projected positions + \t\treturn filter.getCondition().accept(new RexShuttle() { + \t\t\t@Override + \t\t\tpublic org.apache.calcite.rex.RexNode visitInputRef(RexInputRef inputRef) { + \t\t\t\tInteger projectedPos = tableToProjectMapping.get(inputRef.getIndex()); + \t\t\t\tif (projectedPos != null) { + \t\t\t\t\treturn rexBuilder.makeInputRef(inputRef.getType(), projectedPos); + \t\t\t\t} + \t\t\t\treturn inputRef; + \t\t\t} + \t\t}); + \t} + + """ + ); + } + builder.append("\t@Override\n\tpublic void onMatch(RelOptRuleCall call) {\n"); transform.statements().forEach(statement -> builder.append("\t\t").append(statement).append("\n")); builder.append("\t}\n\n"); @@ -313,7 +356,17 @@ public Env transformPred(Env env, RexRN.Pred pred) { String operatorCall = "((org.apache.calcite.rex.RexCall) ((LogicalJoin) call.rel(0)).getCondition()).getOperator()"; return currentEnv.focus(env.current() + ".call(" + operatorCall + ", " + argsString + ")"); - } else { + } + else if (pred.sources().anyMatch(source -> source instanceof RexRN.Proj)) { + return env.focus( + "RelOptUtil.pushFilterPastProject(((LogicalFilter) call.rel(0)).getCondition(), " + + "((LogicalProject) call.rel(1)))" + ); + } + else if (env.rulename.equals("ProjectFilterTranspose")) { + return env.focus("mapFilterToProjectedColumns(call)"); + } + else { return env.focus(env.symbols().get(pred.operator().getName())); } } @@ -472,15 +525,9 @@ public Env transformProj(Env env, RexRN.Proj proj) { @Override public Env transformProject(Env env, RelRN.Project project) { - // First transform the source relation var source_transform = transform(env, project.source()); var source_expression = source_transform.current(); - - // Then transform the projection map var map_transform = transform(source_transform, project.map()); - - // Combine the source and projection using the project operation - // This creates a projection on top of the source relation return map_transform.focus(source_expression + ".project(" + map_transform.current() + ")"); } @@ -550,26 +597,26 @@ public Env transformCustom(Env env, RelRN custom) { } public record Env(AtomicInteger varId, int rel, String current, String skeleton, Seq statements, - ImmutableMap symbols) { - public static Env empty() { + ImmutableMap symbols, String rulename) { + public static Env empty(String rulename) { return new Env(new AtomicInteger(), 0, "call.rel(0)", "/* Unspecified skeleton */", Seq.empty(), - ImmutableMap.empty()); + ImmutableMap.empty(), rulename); } public Env next() { - return new Env(varId, rel + 1, "call.rel(" + (rel + 1) + ")", skeleton, statements, symbols); + return new Env(varId, rel + 1, "call.rel(" + (rel + 1) + ")", skeleton, statements, symbols, rulename); } public Env focus(String target) { - return new Env(varId, rel, target, skeleton, statements, symbols); + return new Env(varId, rel, target, skeleton, statements, symbols, rulename); } public Env state(String statement) { - return new Env(varId, rel, current, skeleton, statements.appended(statement), symbols); + return new Env(varId, rel, current, skeleton, statements.appended(statement), symbols, rulename); } public Env symbol(String symbol, String expression) { - return new Env(varId, rel, current, skeleton, statements, symbols.putted(symbol, expression)); + return new Env(varId, rel, current, skeleton, statements, symbols.putted(symbol, expression), rulename); } public Tuple2 declare(String expression) { @@ -579,7 +626,7 @@ public Tuple2 declare(String expression) { public Env grow(String requirement) { var vn = "s_" + varId.getAndIncrement(); - return new Env(varId, rel, current, vn + " -> " + vn + "." + requirement, statements, symbols); + return new Env(varId, rel, current, vn + " -> " + vn + "." + requirement, statements, symbols, rulename); } } diff --git a/src/main/java/org/qed/Generated/CalciteTester.java b/src/main/java/org/qed/Generated/CalciteTester.java index 4a1d6b3..1b15928 100644 --- a/src/main/java/org/qed/Generated/CalciteTester.java +++ b/src/main/java/org/qed/Generated/CalciteTester.java @@ -16,6 +16,7 @@ import org.apache.calcite.sql.fun.SqlStdOperatorTable; import org.qed.*; import org.reflections.Reflections; +import org.apache.calcite.rel.rules.*; import java.io.File; import java.io.IOException; @@ -52,7 +53,8 @@ public static Seq ruleList() { var concreteRuleClasses = ruleClasses.stream() .filter(clazz -> !clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers()) && - !clazz.getName().contains("$")) + !clazz.getName().contains("$") && + clazz.getSimpleName().equals("ProjectFilterTranspose")) .collect(Collectors.toSet()); var individuals = Seq.from(concreteRuleClasses) @@ -84,18 +86,20 @@ public static void generate() { public static void runAllTests() { try { - org.qed.Generated.Tests.FilterIntoJoinTest.runTest(); - org.qed.Generated.Tests.FilterMergeTest.runTest(); - org.qed.Generated.Tests.FilterProjectTransposeTest.runTest(); - org.qed.Generated.Tests.UnionMergeTest.runTest(); - org.qed.Generated.Tests.IntersectMergeTest.runTest(); - org.qed.Generated.Tests.FilterSetOpTransposeTest.runTest(); - org.qed.Generated.Tests.JoinExtractFilterTest.runTest(); - org.qed.Generated.Tests.SemiJoinFilterTransposeTest.runTest(); - org.qed.Generated.Tests.MinusMergeTest.runTest(); + // org.qed.Generated.Tests.FilterIntoJoinTest.runTest(); + // org.qed.Generated.Tests.FilterMergeTest.runTest(); + // org.qed.Generated.Tests.FilterProjectTransposeTest.runTest(); + // org.qed.Generated.Tests.UnionMergeTest.runTest(); + // org.qed.Generated.Tests.IntersectMergeTest.runTest(); + // org.qed.Generated.Tests.FilterSetOpTransposeTest.runTest(); + // org.qed.Generated.Tests.JoinExtractFilterTest.runTest(); + // org.qed.Generated.Tests.SemiJoinFilterTransposeTest.runTest(); + // org.qed.Generated.Tests.MinusMergeTest.runTest(); org.qed.Generated.Tests.ProjectFilterTransposeTest.runTest(); - org.qed.Generated.Tests.JoinPushTransitivePredicatesTest.runTest(); - org.qed.Generated.Tests.JoinCommuteTest.runTest(); + // org.qed.Generated.Tests.JoinPushTransitivePredicatesTest.runTest(); + // org.qed.Generated.Tests.JoinCommuteTest.runTest(); + // org.qed.Generated.Tests.JoinConditionPushTest.runTest(); + // org.qed.Generated.Tests.AggregateProjectMergeTest.runTest(); } catch (Exception e) { System.out.println("Test failed: " + e.getMessage()); e.printStackTrace(); @@ -103,7 +107,7 @@ public static void runAllTests() { } public static void main(String[] args) throws IOException { - var rule = new org.qed.Generated.RRuleInstances.JoinCommute(); + var rule = new org.qed.Generated.RRuleInstances.FilterProjectTranspose(); System.out.println(rule.explain()); Files.createDirectories(Path.of(rulePath)); new ObjectMapper().writerWithDefaultPrettyPrinter().writeValue(Path.of(rulePath, rule.name() + "-" + rule.info() + ".json").toFile(), rule.toJson()); @@ -154,7 +158,13 @@ public void verify(HepPlanner runner, RelNode source, RelNode target) { System.out.println("> Actual rewritten RelNode:\n" + answerExplain); System.out.println("> Expected rewritten RelNode:\n" + targetExplain); } - else System.out.println("succeeded"); + else + { + System.out.println("succeeded"); + System.out.println("> Given source RelNode:\n" + source.explain()); + System.out.println("> Actual rewritten RelNode:\n" + answerExplain); + System.out.println("> Expected rewritten RelNode:\n" + targetExplain); + } return; } System.out.println("failed"); diff --git a/src/main/java/org/qed/Generated/FilterProjectTranspose.java b/src/main/java/org/qed/Generated/FilterProjectTranspose.java index 89a31a0..f423b99 100644 --- a/src/main/java/org/qed/Generated/FilterProjectTranspose.java +++ b/src/main/java/org/qed/Generated/FilterProjectTranspose.java @@ -2,6 +2,8 @@ import org.apache.calcite.plan.RelOptRuleCall; import org.apache.calcite.plan.RelRule; +import org.apache.calcite.plan.RelOptUtil; +import java.util.List; import org.apache.calcite.rel.RelNode; import org.apache.calcite.rel.core.JoinRelType; import org.apache.calcite.rel.logical.*; @@ -14,7 +16,7 @@ protected FilterProjectTranspose(Config config) { @Override public void onMatch(RelOptRuleCall call) { var var_3 = call.builder(); - call.transformTo(var_3.push(call.rel(2)).project(((LogicalProject) call.rel(0)).getProjects()).filter(((LogicalFilter) call.rel(1)).getCondition()).build()); + call.transformTo(var_3.push(call.rel(2)).filter(RelOptUtil.pushFilterPastProject(((LogicalFilter) call.rel(0)).getCondition(), ((LogicalProject) call.rel(1)))).project(((LogicalProject) call.rel(1)).getProjects()).build()); } public interface Config extends EmptyConfig { @@ -32,7 +34,7 @@ default String description() { @Override default RelRule.OperandTransform operandSupplier() { - return s_2 -> s_2.operand(LogicalProject.class).oneInput(s_1 -> s_1.operand(LogicalFilter.class).oneInput(s_0 -> s_0.operand(RelNode.class).anyInputs())); + return s_2 -> s_2.operand(LogicalFilter.class).oneInput(s_1 -> s_1.operand(LogicalProject.class).oneInput(s_0 -> s_0.operand(RelNode.class).anyInputs())); } } diff --git a/src/main/java/org/qed/Generated/ProjectFilterTranspose.java b/src/main/java/org/qed/Generated/ProjectFilterTranspose.java index cbbd7d8..228626a 100644 --- a/src/main/java/org/qed/Generated/ProjectFilterTranspose.java +++ b/src/main/java/org/qed/Generated/ProjectFilterTranspose.java @@ -2,19 +2,51 @@ import org.apache.calcite.plan.RelOptRuleCall; import org.apache.calcite.plan.RelRule; +import org.apache.calcite.plan.RelOptUtil; +import java.util.List; import org.apache.calcite.rel.RelNode; import org.apache.calcite.rel.core.JoinRelType; import org.apache.calcite.rel.logical.*; +import org.apache.calcite.rex.RexInputRef; +import org.apache.calcite.rex.RexShuttle; +import java.util.HashMap; public class ProjectFilterTranspose extends RelRule { protected ProjectFilterTranspose(Config config) { super(config); } + private static org.apache.calcite.rex.RexNode mapFilterToProjectedColumns(RelOptRuleCall call) { + var filter = (LogicalFilter) call.rel(1); + var project = (LogicalProject) call.rel(0); + var rexBuilder = project.getCluster().getRexBuilder(); + + // Create mapping from table column index to projected position + var tableToProjectMapping = new HashMap(); + for (int projectedPos = 0; projectedPos < project.getProjects().size(); projectedPos++) { + var projectExpr = project.getProjects().get(projectedPos); + if (projectExpr instanceof RexInputRef inputRef) { + tableToProjectMapping.put(inputRef.getIndex(), projectedPos); + } + } + + // Rewrite filter condition to use projected positions + return filter.getCondition().accept(new RexShuttle() { + @Override + public org.apache.calcite.rex.RexNode visitInputRef(RexInputRef inputRef) { + Integer projectedPos = tableToProjectMapping.get(inputRef.getIndex()); + if (projectedPos != null) { + return rexBuilder.makeInputRef(inputRef.getType(), projectedPos); + } + return inputRef; + } + }); + } + @Override public void onMatch(RelOptRuleCall call) { var var_3 = call.builder(); - call.transformTo(var_3.push(call.rel(2)).filter(((LogicalFilter) call.rel(0)).getCondition()).project(((LogicalProject) call.rel(1)).getProjects()).build()); + call.transformTo(var_3.push(call.rel(2)).project(((LogicalProject) call.rel(0)).getProjects()).filter(mapFilterToProjectedColumns(call)).build()); } public interface Config extends EmptyConfig { @@ -32,7 +64,7 @@ default String description() { @Override default RelRule.OperandTransform operandSupplier() { - return s_2 -> s_2.operand(LogicalFilter.class).oneInput(s_1 -> s_1.operand(LogicalProject.class).oneInput(s_0 -> s_0.operand(RelNode.class).anyInputs())); + return s_2 -> s_2.operand(LogicalProject.class).oneInput(s_1 -> s_1.operand(LogicalFilter.class).oneInput(s_0 -> s_0.operand(RelNode.class).anyInputs())); } } diff --git a/src/main/java/org/qed/Generated/RRuleInstances/AggregateProjectConstantToDummyJoin.java b/src/main/java/org/qed/Generated/RRuleInstances/AggregateProjectConstantToDummyJoin.java new file mode 100644 index 0000000..847ad7d --- /dev/null +++ b/src/main/java/org/qed/Generated/RRuleInstances/AggregateProjectConstantToDummyJoin.java @@ -0,0 +1,197 @@ +package org.qed.Generated.RRuleInstances; + +import org.apache.calcite.rel.RelNode; +import org.apache.calcite.rel.core.JoinRelType; +import org.qed.RelRN; +import org.qed.RexRN; +import org.qed.RRule; +import org.qed.RuleBuilder; +import org.qed.RelType; +import kala.collection.Seq; +import kala.tuple.Tuple; + +/** + * AggregateProjectConstantToDummyJoinRule: Replaces constant literals in GROUP BY + * with a dummy table join. + * + * Pattern: + * Aggregate(group=[constant_literal, regular_field]) + * Project(constant_literal, regular_field) + * Scan + * + * => + * + * Aggregate(group=[dummy.constant, regular_field]) + * Project(dummy.constant, regular_field) + * Join(Scan, DummyValues(constant_literal)) + * + * This optimization can help with certain database engines that handle + * joins more efficiently than literal constants in GROUP BY clauses. + */ +public record AggregateProjectConstantToDummyJoin() implements RRule { + + // Base table for the pattern + static final RelRN baseTable = new BaseEmployeeTable(); + + @Override + public RelRN before() { + // Aggregate over project with constant literals + var projectWithConstants = new ProjectWithConstantLiterals(baseTable); + return new AggregateGroupingByConstants(projectWithConstants); + } + + @Override + public RelRN after() { + // Optimized: join with dummy table containing constants + var dummyTable = new DummyConstantsTable(); + var joinWithDummy = new JoinWithDummyTable(baseTable, dummyTable); + var projectWithDummyFields = new ProjectWithDummyFields(joinWithDummy); + return new AggregateGroupingByDummyFields(projectWithDummyFields); + } + + /** + * Base employee table + */ + public static record BaseEmployeeTable() implements RelRN { + @Override + public RelNode semantics() { + var builder = RuleBuilder.create(); + + var table = builder.createQedTable(Seq.of( + Tuple.of(RelType.fromString("INTEGER", true), false), // emp_id + Tuple.of(RelType.fromString("DECIMAL", true), false), // salary + Tuple.of(RelType.fromString("INTEGER", true), false) // dept_id + )); + + builder.addTable(table); + return builder.scan(table.getName()).build(); + } + } + + /** + * Project with constant literals: SELECT emp_id, TRUE as active_flag, '2024' as year_label, salary + */ + public static record ProjectWithConstantLiterals(RelRN input) implements RelRN { + @Override + public RelNode semantics() { + var builder = RuleBuilder.create(); + builder.push(input.semantics()); + + builder.project( + builder.field(0), // emp_id + builder.alias(builder.literal(true), "active_flag"), // constant: TRUE + builder.alias(builder.literal("2024"), "year_label"), // constant: '2024' + builder.field(1) // salary + ); + + return builder.build(); + } + } + + /** + * Aggregate grouping by constant literals: GROUP BY active_flag, year_label, emp_id + */ + public static record AggregateGroupingByConstants(RelRN input) implements RelRN { + @Override + public RelNode semantics() { + var builder = RuleBuilder.create(); + builder.push(input.semantics()); + + // Group by the constant fields and emp_id + var groupKey = builder.groupKey( + builder.field(1), // active_flag (constant) + builder.field(2), // year_label (constant) + builder.field(0) // emp_id (regular field) + ); + + // Aggregate: AVG(salary) + var avgSalary = builder.avg(builder.field(3)); + + builder.aggregate(groupKey, avgSalary); + return builder.build(); + } + } + + /** + * Dummy table containing the constant values: VALUES (TRUE, '2024') + */ + public static record DummyConstantsTable() implements RelRN { + @Override + public RelNode semantics() { + var builder = RuleBuilder.create(); + + // Create a values table with the constants + // Using the correct values() method signature + builder.values( + new String[]{"active_flag", "year_label"}, // Column names + true, // TRUE constant value + "2024" // '2024' constant value + ); + + return builder.build(); + } + } + + /** + * Join base table with dummy constants table + */ + public static record JoinWithDummyTable(RelRN baseTable, RelRN dummyTable) implements RelRN { + @Override + public RelNode semantics() { + var builder = RuleBuilder.create(); + + builder.push(baseTable.semantics()); + builder.push(dummyTable.semantics()); + + // Cross join (INNER JOIN with TRUE condition) + builder.join(JoinRelType.INNER, builder.literal(true)); + + return builder.build(); + } + } + + /** + * Project using dummy fields instead of constants: SELECT emp_id, dummy.active_flag, dummy.year_label, salary + */ + public static record ProjectWithDummyFields(RelRN input) implements RelRN { + @Override + public RelNode semantics() { + var builder = RuleBuilder.create(); + builder.push(input.semantics()); + + // After join: base table fields are 0,1,2 and dummy fields are 3,4 + builder.project( + builder.field(0), // emp_id (from base table) + builder.field(3), // active_flag (from dummy table) + builder.field(4), // year_label (from dummy table) + builder.field(1) // salary (from base table) + ); + + return builder.build(); + } + } + + /** + * Aggregate grouping by dummy fields: GROUP BY dummy.active_flag, dummy.year_label, emp_id + */ + public static record AggregateGroupingByDummyFields(RelRN input) implements RelRN { + @Override + public RelNode semantics() { + var builder = RuleBuilder.create(); + builder.push(input.semantics()); + + // Group by the dummy fields and emp_id + var groupKey = builder.groupKey( + builder.field(1), // active_flag (from dummy) + builder.field(2), // year_label (from dummy) + builder.field(0) // emp_id (regular field) + ); + + // Same aggregate: AVG(salary) + var avgSalary = builder.avg(builder.field(3)); + + builder.aggregate(groupKey, avgSalary); + return builder.build(); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/qed/Generated/RRuleInstances/AggregateProjectMerge.java b/src/main/java/org/qed/Generated/RRuleInstances/AggregateProjectMerge.java new file mode 100644 index 0000000..35d38e0 --- /dev/null +++ b/src/main/java/org/qed/Generated/RRuleInstances/AggregateProjectMerge.java @@ -0,0 +1,91 @@ +package org.qed.Generated.RRuleInstances; + +import org.apache.calcite.rel.RelNode; +import org.qed.RelRN; +import org.qed.RRule; +import org.qed.RuleBuilder; +import org.qed.RelType; +import kala.collection.Seq; +import kala.tuple.Tuple; + +/** + * Abstract AggregateProjectMergeRule that represents valid transformations: + * + * Aggregate(Project_with_field_references(R)) => Aggregate(R) + * + * The project must contain only field references, not expressions. + */ +public record AggregateProjectMerge() implements RRule { + + // Multi-column base relation + static final RelRN R = new MultiColumnRelation(); + + @Override + public RelRN before() { + // Pattern: Aggregate over Project that selects/reorders fields + var projection = new FieldReferenceProject(R); + return new SimpleAggregate(projection); + } + + @Override + public RelRN after() { + // Pattern: Same aggregate directly on base relation + return new SimpleAggregate(R); + } + + /** + * Base relation with multiple columns + */ + public static record MultiColumnRelation() implements RelRN { + @Override + public RelNode semantics() { + var builder = RuleBuilder.create(); + + var table = builder.createQedTable(Seq.of( + Tuple.of(RelType.fromString("INTEGER", true), false), // col0 + Tuple.of(RelType.fromString("INTEGER", true), false), // col1 + Tuple.of(RelType.fromString("VARCHAR", true), false) // col2 + )); + + builder.addTable(table); + return builder.scan(table.getName()).build(); + } + } + + /** + * Project that contains ONLY field references (no expressions) + * This represents field selection/reordering that can be eliminated + */ + public static record FieldReferenceProject(RelRN input) implements RelRN { + @Override + public RelNode semantics() { + var builder = RuleBuilder.create(); + builder.push(input.semantics()); + + // Project that selects only first 2 fields (eliminates 3rd) + // This is pure field selection, not function application + builder.project( + builder.field(0) // $f0 = $0 (field reference) + ); + + return builder.build(); + } + } + + /** + * Simple aggregate: GROUP BY first field, COUNT(*) + */ + public static record SimpleAggregate(RelRN input) implements RelRN { + @Override + public RelNode semantics() { + var builder = RuleBuilder.create(); + builder.push(input.semantics()); + + var groupKey = builder.groupKey(builder.field(0)); + var countCall = builder.count(); + builder.aggregate(groupKey, countCall); + + return builder.build(); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/qed/Generated/RRuleInstances/FilterProjectTranspose.java b/src/main/java/org/qed/Generated/RRuleInstances/FilterProjectTranspose.java index 31a3a74..4c1b229 100644 --- a/src/main/java/org/qed/Generated/RRuleInstances/FilterProjectTranspose.java +++ b/src/main/java/org/qed/Generated/RRuleInstances/FilterProjectTranspose.java @@ -15,11 +15,11 @@ public record FilterProjectTranspose() implements RRule { @Override public RelRN before() { - return source.filter(proj.pred("pred")).project(proj); + return source.project(proj).filter("pred"); } @Override public RelRN after() { - return source.project(proj).filter("pred"); + return source.filter(proj.pred("pred")).project(proj); } } diff --git a/src/main/java/org/qed/Generated/RRuleInstances/JoinConditionPush.java b/src/main/java/org/qed/Generated/RRuleInstances/JoinConditionPush.java new file mode 100644 index 0000000..f60c8a2 --- /dev/null +++ b/src/main/java/org/qed/Generated/RRuleInstances/JoinConditionPush.java @@ -0,0 +1,61 @@ +package org.qed.Generated.RRuleInstances; + +import kala.collection.Seq; +import org.apache.calcite.rel.core.JoinRelType; +import org.apache.calcite.sql.SqlOperator; +import org.qed.RelRN; +import org.qed.RexRN; +import org.qed.RRule; +import org.qed.RuleBuilder; + +/** + * Implementation of JoinConditionPushRule. + * This rule analyzes join conditions and pushes single-table conditions down as filters. + * Based on Calcite's FilterJoinRule with no-filter configuration. + */ +public record JoinConditionPush() implements RRule { + static final RelRN left = RelRN.scan("Left", "Left_Type"); + static final RelRN right = RelRN.scan("Right", "Right_Type"); + + @Override + public RelRN before() { + SqlOperator joinOp = RuleBuilder.create().genericPredicateOp("joinCond", true); + RexRN crossTableCond = new RexRN.Pred(joinOp, Seq.of( + new RexRN.JoinField(0, left, right), // Left table field 0 (absolute position 0) + new RexRN.JoinField(1, left, right) // Right table field 0 (absolute position 1) + )); + SqlOperator leftOp = RuleBuilder.create().genericPredicateOp("leftCond", true); + RexRN leftOnlyCond = new RexRN.Pred(leftOp, Seq.of( + new RexRN.JoinField(0, left, right) // Left table field 0 (absolute position 0) + )); + SqlOperator rightOp = RuleBuilder.create().genericPredicateOp("rightCond", true); + RexRN rightOnlyCond = new RexRN.Pred(rightOp, Seq.of( + new RexRN.JoinField(1, left, right) // Right table field 0 (absolute position 1) + )); + return left.join(JoinRelType.INNER, + RexRN.and(crossTableCond, leftOnlyCond, rightOnlyCond), right); + } + + @Override + public RelRN after() { + SqlOperator joinOp = RuleBuilder.create().genericPredicateOp("joinCond", true); + RexRN crossTableCond = new RexRN.Pred(joinOp, Seq.of( + new RexRN.JoinField(0, left, right), // Left table field 0 (absolute position 0) + new RexRN.JoinField(1, left, right) // Right table field 0 (absolute position 1) + )); + + SqlOperator leftOp = RuleBuilder.create().genericPredicateOp("leftCond", true); + RexRN leftFilterCond = new RexRN.Pred(leftOp, Seq.of( + new RexRN.Field(0, left) // Field 0 of left table directly + )); + RelRN filteredLeft = left.filter(leftFilterCond); + + SqlOperator rightOp = RuleBuilder.create().genericPredicateOp("rightCond", true); + RexRN rightFilterCond = new RexRN.Pred(rightOp, Seq.of( + new RexRN.Field(0, right) // Field 0 of right table directly + )); + RelRN filteredRight = right.filter(rightFilterCond); + + return filteredLeft.join(JoinRelType.INNER, crossTableCond, filteredRight); + } +} \ No newline at end of file diff --git a/src/main/java/org/qed/Generated/RRuleInstances/ProjectAggregateMerge.java b/src/main/java/org/qed/Generated/RRuleInstances/ProjectAggregateMerge.java new file mode 100644 index 0000000..da37476 --- /dev/null +++ b/src/main/java/org/qed/Generated/RRuleInstances/ProjectAggregateMerge.java @@ -0,0 +1,164 @@ +package org.qed.Generated.RRuleInstances; + +import org.apache.calcite.rel.RelNode; +import org.qed.RelRN; +import org.qed.RRule; +import org.qed.RuleBuilder; +import org.qed.RelType; +import kala.collection.Seq; +import kala.tuple.Tuple; + +/** + * ProjectAggregateMergeRule: Eliminates unused aggregate calls from projections + * and converts COALESCE(SUM(x), 0) to SUM0(x) for better optimization. + * + * Pattern: + * Project(used_group_fields, used_agg_calls, unused_agg_calls) + * Aggregate(group_fields, used_agg_calls, unused_agg_calls) + * Scan + * + * => + * + * Project(used_group_fields, used_agg_calls) + * Aggregate(group_fields, used_agg_calls) -- unused calls removed + * Scan + * + * This optimization reduces the cost of aggregation by eliminating + * aggregate computations that are not used in the final result. + */ +public record ProjectAggregateMerge() implements RRule { + + // Base table for the pattern + static final RelRN baseTable = new SalesTable(); + + @Override + public RelRN before() { + // Project that uses only some of the aggregate results + var aggregateWithUnusedCalls = new AggregateWithMultipleCalls(baseTable); + return new ProjectUsingSubsetOfAggregates(aggregateWithUnusedCalls); + } + + @Override + public RelRN after() { + // Optimized: aggregate with only used calls + var aggregateOptimized = new AggregateWithUsedCallsOnly(baseTable); + return new ProjectOptimized(aggregateOptimized); + } + + /** + * Sales table with multiple numeric columns for aggregation + */ + public static record SalesTable() implements RelRN { + @Override + public RelNode semantics() { + var builder = RuleBuilder.create(); + + var table = builder.createQedTable(Seq.of( + Tuple.of(RelType.fromString("INTEGER", true), false), // region_id + Tuple.of(RelType.fromString("DECIMAL", true), false), // sales_amount + Tuple.of(RelType.fromString("DECIMAL", true), false), // cost_amount + Tuple.of(RelType.fromString("INTEGER", true), false) // quantity + )); + + builder.addTable(table); + return builder.scan(table.getName()).build(); + } + } + + /** + * Aggregate with multiple calls: GROUP BY region_id, SUM(sales), AVG(cost), COUNT(quantity), MAX(sales) + * Some of these aggregates will be unused in the projection + */ + public static record AggregateWithMultipleCalls(RelRN input) implements RelRN { + @Override + public RelNode semantics() { + var builder = RuleBuilder.create(); + builder.push(input.semantics()); + + // Group by region_id + var groupKey = builder.groupKey(builder.field(0)); + + // Multiple aggregate calls + var sumSales = builder.sum(false, "sum_sales", builder.field(1)); // Will be used + var avgCost = builder.avg(builder.field(2)); // Will be unused + var countQty = builder.count(false, "count_qty", builder.field(3)); // Will be used + var maxSales = builder.max(builder.field(1)); // Will be unused + + builder.aggregate(groupKey, sumSales, avgCost, countQty, maxSales); + return builder.build(); + } + } + + /** + * Project that uses only some aggregates: SELECT region_id, sum_sales, count_qty + * (avgCost and maxSales are not projected, so they're unused) + */ + public static record ProjectUsingSubsetOfAggregates(RelRN input) implements RelRN { + @Override + public RelNode semantics() { + var builder = RuleBuilder.create(); + builder.push(input.semantics()); + + // Project only used fields: + // field(0) = region_id (group key) + // field(1) = sum_sales (used aggregate) + // field(2) = avg_cost (UNUSED - not projected) + // field(3) = count_qty (used aggregate) + // field(4) = max_sales (UNUSED - not projected) + builder.project( + builder.alias(builder.field(0), "region_id"), // Group key + builder.alias(builder.field(1), "total_sales"), // Used: sum_sales + builder.alias(builder.field(3), "total_count") // Used: count_qty + // avg_cost (field 2) and max_sales (field 4) are not projected + ); + + return builder.build(); + } + } + + /** + * Optimized aggregate with only used calls: GROUP BY region_id, SUM(sales), COUNT(quantity) + * avgCost and maxSales are eliminated since they're not used + */ + public static record AggregateWithUsedCallsOnly(RelRN input) implements RelRN { + @Override + public RelNode semantics() { + var builder = RuleBuilder.create(); + builder.push(input.semantics()); + + // Same group key + var groupKey = builder.groupKey(builder.field(0)); + + // Only the aggregate calls that are actually used + var sumSales = builder.sum(false, "sum_sales", builder.field(1)); // Used + var countQty = builder.count(false, "count_qty", builder.field(3)); // Used + // avgCost and maxSales removed - they were unused + + builder.aggregate(groupKey, sumSales, countQty); + return builder.build(); + } + } + + /** + * Optimized project with adjusted field references + */ + public static record ProjectOptimized(RelRN input) implements RelRN { + @Override + public RelNode semantics() { + var builder = RuleBuilder.create(); + builder.push(input.semantics()); + + // After optimization, field layout is: + // field(0) = region_id (group key) + // field(1) = sum_sales (was field 1, still field 1) + // field(2) = count_qty (was field 3, now field 2) + builder.project( + builder.alias(builder.field(0), "region_id"), // Group key + builder.alias(builder.field(1), "total_sales"), // sum_sales + builder.alias(builder.field(2), "total_count") // count_qty (field index adjusted) + ); + + return builder.build(); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/qed/Generated/RRuleInstances/ProjectFilterTranspose.java b/src/main/java/org/qed/Generated/RRuleInstances/ProjectFilterTranspose.java index 8a17d2c..c8d97bc 100644 --- a/src/main/java/org/qed/Generated/RRuleInstances/ProjectFilterTranspose.java +++ b/src/main/java/org/qed/Generated/RRuleInstances/ProjectFilterTranspose.java @@ -10,11 +10,11 @@ public record ProjectFilterTranspose() implements RRule { @Override public RelRN before() { - return source.project(proj).filter("pred"); + return source.filter(proj.pred("pred")).project(proj); } @Override public RelRN after() { - return source.filter(proj.pred("pred")).project(proj); + return source.project(proj).filter("pred"); } } diff --git a/src/main/java/org/qed/Generated/RRuleInstances/UnionPullUpConstants.java b/src/main/java/org/qed/Generated/RRuleInstances/UnionPullUpConstants.java new file mode 100644 index 0000000..6e3b060 --- /dev/null +++ b/src/main/java/org/qed/Generated/RRuleInstances/UnionPullUpConstants.java @@ -0,0 +1,222 @@ +package org.qed.Generated.RRuleInstances; + +import org.apache.calcite.rel.RelNode; +import org.qed.RelRN; +import org.qed.RRule; +import org.qed.RuleBuilder; +import org.qed.RelType; +import kala.collection.Seq; +import kala.tuple.Tuple; + +/** + * UnionPullUpConstantsRule: Pulls up constant expressions through Union operators + * + * Pattern: + * Union( + * Project(col1, constant_value, col3), + * Project(col1, constant_value, col3) + * ) + * => + * Project(col1, constant_value, col3, + * Union( + * Project(col1, col3), + * Project(col1, col3) + * ) + * ) + * + * This optimization reduces the Union to only non-constant columns, + * then adds back the constants in a top-level projection. + */ +public record UnionPullUpConstants() implements RRule { + + // Base tables for demonstrating the pattern + static final RelRN leftTable = new LeftTableWithConstants(); + static final RelRN rightTable = new RightTableWithConstants(); + + @Override + public RelRN before() { + // Union of two projections that both have constants + var leftProjection = new LeftProjectionWithConstants(leftTable); + var rightProjection = new RightProjectionWithConstants(rightTable); + return new UnionWithConstantColumns(leftProjection, rightProjection); + } + + @Override + public RelRN after() { + // Optimized: constants pulled up, union reduced to non-constant columns + var leftProjectionReduced = new LeftProjectionNonConstants(leftTable); + var rightProjectionReduced = new RightProjectionNonConstants(rightTable); + var reducedUnion = new UnionReducedColumns(leftProjectionReduced, rightProjectionReduced); + return new TopProjectionWithConstants(reducedUnion); + } + + /** + * Left source table + */ + public static record LeftTableWithConstants() implements RelRN { + @Override + public RelNode semantics() { + var builder = RuleBuilder.create(); + + var table = builder.createQedTable(Seq.of( + Tuple.of(RelType.fromString("INTEGER", true), false), // emp_id + Tuple.of(RelType.fromString("VARCHAR", true), false), // emp_name + Tuple.of(RelType.fromString("INTEGER", true), false) // dept_id + )); + + builder.addTable(table); + return builder.scan(table.getName()).build(); + } + } + + /** + * Right source table + */ + public static record RightTableWithConstants() implements RelRN { + @Override + public RelNode semantics() { + var builder = RuleBuilder.create(); + + var table = builder.createQedTable(Seq.of( + Tuple.of(RelType.fromString("INTEGER", true), false), // emp_id + Tuple.of(RelType.fromString("VARCHAR", true), false), // emp_name + Tuple.of(RelType.fromString("INTEGER", true), false) // dept_id + )); + + builder.addTable(table); + return builder.scan(table.getName()).build(); + } + } + + /** + * Left projection with constants: SELECT emp_id, 'ACTIVE' as status, dept_id + */ + public static record LeftProjectionWithConstants(RelRN input) implements RelRN { + @Override + public RelNode semantics() { + var builder = RuleBuilder.create(); + builder.push(input.semantics()); + + builder.project( + builder.field(0), // emp_id + builder.alias(builder.literal("ACTIVE"), "status"), // constant: 'ACTIVE' + builder.field(2) // dept_id + ); + + return builder.build(); + } + } + + /** + * Right projection with SAME constants: SELECT emp_id, 'ACTIVE' as status, dept_id + */ + public static record RightProjectionWithConstants(RelRN input) implements RelRN { + @Override + public RelNode semantics() { + var builder = RuleBuilder.create(); + builder.push(input.semantics()); + + builder.project( + builder.field(0), // emp_id + builder.alias(builder.literal("ACTIVE"), "status"), // same constant: 'ACTIVE' + builder.field(2) // dept_id + ); + + return builder.build(); + } + } + + /** + * Union with constant columns (before optimization) + */ + public static record UnionWithConstantColumns(RelRN left, RelRN right) implements RelRN { + @Override + public RelNode semantics() { + var builder = RuleBuilder.create(); + + builder.push(left.semantics()); + builder.push(right.semantics()); + + builder.union(true, 2); // UNION ALL + + return builder.build(); + } + } + + /** + * Left projection with constants removed: SELECT emp_id, dept_id + */ + public static record LeftProjectionNonConstants(RelRN input) implements RelRN { + @Override + public RelNode semantics() { + var builder = RuleBuilder.create(); + builder.push(input.semantics()); + + // Project only non-constant columns + builder.project( + builder.field(0), // emp_id + builder.field(2) // dept_id + // status constant removed + ); + + return builder.build(); + } + } + + /** + * Right projection with constants removed: SELECT emp_id, dept_id + */ + public static record RightProjectionNonConstants(RelRN input) implements RelRN { + @Override + public RelNode semantics() { + var builder = RuleBuilder.create(); + builder.push(input.semantics()); + + // Project only non-constant columns + builder.project( + builder.field(0), // emp_id + builder.field(2) // dept_id + // status constant removed + ); + + return builder.build(); + } + } + + /** + * Union of reduced columns (constants removed) + */ + public static record UnionReducedColumns(RelRN left, RelRN right) implements RelRN { + @Override + public RelNode semantics() { + var builder = RuleBuilder.create(); + + builder.push(left.semantics()); + builder.push(right.semantics()); + + builder.union(true, 2); // UNION ALL on reduced columns + + return builder.build(); + } + } + + /** + * Top projection that adds back the constants: SELECT emp_id, 'ACTIVE' as status, dept_id + */ + public static record TopProjectionWithConstants(RelRN input) implements RelRN { + @Override + public RelNode semantics() { + var builder = RuleBuilder.create(); + builder.push(input.semantics()); + + // Add back the constant in the final projection + builder.project( + builder.field(0), // emp_id (from union) + builder.alias(builder.literal("ACTIVE"), "status"), // constant added back + builder.field(1) // dept_id (from union) + ); + + return builder.build(); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/qed/Generated/RRuleInstances/UnionToDistinct.java b/src/main/java/org/qed/Generated/RRuleInstances/UnionToDistinct.java new file mode 100644 index 0000000..e45c069 --- /dev/null +++ b/src/main/java/org/qed/Generated/RRuleInstances/UnionToDistinct.java @@ -0,0 +1,131 @@ +package org.qed.Generated.RRuleInstances; + +import org.apache.calcite.rel.RelNode; +import org.qed.RelRN; +import org.qed.RRule; +import org.qed.RuleBuilder; +import org.qed.RelType; +import kala.collection.Seq; +import kala.tuple.Tuple; + +/** + * UnionToDistinctRule: Transforms UNION DISTINCT into UNION ALL + DISTINCT aggregate + * + * Pattern: Union(all=false, inputs...) => Aggregate(DISTINCT group by all fields)(Union(all=true, inputs...)) + * + * This optimization can be beneficial when the underlying system can handle + * UNION ALL more efficiently than UNION DISTINCT. + */ +public record UnionToDistinct() implements RRule { + + // Base tables for the union + static final RelRN leftTable = new LeftSourceTable(); + static final RelRN rightTable = new RightSourceTable(); + + @Override + public RelRN before() { + // UNION DISTINCT (all=false) + return new DistinctUnion(leftTable, rightTable); + } + + @Override + public RelRN after() { + // UNION ALL + DISTINCT aggregate + var unionAll = new UnionAll(leftTable, rightTable); + return new DistinctAggregate(unionAll); + } + + /** + * Left source table + */ + public static record LeftSourceTable() implements RelRN { + @Override + public RelNode semantics() { + var builder = RuleBuilder.create(); + + var table = builder.createQedTable(Seq.of( + Tuple.of(RelType.fromString("INTEGER", true), false), // col0 + Tuple.of(RelType.fromString("VARCHAR", true), false) // col1 + )); + + builder.addTable(table); + return builder.scan(table.getName()).build(); + } + } + + /** + * Right source table (same schema as left for UNION compatibility) + */ + public static record RightSourceTable() implements RelRN { + @Override + public RelNode semantics() { + var builder = RuleBuilder.create(); + + var table = builder.createQedTable(Seq.of( + Tuple.of(RelType.fromString("INTEGER", true), false), // col0 + Tuple.of(RelType.fromString("VARCHAR", true), false) // col1 + )); + + builder.addTable(table); + return builder.scan(table.getName()).build(); + } + } + + /** + * UNION DISTINCT operation (all=false) + */ + public static record DistinctUnion(RelRN left, RelRN right) implements RelRN { + @Override + public RelNode semantics() { + var builder = RuleBuilder.create(); + + // Push both inputs + builder.push(left.semantics()); + builder.push(right.semantics()); + + // Create UNION with all=false (DISTINCT) + builder.union(false, 2); + + return builder.build(); + } + } + + /** + * UNION ALL operation (all=true) + */ + public static record UnionAll(RelRN left, RelRN right) implements RelRN { + @Override + public RelNode semantics() { + var builder = RuleBuilder.create(); + + // Push both inputs + builder.push(left.semantics()); + builder.push(right.semantics()); + + // Create UNION with all=true (ALL) + builder.union(true, 2); + + return builder.build(); + } + } + + /** + * DISTINCT aggregate - groups by all fields to eliminate duplicates + */ + public static record DistinctAggregate(RelRN input) implements RelRN { + @Override + public RelNode semantics() { + var builder = RuleBuilder.create(); + builder.push(input.semantics()); + + // Group by all fields (creates DISTINCT effect) + // For a 2-column table, group by field 0 and field 1 + var groupKey = builder.groupKey(builder.field(0), builder.field(1)); + + // No aggregate functions needed - just grouping creates DISTINCT + builder.aggregate(groupKey); + + return builder.build(); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/qed/Generated/Tests/FilterProjectTransposeTest.java b/src/main/java/org/qed/Generated/Tests/FilterProjectTransposeTest.java index 0131765..8797dc8 100644 --- a/src/main/java/org/qed/Generated/Tests/FilterProjectTransposeTest.java +++ b/src/main/java/org/qed/Generated/Tests/FilterProjectTransposeTest.java @@ -11,24 +11,45 @@ public class FilterProjectTransposeTest { public static void runTest() { var tester = new CalciteTester(); var builder = RuleBuilder.create(); + + // Create table with 3 columns: id, salary, dept_id var table = builder.createQedTable(Seq.of( - Tuple.of(RelType.fromString("INTEGER", true), false), - Tuple.of(RelType.fromString("INTEGER", true), false) + Tuple.of(RelType.fromString("INTEGER", true), false), // id (col 0) + Tuple.of(RelType.fromString("INTEGER", true), false), // salary (col 1) + Tuple.of(RelType.fromString("INTEGER", true), false) // dept_id (col 2) )); builder.addTable(table); var scan = builder.scan(table.getName()).build(); - + + // Project: SELECT salary, dept_id (reordered columns) + // Filter: WHERE salary > 50000 AND dept_id = 5 var before = builder .push(scan) - .filter(builder.equals(builder.field(0), builder.literal(10))) - .project(builder.field(0)) + .project( + builder.field(1), // salary -> position 0 in projection + builder.field(2) // dept_id -> position 1 in projection + ) + .filter( + builder.and( + builder.greaterThan(builder.field(0), builder.literal(50000)), // projected salary > 50000 + builder.equals(builder.field(1), builder.literal(5)) // projected dept_id = 5 + ) + ) .build(); - + var after = builder .push(scan) - .project(builder.field(0)) - .filter(builder.equals(builder.field(0), builder.literal(10))) + .filter( + builder.and( + builder.greaterThan(builder.field(1), builder.literal(50000)), // table salary > 50000 + builder.equals(builder.field(2), builder.literal(5)) // table dept_id = 5 + ) + ) + .project( + builder.field(1), // salary + builder.field(2) // dept_id + ) .build(); var runner = CalciteTester.loadRule(org.qed.Generated.FilterProjectTranspose.Config.DEFAULT.toRule()); diff --git a/src/main/java/org/qed/Generated/Tests/ProjectFilterTransposeTest.java b/src/main/java/org/qed/Generated/Tests/ProjectFilterTransposeTest.java index 05bfc31..d4065ba 100644 --- a/src/main/java/org/qed/Generated/Tests/ProjectFilterTransposeTest.java +++ b/src/main/java/org/qed/Generated/Tests/ProjectFilterTransposeTest.java @@ -10,26 +10,45 @@ public class ProjectFilterTransposeTest { public static void runTest() { var tester = new CalciteTester(); var builder = RuleBuilder.create(); + + // Create table with 3 columns: id, salary, dept_id var table = builder.createQedTable(Seq.of( - Tuple.of(RelType.fromString("INTEGER", true), false), - Tuple.of(RelType.fromString("INTEGER", true), false) + Tuple.of(RelType.fromString("INTEGER", true), false), // id (col 0) + Tuple.of(RelType.fromString("INTEGER", true), false), // salary (col 1) + Tuple.of(RelType.fromString("INTEGER", true), false) // dept_id (col 2) )); builder.addTable(table); - + var scan = builder.scan(table.getName()).build(); var before = builder - .push(scan) - .project(builder.field(0)) - .filter(builder.equals(builder.field(0), builder.literal(10))) - .build(); + .push(scan) + .filter( + builder.and( + builder.greaterThan(builder.field(1), builder.literal(50000)), // table salary > 50000 + builder.equals(builder.field(2), builder.literal(5)) // table dept_id = 5 + ) + ) + .project( + builder.field(1), // salary + builder.field(2) // dept_id + ) + .build(); var after = builder - .push(scan) - .filter(builder.equals(builder.field(0), builder.literal(10))) - .project(builder.field(0)) - .build(); - + .push(scan) + .project( + builder.field(1), // salary -> position 0 in projection + builder.field(2) // dept_id -> position 1 in projection + ) + .filter( + builder.and( + builder.greaterThan(builder.field(0), builder.literal(50000)), // projected salary > 50000 + builder.equals(builder.field(1), builder.literal(5)) // projected dept_id = 5 + ) + ) + .build(); + var runner = CalciteTester.loadRule(org.qed.Generated.ProjectFilterTranspose.Config.DEFAULT.toRule()); tester.verify(runner, before, after); }