blob: db4463d8fa077fb18d1cb91fe089591517af2e2d [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.JavaTypeFactory;
import org.apache.calcite.jdbc.CalcitePrepare;
import org.apache.calcite.jdbc.CalciteSchema;
import org.apache.calcite.jdbc.ContextSqlValidator;
import org.apache.calcite.linq4j.Enumerator;
import org.apache.calcite.linq4j.Linq4j;
import org.apache.calcite.linq4j.QueryProvider;
import org.apache.calcite.linq4j.Queryable;
import org.apache.calcite.linq4j.tree.Expression;
import org.apache.calcite.rel.RelRoot;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
import org.apache.calcite.rel.type.RelDataTypeImpl;
import org.apache.calcite.rel.type.RelProtoDataType;
import org.apache.calcite.schema.Function;
import org.apache.calcite.schema.SchemaPlus;
import org.apache.calcite.schema.Schemas;
import org.apache.calcite.schema.TranslatableTable;
import org.apache.calcite.schema.impl.AbstractTableQueryable;
import org.apache.calcite.schema.impl.ViewTable;
import org.apache.calcite.schema.impl.ViewTableMacro;
import org.apache.calcite.server.DdlExecutorImpl;
import org.apache.calcite.sql.SqlCall;
import org.apache.calcite.sql.SqlDataTypeSpec;
import org.apache.calcite.sql.SqlIdentifier;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlNodeList;
import org.apache.calcite.sql.SqlSelect;
import org.apache.calcite.sql.SqlUtil;
import org.apache.calcite.sql.ddl.SqlColumnDeclaration;
import org.apache.calcite.sql.ddl.SqlCreateTable;
import org.apache.calcite.sql.ddl.SqlCreateView;
import org.apache.calcite.sql.dialect.CalciteSqlDialect;
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
import org.apache.calcite.sql.parser.SqlParseException;
import org.apache.calcite.sql.parser.SqlParserPos;
import org.apache.calcite.sql.pretty.SqlPrettyWriter;
import org.apache.calcite.sql.validate.SqlValidator;
import org.apache.calcite.tools.FrameworkConfig;
import org.apache.calcite.tools.Frameworks;
import org.apache.calcite.tools.Planner;
import org.apache.calcite.tools.RelConversionException;
import org.apache.calcite.tools.ValidationException;
import org.apache.calcite.util.Pair;
import org.apache.calcite.util.Util;
import com.google.common.collect.ImmutableList;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.lang.reflect.Type;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.BiConsumer;
import static org.apache.calcite.util.Static.RESOURCE;
import static java.util.Objects.requireNonNull;
/** Executes the few DDL commands. */
public class MockDdlExecutor extends DdlExecutorImpl {
/** Returns the schema in which to create an object. */
static Pair<CalciteSchema, String> schema(CalcitePrepare.Context context,
boolean mutable, SqlIdentifier id) {
final String name;
final List<String> path;
if (id.isSimple()) {
path = context.getDefaultSchemaPath();
name = id.getSimple();
} else {
path = Util.skipLast(id.names);
name = Util.last(id.names);
}
CalciteSchema schema =
mutable ? context.getMutableRootSchema()
: context.getRootSchema();
for (String p : path) {
schema = requireNonNull(schema.getSubSchema(p, true));
}
return Pair.of(schema, name);
}
/** Wraps a query to rename its columns. Used by CREATE VIEW and CREATE
* MATERIALIZED VIEW. */
static SqlNode renameColumns(@Nullable SqlNodeList columnList,
SqlNode query) {
if (columnList == null) {
return query;
}
final SqlParserPos p = query.getParserPosition();
final SqlNodeList selectList = SqlNodeList.SINGLETON_STAR;
final SqlCall from =
SqlStdOperatorTable.AS.createCall(p,
ImmutableList.<SqlNode>builder()
.add(query)
.add(new SqlIdentifier("_", p))
.addAll(columnList)
.build());
return new SqlSelect(p, null, selectList, from, null, null, null, null,
null, null, null, null, null);
}
/** Executes a {@code CREATE TABLE} command. Called via reflection. */
public void execute(SqlCreateTable create, CalcitePrepare.Context context) {
final CalciteSchema schema =
Schemas.subSchema(context.getRootSchema(),
context.getDefaultSchemaPath());
final JavaTypeFactory typeFactory = context.getTypeFactory();
final RelDataType queryRowType;
if (create.query != null) {
// A bit of a hack: pretend it's a view, to get its row type
final String sql =
create.query.toSqlString(CalciteSqlDialect.DEFAULT).getSql();
final ViewTableMacro viewTableMacro =
ViewTable.viewMacro(schema.plus(), sql, schema.path(null),
context.getObjectPath(), false);
final TranslatableTable x = viewTableMacro.apply(ImmutableList.of());
queryRowType = x.getRowType(typeFactory);
if (create.columnList != null
&& queryRowType.getFieldCount() != create.columnList.size()) {
throw SqlUtil.newContextException(create.columnList.getParserPosition(),
RESOURCE.columnCountMismatch());
}
} else {
queryRowType = null;
}
final RelDataTypeFactory.Builder builder = typeFactory.builder();
if (create.columnList != null) {
final SqlValidator validator = new ContextSqlValidator(context, false);
forEachNameType(create, (name, typeSpec) ->
builder.add(name.getSimple(), typeSpec.deriveType(validator, true)));
} else {
if (queryRowType == null) {
// "CREATE TABLE t" is invalid; because there is no "AS query" we need
// a list of column names and types, "CREATE TABLE t (INT c)".
throw SqlUtil.newContextException(create.name.getParserPosition(),
RESOURCE.createTableRequiresColumnList());
}
builder.addAll(queryRowType.getFieldList());
}
final RelDataType rowType = builder.build();
schema.add(create.name.getSimple(),
new MutableArrayTable(create.name.getSimple(),
RelDataTypeImpl.proto(rowType)));
if (create.query != null) {
populate(create.name, create.query, context);
}
}
/** Executes a {@code CREATE VIEW} command. */
public void execute(SqlCreateView create,
CalcitePrepare.Context context) {
final Pair<CalciteSchema, String> pair =
schema(context, true, create.name);
final SchemaPlus schemaPlus = pair.left.plus();
for (Function function : schemaPlus.getFunctions(pair.right)) {
if (function.getParameters().isEmpty()) {
if (!create.getReplace()) {
throw SqlUtil.newContextException(create.name.getParserPosition(),
RESOURCE.viewExists(pair.right));
}
pair.left.removeFunction(pair.right);
}
}
final SqlNode q = renameColumns(create.columnList, create.query);
final String sql = q.toSqlString(CalciteSqlDialect.DEFAULT).getSql();
final ViewTableMacro viewTableMacro =
ViewTable.viewMacro(schemaPlus, sql, pair.left.path(null),
context.getObjectPath(), false);
final TranslatableTable x = viewTableMacro.apply(ImmutableList.of());
Util.discard(x);
schemaPlus.add(pair.right, viewTableMacro);
}
/** Populates the table called {@code name} by executing {@code query}. */
protected static void populate(SqlIdentifier name, SqlNode query,
CalcitePrepare.Context context) {
// Generate, prepare and execute an "INSERT INTO table query" statement.
// (It's a bit inefficient that we convert from SqlNode to SQL and back
// again.)
final FrameworkConfig config = Frameworks.newConfigBuilder()
.defaultSchema(
requireNonNull(
Schemas.subSchema(context.getRootSchema(),
context.getDefaultSchemaPath())).plus())
.build();
final Planner planner = Frameworks.getPlanner(config);
try {
final StringBuilder buf = new StringBuilder();
final SqlPrettyWriter w =
new SqlPrettyWriter(
SqlPrettyWriter.config()
.withDialect(CalciteSqlDialect.DEFAULT)
.withAlwaysUseParentheses(false),
buf);
buf.append("INSERT INTO ");
name.unparse(w, 0, 0);
buf.append(" ");
query.unparse(w, 0, 0);
final String sql = buf.toString();
final SqlNode query1 = planner.parse(sql);
final SqlNode query2 = planner.validate(query1);
final RelRoot r = planner.rel(query2);
final PreparedStatement prepare =
context.getRelRunner().prepareStatement(r.rel);
int rowCount = prepare.executeUpdate();
Util.discard(rowCount);
prepare.close();
} catch (SqlParseException | ValidationException
| RelConversionException | SQLException e) {
throw Util.throwAsRuntime(e);
}
}
/** Calls an action for each (name, type) pair from {@code SqlCreateTable::columnList}, in which
* they alternate. */
@SuppressWarnings({"unchecked", "rawtypes"})
protected void forEachNameType(SqlCreateTable createTable,
BiConsumer<SqlIdentifier, SqlDataTypeSpec> consumer) {
createTable.columnList.forEach(sqlNode -> {
if (sqlNode instanceof SqlColumnDeclaration) {
final SqlColumnDeclaration d = (SqlColumnDeclaration) sqlNode;
consumer.accept(d.name, d.dataType);
} else {
throw new AssertionError(sqlNode.getClass());
}
});
}
/** Table backed by a Java list. */
private static class MutableArrayTable
extends AbstractModifiableTable {
final List list = new ArrayList();
private final RelProtoDataType protoRowType;
MutableArrayTable(String name, RelProtoDataType protoRowType) {
super(name);
this.protoRowType = protoRowType;
}
@Override public Collection getModifiableCollection() {
return list;
}
@Override public <T> Queryable<T> asQueryable(QueryProvider queryProvider,
SchemaPlus schema, String tableName) {
return new AbstractTableQueryable<T>(queryProvider, schema, this,
tableName) {
@Override public Enumerator<T> enumerator() {
//noinspection unchecked
return (Enumerator<T>) Linq4j.enumerator(list);
}
};
}
@Override public Type getElementType() {
return Object[].class;
}
@Override public Expression getExpression(SchemaPlus schema, String tableName,
Class clazz) {
return Schemas.tableExpression(schema, getElementType(),
tableName, clazz);
}
@Override public RelDataType getRowType(RelDataTypeFactory typeFactory) {
return protoRowType.apply(typeFactory);
}
}
}