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.calcite.sql.type;
  
  import org.apache.calcite.rel.type.RelDataType;
  import org.apache.calcite.rel.type.RelDataTypeFactory;
  import org.apache.calcite.rel.type.RelDataTypeFactoryImpl;
  import org.apache.calcite.rel.type.RelDataTypeFamily;
  import org.apache.calcite.rel.type.RelDataTypeSystem;
  import org.apache.calcite.sql.SqlCollation;
  import org.apache.calcite.sql.SqlIntervalQualifier;
  import org.apache.calcite.util.Util;
  
  import org.checkerframework.checker.nullness.qual.Nullable;
  
  import java.nio.charset.Charset;
  import java.util.List;
  
  import static com.google.common.base.Preconditions.checkArgument;
  
  import static org.apache.calcite.util.Static.RESOURCE;
  
  import static java.util.Objects.requireNonNull;
  
  /**
   * SqlTypeFactoryImpl provides a default implementation of
   * {@link RelDataTypeFactory} which supports SQL types.
   */
  public class SqlTypeFactoryImpl extends RelDataTypeFactoryImpl {
    //~ Constructors -----------------------------------------------------------
  
    public SqlTypeFactoryImpl(RelDataTypeSystem typeSystem) {
      super(typeSystem);
    }
  
    //~ Methods ----------------------------------------------------------------
  
    @Override public RelDataType createSqlType(SqlTypeName typeName) {
      if (typeName.allowsPrec()) {
        return createSqlType(typeName, typeSystem.getDefaultPrecision(typeName));
      }
      assertBasic(typeName);
      RelDataType newType = new BasicSqlType(typeSystem, typeName);
      return canonize(newType);
    }
  
    @Override public RelDataType createSqlType(
        SqlTypeName typeName,
        int precision) {
      if (typeName.allowsScale()) {
        return createSqlType(typeName, precision, typeSystem.getDefaultScale(typeName));
      }
      final int maxPrecision = typeSystem.getMaxPrecision(typeName);
      if (maxPrecision >= 0 && precision > maxPrecision) {
        precision = maxPrecision;
      }
      assertBasic(typeName);
      assert (precision >= 0)
          || (precision == RelDataType.PRECISION_NOT_SPECIFIED);
      // Does not check precision when typeName is SqlTypeName#NULL.
      RelDataType newType = precision == RelDataType.PRECISION_NOT_SPECIFIED
          ? new BasicSqlType(typeSystem, typeName)
          : new BasicSqlType(typeSystem, typeName, precision);
      newType = SqlTypeUtil.addCharsetAndCollation(newType, this);
      return canonize(newType);
    }
  
    @SuppressWarnings("deprecation") // [CALCITE-6598]
    @Override public RelDataType createSqlType(
        SqlTypeName typeName,
        int precision,
        int scale) {
      assertBasic(typeName);
      assert (precision >= 0)
          || (precision == RelDataType.PRECISION_NOT_SPECIFIED);
      if (precision != RelDataType.PRECISION_NOT_SPECIFIED) {
        final int minPrecision = typeSystem.getMinPrecision(typeName);
        final int maxPrecision = typeSystem.getMaxPrecision(typeName);
        if (maxPrecision >= 0 && precision > maxPrecision) {
          precision = maxPrecision;
        }
        if (precision < minPrecision) {
          throw RESOURCE.invalidPrecisionForDecimalType(precision, maxPrecision)
              .ex();
        }
      }
      if (scale != RelDataType.SCALE_NOT_SPECIFIED) {
        final int minScale = typeSystem.getMinScale(typeName);
        final int maxScale = typeSystem.getMaxNumericScale();
        if (scale < minScale) {
          throw RESOURCE.invalidScaleForDecimalType(scale, minScale, maxScale)
              .ex();
        }
      }
      RelDataType newType =
          new BasicSqlType(typeSystem, typeName, precision, scale);
      newType = SqlTypeUtil.addCharsetAndCollation(newType, this);
      return canonize(newType);
    }
  
    @Override public RelDataType createUnknownType() {
      return createSqlType(SqlTypeName.UNKNOWN);
    }
  
    @Override public RelDataType createMultisetType(
        RelDataType type,
        long maxCardinality) {
      assert maxCardinality == -1;
      RelDataType newType = new MultisetSqlType(type, false);
      return canonize(newType);
    }
  
    @Override public RelDataType createArrayType(
        RelDataType elementType,
        long maxCardinality) {
      assert maxCardinality == -1;
      ArraySqlType newType = new ArraySqlType(elementType, false);
      return canonize(newType);
    }
  
    @Override public RelDataType createMapType(
        RelDataType keyType,
        RelDataType valueType) {
      MapSqlType newType = new MapSqlType(keyType, valueType, false);
      return canonize(newType);
    }
  
    @Override public RelDataType createFunctionSqlType(
        RelDataType parameterType,
        RelDataType returnType) {
      return canonize(new FunctionSqlType(parameterType, returnType));
    }
  
    @Override public RelDataType createMeasureType(RelDataType valueType) {
      MeasureSqlType newType = MeasureSqlType.create(valueType);
      return canonize(newType);
    }
  
    @Override public RelDataType createSqlIntervalType(
        SqlIntervalQualifier intervalQualifier) {
      RelDataType newType =
          new IntervalSqlType(typeSystem, intervalQualifier, false);
      return canonize(newType);
    }
  
    @Override public RelDataType createTypeWithCharsetAndCollation(
        RelDataType type,
        Charset charset,
        SqlCollation collation) {
      assert SqlTypeUtil.inCharFamily(type) : type;
      requireNonNull(charset, "charset");
      requireNonNull(collation, "collation");
      RelDataType newType;
      if (type instanceof BasicSqlType) {
        BasicSqlType sqlType = (BasicSqlType) type;
        newType = sqlType.createWithCharsetAndCollation(charset, collation);
      } else if (type instanceof JavaType) {
        JavaType javaType = (JavaType) type;
        newType =
            new JavaType(
                javaType.getJavaClass(),
                javaType.isNullable(),
                charset,
                collation);
      } else {
        throw Util.needToImplement("need to implement " + type);
      }
      return canonize(newType);
    }
  
    @Override public @Nullable RelDataType leastRestrictive(
        List<RelDataType> types,
        SqlTypeMappingRule mappingRule) {
      requireNonNull(types, "types");
      requireNonNull(mappingRule, "mappingRule");
      checkArgument(types.size() >= 1, "types.size >= 1");
  
      RelDataType type0 = types.get(0);
      if (type0.getSqlTypeName() != null) {
        RelDataType resultType = leastRestrictiveSqlType(types);
        if (resultType != null) {
          return resultType;
        }
        return leastRestrictiveByCast(types, mappingRule);
      }
  
      return super.leastRestrictive(types, mappingRule);
    }
  
    private @Nullable RelDataType leastRestrictiveByCast(List<RelDataType> types,
        SqlTypeMappingRule mappingRule) {
      RelDataType resultType = types.get(0);
      boolean anyNullable = resultType.isNullable();
      for (int i = 1; i < types.size(); i++) {
        RelDataType type = types.get(i);
        if (type.getSqlTypeName() == SqlTypeName.NULL) {
          anyNullable = true;
          continue;
        }
  
        if (type.isNullable()) {
          anyNullable = true;
        }
  
        if (SqlTypeUtil.canCastFrom(type, resultType, mappingRule)) {
          resultType = type;
        } else {
          if (!SqlTypeUtil.canCastFrom(resultType, type, mappingRule)) {
            return null;
          }
        }
      }
      if (anyNullable) {
        return createTypeWithNullability(resultType, true);
      } else {
        return resultType;
      }
    }
  
    @Override public RelDataType createTypeWithNullability(
        final RelDataType type,
        final boolean nullable) {
      final RelDataType newType;
      if (type instanceof BasicSqlType) {
        newType = ((BasicSqlType) type).createWithNullability(nullable);
      } else if (type instanceof MapSqlType) {
        newType = copyMapType(type, nullable);
      } else if (type instanceof ArraySqlType) {
        newType = copyArrayType(type, nullable);
      } else if (type instanceof MultisetSqlType) {
        newType = copyMultisetType(type, nullable);
      } else if (type instanceof IntervalSqlType) {
        newType = copyIntervalType(type, nullable);
      } else if (type instanceof ObjectSqlType) {
        newType = copyObjectType(type, nullable);
      } else {
        return super.createTypeWithNullability(type, nullable);
      }
      return canonize(newType);
    }
  
    @Override public RelDataType enforceTypeWithNullability(
        final RelDataType type,
        final boolean nullable) {
      final RelDataType newType;
      if (type instanceof BasicSqlType) {
        newType = ((BasicSqlType) type).createWithNullability(nullable);
      } else if (type instanceof MapSqlType) {
        newType = copyMapType(type, nullable);
      } else if (type instanceof ArraySqlType) {
        newType = copyArrayType(type, nullable);
      } else if (type instanceof MultisetSqlType) {
        newType = copyMultisetType(type, nullable);
      } else if (type instanceof IntervalSqlType) {
        newType = copyIntervalType(type, nullable);
      } else if (type instanceof ObjectSqlType) {
        newType = copyObjectType(type, nullable);
      } else {
        return super.enforceTypeWithNullability(type, nullable);
      }
      return canonize(newType);
    }
  
    private static void assertBasic(SqlTypeName typeName) {
      assert typeName != null;
      assert typeName != SqlTypeName.MULTISET
          : "use createMultisetType() instead";
      assert typeName != SqlTypeName.ARRAY
          : "use createArrayType() instead";
      assert typeName != SqlTypeName.MAP
          : "use createMapType() instead";
      assert typeName != SqlTypeName.ROW
          : "use createStructType() instead";
      assert !SqlTypeName.INTERVAL_TYPES.contains(typeName)
          : "use createSqlIntervalType() instead";
    }
  
    @SuppressWarnings("deprecation") // [CALCITE-6598]
    private @Nullable RelDataType leastRestrictiveSqlType(List<RelDataType> types) {
      RelDataType resultType = null;
      int nullCount = 0;
      int nullableCount = 0;
      int javaCount = 0;
      int anyCount = 0;
  
      for (RelDataType type : types) {
        final SqlTypeName typeName = type.getSqlTypeName();
        if (typeName == null) {
          return null;
        }
        if (typeName == SqlTypeName.ANY) {
          anyCount++;
        }
        if (type.isNullable()) {
          ++nullableCount;
        }
        if (typeName == SqlTypeName.NULL) {
          ++nullCount;
        }
        if (isJavaType(type)) {
          ++javaCount;
        }
      }
  
      //  if any of the inputs are ANY, the output is ANY
      if (anyCount > 0) {
        return createTypeWithNullability(createSqlType(SqlTypeName.ANY),
            nullCount > 0 || nullableCount > 0);
      }
  
      for (int i = 0; i < types.size(); ++i) {
        RelDataType type = types.get(i);
        RelDataTypeFamily family = type.getFamily();
  
        final SqlTypeName typeName = type.getSqlTypeName();
        if (typeName == SqlTypeName.NULL) {
          continue;
        }
  
        // Convert Java types; for instance, JavaType(int) becomes INTEGER.
        // Except if all types are either NULL or Java types.
        if (isJavaType(type) && javaCount + nullCount < types.size()) {
          final RelDataType originalType = type;
          type = typeName.allowsPrecScale(true, true)
              ? createSqlType(typeName, type.getPrecision(), type.getScale())
              : typeName.allowsPrecScale(true, false)
              ? createSqlType(typeName, type.getPrecision())
              : createSqlType(typeName);
          type = createTypeWithNullability(type, originalType.isNullable());
          // update java type's family
          family = type.getFamily();
        }
  
        if (resultType == null) {
          resultType = type;
          SqlTypeName sqlTypeName = resultType.getSqlTypeName();
          if (sqlTypeName == SqlTypeName.ROW) {
            return leastRestrictiveStructuredType(types);
          }
          if (sqlTypeName == SqlTypeName.ARRAY
              || sqlTypeName == SqlTypeName.MULTISET) {
            return leastRestrictiveArrayMultisetType(types, sqlTypeName);
          }
          if (sqlTypeName == SqlTypeName.MAP) {
            return leastRestrictiveMapType(types, sqlTypeName);
          }
        }
  
        RelDataTypeFamily resultFamily = resultType.getFamily();
        SqlTypeName resultTypeName = resultType.getSqlTypeName();
  
        if (resultFamily != family) {
          return null;
        }
        if (SqlTypeUtil.inCharOrBinaryFamilies(type)) {
          Charset charset1 = type.getCharset();
          Charset charset2 = resultType.getCharset();
          SqlCollation collation1 = type.getCollation();
          SqlCollation collation2 = resultType.getCollation();
  
          final int precision =
              SqlTypeUtil.maxPrecision(resultType.getPrecision(),
                  type.getPrecision());
  
          // If either type is LOB, then result is LOB with no precision.
          // Otherwise, if either is variable width, result is variable
          // width.  Otherwise, result is fixed width.
          if (SqlTypeUtil.isLob(resultType)) {
            resultType = createSqlType(resultType.getSqlTypeName());
          } else if (SqlTypeUtil.isLob(type)) {
            resultType = createSqlType(type.getSqlTypeName());
          } else if (SqlTypeUtil.isBoundedVariableWidth(resultType)) {
            resultType =
                createSqlType(
                    resultType.getSqlTypeName(),
                    precision);
          } else {
            // this catch-all case covers type variable, and both fixed
  
            SqlTypeName newTypeName = type.getSqlTypeName();
  
            if (typeSystem.shouldConvertRaggedUnionTypesToVarying()) {
              if (resultType.getPrecision() != type.getPrecision()) {
                if (newTypeName == SqlTypeName.CHAR) {
                  newTypeName = SqlTypeName.VARCHAR;
                } else if (newTypeName == SqlTypeName.BINARY) {
                  newTypeName = SqlTypeName.VARBINARY;
                }
              }
            }
  
            resultType =
                createSqlType(
                    newTypeName,
                    precision);
          }
          Charset charset = null;
          // TODO:  refine collation combination rules
          SqlCollation collation0 = collation1 != null && collation2 != null
              ? SqlCollation.getCoercibilityDyadicOperator(collation1, collation2)
              : null;
          SqlCollation collation = null;
          if ((charset1 != null) || (charset2 != null)) {
            if (charset1 == null) {
              charset = charset2;
              collation = collation2;
            } else if (charset2 == null) {
              charset = charset1;
              collation = collation1;
            } else if (charset1.equals(charset2)) {
              charset = charset1;
              collation = collation1;
            } else if (charset1.contains(charset2)) {
              charset = charset1;
              collation = collation1;
            } else {
              charset = charset2;
              collation = collation2;
            }
          }
          if (charset != null) {
            resultType =
                createTypeWithCharsetAndCollation(
                    resultType,
                    charset,
                    collation0 != null ? collation0 : requireNonNull(collation, "collation"));
          }
        } else if (SqlTypeUtil.isExactNumeric(type)) {
          if (SqlTypeUtil.isExactNumeric(resultType)) {
            // TODO: come up with a cleaner way to support
            // interval + datetime = datetime
            if (types.size() > (i + 1)) {
              RelDataType type1 = types.get(i + 1);
              if (SqlTypeUtil.isDatetime(type1)) {
                resultType = type1;
                return createTypeWithNullability(resultType,
                    nullCount > 0 || nullableCount > 0);
              }
            }
            if (!type.equals(resultType)) {
              if (!typeName.allowsPrec()
                  && !resultTypeName.allowsPrec()) {
                // use the bigger primitive
                if (type.getPrecision()
                    > resultType.getPrecision()) {
                  resultType = type;
                }
              } else {
                // Let the result type have precision (p), scale (s)
                // and number of whole digits (d) as follows: d =
                // max(p1 - s1, p2 - s2) s <= max(s1, s2) p = s + d
  
                int p1 = resultType.getPrecision();
                int p2 = type.getPrecision();
                int s1 = resultType.getScale();
                int s2 = type.getScale();
                final int maxPrecision = typeSystem.getMaxNumericPrecision();
                final int maxScale = typeSystem.getMaxNumericScale();
  
                int dout = Math.max(p1 - s1, p2 - s2);
                dout =
                    Math.min(
                        dout,
                        maxPrecision);
  
                int scale = Math.max(s1, s2);
                scale =
                    Math.min(
                        scale,
                        maxPrecision - dout);
                scale = Math.min(scale, maxScale);
  
                int precision = dout + scale;
                assert precision <= maxPrecision;
                assert precision > 0
                    || (resultType.getSqlTypeName() == SqlTypeName.DECIMAL
                        && precision == 0
                        && scale == 0);
  
                resultType =
                    createSqlType(
                        SqlTypeName.DECIMAL,
                        precision,
                        scale);
              }
            }
          } else if (SqlTypeUtil.isApproximateNumeric(resultType)) {
            // already approximate; promote to double just in case
            // TODO:  only promote when required
            if (SqlTypeUtil.isDecimal(type)) {
              // Only promote to double for decimal types
              resultType = createDoublePrecisionType();
            }
          } else {
            return null;
          }
        } else if (SqlTypeUtil.isApproximateNumeric(type)) {
          if (SqlTypeUtil.isApproximateNumeric(resultType)) {
            if (type.getPrecision() > resultType.getPrecision()) {
              resultType = type;
            }
          } else if (SqlTypeUtil.isExactNumeric(resultType)) {
            if (SqlTypeUtil.isDecimal(resultType)) {
              resultType = createDoublePrecisionType();
            } else {
              resultType = type;
            }
          } else {
            return null;
          }
        } else if (SqlTypeUtil.isInterval(type)) {
          // TODO: come up with a cleaner way to support
          // interval + datetime = datetime
          if (types.size() > (i + 1)) {
            RelDataType type1 = types.get(i + 1);
            if (SqlTypeUtil.isDatetime(type1)) {
              resultType = leastRestrictiveIntervalDatetimeType(type1, type);
              return createTypeWithNullability(resultType,
                  nullCount > 0 || nullableCount > 0);
            }
          }
  
          if (!type.equals(resultType)) {
            // TODO jvs 4-June-2005:  This shouldn't be necessary;
            // move logic into IntervalSqlType.combine
            Object type1 = resultType;
            resultType =
                ((IntervalSqlType) resultType).combine(
                    this,
                    (IntervalSqlType) type);
            resultType =
                ((IntervalSqlType) resultType).combine(
                    this,
                    (IntervalSqlType) type1);
          }
        } else if (SqlTypeUtil.isDatetime(type)) {
          // TODO: come up with a cleaner way to support
          // datetime +/- interval (or integer) = datetime
          if (types.size() > (i + 1)) {
            RelDataType type1 = types.get(i + 1);
            final boolean isInterval1 = SqlTypeUtil.isInterval(type1);
            final boolean isInt1 = SqlTypeUtil.isIntType(type1);
            if (isInterval1 || isInt1) {
              resultType = leastRestrictiveIntervalDatetimeType(type, type1);
              return createTypeWithNullability(resultType,
                  nullCount > 0 || nullableCount > 0);
            }
          }
  
          if (type.getSqlTypeName() == resultType.getSqlTypeName()
              && type.getSqlTypeName().allowsPrec()
              && type.getPrecision() != resultType.getPrecision()) {
            final int precision =
                SqlTypeUtil.maxPrecision(resultType.getPrecision(),
                    type.getPrecision());
  
            resultType = createSqlType(type.getSqlTypeName(), precision);
          }
        } else {
          return null;
        }
      }
      if (resultType != null && nullableCount > 0) {
        resultType = createTypeWithNullability(resultType, true);
      }
      return resultType;
    }
  
    private RelDataType createDoublePrecisionType() {
      return createSqlType(SqlTypeName.DOUBLE);
    }
  
    private RelDataType copyMultisetType(RelDataType type, boolean nullable) {
      MultisetSqlType mt = (MultisetSqlType) type;
      RelDataType elementType = copyType(mt.getComponentType());
      return new MultisetSqlType(elementType, nullable);
    }
  
    private RelDataType copyIntervalType(RelDataType type, boolean nullable) {
      return new IntervalSqlType(typeSystem,
          requireNonNull(type.getIntervalQualifier(),
              () -> "type.getIntervalQualifier() for " + type),
          nullable);
    }
  
    private static RelDataType copyObjectType(RelDataType type, boolean nullable) {
      return new ObjectSqlType(
          type.getSqlTypeName(),
          type.getSqlIdentifier(),
          nullable,
          type.getFieldList(),
          type.getComparability());
    }
  
    private RelDataType copyArrayType(RelDataType type, boolean nullable) {
      ArraySqlType at = (ArraySqlType) type;
      RelDataType elementType = copyType(at.getComponentType());
      return new ArraySqlType(elementType, nullable);
    }
  
    private RelDataType copyMapType(RelDataType type, boolean nullable) {
      MapSqlType mt = (MapSqlType) type;
      RelDataType keyType = copyType(mt.getKeyType());
      RelDataType valueType = copyType(mt.getValueType());
      return new MapSqlType(keyType, valueType, nullable);
    }
  
    @Override protected RelDataType canonize(RelDataType type) {
      type = super.canonize(type);
      if (!(type instanceof ObjectSqlType)) {
        return type;
      }
      ObjectSqlType objectType = (ObjectSqlType) type;
      if (!objectType.isNullable()) {
        objectType.setFamily(objectType);
      } else {
        objectType.setFamily(
            (RelDataTypeFamily) createTypeWithNullability(
                objectType,
                false));
      }
      return type;
    }
  }
  