blob: 3d2774bc19bc230a92f0a2630e961f72a67233f1 [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.hudi.internal.schema;
import org.apache.hudi.internal.schema.Type.NestedType;
import org.apache.hudi.internal.schema.Type.PrimitiveType;
import java.io.Serializable;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* Types supported in schema evolution.
*/
public class Types {
private Types() {
}
/**
* Boolean primitive type.
*/
public static class BooleanType extends PrimitiveType {
private static final BooleanType INSTANCE = new BooleanType();
public static BooleanType get() {
return INSTANCE;
}
@Override
public TypeID typeId() {
return Type.TypeID.BOOLEAN;
}
@Override
public String toString() {
return "boolean";
}
}
/**
* Integer primitive type.
*/
public static class IntType extends PrimitiveType {
private static final IntType INSTANCE = new IntType();
public static IntType get() {
return INSTANCE;
}
@Override
public TypeID typeId() {
return TypeID.INT;
}
@Override
public String toString() {
return "int";
}
}
/**
* Long primitive type.
*/
public static class LongType extends PrimitiveType {
private static final LongType INSTANCE = new LongType();
public static LongType get() {
return INSTANCE;
}
@Override
public TypeID typeId() {
return TypeID.LONG;
}
@Override
public String toString() {
return "long";
}
}
/**
* Float primitive type.
*/
public static class FloatType extends PrimitiveType {
private static final FloatType INSTANCE = new FloatType();
public static FloatType get() {
return INSTANCE;
}
@Override
public TypeID typeId() {
return TypeID.FLOAT;
}
@Override
public String toString() {
return "float";
}
}
/**
* Double primitive type.
*/
public static class DoubleType extends PrimitiveType {
private static final DoubleType INSTANCE = new DoubleType();
public static DoubleType get() {
return INSTANCE;
}
@Override
public TypeID typeId() {
return TypeID.DOUBLE;
}
@Override
public String toString() {
return "double";
}
}
/**
* Date primitive type.
*/
public static class DateType extends PrimitiveType {
private static final DateType INSTANCE = new DateType();
public static DateType get() {
return INSTANCE;
}
@Override
public TypeID typeId() {
return TypeID.DATE;
}
@Override
public String toString() {
return "date";
}
}
/**
* Time primitive type.
*/
public static class TimeType extends PrimitiveType {
private static final TimeType INSTANCE = new TimeType();
public static TimeType get() {
return INSTANCE;
}
private TimeType() {
}
@Override
public TypeID typeId() {
return TypeID.TIME;
}
@Override
public String toString() {
return "time";
}
}
/**
* Time primitive type.
*/
public static class TimestampType extends PrimitiveType {
private static final TimestampType INSTANCE = new TimestampType();
public static TimestampType get() {
return INSTANCE;
}
private TimestampType() {
}
@Override
public TypeID typeId() {
return TypeID.TIMESTAMP;
}
@Override
public String toString() {
return "timestamp";
}
}
/**
* String primitive type.
*/
public static class StringType extends PrimitiveType {
private static final StringType INSTANCE = new StringType();
public static StringType get() {
return INSTANCE;
}
@Override
public TypeID typeId() {
return TypeID.STRING;
}
@Override
public String toString() {
return "string";
}
}
/**
* Binary primitive type.
*/
public static class BinaryType extends PrimitiveType {
private static final BinaryType INSTANCE = new BinaryType();
public static BinaryType get() {
return INSTANCE;
}
@Override
public TypeID typeId() {
return TypeID.BINARY;
}
@Override
public String toString() {
return "binary";
}
}
/**
* Fixed primitive type.
*/
public static class FixedType extends PrimitiveType {
public static FixedType getFixed(int size) {
return new FixedType(size);
}
private final int size;
private FixedType(int length) {
this.size = length;
}
public int getFixedSize() {
return size;
}
@Override
public TypeID typeId() {
return TypeID.FIXED;
}
@Override
public String toString() {
return String.format("fixed[%d]", size);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
} else if (!(o instanceof FixedType)) {
return false;
}
FixedType fixedType = (FixedType) o;
return size == fixedType.size;
}
@Override
public int hashCode() {
return Objects.hash(FixedType.class, size);
}
}
/**
* Decimal primitive type.
*/
public static class DecimalType extends PrimitiveType {
public static DecimalType get(int precision, int scale) {
return new DecimalType(precision, scale);
}
private final int scale;
private final int precision;
private DecimalType(int precision, int scale) {
this.scale = scale;
this.precision = precision;
}
/**
* Returns whether this DecimalType is wider than `other`. If yes, it means `other`
* can be casted into `this` safely without losing any precision or range.
*/
public boolean isWiderThan(PrimitiveType other) {
if (other instanceof DecimalType) {
DecimalType dt = (DecimalType) other;
return (precision - scale) >= (dt.precision - dt.scale) && scale > dt.scale;
}
if (other instanceof IntType) {
return isWiderThan(get(10, 0));
}
return false;
}
/**
* Returns whether this DecimalType is tighter than `other`. If yes, it means `this`
* can be casted into `other` safely without losing any precision or range.
*/
public boolean isTighterThan(PrimitiveType other) {
if (other instanceof DecimalType) {
DecimalType dt = (DecimalType) other;
return (precision - scale) <= (dt.precision - dt.scale) && scale <= dt.scale;
}
if (other instanceof IntType) {
return isTighterThan(get(10, 0));
}
return false;
}
public int scale() {
return scale;
}
public int precision() {
return precision;
}
@Override
public TypeID typeId() {
return TypeID.DECIMAL;
}
@Override
public String toString() {
return String.format("decimal(%d, %d)", precision, scale);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
} else if (!(o instanceof DecimalType)) {
return false;
}
DecimalType that = (DecimalType) o;
if (scale != that.scale) {
return false;
}
return precision == that.precision;
}
@Override
public int hashCode() {
return Objects.hash(DecimalType.class, scale, precision);
}
}
/**
* UUID primitive type.
*/
public static class UUIDType extends PrimitiveType {
private static final UUIDType INSTANCE = new UUIDType();
public static UUIDType get() {
return INSTANCE;
}
@Override
public TypeID typeId() {
return TypeID.UUID;
}
@Override
public String toString() {
return "uuid";
}
}
/** A field within a record. */
public static class Field implements Serializable {
// Experimental method to support defaultValue
public static Field get(int id, boolean isOptional, String name, Type type, String doc, Object defaultValue) {
return new Field(isOptional, id, name, type, doc, defaultValue);
}
public static Field get(int id, boolean isOptional, String name, Type type, String doc) {
return new Field(isOptional, id, name, type, doc, null);
}
public static Field get(int id, boolean isOptional, String name, Type type) {
return new Field(isOptional, id, name, type, null, null);
}
public static Field get(int id, String name, Type type) {
return new Field(true, id, name, type, null, null);
}
private final boolean isOptional;
private final int id;
private final String name;
private final Type type;
private final String doc;
// Experimental properties
private final Object defaultValue;
private Field(boolean isOptional, int id, String name, Type type, String doc, Object defaultValue) {
this.isOptional = isOptional;
this.id = id;
this.name = name;
this.type = type;
this.doc = doc;
this.defaultValue = defaultValue;
}
public Object getDefaultValue() {
return defaultValue;
}
public boolean isOptional() {
return isOptional;
}
public int fieldId() {
return id;
}
public String name() {
return name;
}
public Type type() {
return type;
}
public String doc() {
return doc;
}
@Override
public String toString() {
return String.format("%d: %s: %s %s",
id, name, isOptional ? "optional" : "required", type) + (doc != null ? " (" + doc + ")" : "");
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
} else if (!(o instanceof Field)) {
return false;
}
Field that = (Field) o;
if (isOptional != that.isOptional) {
return false;
} else if (id != that.id) {
return false;
} else if (!name.equals(that.name)) {
return false;
} else if (!Objects.equals(doc, that.doc)) {
return false;
}
return type.equals(that.type);
}
@Override
public int hashCode() {
return Objects.hash(Field.class, id, isOptional, name, type);
}
}
/**
* Record nested type.
*/
public static class RecordType extends NestedType {
// NOTE: This field is necessary to provide for lossless conversion b/w Avro and
// InternalSchema and back (Avro unfortunately relies not only on structural equivalence of
// schemas but also corresponding Record type's "name" when evaluating their compatibility);
// This field is nullable
private final String name;
private final Field[] fields;
private transient Map<String, Field> nameToFields = null;
private transient Map<Integer, Field> idToFields = null;
private RecordType(List<Field> fields, String name) {
this.name = name;
this.fields = fields.toArray(new Field[0]);
}
@Override
public List<Field> fields() {
return Arrays.asList(fields);
}
public Field field(String name) {
if (nameToFields == null) {
nameToFields = new HashMap<>();
for (Field field : fields) {
nameToFields.put(field.name().toLowerCase(Locale.ROOT), field);
}
}
return nameToFields.get(name.toLowerCase(Locale.ROOT));
}
@Override
public Field field(int id) {
if (idToFields == null) {
idToFields = new HashMap<>();
for (Field field : fields) {
idToFields.put(field.fieldId(), field);
}
}
return idToFields.get(id);
}
@Override
public Type fieldType(String name) {
Field field = field(name);
if (field != null) {
return field.type();
}
return null;
}
public String name() {
return name;
}
@Override
public TypeID typeId() {
return TypeID.RECORD;
}
@Override
public String toString() {
return String.format("Record<%s>", Arrays.stream(fields).map(f -> f.toString()).collect(Collectors.joining("-")));
}
@Override
public boolean equals(Object o) {
// NOTE: We're not comparing {@code RecordType}'s names here intentionally
// relying exclusively on structural equivalence
if (this == o) {
return true;
} else if (!(o instanceof RecordType)) {
return false;
}
RecordType that = (RecordType) o;
return Arrays.equals(fields, that.fields);
}
@Override
public int hashCode() {
// NOTE: {@code hashCode} has to match for objects for which {@code equals} returns true,
// hence we don't hash the {@code name} in here
return Objects.hash(Field.class, Arrays.hashCode(fields));
}
public static RecordType get(List<Field> fields) {
return new RecordType(fields, null);
}
public static RecordType get(List<Field> fields, String recordName) {
return new RecordType(fields, recordName);
}
public static RecordType get(Field... fields) {
return new RecordType(Arrays.asList(fields), null);
}
}
/**
* Array nested type.
*/
public static class ArrayType extends NestedType {
public static ArrayType get(int elementId, boolean isOptional, Type elementType) {
return new ArrayType(Field.get(elementId, isOptional, "element", elementType));
}
private final Field elementField;
private ArrayType(Field elementField) {
this.elementField = elementField;
}
public Type elementType() {
return elementField.type();
}
@Override
public Type fieldType(String name) {
if ("element".equals(name)) {
return elementType();
}
return null;
}
@Override
public Field field(int id) {
if (elementField.fieldId() == id) {
return elementField;
}
return null;
}
@Override
public List<Field> fields() {
return Arrays.asList(elementField);
}
public int elementId() {
return elementField.fieldId();
}
public boolean isElementOptional() {
return elementField.isOptional;
}
@Override
public TypeID typeId() {
return TypeID.ARRAY;
}
@Override
public String toString() {
return String.format("list<%s>", elementField.type());
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
} else if (!(o instanceof ArrayType)) {
return false;
}
ArrayType listType = (ArrayType) o;
return elementField.equals(listType.elementField);
}
@Override
public int hashCode() {
return Objects.hash(ArrayType.class, elementField);
}
}
/**
* Map nested type.
*/
public static class MapType extends NestedType {
public static MapType get(int keyId, int valueId, Type keyType, Type valueType) {
return new MapType(
Field.get(keyId, "key", keyType),
Field.get(valueId, "value", valueType));
}
public static MapType get(int keyId, int valueId, Type keyType, Type valueType, boolean isOptional) {
return new MapType(
Field.get(keyId, isOptional, "key", keyType),
Field.get(valueId, isOptional, "value", valueType));
}
private final Field keyField;
private final Field valueField;
private transient List<Field> fields = null;
private MapType(Field keyField, Field valueField) {
this.keyField = keyField;
this.valueField = valueField;
}
public Type keyType() {
return keyField.type();
}
public Type valueType() {
return valueField.type();
}
@Override
public Type fieldType(String name) {
if ("key".equals(name)) {
return keyField.type();
} else if ("value".equals(name)) {
return valueField.type();
}
return null;
}
@Override
public Field field(int id) {
if (keyField.fieldId() == id) {
return keyField;
} else if (valueField.fieldId() == id) {
return valueField;
}
return null;
}
@Override
public List<Field> fields() {
if (fields == null) {
fields = Arrays.asList(keyField, valueField);
}
return fields;
}
public int keyId() {
return keyField.fieldId();
}
public int valueId() {
return valueField.fieldId();
}
public boolean isValueOptional() {
return valueField.isOptional;
}
@Override
public TypeID typeId() {
return TypeID.MAP;
}
@Override
public String toString() {
return String.format("map<%s, %s>", keyField.type(), valueField.type());
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
} else if (!(o instanceof MapType)) {
return false;
}
MapType mapType = (MapType) o;
if (!keyField.equals(mapType.keyField)) {
return false;
}
return valueField.equals(mapType.valueField);
}
@Override
public int hashCode() {
return Objects.hash(MapType.class, keyField, valueField);
}
}
}