| /* |
| * 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 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) { |
| final int maxPrecision = typeSystem.getMaxPrecision(typeName); |
| if (maxPrecision >= 0 && precision > maxPrecision) { |
| precision = maxPrecision; |
| } |
| if (typeName.allowsScale()) { |
| return createSqlType(typeName, precision, typeName.getDefaultScale()); |
| } |
| 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); |
| } |
| |
| @Override public RelDataType createSqlType( |
| SqlTypeName typeName, |
| int precision, |
| int scale) { |
| assertBasic(typeName); |
| assert (precision >= 0) |
| || (precision == RelDataType.PRECISION_NOT_SPECIFIED); |
| final int maxPrecision = typeSystem.getMaxPrecision(typeName); |
| if (maxPrecision >= 0 && precision > maxPrecision) { |
| precision = maxPrecision; |
| } |
| 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); |
| } |
| |
| 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"; |
| } |
| |
| 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()); |
| } |
| |
| 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; |
| } |
| } |