/* * Licensed 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 com.facebook.presto.spi.type; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.TreeMap; import java.util.stream.Collectors; import static java.lang.String.format; import static java.util.Arrays.asList; import static java.util.Collections.unmodifiableList; import static java.util.Objects.requireNonNull; public class TypeSignature { private final String base; private final List<TypeSignatureParameter> parameters; private final boolean calculated; private static final Map<String, String> BASE_NAME_ALIAS_TO_CANONICAL = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER); static { BASE_NAME_ALIAS_TO_CANONICAL.put("int", StandardTypes.INTEGER); } public TypeSignature(String base, TypeSignatureParameter... parameters) { this(base, asList(parameters)); } public TypeSignature(String base, List<TypeSignatureParameter> parameters) { checkArgument(base != null, "base is null"); this.base = base; checkArgument(!base.isEmpty(), "base is empty"); checkArgument(validateName(base), "Bad characters in base type: %s", base); checkArgument(parameters != null, "parameters is null"); this.parameters = unmodifiableList(new ArrayList<>(parameters)); this.calculated = parameters.stream().anyMatch(TypeSignatureParameter::isCalculated); } // TODO: merge literalParameters for Row with TypeSignatureParameter @Deprecated public TypeSignature(String base, List<TypeSignature> typeSignatureParameters, List<String> literalParameters) { this(base, createNamedTypeParameters(typeSignatureParameters, literalParameters)); } public String getBase() { return base; } public List<TypeSignatureParameter> getParameters() { return parameters; } public List<TypeSignature> getTypeParametersAsTypeSignatures() { List<TypeSignature> result = new ArrayList<>(); for (TypeSignatureParameter parameter : parameters) { if (parameter.getKind() != ParameterKind.TYPE) { throw new IllegalStateException( format("Expected all parameters to be TypeSignatures but [%s] was found", parameter.toString())); } result.add(parameter.getTypeSignature()); } return result; } public boolean isCalculated() { return calculated; } @JsonCreator public static TypeSignature parseTypeSignature(String signature) { return parseTypeSignature(signature, new HashSet<>()); } public static TypeSignature parseTypeSignature(String signature, Set<String> literalCalculationParameters) { if (!signature.contains("<") && !signature.contains("(")) { if (signature.equalsIgnoreCase(StandardTypes.VARCHAR)) { return VarcharType.createUnboundedVarcharType().getTypeSignature(); } checkArgument(!literalCalculationParameters.contains(signature), "Bad type signature: '%s'", signature); return new TypeSignature(canonicalizeBaseName(signature), new ArrayList<>()); } if (signature.toLowerCase(Locale.ENGLISH).startsWith(StandardTypes.ROW + "(")) { return parseRowTypeSignature(signature, literalCalculationParameters); } // TODO: remove the support of parsing old style row type // when everything has been moved to use new style if (signature.toLowerCase(Locale.ENGLISH).startsWith(StandardTypes.ROW + "<")) { return parseOldStyleRowTypeSignature(signature, literalCalculationParameters); } String baseName = null; List<TypeSignatureParameter> parameters = new ArrayList<>(); int parameterStart = -1; int bracketCount = 0; for (int i = 0; i < signature.length(); i++) { char c = signature.charAt(i); // TODO: remove angle brackets support once ROW<TYPE>(name) will be dropped // Angle brackets here are checked not for the support of ARRAY<> and MAP<> // but to correctly parse ARRAY(row<BIGINT, BIGINT>('a','b')) if (c == '(' || c == '<') { if (bracketCount == 0) { verify(baseName == null, "Expected baseName to be null"); verify(parameterStart == -1, "Expected parameter start to be -1"); baseName = canonicalizeBaseName(signature.substring(0, i)); checkArgument(!literalCalculationParameters.contains(baseName), "Bad type signature: '%s'", signature); parameterStart = i + 1; } bracketCount++; } else if (c == ')' || c == '>') { bracketCount--; checkArgument(bracketCount >= 0, "Bad type signature: '%s'", signature); if (bracketCount == 0) { checkArgument(parameterStart >= 0, "Bad type signature: '%s'", signature); parameters.add(parseTypeSignatureParameter(signature, parameterStart, i, literalCalculationParameters)); parameterStart = i + 1; if (i == signature.length() - 1) { return new TypeSignature(baseName, parameters); } } } else if (c == ',') { if (bracketCount == 1) { checkArgument(parameterStart >= 0, "Bad type signature: '%s'", signature); parameters.add(parseTypeSignatureParameter(signature, parameterStart, i, literalCalculationParameters)); parameterStart = i + 1; } } } throw new IllegalArgumentException(format("Bad type signature: '%s'", signature)); } @Deprecated private static TypeSignature parseRowTypeSignature(String signature, Set<String> literalParameters) { String baseName = null; List<TypeSignature> parameters = new ArrayList<>(); List<String> fieldNames = new ArrayList<>(); int parameterStart = -1; int bracketCount = 0; boolean inFieldName = false; for (int i = 0; i < signature.length(); i++) { char c = signature.charAt(i); if (c == '(') { if (bracketCount == 0) { verify(baseName == null, "Expected baseName to be null"); verify(parameterStart == -1, "Expected parameter start to be -1"); baseName = canonicalizeBaseName(signature.substring(0, i)); parameterStart = i + 1; inFieldName = true; } bracketCount++; } else if (c == ' ') { if (bracketCount == 1 && inFieldName) { checkArgument(parameterStart >= 0 && parameterStart < i, "Bad type signature: '%s'", signature); fieldNames.add(signature.substring(parameterStart, i)); parameterStart = i + 1; inFieldName = false; } } else if (c == ',') { if (bracketCount == 1) { checkArgument(parameterStart >= 0, "Bad type signature: '%s'", signature); parameters.add(parseTypeSignature(signature.substring(parameterStart, i), literalParameters)); parameterStart = i + 1; inFieldName = true; } } else if (c == ')') { bracketCount--; if (bracketCount == 0) { checkArgument(i == signature.length() - 1, "Bad type signature: '%s'", signature); checkArgument(parameterStart >= 0, "Bad type signature: '%s'", signature); parameters.add(parseTypeSignature(signature.substring(parameterStart, i), literalParameters)); return new TypeSignature(baseName, createNamedTypeParameters(parameters, fieldNames)); } } } throw new IllegalArgumentException(format("Bad type signature: '%s'", signature)); } // TODO: remove this when old style row type is removed @Deprecated private static TypeSignature parseOldStyleRowTypeSignature(String signature, Set<String> literalParameters) { String baseName = null; List<TypeSignature> parameters = new ArrayList<>(); List<String> fieldNames = new ArrayList<>(); int parameterStart = -1; int bracketCount = 0; boolean inLiteralParameters = false; for (int i = 0; i < signature.length(); i++) { char c = signature.charAt(i); if (c == '<') { if (bracketCount == 0) { verify(baseName == null, "Expected baseName to be null"); verify(parameterStart == -1, "Expected parameter start to be -1"); baseName = canonicalizeBaseName(signature.substring(0, i)); parameterStart = i + 1; } bracketCount++; } else if (c == '>') { bracketCount--; checkArgument(bracketCount >= 0, "Bad type signature: '%s'", signature); if (bracketCount == 0) { checkArgument(parameterStart >= 0, "Bad type signature: '%s'", signature); parameters.add(parseTypeSignature(signature.substring(parameterStart, i), literalParameters)); parameterStart = i + 1; } } else if (c == ',') { if (bracketCount == 1) { if (!inLiteralParameters) { checkArgument(parameterStart >= 0, "Bad type signature: '%s'", signature); parameters.add(parseTypeSignature(signature.substring(parameterStart, i), literalParameters)); parameterStart = i + 1; } else { checkArgument(parameterStart >= 0, "Bad type signature: '%s'", signature); fieldNames.add(parseFieldName(signature.substring(parameterStart, i))); parameterStart = i + 1; } } } else if (c == '(') { if (bracketCount == 0) { inLiteralParameters = true; if (baseName == null) { verify(parameters.isEmpty(), "Expected no parameters"); verify(parameterStart == -1, "Expected parameter start to be -1"); baseName = canonicalizeBaseName(signature.substring(0, i)); } parameterStart = i + 1; } bracketCount++; } else if (c == ')') { bracketCount--; if (bracketCount == 0) { checkArgument(inLiteralParameters, "Bad type signature: '%s'", signature); inLiteralParameters = false; checkArgument(i == signature.length() - 1, "Bad type signature: '%s'", signature); checkArgument(parameterStart >= 0, "Bad type signature: '%s'", signature); fieldNames.add(parseFieldName(signature.substring(parameterStart, i))); return new TypeSignature(baseName, createNamedTypeParameters(parameters, fieldNames)); } } } throw new IllegalArgumentException(format("Bad type signature: '%s'", signature)); } private static List<TypeSignatureParameter> createNamedTypeParameters(List<TypeSignature> parameters, List<String> fieldNames) { requireNonNull(parameters, "parameters is null"); requireNonNull(fieldNames, "fieldNames is null"); verify(parameters.size() == fieldNames.size() || fieldNames.isEmpty(), "Number of parameters and fieldNames for ROW type doesn't match"); List<TypeSignatureParameter> result = new ArrayList<>(); for (int i = 0; i < parameters.size(); i++) { // Use "field" + i instead of String.format to avoid expensive string formatting String fieldName = fieldNames.isEmpty() ? ("field" + i) : fieldNames.get(i); result.add(TypeSignatureParameter.of(new NamedTypeSignature(fieldName, parameters.get(i)))); } return result; } private static TypeSignatureParameter parseTypeSignatureParameter( String signature, int begin, int end, Set<String> literalCalculationParameters) { String parameterName = signature.substring(begin, end).trim(); if (Character.isDigit(signature.charAt(begin))) { return TypeSignatureParameter.of(Long.parseLong(parameterName)); } else if (literalCalculationParameters.contains(parameterName)) { return TypeSignatureParameter.of(parameterName); } else { return TypeSignatureParameter.of(parseTypeSignature(parameterName, literalCalculationParameters)); } } // TODO: remove this when old style row type is removed @Deprecated private static String parseFieldName(String fieldName) { checkArgument(fieldName != null && fieldName.length() >= 2, "Bad fieldName: '%s'", fieldName); checkArgument(fieldName.startsWith("'") && fieldName.endsWith("'"), "Bad fieldName: '%s'", fieldName); return fieldName.substring(1, fieldName.length() - 1); } @Override @JsonValue public String toString() { // TODO: remove these hacks if (base.equalsIgnoreCase(StandardTypes.ROW)) { return rowToString(); } else if (base.equalsIgnoreCase(StandardTypes.VARCHAR) && (parameters.size() == 1) && parameters.get(0).isLongLiteral() && parameters.get(0).getLongLiteral() == VarcharType.UNBOUNDED_LENGTH) { return base; } else { StringBuilder typeName = new StringBuilder(base); if (!parameters.isEmpty()) { typeName.append("("); boolean first = true; for (TypeSignatureParameter parameter : parameters) { if (!first) { typeName.append(","); } first = false; typeName.append(parameter.toString()); } typeName.append(")"); } return typeName.toString(); } } @Deprecated private String rowToString() { verify(parameters.stream().allMatch(parameter -> parameter.getKind() == ParameterKind.NAMED_TYPE), format("Incorrect parameters for row type %s", parameters)); String fields = parameters.stream() .map(TypeSignatureParameter::getNamedTypeSignature) .map(parameter -> format("%s %s", parameter.getName(), parameter.getTypeSignature().toString())) .collect(Collectors.joining(",")); return format("row(%s)", fields); } private static void checkArgument(boolean argument, String format, Object... args) { if (!argument) { throw new IllegalArgumentException(format(format, args)); } } private static void verify(boolean argument, String message) { if (!argument) { throw new AssertionError(message); } } private static boolean validateName(String name) { return name.chars().noneMatch(c -> c == '<' || c == '>' || c == ','); } private static String canonicalizeBaseName(String baseName) { String canonicalBaseName = BASE_NAME_ALIAS_TO_CANONICAL.get(baseName); if (canonicalBaseName == null) { return baseName; } return canonicalBaseName; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } TypeSignature other = (TypeSignature) o; return Objects.equals(this.base.toLowerCase(Locale.ENGLISH), other.base.toLowerCase(Locale.ENGLISH)) && Objects.equals(this.parameters, other.parameters); } @Override public int hashCode() { return Objects.hash(base.toLowerCase(Locale.ENGLISH), parameters); } }