Java tutorial
/* * 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 com.facebook.presto.sql.planner.iterative.rule; import com.facebook.presto.Session; import com.facebook.presto.execution.Lifespan; import com.facebook.presto.geospatial.KdbTree; import com.facebook.presto.geospatial.KdbTreeUtils; import com.facebook.presto.matching.Capture; import com.facebook.presto.matching.Captures; import com.facebook.presto.matching.Pattern; import com.facebook.presto.metadata.Metadata; import com.facebook.presto.metadata.QualifiedObjectName; import com.facebook.presto.metadata.Split; import com.facebook.presto.metadata.TableHandle; import com.facebook.presto.metadata.TableLayoutResult; import com.facebook.presto.spi.ColumnHandle; import com.facebook.presto.spi.ConnectorPageSource; import com.facebook.presto.spi.Constraint; import com.facebook.presto.spi.Page; import com.facebook.presto.spi.PrestoException; import com.facebook.presto.spi.type.ArrayType; import com.facebook.presto.spi.type.TypeSignature; import com.facebook.presto.split.PageSourceManager; import com.facebook.presto.split.SplitManager; import com.facebook.presto.split.SplitSource; import com.facebook.presto.split.SplitSource.SplitBatch; import com.facebook.presto.sql.planner.Symbol; import com.facebook.presto.sql.planner.iterative.Rule; import com.facebook.presto.sql.planner.iterative.Rule.Context; import com.facebook.presto.sql.planner.iterative.Rule.Result; import com.facebook.presto.sql.planner.plan.Assignments; import com.facebook.presto.sql.planner.plan.FilterNode; import com.facebook.presto.sql.planner.plan.JoinNode; import com.facebook.presto.sql.planner.plan.PlanNode; import com.facebook.presto.sql.planner.plan.PlanNodeId; import com.facebook.presto.sql.planner.plan.ProjectNode; import com.facebook.presto.sql.planner.plan.SpatialJoinNode; import com.facebook.presto.sql.planner.plan.UnnestNode; import com.facebook.presto.sql.tree.Cast; import com.facebook.presto.sql.tree.ComparisonExpression; import com.facebook.presto.sql.tree.Expression; import com.facebook.presto.sql.tree.FunctionCall; import com.facebook.presto.sql.tree.QualifiedName; import com.facebook.presto.sql.tree.StringLiteral; import com.facebook.presto.sql.tree.SymbolReference; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import java.io.IOException; import java.io.UncheckedIOException; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import static com.facebook.presto.SystemSessionProperties.getSpatialPartitioningTableName; import static com.facebook.presto.SystemSessionProperties.isSpatialJoinEnabled; import static com.facebook.presto.matching.Capture.newCapture; import static com.facebook.presto.spi.StandardErrorCode.INVALID_SPATIAL_PARTITIONING; import static com.facebook.presto.spi.connector.ConnectorSplitManager.SplitSchedulingStrategy.UNGROUPED_SCHEDULING; import static com.facebook.presto.spi.connector.NotPartitionedPartitionHandle.NOT_PARTITIONED; import static com.facebook.presto.spi.type.DoubleType.DOUBLE; import static com.facebook.presto.spi.type.IntegerType.INTEGER; import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; import static com.facebook.presto.spi.type.VarcharType.VARCHAR; import static com.facebook.presto.sql.planner.ExpressionNodeInliner.replaceExpression; import static com.facebook.presto.sql.planner.SymbolsExtractor.extractUnique; import static com.facebook.presto.sql.planner.plan.JoinNode.Type.INNER; import static com.facebook.presto.sql.planner.plan.JoinNode.Type.LEFT; import static com.facebook.presto.sql.planner.plan.Patterns.filter; import static com.facebook.presto.sql.planner.plan.Patterns.join; import static com.facebook.presto.sql.planner.plan.Patterns.source; import static com.facebook.presto.sql.tree.ComparisonExpression.Operator.LESS_THAN; import static com.facebook.presto.sql.tree.ComparisonExpression.Operator.LESS_THAN_OR_EQUAL; import static com.facebook.presto.util.SpatialJoinUtils.extractSupportedSpatialComparisons; import static com.facebook.presto.util.SpatialJoinUtils.extractSupportedSpatialFunctions; import static com.google.common.base.Verify.verify; import static com.google.common.collect.ImmutableList.toImmutableList; import static io.airlift.concurrent.MoreFutures.getFutureValue; import static java.lang.String.format; import static java.util.Objects.requireNonNull; /** * Applies to broadcast spatial joins, inner and left, expressed via ST_Contains, * ST_Intersects and ST_Distance functions. * <p> * For example: * <ul> * <li>SELECT ... FROM a, b WHERE ST_Contains(b.geometry, a.geometry)</li> * <li>SELECT ... FROM a, b WHERE ST_Intersects(b.geometry, a.geometry)</li> * <li>SELECT ... FROM a, b WHERE ST_Distance(b.geometry, a.geometry) <= 300</li> * <li>SELECT ... FROM a, b WHERE 15.5 > ST_Distance(b.geometry, a.geometry)</li> * </ul> * <p> * Joins expressed via ST_Contains and ST_Intersects functions must match all of * the following criteria: * <p> * - arguments of the spatial function are non-scalar expressions; * - one of the arguments uses symbols from left side of the join, the other from right. * <p> * Joins expressed via ST_Distance function must use less than or less than or equals operator * to compare ST_Distance value with a radius and must match all of the following criteria: * <p> * - arguments of the spatial function are non-scalar expressions; * - one of the arguments uses symbols from left side of the join, the other from right; * - radius is either scalar expression or uses symbols only from the right (build) side of the join. * <p> * For inner join, replaces cross join node and a qualifying filter on top with a single * spatial join node. * <p> * For both inner and left joins, pushes non-trivial expressions of the spatial function * arguments and radius into projections on top of join child nodes. * <p> * Examples: * <pre> * Point-in-polygon inner join * ST_Contains(ST_GeometryFromText(a.wkt), ST_Point(b.longitude, b.latitude)) * becomes a spatial join * ST_Contains(st_geometryfromtext, st_point) * with st_geometryfromtext -> 'ST_GeometryFromText(a.wkt)' and * st_point -> 'ST_Point(b.longitude, b.latitude)' projections on top of child nodes. * * Distance query * ST_Distance(ST_Point(a.lon, a.lat), ST_Point(b.lon, b.lat)) <= 10 / (111.321 * cos(radians(b.lat))) * becomes a spatial join * ST_Distance(st_point_a, st_point_b) <= radius * with st_point_a -> 'ST_Point(a.lon, a.lat)', st_point_b -> 'ST_Point(b.lon, b.lat)' * and radius -> '10 / (111.321 * cos(radians(b.lat)))' projections on top of child nodes. * </pre> */ public class ExtractSpatialJoins { private static final TypeSignature GEOMETRY_TYPE_SIGNATURE = parseTypeSignature("Geometry"); private static final String KDB_TREE_TYPENAME = "KdbTree"; private final Metadata metadata; private final SplitManager splitManager; private final PageSourceManager pageSourceManager; public ExtractSpatialJoins(Metadata metadata, SplitManager splitManager, PageSourceManager pageSourceManager) { this.metadata = requireNonNull(metadata, "metadata is null"); this.splitManager = requireNonNull(splitManager, "splitManager is null"); this.pageSourceManager = requireNonNull(pageSourceManager, "pageSourceManager is null"); } public Set<Rule<?>> rules() { return ImmutableSet.of(new ExtractSpatialInnerJoin(metadata, splitManager, pageSourceManager), new ExtractSpatialLeftJoin(metadata, splitManager, pageSourceManager)); } @VisibleForTesting public static final class ExtractSpatialInnerJoin implements Rule<FilterNode> { private static final Capture<JoinNode> JOIN = newCapture(); private static final Pattern<FilterNode> PATTERN = filter() .with(source().matching(join().capturedAs(JOIN).matching(JoinNode::isCrossJoin))); private final Metadata metadata; private final SplitManager splitManager; private final PageSourceManager pageSourceManager; public ExtractSpatialInnerJoin(Metadata metadata, SplitManager splitManager, PageSourceManager pageSourceManager) { this.metadata = requireNonNull(metadata, "metadata is null"); this.splitManager = requireNonNull(splitManager, "splitManager is null"); this.pageSourceManager = requireNonNull(pageSourceManager, "pageSourceManager is null"); } @Override public boolean isEnabled(Session session) { return isSpatialJoinEnabled(session); } @Override public Pattern<FilterNode> getPattern() { return PATTERN; } @Override public Result apply(FilterNode node, Captures captures, Context context) { JoinNode joinNode = captures.get(JOIN); Expression filter = node.getPredicate(); List<FunctionCall> spatialFunctions = extractSupportedSpatialFunctions(filter); for (FunctionCall spatialFunction : spatialFunctions) { Result result = tryCreateSpatialJoin(context, joinNode, filter, node.getId(), node.getOutputSymbols(), spatialFunction, Optional.empty(), metadata, splitManager, pageSourceManager); if (!result.isEmpty()) { return result; } } List<ComparisonExpression> spatialComparisons = extractSupportedSpatialComparisons(filter); for (ComparisonExpression spatialComparison : spatialComparisons) { Result result = tryCreateSpatialJoin(context, joinNode, filter, node.getId(), node.getOutputSymbols(), spatialComparison, metadata, splitManager, pageSourceManager); if (!result.isEmpty()) { return result; } } return Result.empty(); } } @VisibleForTesting public static final class ExtractSpatialLeftJoin implements Rule<JoinNode> { private static final Pattern<JoinNode> PATTERN = join().matching( node -> node.getCriteria().isEmpty() && node.getFilter().isPresent() && node.getType() == LEFT); private final Metadata metadata; private final SplitManager splitManager; private final PageSourceManager pageSourceManager; public ExtractSpatialLeftJoin(Metadata metadata, SplitManager splitManager, PageSourceManager pageSourceManager) { this.metadata = requireNonNull(metadata, "metadata is null"); this.splitManager = requireNonNull(splitManager, "splitManager is null"); this.pageSourceManager = requireNonNull(pageSourceManager, "pageSourceManager is null"); } @Override public boolean isEnabled(Session session) { return isSpatialJoinEnabled(session); } @Override public Pattern<JoinNode> getPattern() { return PATTERN; } @Override public Result apply(JoinNode joinNode, Captures captures, Context context) { Expression filter = joinNode.getFilter().get(); List<FunctionCall> spatialFunctions = extractSupportedSpatialFunctions(filter); for (FunctionCall spatialFunction : spatialFunctions) { Result result = tryCreateSpatialJoin(context, joinNode, filter, joinNode.getId(), joinNode.getOutputSymbols(), spatialFunction, Optional.empty(), metadata, splitManager, pageSourceManager); if (!result.isEmpty()) { return result; } } List<ComparisonExpression> spatialComparisons = extractSupportedSpatialComparisons(filter); for (ComparisonExpression spatialComparison : spatialComparisons) { Result result = tryCreateSpatialJoin(context, joinNode, filter, joinNode.getId(), joinNode.getOutputSymbols(), spatialComparison, metadata, splitManager, pageSourceManager); if (!result.isEmpty()) { return result; } } return Result.empty(); } } private static Result tryCreateSpatialJoin(Context context, JoinNode joinNode, Expression filter, PlanNodeId nodeId, List<Symbol> outputSymbols, ComparisonExpression spatialComparison, Metadata metadata, SplitManager splitManager, PageSourceManager pageSourceManager) { PlanNode leftNode = joinNode.getLeft(); PlanNode rightNode = joinNode.getRight(); List<Symbol> leftSymbols = leftNode.getOutputSymbols(); List<Symbol> rightSymbols = rightNode.getOutputSymbols(); Expression radius; Optional<Symbol> newRadiusSymbol; ComparisonExpression newComparison; if (spatialComparison.getOperator() == LESS_THAN || spatialComparison.getOperator() == LESS_THAN_OR_EQUAL) { // ST_Distance(a, b) <= r radius = spatialComparison.getRight(); Set<Symbol> radiusSymbols = extractUnique(radius); if (radiusSymbols.isEmpty() || (rightSymbols.containsAll(radiusSymbols) && containsNone(leftSymbols, radiusSymbols))) { newRadiusSymbol = newRadiusSymbol(context, radius); newComparison = new ComparisonExpression(spatialComparison.getOperator(), spatialComparison.getLeft(), toExpression(newRadiusSymbol, radius)); } else { return Result.empty(); } } else { // r >= ST_Distance(a, b) radius = spatialComparison.getLeft(); Set<Symbol> radiusSymbols = extractUnique(radius); if (radiusSymbols.isEmpty() || (rightSymbols.containsAll(radiusSymbols) && containsNone(leftSymbols, radiusSymbols))) { newRadiusSymbol = newRadiusSymbol(context, radius); newComparison = new ComparisonExpression(spatialComparison.getOperator().flip(), spatialComparison.getRight(), toExpression(newRadiusSymbol, radius)); } else { return Result.empty(); } } Expression newFilter = replaceExpression(filter, ImmutableMap.of(spatialComparison, newComparison)); PlanNode newRightNode = newRadiusSymbol.map(symbol -> addProjection(context, rightNode, symbol, radius)) .orElse(rightNode); JoinNode newJoinNode = new JoinNode(joinNode.getId(), joinNode.getType(), leftNode, newRightNode, joinNode.getCriteria(), joinNode.getOutputSymbols(), Optional.of(newFilter), joinNode.getLeftHashSymbol(), joinNode.getRightHashSymbol(), joinNode.getDistributionType()); return tryCreateSpatialJoin(context, newJoinNode, newFilter, nodeId, outputSymbols, (FunctionCall) newComparison.getLeft(), Optional.of(newComparison.getRight()), metadata, splitManager, pageSourceManager); } private static Result tryCreateSpatialJoin(Context context, JoinNode joinNode, Expression filter, PlanNodeId nodeId, List<Symbol> outputSymbols, FunctionCall spatialFunction, Optional<Expression> radius, Metadata metadata, SplitManager splitManager, PageSourceManager pageSourceManager) { // TODO Add support for distributed left spatial joins Optional<String> spatialPartitioningTableName = joinNode.getType() == INNER ? getSpatialPartitioningTableName(context.getSession()) : Optional.empty(); Optional<KdbTree> kdbTree = spatialPartitioningTableName.map(tableName -> loadKdbTree(tableName, context.getSession(), metadata, splitManager, pageSourceManager)); List<Expression> arguments = spatialFunction.getArguments(); verify(arguments.size() == 2); Expression firstArgument = arguments.get(0); Expression secondArgument = arguments.get(1); Set<Symbol> firstSymbols = extractUnique(firstArgument); Set<Symbol> secondSymbols = extractUnique(secondArgument); if (firstSymbols.isEmpty() || secondSymbols.isEmpty()) { return Result.empty(); } Optional<Symbol> newFirstSymbol = newGeometrySymbol(context, firstArgument, metadata); Optional<Symbol> newSecondSymbol = newGeometrySymbol(context, secondArgument, metadata); PlanNode leftNode = joinNode.getLeft(); PlanNode rightNode = joinNode.getRight(); PlanNode newLeftNode; PlanNode newRightNode; // Check if the order of arguments of the spatial function matches the order of join sides int alignment = checkAlignment(joinNode, firstSymbols, secondSymbols); if (alignment > 0) { newLeftNode = newFirstSymbol.map(symbol -> addProjection(context, leftNode, symbol, firstArgument)) .orElse(leftNode); newRightNode = newSecondSymbol.map(symbol -> addProjection(context, rightNode, symbol, secondArgument)) .orElse(rightNode); } else if (alignment < 0) { newLeftNode = newSecondSymbol.map(symbol -> addProjection(context, leftNode, symbol, secondArgument)) .orElse(leftNode); newRightNode = newFirstSymbol.map(symbol -> addProjection(context, rightNode, symbol, firstArgument)) .orElse(rightNode); } else { return Result.empty(); } Expression newFirstArgument = toExpression(newFirstSymbol, firstArgument); Expression newSecondArgument = toExpression(newSecondSymbol, secondArgument); Optional<Symbol> leftPartitionSymbol = Optional.empty(); Optional<Symbol> rightPartitionSymbol = Optional.empty(); if (kdbTree.isPresent()) { leftPartitionSymbol = Optional.of(context.getSymbolAllocator().newSymbol("pid", INTEGER)); rightPartitionSymbol = Optional.of(context.getSymbolAllocator().newSymbol("pid", INTEGER)); if (alignment > 0) { newLeftNode = addPartitioningNodes(context, newLeftNode, leftPartitionSymbol.get(), kdbTree.get(), newFirstArgument, Optional.empty()); newRightNode = addPartitioningNodes(context, newRightNode, rightPartitionSymbol.get(), kdbTree.get(), newSecondArgument, radius); } else { newLeftNode = addPartitioningNodes(context, newLeftNode, leftPartitionSymbol.get(), kdbTree.get(), newSecondArgument, Optional.empty()); newRightNode = addPartitioningNodes(context, newRightNode, rightPartitionSymbol.get(), kdbTree.get(), newFirstArgument, radius); } } Expression newSpatialFunction = new FunctionCall(spatialFunction.getName(), ImmutableList.of(newFirstArgument, newSecondArgument)); Expression newFilter = replaceExpression(filter, ImmutableMap.of(spatialFunction, newSpatialFunction)); return Result.ofPlanNode(new SpatialJoinNode(nodeId, SpatialJoinNode.Type.fromJoinNodeType(joinNode.getType()), newLeftNode, newRightNode, outputSymbols, newFilter, leftPartitionSymbol, rightPartitionSymbol, kdbTree.map(KdbTreeUtils::toJson))); } private static KdbTree loadKdbTree(String tableName, Session session, Metadata metadata, SplitManager splitManager, PageSourceManager pageSourceManager) { QualifiedObjectName name = toQualifiedObjectName(tableName, session.getCatalog().get(), session.getSchema().get()); TableHandle tableHandle = metadata.getTableHandle(session, name).orElseThrow( () -> new PrestoException(INVALID_SPATIAL_PARTITIONING, format("Table not found: %s", name))); Map<String, ColumnHandle> columnHandles = metadata.getColumnHandles(session, tableHandle); List<ColumnHandle> visibleColumnHandles = columnHandles.values().stream() .filter(handle -> !metadata.getColumnMetadata(session, tableHandle, handle).isHidden()) .collect(toImmutableList()); checkSpatialPartitioningTable(visibleColumnHandles.size() == 1, "Expected single column for table %s, but found %s columns", name, columnHandles.size()); ColumnHandle kdbTreeColumn = Iterables.getOnlyElement(visibleColumnHandles); List<TableLayoutResult> layouts = metadata.getLayouts(session, tableHandle, Constraint.alwaysTrue(), Optional.of(ImmutableSet.of(kdbTreeColumn))); checkSpatialPartitioningTable(!layouts.isEmpty(), "Table is empty: %s", name); Optional<KdbTree> kdbTree = Optional.empty(); try (SplitSource splitSource = splitManager.getSplits(session, layouts.get(0).getLayout().getHandle(), UNGROUPED_SCHEDULING)) { while (!Thread.currentThread().isInterrupted()) { SplitBatch splitBatch = getFutureValue( splitSource.getNextBatch(NOT_PARTITIONED, Lifespan.taskWide(), 1000)); List<Split> splits = splitBatch.getSplits(); for (Split split : splits) { try (ConnectorPageSource pageSource = pageSourceManager.createPageSource(session, split, ImmutableList.of(kdbTreeColumn))) { do { getFutureValue(pageSource.isBlocked()); Page page = pageSource.getNextPage(); if (page != null && page.getPositionCount() > 0) { checkSpatialPartitioningTable(!kdbTree.isPresent(), "Expected exactly one row for table %s, but found more", name); checkSpatialPartitioningTable(page.getPositionCount() == 1, "Expected exactly one row for table %s, but found %s rows", name, page.getPositionCount()); String kdbTreeJson = VARCHAR.getSlice(page.getBlock(0), 0).toStringUtf8(); try { kdbTree = Optional.of(KdbTreeUtils.fromJson(kdbTreeJson)); } catch (IllegalArgumentException e) { checkSpatialPartitioningTable(false, "Invalid JSON string for KDB tree: %s", e.getMessage()); } } } while (!pageSource.isFinished()); } catch (IOException e) { throw new UncheckedIOException(e); } } if (splitBatch.isLastBatch()) { break; } } } checkSpatialPartitioningTable(kdbTree.isPresent(), "Expected exactly one row for table %s, but got none", name); return kdbTree.get(); } private static void checkSpatialPartitioningTable(boolean condition, String message, Object... arguments) { if (!condition) { throw new PrestoException(INVALID_SPATIAL_PARTITIONING, format(message, arguments)); } } private static QualifiedObjectName toQualifiedObjectName(String name, String catalog, String schema) { ImmutableList<String> ids = ImmutableList.copyOf(Splitter.on('.').split(name)); if (ids.size() == 3) { return new QualifiedObjectName(ids.get(0), ids.get(1), ids.get(2)); } if (ids.size() == 2) { return new QualifiedObjectName(catalog, ids.get(0), ids.get(1)); } if (ids.size() == 1) { return new QualifiedObjectName(catalog, schema, ids.get(0)); } throw new PrestoException(INVALID_SPATIAL_PARTITIONING, format("Invalid name: %s", name)); } private static int checkAlignment(JoinNode joinNode, Set<Symbol> maybeLeftSymbols, Set<Symbol> maybeRightSymbols) { List<Symbol> leftSymbols = joinNode.getLeft().getOutputSymbols(); List<Symbol> rightSymbols = joinNode.getRight().getOutputSymbols(); if (leftSymbols.containsAll(maybeLeftSymbols) && containsNone(leftSymbols, maybeRightSymbols) && rightSymbols.containsAll(maybeRightSymbols) && containsNone(rightSymbols, maybeLeftSymbols)) { return 1; } if (leftSymbols.containsAll(maybeRightSymbols) && containsNone(leftSymbols, maybeLeftSymbols) && rightSymbols.containsAll(maybeLeftSymbols) && containsNone(rightSymbols, maybeRightSymbols)) { return -1; } return 0; } private static Expression toExpression(Optional<Symbol> optionalSymbol, Expression defaultExpression) { return optionalSymbol.map(symbol -> (Expression) symbol.toSymbolReference()).orElse(defaultExpression); } private static Optional<Symbol> newGeometrySymbol(Context context, Expression expression, Metadata metadata) { if (expression instanceof SymbolReference) { return Optional.empty(); } return Optional .of(context.getSymbolAllocator().newSymbol(expression, metadata.getType(GEOMETRY_TYPE_SIGNATURE))); } private static Optional<Symbol> newRadiusSymbol(Context context, Expression expression) { if (expression instanceof SymbolReference) { return Optional.empty(); } return Optional.of(context.getSymbolAllocator().newSymbol(expression, DOUBLE)); } private static PlanNode addProjection(Context context, PlanNode node, Symbol symbol, Expression expression) { Assignments.Builder projections = Assignments.builder(); for (Symbol outputSymbol : node.getOutputSymbols()) { projections.putIdentity(outputSymbol); } projections.put(symbol, expression); return new ProjectNode(context.getIdAllocator().getNextId(), node, projections.build()); } private static PlanNode addPartitioningNodes(Context context, PlanNode node, Symbol partitionSymbol, KdbTree kdbTree, Expression geometry, Optional<Expression> radius) { Assignments.Builder projections = Assignments.builder(); for (Symbol outputSymbol : node.getOutputSymbols()) { projections.putIdentity(outputSymbol); } ImmutableList.Builder<Expression> partitioningArguments = ImmutableList.<Expression>builder() .add(new Cast(new StringLiteral(KdbTreeUtils.toJson(kdbTree)), KDB_TREE_TYPENAME)).add(geometry); radius.map(partitioningArguments::add); FunctionCall partitioningFunction = new FunctionCall(QualifiedName.of("spatial_partitions"), partitioningArguments.build()); Symbol partitionsSymbol = context.getSymbolAllocator().newSymbol(partitioningFunction, new ArrayType(INTEGER)); projections.put(partitionsSymbol, partitioningFunction); return new UnnestNode(context.getIdAllocator().getNextId(), new ProjectNode(context.getIdAllocator().getNextId(), node, projections.build()), node.getOutputSymbols(), ImmutableMap.of(partitionsSymbol, ImmutableList.of(partitionSymbol)), Optional.empty()); } private static boolean containsNone(Collection<Symbol> values, Collection<Symbol> testValues) { return values.stream().noneMatch(ImmutableSet.copyOf(testValues)::contains); } }