package com.tesora.dve.sql.schema.types;
/*
* #%L
* Tesora Inc.
* Database Virtualization Engine
* %%
* Copyright (C) 2011 - 2014 Tesora Inc.
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import com.tesora.dve.common.catalog.UserColumn;
import com.tesora.dve.db.DBNative;
import com.tesora.dve.db.NativeType;
import com.tesora.dve.db.NativeTypeCatalog;
import com.tesora.dve.db.mysql.MysqlNativeType;
import com.tesora.dve.db.mysql.MysqlNativeType.MysqlType;
import com.tesora.dve.db.mysql.common.ColumnAttributes;
import com.tesora.dve.exceptions.PECodingException;
import com.tesora.dve.exceptions.PEException;
import com.tesora.dve.server.global.HostService;
import com.tesora.dve.singleton.Singletons;
import com.tesora.dve.sql.ParserException.Pass;
import com.tesora.dve.sql.SchemaException;
import com.tesora.dve.sql.infoschema.persist.CatalogColumnEntity;
import com.tesora.dve.sql.node.expression.LiteralExpression;
import com.tesora.dve.sql.schema.FloatSizeTypeAttribute;
import com.tesora.dve.sql.schema.Name;
import com.tesora.dve.sql.schema.SchemaContext;
import com.tesora.dve.sql.schema.SizeTypeAttribute;
import com.tesora.dve.sql.schema.UnqualifiedName;
import com.tesora.dve.sql.schema.modifiers.StringTypeModifier;
import com.tesora.dve.sql.schema.modifiers.TypeModifier;
import com.tesora.dve.sql.schema.modifiers.TypeModifierKind;
public class BasicType implements Type {
public static final String COMPARISON_TAG = "COMPARISON";
public static final short UNSIGNED = 1;
public static final short ZEROFILL = 2;
public static final short BINARY = 4;
protected NativeType base;
protected short flags;
protected BasicType(NativeType nt, short f) {
base = nt;
flags = f;
if (nt == null)
throw new IllegalArgumentException("BasicType native type parameter cannot be null");
}
protected boolean isSet(int f) {
return (flags & f) != 0;
}
protected void clearFlag(int f) {
flags &= ~f;
}
protected void setFlag(int f) {
flags |= f;
}
@Override
public NativeType getBaseType() { return base; }
@Override
public MysqlNativeType getMysqlType() {
return (MysqlNativeType) base;
}
@Override
public boolean isUnsigned() {
return isSet(UNSIGNED);
}
@Override
public boolean isZeroFill() {
return isSet(ZEROFILL);
}
@Override
public boolean isBinaryText() {
return isSet(BINARY);
}
@Override
public int getSize() {
return 0;
}
@Override
public boolean hasSize() {
return false;
}
@Override
public boolean hasPrecisionAndScale() {
return false;
}
@Override
public int getPrecision() {
return 0;
}
@Override
public int getScale() {
return 0;
}
@Override
public UnqualifiedName getCharset() {
return null;
}
@Override
public UnqualifiedName getCollation() {
return null;
}
@Override
public Integer getIndexSize() {
MysqlNativeType nt = (MysqlNativeType) getBaseType();
if (nt == null) return null;
switch(nt.getMysqlType()) {
case BIGINT:
case DOUBLE:
case DATETIME:
return 8;
case INT:
case FLOAT:
case TIMESTAMP:
return 4;
case MEDIUMINT:
case DATE:
case TIME:
return 3;
case SMALLINT:
return 2;
case TINYINT:
case YEAR:
return 1;
default:
return null;
}
}
@Override
public TextType asTextType() {
return new TextType(getBaseType(),flags,getSize(),null,null);
}
@Override
public String getComparison() {
return null;
}
@Override
public boolean declUsesSizing() {
return true;
}
@Override
public boolean isSerialPlaceholder() {
return false;
}
@Override
public void addColumnTypeModifiers(UserColumn uc) {
uc.setUnsigned(isUnsigned());
uc.setZerofill(isZeroFill());
uc.setBinaryText(isBinaryText());
}
@Override
public void addColumnTypeModifiers(CatalogColumnEntity cce) throws PEException {
int flags = cce.getFlags();
int typeFlags = getBaseType().getDefaultColumnAttrFlags();
flags |= typeFlags;
flags = ColumnAttributes.set(flags, ColumnAttributes.UNSIGNED, isUnsigned());
flags = ColumnAttributes.set(flags, ColumnAttributes.ZEROFILL, isZeroFill());
flags = ColumnAttributes.set(flags, ColumnAttributes.BINARY, isBinaryText());
cce.setFlags(flags);
}
@Override
public String getTypeName() {
return getBaseType().getTypeName();
}
@Override
public void persistTypeName(UserColumn uc) {
uc.setTypeName(getTypeName());
}
@Override
public Integer getDataType() {
return (getBaseType() == null ? null : getBaseType().getDataType());
}
@Override
public boolean comparableForDistribution(Type t) {
if (t instanceof TempColumnType)
return ((TempColumnType)t).comparableForDistribution(this);
return getBaseType().getDataType() == t.getBaseType().getDataType()
&& getSize() == t.getSize();
}
@Override
public String getName() {
StringBuilder buf = new StringBuilder();
Singletons.require(DBNative.class).getEmitter().emitDeclaration(this, null, buf, false);
return buf.toString();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((base == null) ? 0 : base.hashCode());
result = prime * result + flags;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
BasicType other = (BasicType) obj;
if (base == null) {
if (other.base != null)
return false;
} else if (!base.equals(other.base))
return false;
if (flags != other.flags)
return false;
return true;
}
public static NativeType lookupNativeType(String name, NativeTypeCatalog typeCatalog) {
try {
return typeCatalog.findType(name, true);
} catch (PEException pe) {
throw new SchemaException(Pass.SECOND, "No such type: " + name,pe);
}
}
/**
* @param dataType
* @param sizing
* @param dbn
* @return
*/
private static NativeType lookupNativeType(int dataType, int sizing, DBNative dbn) {
try {
return dbn.getTypeCatalog().findType(dataType, /* sizing, */true);
} catch (PEException pe) {
throw new SchemaException(Pass.SECOND, "No type for type code: " + dataType,pe);
}
}
protected static short buildFlags(List<TypeModifier> modifiers) {
short flags = 0;
for(Iterator<TypeModifier> iter = modifiers.iterator(); iter.hasNext();) {
TypeModifier tm = iter.next();
if (tm.getKind() == TypeModifierKind.UNSIGNED) {
flags |= UNSIGNED;
iter.remove();
} else if (tm.getKind() == TypeModifierKind.BINARY) {
flags |= BINARY;
iter.remove();
} else if (tm.getKind() == TypeModifierKind.ZEROFILL) {
flags |= ZEROFILL;
flags |= UNSIGNED;
iter.remove();
} else if (tm.getKind() == TypeModifierKind.SIGNED) {
// ignore - we're signed by default unless we are unsigned
iter.remove();
}
}
return flags;
}
protected static String buildStringModifier(TypeModifierKind tk, List<TypeModifier> modifiers) {
for(Iterator<TypeModifier> iter = modifiers.iterator(); iter.hasNext();) {
TypeModifier tm = iter.next();
if (tm.getKind() == tk) {
iter.remove();
StringTypeModifier stm = (StringTypeModifier) tm;
return stm.getValue();
}
}
return null;
}
protected static UnqualifiedName buildNameModifier(TypeModifierKind tk, List<TypeModifier> modifiers) {
String value = buildStringModifier(tk,modifiers);
if (value != null)
return new UnqualifiedName(value);
return null;
}
protected static class FlagsAndModifiers {
public short flags;
public UnqualifiedName charset;
public UnqualifiedName collation;
public String comparator;
}
protected static FlagsAndModifiers buildFlagsAndModifiers(List<TypeModifier> modifiers) {
FlagsAndModifiers out = new FlagsAndModifiers();
out.flags = buildFlags(modifiers);
out.charset = buildNameModifier(TypeModifierKind.CHARSET, modifiers);
out.collation = buildNameModifier(TypeModifierKind.COLLATE, modifiers);
out.comparator = buildStringModifier(TypeModifierKind.COMPARISON, modifiers);
if (!modifiers.isEmpty())
throw new SchemaException(Pass.SECOND, "Unhandled type modifier: " + modifiers.get(0).getKind().getSQL());
return out;
}
public static BasicType buildType(NativeType backing, Integer size, List<TypeModifier> modifiers) {
FlagsAndModifiers fam = buildFlagsAndModifiers(modifiers);
if (fam.comparator != null)
return new ComparisonType(backing, fam.flags, (size == null ? 0 : size), fam.charset, fam.collation, fam.comparator);
else if (fam.charset != null || fam.collation != null)
return new TextType(backing,fam.flags,(size == null ? 0 : size),fam.charset,fam.collation);
else if (size != null && size > 0)
return new SizedType(backing,fam.flags,size);
else
return new BasicType(backing,fam.flags);
}
public static BasicType buildType(NativeType backing, int size, int precision, int scale, List<TypeModifier> modifiers) {
FlagsAndModifiers fam = buildFlagsAndModifiers(modifiers);
if (fam.charset != null || fam.collation != null)
throw new SchemaException(Pass.SECOND,"Invalid type modifiers, found charset or collation on floating point");
else if (fam.comparator != null)
throw new SchemaException(Pass.SECOND,"Invalid type modifiers, found comparator on floating point");
return new FloatingPointType(backing,fam.flags,size,precision,scale);
}
public static Type buildType(String typeName, int size, List<TypeModifier> modifiers, NativeTypeCatalog typeCatalog) {
ArrayList<TypeModifier> copy = new ArrayList<TypeModifier>(modifiers);
NativeType bt = lookupNativeType(typeName.toString(), typeCatalog);
return buildType(bt,size,copy);
}
public static BasicType buildType(List<Name> typeNames, List<SizeTypeAttribute> sizes, List<TypeModifier> modifiers,
NativeTypeCatalog typeCatalog) {
StringBuilder buf = new StringBuilder();
for(Iterator<Name> iter = typeNames.iterator(); iter.hasNext();) {
buf.append(iter.next().getCapitalized().get());
if (iter.hasNext())
buf.append(" ");
}
String str = buf.toString();
BasicType any = handleSerialType(str,typeCatalog);
if (any != null) return any;
NativeType bt = lookupNativeType(str,typeCatalog);
// there could be multiple sizing hints - collapse them down to one
FloatSizeTypeAttribute floatSizing = null;
SizeTypeAttribute sizing = null;
for(SizeTypeAttribute sta : sizes) {
if (sta instanceof FloatSizeTypeAttribute)
floatSizing = (FloatSizeTypeAttribute)sta;
else
sizing = sta;
}
if (floatSizing != null && !bt.getSupportsPrecision()) {
throw new SchemaException(Pass.SECOND, "Type " + bt.getTypeName() + " does not support precision/scale");
}
if (floatSizing != null && sizing != null) {
throw new SchemaException(Pass.SECOND, "Cannot specify both sizing and precision");
}
if (floatSizing != null)
return buildType(bt,floatSizing.getSize(),floatSizing.getPrecision(),floatSizing.getScale(),modifiers);
return buildType(bt,(sizing == null ? 0 : sizing.getSize()),modifiers);
}
public static Type buildType(int dataType, int sizing, DBNative dbn) {
NativeType nativeType = lookupNativeType(dataType, sizing, dbn);
if (sizing > 0)
return new SizedType(nativeType,(short)0,sizing);
return new BasicType(nativeType,(short)0);
}
public static Type buildType(UserColumn uc, NativeTypeCatalog types) {
MysqlNativeType mnType = (MysqlNativeType)lookupNativeType(uc.getTypeName(),types);
if (MysqlType.ENUM.equals(mnType.getMysqlType()) || MysqlType.SET.equals(mnType.getMysqlType()))
return DBEnumType.buildType(uc, types);
List<TypeModifier> modifiers = buildModifiers(uc);
if ((uc.getPrecision() > 0) || (uc.getScale() > 0)) {
return buildType(mnType,uc.getSize(),uc.getPrecision(),uc.getScale(),modifiers);
}
return buildType(mnType,uc.getSize(),modifiers);
}
public static List<TypeModifier> buildModifiers(UserColumn uc) {
List<TypeModifier> modifiers = new ArrayList<TypeModifier>();
if (uc.isUnsigned())
modifiers.add(new TypeModifier(TypeModifierKind.UNSIGNED));
if (uc.isZerofill())
modifiers.add(new TypeModifier(TypeModifierKind.ZEROFILL));
if (uc.isBinaryText())
modifiers.add(new TypeModifier(TypeModifierKind.BINARY));
String mods = uc.getESUniverse();
if (mods != null) {
int offset = mods.indexOf(COMPARISON_TAG);
if (offset > -1) {
int boundary = offset + COMPARISON_TAG.length();
int nextSpace = mods.indexOf(" ", boundary);
String value = mods.substring(boundary,nextSpace);
modifiers.add(new StringTypeModifier(TypeModifierKind.COMPARISON, value));
}
}
if (uc.getCharset() != null)
modifiers.add(new StringTypeModifier(TypeModifierKind.CHARSET, uc.getCharset()));
if (uc.getCollation() != null)
modifiers.add(new StringTypeModifier(TypeModifierKind.COLLATE, uc.getCollation()));
return modifiers;
}
@Override
public boolean mustParameterize() {
return isBinaryType() || isStringType();
}
@Override
public boolean isBinaryType() {
return getBaseType().isBinaryType();
}
@Override
public boolean supportsDefaultValue() {
return getBaseType().supportsDefaultValue();
}
@Override
public boolean isStringType() {
return getBaseType().isStringType();
}
@Override
public boolean isFloatType() {
return getBaseType().isFloatType();
}
@Override
public boolean isNumericType() {
return getBaseType().isNumericType();
}
@Override
public boolean isDecimalType() {
return getBaseType().isDecimalType();
}
@Override
public boolean isIntegralType() {
return getBaseType().isIntegralType();
}
@Override
public boolean isBitType() {
return getMysqlType().getMysqlType() == MysqlType.BIT;
}
@Override
public boolean isTimestampType() {
return getBaseType().isTimestampType();
}
@Override
public boolean asKeyRequiresPrefix() {
return getBaseType().asKeyRequiresPrefix();
}
@Override
public LiteralExpression getZeroValueLiteral() {
Object value = null;
try {
value = getBaseType().getZeroValue();
} catch (PEException pe) {
throw new SchemaException(Pass.SECOND, "Unable to determine zero value", pe);
}
if (value == null)
return LiteralExpression.makeNullLiteral();
else if (value instanceof String)
return LiteralExpression.makeStringLiteral((String)value);
else if (value instanceof Number)
return LiteralExpression.makeLongLiteral(((Number)value).longValue());
else
throw new SchemaException(Pass.SECOND, "Unhandled zero value: " + value + " (type=" + value.getClass().getSimpleName() + ")");
}
@SuppressWarnings("unchecked")
public static Type buildFromLiteralExpression(SchemaContext sc, LiteralExpression literal) throws PEException {
if (literal.isNumericLiteral()) {
if (literal.isFloatLiteral()) {
NativeType nt = Singletons.require(DBNative.class).findType("DECIMAL");
return BasicType.buildType(nt, 0, (int)nt.getPrecision(), nt.getMaximumScale(), Collections.EMPTY_LIST);
}
// assume integer
return BasicType.buildType(java.sql.Types.BIGINT, 21, Singletons.require(DBNative.class));
}
// get the length of the string
String value = (String)(literal.getValue(sc.getValues()));
return BasicType.buildType(java.sql.Types.VARCHAR, value.length(), Singletons.require(DBNative.class));
}
public static Type getLongType(NativeTypeCatalog typeCatalog) {
return buildType("INT",typeCatalog);
}
public static Type getDateTimeType(NativeTypeCatalog typeCatalog) {
return buildType("DATETIME",typeCatalog);
}
private static Type buildType(String nativeName, NativeTypeCatalog typeCatalog) {
NativeType nt = lookupNativeType(nativeName,typeCatalog);
return new BasicType(nt, (short)0);
}
public static class SerialPlaceholderType extends BasicType {
public SerialPlaceholderType(NativeTypeCatalog typeCatalog) {
super(lookupNativeType("BIGINT",typeCatalog),UNSIGNED);
}
@Override
public boolean isSerialPlaceholder() {
return true;
}
public Type convert() {
return new BasicType(base,flags);
}
}
private static BasicType handleSerialType(String tn, NativeTypeCatalog typeCatalog) {
if ("SERIAL".equalsIgnoreCase(tn)) {
return new SerialPlaceholderType(typeCatalog);
}
return null;
}
@Override
public BasicType normalize() {
if (base instanceof MysqlNativeType) {
MysqlNativeType mnt = (MysqlNativeType) base;
Normalizer n = normalizers.get(mnt.getMysqlType());
if (n != null)
return n.normalize(this);
}
return this;
}
private static final Map<MysqlNativeType.MysqlType, Normalizer> normalizers = buildNormalizers();
@SuppressWarnings("synthetic-access")
private static Map<MysqlNativeType.MysqlType,Normalizer> buildNormalizers() {
HashMap<MysqlNativeType.MysqlType,Normalizer> out = new HashMap<MysqlNativeType.MysqlType,Normalizer>();
out.put(MysqlType.BIGINT, new IntegralTypeNormalizer(20,20));
out.put(MysqlType.INT, new IntegralTypeNormalizer(11,10));
out.put(MysqlType.SMALLINT, new IntegralTypeNormalizer(6,5));
out.put(MysqlType.MEDIUMINT, new IntegralTypeNormalizer(9,8));
out.put(MysqlType.TINYINT, new IntegralTypeNormalizer(4,3));
out.put(MysqlType.DECIMAL, new DecimalTypeNormalizer());
out.put(MysqlType.BIT, new FixedSizeNormalizer(1));
out.put(MysqlType.YEAR, new FixedSizeNormalizer(4));
out.put(MysqlType.CHAR, new StringTypeNormalizer(true));
out.put(MysqlType.VARCHAR, new StringTypeNormalizer(true));
out.put(MysqlType.BINARY, new FixedSizeNormalizer(1));
out.put(MysqlType.TEXT, new StringTypeNormalizer(false));
out.put(MysqlType.TINYTEXT, new StringTypeNormalizer(false));
out.put(MysqlType.MEDIUMTEXT, new StringTypeNormalizer(false));
out.put(MysqlType.LONGTEXT, new StringTypeNormalizer(false));
return out;
}
private static abstract class Normalizer {
public abstract BasicType normalize(BasicType t);
}
private static class IntegralTypeNormalizer extends Normalizer {
int signedSize;
int unsignedSize;
@SuppressWarnings("synthetic-access")
public IntegralTypeNormalizer(int signed, int unsigned) {
signedSize = signed;
unsignedSize = unsigned;
}
@Override
public BasicType normalize(BasicType t) {
if (t.getSize() > 0) return t;
String comparator = t.getComparison();
int newSize = (t.isUnsigned() ? unsignedSize : signedSize);
if (comparator == null) {
return new SizedType(t.getBaseType(),t.flags,newSize);
}
return new ComparisonType(t.getBaseType(), t.flags, newSize, t.getCharset(), t.getCollation(), t.getComparison());
}
}
@SuppressWarnings("synthetic-access")
private static class DecimalTypeNormalizer extends Normalizer {
@Override
public BasicType normalize(BasicType t) {
if (t.getPrecision() > 0) return t;
int prec = 0;
int scale = 0;
if (t.getSize() > 0)
prec = t.getSize();
else
prec = 10;
String comparator = t.getComparison();
if (comparator == null) {
return new FloatingPointType(t.getBaseType(),t.flags,prec,prec,scale);
}
throw new SchemaException(Pass.NORMALIZE, "Invalid type modifier: comparator on floating point type");
}
}
private static class FixedSizeNormalizer extends Normalizer {
private final int size;
@SuppressWarnings("synthetic-access")
public FixedSizeNormalizer(int s) {
super();
size = s;
}
@Override
public BasicType normalize(BasicType t) {
if (t.getSize() > 0) return t;
String comparator = t.getComparison();
if (comparator == null) {
return new SizedType(t.getBaseType(),t.flags,size);
}
return new ComparisonType(t.getBaseType(),t.flags,size,t.getCharset(),t.getCollation(),comparator);
}
}
private static class StringTypeNormalizer extends Normalizer {
private final boolean sized;
@SuppressWarnings("synthetic-access")
public StringTypeNormalizer(boolean hasSize) {
super();
sized = hasSize;
}
@Override
public BasicType normalize(BasicType t) {
if (t.isBinaryText()) {
if (t.getCharset() != null && t.getCollation() != null && t.getSize() > 0)
return t;
int ns = t.getSize();
if (ns == 0 && sized)
ns = 1;
UnqualifiedName cs = t.getCharset();
if (cs == null)
cs = new UnqualifiedName(Singletons.require(DBNative.class).getDefaultServerCharacterSet());
UnqualifiedName coll = t.getCollation();
if (coll == null)
coll = new UnqualifiedName(Singletons.require(DBNative.class).getDefaultServerBinaryCollation());
String comparator = t.getComparison();
if (comparator != null)
return new ComparisonType(t.getBaseType(),t.flags,ns,cs,coll,comparator);
return new TextType(t.getBaseType(),t.flags,ns,cs,coll);
}
if (t.getSize() > 0)
return t;
int ns = t.getSize();
if (ns == 0 && sized)
ns = 1;
UnqualifiedName cs = t.getCharset();
UnqualifiedName coll = t.getCollation();
String comparator = t.getComparison();
if (comparator != null)
return new ComparisonType(t.getBaseType(),t.flags,ns,cs,coll,comparator);
else if (cs != null || coll != null)
return new TextType(t.getBaseType(),t.flags,ns,cs,coll);
else
return new SizedType(t.getBaseType(),t.flags,ns);
}
}
// determining whether types are range dist compatible. the default is they are only compatible if they
// are exactly the same (upto flags like unsigned, zerofill). we relax these requirements for some types.
@Override
public boolean isAcceptableColumnTypeForRangeType(Type columnType) {
if (isFloatType())
return false;
if (columnType.isFloatType() || columnType.isNumericType())
return false;
if (getMysqlType().getMysqlType() != columnType.getMysqlType().getMysqlType())
return false;
return true;
}
// generally types are acceptable
@Override
public boolean isAcceptableRangeType() {
if (getMysqlType().getMysqlType() == MysqlType.SET)
return false;
if (isFloatType() || isDecimalType() || isBitType())
return false;
return true;
}
@Override
public boolean isUnknown() {
return false;
}
@Override
public TextType toTextType() {
if (this.isStringType()) {
if (!(this instanceof TextType)) {
return this.asTextType();
}
return (TextType) this;
}
throw new PECodingException("Type '" + this.getName() + "' cannot be converted to text.");
}
@Override
public int getColumnAttributesFlags() {
int def = getBaseType().getDefaultColumnAttrFlags();
if (isUnsigned())
def = ColumnAttributes.set(def, ColumnAttributes.UNSIGNED);
if (isZeroFill())
def = ColumnAttributes.set(def, ColumnAttributes.ZEROFILL);
if (isBinaryText())
def = ColumnAttributes.set(def, ColumnAttributes.BINARY);
return def;
}
}