Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
io.trino.sql.planner.QueryPlanner Maven / Gradle / Ivy
/*
* Licensed 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 io.trino.sql.planner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import io.trino.Session;
import io.trino.metadata.Metadata;
import io.trino.metadata.ResolvedFunction;
import io.trino.metadata.TableHandle;
import io.trino.metadata.TableLayout;
import io.trino.metadata.TableMetadata;
import io.trino.spi.connector.ColumnHandle;
import io.trino.spi.connector.ColumnSchema;
import io.trino.spi.connector.RowChangeParadigm;
import io.trino.spi.connector.SortOrder;
import io.trino.spi.type.DecimalType;
import io.trino.spi.type.Type;
import io.trino.sql.ExpressionUtils;
import io.trino.sql.NodeUtils;
import io.trino.sql.PlannerContext;
import io.trino.sql.analyzer.Analysis;
import io.trino.sql.analyzer.Analysis.GroupingSetAnalysis;
import io.trino.sql.analyzer.Analysis.MergeAnalysis;
import io.trino.sql.analyzer.Analysis.ResolvedWindow;
import io.trino.sql.analyzer.Analysis.SelectExpression;
import io.trino.sql.analyzer.FieldId;
import io.trino.sql.analyzer.RelationType;
import io.trino.sql.planner.RelationPlanner.PatternRecognitionComponents;
import io.trino.sql.planner.plan.AggregationNode;
import io.trino.sql.planner.plan.AggregationNode.Aggregation;
import io.trino.sql.planner.plan.AssignUniqueId;
import io.trino.sql.planner.plan.Assignments;
import io.trino.sql.planner.plan.DataOrganizationSpecification;
import io.trino.sql.planner.plan.FilterNode;
import io.trino.sql.planner.plan.GroupIdNode;
import io.trino.sql.planner.plan.LimitNode;
import io.trino.sql.planner.plan.MarkDistinctNode;
import io.trino.sql.planner.plan.MergeProcessorNode;
import io.trino.sql.planner.plan.MergeWriterNode;
import io.trino.sql.planner.plan.OffsetNode;
import io.trino.sql.planner.plan.PatternRecognitionNode;
import io.trino.sql.planner.plan.PlanNode;
import io.trino.sql.planner.plan.ProjectNode;
import io.trino.sql.planner.plan.SimplePlanRewriter;
import io.trino.sql.planner.plan.SortNode;
import io.trino.sql.planner.plan.TableWriterNode.MergeParadigmAndTypes;
import io.trino.sql.planner.plan.TableWriterNode.MergeTarget;
import io.trino.sql.planner.plan.UnionNode;
import io.trino.sql.planner.plan.ValuesNode;
import io.trino.sql.planner.plan.WindowNode;
import io.trino.sql.tree.Cast;
import io.trino.sql.tree.CoalesceExpression;
import io.trino.sql.tree.ComparisonExpression;
import io.trino.sql.tree.DecimalLiteral;
import io.trino.sql.tree.Delete;
import io.trino.sql.tree.Expression;
import io.trino.sql.tree.FetchFirst;
import io.trino.sql.tree.FieldReference;
import io.trino.sql.tree.FrameBound;
import io.trino.sql.tree.FunctionCall;
import io.trino.sql.tree.FunctionCall.NullTreatment;
import io.trino.sql.tree.GenericLiteral;
import io.trino.sql.tree.IfExpression;
import io.trino.sql.tree.IntervalLiteral;
import io.trino.sql.tree.IsNotNullPredicate;
import io.trino.sql.tree.IsNullPredicate;
import io.trino.sql.tree.Join;
import io.trino.sql.tree.LambdaArgumentDeclaration;
import io.trino.sql.tree.LambdaExpression;
import io.trino.sql.tree.LogicalExpression;
import io.trino.sql.tree.LongLiteral;
import io.trino.sql.tree.MeasureDefinition;
import io.trino.sql.tree.Merge;
import io.trino.sql.tree.MergeCase;
import io.trino.sql.tree.MergeDelete;
import io.trino.sql.tree.MergeInsert;
import io.trino.sql.tree.MergeUpdate;
import io.trino.sql.tree.Node;
import io.trino.sql.tree.NodeRef;
import io.trino.sql.tree.NotExpression;
import io.trino.sql.tree.NullLiteral;
import io.trino.sql.tree.Offset;
import io.trino.sql.tree.OrderBy;
import io.trino.sql.tree.PatternRecognitionRelation.RowsPerMatch;
import io.trino.sql.tree.Query;
import io.trino.sql.tree.QuerySpecification;
import io.trino.sql.tree.Relation;
import io.trino.sql.tree.Row;
import io.trino.sql.tree.SearchedCaseExpression;
import io.trino.sql.tree.SortItem;
import io.trino.sql.tree.SubscriptExpression;
import io.trino.sql.tree.Table;
import io.trino.sql.tree.Union;
import io.trino.sql.tree.Update;
import io.trino.sql.tree.VariableDefinition;
import io.trino.sql.tree.WhenClause;
import io.trino.sql.tree.WindowFrame;
import io.trino.sql.tree.WindowOperation;
import io.trino.type.TypeCoercion;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.Iterables.getOnlyElement;
import static io.trino.SystemSessionProperties.getMaxRecursionDepth;
import static io.trino.SystemSessionProperties.isSkipRedundantSort;
import static io.trino.spi.StandardErrorCode.CONSTRAINT_VIOLATION;
import static io.trino.spi.StandardErrorCode.INVALID_ARGUMENTS;
import static io.trino.spi.StandardErrorCode.INVALID_WINDOW_FRAME;
import static io.trino.spi.StandardErrorCode.MERGE_TARGET_ROW_MULTIPLE_MATCHES;
import static io.trino.spi.StandardErrorCode.NOT_SUPPORTED;
import static io.trino.spi.connector.ConnectorMergeSink.DELETE_OPERATION_NUMBER;
import static io.trino.spi.connector.ConnectorMergeSink.INSERT_OPERATION_NUMBER;
import static io.trino.spi.connector.ConnectorMergeSink.UPDATE_OPERATION_NUMBER;
import static io.trino.spi.type.BigintType.BIGINT;
import static io.trino.spi.type.BooleanType.BOOLEAN;
import static io.trino.spi.type.IntegerType.INTEGER;
import static io.trino.spi.type.TinyintType.TINYINT;
import static io.trino.spi.type.VarbinaryType.VARBINARY;
import static io.trino.sql.ExpressionUtils.and;
import static io.trino.sql.NodeUtils.getSortItemsFromOrderBy;
import static io.trino.sql.analyzer.ExpressionAnalyzer.isNumericType;
import static io.trino.sql.analyzer.TypeSignatureTranslator.toSqlType;
import static io.trino.sql.planner.GroupingOperationRewriter.rewriteGroupingOperation;
import static io.trino.sql.planner.LogicalPlanner.failFunction;
import static io.trino.sql.planner.OrderingScheme.sortItemToSortOrder;
import static io.trino.sql.planner.PlanBuilder.newPlanBuilder;
import static io.trino.sql.planner.ScopeAware.scopeAwareKey;
import static io.trino.sql.planner.SystemPartitioningHandle.FIXED_HASH_DISTRIBUTION;
import static io.trino.sql.planner.plan.AggregationNode.groupingSets;
import static io.trino.sql.planner.plan.AggregationNode.singleAggregation;
import static io.trino.sql.planner.plan.AggregationNode.singleGroupingSet;
import static io.trino.sql.planner.plan.WindowNode.Frame.DEFAULT_FRAME;
import static io.trino.sql.tree.BooleanLiteral.TRUE_LITERAL;
import static io.trino.sql.tree.ComparisonExpression.Operator.GREATER_THAN_OR_EQUAL;
import static io.trino.sql.tree.ComparisonExpression.Operator.LESS_THAN_OR_EQUAL;
import static io.trino.sql.tree.IntervalLiteral.IntervalField.DAY;
import static io.trino.sql.tree.IntervalLiteral.IntervalField.YEAR;
import static io.trino.sql.tree.IntervalLiteral.Sign.POSITIVE;
import static io.trino.sql.tree.WindowFrame.Type.GROUPS;
import static io.trino.sql.tree.WindowFrame.Type.RANGE;
import static io.trino.sql.tree.WindowFrame.Type.ROWS;
import static io.trino.type.IntervalDayTimeType.INTERVAL_DAY_TIME;
import static io.trino.type.IntervalYearMonthType.INTERVAL_YEAR_MONTH;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
class QueryPlanner
{
private final Analysis analysis;
private final SymbolAllocator symbolAllocator;
private final PlanNodeIdAllocator idAllocator;
private final Map, Symbol> lambdaDeclarationToSymbolMap;
private final PlannerContext plannerContext;
private final TypeCoercion typeCoercion;
private final Session session;
private final SubqueryPlanner subqueryPlanner;
private final Optional outerContext;
private final Map, RelationPlan> recursiveSubqueries;
QueryPlanner(
Analysis analysis,
SymbolAllocator symbolAllocator,
PlanNodeIdAllocator idAllocator,
Map, Symbol> lambdaDeclarationToSymbolMap,
PlannerContext plannerContext,
Optional outerContext,
Session session,
Map, RelationPlan> recursiveSubqueries)
{
requireNonNull(analysis, "analysis is null");
requireNonNull(symbolAllocator, "symbolAllocator is null");
requireNonNull(idAllocator, "idAllocator is null");
requireNonNull(lambdaDeclarationToSymbolMap, "lambdaDeclarationToSymbolMap is null");
requireNonNull(plannerContext, "plannerContext is null");
requireNonNull(session, "session is null");
requireNonNull(outerContext, "outerContext is null");
requireNonNull(recursiveSubqueries, "recursiveSubqueries is null");
this.analysis = analysis;
this.symbolAllocator = symbolAllocator;
this.idAllocator = idAllocator;
this.lambdaDeclarationToSymbolMap = lambdaDeclarationToSymbolMap;
this.plannerContext = plannerContext;
this.typeCoercion = new TypeCoercion(plannerContext.getTypeManager()::getType);
this.session = session;
this.outerContext = outerContext;
this.subqueryPlanner = new SubqueryPlanner(analysis, symbolAllocator, idAllocator, lambdaDeclarationToSymbolMap, plannerContext, typeCoercion, outerContext, session, recursiveSubqueries);
this.recursiveSubqueries = recursiveSubqueries;
}
public RelationPlan plan(Query query)
{
PlanBuilder builder = planQueryBody(query);
List orderBy = analysis.getOrderByExpressions(query);
builder = subqueryPlanner.handleSubqueries(builder, orderBy, analysis.getSubqueries(query));
List selectExpressions = analysis.getSelectExpressions(query);
List outputs = selectExpressions.stream()
.map(SelectExpression::getExpression)
.collect(toImmutableList());
builder = builder.appendProjections(Iterables.concat(orderBy, outputs), symbolAllocator, idAllocator);
Optional orderingScheme = orderingScheme(builder, query.getOrderBy(), analysis.getOrderByExpressions(query));
builder = sort(builder, orderingScheme);
builder = offset(builder, query.getOffset());
builder = limit(builder, query.getLimit(), orderingScheme);
builder = builder.appendProjections(outputs, symbolAllocator, idAllocator);
return new RelationPlan(
builder.getRoot(),
analysis.getScope(query),
computeOutputs(builder, outputs),
outerContext);
}
public RelationPlan planExpand(Query query)
{
checkArgument(analysis.isExpandableQuery(query), "query is not registered as expandable");
Union union = (Union) query.getQueryBody();
ImmutableList.Builder recursionSteps = ImmutableList.builder();
// plan anchor relation
Relation anchorNode = union.getRelations().get(0);
RelationPlan anchorPlan = new RelationPlanner(analysis, symbolAllocator, idAllocator, lambdaDeclarationToSymbolMap, plannerContext, outerContext, session, recursiveSubqueries)
.process(anchorNode, null);
// prune anchor plan outputs to contain only the symbols exposed in the scope
NodeAndMappings prunedAnchorPlan = pruneInvisibleFields(anchorPlan, idAllocator);
// if the anchor plan has duplicate output symbols, add projection on top to make the symbols unique
// This is necessary to successfully unroll recursion: the recursion step relation must follow
// the same layout while it might not have duplicate outputs where the anchor plan did
NodeAndMappings disambiguatedAnchorPlan = disambiguateOutputs(prunedAnchorPlan, symbolAllocator, idAllocator);
anchorPlan = new RelationPlan(disambiguatedAnchorPlan.getNode(), analysis.getScope(query), disambiguatedAnchorPlan.getFields(), outerContext);
recursionSteps.add(copy(anchorPlan.getRoot(), anchorPlan.getFieldMappings()));
// plan recursion step
Relation recursionStepRelation = union.getRelations().get(1);
RelationPlan recursionStepPlan = new RelationPlanner(
analysis,
symbolAllocator,
idAllocator,
lambdaDeclarationToSymbolMap,
plannerContext,
outerContext,
session,
ImmutableMap.of(NodeRef.of(analysis.getRecursiveReference(query)), anchorPlan))
.process(recursionStepRelation, null);
// coerce recursion step outputs and prune them to contain only the symbols exposed in the scope
NodeAndMappings coercedRecursionStep;
List types = analysis.getRelationCoercion(recursionStepRelation);
if (types == null) {
coercedRecursionStep = pruneInvisibleFields(recursionStepPlan, idAllocator);
}
else {
coercedRecursionStep = coerce(recursionStepPlan, types, symbolAllocator, idAllocator);
}
NodeAndMappings replacementSpot = new NodeAndMappings(anchorPlan.getRoot(), anchorPlan.getFieldMappings());
PlanNode recursionStep = coercedRecursionStep.getNode();
List mappings = coercedRecursionStep.getFields();
// unroll recursion
int maxRecursionDepth = getMaxRecursionDepth(session);
for (int i = 0; i < maxRecursionDepth; i++) {
recursionSteps.add(copy(recursionStep, mappings));
NodeAndMappings replacement = copy(recursionStep, mappings);
// if the recursion step plan has duplicate output symbols, add projection on top to make the symbols unique
// This is necessary to successfully unroll recursion: the relation on the next recursion step must follow
// the same layout while it might not have duplicate outputs where the plan for this step did
replacement = disambiguateOutputs(replacement, symbolAllocator, idAllocator);
recursionStep = replace(recursionStep, replacementSpot, replacement);
replacementSpot = replacement;
}
// after the last recursion step, check if the recursion converged. the last step is expected to return empty result
// 1. append window to count rows
NodeAndMappings checkConvergenceStep = copy(recursionStep, mappings);
Symbol countSymbol = symbolAllocator.newSymbol("count", BIGINT);
ResolvedFunction function = plannerContext.getMetadata().resolveBuiltinFunction("count", ImmutableList.of());
WindowNode.Function countFunction = new WindowNode.Function(function, ImmutableList.of(), DEFAULT_FRAME, false);
WindowNode windowNode = new WindowNode(
idAllocator.getNextId(),
checkConvergenceStep.getNode(),
new DataOrganizationSpecification(ImmutableList.of(), Optional.empty()),
ImmutableMap.of(countSymbol, countFunction),
Optional.empty(),
ImmutableSet.of(),
0);
// 2. append filter to fail on non-empty result
String recursionLimitExceededMessage = format("Recursion depth limit exceeded (%s). Use 'max_recursion_depth' session property to modify the limit.", maxRecursionDepth);
Expression predicate = new IfExpression(
new ComparisonExpression(
GREATER_THAN_OR_EQUAL,
countSymbol.toSymbolReference(),
new GenericLiteral("BIGINT", "0")),
new Cast(
failFunction(plannerContext.getMetadata(), NOT_SUPPORTED, recursionLimitExceededMessage),
toSqlType(BOOLEAN)),
TRUE_LITERAL);
FilterNode filterNode = new FilterNode(idAllocator.getNextId(), windowNode, predicate);
recursionSteps.add(new NodeAndMappings(filterNode, checkConvergenceStep.getFields()));
// union all the recursion steps
List recursionStepsToUnion = recursionSteps.build();
List unionOutputSymbols = anchorPlan.getFieldMappings().stream()
.map(symbol -> symbolAllocator.newSymbol(symbol, "_expanded"))
.collect(toImmutableList());
ImmutableListMultimap.Builder unionSymbolMapping = ImmutableListMultimap.builder();
for (NodeAndMappings plan : recursionStepsToUnion) {
for (int i = 0; i < unionOutputSymbols.size(); i++) {
unionSymbolMapping.put(unionOutputSymbols.get(i), plan.getFields().get(i));
}
}
List nodesToUnion = recursionStepsToUnion.stream()
.map(NodeAndMappings::getNode)
.collect(toImmutableList());
PlanNode result = new UnionNode(idAllocator.getNextId(), nodesToUnion, unionSymbolMapping.build(), unionOutputSymbols);
if (union.isDistinct()) {
result = singleAggregation(
idAllocator.getNextId(),
result,
ImmutableMap.of(),
singleGroupingSet(result.getOutputSymbols()));
}
return new RelationPlan(result, anchorPlan.getScope(), unionOutputSymbols, outerContext);
}
// Return a copy of the plan and remapped field mappings. In the copied plan:
// - all PlanNodeIds are replaced with new values,
// - all symbols are replaced with new symbols.
// Copying the plan might reorder symbols. The returned field mappings keep the original
// order and might be used to identify the original output symbols with their copies.
private NodeAndMappings copy(PlanNode plan, List fields)
{
return PlanCopier.copyPlan(plan, fields, plannerContext.getMetadata(), symbolAllocator, idAllocator);
}
private PlanNode replace(PlanNode plan, NodeAndMappings replacementSpot, NodeAndMappings replacement)
{
checkArgument(
replacementSpot.getFields().size() == replacement.getFields().size(),
"mismatching outputs in replacement, expected: %s, got: %s",
replacementSpot.getFields().size(),
replacement.getFields().size());
return SimplePlanRewriter.rewriteWith(new SimplePlanRewriter()
{
@Override
protected PlanNode visitPlan(PlanNode node, RewriteContext context)
{
return node.replaceChildren(node.getSources().stream()
.map(child -> {
if (child == replacementSpot.getNode()) {
// add projection to adjust symbols
Assignments.Builder assignments = Assignments.builder();
for (int i = 0; i < replacementSpot.getFields().size(); i++) {
assignments.put(replacementSpot.getFields().get(i), replacement.getFields().get(i).toSymbolReference());
}
return new ProjectNode(idAllocator.getNextId(), replacement.getNode(), assignments.build());
}
return context.rewrite(child);
})
.collect(toImmutableList()));
}
}, plan, null);
}
public RelationPlan plan(QuerySpecification node)
{
PlanBuilder builder = planFrom(node);
builder = filter(builder, analysis.getWhere(node), node);
builder = aggregate(builder, node);
builder = filter(builder, analysis.getHaving(node), node);
builder = planWindowFunctions(node, builder, ImmutableList.copyOf(analysis.getWindowFunctions(node)));
builder = planWindowMeasures(node, builder, ImmutableList.copyOf(analysis.getWindowMeasures(node)));
List selectExpressions = analysis.getSelectExpressions(node);
List expressions = selectExpressions.stream()
.map(SelectExpression::getExpression)
.collect(toImmutableList());
builder = subqueryPlanner.handleSubqueries(builder, expressions, analysis.getSubqueries(node));
if (hasExpressionsToUnfold(selectExpressions)) {
// pre-project the folded expressions to preserve any non-deterministic semantics of functions that might be referenced
builder = builder.appendProjections(expressions, symbolAllocator, idAllocator);
}
List outputs = outputExpressions(selectExpressions);
if (node.getOrderBy().isPresent()) {
// ORDER BY requires outputs of SELECT to be visible.
// For queries with aggregation, it also requires grouping keys and translated aggregations.
if (analysis.isAggregation(node)) {
// Add projections for aggregations required by ORDER BY. After this step, grouping keys and translated
// aggregations are visible.
List orderByAggregates = analysis.getOrderByAggregates(node.getOrderBy().get());
builder = builder.appendProjections(orderByAggregates, symbolAllocator, idAllocator);
}
// Add projections for the outputs of SELECT, but stack them on top of the ones from the FROM clause so both are visible
// when resolving the ORDER BY clause.
builder = builder.appendProjections(outputs, symbolAllocator, idAllocator);
// The new scope is the composite of the fields from the FROM and SELECT clause (local nested scopes). Fields from the bottom of
// the scope stack need to be placed first to match the expected layout for nested scopes.
List newFields = new ArrayList<>();
newFields.addAll(builder.getTranslations().getFieldSymbols());
outputs.stream()
.map(builder::translate)
.forEach(newFields::add);
builder = builder.withScope(analysis.getScope(node.getOrderBy().get()), newFields);
builder = planWindowFunctions(node, builder, ImmutableList.copyOf(analysis.getOrderByWindowFunctions(node.getOrderBy().get())));
builder = planWindowMeasures(node, builder, ImmutableList.copyOf(analysis.getOrderByWindowMeasures(node.getOrderBy().get())));
}
List orderBy = analysis.getOrderByExpressions(node);
builder = subqueryPlanner.handleSubqueries(builder, orderBy, analysis.getSubqueries(node));
builder = builder.appendProjections(Iterables.concat(orderBy, outputs), symbolAllocator, idAllocator);
builder = distinct(builder, node, outputs);
Optional orderingScheme = orderingScheme(builder, node.getOrderBy(), analysis.getOrderByExpressions(node));
builder = sort(builder, orderingScheme);
builder = offset(builder, node.getOffset());
builder = limit(builder, node.getLimit(), orderingScheme);
builder = builder.appendProjections(outputs, symbolAllocator, idAllocator);
return new RelationPlan(
builder.getRoot(),
analysis.getScope(node),
computeOutputs(builder, outputs),
outerContext);
}
private static boolean hasExpressionsToUnfold(List selectExpressions)
{
return selectExpressions.stream()
.map(SelectExpression::getUnfoldedExpressions)
.anyMatch(Optional::isPresent);
}
private static List outputExpressions(List selectExpressions)
{
ImmutableList.Builder result = ImmutableList.builder();
for (SelectExpression selectExpression : selectExpressions) {
if (selectExpression.getUnfoldedExpressions().isPresent()) {
result.addAll(selectExpression.getUnfoldedExpressions().get());
}
else {
result.add(selectExpression.getExpression());
}
}
return result.build();
}
public PlanNode plan(Delete node)
{
Table table = node.getTable();
TableHandle handle = analysis.getTableHandle(table);
// create table scan
RelationPlan relationPlan = new RelationPlanner(analysis, symbolAllocator, idAllocator, lambdaDeclarationToSymbolMap, plannerContext, outerContext, session, recursiveSubqueries)
.process(table, null);
PlanBuilder builder = newPlanBuilder(relationPlan, analysis, lambdaDeclarationToSymbolMap, session, plannerContext);
if (node.getWhere().isPresent()) {
builder = filter(builder, node.getWhere().get(), node);
}
FieldReference reference = analysis.getRowIdField(table);
Symbol rowIdSymbol = builder.translate(reference);
List outputs = ImmutableList.of(
symbolAllocator.newSymbol("partialrows", BIGINT),
symbolAllocator.newSymbol("fragment", VARBINARY));
TableMetadata tableMetadata = plannerContext.getMetadata().getTableMetadata(session, handle);
ImmutableList.Builder typeBuilder = ImmutableList.builder();
ImmutableList.Builder namesBuilder = ImmutableList.builder();
tableMetadata.getMetadata().getColumns().stream()
.filter(column -> !column.isHidden())
.forEach(columnMetadata -> {
typeBuilder.add(columnMetadata.getType());
namesBuilder.add(columnMetadata.getName());
});
Type rowIdType = analysis.getType(analysis.getRowIdField(table));
MergeParadigmAndTypes paradigmAndTypes = new MergeParadigmAndTypes(Optional.empty(), typeBuilder.build(), namesBuilder.build(), rowIdType);
MergeAnalysis mergeAnalysis = analysis.getMergeAnalysis().orElseThrow(() -> new IllegalArgumentException("Didn't find mergeAnalysis in analysis"));
// Create a ProjectNode with the references
Assignments.Builder assignmentsBuilder = new Assignments.Builder();
ImmutableList.Builder columnSymbolsBuilder = ImmutableList.builder();
for (ColumnHandle columnHandle : mergeAnalysis.getDataColumnHandles()) {
int fieldIndex = requireNonNull(mergeAnalysis.getColumnHandleFieldNumbers().get(columnHandle), "Could not find field number for column handle");
Symbol symbol = relationPlan.getFieldMappings().get(fieldIndex);
columnSymbolsBuilder.add(symbol);
if (mergeAnalysis.getRedistributionColumnHandles().contains(columnHandle)) {
assignmentsBuilder.putIdentity(symbol);
}
else {
assignmentsBuilder.put(symbol, new Cast(new NullLiteral(), toSqlType(symbolAllocator.getTypes().get(symbol))));
}
}
List columnSymbols = columnSymbolsBuilder.build();
Symbol operationSymbol = symbolAllocator.newSymbol("operation", TINYINT);
assignmentsBuilder.put(operationSymbol, new GenericLiteral("TINYINT", String.valueOf(DELETE_OPERATION_NUMBER)));
Symbol projectedRowIdSymbol = symbolAllocator.newSymbol(rowIdSymbol.getName(), rowIdType);
assignmentsBuilder.put(projectedRowIdSymbol, rowIdSymbol.toSymbolReference());
assignmentsBuilder.put(symbolAllocator.newSymbol("insert_from_update", TINYINT), new GenericLiteral("TINYINT", "0"));
Assignments assignments = assignmentsBuilder.build();
ProjectNode projectNode = new ProjectNode(idAllocator.getNextId(), builder.getRoot(), assignments);
Optional partitioningScheme = createMergePartitioningScheme(
mergeAnalysis.getInsertLayout(),
columnSymbols,
mergeAnalysis.getInsertPartitioningArgumentIndexes(),
mergeAnalysis.getUpdateLayout(),
projectedRowIdSymbol,
operationSymbol);
return new MergeWriterNode(
idAllocator.getNextId(),
projectNode,
new MergeTarget(
handle,
Optional.empty(),
tableMetadata.getTable(),
paradigmAndTypes),
projectNode.getOutputSymbols(),
partitioningScheme,
outputs);
}
public PlanNode plan(Update node)
{
MergeAnalysis mergeAnalysis = analysis.getMergeAnalysis().orElseThrow();
Table table = mergeAnalysis.getTargetTable();
List dataColumnSchemas = mergeAnalysis.getDataColumnSchemas();
List dataColumnHandles = mergeAnalysis.getDataColumnHandles();
List updatedColumnHandles = mergeAnalysis.getMergeCaseColumnHandles().get(0);
ImmutableMap.Builder nameToHandleBuilder = ImmutableMap.builder();
for (int columnIndex = 0; columnIndex < mergeAnalysis.getDataColumnSchemas().size(); columnIndex++) {
nameToHandleBuilder.put(dataColumnSchemas.get(columnIndex).getName(), dataColumnHandles.get(columnIndex));
}
Map nameToHandle = nameToHandleBuilder.buildOrThrow();
Expression[] orderedColumnValuesArray = new Expression[updatedColumnHandles.size()];
node.getAssignments().forEach(assignment -> {
String name = assignment.getName().getValue();
ColumnHandle handle = nameToHandle.get(name);
int index = updatedColumnHandles.indexOf(handle);
if (index >= 0) {
orderedColumnValuesArray[index] = assignment.getValue();
}
});
List orderedColumnValues = Arrays.stream(orderedColumnValuesArray).toList();
// create table scan
RelationPlan relationPlan = new RelationPlanner(analysis, symbolAllocator, idAllocator, lambdaDeclarationToSymbolMap, plannerContext, outerContext, session, recursiveSubqueries)
.process(table, null);
PlanBuilder subPlanBuilder = newPlanBuilder(relationPlan, analysis, lambdaDeclarationToSymbolMap, session, plannerContext);
// Add the WHERE clause, if any
if (node.getWhere().isPresent()) {
subPlanBuilder = filter(subPlanBuilder, node.getWhere().get(), node);
}
// Handle subqueries in the update SET expression values
subPlanBuilder = subqueryPlanner.handleSubqueries(subPlanBuilder, orderedColumnValues, analysis.getSubqueries(node));
// Build the merge rowblock, containing
// All data columns in table order
// The boolean present field, always TRUE for update
// The tinyint operation number, always UPDATE_OPERATION_NUMBER for update
// The integer merge case number, always 0 for update
Metadata metadata = plannerContext.getMetadata();
ImmutableList.Builder rowBuilder = ImmutableList.builder();
Assignments.Builder assignments = Assignments.builder();
// Add column values to the rowBuilder - - the SET expression value for updated
// columns, and the existing column value for non-updated columns
for (int columnIndex = 0; columnIndex < mergeAnalysis.getDataColumnHandles().size(); columnIndex++) {
ColumnHandle dataColumnHandle = mergeAnalysis.getDataColumnHandles().get(columnIndex);
ColumnSchema columnSchema = mergeAnalysis.getDataColumnSchemas().get(columnIndex);
int fieldNumber = mergeAnalysis.getColumnHandleFieldNumbers().get(dataColumnHandle);
Symbol field = relationPlan.getFieldMappings().get(fieldNumber);
int index = updatedColumnHandles.indexOf(dataColumnHandle);
if (index >= 0) {
// This column is updated...
Expression original = orderedColumnValues.get(index);
Expression setExpression = coerceIfNecessary(analysis, original, original);
subPlanBuilder = subqueryPlanner.handleSubqueries(subPlanBuilder, setExpression, analysis.getSubqueries(node));
Expression rewritten = subPlanBuilder.rewrite(setExpression);
// If the updated column is non-null, check that the value is not null
if (mergeAnalysis.getNonNullableColumnHandles().contains(dataColumnHandle)) {
String columnName = columnSchema.getName();
rewritten = new CoalesceExpression(rewritten, new Cast(failFunction(metadata, INVALID_ARGUMENTS, "NULL value not allowed for NOT NULL column: " + columnName), toSqlType(columnSchema.getType())));
}
rowBuilder.add(rewritten);
assignments.put(field, rewritten);
}
else {
// Get the non-updated column value from the table
rowBuilder.add(field.toSymbolReference());
assignments.putIdentity(field);
}
}
FieldReference rowIdReference = analysis.getRowIdField(mergeAnalysis.getTargetTable());
assignments.putIdentity(relationPlan.getFieldMappings().get(rowIdReference.getFieldIndex()));
// Add the "present" field
rowBuilder.add(new GenericLiteral("BOOLEAN", "TRUE"));
// Add the operation number
rowBuilder.add(new GenericLiteral("TINYINT", String.valueOf(UPDATE_OPERATION_NUMBER)));
// Add the merge case number
rowBuilder.add(new GenericLiteral("INTEGER", "0"));
// Finally, the merge row is complete
Expression mergeRow = new Row(rowBuilder.build());
List constraints = analysis.getCheckConstraints(table);
if (!constraints.isEmpty()) {
subPlanBuilder = subPlanBuilder.withNewRoot(new ProjectNode(
idAllocator.getNextId(),
subPlanBuilder.getRoot(),
assignments.build()));
subPlanBuilder = addCheckConstraints(constraints, subPlanBuilder);
}
// Build the page, containing:
// The write redistribution columns if any
// For partitioned or bucketed tables, a long hash value column.
// The rowId column for the row to be updated
// The merge case RowBlock
// The integer case number block, always 0 for update
// The byte is_distinct block, always true for update
Symbol rowIdSymbol = relationPlan.getFieldMappings().get(rowIdReference.getFieldIndex());
Symbol mergeRowSymbol = symbolAllocator.newSymbol("merge_row", mergeAnalysis.getMergeRowType());
Symbol caseNumberSymbol = symbolAllocator.newSymbol("case_number", INTEGER);
Symbol isDistinctSymbol = symbolAllocator.newSymbol("is_distinct", BOOLEAN);
Assignments.Builder projectionAssignmentsBuilder = Assignments.builder();
// Copy the redistribution columns
for (ColumnHandle column : mergeAnalysis.getRedistributionColumnHandles()) {
int fieldIndex = requireNonNull(mergeAnalysis.getColumnHandleFieldNumbers().get(column), "Could not find fieldIndex for redistribution column");
Symbol symbol = relationPlan.getFieldMappings().get(fieldIndex);
projectionAssignmentsBuilder.putIdentity(symbol);
}
// Add the rest of the page columns: rowId, merge row, case number and is_distinct
projectionAssignmentsBuilder.putIdentity(rowIdSymbol);
projectionAssignmentsBuilder.put(mergeRowSymbol, mergeRow);
projectionAssignmentsBuilder.put(caseNumberSymbol, new GenericLiteral("INTEGER", "0"));
projectionAssignmentsBuilder.put(isDistinctSymbol, TRUE_LITERAL);
ProjectNode projectNode = new ProjectNode(idAllocator.getNextId(), subPlanBuilder.getRoot(), projectionAssignmentsBuilder.build());
return createMergePipeline(table, relationPlan, projectNode, rowIdSymbol, mergeRowSymbol);
}
private PlanBuilder addCheckConstraints(List constraints, PlanBuilder subPlanBuilder)
{
PlanBuilder constraintBuilder = subPlanBuilder.appendProjections(constraints, symbolAllocator, idAllocator);
List predicates = new ArrayList<>();
for (Expression constraint : constraints) {
Expression symbol = constraintBuilder.translate(constraint).toSymbolReference();
Expression predicate = new IfExpression(
// When predicate evaluates to UNKNOWN (e.g. NULL > 100), it should not violate the check constraint.
new CoalesceExpression(coerceIfNecessary(analysis, symbol, symbol), TRUE_LITERAL),
TRUE_LITERAL,
new Cast(failFunction(plannerContext.getMetadata(), CONSTRAINT_VIOLATION, "Check constraint violation: " + constraint), toSqlType(BOOLEAN)));
predicates.add(predicate);
}
return subPlanBuilder.withNewRoot(new FilterNode(idAllocator.getNextId(), constraintBuilder.getRoot(), and(predicates)));
}
public MergeWriterNode plan(Merge merge)
{
MergeAnalysis mergeAnalysis = analysis.getMergeAnalysis().orElseThrow(() -> new IllegalArgumentException("analysis.getMergeAnalysis() isn't present"));
List> mergeCaseColumnsHandles = mergeAnalysis.getMergeCaseColumnHandles();
// Make the plan for the merge target table scan
RelationPlan targetTablePlan = new RelationPlanner(analysis, symbolAllocator, idAllocator, lambdaDeclarationToSymbolMap, plannerContext, outerContext, session, recursiveSubqueries)
.process(merge.getTarget());
// Assign a unique id to every target table row
Symbol uniqueIdSymbol = symbolAllocator.newSymbol("unique_id", BIGINT);
RelationPlan planWithUniqueId = new RelationPlan(
new AssignUniqueId(idAllocator.getNextId(), targetTablePlan.getRoot(), uniqueIdSymbol),
mergeAnalysis.getTargetTableScope(),
targetTablePlan.getFieldMappings(),
outerContext);
// Project the "present" column
Assignments.Builder projections = Assignments.builder();
projections.putIdentities(planWithUniqueId.getRoot().getOutputSymbols());
Symbol presentColumn = symbolAllocator.newSymbol("present", BOOLEAN);
projections.put(presentColumn, TRUE_LITERAL);
RelationPlan planWithPresentColumn = new RelationPlan(
new ProjectNode(idAllocator.getNextId(), planWithUniqueId.getRoot(), projections.build()),
mergeAnalysis.getTargetTableScope(),
planWithUniqueId.getFieldMappings(),
outerContext);
RelationPlan source = new RelationPlanner(analysis, symbolAllocator, idAllocator, lambdaDeclarationToSymbolMap, plannerContext, outerContext, session, recursiveSubqueries)
.process(merge.getSource());
RelationPlan joinPlan = new RelationPlanner(analysis, symbolAllocator, idAllocator, lambdaDeclarationToSymbolMap, plannerContext, outerContext, session, recursiveSubqueries)
.planJoin(coerceIfNecessary(analysis, merge.getPredicate(), merge.getPredicate()), Join.Type.RIGHT, mergeAnalysis.getJoinScope(), planWithPresentColumn, source, analysis.getSubqueries(merge));
PlanBuilder subPlan = newPlanBuilder(joinPlan, analysis, lambdaDeclarationToSymbolMap, session, plannerContext);
FieldReference rowIdReference = analysis.getRowIdField(mergeAnalysis.getTargetTable());
Symbol rowIdSymbol = planWithPresentColumn.getFieldMappings().get(rowIdReference.getFieldIndex());
// Build the SearchedCaseExpression that creates the project merge_row
Metadata metadata = plannerContext.getMetadata();
List dataColumnSchemas = mergeAnalysis.getDataColumnSchemas();
ImmutableList.Builder whenClauses = ImmutableList.builder();
Set nonNullableColumnHandles = mergeAnalysis.getNonNullableColumnHandles();
for (int caseNumber = 0; caseNumber < merge.getMergeCases().size(); caseNumber++) {
MergeCase mergeCase = merge.getMergeCases().get(caseNumber);
Optional casePredicate = Optional.empty();
if (mergeCase.getExpression().isPresent()) {
Expression original = mergeCase.getExpression().get();
Expression predicate = coerceIfNecessary(analysis, original, original);
casePredicate = Optional.of(predicate);
subPlan = subqueryPlanner.handleSubqueries(subPlan, predicate, analysis.getSubqueries(merge));
}
ImmutableList.Builder rowBuilder = ImmutableList.builder();
Assignments.Builder assignments = Assignments.builder();
List mergeCaseSetColumns = mergeCaseColumnsHandles.get(caseNumber);
for (ColumnHandle dataColumnHandle : mergeAnalysis.getDataColumnHandles()) {
int index = mergeCaseSetColumns.indexOf(dataColumnHandle);
int fieldNumber = mergeAnalysis.getColumnHandleFieldNumbers().get(dataColumnHandle);
Symbol field = planWithPresentColumn.getFieldMappings().get(fieldNumber);
if (index >= 0) {
Expression setExpression = mergeCase.getSetExpressions().get(index);
subPlan = subqueryPlanner.handleSubqueries(subPlan, setExpression, analysis.getSubqueries(merge));
Expression rewritten = subPlan.rewrite(setExpression);
rewritten = coerceIfNecessary(analysis, setExpression, rewritten);
if (nonNullableColumnHandles.contains(dataColumnHandle)) {
ColumnSchema columnSchema = dataColumnSchemas.get(fieldNumber);
String columnName = columnSchema.getName();
rewritten = new CoalesceExpression(rewritten, new Cast(failFunction(metadata, INVALID_ARGUMENTS, "Assigning NULL to non-null MERGE target table column " + columnName), toSqlType(columnSchema.getType())));
}
rowBuilder.add(rewritten);
assignments.put(field, rewritten);
}
else {
rowBuilder.add(field.toSymbolReference());
assignments.putIdentity(field);
}
}
// Build the match condition for the MERGE case
// Add a boolean column which is true if a target table row was matched
rowBuilder.add(new IsNotNullPredicate(presentColumn.toSymbolReference()));
// Add the operation number
rowBuilder.add(new GenericLiteral("TINYINT", String.valueOf(getMergeCaseOperationNumber(mergeCase))));
// Add the merge case number, needed by MarkDistinct
rowBuilder.add(new GenericLiteral("INTEGER", String.valueOf(caseNumber)));
Optional rewritten = casePredicate.map(subPlan::rewrite);
Expression condition = presentColumn.toSymbolReference();
if (mergeCase instanceof MergeInsert) {
condition = new IsNullPredicate(presentColumn.toSymbolReference());
}
if (rewritten.isPresent()) {
condition = ExpressionUtils.and(condition, rewritten.get());
}
whenClauses.add(new WhenClause(condition, new Row(rowBuilder.build())));
List constraints = analysis.getCheckConstraints(mergeAnalysis.getTargetTable());
if (!constraints.isEmpty()) {
assignments.putIdentity(uniqueIdSymbol);
assignments.putIdentity(presentColumn);
assignments.putIdentity(rowIdSymbol);
assignments.putIdentities(source.getFieldMappings());
subPlan = subPlan.withNewRoot(new ProjectNode(
idAllocator.getNextId(),
subPlan.getRoot(),
assignments.build()));
subPlan = addCheckConstraints(constraints, subPlan.withScope(targetTablePlan.getScope(), targetTablePlan.getFieldMappings()));
}
}
// Build the "else" clause for the SearchedCaseExpression
ImmutableList.Builder rowBuilder = ImmutableList.builder();
dataColumnSchemas.forEach(columnSchema ->
rowBuilder.add(new Cast(new NullLiteral(), toSqlType(columnSchema.getType()))));
rowBuilder.add(new IsNotNullPredicate(presentColumn.toSymbolReference()));
// The operation number
rowBuilder.add(new GenericLiteral("TINYINT", "-1"));
// The case number
rowBuilder.add(new GenericLiteral("INTEGER", "-1"));
SearchedCaseExpression caseExpression = new SearchedCaseExpression(whenClauses.build(), Optional.of(new Row(rowBuilder.build())));
Symbol mergeRowSymbol = symbolAllocator.newSymbol("merge_row", mergeAnalysis.getMergeRowType());
Symbol caseNumberSymbol = symbolAllocator.newSymbol("case_number", INTEGER);
// Project the partition symbols, the merge_row, the rowId, and the unique_id symbol
Assignments.Builder projectionAssignmentsBuilder = Assignments.builder();
for (ColumnHandle column : mergeAnalysis.getRedistributionColumnHandles()) {
int fieldIndex = requireNonNull(mergeAnalysis.getColumnHandleFieldNumbers().get(column), "Could not find fieldIndex for redistribution column");
Symbol symbol = planWithPresentColumn.getFieldMappings().get(fieldIndex);
projectionAssignmentsBuilder.putIdentity(symbol);
}
projectionAssignmentsBuilder.putIdentity(uniqueIdSymbol);
projectionAssignmentsBuilder.putIdentity(rowIdSymbol);
projectionAssignmentsBuilder.put(mergeRowSymbol, caseExpression);
ProjectNode subPlanProject = new ProjectNode(
idAllocator.getNextId(),
subPlan.getRoot(),
projectionAssignmentsBuilder.build());
// Now add a column for the case_number, gotten from the merge_row
ProjectNode project = new ProjectNode(
idAllocator.getNextId(),
subPlanProject,
Assignments.builder()
.putIdentities(subPlanProject.getOutputSymbols())
.put(caseNumberSymbol, new SubscriptExpression(mergeRowSymbol.toSymbolReference(), new LongLiteral(Long.toString(mergeAnalysis.getMergeRowType().getFields().size()))))
.build());
// Mark distinct combinations of the unique_id value and the case_number
Symbol isDistinctSymbol = symbolAllocator.newSymbol("is_distinct", BOOLEAN);
MarkDistinctNode markDistinctNode = new MarkDistinctNode(idAllocator.getNextId(), project, isDistinctSymbol, ImmutableList.of(uniqueIdSymbol, caseNumberSymbol), Optional.empty());
// Raise an error if unique_id symbol is non-null and the unique_id/case_number combination was not distinct
Expression filter = new IfExpression(
LogicalExpression.and(
new NotExpression(isDistinctSymbol.toSymbolReference()),
new IsNotNullPredicate(uniqueIdSymbol.toSymbolReference())),
new Cast(
failFunction(metadata, MERGE_TARGET_ROW_MULTIPLE_MATCHES, "One MERGE target table row matched more than one source row"),
toSqlType(BOOLEAN)),
TRUE_LITERAL);
FilterNode filterNode = new FilterNode(idAllocator.getNextId(), markDistinctNode, filter);
return createMergePipeline(merge.getTargetTable(), planWithPresentColumn, filterNode, rowIdSymbol, mergeRowSymbol);
}
private MergeWriterNode createMergePipeline(Table table, RelationPlan relationPlan, PlanNode planNode, Symbol rowIdSymbol, Symbol mergeRowSymbol)
{
TableHandle handle = analysis.getTableHandle(table);
MergeAnalysis mergeAnalysis = analysis.getMergeAnalysis().orElseThrow();
Metadata metadata = plannerContext.getMetadata();
RowChangeParadigm paradigm = metadata.getRowChangeParadigm(session, handle);
Type rowIdType = analysis.getType(analysis.getRowIdField(table));
ImmutableList.Builder typesBuilder = ImmutableList.builder();
ImmutableList.Builder columnNamesBuilder = ImmutableList.builder();
mergeAnalysis.getDataColumnSchemas().stream()
.filter(columnSchema -> !columnSchema.isHidden())
.forEach(columnSchema -> {
typesBuilder.add(columnSchema.getType());
columnNamesBuilder.add(columnSchema.getName());
});
MergeParadigmAndTypes mergeParadigmAndTypes = new MergeParadigmAndTypes(Optional.of(paradigm), typesBuilder.build(), columnNamesBuilder.build(), rowIdType);
MergeTarget mergeTarget = new MergeTarget(handle, Optional.empty(), metadata.getTableName(session, handle).getSchemaTableName(), mergeParadigmAndTypes);
ImmutableList.Builder columnSymbolsBuilder = ImmutableList.builder();
for (ColumnHandle columnHandle : mergeAnalysis.getDataColumnHandles()) {
int fieldIndex = requireNonNull(mergeAnalysis.getColumnHandleFieldNumbers().get(columnHandle), "Could not find field number for column handle");
columnSymbolsBuilder.add(relationPlan.getFieldMappings().get(fieldIndex));
}
List columnSymbols = columnSymbolsBuilder.build();
ImmutableList.Builder redistributionSymbolsBuilder = ImmutableList.builder();
for (ColumnHandle columnHandle : mergeAnalysis.getRedistributionColumnHandles()) {
int fieldIndex = requireNonNull(mergeAnalysis.getColumnHandleFieldNumbers().get(columnHandle), "Could not find field number for column handle");
redistributionSymbolsBuilder.add(relationPlan.getFieldMappings().get(fieldIndex));
}
Symbol operationSymbol = symbolAllocator.newSymbol("operation", TINYINT);
Symbol insertFromUpdateSymbol = symbolAllocator.newSymbol("insert_from_update", TINYINT);
List projectedSymbols = ImmutableList.builder()
.addAll(columnSymbols)
.add(operationSymbol)
.add(rowIdSymbol)
.add(insertFromUpdateSymbol)
.build();
MergeProcessorNode mergeProcessorNode = new MergeProcessorNode(
idAllocator.getNextId(),
planNode,
mergeTarget,
rowIdSymbol,
mergeRowSymbol,
columnSymbols,
redistributionSymbolsBuilder.build(),
projectedSymbols);
Optional partitioningScheme = createMergePartitioningScheme(
mergeAnalysis.getInsertLayout(),
columnSymbols,
mergeAnalysis.getInsertPartitioningArgumentIndexes(),
mergeAnalysis.getUpdateLayout(),
rowIdSymbol,
operationSymbol);
List outputs = ImmutableList.of(
symbolAllocator.newSymbol("partialrows", BIGINT),
symbolAllocator.newSymbol("fragment", VARBINARY));
return new MergeWriterNode(
idAllocator.getNextId(),
mergeProcessorNode,
mergeTarget,
projectedSymbols,
partitioningScheme,
outputs);
}
private static int getMergeCaseOperationNumber(MergeCase mergeCase)
{
if (mergeCase instanceof MergeInsert) {
return INSERT_OPERATION_NUMBER;
}
if (mergeCase instanceof MergeUpdate) {
return UPDATE_OPERATION_NUMBER;
}
if (mergeCase instanceof MergeDelete) {
return DELETE_OPERATION_NUMBER;
}
throw new IllegalArgumentException("Unrecognized MergeCase: " + mergeCase);
}
public static Optional createMergePartitioningScheme(
Optional insertLayout,
List symbols,
List insertPartitioningArgumentIndexes,
Optional updateLayout,
Symbol rowIdSymbol,
Symbol operationSymbol)
{
if (insertLayout.isEmpty() && updateLayout.isEmpty()) {
return Optional.empty();
}
Optional insertPartitioning = insertLayout.map(layout -> {
List arguments = insertPartitioningArgumentIndexes.stream()
.map(symbols::get)
.collect(toImmutableList());
return layout.getPartitioning()
.map(handle -> new PartitioningScheme(Partitioning.create(handle, arguments), symbols))
// empty connector partitioning handle means evenly partitioning on partitioning columns
.orElseGet(() -> new PartitioningScheme(Partitioning.create(FIXED_HASH_DISTRIBUTION, arguments), symbols));
});
Optional updatePartitioning = updateLayout.map(handle ->
new PartitioningScheme(Partitioning.create(handle, ImmutableList.of(rowIdSymbol)), ImmutableList.of(rowIdSymbol)));
PartitioningHandle partitioningHandle = new PartitioningHandle(
Optional.empty(),
Optional.empty(),
new MergePartitioningHandle(insertPartitioning, updatePartitioning));
List combinedSymbols = new ArrayList<>();
combinedSymbols.add(operationSymbol);
insertPartitioning.ifPresent(scheme -> combinedSymbols.addAll(partitioningSymbols(scheme)));
updatePartitioning.ifPresent(scheme -> combinedSymbols.addAll(partitioningSymbols(scheme)));
return Optional.of(new PartitioningScheme(Partitioning.create(partitioningHandle, combinedSymbols), combinedSymbols));
}
private static List partitioningSymbols(PartitioningScheme scheme)
{
return scheme.getPartitioning().getArguments().stream()
.map(Partitioning.ArgumentBinding::getColumn)
.collect(toImmutableList());
}
private static List computeOutputs(PlanBuilder builder, List outputExpressions)
{
ImmutableList.Builder outputSymbols = ImmutableList.builder();
for (Expression expression : outputExpressions) {
outputSymbols.add(builder.translate(expression));
}
return outputSymbols.build();
}
private PlanBuilder planQueryBody(Query query)
{
RelationPlan relationPlan = new RelationPlanner(analysis, symbolAllocator, idAllocator, lambdaDeclarationToSymbolMap, plannerContext, outerContext, session, recursiveSubqueries)
.process(query.getQueryBody(), null);
return newPlanBuilder(relationPlan, analysis, lambdaDeclarationToSymbolMap, session, plannerContext);
}
private PlanBuilder planFrom(QuerySpecification node)
{
if (node.getFrom().isPresent()) {
RelationPlan relationPlan = new RelationPlanner(analysis, symbolAllocator, idAllocator, lambdaDeclarationToSymbolMap, plannerContext, outerContext, session, recursiveSubqueries)
.process(node.getFrom().get(), null);
return newPlanBuilder(relationPlan, analysis, lambdaDeclarationToSymbolMap, session, plannerContext);
}
return new PlanBuilder(
new TranslationMap(outerContext, analysis.getImplicitFromScope(node), analysis, lambdaDeclarationToSymbolMap, ImmutableList.of(), session, plannerContext),
new ValuesNode(idAllocator.getNextId(), 1));
}
private PlanBuilder filter(PlanBuilder subPlan, Expression predicate, Node node)
{
if (predicate == null) {
return subPlan;
}
subPlan = subqueryPlanner.handleSubqueries(subPlan, predicate, analysis.getSubqueries(node));
return subPlan.withNewRoot(new FilterNode(idAllocator.getNextId(), subPlan.getRoot(), coerceIfNecessary(analysis, predicate, subPlan.rewrite(predicate))));
}
private PlanBuilder aggregate(PlanBuilder subPlan, QuerySpecification node)
{
if (!analysis.isAggregation(node)) {
return subPlan;
}
ImmutableList.Builder inputBuilder = ImmutableList.builder();
analysis.getAggregates(node).stream()
.map(FunctionCall::getArguments)
.flatMap(List::stream)
.filter(expression -> !(expression instanceof LambdaExpression)) // lambda expression is generated at execution time
.forEach(inputBuilder::add);
analysis.getAggregates(node).stream()
.map(FunctionCall::getOrderBy)
.map(NodeUtils::getSortItemsFromOrderBy)
.flatMap(List::stream)
.map(SortItem::getSortKey)
.forEach(inputBuilder::add);
// filter expressions need to be projected first
analysis.getAggregates(node).stream()
.map(FunctionCall::getFilter)
.filter(Optional::isPresent)
.map(Optional::get)
.forEach(inputBuilder::add);
GroupingSetAnalysis groupingSetAnalysis = analysis.getGroupingSets(node);
inputBuilder.addAll(groupingSetAnalysis.getComplexExpressions());
List inputs = inputBuilder.build();
subPlan = subqueryPlanner.handleSubqueries(subPlan, inputs, analysis.getSubqueries(node));
subPlan = subPlan.appendProjections(inputs, symbolAllocator, idAllocator);
// Add projection to coerce inputs to their site-specific types.
// This is important because the same lexical expression may need to be coerced
// in different ways if it's referenced by multiple arguments to the window function.
// For example, given v::integer,
// avg(v)
// Needs to be rewritten as
// avg(CAST(v AS double))
PlanAndMappings coercions = coerce(subPlan, inputs, analysis, idAllocator, symbolAllocator, typeCoercion);
subPlan = coercions.getSubPlan();
GroupingSetsPlan groupingSets = planGroupingSets(subPlan, node, groupingSetAnalysis);
subPlan = planAggregation(groupingSets.getSubPlan(), groupingSets.getGroupingSets(), groupingSets.getGroupIdSymbol(), analysis.getAggregates(node), coercions::get);
return planGroupingOperations(subPlan, node, groupingSets.getGroupIdSymbol(), groupingSets.getColumnOnlyGroupingSets());
}
private GroupingSetsPlan planGroupingSets(PlanBuilder subPlan, QuerySpecification node, GroupingSetAnalysis groupingSetAnalysis)
{
Map groupingSetMappings = new LinkedHashMap<>();
// Compute a set of artificial columns that will contain the values of the original columns
// filtered by whether the column is included in the grouping set
// This will become the basis for the scope for any column references
Symbol[] fields = new Symbol[subPlan.getTranslations().getFieldSymbols().size()];
for (FieldId field : groupingSetAnalysis.getAllFields()) {
Symbol input = subPlan.getTranslations().getFieldSymbols().get(field.getFieldIndex());
Symbol output = symbolAllocator.newSymbol(input, "gid");
fields[field.getFieldIndex()] = output;
groupingSetMappings.put(output, input);
}
Map, Symbol> complexExpressions = new LinkedHashMap<>();
for (Expression expression : groupingSetAnalysis.getComplexExpressions()) {
if (!complexExpressions.containsKey(scopeAwareKey(expression, analysis, subPlan.getScope()))) {
Symbol input = subPlan.translate(expression);
Symbol output = symbolAllocator.newSymbol(expression, analysis.getType(expression), "gid");
complexExpressions.put(scopeAwareKey(expression, analysis, subPlan.getScope()), output);
groupingSetMappings.put(output, input);
}
}
// For the purpose of "distinct", we need to canonicalize column references that may have varying
// syntactic forms (e.g., "t.a" vs "a"). Thus we need to enumerate grouping sets based on the underlying
// fieldId associated with each column reference expression.
// The catch is that simple group-by expressions can be arbitrary expressions (this is a departure from the SQL specification).
// But, they don't affect the number of grouping sets or the behavior of "distinct" . We can compute all the candidate
// grouping sets in terms of fieldId, dedup as appropriate and then cross-join them with the complex expressions.
// This tracks the grouping sets before complex expressions are considered.
// It's also used to compute the descriptors needed to implement grouping()
List> columnOnlyGroupingSets = enumerateGroupingSets(groupingSetAnalysis);
if (node.getGroupBy().isPresent() && node.getGroupBy().get().isDistinct()) {
columnOnlyGroupingSets = columnOnlyGroupingSets.stream()
.distinct()
.collect(toImmutableList());
}
// translate from FieldIds to Symbols
List> sets = columnOnlyGroupingSets.stream()
.map(set -> set.stream()
.map(FieldId::getFieldIndex)
.map(index -> fields[index])
.collect(toImmutableList()))
.collect(toImmutableList());
// combine (cartesian product) with complex expressions
List> groupingSets = sets.stream()
.map(set -> ImmutableList.builder()
.addAll(set)
.addAll(complexExpressions.values())
.build())
.collect(toImmutableList());
// Generate GroupIdNode (multiple grouping sets) or ProjectNode (single grouping set)
PlanNode groupId;
Optional groupIdSymbol = Optional.empty();
if (groupingSets.size() > 1) {
groupIdSymbol = Optional.of(symbolAllocator.newSymbol("groupId", BIGINT));
groupId = new GroupIdNode(
idAllocator.getNextId(),
subPlan.getRoot(),
groupingSets,
groupingSetMappings,
subPlan.getRoot().getOutputSymbols(),
groupIdSymbol.get());
}
else {
Assignments.Builder assignments = Assignments.builder();
assignments.putIdentities(subPlan.getRoot().getOutputSymbols());
groupingSetMappings.forEach((key, value) -> assignments.put(key, value.toSymbolReference()));
groupId = new ProjectNode(idAllocator.getNextId(), subPlan.getRoot(), assignments.build());
}
subPlan = new PlanBuilder(
subPlan.getTranslations()
.withNewMappings(complexExpressions, Arrays.asList(fields)),
groupId);
return new GroupingSetsPlan(subPlan, columnOnlyGroupingSets, groupingSets, groupIdSymbol);
}
private PlanBuilder planAggregation(PlanBuilder subPlan, List> groupingSets, Optional groupIdSymbol, List aggregates, Function coercions)
{
ImmutableList.Builder aggregateMappingBuilder = ImmutableList.builder();
// deduplicate based on scope-aware equality
for (FunctionCall function : scopeAwareDistinct(subPlan, aggregates)) {
Symbol symbol = symbolAllocator.newSymbol(function, analysis.getType(function));
// TODO: for ORDER BY arguments, rewrite them such that they match the actual arguments to the function. This is necessary to maintain the semantics of DISTINCT + ORDER BY,
// which requires that ORDER BY be a subset of arguments
// What can happen currently is that if the argument requires a coercion, the argument will take a different input that the ORDER BY clause, which is undefined behavior
Aggregation aggregation = new Aggregation(
analysis.getResolvedFunction(function),
function.getArguments().stream()
.map(argument -> {
if (argument instanceof LambdaExpression) {
return subPlan.rewrite(argument);
}
return coercions.apply(argument).toSymbolReference();
})
.collect(toImmutableList()),
function.isDistinct(),
function.getFilter().map(coercions),
function.getOrderBy().map(orderBy -> translateOrderingScheme(orderBy.getSortItems(), coercions)),
Optional.empty());
aggregateMappingBuilder.add(new AggregationAssignment(symbol, function, aggregation));
}
List aggregateMappings = aggregateMappingBuilder.build();
ImmutableSet.Builder globalGroupingSets = ImmutableSet.builder();
for (int i = 0; i < groupingSets.size(); i++) {
if (groupingSets.get(i).isEmpty()) {
globalGroupingSets.add(i);
}
}
ImmutableList.Builder groupingKeys = ImmutableList.builder();
groupingSets.stream()
.flatMap(List::stream)
.distinct()
.forEach(groupingKeys::add);
groupIdSymbol.ifPresent(groupingKeys::add);
AggregationNode aggregationNode = new AggregationNode(
idAllocator.getNextId(),
subPlan.getRoot(),
aggregateMappings.stream()
.collect(toImmutableMap(AggregationAssignment::getSymbol, AggregationAssignment::getRewritten)),
groupingSets(
groupingKeys.build(),
groupingSets.size(),
globalGroupingSets.build()),
ImmutableList.of(),
AggregationNode.Step.SINGLE,
Optional.empty(),
groupIdSymbol);
return new PlanBuilder(
subPlan.getTranslations()
.withAdditionalMappings(aggregateMappings.stream()
.collect(toImmutableMap(assignment -> scopeAwareKey(assignment.getAstExpression(), analysis, subPlan.getScope()), AggregationAssignment::getSymbol))),
aggregationNode);
}
private List scopeAwareDistinct(PlanBuilder subPlan, List expressions)
{
return expressions.stream()
.map(function -> scopeAwareKey(function, analysis, subPlan.getScope()))
.distinct()
.map(ScopeAware::getNode)
.collect(toImmutableList());
}
public static OrderingScheme translateOrderingScheme(List items, Function coercions)
{
List coerced = items.stream()
.map(SortItem::getSortKey)
.map(coercions)
.collect(toImmutableList());
ImmutableList.Builder symbols = ImmutableList.builder();
Map orders = new HashMap<>();
for (int i = 0; i < coerced.size(); i++) {
Symbol symbol = coerced.get(i);
// for multiple sort items based on the same expression, retain the first one:
// ORDER BY x DESC, x ASC, y --> ORDER BY x DESC, y
if (!orders.containsKey(symbol)) {
symbols.add(symbol);
orders.put(symbol, OrderingScheme.sortItemToSortOrder(items.get(i)));
}
}
return new OrderingScheme(symbols.build(), orders);
}
private static List> enumerateGroupingSets(GroupingSetAnalysis groupingSetAnalysis)
{
List>> partialSets = new ArrayList<>();
for (List> cube : groupingSetAnalysis.getCubes()) {
List> sets = Sets.powerSet(ImmutableSet.copyOf(cube)).stream()
.map(set -> set.stream()
.flatMap(Collection::stream)
.collect(toImmutableSet()))
.collect(toImmutableList());
partialSets.add(sets);
}
for (List> rollup : groupingSetAnalysis.getRollups()) {
List> sets = IntStream.rangeClosed(0, rollup.size())
.mapToObj(prefixLength -> rollup.subList(0, prefixLength).stream()
.flatMap(Collection::stream)
.collect(toImmutableSet()))
.collect(toImmutableList());
partialSets.add(sets);
}
partialSets.addAll(groupingSetAnalysis.getOrdinarySets());
if (partialSets.isEmpty()) {
return ImmutableList.of(ImmutableSet.of());
}
// compute the cross product of the partial sets
List> allSets = new ArrayList<>();
partialSets.get(0)
.stream()
.map(ImmutableSet::copyOf)
.forEach(allSets::add);
for (int i = 1; i < partialSets.size(); i++) {
List> groupingSets = partialSets.get(i);
List> oldGroupingSetsCrossProduct = ImmutableList.copyOf(allSets);
allSets.clear();
for (Set existingSet : oldGroupingSetsCrossProduct) {
for (Set groupingSet : groupingSets) {
Set concatenatedSet = ImmutableSet.builder()
.addAll(existingSet)
.addAll(groupingSet)
.build();
allSets.add(concatenatedSet);
}
}
}
return allSets;
}
private PlanBuilder planGroupingOperations(PlanBuilder subPlan, QuerySpecification node, Optional groupIdSymbol, List> groupingSets)
{
if (analysis.getGroupingOperations(node).isEmpty()) {
return subPlan;
}
List> descriptor = groupingSets.stream()
.map(set -> set.stream()
.map(FieldId::getFieldIndex)
.collect(toImmutableSet()))
.collect(toImmutableList());
return subPlan.appendProjections(
analysis.getGroupingOperations(node),
symbolAllocator,
idAllocator,
(translations, groupingOperation) -> rewriteGroupingOperation(groupingOperation, descriptor, analysis.getColumnReferenceFields(), groupIdSymbol),
(translations, groupingOperation) -> false);
}
private PlanBuilder planWindowFunctions(Node node, PlanBuilder subPlan, List windowFunctions)
{
if (windowFunctions.isEmpty()) {
return subPlan;
}
Map> functions = scopeAwareDistinct(subPlan, windowFunctions)
.stream()
.collect(Collectors.groupingBy(analysis::getWindow));
for (Map.Entry> entry : functions.entrySet()) {
ResolvedWindow window = entry.getKey();
List functionCalls = entry.getValue();
// Pre-project inputs.
// Predefined window parts (specified in WINDOW clause) can only use source symbols, and no output symbols.
// It matters in case when this window planning takes place in ORDER BY clause, where both source and output
// symbols are visible.
// This issue is solved by analyzing window definitions in the source scope. After analysis, the expressions
// are recorded as belonging to the source scope, and consequentially source symbols will be used to plan them.
ImmutableList.Builder inputsBuilder = ImmutableList.builder()
.addAll(window.getPartitionBy())
.addAll(getSortItemsFromOrderBy(window.getOrderBy()).stream()
.map(SortItem::getSortKey)
.iterator());
if (window.getFrame().isPresent()) {
WindowFrame frame = window.getFrame().get();
frame.getStart().getValue().ifPresent(inputsBuilder::add);
if (frame.getEnd().isPresent()) {
frame.getEnd().get().getValue().ifPresent(inputsBuilder::add);
}
}
for (FunctionCall windowFunction : functionCalls) {
inputsBuilder.addAll(windowFunction.getArguments().stream()
.filter(argument -> !(argument instanceof LambdaExpression)) // lambda expression is generated at execution time
.collect(Collectors.toList()));
}
List inputs = inputsBuilder.build();
subPlan = subqueryPlanner.handleSubqueries(subPlan, inputs, analysis.getSubqueries(node));
subPlan = subPlan.appendProjections(inputs, symbolAllocator, idAllocator);
// Add projection to coerce inputs to their site-specific types.
// This is important because the same lexical expression may need to be coerced
// in different ways if it's referenced by multiple arguments to the window function.
// For example, given v::integer,
// avg(v) OVER (ORDER BY v)
// Needs to be rewritten as
// avg(CAST(v AS double)) OVER (ORDER BY v)
PlanAndMappings coercions = coerce(subPlan, inputs, analysis, idAllocator, symbolAllocator, typeCoercion);
subPlan = coercions.getSubPlan();
// For frame of type RANGE, append casts and functions necessary for frame bound calculations
Optional frameStart = Optional.empty();
Optional frameEnd = Optional.empty();
Optional sortKeyCoercedForFrameStartComparison = Optional.empty();
Optional sortKeyCoercedForFrameEndComparison = Optional.empty();
if (window.getFrame().isPresent() && window.getFrame().get().getType() == RANGE) {
Optional startValue = window.getFrame().get().getStart().getValue();
Optional endValue = window.getFrame().get().getEnd().flatMap(FrameBound::getValue);
// record sortKey coercions for reuse
Map sortKeyCoercions = new HashMap<>();
// process frame start
FrameBoundPlanAndSymbols plan = planFrameBound(subPlan, coercions, startValue, window, sortKeyCoercions);
subPlan = plan.getSubPlan();
frameStart = plan.getFrameBoundSymbol();
sortKeyCoercedForFrameStartComparison = plan.getSortKeyCoercedForFrameBoundComparison();
// process frame end
plan = planFrameBound(subPlan, coercions, endValue, window, sortKeyCoercions);
subPlan = plan.getSubPlan();
frameEnd = plan.getFrameBoundSymbol();
sortKeyCoercedForFrameEndComparison = plan.getSortKeyCoercedForFrameBoundComparison();
}
else if (window.getFrame().isPresent() && (window.getFrame().get().getType() == ROWS || window.getFrame().get().getType() == GROUPS)) {
Optional startValue = window.getFrame().get().getStart().getValue();
Optional endValue = window.getFrame().get().getEnd().flatMap(FrameBound::getValue);
// process frame start
FrameOffsetPlanAndSymbol plan = planFrameOffset(subPlan, startValue.map(coercions::get));
subPlan = plan.getSubPlan();
frameStart = plan.getFrameOffsetSymbol();
// process frame end
plan = planFrameOffset(subPlan, endValue.map(coercions::get));
subPlan = plan.getSubPlan();
frameEnd = plan.getFrameOffsetSymbol();
}
else if (window.getFrame().isPresent()) {
throw new IllegalArgumentException("unexpected window frame type: " + window.getFrame().get().getType());
}
if (window.getFrame().isPresent() && window.getFrame().get().getPattern().isPresent()) {
WindowFrame frame = window.getFrame().get();
subPlan = subqueryPlanner.handleSubqueries(subPlan, extractPatternRecognitionExpressions(frame.getVariableDefinitions(), frame.getMeasures()), analysis.getSubqueries(node));
subPlan = planPatternRecognition(subPlan, functionCalls, window, coercions, frameEnd);
}
else {
subPlan = planWindow(subPlan, functionCalls, window, coercions, frameStart, sortKeyCoercedForFrameStartComparison, frameEnd, sortKeyCoercedForFrameEndComparison);
}
}
return subPlan;
}
private FrameBoundPlanAndSymbols planFrameBound(PlanBuilder subPlan, PlanAndMappings coercions, Optional frameOffset, ResolvedWindow window, Map sortKeyCoercions)
{
Optional frameBoundCalculationFunction = frameOffset.map(analysis::getFrameBoundCalculation);
// Empty frameBoundCalculationFunction indicates that frame bound type is CURRENT ROW or UNBOUNDED.
// Handling it doesn't require any additional symbols.
if (frameBoundCalculationFunction.isEmpty()) {
return new FrameBoundPlanAndSymbols(subPlan, Optional.empty(), Optional.empty());
}
// Present frameBoundCalculationFunction indicates that frame bound type is PRECEDING or FOLLOWING.
// It requires adding certain projections to the plan so that the operator can determine frame bounds.
// First, append filter to validate offset values. They mustn't be negative or null.
Symbol offsetSymbol = coercions.get(frameOffset.get());
Expression zeroOffset = zeroOfType(symbolAllocator.getTypes().get(offsetSymbol));
Expression predicate = new IfExpression(
new ComparisonExpression(
GREATER_THAN_OR_EQUAL,
offsetSymbol.toSymbolReference(),
zeroOffset),
TRUE_LITERAL,
new Cast(
failFunction(plannerContext.getMetadata(), INVALID_WINDOW_FRAME, "Window frame offset value must not be negative or null"),
toSqlType(BOOLEAN)));
subPlan = subPlan.withNewRoot(new FilterNode(
idAllocator.getNextId(),
subPlan.getRoot(),
predicate));
// Then, coerce the sortKey so that we can add / subtract the offset.
// Note: for that we cannot rely on the usual mechanism of using the coerce() method. The coerce() method can only handle one coercion for a node,
// while the sortKey node might require several different coercions, e.g. one for frame start and one for frame end.
Expression sortKey = Iterables.getOnlyElement(window.getOrderBy().orElseThrow().getSortItems()).getSortKey();
Symbol sortKeyCoercedForFrameBoundCalculation = coercions.get(sortKey);
Optional coercion = frameOffset.map(analysis::getSortKeyCoercionForFrameBoundCalculation);
if (coercion.isPresent()) {
Type expectedType = coercion.get();
Symbol alreadyCoerced = sortKeyCoercions.get(expectedType);
if (alreadyCoerced != null) {
sortKeyCoercedForFrameBoundCalculation = alreadyCoerced;
}
else {
Expression cast = new Cast(
coercions.get(sortKey).toSymbolReference(),
toSqlType(expectedType),
false,
typeCoercion.isTypeOnlyCoercion(analysis.getType(sortKey), expectedType));
sortKeyCoercedForFrameBoundCalculation = symbolAllocator.newSymbol(cast, expectedType);
sortKeyCoercions.put(expectedType, sortKeyCoercedForFrameBoundCalculation);
subPlan = subPlan.withNewRoot(new ProjectNode(
idAllocator.getNextId(),
subPlan.getRoot(),
Assignments.builder()
.putIdentities(subPlan.getRoot().getOutputSymbols())
.put(sortKeyCoercedForFrameBoundCalculation, cast)
.build()));
}
}
// Next, pre-project the function which combines sortKey with the offset.
// Note: if frameOffset needs a coercion, it was added before by a call to coerce() method.
ResolvedFunction function = frameBoundCalculationFunction.get();
Expression functionCall = new FunctionCall(
function.toQualifiedName(),
ImmutableList.of(
sortKeyCoercedForFrameBoundCalculation.toSymbolReference(),
offsetSymbol.toSymbolReference()));
Symbol frameBoundSymbol = symbolAllocator.newSymbol(functionCall, function.getSignature().getReturnType());
subPlan = subPlan.withNewRoot(new ProjectNode(
idAllocator.getNextId(),
subPlan.getRoot(),
Assignments.builder()
.putIdentities(subPlan.getRoot().getOutputSymbols())
.put(frameBoundSymbol, functionCall)
.build()));
// Finally, coerce the sortKey to the type of frameBound so that the operator can perform comparisons on them
Optional sortKeyCoercedForFrameBoundComparison = Optional.of(coercions.get(sortKey));
coercion = frameOffset.map(analysis::getSortKeyCoercionForFrameBoundComparison);
if (coercion.isPresent()) {
Type expectedType = coercion.get();
Symbol alreadyCoerced = sortKeyCoercions.get(expectedType);
if (alreadyCoerced != null) {
sortKeyCoercedForFrameBoundComparison = Optional.of(alreadyCoerced);
}
else {
Expression cast = new Cast(
coercions.get(sortKey).toSymbolReference(),
toSqlType(expectedType),
false,
typeCoercion.isTypeOnlyCoercion(analysis.getType(sortKey), expectedType));
Symbol castSymbol = symbolAllocator.newSymbol(cast, expectedType);
sortKeyCoercions.put(expectedType, castSymbol);
subPlan = subPlan.withNewRoot(new ProjectNode(
idAllocator.getNextId(),
subPlan.getRoot(),
Assignments.builder()
.putIdentities(subPlan.getRoot().getOutputSymbols())
.put(castSymbol, cast)
.build()));
sortKeyCoercedForFrameBoundComparison = Optional.of(castSymbol);
}
}
return new FrameBoundPlanAndSymbols(subPlan, Optional.of(frameBoundSymbol), sortKeyCoercedForFrameBoundComparison);
}
private FrameOffsetPlanAndSymbol planFrameOffset(PlanBuilder subPlan, Optional frameOffset)
{
if (frameOffset.isEmpty()) {
return new FrameOffsetPlanAndSymbol(subPlan, Optional.empty());
}
Symbol offsetSymbol = frameOffset.get();
Type offsetType = symbolAllocator.getTypes().get(offsetSymbol);
// Append filter to validate offset values. They mustn't be negative or null.
Expression zeroOffset = zeroOfType(offsetType);
Expression predicate = new IfExpression(
new ComparisonExpression(GREATER_THAN_OR_EQUAL, offsetSymbol.toSymbolReference(), zeroOffset),
TRUE_LITERAL,
new Cast(
failFunction(plannerContext.getMetadata(), INVALID_WINDOW_FRAME, "Window frame offset value must not be negative or null"),
toSqlType(BOOLEAN)));
subPlan = subPlan.withNewRoot(new FilterNode(
idAllocator.getNextId(),
subPlan.getRoot(),
predicate));
if (offsetType.equals(BIGINT)) {
return new FrameOffsetPlanAndSymbol(subPlan, Optional.of(offsetSymbol));
}
Expression offsetToBigint;
if (offsetType instanceof DecimalType && !((DecimalType) offsetType).isShort()) {
String maxBigint = Long.toString(Long.MAX_VALUE);
int maxBigintPrecision = maxBigint.length();
int actualPrecision = ((DecimalType) offsetType).getPrecision();
if (actualPrecision < maxBigintPrecision) {
offsetToBigint = new Cast(offsetSymbol.toSymbolReference(), toSqlType(BIGINT));
}
else if (actualPrecision > maxBigintPrecision) {
// If the offset value exceeds max bigint, it implies that the frame bound falls beyond the partition bound.
// In such case, the frame bound is set to the partition bound. Passing max bigint as the offset value has
// the same effect. The offset value can be truncated to max bigint for the purpose of cast.
offsetToBigint = new GenericLiteral("BIGINT", maxBigint);
}
else {
offsetToBigint = new IfExpression(
new ComparisonExpression(LESS_THAN_OR_EQUAL, offsetSymbol.toSymbolReference(), new DecimalLiteral(maxBigint)),
new Cast(offsetSymbol.toSymbolReference(), toSqlType(BIGINT)),
new GenericLiteral("BIGINT", maxBigint));
}
}
else {
offsetToBigint = new Cast(
offsetSymbol.toSymbolReference(),
toSqlType(BIGINT),
false,
typeCoercion.isTypeOnlyCoercion(offsetType, BIGINT));
}
Symbol coercedOffsetSymbol = symbolAllocator.newSymbol(offsetToBigint, BIGINT);
subPlan = subPlan.withNewRoot(new ProjectNode(
idAllocator.getNextId(),
subPlan.getRoot(),
Assignments.builder()
.putIdentities(subPlan.getRoot().getOutputSymbols())
.put(coercedOffsetSymbol, offsetToBigint)
.build()));
return new FrameOffsetPlanAndSymbol(subPlan, Optional.of(coercedOffsetSymbol));
}
private static Expression zeroOfType(Type type)
{
if (isNumericType(type)) {
return new Cast(new LongLiteral("0"), toSqlType(type));
}
if (type.equals(INTERVAL_DAY_TIME)) {
return new IntervalLiteral("0", POSITIVE, DAY);
}
if (type.equals(INTERVAL_YEAR_MONTH)) {
return new IntervalLiteral("0", POSITIVE, YEAR);
}
throw new IllegalArgumentException("unexpected type: " + type);
}
private PlanBuilder planWindow(
PlanBuilder subPlan,
List windowFunctions,
ResolvedWindow window,
PlanAndMappings coercions,
Optional frameStartSymbol,
Optional sortKeyCoercedForFrameStartComparison,
Optional frameEndSymbol,
Optional sortKeyCoercedForFrameEndComparison)
{
WindowFrame.Type frameType = WindowFrame.Type.RANGE;
FrameBound.Type frameStartType = FrameBound.Type.UNBOUNDED_PRECEDING;
FrameBound.Type frameEndType = FrameBound.Type.CURRENT_ROW;
Optional frameStartExpression = Optional.empty();
Optional frameEndExpression = Optional.empty();
if (window.getFrame().isPresent()) {
WindowFrame frame = window.getFrame().get();
frameType = frame.getType();
frameStartType = frame.getStart().getType();
frameStartExpression = frame.getStart().getValue();
if (frame.getEnd().isPresent()) {
frameEndType = frame.getEnd().get().getType();
frameEndExpression = frame.getEnd().get().getValue();
}
}
DataOrganizationSpecification specification = planWindowSpecification(window.getPartitionBy(), window.getOrderBy(), coercions::get);
// Rewrite frame bounds in terms of pre-projected inputs
WindowNode.Frame frame = new WindowNode.Frame(
frameType,
frameStartType,
frameStartSymbol,
sortKeyCoercedForFrameStartComparison,
frameEndType,
frameEndSymbol,
sortKeyCoercedForFrameEndComparison,
frameStartExpression,
frameEndExpression);
ImmutableMap.Builder, Symbol> mappings = ImmutableMap.builder();
ImmutableMap.Builder functions = ImmutableMap.builder();
for (FunctionCall windowFunction : windowFunctions) {
Symbol newSymbol = symbolAllocator.newSymbol(windowFunction, analysis.getType(windowFunction));
NullTreatment nullTreatment = windowFunction.getNullTreatment()
.orElse(NullTreatment.RESPECT);
WindowNode.Function function = new WindowNode.Function(
analysis.getResolvedFunction(windowFunction),
windowFunction.getArguments().stream()
.map(argument -> {
if (argument instanceof LambdaExpression) {
return subPlan.rewrite(argument);
}
return coercions.get(argument).toSymbolReference();
})
.collect(toImmutableList()),
frame,
nullTreatment == NullTreatment.IGNORE);
functions.put(newSymbol, function);
mappings.put(scopeAwareKey(windowFunction, analysis, subPlan.getScope()), newSymbol);
}
// create window node
return new PlanBuilder(
subPlan.getTranslations()
.withAdditionalMappings(mappings.buildOrThrow()),
new WindowNode(
idAllocator.getNextId(),
subPlan.getRoot(),
specification,
functions.buildOrThrow(),
Optional.empty(),
ImmutableSet.of(),
0));
}
private PlanBuilder planPatternRecognition(
PlanBuilder subPlan,
List windowFunctions,
ResolvedWindow window,
PlanAndMappings coercions,
Optional frameEndSymbol)
{
DataOrganizationSpecification specification = planWindowSpecification(window.getPartitionBy(), window.getOrderBy(), coercions::get);
// in window frame with pattern recognition, the frame extent is specified as `ROWS BETWEEN CURRENT ROW AND ... `
WindowFrame frame = window.getFrame().orElseThrow();
FrameBound frameEnd = frame.getEnd().orElseThrow();
WindowNode.Frame baseFrame = new WindowNode.Frame(
WindowFrame.Type.ROWS,
FrameBound.Type.CURRENT_ROW,
Optional.empty(),
Optional.empty(),
frameEnd.getType(),
frameEndSymbol,
Optional.empty(),
Optional.empty(),
frameEnd.getValue());
ImmutableMap.Builder, Symbol> mappings = ImmutableMap.builder();
ImmutableMap.Builder functions = ImmutableMap.builder();
for (FunctionCall windowFunction : windowFunctions) {
Symbol newSymbol = symbolAllocator.newSymbol(windowFunction, analysis.getType(windowFunction));
NullTreatment nullTreatment = windowFunction.getNullTreatment()
.orElse(NullTreatment.RESPECT);
WindowNode.Function function = new WindowNode.Function(
analysis.getResolvedFunction(windowFunction),
windowFunction.getArguments().stream()
.map(argument -> {
if (argument instanceof LambdaExpression) {
return subPlan.rewrite(argument);
}
return coercions.get(argument).toSymbolReference();
})
.collect(toImmutableList()),
baseFrame,
nullTreatment == NullTreatment.IGNORE);
functions.put(newSymbol, function);
mappings.put(scopeAwareKey(windowFunction, analysis, subPlan.getScope()), newSymbol);
}
PatternRecognitionComponents components = new RelationPlanner(analysis, symbolAllocator, idAllocator, lambdaDeclarationToSymbolMap, plannerContext, outerContext, session, recursiveSubqueries)
.planPatternRecognitionComponents(
subPlan::rewrite,
frame.getSubsets(),
ImmutableList.of(),
frame.getAfterMatchSkipTo(),
frame.getPatternSearchMode(),
frame.getPattern().orElseThrow(),
frame.getVariableDefinitions());
// create pattern recognition node
return new PlanBuilder(
subPlan.getTranslations()
.withAdditionalMappings(mappings.buildOrThrow()),
new PatternRecognitionNode(
idAllocator.getNextId(),
subPlan.getRoot(),
specification,
Optional.empty(),
ImmutableSet.of(),
0,
functions.buildOrThrow(),
components.getMeasures(),
Optional.of(baseFrame),
RowsPerMatch.WINDOW,
components.getSkipToLabel(),
components.getSkipToPosition(),
components.isInitial(),
components.getPattern(),
components.getSubsets(),
components.getVariableDefinitions()));
}
public static DataOrganizationSpecification planWindowSpecification(List partitionBy, Optional orderBy, Function expressionRewrite)
{
// Rewrite PARTITION BY
ImmutableList.Builder partitionBySymbols = ImmutableList.builder();
for (Expression expression : partitionBy) {
partitionBySymbols.add(expressionRewrite.apply(expression));
}
// Rewrite ORDER BY
LinkedHashMap orderings = new LinkedHashMap<>();
for (SortItem item : getSortItemsFromOrderBy(orderBy)) {
Symbol symbol = expressionRewrite.apply(item.getSortKey());
// don't override existing keys, i.e. when "ORDER BY a ASC, a DESC" is specified
orderings.putIfAbsent(symbol, sortItemToSortOrder(item));
}
Optional orderingScheme = Optional.empty();
if (!orderings.isEmpty()) {
orderingScheme = Optional.of(new OrderingScheme(ImmutableList.copyOf(orderings.keySet()), orderings));
}
return new DataOrganizationSpecification(partitionBySymbols.build(), orderingScheme);
}
private PlanBuilder planWindowMeasures(Node node, PlanBuilder subPlan, List windowMeasures)
{
if (windowMeasures.isEmpty()) {
return subPlan;
}
for (WindowOperation windowMeasure : scopeAwareDistinct(subPlan, windowMeasures)) {
ResolvedWindow window = analysis.getWindow(windowMeasure);
checkState(window != null, "no resolved window for: " + windowMeasure);
// pre-project inputs
ImmutableList.Builder inputsBuilder = ImmutableList.builder()
.addAll(window.getPartitionBy())
.addAll(getSortItemsFromOrderBy(window.getOrderBy()).stream()
.map(SortItem::getSortKey)
.iterator());
WindowFrame frame = window.getFrame().orElseThrow();
Optional endValue = frame.getEnd().orElseThrow().getValue();
endValue.ifPresent(inputsBuilder::add);
List inputs = inputsBuilder.build();
subPlan = subqueryPlanner.handleSubqueries(subPlan, inputs, analysis.getSubqueries(node));
subPlan = subPlan.appendProjections(inputs, symbolAllocator, idAllocator);
// process frame end
FrameOffsetPlanAndSymbol plan = planFrameOffset(subPlan, endValue.map(subPlan::translate));
subPlan = plan.getSubPlan();
Optional frameEnd = plan.getFrameOffsetSymbol();
subPlan = subqueryPlanner.handleSubqueries(subPlan, extractPatternRecognitionExpressions(frame.getVariableDefinitions(), frame.getMeasures()), analysis.getSubqueries(node));
subPlan = planPatternRecognition(subPlan, windowMeasure, window, frameEnd);
}
return subPlan;
}
public static List extractPatternRecognitionExpressions(List variableDefinitions, List measureDefinitions)
{
ImmutableList.Builder expressions = ImmutableList.builder();
variableDefinitions.stream()
.map(VariableDefinition::getExpression)
.forEach(expressions::add);
measureDefinitions.stream()
.map(MeasureDefinition::getExpression)
.forEach(expressions::add);
return expressions.build();
}
private PlanBuilder planPatternRecognition(
PlanBuilder subPlan,
WindowOperation windowMeasure,
ResolvedWindow window,
Optional frameEndSymbol)
{
DataOrganizationSpecification specification = planWindowSpecification(window.getPartitionBy(), window.getOrderBy(), subPlan::translate);
// in window frame with pattern recognition, the frame extent is specified as `ROWS BETWEEN CURRENT ROW AND ... `
WindowFrame frame = window.getFrame().orElseThrow();
FrameBound frameEnd = frame.getEnd().orElseThrow();
WindowNode.Frame baseFrame = new WindowNode.Frame(
WindowFrame.Type.ROWS,
FrameBound.Type.CURRENT_ROW,
Optional.empty(),
Optional.empty(),
frameEnd.getType(),
frameEndSymbol,
Optional.empty(),
Optional.empty(),
frameEnd.getValue());
PatternRecognitionComponents components = new RelationPlanner(analysis, symbolAllocator, idAllocator, lambdaDeclarationToSymbolMap, plannerContext, outerContext, session, recursiveSubqueries)
.planPatternRecognitionComponents(
subPlan::rewrite,
frame.getSubsets(),
ImmutableList.of(analysis.getMeasureDefinition(windowMeasure)),
frame.getAfterMatchSkipTo(),
frame.getPatternSearchMode(),
frame.getPattern().orElseThrow(),
frame.getVariableDefinitions());
Symbol measureSymbol = getOnlyElement(components.getMeasures().keySet());
// create pattern recognition node
return new PlanBuilder(
subPlan.getTranslations()
.withAdditionalMappings(ImmutableMap.of(scopeAwareKey(windowMeasure, analysis, subPlan.getScope()), measureSymbol)),
new PatternRecognitionNode(
idAllocator.getNextId(),
subPlan.getRoot(),
specification,
Optional.empty(),
ImmutableSet.of(),
0,
ImmutableMap.of(),
components.getMeasures(),
Optional.of(baseFrame),
RowsPerMatch.WINDOW,
components.getSkipToLabel(),
components.getSkipToPosition(),
components.isInitial(),
components.getPattern(),
components.getSubsets(),
components.getVariableDefinitions()));
}
/**
* Creates a projection with any additional coercions by identity of the provided expressions.
*
* @return the new subplan and a mapping of each expression to the symbol representing the coercion or an existing symbol if a coercion wasn't needed
*/
public static PlanAndMappings coerce(PlanBuilder subPlan, List expressions, Analysis analysis, PlanNodeIdAllocator idAllocator, SymbolAllocator symbolAllocator, TypeCoercion typeCoercion)
{
Assignments.Builder assignments = Assignments.builder();
assignments.putIdentities(subPlan.getRoot().getOutputSymbols());
Map, Symbol> mappings = new HashMap<>();
for (Expression expression : expressions) {
Type coercion = analysis.getCoercion(expression);
// expressions may be repeated, for example, when resolving ordinal references in a GROUP BY clause
if (!mappings.containsKey(NodeRef.of(expression))) {
if (coercion != null) {
Type type = analysis.getType(expression);
Symbol symbol = symbolAllocator.newSymbol(expression, coercion);
assignments.put(symbol, new Cast(
subPlan.rewrite(expression),
toSqlType(coercion),
false,
typeCoercion.isTypeOnlyCoercion(type, coercion)));
mappings.put(NodeRef.of(expression), symbol);
}
else {
mappings.put(NodeRef.of(expression), subPlan.translate(expression));
}
}
}
subPlan = subPlan.withNewRoot(
new ProjectNode(
idAllocator.getNextId(),
subPlan.getRoot(),
assignments.build()));
return new PlanAndMappings(subPlan, mappings);
}
public static Expression coerceIfNecessary(Analysis analysis, Expression original, Expression rewritten)
{
Type coercion = analysis.getCoercion(original);
if (coercion == null) {
return rewritten;
}
return new Cast(
rewritten,
toSqlType(coercion),
false,
analysis.isTypeOnlyCoercion(original));
}
public static NodeAndMappings coerce(RelationPlan plan, List types, SymbolAllocator symbolAllocator, PlanNodeIdAllocator idAllocator)
{
List visibleFields = visibleFields(plan);
checkArgument(visibleFields.size() == types.size());
Assignments.Builder assignments = Assignments.builder();
ImmutableList.Builder mappings = ImmutableList.builder();
for (int i = 0; i < types.size(); i++) {
Symbol input = visibleFields.get(i);
Type type = types.get(i);
if (!symbolAllocator.getTypes().get(input).equals(type)) {
Symbol coerced = symbolAllocator.newSymbol(input.getName(), type);
assignments.put(coerced, new Cast(input.toSymbolReference(), toSqlType(type)));
mappings.add(coerced);
}
else {
assignments.putIdentity(input);
mappings.add(input);
}
}
ProjectNode coerced = new ProjectNode(idAllocator.getNextId(), plan.getRoot(), assignments.build());
return new NodeAndMappings(coerced, mappings.build());
}
public static List visibleFields(RelationPlan subPlan)
{
RelationType descriptor = subPlan.getDescriptor();
return descriptor.getAllFields().stream()
.filter(field -> !field.isHidden())
.map(descriptor::indexOf)
.map(subPlan.getFieldMappings()::get)
.collect(toImmutableList());
}
public static NodeAndMappings pruneInvisibleFields(RelationPlan plan, PlanNodeIdAllocator idAllocator)
{
List visibleFields = visibleFields(plan);
ProjectNode pruned = new ProjectNode(idAllocator.getNextId(), plan.getRoot(), Assignments.identity(visibleFields));
return new NodeAndMappings(pruned, visibleFields);
}
public static NodeAndMappings disambiguateOutputs(NodeAndMappings plan, SymbolAllocator symbolAllocator, PlanNodeIdAllocator idAllocator)
{
Set distinctOutputs = ImmutableSet.copyOf(plan.getFields());
if (distinctOutputs.size() < plan.getFields().size()) {
Assignments.Builder assignments = Assignments.builder();
ImmutableList.Builder newOutputs = ImmutableList.builder();
Set uniqueOutputs = new HashSet<>();
for (Symbol output : plan.getFields()) {
if (uniqueOutputs.add(output)) {
assignments.putIdentity(output);
newOutputs.add(output);
}
else {
Symbol newOutput = symbolAllocator.newSymbol(output);
assignments.put(newOutput, output.toSymbolReference());
newOutputs.add(newOutput);
}
}
return new NodeAndMappings(new ProjectNode(idAllocator.getNextId(), plan.getNode(), assignments.build()), newOutputs.build());
}
return plan;
}
private PlanBuilder distinct(PlanBuilder subPlan, QuerySpecification node, List expressions)
{
if (node.getSelect().isDistinct()) {
List symbols = expressions.stream()
.map(subPlan::translate)
.collect(Collectors.toList());
return subPlan.withNewRoot(
singleAggregation(
idAllocator.getNextId(),
subPlan.getRoot(),
ImmutableMap.of(),
singleGroupingSet(symbols)));
}
return subPlan;
}
private Optional orderingScheme(PlanBuilder subPlan, Optional orderBy, List orderByExpressions)
{
if (orderBy.isEmpty() || (isSkipRedundantSort(session) && analysis.isOrderByRedundant(orderBy.get()))) {
return Optional.empty();
}
Iterator sortItems = orderBy.get().getSortItems().iterator();
ImmutableList.Builder orderBySymbols = ImmutableList.builder();
Map orderings = new HashMap<>();
for (Expression fieldOrExpression : orderByExpressions) {
Symbol symbol = subPlan.translate(fieldOrExpression);
SortItem sortItem = sortItems.next();
if (!orderings.containsKey(symbol)) {
orderBySymbols.add(symbol);
orderings.put(symbol, sortItemToSortOrder(sortItem));
}
}
return Optional.of(new OrderingScheme(orderBySymbols.build(), orderings));
}
private PlanBuilder sort(PlanBuilder subPlan, Optional orderingScheme)
{
if (orderingScheme.isEmpty()) {
return subPlan;
}
return subPlan.withNewRoot(
new SortNode(
idAllocator.getNextId(),
subPlan.getRoot(),
orderingScheme.get(),
false));
}
private PlanBuilder offset(PlanBuilder subPlan, Optional offset)
{
if (offset.isEmpty()) {
return subPlan;
}
return subPlan.withNewRoot(
new OffsetNode(
idAllocator.getNextId(),
subPlan.getRoot(),
analysis.getOffset(offset.get())));
}
private PlanBuilder limit(PlanBuilder subPlan, Optional limit, Optional orderingScheme)
{
if (limit.isPresent() && analysis.getLimit(limit.get()).isPresent()) {
Optional tiesResolvingScheme = Optional.empty();
if (limit.get() instanceof FetchFirst && ((FetchFirst) limit.get()).isWithTies()) {
tiesResolvingScheme = orderingScheme;
}
return subPlan.withNewRoot(
new LimitNode(
idAllocator.getNextId(),
subPlan.getRoot(),
analysis.getLimit(limit.get()).getAsLong(),
tiesResolvingScheme,
false,
ImmutableList.of()));
}
return subPlan;
}
private static class GroupingSetsPlan
{
private final PlanBuilder subPlan;
private final List> columnOnlyGroupingSets;
private final List> groupingSets;
private final Optional groupIdSymbol;
public GroupingSetsPlan(PlanBuilder subPlan, List> columnOnlyGroupingSets, List> groupingSets, Optional groupIdSymbol)
{
this.columnOnlyGroupingSets = columnOnlyGroupingSets;
this.groupingSets = groupingSets;
this.groupIdSymbol = groupIdSymbol;
this.subPlan = subPlan;
}
public PlanBuilder getSubPlan()
{
return subPlan;
}
public List> getColumnOnlyGroupingSets()
{
return columnOnlyGroupingSets;
}
public List> getGroupingSets()
{
return groupingSets;
}
public Optional getGroupIdSymbol()
{
return groupIdSymbol;
}
}
public static class PlanAndMappings
{
private final PlanBuilder subPlan;
private final Map, Symbol> mappings;
public PlanAndMappings(PlanBuilder subPlan, Map, Symbol> mappings)
{
this.subPlan = subPlan;
this.mappings = ImmutableMap.copyOf(mappings);
}
public PlanBuilder getSubPlan()
{
return subPlan;
}
public Symbol get(Expression expression)
{
return tryGet(expression)
.orElseThrow(() -> new IllegalArgumentException(format("No mapping for expression: %s (%s)", expression, System.identityHashCode(expression))));
}
public Optional tryGet(Expression expression)
{
Symbol result = mappings.get(NodeRef.of(expression));
if (result != null) {
return Optional.of(result);
}
return Optional.empty();
}
}
private static class AggregationAssignment
{
private final Symbol symbol;
private final Expression astExpression;
private final Aggregation aggregation;
public AggregationAssignment(Symbol symbol, Expression astExpression, Aggregation aggregation)
{
this.astExpression = astExpression;
this.symbol = symbol;
this.aggregation = aggregation;
}
public Symbol getSymbol()
{
return symbol;
}
public Expression getAstExpression()
{
return astExpression;
}
public Aggregation getRewritten()
{
return aggregation;
}
}
private static class FrameBoundPlanAndSymbols
{
private final PlanBuilder subPlan;
private final Optional frameBoundSymbol;
private final Optional sortKeyCoercedForFrameBoundComparison;
public FrameBoundPlanAndSymbols(PlanBuilder subPlan, Optional frameBoundSymbol, Optional sortKeyCoercedForFrameBoundComparison)
{
this.subPlan = subPlan;
this.frameBoundSymbol = frameBoundSymbol;
this.sortKeyCoercedForFrameBoundComparison = sortKeyCoercedForFrameBoundComparison;
}
public PlanBuilder getSubPlan()
{
return subPlan;
}
public Optional getFrameBoundSymbol()
{
return frameBoundSymbol;
}
public Optional getSortKeyCoercedForFrameBoundComparison()
{
return sortKeyCoercedForFrameBoundComparison;
}
}
private static class FrameOffsetPlanAndSymbol
{
private final PlanBuilder subPlan;
private final Optional frameOffsetSymbol;
public FrameOffsetPlanAndSymbol(PlanBuilder subPlan, Optional frameOffsetSymbol)
{
this.subPlan = subPlan;
this.frameOffsetSymbol = frameOffsetSymbol;
}
public PlanBuilder getSubPlan()
{
return subPlan;
}
public Optional getFrameOffsetSymbol()
{
return frameOffsetSymbol;
}
}
}