/* * Licensed to 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.relations; import com.google.common.collect.ImmutableList; import io.crate.analyze.*; import io.crate.analyze.symbol.Aggregations; import io.crate.analyze.symbol.Field; import io.crate.analyze.symbol.Symbol; import io.crate.analyze.symbol.format.SymbolPrinter; import io.crate.metadata.OutputName; import io.crate.metadata.Path; import io.crate.metadata.ReplaceMode; import io.crate.metadata.ReplacingSymbolVisitor; import io.crate.operation.operator.AndOperator; import io.crate.planner.Limits; import javax.annotation.Nullable; import java.util.*; import java.util.function.Function; final class SubselectRewriter { private final static Visitor INSTANCE = new Visitor(); private SubselectRewriter() { } public static AnalyzedRelation rewrite(AnalyzedRelation relation) { return INSTANCE.process(relation, null); } private final static class Visitor extends AnalyzedRelationVisitor<QueriedSelectRelation, AnalyzedRelation> { @Override protected AnalyzedRelation visitAnalyzedRelation(AnalyzedRelation relation, QueriedSelectRelation parent) { return relation; } @Override public AnalyzedRelation visitQueriedSelectRelation(QueriedSelectRelation relation, QueriedSelectRelation parent) { boolean mergedWithParent = false; QuerySpec currentQS = relation.querySpec(); if (parent != null) { FieldReplacer fieldReplacer = new FieldReplacer(currentQS.outputs()); QuerySpec parentQS = parent.querySpec().copyAndReplace(fieldReplacer); if (canBeMerged(currentQS, parentQS)) { QuerySpec currentWithParentMerged = mergeQuerySpec(currentQS, parentQS); relation = new QueriedSelectRelation( relation.subRelation(), namesFromOutputs(currentWithParentMerged.outputs(), fieldReplacer.replacedFieldsByNewOutput), currentWithParentMerged ); mergedWithParent = true; } } AnalyzedRelation origSubRelation = relation.subRelation(); QueriedRelation subRelation = (QueriedRelation) process(origSubRelation, relation); if (origSubRelation == subRelation) { return relation; } if (!mergedWithParent && parent != null) { parent.subRelation(subRelation); FieldReplacer fieldReplacer = new FieldReplacer(subRelation.fields()); parent.querySpec().replace(fieldReplacer); } if (relation.subRelation() != origSubRelation) { return relation; } return subRelation; } @Override public AnalyzedRelation visitQueriedTable(QueriedTable table, QueriedSelectRelation parent) { if (parent == null) { return table; } QuerySpec currentQS = table.querySpec(); FieldReplacer fieldReplacer = new FieldReplacer(currentQS.outputs()); QuerySpec parentQS = parent.querySpec().copyAndReplace(fieldReplacer); if (canBeMerged(currentQS, parentQS)) { QuerySpec currentWithParentMerged = mergeQuerySpec(currentQS, parentQS); return new QueriedTable( table.tableRelation(), namesFromOutputs(currentWithParentMerged.outputs(), fieldReplacer.replacedFieldsByNewOutput), currentWithParentMerged ); } return table; } @Override public AnalyzedRelation visitQueriedDocTable(QueriedDocTable table, QueriedSelectRelation parent) { if (parent == null) { return table; } QuerySpec currentQS = table.querySpec(); FieldReplacer fieldReplacer = new FieldReplacer(currentQS.outputs()); QuerySpec parentQS = parent.querySpec().copyAndReplace(fieldReplacer); if (canBeMerged(currentQS, parentQS)) { QuerySpec currentWithParentMerged = mergeQuerySpec(currentQS, parentQS); return new QueriedDocTable( table.tableRelation(), namesFromOutputs(currentWithParentMerged.outputs(), fieldReplacer.replacedFieldsByNewOutput), currentWithParentMerged ); } return table; } @Override public AnalyzedRelation visitMultiSourceSelect(MultiSourceSelect multiSourceSelect, QueriedSelectRelation parent) { if (parent == null) { return multiSourceSelect; } QuerySpec currentQS = multiSourceSelect.querySpec(); FieldReplacer fieldReplacer = new FieldReplacer(currentQS.outputs()); QuerySpec parentQS = parent.querySpec().copyAndReplace(fieldReplacer); if (canBeMerged(currentQS, parentQS)) { QuerySpec currentWithParentMerged = mergeQuerySpec(currentQS, parentQS); return new MultiSourceSelect( multiSourceSelect.sources(), namesFromOutputs(currentWithParentMerged.outputs(), fieldReplacer.replacedFieldsByNewOutput), currentWithParentMerged, multiSourceSelect.joinPairs() ); } return multiSourceSelect; } } /** * @return new output names of a relation which has been merged with it's parent. * It tries to preserve the alias/name of the parent if possible */ private static Collection<Path> namesFromOutputs(Collection<? extends Symbol> outputs, Map<Symbol, Field> replacedFieldsByNewOutput) { List<Path> outputNames = new ArrayList<>(outputs.size()); for (Symbol output : outputs) { Field field = replacedFieldsByNewOutput.get(output); if (field == null) { if (output instanceof Path) { outputNames.add((Path) output); } else { outputNames.add(new OutputName(SymbolPrinter.INSTANCE.printSimple(output))); } } else { outputNames.add(field); } } return outputNames; } private static QuerySpec mergeQuerySpec(QuerySpec childQSpec, QuerySpec parentQSpec) { // merge everything: validation that merge is possible has already been done. OrderBy newOrderBy; if (parentQSpec.hasAggregates() || parentQSpec.groupBy().isPresent()) { // select avg(x) from (select x from t order by x) // -> can't keep order, but it doesn't matter for aggregations anyway so // only keep the one from the parent Qspec newOrderBy = parentQSpec.orderBy().orElse(null); } else { newOrderBy = tryReplace(childQSpec.orderBy(), parentQSpec.orderBy()); } return new QuerySpec() .outputs(parentQSpec.outputs()) .where(mergeWhere(childQSpec.where(), parentQSpec.where())) .orderBy(newOrderBy) .offset(Limits.mergeAdd(childQSpec.offset(), parentQSpec.offset())) .limit(Limits.mergeMin(childQSpec.limit(), parentQSpec.limit())) .groupBy(pushGroupBy(childQSpec.groupBy(), parentQSpec.groupBy())) .having(pushHaving(childQSpec.having(), parentQSpec.having())) .hasAggregates(childQSpec.hasAggregates() || parentQSpec.hasAggregates()); } private static WhereClause mergeWhere(WhereClause where1, WhereClause where2) { if (!where1.hasQuery() || where1 == WhereClause.MATCH_ALL) { return where2; } else if (!where2.hasQuery() || where2 == WhereClause.MATCH_ALL) { return where1; } return new WhereClause(AndOperator.join(ImmutableList.of(where2.query(), where1.query()))); } /** * "Merge" OrderBy of child & parent relations. * <p/> * examples: * <pre> * childOrderBy: col1, col2 * parentOrderBy: col2, col3, col4 * * merged OrderBy returned: col2, col3, col4 * </pre> * <p/> * <pre> * childOrderBy: col1, col2 * parentOrderBy: * * merged OrderBy returned: col1, col2 * </pre> * * @param childOrderBy The OrderBy of the relation being processed * @param parentOrderBy The OrderBy of the parent relation (outer select, union, etc.) * @return The merged orderBy */ @Nullable private static OrderBy tryReplace(Optional<OrderBy> childOrderBy, Optional<OrderBy> parentOrderBy) { if (parentOrderBy.isPresent()) { return parentOrderBy.get(); } return childOrderBy.orElse(null); } @Nullable private static List<Symbol> pushGroupBy(Optional<List<Symbol>> childGroupBy, Optional<List<Symbol>> parentGroupBy) { assert !(childGroupBy.isPresent() && parentGroupBy.isPresent()) : "Cannot merge 'group by' if exists in both parent and child relations"; return childGroupBy.map(Optional::of).orElse(parentGroupBy).orElse(null); } @Nullable private static HavingClause pushHaving(Optional<HavingClause> childHaving, Optional<HavingClause> parentHaving) { assert !(childHaving.isPresent() && parentHaving.isPresent()) : "Cannot merge 'having' if exists in both parent and child relations"; return childHaving.map(Optional::of).orElse(parentHaving).orElse(null); } private static boolean canBeMerged(QuerySpec childQuerySpec, QuerySpec parentQuerySpec) { WhereClause parentWhere = parentQuerySpec.where(); boolean parentHasWhere = !parentWhere.equals(WhereClause.MATCH_ALL); boolean childHasLimit = childQuerySpec.limit().isPresent(); if (parentHasWhere && childHasLimit) { return false; } boolean parentHasAggregations = parentQuerySpec.hasAggregates() || parentQuerySpec.groupBy().isPresent(); boolean childHasAggregations = childQuerySpec.hasAggregates() || childQuerySpec.groupBy().isPresent(); if (parentHasAggregations && (childHasLimit || childHasAggregations)) { return false; } Optional<OrderBy> childOrderBy = childQuerySpec.orderBy(); Optional<OrderBy> parentOrderBy = parentQuerySpec.orderBy(); if (childHasLimit && childOrderBy.isPresent() && parentOrderBy.isPresent() && !childOrderBy.equals(parentOrderBy)) { return false; } if (parentHasWhere && parentWhere.hasQuery() && Aggregations.containsAggregation(parentWhere.query())) { return false; } return true; } private final static class FieldReplacer extends ReplacingSymbolVisitor<Void> implements Function<Symbol, Symbol> { private final List<? extends Symbol> outputs; final HashMap<Symbol, Field> replacedFieldsByNewOutput = new HashMap<>(); FieldReplacer(List<? extends Symbol> outputs) { super(ReplaceMode.COPY); this.outputs = outputs; } @Override public Symbol visitField(Field field, Void context) { Symbol newOutput = outputs.get(field.index()); replacedFieldsByNewOutput.put(newOutput, field); return newOutput; } @Override public Symbol apply(Symbol symbol) { return process(symbol, null); } } }