/*
* Licensed to CRATE Technology GmbH ("Crate") under one or more contributor
* license agreements. See the NOTICE file distributed with this work for
* additional information regarding copyright ownership. Crate 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.
*
* However, if you have executed another commercial license agreement
* with Crate these terms will supersede the license and you may use the
* software solely pursuant to the terms of the relevant commercial agreement.
*/
package io.crate.analyze;
import com.google.common.base.Joiner;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import io.crate.action.sql.SessionContext;
import io.crate.analyze.expressions.ExpressionAnalysisContext;
import io.crate.analyze.expressions.ExpressionAnalyzer;
import io.crate.analyze.expressions.ValueNormalizer;
import io.crate.analyze.relations.*;
import io.crate.analyze.symbol.DynamicReference;
import io.crate.analyze.symbol.Field;
import io.crate.analyze.symbol.InputColumn;
import io.crate.analyze.symbol.Symbol;
import io.crate.analyze.symbol.format.SymbolFormatter;
import io.crate.analyze.symbol.format.SymbolPrinter;
import io.crate.exceptions.ColumnUnknownException;
import io.crate.metadata.*;
import io.crate.metadata.doc.DocTableInfo;
import io.crate.metadata.table.Operation;
import io.crate.sql.tree.Assignment;
import io.crate.sql.tree.InsertFromSubquery;
import java.util.*;
class InsertFromSubQueryAnalyzer {
private final Functions functions;
private final Schemas schemas;
private final RelationAnalyzer relationAnalyzer;
private static class ValuesResolver implements ValuesAwareExpressionAnalyzer.ValuesResolver {
private final DocTableRelation targetTableRelation;
private final List<Reference> targetColumns;
ValuesResolver(DocTableRelation targetTableRelation, List<Reference> targetColumns) {
this.targetTableRelation = targetTableRelation;
this.targetColumns = targetColumns;
}
@Override
public Symbol allocateAndResolve(Field argumentColumn) {
Reference reference = targetTableRelation.resolveField(argumentColumn);
int i = targetColumns.indexOf(reference);
if (i < 0) {
throw new IllegalArgumentException(SymbolFormatter.format(
"Column '%s' that is used in the VALUES() expression is not part of the target column list",
argumentColumn));
}
assert reference != null : "reference must not be null";
return new InputColumn(i, argumentColumn.valueType());
}
}
InsertFromSubQueryAnalyzer(Functions functions, Schemas schemas, RelationAnalyzer relationAnalyzer) {
this.functions = functions;
this.schemas = schemas;
this.relationAnalyzer = relationAnalyzer;
}
public AnalyzedStatement analyze(InsertFromSubquery node, Analysis analysis) {
DocTableInfo tableInfo = schemas.getTableInfo(
TableIdent.of(node.table(), analysis.sessionContext().defaultSchema()), Operation.INSERT);
DocTableRelation tableRelation = new DocTableRelation(tableInfo);
FieldProvider fieldProvider = new NameFieldProvider(tableRelation);
QueriedRelation source = (QueriedRelation) relationAnalyzer.analyze(node.subQuery(), analysis);
List<Reference> targetColumns = new ArrayList<>(resolveTargetColumns(node.columns(), tableInfo, source.fields().size()));
validateColumnsAndAddCastsIfNecessary(targetColumns, source.querySpec());
Map<Reference, Symbol> onDuplicateKeyAssignments = null;
if (!node.onDuplicateKeyAssignments().isEmpty()) {
onDuplicateKeyAssignments = processUpdateAssignments(
tableRelation,
targetColumns,
analysis.sessionContext(),
analysis.parameterContext(),
analysis.transactionContext(),
fieldProvider,
node.onDuplicateKeyAssignments());
}
return new InsertFromSubQueryAnalyzedStatement(
source,
tableInfo,
targetColumns,
onDuplicateKeyAssignments);
}
private static Collection<Reference> resolveTargetColumns(Collection<String> targetColumnNames,
DocTableInfo targetTable,
int numSourceColumns) {
if (targetColumnNames.isEmpty()) {
return targetColumnsFromTargetTable(targetTable, numSourceColumns);
}
LinkedHashSet<Reference> columns = new LinkedHashSet<>(targetColumnNames.size());
for (String targetColumnName : targetColumnNames) {
ColumnIdent columnIdent = new ColumnIdent(targetColumnName);
Reference reference = targetTable.getReference(columnIdent);
Reference targetReference;
if (reference == null) {
DynamicReference dynamicReference = targetTable.getDynamic(columnIdent, true);
if (dynamicReference == null) {
throw new ColumnUnknownException(targetColumnName);
}
targetReference = dynamicReference;
} else {
targetReference = reference;
}
if (!columns.add(targetReference)) {
throw new IllegalArgumentException(String.format(Locale.ENGLISH, "reference '%s' repeated", targetColumnName));
}
}
return columns;
}
private static Collection<Reference> targetColumnsFromTargetTable(DocTableInfo targetTable, int numSourceColumns) {
List<Reference> columns = new ArrayList<>(targetTable.columns().size());
int idx = 0;
for (Reference reference : targetTable.columns()) {
if (idx > numSourceColumns) {
break;
}
columns.add(reference);
idx++;
}
return columns;
}
/**
* validate that result columns from subquery match explicit insert columns
* or complete table schema
*/
private static void validateColumnsAndAddCastsIfNecessary(List<Reference> targetColumns,
QuerySpec querySpec) {
if (targetColumns.size() != querySpec.outputs().size()) {
Joiner commaJoiner = Joiner.on(", ");
throw new IllegalArgumentException(String.format(Locale.ENGLISH,
"Number of target columns (%s) of insert statement doesn't match number of source columns (%s)",
commaJoiner.join(Iterables.transform(targetColumns, Reference.TO_COLUMN_NAME)),
commaJoiner.join(Iterables.transform(querySpec.outputs(), SymbolPrinter.FUNCTION))));
}
int failedCastPosition = querySpec.castOutputs(Iterators.transform(targetColumns.iterator(), Symbol::valueType));
if (failedCastPosition >= 0) {
Symbol failedSource = querySpec.outputs().get(failedCastPosition);
Reference failedTarget = targetColumns.get(failedCastPosition);
throw new IllegalArgumentException(String.format(Locale.ENGLISH,
"Type of subquery column %s (%s) does not match is not convertable to the type of table column %s (%s)",
failedSource,
failedSource.valueType(),
failedTarget.ident().columnIdent().fqn(),
failedTarget.valueType()
));
}
}
private Map<Reference, Symbol> processUpdateAssignments(DocTableRelation tableRelation,
List<Reference> targetColumns,
SessionContext sessionContext,
ParameterContext parameterContext,
TransactionContext transactionContext,
FieldProvider fieldProvider,
List<Assignment> assignments) {
ExpressionAnalyzer expressionAnalyzer = new ExpressionAnalyzer(
functions, sessionContext, parameterContext, fieldProvider, null);
ExpressionAnalysisContext expressionAnalysisContext = new ExpressionAnalysisContext();
EvaluatingNormalizer normalizer = new EvaluatingNormalizer(
functions,
RowGranularity.CLUSTER,
ReplaceMode.COPY,
null,
tableRelation);
ValueNormalizer valuesNormalizer = new ValueNormalizer(schemas);
ValuesResolver valuesResolver = new ValuesResolver(tableRelation, targetColumns);
ValuesAwareExpressionAnalyzer valuesAwareExpressionAnalyzer = new ValuesAwareExpressionAnalyzer(
functions, sessionContext, parameterContext, fieldProvider, valuesResolver);
Map<Reference, Symbol> updateAssignments = new HashMap<>(assignments.size());
for (Assignment assignment : assignments) {
Reference columnName = tableRelation.resolveField(
(Field) expressionAnalyzer.convert(assignment.columnName(), expressionAnalysisContext));
assert columnName != null : "columnName must not be null";
Symbol valueSymbol = normalizer.normalize(
valuesAwareExpressionAnalyzer.convert(assignment.expression(), expressionAnalysisContext),
transactionContext);
Symbol assignmentExpression = valuesNormalizer.normalizeInputForReference(valueSymbol, columnName);
updateAssignments.put(columnName, assignmentExpression);
}
return updateAssignments;
}
}