Repository: apache/flink
Pull request #26543: [FLINK-37779][2/N] extend catalog schema reader to get model
Author: login: lihaosky
Labels: name: component=<none>
Created: 2025-05-09T00:10:36Z | Merged: 2025-05-19T02:08:46Z

## What is the purpose of the change

Support `getModel` in `FlinkCalciteCatalogReader`


## Brief change log

* Introduce `CatalogSchemaModel`
* Add `getModel` in `FlinkSchema`
* Add `getModel` in `FlinkCalciteCatalogReader`
* Copy `SimpleCalciteSchema` from Calcite to support `getModel`


## Verifying this change

Unit test

## Does this pull request potentially affect one of the following parts:

  - Dependencies (does it add or upgrade a dependency): (yes)
  - The public API, i.e., is any changed class annotated with `@Public(Evolving)`: (yes)
  - The serializers: (no)
  - The runtime per-record code paths (performance sensitive): (no)
  - Anything that affects deployment or recovery: JobManager (and its components), Checkpointing, Kubernetes/Yarn, ZooKeeper: (no)
  - The S3 file system connector: (no)

## Documentation

  - Does this pull request introduce a new feature? (yes)
  - If yes, how is the feature documented? (JavaDocs)



Diff:
diff --git a/flink-table/flink-table-planner/src/main/java/org/apache/calcite/jdbc/SimpleCalciteSchema.java b/flink-table/flink-table-planner/src/main/java/org/apache/calcite/jdbc/SimpleCalciteSchema.java
new file mode 100644
index 0000000000000..864d50ef5c9b9
--- /dev/null
+++ b/flink-table/flink-table-planner/src/main/java/org/apache/calcite/jdbc/SimpleCalciteSchema.java
@@ -0,0 +1,305 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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 org.apache.calcite.jdbc;
+
+import org.apache.flink.table.planner.catalog.CatalogSchemaModel;
+import org.apache.flink.table.planner.catalog.FlinkSchema;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSortedMap;
+import com.google.common.collect.ImmutableSortedSet;
+import org.apache.calcite.rel.type.RelProtoDataType;
+import org.apache.calcite.schema.Function;
+import org.apache.calcite.schema.Schema;
+import org.apache.calcite.schema.SchemaVersion;
+import org.apache.calcite.schema.Table;
+import org.apache.calcite.schema.TableMacro;
+import org.apache.calcite.util.NameMap;
+import org.apache.calcite.util.NameMultimap;
+import org.apache.calcite.util.NameSet;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * A concrete implementation of {@link org.apache.calcite.jdbc.CalciteSchema} that maintains minimal
+ * state. This is copied from {@link org.apache.calcite.jdbc.CalciteSchema} calcite and modified to
+ * support getModel operation.
+ */
+public class SimpleCalciteSchema extends CalciteSchema {
+
+    // ----- FLINK MODIFICATION START -----
+    public static class ModelEntry extends Entry {
+        private final CatalogSchemaModel model;
+
+        ModelEntry(CalciteSchema schema, String name, CatalogSchemaModel model) {
+            super(schema, name);
+            this.model = model;
+        }
+
+        public CatalogSchemaModel getModel() {
+            return model;
+        }
+    }
+
+    // ----- FLINK MODIFICATION END -----
+
+    /**
+     * Creates a SimpleCalciteSchema.
+     *
+     * <p>Use {@link CalciteSchema#createRootSchema(boolean)} or {@link #add(String, Schema)}.
+     */
+    SimpleCalciteSchema(@Nullable CalciteSchema parent, Schema schema, String name) {
+        this(parent, schema, name, null, null, null, null, null, null, null, null);
+    }
+
+    private SimpleCalciteSchema(
+            @Nullable CalciteSchema parent,
+            Schema schema,
+            String name,
+            @Nullable NameMap<CalciteSchema> subSchemaMap,
+            @Nullable NameMap<TableEntry> tableMap,
+            @Nullable NameMap<LatticeEntry> latticeMap,
+            @Nullable NameMap<TypeEntry> typeMap,
+            @Nullable NameMultimap<FunctionEntry> functionMap,
+            @Nullable NameSet functionNames,
+            @Nullable NameMap<FunctionEntry> nullaryFunctionMap,
+            @Nullable List<? extends List<String>> path) {
+        super(
+                parent,
+                schema,
+                name,
+                subSchemaMap,
+                tableMap,
+                latticeMap,
+                typeMap,
+                functionMap,
+                functionNames,
+                nullaryFunctionMap,
+                path);
+    }
+
+    @Override
+    public void setCache(boolean cache) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public CalciteSchema add(String name, Schema schema) {
+        final CalciteSchema calciteSchema = new SimpleCalciteSchema(this, schema, name);
+        subSchemaMap.put(name, calciteSchema);
+        return calciteSchema;
+    }
+
+    private static @Nullable String caseInsensitiveLookup(Set<String> candidates, String name) {
+        // Exact string lookup
+        if (candidates.contains(name)) {
+            return name;
+        }
+        // Upper case string lookup
+        final String upperCaseName = name.toUpperCase(Locale.ROOT);
+        if (candidates.contains(upperCaseName)) {
+            return upperCaseName;
+        }
+        // Lower case string lookup
+        final String lowerCaseName = name.toLowerCase(Locale.ROOT);
+        if (candidates.contains(lowerCaseName)) {
+            return lowerCaseName;
+        }
+        // Fall through: Set iteration
+        for (String candidate : candidates) {
+            if (candidate.equalsIgnoreCase(name)) {
+                return candidate;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    protected @Nullable CalciteSchema getImplicitSubSchema(
+            String schemaName, boolean caseSensitive) {
+        // Check implicit schemas.
+        final String schemaName2 =
+                caseSensitive
+                        ? schemaName
+                        : caseInsensitiveLookup(schema.getSubSchemaNames(), schemaName);
+        if (schemaName2 == null) {
+            return null;
+        }
+        final Schema s = schema.getSubSchema(schemaName2);
+        if (s == null) {
+            return null;
+        }
+        return new SimpleCalciteSchema(this, s, schemaName2);
+    }
+
+    @Override
+    protected @Nullable TableEntry getImplicitTable(String tableName, boolean caseSensitive) {
+        // Check implicit tables.
+        final String tableName2 =
+                caseSensitive
+                        ? tableName
+                        : caseInsensitiveLookup(schema.getTableNames(), tableName);
+        if (tableName2 == null) {
+            return null;
+        }
+        final Table table = schema.getTable(tableName2);
+        if (table == null) {
+            return null;
+        }
+        return tableEntry(tableName2, table);
+    }
+
+    // ----- FLINK MODIFICATION START -----
+    public @Nullable ModelEntry getModel(String modelName, boolean caseSensitive) {
+        FlinkSchema flinkSchema = (FlinkSchema) schema;
+        final String modelName2 =
+                caseSensitive
+                        ? modelName
+                        : caseInsensitiveLookup(flinkSchema.getModelNames(), modelName);
+        if (modelName2 == null) {
+            return null;
+        }
+        final CatalogSchemaModel model = flinkSchema.getModel(modelName2);
+        if (model == null) {
+            return null;
+        }
+        return new ModelEntry(this, modelName2, model);
+    }
+
+    // ----- FLINK MODIFICATION END -----
+
+    @Override
+    protected @Nullable TypeEntry getImplicitType(String name, boolean caseSensitive) {
+        // Check implicit types.
+        final String name2 =
+                caseSensitive ? name : caseInsensitiveLookup(schema.getTypeNames(), name);
+        if (name2 == null) {
+            return null;
+        }
+        final RelProtoDataType type = schema.getType(name2);
+        if (type == null) {
+            return null;
+        }
+        return typeEntry(name2, type);
+    }
+
+    @Override
+    protected void addImplicitSubSchemaToBuilder(
+            ImmutableSortedMap.Builder<String, CalciteSchema> builder) {
+        ImmutableSortedMap<String, CalciteSchema> explicitSubSchemas = builder.build();
+        for (String schemaName : schema.getSubSchemaNames()) {
+            if (explicitSubSchemas.containsKey(schemaName)) {
+                // explicit subschema wins.
+                continue;
+            }
+            Schema s = schema.getSubSchema(schemaName);
+            if (s != null) {
+                CalciteSchema calciteSchema = new SimpleCalciteSchema(this, s, schemaName);
+                builder.put(schemaName, calciteSchema);
+            }
+        }
+    }
+
+    @Override
+    protected void addImplicitTableToBuilder(ImmutableSortedSet.Builder<String> builder) {
+        builder.addAll(schema.getTableNames());
+    }
+
+    @Override
+    protected void addImplicitFunctionsToBuilder(
+            ImmutableList.Builder<Function> builder, String name, boolean caseSensitive) {
+        Collection<Function> functions = schema.getFunctions(name);
+        if (functions != null) {
+            builder.addAll(functions);
+        }
+    }
+
+    @Override
+    protected void addImplicitFuncNamesToBuilder(ImmutableSortedSet.Builder<String> builder) {
+        builder.addAll(schema.getFunctionNames());
+    }
+
+    @Override
+    protected void addImplicitTypeNamesToBuilder(ImmutableSortedSet.Builder<String> builder) {
+        builder.addAll(schema.getTypeNames());
+    }
+
+    @Override
+    protected void addImplicitTablesBasedOnNullaryFunctionsToBuilder(
+            ImmutableSortedMap.Builder<String, Table> builder) {
+        ImmutableSortedMap<String, Table> explicitTables = builder.build();
+
+        for (String s : schema.getFunctionNames()) {
+            // explicit table wins.
+            if (explicitTables.containsKey(s)) {
+                continue;
+            }
+            for (Function function : schema.getFunctions(s)) {
+                if (function instanceof TableMacro && function.getParameters().isEmpty()) {
+                    final Table table = ((TableMacro) function).apply(ImmutableList.of());
+                    builder.put(s, table);
+                }
+            }
+        }
+    }
+
+    @Override
+    protected @Nullable TableEntry getImplicitTableBasedOnNullaryFunction(
+            String tableName, boolean caseSensitive) {
+        Collection<Function> functions = schema.getFunctions(tableName);
+        if (functions != null) {
+            for (Function function : functions) {
+                if (function instanceof TableMacro && function.getParameters().isEmpty()) {
+                    final Table table = ((TableMacro) function).apply(ImmutableList.of());
+                    return tableEntry(tableName, table);
+                }
+            }
+        }
+        return null;
+    }
+
+    @Override
+    protected CalciteSchema snapshot(@Nullable CalciteSchema parent, SchemaVersion version) {
+        CalciteSchema snapshot =
+                new SimpleCalciteSchema(
+                        parent,
+                        schema.snapshot(version),
+                        name,
+                        null,
+                        tableMap,
+                        latticeMap,
+                        typeMap,
+                        functionMap,
+                        functionNames,
+                        nullaryFunctionMap,
+                        getPath());
+        for (CalciteSchema subSchema : subSchemaMap.map().values()) {
+            CalciteSchema subSchemaSnapshot = subSchema.snapshot(snapshot, version);
+            snapshot.subSchemaMap.put(subSchema.name, subSchemaSnapshot);
+        }
+        return snapshot;
+    }
+
+    @Override
+    protected boolean isCacheEnabled() {
+        return false;
+    }
+}
diff --git a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/catalog/CatalogCalciteSchema.java b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/catalog/CatalogCalciteSchema.java
index 23f7b3ca836ea..4da4f0a0f40c2 100644
--- a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/catalog/CatalogCalciteSchema.java
+++ b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/catalog/CatalogCalciteSchema.java
@@ -97,6 +97,11 @@ public boolean isMutable() {
         return true;
     }
 
+    @Override
+    public CatalogSchemaModel getModel(String name) {
+        return null;
+    }
+
     @Override
     public FlinkSchema copy() {
         return new CatalogCalciteSchema(catalogName, catalogManager, isStreamingMode);
diff --git a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/catalog/CatalogManagerCalciteSchema.java b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/catalog/CatalogManagerCalciteSchema.java
index 118446cb5d6f2..c27b978851e3a 100644
--- a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/catalog/CatalogManagerCalciteSchema.java
+++ b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/catalog/CatalogManagerCalciteSchema.java
@@ -90,6 +90,11 @@ public boolean isMutable() {
         return false;
     }
 
+    @Override
+    public CatalogSchemaModel getModel(String name) {
+        return null;
+    }
+
     @Override
     public FlinkSchema copy() {
         return new CatalogManagerCalciteSchema(catalogManager, isStreamingMode);
diff --git a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/catalog/CatalogSchemaModel.java b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/catalog/CatalogSchemaModel.java
new file mode 100644
index 0000000000000..c26f3a162243d
--- /dev/null
+++ b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/catalog/CatalogSchemaModel.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.flink.table.planner.catalog;
+
+import org.apache.flink.table.catalog.ContextResolvedModel;
+import org.apache.flink.table.catalog.ResolvedSchema;
+import org.apache.flink.table.planner.calcite.FlinkTypeFactory;
+import org.apache.flink.table.runtime.types.PlannerTypeUtils;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.LogicalType;
+
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Represents a wrapper for {@link ContextResolvedModel} in {@link
+ * org.apache.calcite.schema.Schema}.
+ */
+public class CatalogSchemaModel {
+    // ~ Instance fields --------------------------------------------------------
+
+    private final ContextResolvedModel contextResolvedModel;
+
+    // ~ Constructors -----------------------------------------------------------
+
+    /**
+     * Create a CatalogSchemaModel instance.
+     *
+     * @param contextResolvedModel A result of catalog lookup
+     */
+    public CatalogSchemaModel(ContextResolvedModel contextResolvedModel) {
+        this.contextResolvedModel = contextResolvedModel;
+    }
+
+    // ~ Methods ----------------------------------------------------------------
+
+    public ContextResolvedModel getContextResolvedModel() {
+        return contextResolvedModel;
+    }
+
+    public boolean isTemporary() {
+        return contextResolvedModel.isTemporary();
+    }
+
+    public RelDataType getInputRowType(RelDataTypeFactory typeFactory) {
+        final FlinkTypeFactory flinkTypeFactory = (FlinkTypeFactory) typeFactory;
+        final ResolvedSchema schema =
+                contextResolvedModel.getResolvedModel().getResolvedInputSchema();
+        return schemaToRelDataType(flinkTypeFactory, schema);
+    }
+
+    public RelDataType getOutputRowType(RelDataTypeFactory typeFactory) {
+        final FlinkTypeFactory flinkTypeFactory = (FlinkTypeFactory) typeFactory;
+        final ResolvedSchema schema =
+                contextResolvedModel.getResolvedModel().getResolvedOutputSchema();
+        return schemaToRelDataType(flinkTypeFactory, schema);
+    }
+
+    private static RelDataType schemaToRelDataType(
+            FlinkTypeFactory typeFactory, ResolvedSchema schema) {
+        final List<String> fieldNames = schema.getColumnNames();
+        final List<LogicalType> fieldTypes =
+                schema.getColumnDataTypes().stream()
+                        .map(DataType::getLogicalType)
+                        .map(PlannerTypeUtils::removeLegacyTypes)
+                        .collect(Collectors.toList());
+        return typeFactory.buildRelNodeRowType(fieldNames, fieldTypes);
+    }
+}
diff --git a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/catalog/DatabaseCalciteSchema.java b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/catalog/DatabaseCalciteSchema.java
index d3e738ae5ff27..2f10ae539f34d 100644
--- a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/catalog/DatabaseCalciteSchema.java
+++ b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/catalog/DatabaseCalciteSchema.java
@@ -68,6 +68,13 @@ public DatabaseCalciteSchema(
         this.isStreamingMode = isStreamingMode;
     }
 
+    @Override
+    public CatalogSchemaModel getModel(String modelName) {
+        final ObjectIdentifier identifier =
+                ObjectIdentifier.of(catalogName, databaseName, modelName);
+        return catalogManager.getModel(identifier).map(CatalogSchemaModel::new).orElse(null);
+    }
+
     @Override
     public Table getTable(String tableName) {
         final ObjectIdentifier identifier =
@@ -135,6 +142,11 @@ private TableStats extractTableStats(
         }
     }
 
+    @Override
+    public Set<String> getModelNames() {
+        return catalogManager.listModels(catalogName, databaseName);
+    }
+
     @Override
     public Set<String> getTableNames() {
         return catalogManager.listTables(catalogName, databaseName);
diff --git a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/catalog/FlinkSchema.java b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/catalog/FlinkSchema.java
index 4f982abc1892e..84824d0e1b6a6 100644
--- a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/catalog/FlinkSchema.java
+++ b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/catalog/FlinkSchema.java
@@ -55,6 +55,12 @@ public Set<String> getFunctionNames() {
         return Collections.emptySet();
     }
 
+    public abstract CatalogSchemaModel getModel(String name);
+
+    public Set<String> getModelNames() {
+        return Collections.emptySet();
+    }
+
     @Override
     public Schema snapshot(SchemaVersion version) {
         FlinkSchema schema = this.copy();
diff --git a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/FlinkCalciteCatalogReader.java b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/FlinkCalciteCatalogReader.java
index 88e8b080a1c2a..1502f073ac062 100644
--- a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/FlinkCalciteCatalogReader.java
+++ b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/FlinkCalciteCatalogReader.java
@@ -37,6 +37,7 @@
 import org.apache.flink.table.legacy.sources.LookupableTableSource;
 import org.apache.flink.table.legacy.sources.TableSource;
 import org.apache.flink.table.planner.calcite.FlinkSqlNameMatcher;
+import org.apache.flink.table.planner.catalog.CatalogSchemaModel;
 import org.apache.flink.table.planner.catalog.CatalogSchemaTable;
 import org.apache.flink.table.planner.catalog.QueryOperationCatalogViewTable;
 import org.apache.flink.table.planner.catalog.SqlCatalogViewTable;
@@ -47,14 +48,21 @@
 import org.apache.flink.table.planner.plan.stats.FlinkStatistic;
 import org.apache.flink.table.utils.TableSchemaUtils;
 
+import org.apache.flink.shaded.guava33.com.google.common.collect.Iterables;
+
 import org.apache.calcite.config.CalciteConnectionConfig;
 import org.apache.calcite.jdbc.CalciteSchema;
+import org.apache.calcite.jdbc.SimpleCalciteSchema;
 import org.apache.calcite.plan.RelOptSchema;
 import org.apache.calcite.prepare.CalciteCatalogReader;
 import org.apache.calcite.prepare.Prepare;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.calcite.sql.validate.SqlNameMatchers;
+import org.apache.calcite.sql.validate.SqlValidatorUtil;
+import org.apache.calcite.util.Util;
+
+import javax.annotation.Nullable;
 
 import java.util.Collections;
 import java.util.List;
@@ -85,7 +93,7 @@ public FlinkCalciteCatalogReader(
     }
 
     @Override
-    public Prepare.PreparingTable getTable(List<String> names) {
+    public @Nullable Prepare.PreparingTable getTable(List<String> names) {
         Prepare.PreparingTable originRelOptTable = super.getTable(names);
         if (originRelOptTable == null) {
             return null;
@@ -104,6 +112,31 @@ public Prepare.PreparingTable getTable(List<String> names) {
         }
     }
 
+    public @Nullable CatalogSchemaModel getModel(List<String> names) {
+        // ----- COPIED PARTIALLY FROM CalciteCatalogReader#getFunctionsFrom -----
+        for (List<String> schemaNames : getSchemaPaths()) {
+            CalciteSchema calciteSchema =
+                    SqlValidatorUtil.getSchema(
+                            rootSchema,
+                            Iterables.concat(schemaNames, Util.skipLast(names)),
+                            nameMatcher);
+
+            if (calciteSchema != null) {
+                final String name = Util.last(names);
+                boolean caseSensitive = nameMatcher.isCaseSensitive();
+                // ----- FLINK MODIFICATION START -----
+                SimpleCalciteSchema.ModelEntry modelEntry =
+                        ((SimpleCalciteSchema) calciteSchema).getModel(name, caseSensitive);
+                if (modelEntry != null) {
+                    return modelEntry.getModel();
+                }
+                // ----- FLINK MODIFICATION END -----
+            }
+        }
+        // ----- COPY END -----
+        return null;
+    }
+
     /** Translate this {@link CatalogSchemaTable} into Flink source table. */
     private static FlinkPreparingTableBase toPreparingTable(
             RelOptSchema relOptSchema,
diff --git a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/catalog/DatabaseCalciteSchemaTest.java b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/catalog/DatabaseCalciteSchemaTest.java
index 63f7f3db0e9c8..59c57bee5c0ba 100644
--- a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/catalog/DatabaseCalciteSchemaTest.java
+++ b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/catalog/DatabaseCalciteSchemaTest.java
@@ -22,12 +22,14 @@
 import org.apache.flink.table.api.Schema;
 import org.apache.flink.table.catalog.CatalogBaseTable;
 import org.apache.flink.table.catalog.CatalogManager;
+import org.apache.flink.table.catalog.CatalogModel;
 import org.apache.flink.table.catalog.CatalogTable;
 import org.apache.flink.table.catalog.ObjectIdentifier;
 import org.apache.flink.table.utils.CatalogManagerMocks;
 
 import org.apache.calcite.schema.Table;
-import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
 import static org.apache.flink.table.utils.CatalogManagerMocks.DEFAULT_CATALOG;
 import static org.apache.flink.table.utils.CatalogManagerMocks.DEFAULT_DATABASE;
@@ -37,41 +39,59 @@
 class DatabaseCalciteSchemaTest {
 
     private static final String TABLE_NAME = "tab";
+    private static final String TEMP_TABLE_NAME = "tab_temp";
+    private static final String MODEL_NAME = "model";
+    private static final String TEMP_MODEL_NAME = "model_temp";
 
-    @Test
-    void testPermanentTableWithPrimaryKey() {
+    @ParameterizedTest(name = "Permanent table {0}")
+    @ValueSource(booleans = {true, false})
+    void testGetTableWithPrimaryKey(boolean isPermanent) {
         final CatalogManager catalogManager = CatalogManagerMocks.createEmptyCatalogManager();
 
         final DatabaseCalciteSchema calciteSchema =
                 new DatabaseCalciteSchema(DEFAULT_CATALOG, DEFAULT_DATABASE, catalogManager, true);
 
-        catalogManager.createTable(
-                createTable(),
-                ObjectIdentifier.of(DEFAULT_CATALOG, DEFAULT_DATABASE, TABLE_NAME),
-                false);
-
-        final Table table = calciteSchema.getTable(TABLE_NAME);
+        if (isPermanent) {
+            catalogManager.createTable(
+                    createTable(),
+                    ObjectIdentifier.of(DEFAULT_CATALOG, DEFAULT_DATABASE, TABLE_NAME),
+                    false);
+        } else {
+            catalogManager.createTemporaryTable(
+                    createTable(),
+                    ObjectIdentifier.of(DEFAULT_CATALOG, DEFAULT_DATABASE, TEMP_TABLE_NAME),
+                    false);
+        }
+
+        final Table table = calciteSchema.getTable(isPermanent ? TABLE_NAME : TEMP_TABLE_NAME);
         assertThat(table).isInstanceOf(CatalogSchemaTable.class);
         assertThat(((CatalogSchemaTable) table).getStatistic().getUniqueKeys().iterator().next())
                 .containsExactlyInAnyOrder("a", "b");
     }
 
-    @Test
-    void testTemporaryTableWithPrimaryKey() {
+    @ParameterizedTest(name = "Permanent model {0}")
+    @ValueSource(booleans = {true, false})
+    void testGetModel(boolean isPermanent) {
         final CatalogManager catalogManager = CatalogManagerMocks.createEmptyCatalogManager();
 
         final DatabaseCalciteSchema calciteSchema =
-                new DatabaseCalciteSchema("other_catalog", "other_database", catalogManager, true);
-
-        catalogManager.createTemporaryTable(
-                createTable(),
-                ObjectIdentifier.of("other_catalog", "other_database", TABLE_NAME),
-                false);
+                new DatabaseCalciteSchema(DEFAULT_CATALOG, DEFAULT_DATABASE, catalogManager, true);
 
-        final Table table = calciteSchema.getTable(TABLE_NAME);
-        assertThat(table).isInstanceOf(CatalogSchemaTable.class);
-        assertThat(((CatalogSchemaTable) table).getStatistic().getUniqueKeys().iterator().next())
-                .containsExactlyInAnyOrder("a", "b");
+        if (isPermanent) {
+            catalogManager.createModel(
+                    createModel(),
+                    ObjectIdentifier.of(DEFAULT_CATALOG, DEFAULT_DATABASE, MODEL_NAME),
+                    false);
+        } else {
+            catalogManager.createTemporaryModel(
+                    createModel(),
+                    ObjectIdentifier.of(DEFAULT_CATALOG, DEFAULT_DATABASE, TEMP_MODEL_NAME),
+                    false);
+        }
+
+        final CatalogSchemaModel model =
+                calciteSchema.getModel(isPermanent ? MODEL_NAME : TEMP_MODEL_NAME);
+        assertThat(model).isInstanceOf(CatalogSchemaModel.class);
     }
 
     private CatalogBaseTable createTable() {
@@ -85,4 +105,12 @@ private CatalogBaseTable createTable() {
 
         return CatalogTable.newBuilder().schema(schema).build();
     }
+
+    private CatalogModel createModel() {
+        return CatalogModel.of(
+                Schema.newBuilder().column("f1", DataTypes.INT()).build(),
+                Schema.newBuilder().column("label", DataTypes.STRING()).build(),
+                null,
+                "some comment");
+    }
 }
diff --git a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/FlinkCalciteCatalogReaderTest.java b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/FlinkCalciteCatalogReaderTest.java
index cb1b36dbba644..7e0edb896573c 100644
--- a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/FlinkCalciteCatalogReaderTest.java
+++ b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/FlinkCalciteCatalogReaderTest.java
@@ -18,17 +18,24 @@
 
 package org.apache.flink.table.planner.plan;
 
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.catalog.CatalogModel;
 import org.apache.flink.table.catalog.CatalogTable;
+import org.apache.flink.table.catalog.Column;
 import org.apache.flink.table.catalog.ConnectorCatalogTable;
+import org.apache.flink.table.catalog.ContextResolvedModel;
 import org.apache.flink.table.catalog.ContextResolvedTable;
 import org.apache.flink.table.catalog.ObjectIdentifier;
+import org.apache.flink.table.catalog.ResolvedCatalogModel;
 import org.apache.flink.table.catalog.ResolvedCatalogTable;
 import org.apache.flink.table.catalog.ResolvedSchema;
 import org.apache.flink.table.expressions.DefaultSqlFactory;
 import org.apache.flink.table.legacy.api.TableSchema;
 import org.apache.flink.table.planner.calcite.FlinkTypeFactory;
 import org.apache.flink.table.planner.calcite.FlinkTypeSystem;
+import org.apache.flink.table.planner.catalog.CatalogSchemaModel;
 import org.apache.flink.table.planner.catalog.CatalogSchemaTable;
+import org.apache.flink.table.planner.catalog.FlinkSchema;
 import org.apache.flink.table.planner.plan.schema.FlinkPreparingTableBase;
 import org.apache.flink.table.planner.plan.stats.FlinkStatistic;
 import org.apache.flink.table.planner.utils.TestTableSource;
@@ -37,17 +44,27 @@
 import org.apache.calcite.config.CalciteConnectionConfigImpl;
 import org.apache.calcite.config.CalciteConnectionProperty;
 import org.apache.calcite.jdbc.CalciteSchema;
+import org.apache.calcite.jdbc.CalciteSchemaBuilder;
+import org.apache.calcite.linq4j.tree.Expression;
 import org.apache.calcite.prepare.Prepare;
 import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.schema.Schema;
 import org.apache.calcite.schema.SchemaPlus;
 import org.apache.calcite.schema.Table;
+import org.checkerframework.checker.nullness.qual.Nullable;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.Properties;
+import java.util.Set;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -57,6 +74,7 @@ class FlinkCalciteCatalogReaderTest {
             new FlinkTypeFactory(
                     Thread.currentThread().getContextClassLoader(), FlinkTypeSystem.INSTANCE);
     private final String tableMockName = "ts";
+    private final String modelMockName = "ms";
 
     private SchemaPlus rootSchemaPlus;
     private FlinkCalciteCatalogReader catalogReader;
@@ -113,4 +131,126 @@ void testGetNonFlinkPreparingTableBase() {
                 catalogReader.getTable(Collections.singletonList(tableMockName));
         assertThat(resultTable).isNotInstanceOf(FlinkPreparingTableBase.class);
     }
+
+    @ParameterizedTest(name = "Case sensitive {0}")
+    @ValueSource(booleans = {true, false})
+    void testGetCatalogSchemaModel(boolean caseSensitive) {
+        // Mock CatalogSchemaModel
+        final ResolvedCatalogModel resolvedCatalogModel = getModel();
+        CatalogSchemaModel mockModel =
+                new CatalogSchemaModel(
+                        ContextResolvedModel.permanent(
+                                ObjectIdentifier.of(modelMockName, "", ""),
+                                CatalogManagerMocks.createEmptyCatalog(),
+                                resolvedCatalogModel));
+
+        MockFlinkSchema mockFlinkSchema = new MockFlinkSchema();
+        mockFlinkSchema.addModel(modelMockName, mockModel);
+
+        Properties prop = new Properties();
+        prop.setProperty(
+                CalciteConnectionProperty.CASE_SENSITIVE.camelName(),
+                Boolean.toString(caseSensitive));
+        catalogReader =
+                new FlinkCalciteCatalogReader(
+                        CalciteSchemaBuilder.asRootSchema(mockFlinkSchema),
+                        Collections.emptyList(),
+                        typeFactory,
+                        new CalciteConnectionConfigImpl(prop));
+
+        CatalogSchemaModel model = catalogReader.getModel(Collections.singletonList(modelMockName));
+        assertThat(model).isInstanceOf(CatalogSchemaModel.class);
+
+        model = catalogReader.getModel(Collections.singletonList(modelMockName.toUpperCase()));
+        if (caseSensitive) {
+            assertThat(model).isNull();
+        } else {
+            assertThat(model).isInstanceOf(CatalogSchemaModel.class);
+        }
+
+        model = catalogReader.getModel(Collections.singletonList("non_exist_model"));
+        assertThat(model).isNull();
+    }
+
+    @Test
+    void testGetModelFromNonFlinkSchema() {
+        assertThatThrownBy(() -> catalogReader.getModel(Collections.singletonList(modelMockName)))
+                .isInstanceOf(ClassCastException.class)
+                .hasMessageContaining(
+                        "class org.apache.calcite.jdbc.CalciteConnectionImpl$RootSchema cannot be cast to class org.apache.flink.table.planner.catalog.FlinkSchema");
+    }
+
+    private static ResolvedCatalogModel getModel() {
+        final CatalogModel catalogModel =
+                CatalogModel.of(
+                        org.apache.flink.table.api.Schema.newBuilder()
+                                .column("f1", DataTypes.INT())
+                                .build(),
+                        org.apache.flink.table.api.Schema.newBuilder()
+                                .column("label", DataTypes.STRING())
+                                .build(),
+                        null,
+                        "some comment");
+
+        final Column f1 = Column.physical("f1", DataTypes.INT());
+        final Column label = Column.physical("label", DataTypes.STRING());
+        final ResolvedSchema inputSchema = ResolvedSchema.of(f1);
+        final ResolvedSchema outputSchema = ResolvedSchema.of(label);
+
+        return ResolvedCatalogModel.of(catalogModel, inputSchema, outputSchema);
+    }
+
+    private static class MockFlinkSchema extends FlinkSchema {
+
+        private final Map<String, CatalogSchemaModel> modelMap = new HashMap<>();
+
+        @Override
+        public Table getTable(String name) {
+            return null;
+        }
+
+        @Override
+        public Set<String> getTableNames() {
+            return Set.of();
+        }
+
+        @Override
+        public @Nullable Schema getSubSchema(String name) {
+            return null;
+        }
+
+        public void addModel(String name, CatalogSchemaModel model) {
+            modelMap.put(name, model);
+        }
+
+        @Override
+        public CatalogSchemaModel getModel(String name) {
+            return modelMap.get(name);
+        }
+
+        @Override
+        public Set<String> getModelNames() {
+            return modelMap.keySet();
+        }
+
+        @Override
+        public FlinkSchema copy() {
+            return null;
+        }
+
+        @Override
+        public Set<String> getSubSchemaNames() {
+            return Collections.emptySet();
+        }
+
+        @Override
+        public Expression getExpression(@Nullable SchemaPlus parentSchema, String name) {
+            return null;
+        }
+
+        @Override
+        public boolean isMutable() {
+            return false;
+        }
+    }
 }


Reviews:
user: login: fsk119
body: Thanks for your contribution. I left some comments.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T06:30:49Z
user: login: lihaosky
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T23:24:08Z
user: login: fsk119
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T09:04:26Z
user: login: davidradl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T15:02:01Z
user: login: davidradl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T15:04:06Z
user: login: lihaosky
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T17:07:19Z
user: login: fsk119
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T01:41:10Z
user: login: fsk119
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T01:46:20Z
user: login: fsk119
body: LGTM
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-19T02:07:36Z

Review comments:
diff_hunk: @@ -104,6 +111,30 @@ public Prepare.PreparingTable getTable(List<String> names) {
         }
     }
 
+    public CatalogSchemaModel getModel(List<String> names) {
path: flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/FlinkCalciteCatalogReader.java
user: login: fsk119
body: add Nullable annotation.
created_at: 2025-05-14T06:05:29Z
author_association: MEMBER
diff_hunk: @@ -104,6 +111,30 @@ public Prepare.PreparingTable getTable(List<String> names) {
         }
     }
 
+    public CatalogSchemaModel getModel(List<String> names) {
path: flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/FlinkCalciteCatalogReader.java
user: login: fsk119
body: add Nullable annotation.
created_at: 2025-05-14T06:05:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.flink.table.planner.catalog;
+
+import org.apache.flink.table.catalog.ContextResolvedModel;
+import org.apache.flink.table.catalog.ResolvedSchema;
+import org.apache.flink.table.planner.calcite.FlinkTypeFactory;
+import org.apache.flink.table.planner.plan.stats.FlinkStatistic;
+import org.apache.flink.table.runtime.types.PlannerTypeUtils;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.LogicalType;
+
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Represents a wrapper for {@link ContextResolvedModel} in {@link
+ * org.apache.calcite.schema.Schema}.
+ */
+public class CatalogSchemaModel {
+    // ~ Instance fields --------------------------------------------------------
+
+    private final ContextResolvedModel contextResolvedModel;
+    private final FlinkStatistic statistic;
+    private final boolean isStreamingMode;
path: flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/catalog/CatalogSchemaModel.java
user: login: fsk119
body: Do we need this flag? It's better to add required methods.
created_at: 2025-05-14T06:23:16Z
author_association: MEMBER
diff_hunk: @@ -104,6 +111,30 @@ public Prepare.PreparingTable getTable(List<String> names) {
         }
     }
 
+    public CatalogSchemaModel getModel(List<String> names) {
+        for (List<String> schemaPath : getSchemaPaths()) {
+            CalciteSchema schema =
+                    getSchema(
+                            getRootSchema(),
+                            Iterables.concat(schemaPath, Util.skipLast(names)),
+                            nameMatcher());
+            if (schema == null) {
+                continue;
+            }
+
+            if (!(schema.schema instanceof FlinkSchema)) {
+                throw new ValidationException("getModel() only supports FlinkSchema.");
+            }
+
+            FlinkSchema flinkSchema = (FlinkSchema) schema.schema;
+            CatalogSchemaModel model = flinkSchema.getModel(Util.last(names));
path: flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/FlinkCalciteCatalogReader.java
user: login: fsk119
body: Flink pasrer supports to parse statement in case insensitive/case sensitive mode. It's better we can align the behaviour as getTable. cc SimpleCalciteSchema#getImplicitTable
created_at: 2025-05-14T06:27:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.flink.table.planner.catalog;
+
+import org.apache.flink.table.catalog.ContextResolvedModel;
+import org.apache.flink.table.catalog.ResolvedSchema;
+import org.apache.flink.table.planner.calcite.FlinkTypeFactory;
+import org.apache.flink.table.planner.plan.stats.FlinkStatistic;
+import org.apache.flink.table.runtime.types.PlannerTypeUtils;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.LogicalType;
+
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Represents a wrapper for {@link ContextResolvedModel} in {@link
+ * org.apache.calcite.schema.Schema}.
+ */
+public class CatalogSchemaModel {
+    // ~ Instance fields --------------------------------------------------------
+
+    private final ContextResolvedModel contextResolvedModel;
+    private final FlinkStatistic statistic;
+    private final boolean isStreamingMode;
+
+    // ~ Constructors -----------------------------------------------------------
+
+    /**
+     * Create a CatalogSchemaModel instance.
+     *
+     * @param contextResolvedModel A result of catalog lookup
+     * @param statistic Model statistics
+     * @param isStreaming If the model is for streaming mode
+     */
+    public CatalogSchemaModel(
+            ContextResolvedModel contextResolvedModel,
+            FlinkStatistic statistic,
path: flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/catalog/CatalogSchemaModel.java
user: login: fsk119
body: Does model have statistic? FlinkStatistic is used for Table right now. I think it's not suitable to add the table property to model.
created_at: 2025-05-14T06:28:53Z
author_association: MEMBER
diff_hunk: @@ -104,6 +111,30 @@ public Prepare.PreparingTable getTable(List<String> names) {
         }
     }
 
+    public CatalogSchemaModel getModel(List<String> names) {
+        for (List<String> schemaPath : getSchemaPaths()) {
+            CalciteSchema schema =
+                    getSchema(
+                            getRootSchema(),
+                            Iterables.concat(schemaPath, Util.skipLast(names)),
+                            nameMatcher());
+            if (schema == null) {
+                continue;
+            }
+
+            if (!(schema.schema instanceof FlinkSchema)) {
+                throw new ValidationException("getModel() only supports FlinkSchema.");
+            }
+
+            FlinkSchema flinkSchema = (FlinkSchema) schema.schema;
+            CatalogSchemaModel model = flinkSchema.getModel(Util.last(names));
path: flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/FlinkCalciteCatalogReader.java
user: login: lihaosky
body: Good call
created_at: 2025-05-14T23:24:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -104,6 +115,38 @@ public Prepare.PreparingTable getTable(List<String> names) {
         }
     }
 
+    public @Nullable CatalogSchemaModel getModel(List<String> names) {
path: flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/FlinkCalciteCatalogReader.java
user: login: fsk119
body: I prefer to copy org.apache.calcite.jdbc.SimpleCalciteSchema into flink repo and modify SimpleCalciteSchema directly. 

```
 for (List<String> schemaNames : schemaNameList) {
            CalciteSchema calciteSchema =
                    SqlValidatorUtil.getSchema(
                            rootSchema,
                            Iterables.concat(schemaNames, Util.skipLast(names)),
                            nameMatcher);

            if (calciteSchema != null) {
                final String name = Util.last(names);
                boolean caseSensitive = nameMatcher.isCaseSensitive();
                // ----- FLINK MODIFICATION START -----
                SimpleCalciteSchema.ModelEntry modelEntry =
                        ((SimpleCalciteSchema) calciteSchema).getModel(name, caseSensitive);
                if (modelEntry != null) {
                    return modelEntry.getModel();
                }
                // ----- FLINK MODIFICATION END -----
            }
        }
```

Because SimpleCalciteSchema allow us to use cache directly and we don't need to copy some methods from the SimpleCalciteSchema.
created_at: 2025-05-15T09:02:42Z
author_association: MEMBER
diff_hunk: @@ -104,6 +115,38 @@ public Prepare.PreparingTable getTable(List<String> names) {
         }
     }
 
+    public @Nullable CatalogSchemaModel getModel(List<String> names) {
+        for (List<String> schemaPath : getSchemaPaths()) {
+            CalciteSchema schema =
+                    getSchema(
+                            getRootSchema(),
+                            Iterables.concat(schemaPath, Util.skipLast(names)),
+                            nameMatcher());
+            if (schema == null) {
+                continue;
+            }
+
+            if (!(schema.schema instanceof FlinkSchema)) {
+                throw new ValidationException("getModel() only supports FlinkSchema.");
+            }
+
+            FlinkSchema flinkSchema = (FlinkSchema) schema.schema;
+            String modelName =
+                    nameMatcher().isCaseSensitive()
+                            ? Util.last(names)
+                            : caseInsensitiveLookup(flinkSchema.getModelNames(), Util.last(names));
+            if (modelName == null) {
+                return null;
path: flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/FlinkCalciteCatalogReader.java
user: login: davidradl
body: why to we return null instead of continue? Can we not have a schemaPath that we hit later that might have a valid model? I suspect the proposal from @fsk119 will avoid this.


created_at: 2025-05-15T15:02:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -258,4 +301,28 @@ private static boolean isLegacySourceOptions(CatalogSchemaTable schemaTable) {
             }
         }
     }
+
+    private static @Nullable String caseInsensitiveLookup(Set<String> candidates, String name) {
+        // Exact string lookup
+        if (candidates.contains(name)) {
+            return name;
+        }
+        // Upper case string lookup
path: flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/FlinkCalciteCatalogReader.java
user: login: davidradl
body: if we remove the Upper case and lower case if's, it would still have the same logic I think.
created_at: 2025-05-15T15:04:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -104,6 +115,38 @@ public Prepare.PreparingTable getTable(List<String> names) {
         }
     }
 
+    public @Nullable CatalogSchemaModel getModel(List<String> names) {
path: flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/FlinkCalciteCatalogReader.java
user: login: lihaosky
body: I can do this. Looks there's no cache for it though: https://github.com/apache/calcite/blob/main/core/src/main/java/org/apache/calcite/jdbc/SimpleCalciteSchema.java#L69

Do we need to copy https://github.com/apache/calcite/blob/calcite-1.34.0/core/src/main/java/org/apache/calcite/schema/Schema.java as well and add `getModel` and `getModelNames` there?


created_at: 2025-05-15T17:07:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -104,6 +115,38 @@ public Prepare.PreparingTable getTable(List<String> names) {
         }
     }
 
+    public @Nullable CatalogSchemaModel getModel(List<String> names) {
path: flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/FlinkCalciteCatalogReader.java
user: login: fsk119
body: You are right. We don't use cache here. But I perfer to leave the calcite parser logic in the calcite codebase.

I don't think we need to copy `Schema` because we already have a specific class named FlinkSchema here, which can provide required abilities.
created_at: 2025-05-16T01:41:09Z
author_association: MEMBER
diff_hunk: @@ -104,6 +115,38 @@ public Prepare.PreparingTable getTable(List<String> names) {
         }
     }
 
+    public @Nullable CatalogSchemaModel getModel(List<String> names) {
+        for (List<String> schemaPath : getSchemaPaths()) {
+            CalciteSchema schema =
+                    getSchema(
+                            getRootSchema(),
+                            Iterables.concat(schemaPath, Util.skipLast(names)),
+                            nameMatcher());
+            if (schema == null) {
+                continue;
+            }
+
+            if (!(schema.schema instanceof FlinkSchema)) {
+                throw new ValidationException("getModel() only supports FlinkSchema.");
+            }
+
+            FlinkSchema flinkSchema = (FlinkSchema) schema.schema;
+            String modelName =
+                    nameMatcher().isCaseSensitive()
+                            ? Util.last(names)
+                            : caseInsensitiveLookup(flinkSchema.getModelNames(), Util.last(names));
+            if (modelName == null) {
+                return null;
path: flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/FlinkCalciteCatalogReader.java
user: login: fsk119
body: +1. We should continue to iterate other schemas.
created_at: 2025-05-16T01:46:20Z
author_association: MEMBER

PR comments:
user: login: flinkbot
created_at: 2025-05-09T00:15:36Z
body: <!--
Meta data
{
  "version" : 1,
  "metaDataEntries" : [ {
    "hash" : "102421bf918169fc62aeb0433740f1ad9d0a3bda",
    "status" : "DELETED",
    "url" : "https://dev.azure.com/apache-flink/98463496-1af2-4620-8eab-a2ecc1a2e6fe/_build/results?buildId=67537",
    "triggerID" : "102421bf918169fc62aeb0433740f1ad9d0a3bda",
    "triggerType" : "PUSH"
  }, {
    "hash" : "5f7b33712b35cff6af571667f67e94bb9f2d98bb",
    "status" : "DELETED",
    "url" : "https://dev.azure.com/apache-flink/98463496-1af2-4620-8eab-a2ecc1a2e6fe/_build/results?buildId=67550",
    "triggerID" : "5f7b33712b35cff6af571667f67e94bb9f2d98bb",
    "triggerType" : "PUSH"
  }, {
    "hash" : "56b5d291f1c3e29a6bf91cdacb5e392e7124e711",
    "status" : "DELETED",
    "url" : "https://dev.azure.com/apache-flink/98463496-1af2-4620-8eab-a2ecc1a2e6fe/_build/results?buildId=67605",
    "triggerID" : "56b5d291f1c3e29a6bf91cdacb5e392e7124e711",
    "triggerType" : "PUSH"
  }, {
    "hash" : "46f8b79954916d0a58b7083c2cdfbc429e0344cf",
    "status" : "DELETED",
    "url" : "https://dev.azure.com/apache-flink/98463496-1af2-4620-8eab-a2ecc1a2e6fe/_build/results?buildId=67608",
    "triggerID" : "46f8b79954916d0a58b7083c2cdfbc429e0344cf",
    "triggerType" : "PUSH"
  }, {
    "hash" : "a2c38b7730002d0d0448bb614d59fce4bcc0126c",
    "status" : "DELETED",
    "url" : "https://dev.azure.com/apache-flink/98463496-1af2-4620-8eab-a2ecc1a2e6fe/_build/results?buildId=67636",
    "triggerID" : "a2c38b7730002d0d0448bb614d59fce4bcc0126c",
    "triggerType" : "PUSH"
  }, {
    "hash" : "a2c38b7730002d0d0448bb614d59fce4bcc0126c",
    "status" : "DELETED",
    "url" : "https://dev.azure.com/apache-flink/98463496-1af2-4620-8eab-a2ecc1a2e6fe/_build/results?buildId=67636",
    "triggerID" : "2882076684",
    "triggerType" : "MANUAL"
  }, {
    "hash" : "458f4508a4a47f6c38a420eb3a3a924915736326",
    "status" : "SUCCESS",
    "url" : "https://dev.azure.com/apache-flink/98463496-1af2-4620-8eab-a2ecc1a2e6fe/_build/results?buildId=67669",
    "triggerID" : "458f4508a4a47f6c38a420eb3a3a924915736326",
    "triggerType" : "PUSH"
  } ]
}-->
## CI report:

* 458f4508a4a47f6c38a420eb3a3a924915736326 Azure: [SUCCESS](https://dev.azure.com/apache-flink/98463496-1af2-4620-8eab-a2ecc1a2e6fe/_build/results?buildId=67669) 

<details>
<summary>Bot commands</summary>
  The @flinkbot bot supports the following commands:

 - `@flinkbot run azure` re-run the last Azure build
</details>
author_association: COLLABORATOR
user: login: lihaosky
created_at: 2025-05-15T03:07:09Z
body: @flinkbot run azure
author_association: CONTRIBUTOR