blob: 446d2c1f1427abb9d2a8e665002d5e2feef9fe07 [file] [log] [blame]
/*
* 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.ignite.internal.sql.engine.planner;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.util.List;
import java.util.Set;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.core.Filter;
import org.apache.calcite.rel.core.Project;
import org.apache.calcite.rel.logical.LogicalCorrelate;
import org.apache.calcite.rel.logical.LogicalFilter;
import org.apache.calcite.rel.logical.LogicalProject;
import org.apache.calcite.rex.RexCorrelVariable;
import org.apache.calcite.rex.RexFieldAccess;
import org.apache.calcite.sql.SqlNode;
import org.apache.ignite.internal.sql.engine.framework.TestBuilders;
import org.apache.ignite.internal.sql.engine.framework.TestBuilders.TableBuilder;
import org.apache.ignite.internal.sql.engine.prepare.IgnitePlanner;
import org.apache.ignite.internal.sql.engine.prepare.PlannerPhase;
import org.apache.ignite.internal.sql.engine.prepare.PlanningContext;
import org.apache.ignite.internal.sql.engine.rel.IgniteCorrelatedNestedLoopJoin;
import org.apache.ignite.internal.sql.engine.rel.IgniteFilter;
import org.apache.ignite.internal.sql.engine.rel.IgniteRel;
import org.apache.ignite.internal.sql.engine.schema.IgniteSchema;
import org.apache.ignite.internal.sql.engine.schema.IgniteTable;
import org.apache.ignite.internal.sql.engine.trait.IgniteDistributions;
import org.apache.ignite.internal.sql.engine.util.RexUtils;
import org.apache.ignite.internal.type.NativeTypes;
import org.junit.jupiter.api.Test;
/** Tests to verify correlated subquery planning. */
public class CorrelatedSubqueryPlannerTest extends AbstractPlannerTest {
/**
* Test verifies the row type is consistent for correlation variable and node that actually puts this variable into a context.
*
* <p>In this particular test the row type of the left input of CNLJ node should
* match the row type correlated variable in the filter was created with.
*
* @throws Exception In case of any unexpected error.
*/
@Test
public void test() throws Exception {
IgniteSchema schema = createSchema(createTestTable("A", "B", "C", "D", "E"));
String sql = ""
+ "SELECT (SELECT count(*) FROM t1 AS x WHERE x.b<t1.b)\n"
+ " FROM t1\n"
+ " WHERE (a>e-2 AND a<e+2)\n"
+ " OR c>d\n"
+ " ORDER BY 1;";
IgniteRel rel = physicalPlan(sql, schema, "FilterTableScanMergeRule");
IgniteFilter filter = findFirstNode(rel, byClass(IgniteFilter.class)
.and(f -> RexUtils.hasCorrelation(((Filter) f).getCondition())));
RexFieldAccess fieldAccess = findFirst(filter.getCondition(), RexFieldAccess.class);
assertNotNull(fieldAccess);
assertEquals(
RexCorrelVariable.class,
fieldAccess.getReferenceExpr().getClass()
);
IgniteCorrelatedNestedLoopJoin join = findFirstNode(rel, byClass(IgniteCorrelatedNestedLoopJoin.class));
assertEquals(
fieldAccess.getReferenceExpr().getType(),
join.getLeft().getRowType()
);
}
/**
* Test verifies resolving of collisions in the left hand of correlates.
*/
@Test
public void testCorrelatesCollisionsLeftHand() throws Exception {
IgniteSchema schema = createSchema(
createTestTable("A", "B", "C", "D")
);
String sql = "SELECT * FROM t1 as cor WHERE "
+ "EXISTS (SELECT 1 FROM t1 WHERE t1.b = cor.a) AND "
+ "EXISTS (SELECT 1 FROM t1 WHERE t1.c = cor.a) AND "
+ "EXISTS (SELECT 1 FROM t1 WHERE t1.d = cor.a)";
PlanningContext ctx = plannerCtx(sql, schema);
try (IgnitePlanner planner = ctx.planner()) {
RelNode rel = convertSubQueries(planner, ctx);
List<LogicalCorrelate> correlates = findNodes(rel, byClass(LogicalCorrelate.class));
assertEquals(3, correlates.size());
// There are collisions by correlation id.
assertEquals(correlates.get(0).getCorrelationId(), correlates.get(1).getCorrelationId());
assertEquals(correlates.get(0).getCorrelationId(), correlates.get(2).getCorrelationId());
rel = planner.replaceCorrelatesCollisions(rel);
correlates = findNodes(rel, byClass(LogicalCorrelate.class));
assertEquals(3, correlates.size());
// There are no collisions by correlation id.
assertNotEquals(correlates.get(0).getCorrelationId(), correlates.get(1).getCorrelationId());
assertNotEquals(correlates.get(0).getCorrelationId(), correlates.get(2).getCorrelationId());
assertNotEquals(correlates.get(1).getCorrelationId(), correlates.get(2).getCorrelationId());
List<LogicalFilter> filters = findNodes(rel, byClass(LogicalFilter.class)
.and(f -> RexUtils.hasCorrelation(((Filter) f).getCondition())));
assertEquals(3, filters.size());
// Filters match correlates in reverse order (we find outer correlate first, but inner filter first).
assertEquals(Set.of(correlates.get(0).getCorrelationId()),
RexUtils.extractCorrelationIds(filters.get(2).getCondition()));
assertEquals(Set.of(correlates.get(1).getCorrelationId()),
RexUtils.extractCorrelationIds(filters.get(1).getCondition()));
assertEquals(Set.of(correlates.get(2).getCorrelationId()),
RexUtils.extractCorrelationIds(filters.get(0).getCondition()));
}
}
/**
* Test verifies resolving of collisions in the right hand of correlates.
*/
@Test
public void testCorrelatesCollisionsRightHand() throws Exception {
IgniteSchema schema = createSchema(
createTestTable("A")
);
String sql = "SELECT (SELECT (SELECT (SELECT cor.a))) FROM t1 as cor";
PlanningContext ctx = plannerCtx(sql, schema);
try (IgnitePlanner planner = ctx.planner()) {
RelNode rel = convertSubQueries(planner, ctx);
List<LogicalCorrelate> correlates = findNodes(rel, byClass(LogicalCorrelate.class));
assertEquals(3, correlates.size());
// There are collisions by correlation id.
assertEquals(correlates.get(0).getCorrelationId(), correlates.get(1).getCorrelationId());
assertEquals(correlates.get(0).getCorrelationId(), correlates.get(2).getCorrelationId());
rel = planner.replaceCorrelatesCollisions(rel);
correlates = findNodes(rel, byClass(LogicalCorrelate.class));
assertEquals(1, correlates.size());
}
}
/**
* Test verifies resolving of collisions in right and left hands of correlates.
*/
@Test
public void testCorrelatesCollisionsMixed() throws Exception {
IgniteSchema schema = createSchema(
createTestTable("A", "B", "C")
);
String sql = "SELECT * FROM t1 as cor WHERE "
+ "EXISTS (SELECT 1 FROM t1 WHERE t1.b = (SELECT cor.a)) AND "
+ "EXISTS (SELECT 1 FROM t1 WHERE t1.c = (SELECT cor.a))";
PlanningContext ctx = plannerCtx(sql, schema);
try (IgnitePlanner planner = ctx.planner()) {
RelNode rel = convertSubQueries(planner, ctx);
List<LogicalCorrelate> correlates = findNodes(rel, byClass(LogicalCorrelate.class));
assertEquals(4, correlates.size());
// There are collisions by correlation id.
assertEquals(correlates.get(0).getCorrelationId(), correlates.get(1).getCorrelationId());
assertEquals(correlates.get(0).getCorrelationId(), correlates.get(2).getCorrelationId());
assertEquals(correlates.get(0).getCorrelationId(), correlates.get(3).getCorrelationId());
rel = planner.replaceCorrelatesCollisions(rel);
correlates = findNodes(rel, byClass(LogicalCorrelate.class));
assertEquals(2, correlates.size());
// There are no collisions by correlation id.
assertNotEquals(correlates.get(0).getCorrelationId(), correlates.get(1).getCorrelationId());
List<LogicalProject> projects = findNodes(rel, byClass(LogicalProject.class)
.and(f -> RexUtils.hasCorrelation(((Project) f).getProjects())));
assertEquals(2, projects.size());
assertEquals(Set.of(correlates.get(0).getCorrelationId()),
RexUtils.extractCorrelationIds(projects.get(1).getProjects()));
assertEquals(Set.of(correlates.get(1).getCorrelationId()),
RexUtils.extractCorrelationIds(projects.get(0).getProjects()));
}
}
private RelNode convertSubQueries(IgnitePlanner planner, PlanningContext ctx) throws Exception {
// Parse and validate.
SqlNode sqlNode = planner.validate(planner.parse(ctx.query()));
// Create original logical plan.
RelNode rel = planner.rel(sqlNode).rel;
// Convert sub-queries to correlates.
return planner.transform(PlannerPhase.HEP_SUBQUERIES_TO_CORRELATES, rel.getTraitSet(), rel);
}
/** Creates test table with columns of given name and INT32 type. */
static IgniteTable createTestTable(String... columns) {
assert columns.length > 0;
TableBuilder tableBuilder = TestBuilders.table()
.name("T1")
.distribution(IgniteDistributions.single());
for (String column : columns) {
tableBuilder.addColumn(column, NativeTypes.INT32);
}
return tableBuilder.build();
}
}