blob: fd51433aac8d9eb4b9560c95e15562d79ad0338f [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.om.types;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.asterix.common.annotations.IRecordTypeAnnotation;
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.om.base.IAObject;
import org.apache.asterix.om.utils.NonTaggedFormatUtil;
import org.apache.hyracks.algebricks.common.exceptions.AlgebricksException;
import org.apache.hyracks.api.exceptions.HyracksDataException;
import org.apache.hyracks.api.io.IJsonSerializable;
import org.apache.hyracks.api.io.IPersistedResourceRegistry;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
/**
* ARecordType is read-only and shared by different partitions at runtime.
* Note: to check whether a field name is defined in the closed part at runtime,
* please use RuntimeRecordTypeInfo which separates the mutable states
* from ARecordType and has to be one-per-partition.
*/
public class ARecordType extends AbstractComplexType {
private static final long serialVersionUID = 1L;
private static final JavaType SET = OBJECT_MAPPER.getTypeFactory().constructCollectionType(Set.class, String.class);
private static final String IS_OPEN = "isOpen";
private static final String FIELD_NAMES = "fieldNames";
private static final String FIELD_TYPES = "fieldTypes";
private static final String ADDITIONAL_FIELDS = "additionalFieldNames";
private final String[] fieldNames;
private final IAType[] fieldTypes;
private final Map<String, Integer> fieldNameToIndexMap = new HashMap<>();
private final boolean isOpen;
private final transient List<IRecordTypeAnnotation> annotations = new ArrayList<>();
// The following set of names do not belong to the closed part,
// but are additional possible field names. For example, a field "foo" with type
// ANY cannot be in the closed part, but "foo" is a possible field name.
// This is used for resolve a field access with prefix path missing.
// If allPossibleAdditionalFieldNames is null, that means compiler does not know
// the bounded set of all possible additional field names.
private final Set<String> allPossibleAdditionalFieldNames;
/**
* @param typeName the name of the type
* @param fieldNames the names of the closed fields
* @param fieldTypes the types of the closed fields
* @param isOpen whether the record is open
*/
public ARecordType(String typeName, String[] fieldNames, IAType[] fieldTypes, boolean isOpen) {
this(typeName, fieldNames, fieldTypes, isOpen, null);
}
/**
* @param typeName the name of the type
* @param fieldNames the names of the closed fields
* @param fieldTypes the types of the closed fields
* @param isOpen whether the record is open
* @param allPossibleAdditionalFieldNames, all possible additional field names.
*/
public ARecordType(String typeName, String[] fieldNames, IAType[] fieldTypes, boolean isOpen,
Set<String> allPossibleAdditionalFieldNames) {
super(typeName);
this.fieldNames = fieldNames;
this.fieldTypes = fieldTypes;
this.isOpen = isOpen;
// Puts field names to the field name to field index map.
for (int index = 0; index < fieldNames.length; ++index) {
fieldNameToIndexMap.put(fieldNames[index], index);
}
this.allPossibleAdditionalFieldNames = allPossibleAdditionalFieldNames;
}
public boolean canContainField(String fieldName) {
if (this.isOpen && allPossibleAdditionalFieldNames == null) {
// An open record type without information on possible additional fields can potentially contain
// a field with any name.
return true;
}
if (isOpen) {
// An open record type with information on possible additional fields can determine whether
// a field can potentially be contained in a record.
return fieldNameToIndexMap.containsKey(fieldName) || allPossibleAdditionalFieldNames.contains(fieldName);
} else {
return fieldNameToIndexMap.containsKey(fieldName);
}
}
public boolean knowsAllPossibleAdditonalFieldNames() {
return allPossibleAdditionalFieldNames != null;
}
public Set<String> getAllPossibleAdditonalFieldNames() {
return allPossibleAdditionalFieldNames;
}
public String[] getFieldNames() {
return fieldNames;
}
public IAType[] getFieldTypes() {
return fieldTypes;
}
public List<IRecordTypeAnnotation> getAnnotations() {
return annotations;
}
public IRecordTypeAnnotation findAnnotation(IRecordTypeAnnotation.Kind kind) {
if (annotations != null) {
for (IRecordTypeAnnotation ant : annotations) {
if (ant.getKind().equals(kind)) {
return ant;
}
}
}
return null;
}
@Override
public String toString() {
return append(new StringBuilder()).toString();
}
private StringBuilder append(StringBuilder sb) {
if (typeName != null) {
sb.append(typeName).append(": ");
}
sb.append(isOpen ? "open" : "closed");
sb.append(" {\n");
int n = fieldNames.length;
for (int i = 0; i < n; i++) {
sb.append(" ").append(fieldNames[i]).append(": ").append(fieldTypes[i]);
sb.append(i < (n - 1) ? ",\n" : "\n");
}
return sb.append("}\n");
}
@Override
public ATypeTag getTypeTag() {
return ATypeTag.OBJECT;
}
public boolean isOpen() {
return isOpen;
}
/**
* Returns the position of the field in the closed schema or -1 if the field does not exist.
*
* @param fieldName the name of the field whose position is sought
* @return the position of the field in the closed schema or -1 if the field does not exist.
*/
public int getFieldIndex(String fieldName) {
if (fieldNames == null) {
return -1;
}
Integer index = fieldNameToIndexMap.get(fieldName);
if (index == null) {
return -1;
} else {
return index;
}
}
/**
* @param subFieldName The full pathname of the child
* @param parent The type of the parent
* @return the type of the child
*/
public IAType getSubFieldType(List<String> subFieldName, IAType parent) {
ARecordType subRecordType = (ARecordType) parent;
for (int i = 0; i < (subFieldName.size() - 1); i++) {
subRecordType = (ARecordType) subRecordType.getFieldType(subFieldName.get(i));
}
return subRecordType.getFieldType(subFieldName.get(subFieldName.size() - 1));
}
/**
* @param subFieldName The full pathname of the child
* @return the type of the child
* @throws AsterixException
*/
public IAType getSubFieldType(List<String> subFieldName) throws AlgebricksException {
IAType subRecordType = getFieldType(subFieldName.get(0));
for (int i = 1; i < subFieldName.size(); i++) {
if (subRecordType == null) {
return null;
}
if (subRecordType.getTypeTag().equals(ATypeTag.UNION)) {
//enforced SubType
subRecordType = ((AUnionType) subRecordType).getActualType();
if (subRecordType.getTypeTag() != ATypeTag.OBJECT) {
throw new AsterixException(
"Field accessor is not defined for values of type " + subRecordType.getTypeTag());
}
}
subRecordType = ((ARecordType) subRecordType).getFieldType(subFieldName.get(i));
}
return subRecordType;
}
/**
* @param subFieldName The full pathname of the field
* @return The nullability of the field
* @throws AlgebricksException
*/
public boolean isSubFieldNullable(List<String> subFieldName) throws AlgebricksException {
IAType subRecordType = getFieldType(subFieldName.get(0));
for (int i = 1; i < subFieldName.size(); i++) {
if (subRecordType == null) {
// open field is nullable
return true;
}
if (subRecordType.getTypeTag().equals(ATypeTag.UNION)) {
if (NonTaggedFormatUtil.isOptional(subRecordType)) {
return true;
}
subRecordType = ((AUnionType) subRecordType).getActualType();
if (subRecordType.getTypeTag() != ATypeTag.OBJECT) {
throw new AsterixException(
"Field accessor is not defined for values of type " + subRecordType.getTypeTag());
}
}
if (!(subRecordType instanceof ARecordType)) {
throw CompilationException.create(ErrorCode.COMPILATION_ILLEGAL_STATE,
"Illegal field type " + subRecordType.getTypeTag() + " when checking field nullability");
}
subRecordType = ((ARecordType) subRecordType).getFieldType(subFieldName.get(i));
}
return subRecordType == null || NonTaggedFormatUtil.isOptional(subRecordType);
}
/**
* Returns the field type of the field name if it exists, otherwise null.
*
* @param fieldName the fieldName whose type is sought
* @return the field type of the field name if it exists, otherwise null
* NOTE: this method doesn't work for nested fields
*/
public IAType getFieldType(String fieldName) {
int fieldPos = getFieldIndex(fieldName);
if ((fieldPos < 0) || (fieldPos >= fieldTypes.length)) {
return null;
}
return fieldTypes[fieldPos];
}
/**
* Returns true or false indicating whether or not a field is closed.
*
* @param fieldName the name of the field to check
* @return true if fieldName is a closed field, otherwise false
*/
public boolean isClosedField(String fieldName) {
return getFieldIndex(fieldName) != -1;
}
public boolean doesFieldExist(String fieldName) {
for (String f : fieldNames) {
if (f.compareTo(fieldName) == 0) {
return true;
}
}
return false;
}
public ARecordType deepCopy(ARecordType type) {
IAType[] newTypes = new IAType[type.fieldNames.length];
for (int i = 0; i < type.fieldTypes.length; i++) {
if (type.fieldTypes[i].getTypeTag() == ATypeTag.OBJECT) {
newTypes[i] = deepCopy((ARecordType) type.fieldTypes[i]);
} else {
newTypes[i] = type.fieldTypes[i];
}
}
Set<String> newAllPossibleAdditionalFieldNames =
allPossibleAdditionalFieldNames != null ? new HashSet<>(allPossibleAdditionalFieldNames) : null;
return new ARecordType(type.typeName, type.fieldNames, newTypes, type.isOpen,
newAllPossibleAdditionalFieldNames);
}
@Override
public String getDisplayName() {
return "object";
}
@Override
public IAType getType() {
return BuiltinType.ALL_TYPE;
}
@Override
public void generateNestedDerivedTypeNames() {
for (int i = 0; i < fieldTypes.length; i++) {
IAType fieldType = fieldTypes[i];
if (fieldType.getTypeTag().isDerivedType() && (fieldType.getTypeName() == null)) {
AbstractComplexType nestedType = (AbstractComplexType) fieldType;
nestedType.setTypeName(getTypeName() + "_" + fieldNames[i]);
nestedType.generateNestedDerivedTypeNames();
}
}
}
@Override
public boolean deepEqual(IAObject obj) {
if (!(obj instanceof ARecordType)) {
return false;
}
ARecordType rt = (ARecordType) obj;
return (isOpen == rt.isOpen) && Arrays.deepEquals(fieldNames, rt.fieldNames)
&& Arrays.deepEquals(fieldTypes, rt.fieldTypes);
}
@Override
public int hash() {
int h = 0;
for (int i = 0; i < fieldNames.length; i++) {
h += (31 * h) + fieldNames[i].hashCode();
}
for (int i = 0; i < fieldTypes.length; i++) {
h += (31 * h) + fieldTypes[i].hashCode();
}
return h;
}
@Override
public ObjectNode toJSON() {
ObjectMapper om = new ObjectMapper();
ObjectNode type = om.createObjectNode();
type.put("type", ARecordType.class.getName());
type.put("name", typeName);
type.put("open", isOpen);
ArrayNode fields = om.createArrayNode();
for (int i = 0; i < fieldNames.length; i++) {
ObjectNode field = om.createObjectNode();
field.set(fieldNames[i], fieldTypes[i].toJSON());
fields.add(field);
}
type.set("fields", fields);
return type;
}
@Override
public JsonNode toJson(IPersistedResourceRegistry registry) throws HyracksDataException {
final ObjectNode jsonObject = registry.getClassIdentifier(getClass(), serialVersionUID);
addToJson(jsonObject);
jsonObject.put(IS_OPEN, isOpen);
jsonObject.putPOJO(FIELD_NAMES, fieldNames);
jsonObject.putPOJO(ADDITIONAL_FIELDS, allPossibleAdditionalFieldNames);
ArrayNode fieldTypesArray = OBJECT_MAPPER.createArrayNode();
for (int i = 0; i < fieldTypes.length; i++) {
fieldTypesArray.add(fieldTypes[i].toJson(registry));
}
jsonObject.set(FIELD_TYPES, fieldTypesArray);
return jsonObject;
}
public static IJsonSerializable fromJson(IPersistedResourceRegistry registry, JsonNode json)
throws HyracksDataException {
String typeName = json.get(TYPE_NAME_FIELD).asText();
boolean isOpen = json.get(IS_OPEN).asBoolean();
String[] fieldNames = OBJECT_MAPPER.convertValue(json.get(FIELD_NAMES), String[].class);
Set<String> additionalFields = OBJECT_MAPPER.convertValue(json.get(ADDITIONAL_FIELDS), SET);
ArrayNode fieldTypesNode = (ArrayNode) json.get(FIELD_TYPES);
IAType[] fieldTypes = new IAType[fieldTypesNode.size()];
for (int i = 0; i < fieldTypesNode.size(); i++) {
fieldTypes[i] = (IAType) registry.deserialize(fieldTypesNode.get(i));
}
return new ARecordType(typeName, fieldNames, fieldTypes, isOpen, additionalFields);
}
public List<IAType> getFieldTypes(List<List<String>> fields) throws AlgebricksException {
List<IAType> typeList = new ArrayList<>();
for (List<String> field : fields) {
typeList.add(getSubFieldType(field));
}
return typeList;
}
@Override
public boolean containsType(IAType type) {
for (IAType aType : fieldTypes) {
if (aType.getTypeName().equals(type.getTypeName())) {
return true;
}
}
return false;
}
@Override
public <R, T> R accept(IATypeVisitor<R, T> visitor, T arg) {
return visitor.visit(this, arg);
}
}