blob: 6acddeed67157c2fa7e724f1ffe96fcd00ed4881 [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.apache.calcite.tools.Frameworks.createRootSchema;
import static org.apache.calcite.tools.Frameworks.newConfigBuilder;
import static org.apache.ignite.internal.sql.engine.externalize.RelJsonWriter.toJson;
import static org.apache.ignite.internal.sql.engine.util.Commons.FRAMEWORK_CONFIG;
import static org.apache.ignite.internal.util.CollectionUtils.first;
import static org.apache.ignite.internal.util.CollectionUtils.nullOrEmpty;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;
import com.google.common.collect.ImmutableList;
import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import org.apache.calcite.plan.RelOptListener;
import org.apache.calcite.plan.RelOptRule;
import org.apache.calcite.plan.RelOptTable;
import org.apache.calcite.plan.RelOptUtil;
import org.apache.calcite.plan.RelTraitSet;
import org.apache.calcite.rel.AbstractRelNode;
import org.apache.calcite.rel.RelCollation;
import org.apache.calcite.rel.RelDistribution;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.RelVisitor;
import org.apache.calcite.rel.core.TableScan;
import org.apache.calcite.rel.hint.HintStrategyTable;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeField;
import org.apache.calcite.rex.RexCall;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.schema.ColumnStrategy;
import org.apache.calcite.schema.SchemaPlus;
import org.apache.calcite.sql.SqlExplainFormat;
import org.apache.calcite.sql.SqlExplainLevel;
import org.apache.calcite.sql.SqlFunction;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.type.BasicSqlType;
import org.apache.calcite.sql2rel.InitializerContext;
import org.apache.calcite.sql2rel.SqlToRelConverter;
import org.apache.calcite.util.ImmutableBitSet;
import org.apache.calcite.util.Util;
import org.apache.ignite.internal.catalog.CatalogService;
import org.apache.ignite.internal.lang.IgniteStringBuilder;
import org.apache.ignite.internal.sql.engine.exec.mapping.IdGenerator;
import org.apache.ignite.internal.sql.engine.exec.mapping.QuerySplitter;
import org.apache.ignite.internal.sql.engine.externalize.RelJsonReader;
import org.apache.ignite.internal.sql.engine.framework.TestBuilders;
import org.apache.ignite.internal.sql.engine.framework.TestBuilders.HashIndexBuilder;
import org.apache.ignite.internal.sql.engine.framework.TestBuilders.SortedIndexBuilder;
import org.apache.ignite.internal.sql.engine.framework.TestBuilders.TableBuilder;
import org.apache.ignite.internal.sql.engine.prepare.Fragment;
import org.apache.ignite.internal.sql.engine.prepare.IgnitePlanner;
import org.apache.ignite.internal.sql.engine.prepare.PlannerHelper;
import org.apache.ignite.internal.sql.engine.prepare.PlanningContext;
import org.apache.ignite.internal.sql.engine.prepare.bounds.SearchBounds;
import org.apache.ignite.internal.sql.engine.rel.IgniteIndexScan;
import org.apache.ignite.internal.sql.engine.rel.IgniteProject;
import org.apache.ignite.internal.sql.engine.rel.IgniteRel;
import org.apache.ignite.internal.sql.engine.rel.IgniteSystemViewScan;
import org.apache.ignite.internal.sql.engine.rel.IgniteTableScan;
import org.apache.ignite.internal.sql.engine.rule.TableModifyToKeyValuePutRule;
import org.apache.ignite.internal.sql.engine.schema.ColumnDescriptor;
import org.apache.ignite.internal.sql.engine.schema.DefaultValueStrategy;
import org.apache.ignite.internal.sql.engine.schema.IgniteIndex.Collation;
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.schema.TableDescriptor;
import org.apache.ignite.internal.sql.engine.trait.IgniteDistribution;
import org.apache.ignite.internal.sql.engine.trait.IgniteDistributions;
import org.apache.ignite.internal.sql.engine.trait.TraitUtils;
import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
import org.apache.ignite.internal.sql.engine.util.BaseQueryContext;
import org.apache.ignite.internal.sql.engine.util.Commons;
import org.apache.ignite.internal.sql.engine.util.StatementChecker;
import org.apache.ignite.internal.testframework.IgniteAbstractTest;
import org.apache.ignite.internal.testframework.IgniteTestUtils;
import org.apache.ignite.internal.type.NativeType;
import org.apache.ignite.internal.util.Pair;
import org.jetbrains.annotations.Nullable;
/**
* Base test class for planner tests.
*/
public abstract class AbstractPlannerTest extends IgniteAbstractTest {
protected static final String[] DISABLE_KEY_VALUE_MODIFY_RULES = {
TableModifyToKeyValuePutRule.VALUES.toString(),
TableModifyToKeyValuePutRule.PROJECT.toString(),
};
protected static final IgniteTypeFactory TYPE_FACTORY = Commons.typeFactory();
protected static final int DEFAULT_TBL_SIZE = 500_000;
protected static final String DEFAULT_SCHEMA = "PUBLIC";
protected static final int DEFAULT_ZONE_ID = 0;
private static final SqlExplainLevel DEFAULT_EXPLAIN_LEVEL = SqlExplainLevel.EXPPLAN_ATTRIBUTES;
private static final AtomicInteger NEXT_TABLE_ID = new AtomicInteger(2001);
/** Last error message. */
String lastErrorMsg;
<T> Predicate<T> hasGroupSets(Function<T, List<ImmutableBitSet>> groupSets, int groupKey) {
return hasGroupSets(groupSets, List.of(groupKey));
}
<T> Predicate<T> hasGroupSets(Function<T, List<ImmutableBitSet>> groupSets, List<Integer> groupKeys) {
return (node) -> {
List<ImmutableBitSet> allGroupSets = groupSets.apply(node);
ImmutableBitSet firstGroupSet = allGroupSets.get(0);
boolean groupSetsMatch = allGroupSets.equals(List.of(ImmutableBitSet.of(groupKeys)));
boolean groupSetMatches = firstGroupSet.equals(ImmutableBitSet.of(groupKeys));
return groupSetMatches && groupSetsMatch;
};
}
<T> Predicate<T> hasNoGroupSets(Function<T, List<ImmutableBitSet>> groupSets) {
return (node) -> {
List<ImmutableBitSet> allGroupSets = groupSets.apply(node);
List<ImmutableBitSet> emptyGroupSets = List.of(ImmutableBitSet.of());
return emptyGroupSets.equals(allGroupSets);
};
}
<T extends RelNode> Predicate<T> hasCollation(RelCollation expected) {
return (node) -> {
RelCollation collation = TraitUtils.collation(node);
return expected.equals(collation);
};
}
interface TestVisitor {
void visit(RelNode node, int ordinal, RelNode parent);
}
/**
* TestRelVisitor.
* TODO Documentation https://issues.apache.org/jira/browse/IGNITE-15859
*/
public static class TestRelVisitor extends RelVisitor {
final TestVisitor visitor;
/**
* Constructor.
* TODO Documentation https://issues.apache.org/jira/browse/IGNITE-15859
*/
TestRelVisitor(TestVisitor visitor) {
this.visitor = visitor;
}
/** {@inheritDoc} */
@Override
public void visit(RelNode node, int ordinal, RelNode parent) {
visitor.visit(node, ordinal, parent);
super.visit(node, ordinal, parent);
}
}
protected static void relTreeVisit(RelNode n, TestVisitor v) {
v.visit(n, -1, null);
n.childrenAccept(new TestRelVisitor(v));
}
protected static IgniteDistribution someAffinity() {
return IgniteDistributions.affinity(0, nextTableId(), DEFAULT_ZONE_ID);
}
protected static int nextTableId() {
return NEXT_TABLE_ID.getAndIncrement();
}
/**
* FindFirstNode.
* TODO Documentation https://issues.apache.org/jira/browse/IGNITE-15859
*/
public static <T extends RelNode> T findFirstNode(RelNode plan, Predicate<RelNode> pred) {
return first(findNodes(plan, pred));
}
/**
* FindNodes.
* TODO Documentation https://issues.apache.org/jira/browse/IGNITE-15859
*/
public static <T extends RelNode> List<T> findNodes(RelNode plan, Predicate<RelNode> pred) {
List<T> ret = new ArrayList<>();
if (pred.test(plan)) {
ret.add((T) plan);
}
plan.childrenAccept(
new RelVisitor() {
@Override
public void visit(RelNode node, int ordinal, RelNode parent) {
if (pred.test(node)) {
ret.add((T) node);
}
super.visit(node, ordinal, parent);
}
}
);
return ret;
}
/**
* ByClass.
* TODO Documentation https://issues.apache.org/jira/browse/IGNITE-15859
*/
public static <T extends RelNode> Predicate<RelNode> byClass(Class<T> cls) {
return cls::isInstance;
}
/**
* ByClass.
* TODO Documentation https://issues.apache.org/jira/browse/IGNITE-15859
*/
public static <T extends RelNode> Predicate<RelNode> byClass(Class<T> cls, Predicate<RelNode> pred) {
return node -> cls.isInstance(node) && pred.test(node);
}
/**
* Create planner context for specified query.
*/
protected PlanningContext plannerCtx(String sql, IgniteSchema publicSchema, String... disabledRules) {
return plannerCtx(sql, Collections.singleton(publicSchema), null, List.of(), disabledRules);
}
protected PlanningContext plannerCtx(
String sql,
Collection<IgniteSchema> schemas,
HintStrategyTable hintStrategies,
List<Object> params,
String... disabledRules
) {
Int2ObjectArrayMap<Object> paramsMap = new Int2ObjectArrayMap<>();
for (int i = 0; i < params.size(); i++) {
Object value = params.get(i);
if (value != Unspecified.UNKNOWN) {
paramsMap.put(i, value);
}
}
PlanningContext ctx = PlanningContext.builder()
.parentContext(baseQueryContext(schemas, hintStrategies, params.toArray()))
.query(sql)
.parameters(paramsMap)
.build();
IgnitePlanner planner = ctx.planner();
assertNotNull(planner);
planner.setDisabledRules(Set.copyOf(Arrays.asList(disabledRules)));
return ctx;
}
protected BaseQueryContext baseQueryContext(
Collection<IgniteSchema> schemas,
@Nullable HintStrategyTable hintStrategies,
Object... params
) {
SchemaPlus rootSchema = createRootSchema(false);
SchemaPlus dfltSchema = null;
for (IgniteSchema igniteSchema : schemas) {
SchemaPlus schema = rootSchema.add(igniteSchema.getName(), igniteSchema);
if (dfltSchema == null || DEFAULT_SCHEMA.equals(schema.getName())) {
dfltSchema = schema;
}
}
SqlToRelConverter.Config relConvCfg = FRAMEWORK_CONFIG.getSqlToRelConverterConfig();
if (hintStrategies != null) {
relConvCfg = relConvCfg.withHintStrategyTable(hintStrategies);
}
return BaseQueryContext.builder()
.queryId(UUID.randomUUID())
.frameworkConfig(
newConfigBuilder(FRAMEWORK_CONFIG)
.defaultSchema(dfltSchema)
.sqlToRelConverterConfig(relConvCfg)
.build()
)
.parameters(params)
.build();
}
/**
* Returns the first found node of given class from expression tree.
*
* @param expr Expression to search in.
* @param clz Target class of the node we are looking for.
* @param <T> Type of the node we are looking for.
* @return the first matching node in terms of DFS or null if there is no such node.
*/
public static <T> T findFirst(RexNode expr, Class<T> clz) {
if (clz.isAssignableFrom(expr.getClass())) {
return clz.cast(expr);
}
if (!(expr instanceof RexCall)) {
return null;
}
RexCall call = (RexCall) expr;
for (RexNode op : call.getOperands()) {
T res = findFirst(op, clz);
if (res != null) {
return res;
}
}
return null;
}
/**
* Optimize the specified query and build query physical plan for a test.
*/
protected IgniteRel physicalPlan(
String sql,
IgniteSchema publicSchema,
String... disabledRules) throws Exception {
return physicalPlan(sql, publicSchema, null, disabledRules);
}
/**
* Optimize the specified query and build query physical plan for a test.
*/
protected IgniteRel physicalPlan(
String sql,
IgniteSchema publicSchema,
@Nullable RelOptListener listener,
String... disabledRules) throws Exception {
return physicalPlan(sql, Collections.singleton(publicSchema), null, List.of(), listener, disabledRules);
}
protected IgniteRel physicalPlan(
String sql,
Collection<IgniteSchema> schemas,
HintStrategyTable hintStrategies,
List<Object> params,
@Nullable RelOptListener listener,
String... disabledRules
) throws Exception {
PlanningContext planningContext = plannerCtx(sql, schemas, hintStrategies, params, disabledRules);
return physicalPlan(planningContext, listener);
}
protected IgniteRel physicalPlan(PlanningContext ctx) throws Exception {
return physicalPlan(ctx, null);
}
protected IgniteRel physicalPlan(PlanningContext ctx, @Nullable RelOptListener listener) throws Exception {
try (IgnitePlanner planner = ctx.planner()) {
assertNotNull(planner);
assertNotNull(ctx.query());
if (listener != null) {
planner.addListener(listener);
}
return physicalPlan(planner, ctx.query());
}
}
protected IgniteRel physicalPlan(IgnitePlanner planner, String qry) throws Exception {
// Parse
SqlNode sqlNode = planner.parse(qry);
// Validate
sqlNode = planner.validate(sqlNode);
try {
return PlannerHelper.optimize(sqlNode, planner);
} catch (Throwable ex) {
System.err.println(planner.dump());
throw ex;
}
}
/**
* Select.
* TODO Documentation https://issues.apache.org/jira/browse/IGNITE-15859
*/
public static <T> List<T> select(List<T> src, int... idxs) {
ArrayList<T> res = new ArrayList<>(idxs.length);
for (int idx : idxs) {
res.add(src.get(idx));
}
return res;
}
/**
* Predicate builder for "Operator has column names" condition.
*/
protected <T extends RelNode> Predicate<T> hasColumns(String... cols) {
return node -> {
RelDataType rowType = node.getRowType();
String err = "Unexpected columns [expected=" + Arrays.toString(cols) + ", actual=" + rowType.getFieldNames() + ']';
if (rowType.getFieldCount() != cols.length) {
lastErrorMsg = err;
return false;
}
for (int i = 0; i < cols.length; i++) {
if (!cols[i].equals(rowType.getFieldNames().get(i))) {
lastErrorMsg = err;
return false;
}
}
return true;
};
}
protected <T extends RelNode> void assertPlan(
String sql,
IgniteSchema schema,
Predicate<T> predicate,
String... disabledRules
) throws Exception {
assertPlan(sql, Collections.singleton(schema), predicate, List.of(), disabledRules);
}
protected <T extends RelNode> void assertPlan(
String sql,
IgniteSchema schema,
Predicate<T> predicate,
List<Object> params
) throws Exception {
assertPlan(sql, Collections.singleton(schema), predicate, params);
}
protected <T extends RelNode> void assertPlan(
String sql,
Collection<IgniteSchema> schemas,
Predicate<T> predicate,
String... disabledRules
) throws Exception {
assertPlan(sql, schemas, predicate, null, List.of(), disabledRules);
}
protected <T extends RelNode> void assertPlan(
String sql,
Collection<IgniteSchema> schemas,
Predicate<T> predicate,
List<Object> params,
String... disabledRules
) throws Exception {
assertPlan(sql, schemas, predicate, null, params, disabledRules);
}
protected <T extends RelNode> void assertPlan(
String sql,
Collection<IgniteSchema> schemas,
Predicate<T> predicate,
HintStrategyTable hintStrategies,
List<Object> params,
String... disabledRules
) throws Exception {
IgniteRel plan = physicalPlan(sql, schemas, hintStrategies, params, null, disabledRules);
String planString = RelOptUtil.dumpPlan("", plan, SqlExplainFormat.TEXT, DEFAULT_EXPLAIN_LEVEL);
log.info("statement: {}\n{}", sql, planString);
checkSplitAndSerialization(plan, schemas);
try {
if (predicate.test((T) plan)) {
return;
}
} catch (Throwable th) {
if (lastErrorMsg == null) {
lastErrorMsg = th.getMessage();
}
String invalidPlanMsg = "Failed to validate plan (" + lastErrorMsg + "):\n"
+ RelOptUtil.toString(plan, SqlExplainLevel.ALL_ATTRIBUTES);
fail(invalidPlanMsg, th);
}
fail("Invalid plan (" + lastErrorMsg + "):\n"
+ RelOptUtil.toString(plan, SqlExplainLevel.ALL_ATTRIBUTES));
}
/**
* Predicate builder for "Instance of class" condition.
*/
protected <T> Predicate<T> isInstanceOf(Class<T> cls) {
return node -> {
if (cls.isInstance(node)) {
return true;
}
lastErrorMsg = "Unexpected node class [node=" + node + ", cls=" + cls.getSimpleName() + ']';
return false;
};
}
/**
* Predicate builder for "Table scan with given name" condition.
*/
protected <T extends RelNode> Predicate<IgniteTableScan> isTableScan(String tableName) {
return isInstanceOf(IgniteTableScan.class).and(
n -> {
String scanTableName = Util.last(n.getTable().getQualifiedName());
if (tableName.equalsIgnoreCase(scanTableName)) {
return true;
}
lastErrorMsg = "Unexpected table name [exp=" + tableName + ", act=" + scanTableName + ']';
return false;
});
}
/**
* Predicate builder for "Table scan with given name" condition.
*/
protected <T extends RelNode> Predicate<IgniteSystemViewScan> isSystemViewScan(String viewName) {
return isInstanceOf(IgniteSystemViewScan.class).and(
n -> {
String scanViewName = Util.last(n.getTable().getQualifiedName());
if (viewName.equalsIgnoreCase(scanViewName)) {
return true;
}
lastErrorMsg = "Unexpected system view name [exp=" + viewName + ", act=" + scanViewName + ']';
return false;
});
}
/**
* Predicate builder for "Operator has distribution" condition.
*/
protected <T extends IgniteRel> Predicate<IgniteRel> hasDistribution(IgniteDistribution distribution) {
return node -> {
if (distribution.getType() == RelDistribution.Type.HASH_DISTRIBUTED
&& node.distribution().getType() == RelDistribution.Type.HASH_DISTRIBUTED
) {
return distribution.satisfies(node.distribution());
}
return distribution.equals(node.distribution());
};
}
/**
* Predicate builder for "Any child satisfy predicate" condition.
*/
protected <T extends RelNode> Predicate<RelNode> hasChildThat(Predicate<T> predicate) {
return new Predicate<RelNode>() {
public boolean checkRecursively(RelNode node) {
if (predicate.test((T) node)) {
return true;
}
for (RelNode input : node.getInputs()) {
if (checkRecursively(input)) {
return true;
}
}
return false;
}
@Override
public boolean test(RelNode node) {
for (RelNode input : node.getInputs()) {
if (checkRecursively(input)) {
return true;
}
}
lastErrorMsg = "Not found child for defined condition [node=" + node + ']';
return false;
}
};
}
/**
* Predicate builder for "Current node or any child satisfy predicate" condition.
*/
protected Predicate<RelNode> nodeOrAnyChild(Predicate<? extends RelNode> predicate) {
return (Predicate<RelNode>) predicate.or(hasChildThat(predicate));
}
/**
* Predicate builder for "Input with given index satisfy predicate" condition.
*/
protected <T extends RelNode> Predicate<RelNode> input(Predicate<T> predicate) {
return input(0, predicate);
}
/**
* Predicate builder for "Input with given index satisfy predicate" condition.
*/
protected <T extends RelNode> Predicate<RelNode> input(int idx, Predicate<T> predicate) {
return node -> {
int size = nullOrEmpty(node.getInputs()) ? 0 : node.getInputs().size();
if (size <= idx) {
lastErrorMsg = "No input for node [idx=" + idx + ", node=" + node + ']';
return false;
}
return predicate.test((T) node.getInput(idx));
};
}
/**
* Creates public schema from provided tables.
*
* @param tbls Tables to create schema for.
* @return Public schema.
*/
protected static IgniteSchema createSchema(IgniteTable... tbls) {
return createSchema(CatalogService.DEFAULT_SCHEMA_NAME, tbls);
}
/**
* Creates schema with given name from provided tables.
*
* @param schemaName Schema name.
* @param tbls Tables to create schema for.
* @return Schema with given name.
*/
protected static IgniteSchema createSchema(String schemaName, IgniteTable... tbls) {
return new IgniteSchema(schemaName, 0, Arrays.asList(tbls));
}
protected void checkSplitAndSerialization(IgniteRel rel, IgniteSchema publicSchema) {
checkSplitAndSerialization(rel, Collections.singleton(publicSchema));
}
protected void checkSplitAndSerialization(IgniteRel rel, Collection<IgniteSchema> schemas) {
assertNotNull(rel);
assertFalse(schemas.isEmpty());
List<Fragment> fragments = new QuerySplitter(new IdGenerator(0), rel.getCluster()).split(rel);
List<String> serialized = new ArrayList<>(fragments.size());
for (Fragment fragment : fragments) {
serialized.add(toJson(fragment.root()));
}
assertNotNull(serialized);
List<String> nodes = new ArrayList<>(4);
for (int i = 0; i < 4; i++) {
nodes.add(UUID.randomUUID().toString());
}
List<RelNode> deserializedNodes = new ArrayList<>();
BaseQueryContext ctx = baseQueryContext(schemas, null);
for (String s : serialized) {
RelJsonReader reader = new RelJsonReader(ctx.catalogReader());
deserializedNodes.add(reader.read(s));
}
List<RelNode> expectedRels = fragments.stream()
.map(Fragment::root)
.collect(Collectors.toList());
assertEquals(expectedRels.size(), deserializedNodes.size(), "Invalid deserialization fragments count");
for (int i = 0; i < expectedRels.size(); ++i) {
RelNode expected = expectedRels.get(i);
RelNode deserialized = deserializedNodes.get(i);
clearTraits(expected);
clearTraits(deserialized);
// Hints are not serializable.
clearHints(expected);
if (!expected.deepEquals(deserialized)) {
IgniteStringBuilder sb = new IgniteStringBuilder();
fail(
sb.app("Invalid serialization / deserialization.").nl()
.app("Expected:").nl().app(expected).nl()
.app("Deserialized:").nl().app(deserialized).toString()
);
}
}
}
/**
* Predicate builder for "Index scan with given name" condition.
*/
protected <T extends RelNode> Predicate<IgniteIndexScan> isIndexScan(String tableName, String idxName) {
return isInstanceOf(IgniteIndexScan.class).and(
n -> {
String scanTableName = Util.last(n.getTable().getQualifiedName());
if (!tableName.equalsIgnoreCase(scanTableName)) {
lastErrorMsg = "Unexpected table name [exp=" + tableName + ", act=" + scanTableName + ']';
return false;
}
if (!idxName.equalsIgnoreCase(n.indexName())) {
lastErrorMsg = "Unexpected index name [exp=" + idxName + ", act=" + n.indexName() + ']';
return false;
}
return true;
});
}
protected void clearTraits(RelNode rel) {
IgniteTestUtils.setFieldValue(rel, AbstractRelNode.class, "traitSet", RelTraitSet.createEmpty());
rel.getInputs().forEach(this::clearTraits);
}
protected void clearHints(RelNode rel) {
if (rel instanceof TableScan) {
IgniteTestUtils.setFieldValue(rel, TableScan.class, "hints", ImmutableList.of());
}
rel.getInputs().forEach(this::clearHints);
}
protected Predicate<? extends RelNode> projectFromTable(String tableName, String... exprs) {
return nodeOrAnyChild(
isInstanceOf(IgniteProject.class)
.and(projection -> {
String actualProjStr = projection.getProjects().toString();
String expectedProjStr = Arrays.asList(exprs).toString();
return actualProjStr.equals(expectedProjStr);
})
.and(input(isTableScan(tableName)))
);
}
/**
* TestTableDescriptor.
* TODO Documentation https://issues.apache.org/jira/browse/IGNITE-15859
*/
public static class TestTableDescriptor implements TableDescriptor {
private final Supplier<IgniteDistribution> distributionSupp;
private final RelDataType rowType;
/**
* Constructor.
* TODO Documentation https://issues.apache.org/jira/browse/IGNITE-15859
*/
public TestTableDescriptor(Supplier<IgniteDistribution> distribution, RelDataType rowType) {
this.distributionSupp = distribution;
this.rowType = rowType;
}
@Override
public Iterator<ColumnDescriptor> iterator() {
throw new UnsupportedOperationException();
}
/** {@inheritDoc} */
@Override
public IgniteDistribution distribution() {
return distributionSupp.get();
}
/** {@inheritDoc} */
@Override
public RelDataType rowType(IgniteTypeFactory factory, ImmutableBitSet usedColumns) {
return rowType;
}
/** {@inheritDoc} */
@Override
public ColumnDescriptor columnDescriptor(String fieldName) {
RelDataTypeField field = rowType.getField(fieldName, false, false);
NativeType nativeType = field.getType() instanceof BasicSqlType ? IgniteTypeFactory.relDataTypeToNative(field.getType()) : null;
return new TestColumnDescriptor(field.getIndex(), fieldName, nativeType);
}
/** {@inheritDoc} */
@Override
public ColumnDescriptor columnDescriptor(int idx) {
RelDataTypeField field = rowType.getFieldList().get(idx);
NativeType nativeType = field.getType() instanceof BasicSqlType ? IgniteTypeFactory.relDataTypeToNative(field.getType()) : null;
return new TestColumnDescriptor(field.getIndex(), field.getName(), nativeType);
}
/** {@inheritDoc} */
@Override
public int columnsCount() {
return rowType.getFieldCount();
}
/** {@inheritDoc} */
@Override
public boolean isGeneratedAlways(RelOptTable table, int idxColumn) {
throw new AssertionError();
}
/** {@inheritDoc} */
@Override
public ColumnStrategy generationStrategy(RelOptTable table, int idxColumn) {
throw new AssertionError();
}
/** {@inheritDoc} */
@Override
public RexNode newColumnDefaultValue(RelOptTable table, int idxColumn, InitializerContext context) {
throw new AssertionError();
}
/** {@inheritDoc} */
@Override
public BiFunction<InitializerContext, RelNode, RelNode> postExpressionConversionHook() {
throw new AssertionError();
}
/** {@inheritDoc} */
@Override
public RexNode newAttributeInitializer(RelDataType type, SqlFunction constructor, int idxAttribute,
List<RexNode> constructorArgs, InitializerContext context) {
throw new AssertionError();
}
}
static class TestColumnDescriptor implements ColumnDescriptor {
private final int idx;
private final String name;
private final NativeType physicalType;
TestColumnDescriptor(int idx, String name, NativeType physicalType) {
this.idx = idx;
this.name = name;
this.physicalType = physicalType;
}
/** {@inheritDoc} */
@Override
public boolean hidden() {
return false;
}
/** {@inheritDoc} */
@Override
public boolean nullable() {
return true;
}
/** {@inheritDoc} */
@Override
public boolean key() {
return false;
}
/** {@inheritDoc} */
@Override
public DefaultValueStrategy defaultStrategy() {
return DefaultValueStrategy.DEFAULT_NULL;
}
/** {@inheritDoc} */
@Override
public String name() {
return name;
}
/** {@inheritDoc} */
@Override
public int logicalIndex() {
return idx;
}
/** {@inheritDoc} */
@Override
public NativeType physicalType() {
if (physicalType == null) {
throw new AssertionError();
}
return physicalType;
}
/** {@inheritDoc} */
@Override
public Object defaultValue() {
throw new AssertionError();
}
}
Predicate<SearchBounds> empty() {
return Objects::isNull;
}
/**
* Wrapper for RelOptListener with empty realization for each of their methods. Good choice in case you need implement just one or few
* methods for listener.
*/
static class NoopRelOptListener implements RelOptListener {
@Override
public void relEquivalenceFound(RelEquivalenceEvent event) {}
@Override
public void ruleAttempted(RuleAttemptedEvent event) {}
@Override
public void ruleProductionSucceeded(RuleProductionEvent event) {}
@Override
public void relDiscarded(RelDiscardedEvent event) {}
@Override
public void relChosen(RelChosenEvent event) {}
}
/**
* Listener which keeps information about attempts of applying optimization rules.
*/
public class RuleAttemptListener extends NoopRelOptListener {
Set<RelOptRule> attemptedRules = new HashSet<>();
@Override
public void ruleAttempted(RuleAttemptedEvent event) {
if (event.isBefore()) {
attemptedRules.add(event.getRuleCall().getRule());
}
}
public boolean isApplied(RelOptRule rule) {
return attemptedRules.stream().anyMatch(r -> rule.toString().equals(r.toString()));
}
public void reset() {
attemptedRules.clear();
}
}
/**
* A shorthand for {@code checkStatement().sql(statement, params)}.
*/
public StatementChecker sql(String statement, Object... params) {
return checkStatement().sql(statement, params);
}
/**
* Creates an instance of {@link StatementChecker statement checker} to test plans.
* <pre>
* checkStatement().sql("SELECT 1").ok()
* </pre>
*/
public StatementChecker checkStatement() {
return new PlanChecker();
}
/**
* Creates an instance of {@link PlanChecker statement checker} with the given setup.
* A shorthand for {@code checkStatement().setup(func)}.
*/
public StatementChecker checkStatement(Consumer<StatementChecker> setup) {
return new PlanChecker().setup(setup);
}
/**
* An implementation of {@link PlanChecker} with initialized {@link SqlPrepare} to test plans.
*/
public class PlanChecker extends StatementChecker {
PlanChecker() {
super((schema, sql, params, rulesToDisable) -> {
PlanningContext planningContext = plannerCtx(sql, List.of(schema), HintStrategyTable.EMPTY, params, rulesToDisable);
IgnitePlanner planner = planningContext.planner();
IgniteRel igniteRel = physicalPlan(planner, sql);
return new Pair<>(igniteRel, planner);
});
}
/** {@inheritDoc} */
@Override
protected void checkRel(IgniteRel igniteRel, IgnitePlanner planner, IgniteSchema schema) {
checkSplitAndSerialization(igniteRel, schema);
}
}
/** Creates schema using given table builders. */
protected static IgniteSchema createSchemaFrom(Function<TableBuilder, TableBuilder>... tables) {
return createSchema(Arrays.stream(tables).map(op -> op.apply(TestBuilders.table()).build()).toArray(IgniteTable[]::new));
}
/** Creates a function, which builds sorted index with given column names and with default collation. */
protected static UnaryOperator<TableBuilder> addSortIndex(String... columns) {
return tableBuilder -> {
SortedIndexBuilder indexBuilder = tableBuilder.sortedIndex();
StringBuilder nameBuilder = new StringBuilder("IDX");
for (String colName : columns) {
indexBuilder.addColumn(colName.toUpperCase(), Collation.ASC_NULLS_LAST);
nameBuilder.append('_').append(colName);
}
return indexBuilder.name(nameBuilder.toString()).end();
};
}
/** Creates a function, which builds sorted index with given column names and with desired collation. */
protected static UnaryOperator<TableBuilder> addSortIndex(String col1, Collation col1Collation, String col2, Collation col2Collation) {
return tableBuilder -> tableBuilder.sortedIndex()
.name("IDX" + '_' + col1 + '_' + col2)
.addColumn(col1.toUpperCase(), col1Collation)
.addColumn(col2.toUpperCase(), col2Collation)
.end();
}
/** Creates a function, which builds hash index with given column names. */
protected static UnaryOperator<TableBuilder> addHashIndex(String... columns) {
return tableBuilder -> {
HashIndexBuilder indexBuilder = tableBuilder.hashIndex();
StringBuilder nameBuilder = new StringBuilder("IDX");
for (String colName : columns) {
indexBuilder.addColumn(colName.toUpperCase());
nameBuilder.append('_').append(colName);
}
return indexBuilder.name(nameBuilder.toString()).end();
};
}
/** Sets table size. */
static Function<TableBuilder, TableBuilder> setSize(int size) {
return t -> t.size(size);
}
/** Unspecified dynamic parameter. */
public enum Unspecified {
/** Placeholder for unspecified dynamic parameter. */
UNKNOWN
}
}