This is an automated email from the ASF dual-hosted git repository.
mbudiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git
The following commit(s) were added to refs/heads/main by this push:
new 75d35f3b90 [CALCITE-7415] CalciteCatalogReader.lookupOperatorOverloads
keeps original function identifier casing instead of resolved schema-path casing
75d35f3b90 is described below
commit 75d35f3b9032e8e13304a10ffe8f7ab08a62a9bd
Author: zzwqqq <[email protected]>
AuthorDate: Tue Feb 17 22:36:06 2026 +0800
[CALCITE-7415] CalciteCatalogReader.lookupOperatorOverloads keeps original
function identifier casing instead of resolved schema-path casing
---
.../calcite/prepare/CalciteCatalogReader.java | 66 +++++++--
.../prepare/LookupOperatorOverloadsTest.java | 158 +++++++++++++++++++++
2 files changed, 215 insertions(+), 9 deletions(-)
diff --git
a/core/src/main/java/org/apache/calcite/prepare/CalciteCatalogReader.java
b/core/src/main/java/org/apache/calcite/prepare/CalciteCatalogReader.java
index 38f6f8e778..df5e7ade81 100644
--- a/core/src/main/java/org/apache/calcite/prepare/CalciteCatalogReader.java
+++ b/core/src/main/java/org/apache/calcite/prepare/CalciteCatalogReader.java
@@ -62,6 +62,7 @@
import org.apache.calcite.sql.validate.SqlUserDefinedTableMacro;
import org.apache.calcite.sql.validate.SqlValidatorUtil;
import org.apache.calcite.util.Optionality;
+import org.apache.calcite.util.Pair;
import org.apache.calcite.util.Util;
import com.google.common.collect.ImmutableList;
@@ -144,9 +145,10 @@ protected CalciteCatalogReader(CalciteSchema rootSchema,
return config;
}
- private Collection<org.apache.calcite.schema.Function> getFunctionsFrom(
- List<String> names) {
- final List<org.apache.calcite.schema.Function> functions2 =
+ private Collection<Pair<SqlIdentifier, org.apache.calcite.schema.Function>>
getFunctionsFrom(
+ SqlIdentifier identifier) {
+ final List<String> names = identifier.names;
+ final List<Pair<SqlIdentifier, org.apache.calcite.schema.Function>>
functions2 =
new ArrayList<>();
final List<List<String>> schemaNameList = new ArrayList<>();
if (names.size() > 1) {
@@ -171,14 +173,60 @@ private Collection<org.apache.calcite.schema.Function>
getFunctionsFrom(
SqlValidatorUtil.getSchema(rootSchema,
Iterables.concat(schemaNames, Util.skipLast(names)),
nameMatcher);
if (schema != null) {
- final String name = Util.last(names);
- boolean caseSensitive = nameMatcher.isCaseSensitive();
- functions2.addAll(schema.getFunctions(name, caseSensitive));
+ final String functionName = Util.last(names);
+ if (nameMatcher.isCaseSensitive()) {
+ addFunctions(functions2, schema, names,
identifier.getParserPosition(),
+ functionName, true);
+ } else {
+ boolean hasMatchedFunctionName = false;
+ for (String candidateFunctionName : schema.getFunctionNames()) {
+ if (nameMatcher.matches(functionName, candidateFunctionName)) {
+ hasMatchedFunctionName = true;
+ // candidateFunctionName already has canonical case from schema.
+ // Use case-sensitive lookup to bind each function to that exact
name.
+ addFunctions(functions2, schema, names,
identifier.getParserPosition(),
+ candidateFunctionName, true);
+ }
+ }
+ if (!hasMatchedFunctionName) {
+ // Fallback for schemas where getFunctionNames() is incomplete but
+ // getFunctions(name, false) can still resolve functions.
+ addFunctions(functions2, schema, names,
identifier.getParserPosition(),
+ functionName, false);
+ }
+ }
}
}
return functions2;
}
+ private static SqlIdentifier createResolvedIdentifier(CalciteSchema schema,
+ List<String> names, String name, SqlParserPos pos) {
+ final List<String> schemaPath = schema.path(null);
+ // Keep the same qualifier depth as the original call (e.g. schema.func
+ // stays 2-part, catalog.schema.func stays 3-part).
+ final int qualifierCount = names.size() - 1;
+ // Replace only the suffix that corresponds to the resolved schema path.
+ // Any leading qualifiers that are outside this schema path are kept as-is.
+ final int resolvedQualifierCount = Math.min(qualifierCount,
schemaPath.size());
+ final List<String> resolvedNames = new ArrayList<>(names.size());
+ resolvedNames.addAll(names.subList(0, qualifierCount -
resolvedQualifierCount));
+ resolvedNames.addAll(
+ schemaPath.subList(schemaPath.size() - resolvedQualifierCount,
schemaPath.size()));
+ resolvedNames.add(name);
+ return new SqlIdentifier(resolvedNames, pos);
+ }
+
+ private static void addFunctions(
+ List<Pair<SqlIdentifier, org.apache.calcite.schema.Function>> functions,
+ CalciteSchema schema, List<String> names, SqlParserPos pos, String
functionName,
+ boolean caseSensitive) {
+ final SqlIdentifier functionIdentifier =
+ createResolvedIdentifier(schema, names, functionName, pos);
+ schema.getFunctions(functionName, caseSensitive).forEach(function ->
+ functions.add(Pair.of(functionIdentifier, function)));
+ }
+
@Override public @Nullable RelDataType getNamedType(SqlIdentifier typeName) {
CalciteSchema.TypeEntry typeEntry =
SqlValidatorUtil.getTypeEntry(getRootSchema(), typeName);
if (typeEntry != null) {
@@ -274,10 +322,10 @@ private static SqlMonikerImpl moniker(CalciteSchema
schema, @Nullable String nam
!(function instanceof TableMacro
|| function instanceof TableFunction);
}
- getFunctionsFrom(opName.names)
+ getFunctionsFrom(opName)
.stream()
- .filter(predicate)
- .map(function -> toOp(opName, function, config))
+ .filter(pair -> predicate.test(pair.right))
+ .map(pair -> toOp(pair.left, pair.right, config))
.forEachOrdered(operatorList::add);
}
diff --git
a/core/src/test/java/org/apache/calcite/prepare/LookupOperatorOverloadsTest.java
b/core/src/test/java/org/apache/calcite/prepare/LookupOperatorOverloadsTest.java
index cd340315fe..a05dbfc826 100644
---
a/core/src/test/java/org/apache/calcite/prepare/LookupOperatorOverloadsTest.java
+++
b/core/src/test/java/org/apache/calcite/prepare/LookupOperatorOverloadsTest.java
@@ -17,8 +17,13 @@
package org.apache.calcite.prepare;
import org.apache.calcite.adapter.java.JavaTypeFactory;
+import org.apache.calcite.config.CalciteConnectionConfigImpl;
+import org.apache.calcite.config.CalciteConnectionProperty;
import org.apache.calcite.jdbc.CalciteConnection;
import org.apache.calcite.jdbc.CalcitePrepare;
+import org.apache.calcite.jdbc.CalciteSchema;
+import org.apache.calcite.jdbc.JavaTypeFactoryImpl;
+import org.apache.calcite.schema.Function;
import org.apache.calcite.schema.SchemaPlus;
import org.apache.calcite.schema.TableFunction;
import org.apache.calcite.schema.impl.AbstractSchema;
@@ -35,7 +40,9 @@
import org.apache.calcite.util.Smalls;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.junit.jupiter.api.Test;
@@ -45,6 +52,8 @@
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
+import java.util.Locale;
+import java.util.Properties;
import static org.apache.calcite.sql.SqlFunctionCategory.MATCH_RECOGNIZE;
import static
org.apache.calcite.sql.SqlFunctionCategory.USER_DEFINED_CONSTRUCTOR;
@@ -58,6 +67,7 @@
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasSize;
import static java.util.Objects.requireNonNull;
@@ -133,6 +143,154 @@ private static void check(List<SqlFunctionCategory>
actuals,
checkInternal(false);
}
+ // Look up MyCatalog.MySchema.MyFUNC using a lowercase 3-part identifier.
+ @Test void testLookupQualifiedNameUsesResolvedCase() {
+ final String catalogName = "MyCatalog";
+ final String schemaName = "MySchema";
+ final String funcName = "MyFUNC";
+ final CalciteSchema root =
+ CalciteSchema.createRootSchema(false, false, catalogName);
+ final CalciteSchema schema = root.add(schemaName, new AbstractSchema());
+ final TableFunction table =
+ requireNonNull(TableFunctionImpl.create(Smalls.MAZE_METHOD));
+ schema.plus().add(funcName, table);
+
+ final JavaTypeFactory typeFactory = new JavaTypeFactoryImpl();
+ final Properties properties = new Properties();
+
properties.setProperty(CalciteConnectionProperty.CASE_SENSITIVE.camelName(),
"false");
+ final CalciteCatalogReader reader =
+ new CalciteCatalogReader(root, ImmutableList.of(), typeFactory,
+ new CalciteConnectionConfigImpl(properties));
+
+ final List<SqlOperator> operatorList = new ArrayList<>();
+ final SqlIdentifier lowercaseIdentifier =
+ new SqlIdentifier(
+ Lists.newArrayList(catalogName.toLowerCase(Locale.ROOT),
+ schemaName.toLowerCase(Locale.ROOT),
funcName.toLowerCase(Locale.ROOT)),
+ null, SqlParserPos.ZERO, null);
+ reader.lookupOperatorOverloads(lowercaseIdentifier,
+ SqlFunctionCategory.USER_DEFINED_TABLE_FUNCTION, SqlSyntax.FUNCTION,
+ operatorList, SqlNameMatchers.withCaseSensitive(false));
+
+ checkFunctionType(1, funcName, operatorList);
+ assertThat(operatorList.get(0).getNameAsId().names,
+ isListOf(catalogName, schemaName, funcName));
+ }
+
+ // Look up MySchema.MyFUNC using a lowercase 2-part identifier.
+ @Test void testLookupPartiallyQualifiedNameUsesResolvedCase() {
+ final String catalogName = "MyCatalog";
+ final String schemaName = "MySchema";
+ final String funcName = "MyFUNC";
+ final CalciteSchema root =
+ CalciteSchema.createRootSchema(false, false, catalogName);
+ final CalciteSchema schema = root.add(schemaName, new AbstractSchema());
+ final TableFunction table =
+ requireNonNull(TableFunctionImpl.create(Smalls.MAZE_METHOD));
+ schema.plus().add(funcName, table);
+
+ final JavaTypeFactory typeFactory = new JavaTypeFactoryImpl();
+ final Properties properties = new Properties();
+
properties.setProperty(CalciteConnectionProperty.CASE_SENSITIVE.camelName(),
"false");
+ final CalciteCatalogReader reader =
+ new CalciteCatalogReader(root, ImmutableList.of(), typeFactory,
+ new CalciteConnectionConfigImpl(properties));
+
+ final List<SqlOperator> operatorList = new ArrayList<>();
+ final SqlIdentifier lowercaseIdentifier =
+ new SqlIdentifier(
+ Lists.newArrayList(schemaName.toLowerCase(Locale.ROOT),
+ funcName.toLowerCase(Locale.ROOT)),
+ null, SqlParserPos.ZERO, null);
+ reader.lookupOperatorOverloads(lowercaseIdentifier,
+ SqlFunctionCategory.USER_DEFINED_TABLE_FUNCTION, SqlSyntax.FUNCTION,
+ operatorList, SqlNameMatchers.withCaseSensitive(false));
+
+ checkFunctionType(1, funcName, operatorList);
+ assertThat(operatorList.get(0).getNameAsId().names,
+ isListOf(schemaName, funcName));
+ }
+
+ // Look up myfunc when both MyFUNC and myfunc exist in the same schema.
+ @Test void testLookupCaseInsensitiveUsesEachMatchedFunctionName() {
+ final String schemaName = "MySchema";
+ final String upperFuncName = "MyFUNC";
+ final String lowerFuncName = "myfunc";
+ final CalciteSchema root = CalciteSchema.createRootSchema(false, true);
+ final CalciteSchema schema = root.add(schemaName, new AbstractSchema());
+ final TableFunction table =
+ requireNonNull(TableFunctionImpl.create(Smalls.MAZE_METHOD));
+ schema.plus().add(upperFuncName, table);
+ schema.plus().add(lowerFuncName, table);
+
+ final JavaTypeFactory typeFactory = new JavaTypeFactoryImpl();
+ final Properties properties = new Properties();
+
properties.setProperty(CalciteConnectionProperty.CASE_SENSITIVE.camelName(),
"false");
+ final CalciteCatalogReader reader =
+ new CalciteCatalogReader(root, ImmutableList.of(), typeFactory,
+ new CalciteConnectionConfigImpl(properties));
+
+ final List<SqlOperator> operatorList = new ArrayList<>();
+ final SqlIdentifier lowercaseIdentifier =
+ new SqlIdentifier(
+ Lists.newArrayList(schemaName.toLowerCase(Locale.ROOT),
+ lowerFuncName),
+ null, SqlParserPos.ZERO, null);
+ reader.lookupOperatorOverloads(lowercaseIdentifier,
+ SqlFunctionCategory.USER_DEFINED_TABLE_FUNCTION, SqlSyntax.FUNCTION,
+ operatorList, SqlNameMatchers.withCaseSensitive(false));
+
+ assertThat(operatorList, hasSize(2));
+ boolean hasUpperName = false;
+ boolean hasLowerName = false;
+ for (SqlOperator operator : operatorList) {
+ if (operator.getNameAsId().names.equals(Lists.newArrayList(schemaName,
upperFuncName))) {
+ hasUpperName = true;
+ }
+ if (operator.getNameAsId().names.equals(Lists.newArrayList(schemaName,
lowerFuncName))) {
+ hasLowerName = true;
+ }
+ }
+ assertThat(hasUpperName, is(true));
+ assertThat(hasLowerName, is(true));
+ }
+
+ // Example: lookup "myschema.myfunc" against a dynamic schema and resolve it
+ // to "MySchema.MyFUNC" (fresh function instances on each lookup).
+ @Test void testLookupImplicitFunctionUsesResolvedCase() {
+ final String schemaName = "MySchema";
+ final String funcName = "MyFUNC";
+ final CalciteSchema root = CalciteSchema.createRootSchema(false, true);
+ root.add(schemaName, new AbstractSchema() {
+ @Override protected Multimap<String, Function> getFunctionMultimap() {
+ // Return fresh instances to mimic dynamic schemas that do not preserve
+ // function identity across lookups.
+ return ImmutableMultimap.of(funcName,
+ requireNonNull(TableFunctionImpl.create(Smalls.MAZE_METHOD)));
+ }
+ });
+
+ final JavaTypeFactory typeFactory = new JavaTypeFactoryImpl();
+ final Properties properties = new Properties();
+
properties.setProperty(CalciteConnectionProperty.CASE_SENSITIVE.camelName(),
"false");
+ final CalciteCatalogReader reader =
+ new CalciteCatalogReader(root, ImmutableList.of(), typeFactory,
+ new CalciteConnectionConfigImpl(properties));
+
+ final List<SqlOperator> operatorList = new ArrayList<>();
+ final SqlIdentifier lowercaseIdentifier =
+ new SqlIdentifier(
+ Lists.newArrayList(schemaName.toLowerCase(Locale.ROOT),
+ funcName.toLowerCase(Locale.ROOT)),
+ null, SqlParserPos.ZERO, null);
+ reader.lookupOperatorOverloads(lowercaseIdentifier,
+ SqlFunctionCategory.USER_DEFINED_TABLE_FUNCTION, SqlSyntax.FUNCTION,
+ operatorList, SqlNameMatchers.withCaseSensitive(false));
+
+ checkFunctionType(1, funcName, operatorList);
+ assertThat(operatorList.get(0).getNameAsId().names, isListOf(schemaName,
funcName));
+ }
+
private void checkInternal(boolean caseSensitive) throws SQLException {
final SqlNameMatcher nameMatcher =
SqlNameMatchers.withCaseSensitive(caseSensitive);