blob: e19faca671c5a196a2920e0993519e4192e6bc24 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.calcite.rel.core;
import org.apache.calcite.plan.Convention;
import org.apache.calcite.plan.RelOptCluster;
import org.apache.calcite.plan.RelTraitSet;
import org.apache.calcite.rel.RelInput;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.RelWriter;
import org.apache.calcite.rel.SingleRel;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
import org.apache.calcite.rel.type.RelDataTypeField;
import org.apache.calcite.sql.SqlUnnestOperator;
import org.apache.calcite.sql.type.MapSqlType;
import org.apache.calcite.sql.type.SqlTypeName;
import com.google.common.collect.ImmutableList;
import java.util.Collections;
import java.util.List;
import static org.apache.calcite.util.Static.RESOURCE;
/**
* Relational expression that unnests its input's columns into a relation.
*
* <p>The input may have multiple columns, but each must be a multiset or
* array. If {@code withOrdinality}, the output contains an extra
* {@code ORDINALITY} column.
*
* <p>Like its inverse operation {@link Collect}, Uncollect is generally
* invoked in a nested loop, driven by
* {@link org.apache.calcite.rel.logical.LogicalCorrelate} or similar.
*/
public class Uncollect extends SingleRel {
public final boolean withOrdinality;
// To alias the items in Uncollect list,
// i.e., "UNNEST(a, b, c) as T(d, e, f)"
// outputs as row type Record(d, e, f) where the field "d" has element type of "a",
// field "e" has element type of "b"(Presto dialect).
// Without the aliases, the expression "UNNEST(a)" outputs row type
// same with element type of "a".
private final List<String> itemAliases;
//~ Constructors -----------------------------------------------------------
@Deprecated // to be removed before 2.0
public Uncollect(RelOptCluster cluster, RelTraitSet traitSet,
RelNode child) {
this(cluster, traitSet, child, false, Collections.emptyList());
}
/** Creates an Uncollect.
*
* <p>Use {@link #create} unless you know what you're doing. */
@SuppressWarnings("method.invocation.invalid")
public Uncollect(RelOptCluster cluster, RelTraitSet traitSet, RelNode input,
boolean withOrdinality, List<String> itemAliases) {
super(cluster, traitSet, input);
this.withOrdinality = withOrdinality;
this.itemAliases = ImmutableList.copyOf(itemAliases);
assert deriveRowType() != null : "invalid child rowtype";
}
/**
* Creates an Uncollect by parsing serialized output.
*/
public Uncollect(RelInput input) {
this(input.getCluster(), input.getTraitSet(), input.getInput(),
input.getBoolean("withOrdinality", false), Collections.emptyList());
}
/**
* Creates an Uncollect.
*
* <p>Each field of the input relational expression must be an array or
* multiset.
*
* @param traitSet Trait set
* @param input Input relational expression
* @param withOrdinality Whether output should contain an ORDINALITY column
* @param itemAliases Aliases for the operand items
*/
public static Uncollect create(
RelTraitSet traitSet,
RelNode input,
boolean withOrdinality,
List<String> itemAliases) {
final RelOptCluster cluster = input.getCluster();
return new Uncollect(cluster, traitSet, input, withOrdinality, itemAliases);
}
//~ Methods ----------------------------------------------------------------
@Override public RelWriter explainTerms(RelWriter pw) {
return super.explainTerms(pw)
.itemIf("withOrdinality", withOrdinality, withOrdinality);
}
@Override public final RelNode copy(RelTraitSet traitSet,
List<RelNode> inputs) {
return copy(traitSet, sole(inputs));
}
public RelNode copy(RelTraitSet traitSet, RelNode input) {
assert traitSet.containsIfApplicable(Convention.NONE);
return new Uncollect(getCluster(), traitSet, input, withOrdinality, itemAliases);
}
@Override protected RelDataType deriveRowType() {
return deriveUncollectRowType(input, withOrdinality, itemAliases);
}
/**
* Returns the row type returned by applying the 'UNNEST' operation to a
* relational expression.
*
* <p>Each column in the relational expression must be a multiset of
* structs or an array. The return type is the combination of expanding
* element types from each column, plus an ORDINALITY column if {@code
* withOrdinality}. If {@code itemAliases} is not empty, the element types
* would not expand, each column element outputs as a whole (the return
* type has same column types as input type).
*/
public static RelDataType deriveUncollectRowType(RelNode rel,
boolean withOrdinality, List<String> itemAliases) {
RelDataType inputType = rel.getRowType();
assert inputType.isStruct() : inputType + " is not a struct";
boolean requireAlias = !itemAliases.isEmpty();
assert !requireAlias || itemAliases.size() == inputType.getFieldCount();
final List<RelDataTypeField> fields = inputType.getFieldList();
final RelDataTypeFactory typeFactory = rel.getCluster().getTypeFactory();
final RelDataTypeFactory.Builder builder = typeFactory.builder();
if (fields.size() == 1
&& fields.get(0).getType().getSqlTypeName() == SqlTypeName.ANY) {
// Component type is unknown to Uncollect, build a row type with input column name
// and Any type.
return builder
.add(requireAlias ? itemAliases.get(0) : fields.get(0).getName(), SqlTypeName.ANY)
.nullable(true)
.build();
}
for (int i = 0; i < fields.size(); i++) {
RelDataTypeField field = fields.get(i);
if (field.getType() instanceof MapSqlType) {
MapSqlType mapType = (MapSqlType) field.getType();
builder.add(SqlUnnestOperator.MAP_KEY_COLUMN_NAME, mapType.getKeyType());
builder.add(SqlUnnestOperator.MAP_VALUE_COLUMN_NAME, mapType.getValueType());
} else {
RelDataType ret = field.getType().getComponentType();
if (null == ret) {
throw RESOURCE.unnestArgument().ex();
}
if (requireAlias) {
builder.add(itemAliases.get(i), ret);
} else if (ret.isStruct()) {
builder.addAll(ret.getFieldList());
} else {
// Element type is not a record, use the field name of the element directly
builder.add(field.getName(), ret);
}
}
}
if (withOrdinality) {
builder.add(SqlUnnestOperator.ORDINALITY_COLUMN_NAME,
SqlTypeName.INTEGER);
}
return builder.build();
}
}