/*
* Licensed to CRATE Technology GmbH ("Crate") under one or more contributor
* license agreements. See the NOTICE file distributed with this work for
* additional information regarding copyright ownership. Crate licenses
* this file to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may
* obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* However, if you have executed another commercial license agreement
* with Crate these terms will supersede the license and you may use the
* software solely pursuant to the terms of the relevant commercial agreement.
*/
package io.crate.analyze.relations;
import io.crate.analyze.OrderBy;
import io.crate.analyze.symbol.Field;
import io.crate.metadata.ColumnIdent;
import io.crate.metadata.Path;
import io.crate.metadata.Reference;
import io.crate.metadata.table.TableInfo;
import io.crate.sql.tree.QualifiedName;
import io.crate.types.ArrayType;
import io.crate.types.DataType;
import io.crate.types.DataTypes;
import io.crate.types.ObjectType;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.*;
import java.util.function.Predicate;
public abstract class AbstractTableRelation<T extends TableInfo> implements AnalyzedRelation, FieldResolver {
private static final Predicate<Reference> IS_OBJECT_ARRAY =
input -> input != null
&& input.valueType().id() == ArrayType.ID
&& ((ArrayType) input.valueType()).innerType().equals(DataTypes.OBJECT);
protected T tableInfo;
private List<Field> outputs;
private Map<Path, Reference> allocatedFields = new HashMap<>();
private QualifiedName qualifiedName;
public AbstractTableRelation(T tableInfo) {
this.tableInfo = tableInfo;
qualifiedName = new QualifiedName(Arrays.asList(tableInfo.ident().schema(), tableInfo.ident().name()));
}
public T tableInfo() {
return tableInfo;
}
@Nullable
public Field getField(Path path) {
ColumnIdent ci = toColumnIdent(path);
Reference reference = tableInfo.getReference(ci);
if (reference == null) {
return null;
}
reference = checkNestedArray(ci, reference);
return allocate(ci, reference);
}
protected Reference checkNestedArray(ColumnIdent ci, Reference reference) {
// TODO: build type correctly as array when the tableInfo is created and remove the conversion here
DataType dataType = null;
ColumnIdent tmpCI = ci;
Reference tmpRI = reference;
while (!tmpCI.isColumn() && hasMatchingParent(tmpRI, IS_OBJECT_ARRAY)) {
if (DataTypes.isCollectionType(reference.valueType())) {
// TODO: remove this limitation with next type refactoring
throw new UnsupportedOperationException("cannot query for arrays inside object arrays explicitly");
}
// for child fields of object arrays
// return references of primitive types as array
if (dataType == null) {
dataType = new ArrayType(reference.valueType());
if (hasNestedObjectReference(tmpRI)) break;
} else {
dataType = new ArrayType(dataType);
}
tmpCI = tmpCI.getParent();
tmpRI = tableInfo.getReference(tmpCI);
}
if (dataType != null) {
return new Reference(
reference.ident(),
reference.granularity(),
dataType,
reference.columnPolicy(),
reference.indexType(),
reference.isNullable());
} else {
return reference;
}
}
protected static ColumnIdent toColumnIdent(Path path) {
try {
return (ColumnIdent) path;
} catch (ClassCastException e) {
throw new UnsupportedOperationException("TableRelation requires a ColumnIdent as path to get a field");
}
}
protected Field allocate(Path path, Reference reference) {
allocatedFields.put(path, reference);
return new Field(this, path, reference.valueType());
}
@Override
public List<Field> fields() {
if (outputs == null) {
outputs = new ArrayList<>(tableInfo.columns().size());
for (Reference reference : tableInfo.columns()) {
if (reference.valueType().equals(DataTypes.NOT_SUPPORTED)) {
continue;
}
ColumnIdent columnIdent = reference.ident().columnIdent();
outputs.add(getField(columnIdent));
}
}
return outputs;
}
@Override
public QualifiedName getQualifiedName() {
return qualifiedName;
}
@Override
public void setQualifiedName(@Nonnull QualifiedName qualifiedName) {
this.qualifiedName = qualifiedName;
}
/**
* return true if the given {@linkplain com.google.common.base.Predicate}
* returns true for a parent column of this one.
* returns false if info has no parent column.
*/
private boolean hasMatchingParent(Reference info, Predicate<Reference> parentMatchPredicate) {
ColumnIdent parent = info.ident().columnIdent().getParent();
while (parent != null) {
Reference parentInfo = tableInfo.getReference(parent);
if (parentMatchPredicate.test(parentInfo)) {
return true;
}
parent = parent.getParent();
}
return false;
}
private boolean hasNestedObjectReference(Reference info) {
ColumnIdent parent = info.ident().columnIdent().getParent();
if (parent != null) {
Reference parentRef = tableInfo.getReference(parent);
if (parentRef.valueType().id() == ObjectType.ID) {
return hasMatchingParent(parentRef, IS_OBJECT_ARRAY);
}
}
return false;
}
@Override
public String toString() {
String tableName = tableInfo.ident().toString();
String qualifiedName = this.qualifiedName.toString();
// to be able to distinguish tables in a self-joins
if (tableName.equals(qualifiedName)) {
return getClass().getSimpleName() + '{' + tableName + '}';
}
return getClass().getSimpleName() + '{' + tableName + " AS " + qualifiedName + '}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AbstractTableRelation that = (AbstractTableRelation) o;
if (!tableInfo.equals(that.tableInfo)) return false;
if (!qualifiedName.equals(that.qualifiedName)) return false;
return true;
}
@Override
public int hashCode() {
int result = tableInfo.hashCode();
result = 31 * result + qualifiedName.hashCode();
return result;
}
@Override
@Nullable
public Reference resolveField(Field field) {
if (field.relation().equals(this)) {
return allocatedFields.get(field.path());
}
return null;
}
public void validateOrderBy(Optional<OrderBy> orderBy) {
}
}