From 461d3ca984108816cf31ec64eab36f43ac25899d Mon Sep 17 00:00:00 2001 From: Dain Sundstrom Date: Sat, 15 Aug 2020 14:06:27 -0700 Subject: [PATCH] Process function path --- .../java/io/trino/cli/TestQueryRunner.java | 2 +- .../metadata/CatalogSchemaFunctionName.java | 69 +++++++++ .../io/trino/metadata/FunctionResolver.java | 132 +++++++++++++----- .../trino/metadata/GlobalFunctionCatalog.java | 32 +++-- .../io/trino/metadata/MetadataManager.java | 26 ++-- .../io/trino/metadata/SchemaFunctionName.java | 69 +++++++++ .../src/main/java/io/trino/sql/SqlPath.java | 4 + .../java/io/trino/testing/TestingSession.java | 4 - 8 files changed, 276 insertions(+), 62 deletions(-) create mode 100644 core/trino-main/src/main/java/io/trino/metadata/CatalogSchemaFunctionName.java create mode 100644 core/trino-main/src/main/java/io/trino/metadata/SchemaFunctionName.java diff --git a/client/trino-cli/src/test/java/io/trino/cli/TestQueryRunner.java b/client/trino-cli/src/test/java/io/trino/cli/TestQueryRunner.java index 21ab170bf366..536e23700595 100644 --- a/client/trino-cli/src/test/java/io/trino/cli/TestQueryRunner.java +++ b/client/trino-cli/src/test/java/io/trino/cli/TestQueryRunner.java @@ -112,7 +112,7 @@ static ClientSession createClientSession(MockWebServer server) "clientInfo", "catalog", "schema", - "path", + null, ZoneId.of("America/Los_Angeles"), Locale.ENGLISH, ImmutableMap.of(), diff --git a/core/trino-main/src/main/java/io/trino/metadata/CatalogSchemaFunctionName.java b/core/trino-main/src/main/java/io/trino/metadata/CatalogSchemaFunctionName.java new file mode 100644 index 000000000000..6956779f7136 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/metadata/CatalogSchemaFunctionName.java @@ -0,0 +1,69 @@ +/* + * 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 io.trino.metadata; + +import java.util.Objects; + +public final class CatalogSchemaFunctionName +{ + private final String catalogName; + private final SchemaFunctionName schemaFunctionName; + + public CatalogSchemaFunctionName(String catalogName, SchemaFunctionName schemaFunctionName) + { + this.catalogName = catalogName; + this.schemaFunctionName = schemaFunctionName; + } + + public CatalogSchemaFunctionName(String catalogName, String schemaName, String functionName) + { + this(catalogName, new SchemaFunctionName(schemaName, functionName)); + } + + public String getCatalogName() + { + return catalogName; + } + + public SchemaFunctionName getSchemaFunctionName() + { + return schemaFunctionName; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CatalogSchemaFunctionName that = (CatalogSchemaFunctionName) o; + return Objects.equals(catalogName, that.catalogName) && + Objects.equals(schemaFunctionName, that.schemaFunctionName); + } + + @Override + public int hashCode() + { + return Objects.hash(catalogName, schemaFunctionName); + } + + @Override + public String toString() + { + return catalogName + '.' + schemaFunctionName; + } +} diff --git a/core/trino-main/src/main/java/io/trino/metadata/FunctionResolver.java b/core/trino-main/src/main/java/io/trino/metadata/FunctionResolver.java index 9b99c6d04ec0..22f4935b59a7 100644 --- a/core/trino-main/src/main/java/io/trino/metadata/FunctionResolver.java +++ b/core/trino-main/src/main/java/io/trino/metadata/FunctionResolver.java @@ -20,7 +20,9 @@ import io.trino.spi.TrinoException; import io.trino.spi.type.Type; import io.trino.spi.type.TypeManager; +import io.trino.sql.SqlPathElement; import io.trino.sql.analyzer.TypeSignatureProvider; +import io.trino.sql.tree.Identifier; import io.trino.sql.tree.QualifiedName; import java.util.ArrayList; @@ -28,6 +30,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; import static com.google.common.base.MoreObjects.toStringHelper; @@ -35,7 +38,10 @@ import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.Iterables.getOnlyElement; +import static io.trino.metadata.FunctionKind.AGGREGATE; import static io.trino.metadata.FunctionKind.SCALAR; +import static io.trino.metadata.GlobalFunctionCatalog.GLOBAL_CATALOG; +import static io.trino.metadata.GlobalFunctionCatalog.GLOBAL_SCHEMA; import static io.trino.spi.StandardErrorCode.AMBIGUOUS_FUNCTION_CALL; import static io.trino.spi.StandardErrorCode.FUNCTION_IMPLEMENTATION_MISSING; import static io.trino.spi.StandardErrorCode.FUNCTION_NOT_FOUND; @@ -55,24 +61,40 @@ public FunctionResolver(Metadata metadata, TypeManager typeManager) this.typeManager = requireNonNull(typeManager, "typeManager is null"); } - FunctionBinding resolveCoercion(Session session, Collection allCandidates, Signature signature) + boolean isAggregationFunction(Session session, QualifiedName name, Function> candidateLoader) { - List exactCandidates = allCandidates.stream() - .filter(function -> possibleExactCastMatch(signature, function.getSignature())) - .collect(Collectors.toList()); - for (FunctionMetadata candidate : exactCandidates) { - if (canBindSignature(session, candidate.getSignature(), signature)) { - return toFunctionBinding(candidate, signature); + for (CatalogSchemaFunctionName catalogSchemaFunctionName : toPath(session, name)) { + Collection candidates = candidateLoader.apply(catalogSchemaFunctionName); + if (!candidates.isEmpty()) { + return candidates.stream() + .map(FunctionMetadata::getKind) + .anyMatch(AGGREGATE::equals); } } + return false; + } - // only consider generic genericCandidates - List genericCandidates = allCandidates.stream() - .filter(function -> !function.getSignature().getTypeVariableConstraints().isEmpty()) - .collect(Collectors.toList()); - for (FunctionMetadata candidate : genericCandidates) { - if (canBindSignature(session, candidate.getSignature(), signature)) { - return toFunctionBinding(candidate, signature); + FunctionBinding resolveCoercion(Session session, QualifiedName name, Signature signature, Function> candidateLoader) + { + for (CatalogSchemaFunctionName catalogSchemaFunctionName : toPath(session, name)) { + Collection candidates = candidateLoader.apply(catalogSchemaFunctionName); + List exactCandidates = candidates.stream() + .filter(function -> possibleExactCastMatch(signature, function.getSignature())) + .collect(toImmutableList()); + for (FunctionMetadata candidate : exactCandidates) { + if (canBindSignature(session, candidate.getSignature(), signature)) { + return toFunctionBinding(candidate, signature); + } + } + + // only consider generic genericCandidates + List genericCandidates = candidates.stream() + .filter(function -> !function.getSignature().getTypeVariableConstraints().isEmpty()) + .collect(toImmutableList()); + for (FunctionMetadata candidate : genericCandidates) { + if (canBindSignature(session, candidate.getSignature(), signature)) { + return toFunctionBinding(candidate, signature); + } } } @@ -113,37 +135,48 @@ private static boolean possibleExactCastMatch(Signature signature, Signature dec return true; } - FunctionBinding resolveFunction(Session session, Collection allCandidates, QualifiedName name, List parameterTypes) + FunctionBinding resolveFunction( + Session session, + QualifiedName name, + List parameterTypes, + Function> candidateLoader) { - if (allCandidates.isEmpty()) { - throw new TrinoException(FUNCTION_NOT_FOUND, format("Function '%s' not registered", name)); - } + ImmutableList.Builder allCandidates = ImmutableList.builder(); + for (CatalogSchemaFunctionName catalogSchemaFunctionName : toPath(session, name)) { + Collection candidates = candidateLoader.apply(catalogSchemaFunctionName); + List exactCandidates = candidates.stream() + .filter(function -> function.getSignature().getTypeVariableConstraints().isEmpty()) + .collect(toImmutableList()); + + Optional match = matchFunctionExact(session, exactCandidates, parameterTypes); + if (match.isPresent()) { + return match.get(); + } - List exactCandidates = allCandidates.stream() - .filter(function -> function.getSignature().getTypeVariableConstraints().isEmpty()) - .collect(toImmutableList()); + List genericCandidates = candidates.stream() + .filter(function -> !function.getSignature().getTypeVariableConstraints().isEmpty()) + .collect(toImmutableList()); - Optional match = matchFunctionExact(session, exactCandidates, parameterTypes); - if (match.isPresent()) { - return match.get(); - } + match = matchFunctionExact(session, genericCandidates, parameterTypes); + if (match.isPresent()) { + return match.get(); + } - List genericCandidates = allCandidates.stream() - .filter(function -> !function.getSignature().getTypeVariableConstraints().isEmpty()) - .collect(toImmutableList()); + match = matchFunctionWithCoercion(session, candidates, parameterTypes); + if (match.isPresent()) { + return match.get(); + } - match = matchFunctionExact(session, genericCandidates, parameterTypes); - if (match.isPresent()) { - return match.get(); + allCandidates.addAll(candidates); } - match = matchFunctionWithCoercion(session, allCandidates, parameterTypes); - if (match.isPresent()) { - return match.get(); + List candidates = allCandidates.build(); + if (candidates.isEmpty()) { + throw new TrinoException(FUNCTION_NOT_FOUND, format("Function '%s' not registered", name)); } List expectedParameters = new ArrayList<>(); - for (FunctionMetadata function : allCandidates) { + for (FunctionMetadata function : candidates) { String arguments = Joiner.on(", ").join(function.getSignature().getArgumentTypes()); String constraints = Joiner.on(", ").join(function.getSignature().getTypeVariableConstraints()); expectedParameters.add(format("%s(%s) %s", name, arguments, constraints).stripTrailing()); @@ -155,6 +188,35 @@ FunctionBinding resolveFunction(Session session, Collection al throw new TrinoException(FUNCTION_NOT_FOUND, message); } + private static List toPath(Session session, QualifiedName name) + { + List parts = name.getParts(); + checkArgument(parts.size() <= 3, "Function name can only have 3 parts: " + name); + if (parts.size() == 3) { + return ImmutableList.of(new CatalogSchemaFunctionName(parts.get(0), parts.get(1), parts.get(2))); + } + + if (parts.size() == 2) { + String currentCatalog = session.getCatalog() + .orElseThrow(() -> new IllegalArgumentException("Session default catalog must be set to resolve a partial function name: " + name)); + return ImmutableList.of(new CatalogSchemaFunctionName(currentCatalog, parts.get(0), parts.get(1))); + } + + ImmutableList.Builder names = ImmutableList.builder(); + String functionName = parts.get(0); + + // global namespace + names.add(new CatalogSchemaFunctionName(GLOBAL_CATALOG, GLOBAL_SCHEMA, functionName)); + + // add resolved path items + for (SqlPathElement sqlPathElement : session.getPath().getParsedPath()) { + String catalog = sqlPathElement.getCatalog().map(Identifier::getCanonicalValue).or(session::getCatalog) + .orElseThrow(() -> new IllegalArgumentException("Session default catalog must be set to resolve a partial function name: " + name)); + names.add(new CatalogSchemaFunctionName(catalog, sqlPathElement.getSchema().getCanonicalValue(), functionName)); + } + return names.build(); + } + private Optional matchFunctionExact(Session session, List candidates, List actualParameters) { return matchFunction(session, candidates, actualParameters, false); diff --git a/core/trino-main/src/main/java/io/trino/metadata/GlobalFunctionCatalog.java b/core/trino-main/src/main/java/io/trino/metadata/GlobalFunctionCatalog.java index 76d91227042b..526555034fb3 100644 --- a/core/trino-main/src/main/java/io/trino/metadata/GlobalFunctionCatalog.java +++ b/core/trino-main/src/main/java/io/trino/metadata/GlobalFunctionCatalog.java @@ -22,7 +22,6 @@ import io.trino.spi.function.InvocationConvention; import io.trino.spi.function.OperatorType; import io.trino.spi.type.TypeSignature; -import io.trino.sql.tree.QualifiedName; import javax.annotation.concurrent.ThreadSafe; @@ -41,10 +40,13 @@ import static io.trino.spi.type.BigintType.BIGINT; import static io.trino.spi.type.BooleanType.BOOLEAN; import static io.trino.spi.type.IntegerType.INTEGER; +import static java.util.Locale.ENGLISH; @ThreadSafe public class GlobalFunctionCatalog { + public static final String GLOBAL_CATALOG = "system"; + public static final String GLOBAL_SCHEMA = "global"; private volatile FunctionMap functions = new FunctionMap(); public final synchronized void addFunctions(FunctionBundle functionBundle) @@ -115,9 +117,12 @@ public List listFunctions() return functions.list(); } - public Collection getFunctions(QualifiedName name) + public Collection getFunctions(SchemaFunctionName name) { - return functions.get(name); + if (!GLOBAL_SCHEMA.equals(name.getSchemaName())) { + return ImmutableList.of(); + } + return functions.get(name.getFunctionName()); } public FunctionMetadata getFunctionMetadata(FunctionId functionId) @@ -158,13 +163,14 @@ private static class FunctionMap { private final Map functionBundlesById; private final Map functionsById; - private final Multimap functionsByName; + // function names are currently lower cased + private final Multimap functionsByLowerCaseName; public FunctionMap() { functionBundlesById = ImmutableMap.of(); functionsById = ImmutableMap.of(); - functionsByName = ImmutableListMultimap.of(); + functionsByLowerCaseName = ImmutableListMultimap.of(); } public FunctionMap(FunctionMap map, FunctionBundle functionBundle) @@ -181,14 +187,14 @@ public FunctionMap(FunctionMap map, FunctionBundle functionBundle) .collect(toImmutableMap(FunctionMetadata::getFunctionId, Function.identity()))) .buildOrThrow(); - ImmutableListMultimap.Builder functionsByName = ImmutableListMultimap.builder() - .putAll(map.functionsByName); + ImmutableListMultimap.Builder functionsByName = ImmutableListMultimap.builder() + .putAll(map.functionsByLowerCaseName); functionBundle.getFunctions() - .forEach(functionMetadata -> functionsByName.put(QualifiedName.of(functionMetadata.getSignature().getName()), functionMetadata)); - this.functionsByName = functionsByName.build(); + .forEach(functionMetadata -> functionsByName.put(functionMetadata.getSignature().getName().toLowerCase(ENGLISH), functionMetadata)); + this.functionsByLowerCaseName = functionsByName.build(); // Make sure all functions with the same name are aggregations or none of them are - for (Map.Entry> entry : this.functionsByName.asMap().entrySet()) { + for (Map.Entry> entry : this.functionsByLowerCaseName.asMap().entrySet()) { Collection values = entry.getValue(); long aggregations = values.stream() .map(FunctionMetadata::getKind) @@ -200,12 +206,12 @@ public FunctionMap(FunctionMap map, FunctionBundle functionBundle) public List list() { - return ImmutableList.copyOf(functionsByName.values()); + return ImmutableList.copyOf(functionsByLowerCaseName.values()); } - public Collection get(QualifiedName name) + public Collection get(String functionName) { - return functionsByName.get(name); + return functionsByLowerCaseName.get(functionName.toLowerCase(ENGLISH)); } public FunctionMetadata get(FunctionId functionId) diff --git a/core/trino-main/src/main/java/io/trino/metadata/MetadataManager.java b/core/trino-main/src/main/java/io/trino/metadata/MetadataManager.java index 383029f0e95c..64a8c05b9f1d 100644 --- a/core/trino-main/src/main/java/io/trino/metadata/MetadataManager.java +++ b/core/trino-main/src/main/java/io/trino/metadata/MetadataManager.java @@ -133,7 +133,7 @@ import static io.trino.client.NodeVersion.UNKNOWN; import static io.trino.collect.cache.CacheUtils.uncheckedCacheGet; import static io.trino.collect.cache.SafeCaches.buildNonEvictableCache; -import static io.trino.metadata.FunctionKind.AGGREGATE; +import static io.trino.metadata.GlobalFunctionCatalog.GLOBAL_CATALOG; import static io.trino.metadata.QualifiedObjectName.convertFromSchemaTableName; import static io.trino.metadata.RedirectionAwareTableHandle.noRedirection; import static io.trino.metadata.RedirectionAwareTableHandle.withRedirectionTo; @@ -2063,7 +2063,7 @@ public ResolvedFunction resolveOperator(Session session, OperatorType operatorTy private ResolvedFunction resolvedFunctionInternal(Session session, QualifiedName name, List parameterTypes) { return functionDecoder.fromQualifiedName(name) - .orElseGet(() -> resolve(session, functionResolver.resolveFunction(session, functions.getFunctions(name), name, parameterTypes))); + .orElseGet(() -> resolve(session, functionResolver.resolveFunction(session, name, parameterTypes, this::getFunctions))); } @Override @@ -2076,12 +2076,13 @@ public ResolvedFunction getCoercion(Session session, OperatorType operatorType, String name = mangleOperatorName(operatorType); FunctionBinding functionBinding = functionResolver.resolveCoercion( session, - functions.getFunctions(QualifiedName.of(name)), + QualifiedName.of(name), Signature.builder() .name(name) .returnType(toType) .argumentType(fromType) - .build()); + .build(), + this::getFunctions); return resolve(session, functionBinding); }); } @@ -2102,12 +2103,13 @@ public ResolvedFunction getCoercion(Session session, QualifiedName name, Type fr { FunctionBinding functionBinding = functionResolver.resolveCoercion( session, - functions.getFunctions(name), + name, Signature.builder() .name(name.getSuffix()) .returnType(toType) .argumentType(fromType) - .build()); + .build(), + this::getFunctions); return resolve(session, functionBinding); } @@ -2188,9 +2190,15 @@ public ResolvedFunction resolve(Session session, FunctionBinding functionBinding @Override public boolean isAggregationFunction(Session session, QualifiedName name) { - return functions.getFunctions(name).stream() - .map(FunctionMetadata::getKind) - .anyMatch(AGGREGATE::equals); + return functionResolver.isAggregationFunction(session, name, this::getFunctions); + } + + private Collection getFunctions(CatalogSchemaFunctionName name) + { + if (name.getCatalogName().equals(GLOBAL_CATALOG)) { + return functions.getFunctions(name.getSchemaFunctionName()); + } + return ImmutableList.of(); } @Override diff --git a/core/trino-main/src/main/java/io/trino/metadata/SchemaFunctionName.java b/core/trino-main/src/main/java/io/trino/metadata/SchemaFunctionName.java new file mode 100644 index 000000000000..409d3acf124f --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/metadata/SchemaFunctionName.java @@ -0,0 +1,69 @@ +/* + * 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 io.trino.metadata; + +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; + +public final class SchemaFunctionName +{ + private final String schemaName; + private final String functionName; + + public SchemaFunctionName(String schemaName, String functionName) + { + this.schemaName = requireNonNull(schemaName, "schemaName is null"); + checkArgument(!schemaName.isEmpty(), "schemaName is empty"); + this.functionName = requireNonNull(functionName, "functionName is null"); + checkArgument(!functionName.isEmpty(), "functionName is empty"); + } + + public String getSchemaName() + { + return schemaName; + } + + public String getFunctionName() + { + return functionName; + } + + @Override + public int hashCode() + { + return Objects.hash(schemaName, functionName); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + SchemaFunctionName other = (SchemaFunctionName) obj; + return Objects.equals(this.schemaName, other.schemaName) && + Objects.equals(this.functionName, other.functionName); + } + + @Override + public String toString() + { + return schemaName + '.' + functionName; + } +} diff --git a/core/trino-main/src/main/java/io/trino/sql/SqlPath.java b/core/trino-main/src/main/java/io/trino/sql/SqlPath.java index 06d579f106ee..8ef62d1b574d 100644 --- a/core/trino-main/src/main/java/io/trino/sql/SqlPath.java +++ b/core/trino-main/src/main/java/io/trino/sql/SqlPath.java @@ -16,6 +16,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; import io.trino.sql.parser.SqlParser; import io.trino.sql.tree.PathElement; @@ -43,6 +44,9 @@ public SqlPath(@JsonProperty("rawPath") Optional path) { requireNonNull(path, "path is null"); this.rawPath = path; + if (rawPath.isEmpty()) { + parsedPath = ImmutableList.of(); + } } @JsonProperty diff --git a/core/trino-main/src/main/java/io/trino/testing/TestingSession.java b/core/trino-main/src/main/java/io/trino/testing/TestingSession.java index b68a13946569..b02d27bd5907 100644 --- a/core/trino-main/src/main/java/io/trino/testing/TestingSession.java +++ b/core/trino-main/src/main/java/io/trino/testing/TestingSession.java @@ -19,9 +19,6 @@ import io.trino.metadata.SessionPropertyManager; import io.trino.spi.security.Identity; import io.trino.spi.type.TimeZoneKey; -import io.trino.sql.SqlPath; - -import java.util.Optional; import static java.util.Locale.ENGLISH; @@ -55,7 +52,6 @@ public static SessionBuilder testSessionBuilder(SessionPropertyManager sessionPr .setSource("test") .setCatalog("catalog") .setSchema("schema") - .setPath(new SqlPath(Optional.of("path"))) .setTimeZoneKey(DEFAULT_TIME_ZONE_KEY) .setLocale(ENGLISH) .setRemoteUserAddress("address")