blob: 8a9236e562594cc7c4ee097e5dabd932639ecd0d [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.asterix.metadata.utils;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.asterix.common.exceptions.AsterixException;
import org.apache.asterix.common.exceptions.CompilationException;
import org.apache.asterix.common.exceptions.ErrorCode;
import org.apache.asterix.common.metadata.DataverseName;
import org.apache.asterix.metadata.entities.Dataset;
import org.apache.asterix.metadata.entities.Function;
import org.apache.asterix.metadata.entities.Index;
import org.apache.asterix.om.typecomputer.impl.TypeComputeUtils;
import org.apache.asterix.om.types.AOrderedListType;
import org.apache.asterix.om.types.ARecordType;
import org.apache.asterix.om.types.ATypeTag;
import org.apache.asterix.om.types.AUnionType;
import org.apache.asterix.om.types.IAType;
import org.apache.asterix.om.types.TypeSignature;
import org.apache.asterix.om.types.hierachy.ATypeHierarchy;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.hyracks.algebricks.common.exceptions.AlgebricksException;
import org.apache.hyracks.algebricks.common.utils.Pair;
import org.apache.hyracks.algebricks.common.utils.Triple;
/**
* Provider utility methods for data types
*/
public class TypeUtil {
private static final char TYPE_NAME_DELIMITER = '$';
private static final String DATASET_INLINE_TYPE_PREFIX = "$d$t$";
private static final String FUNCTION_INLINE_TYPE_PREFIX = "$f$t$";
private TypeUtil() {
}
private static class EnforcedTypeBuilder {
private final Deque<Triple<IAType, String, Boolean>> typeStack = new ArrayDeque<>();
private List<Boolean> keyUnnestFlags;
private List<String> keyFieldNames;
private ARecordType baseRecordType;
private IAType keyFieldType;
// Output from nested-type-stack construction.
private String bridgeNameFoundFromOpenTypeBuild;
private IAType endOfOpenTypeBuild;
private int indexOfOpenPart;
public void reset(ARecordType baseRecordType, List<String> keyFieldNames, List<Boolean> keyUnnestFlags,
IAType keyFieldType) {
this.baseRecordType = baseRecordType;
this.keyFieldNames = keyFieldNames;
this.keyUnnestFlags = keyUnnestFlags;
this.keyFieldType = keyFieldType;
}
public ARecordType build() throws AlgebricksException {
boolean isOpen = constructNestedTypeStack();
IAType newTypeToAdd = (isOpen) ? buildNewForOpenType() : buildNewForFullyClosedType();
return buildRestOfRecord(newTypeToAdd);
}
private boolean constructNestedTypeStack() throws AlgebricksException {
IAType typeIntermediate = baseRecordType;
List<String> subFieldName = new ArrayList<>();
for (int i = 0; i < keyFieldNames.size() - 1; i++) {
typeStack.push(
new Triple<>(typeIntermediate, keyFieldNames.get(i), i != 0 && keyUnnestFlags.get(i - 1)));
bridgeNameFoundFromOpenTypeBuild = typeIntermediate.getTypeName();
if (i == 0 || !keyUnnestFlags.get(i - 1)) {
subFieldName.add(keyFieldNames.get(i));
} else {
// We have a multi-valued intermediate. Perform our UNNEST then add our field name.
typeIntermediate = TypeComputeUtils.extractListItemType(typeIntermediate);
if (typeIntermediate == null) {
String fName = String.join(".", subFieldName);
throw new AsterixException(ErrorCode.COMPILATION_ERROR,
"No list item type found. Wrong type given from field " + fName);
}
subFieldName.add(keyFieldNames.get(i));
}
// Attempt to resolve the type of our working subfield.
typeIntermediate = TypeComputeUtils.getActualType(typeIntermediate);
typeIntermediate =
((ARecordType) typeIntermediate).getSubFieldType(subFieldName.subList(i, subFieldName.size()));
if (typeIntermediate == null) {
endOfOpenTypeBuild = null;
indexOfOpenPart = i;
return true;
}
ATypeTag tt = TypeComputeUtils.getActualType(typeIntermediate).getTypeTag();
if (tt != ATypeTag.OBJECT && tt != ATypeTag.ARRAY && tt != ATypeTag.MULTISET) {
String fName = String.join(".", subFieldName);
throw new AsterixException(ErrorCode.COMPILATION_ERROR,
"Field accessor is not defined for \"" + fName + "\" of type " + tt);
}
}
endOfOpenTypeBuild = typeIntermediate;
indexOfOpenPart = keyFieldNames.size() - 1;
return false;
}
private IAType buildNewForOpenType() {
// Walk backwards through our flags and construct the desired type.
List<Boolean> unnestFlagsForOpenType = keyUnnestFlags.subList(indexOfOpenPart, keyUnnestFlags.size());
List<String> fieldNamesForOpenType = keyFieldNames.subList(indexOfOpenPart, keyFieldNames.size());
IAType resultant = keyFieldType;
for (int i = unnestFlagsForOpenType.size() - 1; i >= 0; i--) {
// Construct the type name.
StringBuilder recordTypeNameBuilder = new StringBuilder();
recordTypeNameBuilder.append(baseRecordType.getTypeName());
for (int j = 0; j < i + indexOfOpenPart; j++) {
recordTypeNameBuilder.append("_").append(keyFieldNames.get(j));
if (keyUnnestFlags.get(j)) {
recordTypeNameBuilder.append("_Item");
}
}
// Construct the type itself and account for any array steps.
resultant = nestArrayType(resultant, unnestFlagsForOpenType.get(i));
resultant =
new ARecordType(recordTypeNameBuilder.toString(), new String[] { fieldNamesForOpenType.get(i) },
new IAType[] { AUnionType.createUnknownableType(resultant) }, true);
}
// Now update the parent to include this optional field, accounting for intermediate arrays.
Triple<IAType, String, Boolean> gapTriple = this.typeStack.pop();
ARecordType parentRecord =
(ARecordType) unnestArrayType(TypeComputeUtils.getActualType(gapTriple.first), gapTriple.third);
IAType[] parentFieldTypes = ArrayUtils.addAll(parentRecord.getFieldTypes().clone(),
((ARecordType) resultant).getFieldTypes().clone());
resultant = new ARecordType(bridgeNameFoundFromOpenTypeBuild,
ArrayUtils.addAll(parentRecord.getFieldNames(), gapTriple.second), parentFieldTypes, true);
resultant = keepUnknown(gapTriple.first, nestArrayType(resultant, gapTriple.third));
return resultant;
}
private IAType buildNewForFullyClosedType() throws AsterixException {
// The schema is closed all the way to the field itself.
IAType typeIntermediate = TypeComputeUtils.getActualType(endOfOpenTypeBuild);
boolean isOpenTypeWithUnnest = indexOfOpenPart != 0 && keyUnnestFlags.get(indexOfOpenPart - 1);
boolean isKeyTypeWithUnnest = keyUnnestFlags.get(indexOfOpenPart);
ARecordType lastNestedRecord = (ARecordType) unnestArrayType(typeIntermediate, isOpenTypeWithUnnest);
Map<String, IAType> recordNameTypesMap = createRecordNameTypeMap(lastNestedRecord);
// If an enforced field already exists, verify that the type is correct.
IAType enforcedFieldType = recordNameTypesMap.get(keyFieldNames.get(keyFieldNames.size() - 1));
if (enforcedFieldType != null && enforcedFieldType.getTypeTag() == ATypeTag.UNION
&& ((AUnionType) enforcedFieldType).isUnknownableType()) {
enforcedFieldType = ((AUnionType) enforcedFieldType).getActualType();
}
if (enforcedFieldType != null
&& !ATypeHierarchy.canPromote(enforcedFieldType.getTypeTag(), this.keyFieldType.getTypeTag())) {
throw new AsterixException(ErrorCode.COMPILATION_ERROR, "Cannot enforce field \""
+ String.join(".", this.keyFieldNames) + "\" to have type " + this.keyFieldType);
}
if (enforcedFieldType == null) {
recordNameTypesMap.put(keyFieldNames.get(keyFieldNames.size() - 1),
AUnionType.createUnknownableType(nestArrayType(keyFieldType, isKeyTypeWithUnnest)));
}
// Build the nested record, and account for the wrapping array.
IAType resultant = nestArrayType(
new ARecordType(lastNestedRecord.getTypeName(), recordNameTypesMap.keySet().toArray(new String[0]),
recordNameTypesMap.values().toArray(new IAType[0]), lastNestedRecord.isOpen()),
isOpenTypeWithUnnest);
return keepUnknown(endOfOpenTypeBuild, resultant);
}
private ARecordType buildRestOfRecord(IAType newTypeToAdd) {
IAType resultant = TypeComputeUtils.getActualType(newTypeToAdd);
while (!typeStack.isEmpty()) {
Triple<IAType, String, Boolean> typeFromStack = typeStack.pop();
IAType typeIntermediate = unnestArrayType(typeFromStack.first, typeFromStack.third);
ARecordType recordType = (ARecordType) typeIntermediate;
IAType[] fieldTypes = recordType.getFieldTypes().clone();
fieldTypes[recordType.getFieldIndex(typeFromStack.second)] = resultant;
typeIntermediate = nestArrayType(new ARecordType(recordType.getTypeName() + "_enforced",
recordType.getFieldNames(), fieldTypes, recordType.isOpen()), typeFromStack.third);
resultant = keepUnknown(typeFromStack.first, typeIntermediate);
}
return (ARecordType) resultant;
}
private static Map<String, IAType> createRecordNameTypeMap(ARecordType recordType) {
LinkedHashMap<String, IAType> recordNameTypesMap = new LinkedHashMap<>();
for (int j = 0; j < recordType.getFieldNames().length; j++) {
recordNameTypesMap.put(recordType.getFieldNames()[j], recordType.getFieldTypes()[j]);
}
return recordNameTypesMap;
}
private static IAType keepUnknown(IAType originalRecordType, IAType updatedRecordType) {
if (originalRecordType.getTypeTag() == ATypeTag.UNION) {
return AUnionType.createUnknownableType(updatedRecordType, updatedRecordType.getTypeName());
}
return updatedRecordType;
}
private static IAType nestArrayType(IAType originalType, boolean isWithinArray) {
if (isWithinArray) {
String newTypeName = originalType.getTypeName().endsWith("_Item")
? originalType.getTypeName().substring(0, originalType.getTypeName().length() - 5)
: originalType.getTypeName();
return new AOrderedListType(originalType, newTypeName + "_Array");
} else {
return originalType;
}
}
private static IAType unnestArrayType(IAType originalType, boolean isWithinArray) {
IAType resultant = originalType;
if (isWithinArray) {
resultant = TypeComputeUtils.extractListItemType(resultant);
if (resultant != null) {
resultant = TypeComputeUtils.getActualType(resultant);
}
}
return resultant;
}
}
/**
* Merges typed index fields with specified recordType, allowing indexed fields to be optional.
* I.e. the type { "personId":int32, "name": string, "address" : { "street": string } } with typed indexes
* on age:int32, address.state:string will be merged into type { "personId":int32, "name": string,
* "age": int32? "address" : { "street": string, "state": string? } } Used by open indexes to enforce
* the type of an indexed record
*/
public static Pair<ARecordType, ARecordType> createEnforcedType(ARecordType recordType, ARecordType metaType,
List<Index> indexes) throws AlgebricksException {
EnforcedTypeBuilder enforcedTypeBuilder = new EnforcedTypeBuilder();
ARecordType enforcedRecordType = recordType;
for (Index index : indexes) {
if (!index.isSecondaryIndex() || !index.getIndexDetails().isOverridingKeyFieldTypes()) {
continue;
}
switch (Index.IndexCategory.of(index.getIndexType())) {
case VALUE:
enforcedRecordType = appendValueIndexType(enforcedRecordType,
(Index.ValueIndexDetails) index.getIndexDetails(), enforcedTypeBuilder);
break;
case TEXT:
enforcedRecordType = appendTextIndexType(enforcedRecordType,
(Index.TextIndexDetails) index.getIndexDetails(), enforcedTypeBuilder);
break;
case ARRAY:
enforcedRecordType = appendArrayIndexTypes(enforcedRecordType,
(Index.ArrayIndexDetails) index.getIndexDetails(), enforcedTypeBuilder);
break;
default:
throw new CompilationException(ErrorCode.COMPILATION_UNKNOWN_INDEX_TYPE,
String.valueOf(index.getIndexType()));
}
}
validateRecord(enforcedRecordType);
return new Pair<>(enforcedRecordType, metaType);
}
private static ARecordType appendValueIndexType(ARecordType enforcedRecordType,
Index.ValueIndexDetails valueIndexDetails, EnforcedTypeBuilder enforcedTypeBuilder)
throws AlgebricksException {
List<List<String>> keyFieldNames = valueIndexDetails.getKeyFieldNames();
List<IAType> keyFieldTypes = valueIndexDetails.getKeyFieldTypes();
List<Integer> keySources = valueIndexDetails.getKeyFieldSourceIndicators();
for (int i = 0; i < keyFieldNames.size(); i++) {
if (keySources.get(i) != Index.RECORD_INDICATOR) {
throw new CompilationException(ErrorCode.COMPILATION_ERROR,
"Indexing an open field is only supported on the record part");
}
enforcedTypeBuilder.reset(enforcedRecordType, keyFieldNames.get(i),
Collections.nCopies(keyFieldNames.get(i).size(), false), keyFieldTypes.get(i));
validateRecord(enforcedRecordType);
enforcedRecordType = enforcedTypeBuilder.build();
}
return enforcedRecordType;
}
private static ARecordType appendTextIndexType(ARecordType enforcedRecordType,
Index.TextIndexDetails textIndexDetails, EnforcedTypeBuilder enforcedTypeBuilder)
throws AlgebricksException {
List<List<String>> keyFieldNames = textIndexDetails.getKeyFieldNames();
List<IAType> keyFieldTypes = textIndexDetails.getKeyFieldTypes();
List<Integer> keySources = textIndexDetails.getKeyFieldSourceIndicators();
for (int i = 0; i < keyFieldNames.size(); i++) {
if (keySources.get(i) != Index.RECORD_INDICATOR) {
throw new CompilationException(ErrorCode.COMPILATION_ERROR,
"Indexing an open field is only supported on the record part");
}
enforcedTypeBuilder.reset(enforcedRecordType, keyFieldNames.get(i),
Collections.nCopies(keyFieldNames.get(i).size(), false), keyFieldTypes.get(i));
validateRecord(enforcedRecordType);
enforcedRecordType = enforcedTypeBuilder.build();
}
return enforcedRecordType;
}
private static ARecordType appendArrayIndexTypes(ARecordType enforcedRecordType,
Index.ArrayIndexDetails arrayIndexDetails, EnforcedTypeBuilder enforcedTypeBuilder)
throws AlgebricksException {
for (Index.ArrayIndexElement e : arrayIndexDetails.getElementList()) {
if (e.getSourceIndicator() != Index.RECORD_INDICATOR) {
throw new CompilationException(ErrorCode.COMPILATION_ERROR,
"Indexing an open field is only supported on the record part");
}
List<List<String>> unnestList = e.getUnnestList();
List<List<String>> projectList = e.getProjectList();
List<IAType> typeList = e.getTypeList();
for (int i = 0; i < projectList.size(); i++) {
List<String> project = projectList.get(i);
enforcedTypeBuilder.reset(enforcedRecordType,
ArrayIndexUtil.getFlattenedKeyFieldNames(unnestList, project),
ArrayIndexUtil.getUnnestFlags(unnestList, project), typeList.get(i));
validateRecord(enforcedRecordType);
enforcedRecordType = enforcedTypeBuilder.build();
}
}
return enforcedRecordType;
}
/**
* Creates a map from name to type for fields in the passed type
*
* @param recordType the type to be mapped
* @return a map mapping all fields to their types
*/
private static Map<String, IAType> createRecordNameTypeMap(ARecordType recordType) {
LinkedHashMap<String, IAType> recordNameTypesMap = new LinkedHashMap<>();
for (int j = 0; j < recordType.getFieldNames().length; j++) {
recordNameTypesMap.put(recordType.getFieldNames()[j], recordType.getFieldTypes()[j]);
}
return recordNameTypesMap;
}
/**
* Maintains the {@code updatedRecordType} as nullable/missable (?) in case it was originally nullable/missable
* @param originalRecordType the original record type
* @param updatedRecordType the original record type being enforced/modified with new non-declared fields included
* @return {@code updatedRecordType}
*/
private static IAType keepUnknown(IAType originalRecordType, ARecordType updatedRecordType) {
if (originalRecordType.getTypeTag() == ATypeTag.UNION) {
return AUnionType.createUnknownableType(updatedRecordType, updatedRecordType.getTypeName());
}
return updatedRecordType;
}
/**
* Makes sure the dataset record type being enforced/modified stays as a pure record type
* @param enforcedDatasetRecordType the dataset record type enforced and modified by adding the extra fields indexed
*/
private static void validateRecord(IAType enforcedDatasetRecordType) {
if (enforcedDatasetRecordType.getTypeTag() != ATypeTag.OBJECT) {
throw new IllegalStateException("The dataset type must be a record type to be able to build an index");
}
}
/**
* Makes sure the chain of fields accessed and leading to the indexed field are all valid record types.
* E.g. for CREATE INDEX idx on ds(a.b.c.d: int) validate that a, b and c are all valid record types (?).
* @param nestedRecordType the nested record field being accessed
* @param fieldName the name of the nested record field
* @throws AsterixException when supplying bad fields, e.g. CREATE INDEX i on ds(a.b: int, a.b.c: int) (mostly
* for non-declared fields)
*/
private static void validateNestedRecord(IAType nestedRecordType, List<String> fieldName) throws AsterixException {
IAType actualType = TypeComputeUtils.getActualType(nestedRecordType);
if (actualType.getTypeTag() != ATypeTag.OBJECT) {
String fName = String.join(".", fieldName);
throw new AsterixException(ErrorCode.COMPILATION_ERROR,
"Field accessor is not defined for \"" + fName + "\" of type " + actualType.getTypeTag());
}
}
/**
* Warning: this is not a general-purpose method.
* Use only when processing types stored in metadata.
* Doesn't properly handle ANY, UNION, NULL and MISSING types.
* Allows {@code null} type reference which will be filled later during type translation.
*/
public static IAType createQuantifiedType(IAType primeType, boolean nullable, boolean missable) {
if (primeType != null) {
switch (primeType.getTypeTag()) {
case ANY:
case UNION:
case NULL:
case MISSING:
throw new IllegalArgumentException(primeType.getDisplayName());
}
}
IAType resType = primeType;
if (nullable && missable) {
resType = AUnionType.createUnknownableType(resType);
} else if (nullable) {
resType = AUnionType.createNullableType(resType);
} else if (missable) {
resType = AUnionType.createMissableType(resType);
}
return resType;
}
public static boolean isReservedInlineTypeName(String typeName) {
return typeName.length() > 0 && typeName.charAt(0) == TYPE_NAME_DELIMITER;
}
public static String createDatasetInlineTypeName(String datasetName, boolean forMetaItemType) {
char typeChar = forMetaItemType ? 'm' : 'i';
return DATASET_INLINE_TYPE_PREFIX + typeChar + TYPE_NAME_DELIMITER + datasetName;
}
public static boolean isDatasetInlineTypeName(Dataset dataset, DataverseName typeDataverseName, String typeName) {
return isInlineTypeName(dataset.getDataverseName(), typeDataverseName, typeName, DATASET_INLINE_TYPE_PREFIX);
}
private static boolean isInlineTypeName(DataverseName entityDataverseName, DataverseName typeDataverseName,
String typeName, String inlineTypePrefix) {
return entityDataverseName.equals(typeDataverseName) && typeName.startsWith(inlineTypePrefix);
}
public static String createFunctionParameterTypeName(String functionName, int arity, int parameterIndex) {
StringBuilder sb = new StringBuilder(FUNCTION_INLINE_TYPE_PREFIX.length() + functionName.length() + 8);
sb.append(FUNCTION_INLINE_TYPE_PREFIX).append(functionName).append(TYPE_NAME_DELIMITER).append(arity);
if (parameterIndex >= 0) {
sb.append(TYPE_NAME_DELIMITER).append(parameterIndex);
} else if (parameterIndex != -1) {
throw new IllegalArgumentException(String.valueOf(parameterIndex));
} // otherwise it's a return type
return sb.toString();
}
public static boolean isFunctionInlineTypeName(Function function, DataverseName typeDataverseName,
String typeName) {
return isInlineTypeName(function.getDataverseName(), typeDataverseName, typeName, FUNCTION_INLINE_TYPE_PREFIX);
}
public static String getFullyQualifiedDisplayName(DataverseName dataverseName, String typeName) {
return MetadataUtil.getFullyQualifiedDisplayName(dataverseName, typeName);
}
/**
* Inline type names are unique within a function, so we don't need to perform duplicate elimination
*/
public static List<TypeSignature> getFunctionInlineTypes(Function function) {
List<TypeSignature> inlineTypes = Collections.emptyList();
TypeSignature returnType = function.getReturnType();
if (returnType != null
&& isFunctionInlineTypeName(function, returnType.getDataverseName(), returnType.getName())) {
inlineTypes = new ArrayList<>();
inlineTypes.add(returnType);
}
List<TypeSignature> parameterTypes = function.getParameterTypes();
if (parameterTypes != null) {
for (TypeSignature parameterType : parameterTypes) {
if (parameterType != null && isFunctionInlineTypeName(function, parameterType.getDataverseName(),
parameterType.getName())) {
if (inlineTypes.isEmpty()) {
inlineTypes = new ArrayList<>();
}
inlineTypes.add(parameterType);
}
}
}
return inlineTypes;
}
}