blob: 48a2f717a5551a7c063139b512367559bed826a5 [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.calcite.test;
import org.apache.calcite.adapter.java.ReflectiveSchema;
import org.apache.calcite.materialize.MaterializationService;
import org.apache.calcite.plan.RelOptTable;
import org.apache.calcite.prepare.Prepare;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.RelReferentialConstraint;
import org.apache.calcite.rel.RelReferentialConstraintImpl;
import org.apache.calcite.rel.RelVisitor;
import org.apache.calcite.rel.core.TableScan;
import org.apache.calcite.runtime.Hook;
import org.apache.calcite.schema.QueryableTable;
import org.apache.calcite.schema.TranslatableTable;
import org.apache.calcite.test.schemata.hr.Department;
import org.apache.calcite.test.schemata.hr.DepartmentPlus;
import org.apache.calcite.test.schemata.hr.Dependent;
import org.apache.calcite.test.schemata.hr.Employee;
import org.apache.calcite.test.schemata.hr.Event;
import org.apache.calcite.test.schemata.hr.Location;
import org.apache.calcite.util.JsonBuilder;
import org.apache.calcite.util.Smalls;
import org.apache.calcite.util.TryThreadLocal;
import org.apache.calcite.util.mapping.IntPair;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Ordering;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import java.sql.ResultSet;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
/**
* Integration tests for the materialized view rewrite mechanism. Each test has a
* query and one or more materializations (what Oracle calls materialized views)
* and checks that the materialization is used.
*/
@Tag("slow")
public class MaterializationTest {
private static final Consumer<ResultSet> CONTAINS_M0 =
CalciteAssert.checkResultContains(
"EnumerableTableScan(table=[[hr, m0]])");
private static final Consumer<ResultSet> CONTAINS_LOCATIONS =
CalciteAssert.checkResultContains(
"EnumerableTableScan(table=[[hr, locations]])");
private static final Ordering<Iterable<String>> CASE_INSENSITIVE_LIST_COMPARATOR =
Ordering.from(String.CASE_INSENSITIVE_ORDER).lexicographical();
private static final Ordering<Iterable<List<String>>> CASE_INSENSITIVE_LIST_LIST_COMPARATOR =
CASE_INSENSITIVE_LIST_COMPARATOR.lexicographical();
private static final String HR_FKUK_SCHEMA = "{\n"
+ " type: 'custom',\n"
+ " name: 'hr',\n"
+ " factory: '"
+ ReflectiveSchema.Factory.class.getName()
+ "',\n"
+ " operand: {\n"
+ " class: '" + HrFKUKSchema.class.getName() + "'\n"
+ " }\n"
+ " }\n";
private static final String HR_FKUK_MODEL = "{\n"
+ " version: '1.0',\n"
+ " defaultSchema: 'hr',\n"
+ " schemas: [\n"
+ HR_FKUK_SCHEMA
+ " ]\n"
+ "}";
@Test void testScan() {
CalciteAssert.that()
.withMaterializations(
"{\n"
+ " version: '1.0',\n"
+ " defaultSchema: 'SCOTT_CLONE',\n"
+ " schemas: [ {\n"
+ " name: 'SCOTT_CLONE',\n"
+ " type: 'custom',\n"
+ " factory: 'org.apache.calcite.adapter.clone.CloneSchema$Factory',\n"
+ " operand: {\n"
+ " jdbcDriver: '" + JdbcTest.SCOTT.driver + "',\n"
+ " jdbcUser: '" + JdbcTest.SCOTT.username + "',\n"
+ " jdbcPassword: '" + JdbcTest.SCOTT.password + "',\n"
+ " jdbcUrl: '" + JdbcTest.SCOTT.url + "',\n"
+ " jdbcSchema: 'SCOTT'\n"
+ " } } ]\n"
+ "}",
"m0",
"select empno, deptno from emp order by deptno")
.query(
"select empno, deptno from emp")
.enableMaterializations(true)
.explainContains("EnumerableTableScan(table=[[SCOTT_CLONE, m0]])")
.sameResultWithMaterializationsDisabled();
}
@Test void testViewMaterialization() {
try (TryThreadLocal.Memo ignored = Prepare.THREAD_TRIM.push(true)) {
MaterializationService.setThreadLocal();
String materialize = "select \"depts\".\"name\"\n"
+ "from \"depts\"\n"
+ "join \"emps\" on (\"emps\".\"deptno\" = \"depts\".\"deptno\")";
String query = "select \"depts\".\"name\"\n"
+ "from \"depts\"\n"
+ "join \"emps\" on (\"emps\".\"deptno\" = \"depts\".\"deptno\")";
CalciteAssert.that()
.withMaterializations(HR_FKUK_MODEL, true, "matview", materialize)
.query(query)
.enableMaterializations(true)
.explainMatches(
"", CalciteAssert.checkResultContains(
"EnumerableValues(tuples=[[{ 'noname' }]])")).returnsValue("noname");
}
}
@Test void testTableModify() {
final String m = "select \"deptno\", \"empid\", \"name\""
+ "from \"emps\" where \"deptno\" = 10";
final String q = "upsert into \"dependents\""
+ "select \"empid\" + 1 as x, \"name\""
+ "from \"emps\" where \"deptno\" = 10";
final List<List<List<String>>> substitutedNames = new ArrayList<>();
try (TryThreadLocal.Memo ignored = Prepare.THREAD_TRIM.push(true)) {
MaterializationService.setThreadLocal();
CalciteAssert.that()
.withMaterializations(HR_FKUK_MODEL,
"m0", m)
.query(q)
.withHook(Hook.SUB, (Consumer<RelNode>) r ->
substitutedNames.add(new TableNameVisitor().run(r)))
.enableMaterializations(true)
.explainContains("hr, m0");
} catch (Exception e) {
// Table "dependents" not modifiable.
}
assertThat(substitutedNames, is(list3(new String[][][]{{{"hr", "m0"}}})));
}
/** Test case for
* <a href="https://issues.apache.org/jira/browse/CALCITE-761">[CALCITE-761]
* Pre-populated materializations</a>. */
@Test void testPrePopulated() {
String q = "select distinct \"deptno\" from \"emps\"";
try (TryThreadLocal.Memo ignored = Prepare.THREAD_TRIM.push(true)) {
MaterializationService.setThreadLocal();
CalciteAssert.that()
.withMaterializations(
HR_FKUK_MODEL, builder -> {
final Map<String, Object> map = builder.map();
map.put("table", "locations");
String sql = "select distinct `deptno` as `empid`, '' as `name`\n"
+ "from `emps`";
final String sql2 = sql.replace("`", "\"");
map.put("sql", sql2);
return ImmutableList.of(map);
})
.query(q)
.enableMaterializations(true)
.sameResultWithMaterializationsDisabled();
}
}
@Test void testViewSchemaPath() {
try (TryThreadLocal.Memo ignored = Prepare.THREAD_TRIM.push(true)) {
MaterializationService.setThreadLocal();
final String m = "select empno, deptno from emp";
final String q = "select deptno from scott.emp";
final List<String> path = ImmutableList.of("SCOTT");
final JsonBuilder builder = new JsonBuilder();
final String model = "{\n"
+ " version: '1.0',\n"
+ " defaultSchema: 'hr',\n"
+ " schemas: [\n"
+ JdbcTest.SCOTT_SCHEMA
+ " ,\n"
+ " {\n"
+ " materializations: [\n"
+ " {\n"
+ " table: 'm0',\n"
+ " view: 'm0v',\n"
+ " sql: " + builder.toJsonString(m) + ",\n"
+ " viewSchemaPath: " + builder.toJsonString(path)
+ " }\n"
+ " ],\n"
+ " type: 'custom',\n"
+ " name: 'hr',\n"
+ " factory: 'org.apache.calcite.adapter.java.ReflectiveSchema$Factory',\n"
+ " operand: {\n"
+ " class: 'org.apache.calcite.test.JdbcTest$HrSchema'\n"
+ " }\n"
+ " }\n"
+ " ]\n"
+ "}\n";
CalciteAssert.that()
.withModel(model)
.query(q)
.enableMaterializations(true)
.explainMatches("", CONTAINS_M0)
.sameResultWithMaterializationsDisabled();
}
}
@Test void testMultiMaterializationMultiUsage() {
String q = "select *\n"
+ "from (select * from \"emps\" where \"empid\" < 300)\n"
+ "join (select \"deptno\", count(*) as c from \"emps\" group by \"deptno\") using (\"deptno\")";
try (TryThreadLocal.Memo ignored = Prepare.THREAD_TRIM.push(true)) {
MaterializationService.setThreadLocal();
CalciteAssert.that()
.withMaterializations(HR_FKUK_MODEL,
"m0", "select \"deptno\", count(*) as c, sum(\"empid\") as s from \"emps\" group by \"deptno\"",
"m1", "select * from \"emps\" where \"empid\" < 500")
.query(q)
.enableMaterializations(true)
.explainContains("EnumerableTableScan(table=[[hr, m0]])")
.explainContains("EnumerableTableScan(table=[[hr, m1]])")
.sameResultWithMaterializationsDisabled();
}
}
@Disabled("Creating mv for depts considering all its column throws exception")
@Test void testMultiMaterializationOnJoinQuery() {
final String q = "select *\n"
+ "from \"emps\"\n"
+ "join \"depts\" using (\"deptno\") where \"empid\" < 300 "
+ "and \"depts\".\"deptno\" > 200";
try (TryThreadLocal.Memo ignored = Prepare.THREAD_TRIM.push(true)) {
MaterializationService.setThreadLocal();
CalciteAssert.that()
.withMaterializations(HR_FKUK_MODEL,
"m0", "select * from \"emps\" where \"empid\" < 500",
"m1", "select * from \"depts\" where \"deptno\" > 100")
.query(q)
.enableMaterializations(true)
.explainContains("EnumerableTableScan(table=[[hr, m0]])")
.explainContains("EnumerableTableScan(table=[[hr, m1]])")
.sameResultWithMaterializationsDisabled();
}
}
@Test void testMaterializationSubstitution() {
String q = "select *\n"
+ "from (select * from \"emps\" where \"empid\" < 300)\n"
+ "join (select * from \"emps\" where \"empid\" < 200) using (\"empid\")";
final String[][][] expectedNames = {
{{"hr", "emps"}, {"hr", "m0"}},
{{"hr", "emps"}, {"hr", "m1"}},
{{"hr", "m0"}, {"hr", "emps"}},
{{"hr", "m0"}, {"hr", "m0"}},
{{"hr", "m0"}, {"hr", "m1"}},
{{"hr", "m1"}, {"hr", "emps"}},
{{"hr", "m1"}, {"hr", "m0"}},
{{"hr", "m1"}, {"hr", "m1"}}};
try (TryThreadLocal.Memo ignored = Prepare.THREAD_TRIM.push(true)) {
MaterializationService.setThreadLocal();
final List<List<List<String>>> substitutedNames = new ArrayList<>();
CalciteAssert.that()
.withMaterializations(HR_FKUK_MODEL,
"m0", "select * from \"emps\" where \"empid\" < 300",
"m1", "select * from \"emps\" where \"empid\" < 600")
.query(q)
.withHook(Hook.SUB, (Consumer<RelNode>) r ->
substitutedNames.add(new TableNameVisitor().run(r)))
.enableMaterializations(true)
.sameResultWithMaterializationsDisabled();
substitutedNames.sort(CASE_INSENSITIVE_LIST_LIST_COMPARATOR);
assertThat(substitutedNames, is(list3(expectedNames)));
}
}
@Test void testMaterializationSubstitution2() {
String q = "select *\n"
+ "from (select * from \"emps\" where \"empid\" < 300)\n"
+ "join (select * from \"emps\" where \"empid\" < 200) using (\"empid\")";
final String[][][] expectedNames = {
{{"hr", "emps"}, {"hr", "m0"}},
{{"hr", "emps"}, {"hr", "m1"}},
{{"hr", "emps"}, {"hr", "m2"}},
{{"hr", "m0"}, {"hr", "emps"}},
{{"hr", "m0"}, {"hr", "m0"}},
{{"hr", "m0"}, {"hr", "m1"}},
{{"hr", "m0"}, {"hr", "m2"}},
{{"hr", "m1"}, {"hr", "emps"}},
{{"hr", "m1"}, {"hr", "m0"}},
{{"hr", "m1"}, {"hr", "m1"}},
{{"hr", "m1"}, {"hr", "m2"}},
{{"hr", "m2"}, {"hr", "emps"}},
{{"hr", "m2"}, {"hr", "m0"}},
{{"hr", "m2"}, {"hr", "m1"}},
{{"hr", "m2"}, {"hr", "m2"}}};
try (TryThreadLocal.Memo ignored = Prepare.THREAD_TRIM.push(true)) {
MaterializationService.setThreadLocal();
final List<List<List<String>>> substitutedNames = new ArrayList<>();
CalciteAssert.that()
.withMaterializations(HR_FKUK_MODEL,
"m0", "select * from \"emps\" where \"empid\" < 300",
"m1", "select * from \"emps\" where \"empid\" < 600",
"m2", "select * from \"m1\"")
.query(q)
.withHook(Hook.SUB, (Consumer<RelNode>) r ->
substitutedNames.add(new TableNameVisitor().run(r)))
.enableMaterializations(true)
.sameResultWithMaterializationsDisabled();
substitutedNames.sort(CASE_INSENSITIVE_LIST_LIST_COMPARATOR);
assertThat(substitutedNames, is(list3(expectedNames)));
}
}
private static <E> List<List<List<E>>> list3(E[][][] as) {
final ImmutableList.Builder<List<List<E>>> builder =
ImmutableList.builder();
for (E[][] a : as) {
builder.add(list2(a));
}
return builder.build();
}
private static <E> List<List<E>> list2(E[][] as) {
final ImmutableList.Builder<List<E>> builder = ImmutableList.builder();
for (E[] a : as) {
builder.add(ImmutableList.copyOf(a));
}
return builder.build();
}
/**
* Implementation of RelVisitor to extract substituted table names.
*/
private static class TableNameVisitor extends RelVisitor {
private List<List<String>> names = new ArrayList<>();
List<List<String>> run(RelNode input) {
go(input);
return names;
}
@Override public void visit(RelNode node, int ordinal, @Nullable RelNode parent) {
if (node instanceof TableScan) {
RelOptTable table = node.getTable();
List<String> qName = table.getQualifiedName();
names.add(qName);
}
super.visit(node, ordinal, parent);
}
}
/**
* Hr schema with FK-UK relationship.
*/
public static class HrFKUKSchema {
@Override public String toString() {
return "HrFKUKSchema";
}
public final Employee[] emps = {
new Employee(100, 10, "Bill", 10000, 1000),
new Employee(200, 20, "Eric", 8000, 500),
new Employee(150, 10, "Sebastian", 7000, null),
new Employee(110, 10, "Theodore", 10000, 250),
};
public final Department[] depts = {
new Department(10, "Sales", Arrays.asList(emps[0], emps[2], emps[3]),
new Location(-122, 38)),
new Department(30, "Marketing", ImmutableList.of(),
new Location(0, 52)),
new Department(20, "HR", Collections.singletonList(emps[1]), null),
};
public final DepartmentPlus[] depts2 = {
new DepartmentPlus(10, "Sales", Arrays.asList(emps[0], emps[2], emps[3]),
new Location(-122, 38), new Timestamp(0)),
new DepartmentPlus(30, "Marketing", ImmutableList.of(),
new Location(0, 52), new Timestamp(0)),
new DepartmentPlus(20, "HR", Collections.singletonList(emps[1]),
null, new Timestamp(0)),
};
public final Dependent[] dependents = {
new Dependent(10, "Michael"),
new Dependent(10, "Jane"),
};
public final Dependent[] locations = {
new Dependent(10, "San Francisco"),
new Dependent(20, "San Diego"),
};
public final Event[] events = {
new Event(100, new Timestamp(0)),
new Event(200, new Timestamp(0)),
new Event(150, new Timestamp(0)),
new Event(110, null),
};
public final RelReferentialConstraint rcs0 =
RelReferentialConstraintImpl.of(
ImmutableList.of("hr", "emps"), ImmutableList.of("hr", "depts"),
ImmutableList.of(IntPair.of(1, 0)));
public QueryableTable foo(int count) {
return Smalls.generateStrings(count);
}
public TranslatableTable view(String s) {
return Smalls.view(s);
}
public TranslatableTable matview() {
return Smalls.strView("noname");
}
}
}