/*
* 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.where;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import io.crate.analyze.EvaluatingNormalizer;
import io.crate.analyze.GeneratedColumnComparisonReplacer;
import io.crate.analyze.SymbolToTrueVisitor;
import io.crate.analyze.WhereClause;
import io.crate.analyze.relations.DocTableRelation;
import io.crate.analyze.symbol.Literal;
import io.crate.analyze.symbol.Symbol;
import io.crate.analyze.symbol.Symbols;
import io.crate.metadata.*;
import io.crate.metadata.doc.DocSysColumns;
import io.crate.metadata.doc.DocTableInfo;
import io.crate.operation.reference.partitioned.PartitionExpression;
import org.elasticsearch.common.collect.Tuple;
import javax.annotation.Nullable;
import java.util.*;
public class WhereClauseAnalyzer {
private final static GeneratedColumnComparisonReplacer GENERATED_COLUMN_COMPARISON_REPLACER = new GeneratedColumnComparisonReplacer();
private final Functions functions;
private final DocTableInfo tableInfo;
private final EqualityExtractor eqExtractor;
private final EvaluatingNormalizer normalizer;
public WhereClauseAnalyzer(Functions functions, DocTableRelation tableRelation) {
this.functions = functions;
this.tableInfo = tableRelation.tableInfo();
this.normalizer = new EvaluatingNormalizer(
functions, RowGranularity.CLUSTER, ReplaceMode.COPY, null, tableRelation);
this.eqExtractor = new EqualityExtractor(normalizer);
}
public WhereClause analyze(WhereClause whereClause, TransactionContext transactionContext) {
if (!whereClause.hasQuery()) {
return whereClause;
}
Set<Symbol> clusteredBy = null;
if (whereClause.hasQuery()) {
WhereClauseValidator.validate(whereClause);
Symbol query = GENERATED_COLUMN_COMPARISON_REPLACER.replaceIfPossible(whereClause.query(), tableInfo);
if (!whereClause.query().equals(query)) {
whereClause = new WhereClause(normalizer.normalize(query, transactionContext));
}
}
List<ColumnIdent> pkCols;
boolean versionInQuery = Symbols.containsColumn(whereClause.query(), DocSysColumns.VERSION);
if (versionInQuery) {
pkCols = new ArrayList<>(tableInfo.primaryKey().size() + 1);
pkCols.addAll(tableInfo.primaryKey());
pkCols.add(DocSysColumns.VERSION);
} else {
pkCols = tableInfo.primaryKey();
}
List<List<Symbol>> pkValues = eqExtractor.extractExactMatches(pkCols, whereClause.query(), transactionContext);
if (!pkCols.isEmpty() && pkValues != null) {
int clusterdIdx = -1;
if (tableInfo.clusteredBy() != null) {
clusterdIdx = tableInfo.primaryKey().indexOf(tableInfo.clusteredBy());
clusteredBy = new HashSet<>(pkValues.size());
}
List<Integer> partitionsIdx = null;
if (tableInfo.isPartitioned()) {
partitionsIdx = new ArrayList<>(tableInfo.partitionedByColumns().size());
for (ColumnIdent columnIdent : tableInfo.partitionedBy()) {
int posPartitionColumn = tableInfo.primaryKey().indexOf(columnIdent);
if (posPartitionColumn >= 0) {
partitionsIdx.add(posPartitionColumn);
}
}
}
whereClause.docKeys(new DocKeys(pkValues, versionInQuery, clusterdIdx, partitionsIdx));
if (clusterdIdx >= 0) {
for (List<Symbol> row : pkValues) {
clusteredBy.add(row.get(clusterdIdx));
}
whereClause.clusteredBy(clusteredBy);
}
} else {
clusteredBy = getClusteredByLiterals(whereClause, eqExtractor, transactionContext);
}
if (clusteredBy != null) {
whereClause.clusteredBy(clusteredBy);
}
if (tableInfo.isPartitioned() && !whereClause.docKeys().isPresent()) {
whereClause = resolvePartitions(new WhereClause(normalizer.normalize(whereClause.query(), transactionContext)), tableInfo, functions, transactionContext);
}
return whereClause;
}
@Nullable
private Set<Symbol> getClusteredByLiterals(WhereClause whereClause, EqualityExtractor ee, TransactionContext transactionContext) {
if (tableInfo.clusteredBy() != null) {
List<List<Symbol>> clusteredValues = ee.extractParentMatches(
ImmutableList.of(tableInfo.clusteredBy()),
whereClause.query(), transactionContext);
if (clusteredValues != null) {
Set<Symbol> clusteredBy = new HashSet<>(clusteredValues.size());
for (List<Symbol> row : clusteredValues) {
clusteredBy.add(row.get(0));
}
return clusteredBy;
}
}
return null;
}
private static PartitionReferenceResolver preparePartitionResolver(List<Reference> partitionColumns) {
List<PartitionExpression> partitionExpressions = new ArrayList<>(partitionColumns.size());
int idx = 0;
for (Reference partitionedByColumn : partitionColumns) {
partitionExpressions.add(new PartitionExpression(partitionedByColumn, idx));
idx++;
}
return new PartitionReferenceResolver(partitionExpressions);
}
private static WhereClause resolvePartitions(WhereClause whereClause,
DocTableInfo tableInfo,
Functions functions,
TransactionContext transactionContext) {
assert tableInfo.isPartitioned() : "table must be partitioned in order to resolve partitions";
assert whereClause.partitions().isEmpty() : "partitions must not be analyzed twice";
if (tableInfo.partitions().isEmpty()) {
return WhereClause.NO_MATCH; // table is partitioned but has no data / no partitions
}
PartitionReferenceResolver partitionReferenceResolver = preparePartitionResolver(
tableInfo.partitionedByColumns());
EvaluatingNormalizer normalizer = new EvaluatingNormalizer(
functions, RowGranularity.PARTITION, ReplaceMode.COPY, partitionReferenceResolver, null);
Symbol normalized;
Map<Symbol, List<Literal>> queryPartitionMap = new HashMap<>();
for (PartitionName partitionName : tableInfo.partitions()) {
for (PartitionExpression partitionExpression : partitionReferenceResolver.expressions()) {
partitionExpression.setNextRow(partitionName);
}
normalized = normalizer.normalize(whereClause.query(), transactionContext);
assert normalized != null : "normalizing a query must not return null";
if (normalized.equals(whereClause.query())) {
return whereClause; // no partition columns inside the where clause
}
boolean canMatch = WhereClause.canMatch(normalized);
if (canMatch) {
List<Literal> partitions = queryPartitionMap.get(normalized);
if (partitions == null) {
partitions = new ArrayList<>();
queryPartitionMap.put(normalized, partitions);
}
partitions.add(Literal.of(partitionName.asIndexName()));
}
}
if (queryPartitionMap.size() == 1) {
Map.Entry<Symbol, List<Literal>> entry = Iterables.getOnlyElement(queryPartitionMap.entrySet());
whereClause = new WhereClause(
entry.getKey(),
whereClause.docKeys().orElse(null),
new ArrayList<String>(entry.getValue().size()));
whereClause.partitions(entry.getValue());
return whereClause;
} else if (queryPartitionMap.size() > 0) {
return tieBreakPartitionQueries(normalizer, queryPartitionMap, whereClause, transactionContext);
} else {
return WhereClause.NO_MATCH;
}
}
private static WhereClause tieBreakPartitionQueries(EvaluatingNormalizer normalizer,
Map<Symbol, List<Literal>> queryPartitionMap,
WhereClause whereClause,
TransactionContext transactionContext) throws UnsupportedOperationException {
/*
* Got multiple normalized queries which all could match.
* This might be the case if one partition resolved to null
*
* e.g.
*
* p = 1 and x = 2
*
* might lead to
*
* null and x = 2
* true and x = 2
*
* At this point it is unknown if they really match.
* In order to figure out if they could potentially match all conditions involving references are now set to true
*
* null and true -> can't match
* true and true -> can match, can use this query + partition
*
* If there is still more than 1 query that can match it's not possible to execute the query :(
*/
List<Tuple<Symbol, List<Literal>>> canMatch = new ArrayList<>();
SymbolToTrueVisitor symbolToTrueVisitor = new SymbolToTrueVisitor();
for (Map.Entry<Symbol, List<Literal>> entry : queryPartitionMap.entrySet()) {
Symbol query = entry.getKey();
List<Literal> partitions = entry.getValue();
Symbol symbol = symbolToTrueVisitor.process(query, null);
Symbol normalized = normalizer.normalize(symbol, transactionContext);
assert normalized instanceof Literal :
"after normalization and replacing all reference occurrences with true there must only be a literal left";
Object value = ((Literal) normalized).value();
if (value != null && (Boolean) value) {
canMatch.add(new Tuple<>(query, partitions));
}
}
if (canMatch.size() == 1) {
Tuple<Symbol, List<Literal>> symbolListTuple = canMatch.get(0);
WhereClause where = new WhereClause(symbolListTuple.v1(),
whereClause.docKeys().orElse(null),
new ArrayList<String>(symbolListTuple.v2().size()));
where.partitions(symbolListTuple.v2());
return where;
}
throw new UnsupportedOperationException(
"logical conjunction of the conditions in the WHERE clause which " +
"involve partitioned columns led to a query that can't be executed.");
}
}