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);

Reply via email to