content: /*
   * 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.common.metadata.MetadataUtil;
  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.base.IAObject;
  import org.apache.asterix.om.functions.BuiltinFunctions;
  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.BuiltinType;
  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.asterix.om.utils.ConstantExpressionUtil;
  import org.apache.commons.lang3.ArrayUtils;
  import org.apache.commons.lang3.mutable.Mutable;
  import org.apache.hyracks.algebricks.common.exceptions.AlgebricksException;
  import org.apache.hyracks.algebricks.common.utils.Pair;
  import org.apache.hyracks.algebricks.common.utils.Triple;
  import org.apache.hyracks.algebricks.core.algebra.base.ILogicalExpression;
  import org.apache.hyracks.algebricks.core.algebra.expressions.AbstractFunctionCallExpression;
  import org.apache.hyracks.algebricks.core.algebra.functions.FunctionIdentifier;
  import org.apache.hyracks.api.exceptions.SourceLocation;
  import org.apache.hyracks.util.LogRedactionUtil;
  
  /**
   * 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$";
  
      public static final String DATETIME_PARAMETER_NAME = BuiltinType.ADATETIME.getTypeName();
  
      public static final String DATE_PARAMETER_NAME = BuiltinType.ADATE.getTypeName();
  
      public static final String TIME_PARAMETER_NAME = BuiltinType.ATIME.getTypeName();
  
      private TypeUtil() {
      }
  
      /**
       * @param type type
       * @param withFormat true when needing a constructor with format
       *
       * @return a type constructor that produces NULL for MISSING input
       */
      public static FunctionIdentifier getTypeConstructorDefaultNull(IAType type, boolean withFormat) {
          switch (type.getTypeTag()) {
              case TINYINT:
                  return BuiltinFunctions.INT8_DEFAULT_NULL_CONSTRUCTOR;
              case SMALLINT:
                  return BuiltinFunctions.INT16_DEFAULT_NULL_CONSTRUCTOR;
              case INTEGER:
                  return BuiltinFunctions.INT32_DEFAULT_NULL_CONSTRUCTOR;
              case BIGINT:
                  return BuiltinFunctions.INT64_DEFAULT_NULL_CONSTRUCTOR;
              case FLOAT:
                  return BuiltinFunctions.FLOAT_DEFAULT_NULL_CONSTRUCTOR;
              case DOUBLE:
                  return BuiltinFunctions.DOUBLE_DEFAULT_NULL_CONSTRUCTOR;
              case BOOLEAN:
                  return BuiltinFunctions.BOOLEAN_DEFAULT_NULL_CONSTRUCTOR;
              case STRING:
                  return BuiltinFunctions.STRING_DEFAULT_NULL_CONSTRUCTOR;
              case DATE:
                  return withFormat ? BuiltinFunctions.DATE_DEFAULT_NULL_CONSTRUCTOR_WITH_FORMAT
                          : BuiltinFunctions.DATE_DEFAULT_NULL_CONSTRUCTOR;
              case TIME:
                  return withFormat ? BuiltinFunctions.TIME_DEFAULT_NULL_CONSTRUCTOR_WITH_FORMAT
                          : BuiltinFunctions.TIME_DEFAULT_NULL_CONSTRUCTOR;
              case DATETIME:
                  return withFormat ? BuiltinFunctions.DATETIME_DEFAULT_NULL_CONSTRUCTOR_WITH_FORMAT
                          : BuiltinFunctions.DATETIME_DEFAULT_NULL_CONSTRUCTOR;
              case YEARMONTHDURATION:
                  return BuiltinFunctions.YEAR_MONTH_DURATION_DEFAULT_NULL_CONSTRUCTOR;
              case DAYTIMEDURATION:
                  return BuiltinFunctions.DAY_TIME_DURATION_DEFAULT_NULL_CONSTRUCTOR;
              case DURATION:
                  return BuiltinFunctions.DURATION_DEFAULT_NULL_CONSTRUCTOR;
              case UUID:
                  return BuiltinFunctions.UUID_DEFAULT_NULL_CONSTRUCTOR;
              case BINARY:
                  return BuiltinFunctions.BINARY_BASE64_DEFAULT_NULL_CONSTRUCTOR;
              default:
                  return null;
          }
      }
  
      public static Map<String, String> validateConfiguration(Map<String, String> config, SourceLocation sourceLoc)
              throws CompilationException {
          if (config == null) {
              return Collections.emptyMap();
          }
          for (Map.Entry<String, String> me : config.entrySet()) {
              String name = me.getKey();
              String value = me.getValue();
              if (DATETIME_PARAMETER_NAME.equals(name) || DATE_PARAMETER_NAME.equals(name)
                      || TIME_PARAMETER_NAME.equals(name)) {
                  if (value == null) {
                      throw new CompilationException(ErrorCode.INVALID_REQ_PARAM_VAL, sourceLoc, name, "null");
                  }
              } else {
                  throw new CompilationException(ErrorCode.ILLEGAL_SET_PARAMETER, sourceLoc, name);
              }
          }
          return config;
      }
  
      public static String getDatetimeFormat(Map<String, String> config) {
          return config.get(DATETIME_PARAMETER_NAME);
      }
  
      public static String getDateFormat(Map<String, String> config) {
          return config.get(DATE_PARAMETER_NAME);
      }
  
      public static String getTimeFormat(Map<String, String> config) {
          return config.get(TIME_PARAMETER_NAME);
      }
  
      public static String getTemporalFormat(IAType targetType, Triple<String, String, String> temporalFormatByType) {
          switch (targetType.getTypeTag()) {
              case DATETIME:
                  return temporalFormatByType.first;
              case DATE:
                  return temporalFormatByType.second;
              case TIME:
                  return temporalFormatByType.third;
              default:
                  return null;
          }
      }
  
      public static IAObject getTemporalFormatArg(AbstractFunctionCallExpression funExpr) {
          FunctionIdentifier funId = funExpr.getFunctionIdentifier();
          if (BuiltinFunctions.DATE_DEFAULT_NULL_CONSTRUCTOR_WITH_FORMAT.equals(funId)
                  || BuiltinFunctions.TIME_DEFAULT_NULL_CONSTRUCTOR_WITH_FORMAT.equals(funId)
                  || BuiltinFunctions.DATETIME_DEFAULT_NULL_CONSTRUCTOR_WITH_FORMAT.equals(funId)) {
              List<Mutable<ILogicalExpression>> arguments = funExpr.getArguments();
              if (arguments.size() > 1) {
                  return ConstantExpressionUtil.getConstantIaObject(arguments.get(1).getValue(), null);
              }
          }
          return null;
      }
  
      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;
          private boolean castDefaultNull;
  
          public void reset(ARecordType baseRecordType, List<String> keyFieldNames, List<Boolean> keyUnnestFlags,
                  IAType keyFieldType, boolean castDefaultNull) {
              this.baseRecordType = baseRecordType;
              this.keyFieldNames = keyFieldNames;
              this.keyUnnestFlags = keyUnnestFlags;
              this.keyFieldType = keyFieldType;
              this.castDefaultNull = castDefaultNull;
          }
  
          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 "
                                          + LogRedactionUtil.userData(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 '"
                              + LogRedactionUtil.userData(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) {
                  // choose the type specified in the DDL over the type in the dataset schema if CAST is used
                  if (castDefaultNull) {
                      recordNameTypesMap.put(keyFieldNames.get(keyFieldNames.size() - 1),
                              AUnionType.createNullableType(nestArrayType(keyFieldType, isKeyTypeWithUnnest)));
                  } else if (!ATypeHierarchy.canPromote(enforcedFieldType.getTypeTag(), this.keyFieldType.getTypeTag())) {
                      throw new AsterixException(ErrorCode.COMPILATION_ERROR,
                              "Cannot enforce field '" + LogRedactionUtil.userData(String.join(".", this.keyFieldNames))
                                      + "' to have type " + this.keyFieldType);
                  }
              } else {
                  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);
                  typeIntermediate = TypeComputeUtils.getActualType(typeIntermediate);
                  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),
                      valueIndexDetails.getCastDefaultNull().getOrElse(false));
              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), false);
              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), false);
                  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 '"
                      + LogRedactionUtil.userData(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, String typeDatabaseName,
              DataverseName typeDataverseName, String typeName) {
          return isInlineTypeName(dataset.getDatabaseName(), dataset.getDataverseName(), typeDatabaseName,
                  typeDataverseName, typeName, DATASET_INLINE_TYPE_PREFIX);
      }
  
      private static boolean isInlineTypeName(String entityDatabaseName, DataverseName entityDataverseName,
              String typeDatabaseName, DataverseName typeDataverseName, String typeName, String inlineTypePrefix) {
          return entityDatabaseName.equals(typeDatabaseName) && 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, String typeDatabaseName,
              DataverseName typeDataverseName, String typeName) {
          return isInlineTypeName(function.getDatabaseName(), function.getDataverseName(), typeDatabaseName,
                  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.getDatabaseName(),
                  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.getDatabaseName(),
                          parameterType.getDataverseName(), parameterType.getName())) {
                      if (inlineTypes.isEmpty()) {
                          inlineTypes = new ArrayList<>();
                      }
                      inlineTypes.add(parameterType);
                  }
              }
          }
          return inlineTypes;
      }
  }
  