This is an automated email from the ASF dual-hosted git repository.

yuqi1129 pushed a commit to branch branch-1.3
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/branch-1.3 by this push:
     new c504421dbc [Cherry-pick to branch-1.3] [#13301] improvement 
(trino-connector): Make gravitino.metalake optional (#13302) (#13319)
c504421dbc is described below

commit c504421dbc4aa9ca9f0e46a3db29bd80018a9616
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri Sep 18 21:20:11 2026 +0800

    [Cherry-pick to branch-1.3] [#13301] improvement (trino-connector): Make 
gravitino.metalake optional (#13302) (#13319)
    
    **Cherry-pick Information:**
    - Original commit: c8e6d3b1f158a78f3a095b6864d7295baaec1aac
    - Target branch: `branch-1.3`
    - Status: ⚠️ **Has conflicts - manual resolution required**
    
    ---------
    
    Co-authored-by: Yuhui <[email protected]>
    Co-authored-by: Claude Opus 5 <[email protected]>
---
 docs/trino-connector/catalog-iceberg.md            |   7 +-
 docs/trino-connector/configuration.md              |  60 +++---
 docs/trino-connector/installation.md               |   2 +-
 docs/trino-connector/supported-catalog.md          |  38 +++-
 .../testsets/jdbc-mysql/00008_alter_catalog.txt    |   8 +-
 .../gravitino/trino/connector/GravitinoConfig.java | 103 +++++++---
 .../trino/connector/GravitinoConnectorFactory.java |  14 +-
 .../connector/catalog/CatalogConnectorManager.java | 103 +++++++---
 .../AlterCatalogStoredProcedure.java               |  42 +++--
 .../CreateCatalogStoredProcedure.java              |  40 ++--
 .../DropCatalogStoredProcedure.java                |  28 +--
 .../storedprocedure/GravitinoStoredProcedure.java  |  29 +++
 .../GravitinoStoredProcedureFactory.java           |  16 +-
 .../system/table/GravitinoSystemTableCatalog.java  |  29 +--
 .../table/GravitinoSystemTableCatalogStatus.java   |   9 +-
 .../system/table/GravitinoSystemTableFactory.java  |   8 +-
 .../table/GravitinoSystemTableLoadStatus.java      |  19 +-
 .../trino/connector/TestGravitinoConfig.java       | 157 ++++++++++++---
 .../TestGravitinoConnectorFactoryStart.java        |  11 ++
 .../catalog/TestCatalogConnectorManager.java       | 210 ++++++++++++++++++++-
 .../TestAlterCatalogStoredProcedure.java           | 108 +++++++++++
 .../TestCreateCatalogStoredProcedure.java          | 108 +++++++++++
 .../TestDropCatalogStoredProcedure.java            |  64 ++++++-
 .../TestGravitinoStoredProcedureFactory.java       |  55 ++++++
 .../table/TestGravitinoSystemStatusTables.java     |  71 ++++++-
 25 files changed, 1130 insertions(+), 209 deletions(-)

diff --git a/docs/trino-connector/catalog-iceberg.md 
b/docs/trino-connector/catalog-iceberg.md
index ccfae7bf9e..e3721dc354 100644
--- a/docs/trino-connector/catalog-iceberg.md
+++ b/docs/trino-connector/catalog-iceberg.md
@@ -135,9 +135,10 @@ token cannot be exchanged, so it would carry no identity 
to the IRC. Set
 
 - One IRC serves exactly one metalake, fixed at startup by
   `gravitino.iceberg-rest.gravitino-metalake`. The Gravitino server only 
reports the IRC's endpoint
-  for that metalake. In multi-metalake mode 
(`gravitino.use-single-metalake=false`), a non-REST
-  Iceberg catalog in another metalake therefore requires a metalake-scoped 
manual URI or remains
-  unregistered while REST routing is enabled.
+  for that metalake. When several metalakes are loaded 
(`gravitino.catalog-name-with-metalake=true` or `gravitino.metalake` unset), a 
non-REST
+  Iceberg catalog in another metalake therefore requires a manual URI 
(`gravitino.iceberg.rest-uri`
+  as the default, overridden per metalake by 
`gravitino.iceberg.rest-uri.<metalake_name>`) or
+  remains unregistered while REST routing is enabled.
 - A catalog created with `catalog-backend=rest` keeps pointing at its own 
configured `uri` and is
   not re-routed, since it already reaches an Iceberg REST catalog directly.
 - A deployment that does not run the IRC must set
diff --git a/docs/trino-connector/configuration.md 
b/docs/trino-connector/configuration.md
index 449969e289..c682782e98 100644
--- a/docs/trino-connector/configuration.md
+++ b/docs/trino-connector/configuration.md
@@ -7,31 +7,32 @@ license: "This software is licensed under the Apache License 
version 2."
 
 ## Introduction
 
-| Property                                    | Type    | Default Value        
 | Description                                                                  
                                                                                
                                                                                
                                                                                
                    | Required |
-|---------------------------------------------|---------|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|
-| connector.name                              | string  | (none)               
 | The `connector.name` defines the type of Trino connector, this value is 
always 'gravitino'.                                                             
                                                                                
                                                                                
                         | Yes      |
-| gravitino.metalake                          | string  | (none)               
 | The `gravitino.metalake` defines which metalake in Gravitino server the 
Trino connector uses. Trino connector should set it at start, the value of 
`gravitino.metalake` needs to be a valid name, Trino connector can detect and 
load the metalake with catalogs, schemas and tables once created and keep in 
sync.                              | Yes      |
-| gravitino.uri                               | string  | 
http://localhost:8090 | The `gravitino.uri` defines the connection URL of the 
Gravitino server, the default value is `http://localhost:8090`. Trino connector 
can detect and connect to Gravitino server once it is ready, no need to start 
Gravitino server beforehand.                                                    
                                             | No       |
-| trino.jdbc.user                             | string  | admin                
 | The jdbc user name of current Trino.                                         
                                                                                
                                                                                
                                                                                
                    | NO       |
-| trino.jdbc.password                         | string  | (none)               
 | The jdbc password of current Trino.                                          
                                                                                
                                                                                
                                                                                
                    | NO       |
-| trino.jdbc.ssl.enabled                      | boolean | (derived)            
 | Whether the internal JDBC connection to the Trino coordinator uses TLS. If 
not set, it is derived from the scheme of the Trino `discovery.uri`, so a 
coordinator whose `discovery.uri` is `https://...` needs no explicit setting.   
                                                                                
                            | No       |
-| trino.jdbc.ssl.truststore.path              | string  | (none)               
 | Path of the truststore holding the Trino coordinator certificate. If 
omitted, the default JVM truststore is used. Requires TLS, which is enabled 
automatically for an HTTPS `discovery.uri` or explicitly with 
`trino.jdbc.ssl.enabled=true`, and a `trino.jdbc.ssl.verification` other than 
`NONE`.                                             | No       |
-| trino.jdbc.ssl.truststore.password          | string  | (none)               
 | Password of the truststore configured by `trino.jdbc.ssl.truststore.path`. 
Requires TLS and `trino.jdbc.ssl.truststore.path`, otherwise the connector 
fails to start.                                                                 
                                                                                
                           | No       |
-| trino.jdbc.ssl.truststore.type              | string  | (none)               
 | Type of the truststore, for example `JKS` or `PKCS12`. If omitted, the 
default JVM truststore type is used. Requires TLS and 
`trino.jdbc.ssl.truststore.path`, otherwise the connector fails to start.       
                                                                                
                                                    | No       |
-| trino.jdbc.ssl.keystore.path                | string  | (none)               
 | Path of the keystore holding the client certificate presented to the 
coordinator, for coordinators that require mutual TLS. Requires TLS, which is 
enabled automatically for an HTTPS `discovery.uri` or explicitly with 
`trino.jdbc.ssl.enabled=true`, and a `trino.jdbc.ssl.verification` other than 
`NONE`. See the note on mutual TLS below. | No       |
-| trino.jdbc.ssl.keystore.password            | string  | (none)               
 | Password of the keystore configured by `trino.jdbc.ssl.keystore.path`. 
Requires TLS and `trino.jdbc.ssl.keystore.path`, otherwise the connector fails 
to start.                                                                       
                                                                                
                           | No       |
-| trino.jdbc.ssl.keystore.type                | string  | (none)               
 | Type of the keystore, for example `JKS` or `PKCS12`. If omitted, the default 
JVM keystore type is used. Requires TLS and `trino.jdbc.ssl.keystore.path`, 
otherwise the connector fails to start.                                         
                                                                                
                        | No       |
-| trino.jdbc.ssl.verification                 | string  | FULL                 
 | Certificate verification mode of the internal JDBC connection: `FULL`, `CA` 
or `NONE`. Any value other than `FULL` requires TLS, which may be derived from 
an HTTPS `discovery.uri`. `NONE` disables certificate verification entirely and 
should only be used for troubleshooting.                                        
                      | No       |
-| trino.jdbc.roles                            | string  | (none)               
 | Session roles applied to the internal JDBC connection, for example 
`system:sysadmin`. Required by deployments that only allow `CREATE CATALOG` 
with a privileged role.                                                         
                                                                                
                                  | No       |
-| trino.jdbc.properties.                      | string  | (none)               
 | The configuration key prefix for raw Trino JDBC driver properties, see 
[Connecting to a TLS-enabled 
coordinator](#connecting-to-a-tls-enabled-coordinator).                         
                                                                                
                                                                             | 
No       |
-| gravitino.metadata.refresh-interval-seconds | integer | 10                   
 | The `gravitino.metadata.refresh-interval-seconds` defines the interval in 
seconds to refresh metadata from Gravitino server, the default value is 10 
seconds.                                                                        
                                                                                
                            | No       |
-| gravitino.trino.skip-version-validation     | boolean | false                
 | The `gravitino.trino.skip-version-validation` defines whether to skip Trino 
version validation. Gravitino supports Trino versions between 435 and 478. If 
this option is `true`, unsupported Trino versions can still be used, but 
compatibility is not guaranteed.                                                
                              | No       |
-| gravitino.client.                           | string  | (none)               
 | The configuration key prefix for the Gravitino client config.                
                                                                                
                                                                                
                                                                                
                    | No       |
-| gravitino.trino.skip-catalog-patterns       | string  | (none)               
 | The `gravitino.trino.skip-catalog-patterns` defines a comma-separated list 
of catalog name regex patterns that should be excluded from loading. For 
example, `test_.*, .*_tmp` excludes all catalogs starting with `test_` or 
ending with `_tmp`.                                                             
                                   | No       |
-| gravitino.use-single-metalake               | boolean | true                 
 | If `true`, only one metalake is used and catalogs are identified by 
`<catalog_name>`. If `false`, multi-metalake mode is enabled and catalogs are 
identified by `<metalake_name>.<catalog_name>`.                                 
                                                                                
                               | No       |
-| gravitino.iceberg.rest-routing-enabled      | boolean | true                 
 | Whether non-REST `lakehouse-iceberg` catalogs must be routed through the 
Gravitino Iceberg REST server. When enabled, a catalog is not registered until 
discovery succeeds or `gravitino.iceberg.rest-uri` is configured. Set this to 
`false` to retain legacy `catalog-backend` translation and skip discovery. | No 
      |
-| gravitino.iceberg.rest-uri                  | string  | (none)               
 | The endpoint of the Gravitino Iceberg REST server (IRC). It is discovered 
automatically from the Gravitino server for this connector's metalake; set this 
only to override the discovered value. When available, eligible 
`lakehouse-iceberg` catalogs are loaded through IRC, enabling credential 
vending.                                               | No       |
-| gravitino.iceberg.rest-catalog.             | string  | (none)               
 | Prefix for properties passed to the internal Trino Iceberg REST catalog. The 
prefix is rewritten to `iceberg.rest-catalog.`. The `uri`, `warehouse`, and 
`prefix` keys are reserved and derived by the connector.                        
                                                                                
                       | No       |
+| Property                                    | Type    | Default Value        
 | Description                                                                  
                                                                                
                                                                                
                                                                                
                                                                                
              [...]
+|---------------------------------------------|---------|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 [...]
+| connector.name                              | string  | (none)               
 | The `connector.name` defines the type of Trino connector, this value is 
always 'gravitino'.                                                             
                                                                                
                                                                                
                                                                                
                   [...]
+| gravitino.metalake                          | string  | (none)               
 | The `gravitino.metalake` defines which metalake in Gravitino server the 
Trino connector uses. The value needs to be a valid name, Trino connector can 
detect and load the metalake with catalogs, schemas and tables once created and 
keep in sync. When unset, the catalogs of every metalake are loaded, the system 
tables report every metalake, and the `METALAKE` argument of the stored 
procedures is required.      [...]
+| gravitino.uri                               | string  | 
http://localhost:8090 | The `gravitino.uri` defines the connection URL of the 
Gravitino server, the default value is `http://localhost:8090`. Trino connector 
can detect and connect to Gravitino server once it is ready, no need to start 
Gravitino server beforehand.                                                    
                                                                                
                                       [...]
+| trino.jdbc.user                             | string  | admin                
 | The jdbc user name of current Trino.                                         
                                                                                
                                                                                
                                                                                
                                                                                
              [...]
+| trino.jdbc.password                         | string  | (none)               
 | The jdbc password of current Trino.                                          
                                                                                
                                                                                
                                                                                
                                                                                
              [...]
+| trino.jdbc.ssl.enabled                      | boolean | (derived)            
 | Whether the internal JDBC connection to the Trino coordinator uses TLS. If 
not set, it is derived from the scheme of the Trino `discovery.uri`, so a 
coordinator whose `discovery.uri` is `https://...` needs no explicit setting.   
                                                                                
                                                                                
                      [...]
+| trino.jdbc.ssl.truststore.path              | string  | (none)               
 | Path of the truststore holding the Trino coordinator certificate. If 
omitted, the default JVM truststore is used. Requires TLS, which is enabled 
automatically for an HTTPS `discovery.uri` or explicitly with 
`trino.jdbc.ssl.enabled=true`, and a `trino.jdbc.ssl.verification` other than 
`NONE`.                                                                         
                                              [...]
+| trino.jdbc.ssl.truststore.password          | string  | (none)               
 | Password of the truststore configured by `trino.jdbc.ssl.truststore.path`. 
Requires TLS and `trino.jdbc.ssl.truststore.path`, otherwise the connector 
fails to start.                                                                 
                                                                                
                                                                                
                     [...]
+| trino.jdbc.ssl.truststore.type              | string  | (none)               
 | Type of the truststore, for example `JKS` or `PKCS12`. If omitted, the 
default JVM truststore type is used. Requires TLS and 
`trino.jdbc.ssl.truststore.path`, otherwise the connector fails to start.       
                                                                                
                                                                                
                                              [...]
+| trino.jdbc.ssl.keystore.path                | string  | (none)               
 | Path of the keystore holding the client certificate presented to the 
coordinator, for coordinators that require mutual TLS. Requires TLS, which is 
enabled automatically for an HTTPS `discovery.uri` or explicitly with 
`trino.jdbc.ssl.enabled=true`, and a `trino.jdbc.ssl.verification` other than 
`NONE`. See the note on mutual TLS below.                                       
                                    [...]
+| trino.jdbc.ssl.keystore.password            | string  | (none)               
 | Password of the keystore configured by `trino.jdbc.ssl.keystore.path`. 
Requires TLS and `trino.jdbc.ssl.keystore.path`, otherwise the connector fails 
to start.                                                                       
                                                                                
                                                                                
                     [...]
+| trino.jdbc.ssl.keystore.type                | string  | (none)               
 | Type of the keystore, for example `JKS` or `PKCS12`. If omitted, the default 
JVM keystore type is used. Requires TLS and `trino.jdbc.ssl.keystore.path`, 
otherwise the connector fails to start.                                         
                                                                                
                                                                                
                  [...]
+| trino.jdbc.ssl.verification                 | string  | FULL                 
 | Certificate verification mode of the internal JDBC connection: `FULL`, `CA` 
or `NONE`. Any value other than `FULL` requires TLS, which may be derived from 
an HTTPS `discovery.uri`. `NONE` disables certificate verification entirely and 
should only be used for troubleshooting.                                        
                                                                                
                [...]
+| trino.jdbc.roles                            | string  | (none)               
 | Session roles applied to the internal JDBC connection, for example 
`system:sysadmin`. Required by deployments that only allow `CREATE CATALOG` 
with a privileged role.                                                         
                                                                                
                                                                                
                            [...]
+| trino.jdbc.properties.                      | string  | (none)               
 | The configuration key prefix for raw Trino JDBC driver properties, see 
[Connecting to a TLS-enabled 
coordinator](#connecting-to-a-tls-enabled-coordinator).                         
                                                                                
                                                                                
                                                                       [...]
+| gravitino.metadata.refresh-interval-seconds | integer | 10                   
 | The `gravitino.metadata.refresh-interval-seconds` defines the interval in 
seconds to refresh metadata from Gravitino server, the default value is 10 
seconds.                                                                        
                                                                                
                                                                                
                      [...]
+| gravitino.trino.skip-version-validation     | boolean | false                
 | The `gravitino.trino.skip-version-validation` defines whether to skip Trino 
version validation. Gravitino supports Trino versions between 435 and 478. If 
this option is `true`, unsupported Trino versions can still be used, but 
compatibility is not guaranteed.                                                
                                                                                
                        [...]
+| gravitino.client.                           | string  | (none)               
 | The configuration key prefix for the Gravitino client config.                
                                                                                
                                                                                
                                                                                
                                                                                
              [...]
+| gravitino.trino.skip-catalog-patterns       | string  | (none)               
 | The `gravitino.trino.skip-catalog-patterns` defines a comma-separated list 
of catalog name regex patterns that should be excluded from loading. For 
example, `test_.*, .*_tmp` excludes all catalogs starting with `test_` or 
ending with `_tmp`.                                                             
                                                                                
                             [...]
+| gravitino.catalog-name-with-metalake        | boolean | false                
 | If `true`, multi-metalake mode is enabled: every metalake is loaded and 
catalogs are identified by `<metalake_name>.<catalog_name>`. If `false`, 
catalogs are identified by `<catalog_name>`; when several metalakes are loaded 
and two of them have a catalog with the same name, only one of them is 
registered, in the order the server lists the metalakes, and the other is 
reported as `FAILED` in `gravitino.system [...]
+| gravitino.use-single-metalake               | boolean | true                 
 | **Deprecated**, use `gravitino.catalog-name-with-metalake` instead. `false` 
is equivalent to `gravitino.catalog-name-with-metalake=true`. Ignored when 
`gravitino.catalog-name-with-metalake` is set.                                  
                                                                                
                                                                                
                    [...]
+| gravitino.iceberg.rest-routing-enabled      | boolean | true                 
 | Whether non-REST `lakehouse-iceberg` catalogs must be routed through the 
Gravitino Iceberg REST server. When enabled, a catalog is not registered until 
discovery succeeds or `gravitino.iceberg.rest-uri` is configured. Set this to 
`false` to retain legacy `catalog-backend` translation and skip discovery.      
                                                                                
                     [...]
+| gravitino.iceberg.rest-uri                  | string  | (none)               
 | The endpoint of the Gravitino Iceberg REST server (IRC). It is discovered 
automatically from the Gravitino server for each loaded metalake; set this only 
to override the discovered value. When available, eligible `lakehouse-iceberg` 
catalogs are loaded through IRC, enabling credential vending.                   
                                                                                
                  [...]
+| gravitino.iceberg.rest-catalog.             | string  | (none)               
 | Prefix for properties passed to the internal Trino Iceberg REST catalog. The 
prefix is rewritten to `iceberg.rest-catalog.`. The `uri`, `warehouse`, and 
`prefix` keys are reserved and derived by the connector.                        
                                                                                
                                                                                
                  [...]
 
 To configure the Gravitino client, use properties prefixed with 
`gravitino.client.`. These properties will directly passed to the Gravitino 
client.
 
@@ -43,12 +44,11 @@ legacy `catalog-backend` translation. Otherwise, non-REST 
`lakehouse-iceberg` ca
 unregistered until discovery succeeds. See
 [Iceberg catalog](./catalog-iceberg.md#how-trino-reaches-the-catalog).
 
-Multi-metalake mode (`gravitino.use-single-metalake=false`) is supported on 
Trino connector versions 435-445 and 469-478. On versions 446-468, a warning is 
logged and the connector initializes, but the mode is not fully supported and 
some operations may fail.
+Qualified catalog names (`gravitino.catalog-name-with-metalake=true`) are 
supported on Trino connector versions 435-445 and 469-478. On versions 446-468, 
a warning is logged and the connector initializes, but the mode is not fully 
supported and some operations may fail.
 
-**Note:** In multi-metalake mode, `gravitino.iceberg.rest-uri` is only honored 
when scoped to a
-metalake, as `gravitino.iceberg.rest-uri.<metalake_name>` — the unscoped form 
is ignored, since a
-single Iceberg REST server serves exactly one metalake and applying it to 
every metalake would
-misroute the others. The unscoped form remains valid in single-metalake mode.
+**Note:** `gravitino.iceberg.rest-uri` is the default endpoint for every 
loaded metalake. Since a
+single Iceberg REST server serves exactly one metalake, override it per 
metalake with
+`gravitino.iceberg.rest-uri.<metalake_name>` when several metalakes are loaded.
 
 ## Connecting to a TLS-enabled coordinator
 
diff --git a/docs/trino-connector/installation.md 
b/docs/trino-connector/installation.md
index 9543261510..edb1824b51 100644
--- a/docs/trino-connector/installation.md
+++ b/docs/trino-connector/installation.md
@@ -145,7 +145,7 @@ gravitino.metalake=test
 ```
 
 - The `gravitino.name` defines which Gravitino Trino connector is used. It 
must be `gravitino`.
-- The `gravitino.metalake` defines which metalake are used. It should exist in 
the Gravitino server.
+- The `gravitino.metalake` defines which metalake is used. It should exist in 
the Gravitino server. It is optional: when omitted, the catalogs of every 
metalake are loaded, see [configuration](./configuration.md).
 - The `gravitino.uri` defines the connection information about Gravitino 
server. Make sure your container can access the Gravitino server.
 
 Full configurations for Apache Gravitino Trino connector can be seen 
[here](configuration.md)
diff --git a/docs/trino-connector/supported-catalog.md 
b/docs/trino-connector/supported-catalog.md
index 0c2d547eeb..762a5d5bbf 100644
--- a/docs/trino-connector/supported-catalog.md
+++ b/docs/trino-connector/supported-catalog.md
@@ -24,13 +24,14 @@ User can also use the system table `catalog` to describe 
all the catalogs.
 Create catalog:
 
 ```sql
-create_catalog(CATALOG varchar, PROVIDER varchar, PROPERTIES MAP(VARCHAR, 
VARCHAR), IGNORE_EXIST boolean);
+create_catalog(CATALOG varchar, PROVIDER varchar, PROPERTIES MAP(VARCHAR, 
VARCHAR), IGNORE_EXIST boolean, METALAKE varchar);
 ```
 
 - CATALOG: The catalog name to be created.
 - PROVIDER: The catalog provider. Supported values: `hive`, 
`lakehouse-iceberg`, `jdbc-mysql`, `jdbc-postgresql`, `glue`.
 - PROPERTIES: The properties of the catalog.
 - IGNORE_EXIST: The flag to ignore the error if the catalog already exists. 
It's optional, the default value is `false`.
+- METALAKE: The metalake to create the catalog in. It's optional, the default 
value is the configured `gravitino.metalake`; it is required when 
`gravitino.metalake` is unset.
 
 The type of catalog properties reference:
 - [Hive catalog](../apache-hive-catalog.md#catalog-properties)
@@ -43,30 +44,47 @@ The type of catalog properties reference:
 Drop catalog:
 
 ```sql
-drop_catalog(CATALOG varchar, IGNORE_NOT_EXIST boolean);
+drop_catalog(CATALOG varchar, IGNORE_NOT_EXIST boolean, METALAKE varchar);
 ```
 
 - CATALOG: The catalog name to be deleted.
 - IGNORE_NOT_EXIST: The flag to ignore the error if the catalog does not 
exist. It's optional, the default value is `false`.
+- METALAKE: The metalake the catalog belongs to. It's optional, the default 
value is the configured `gravitino.metalake`; it is required when 
`gravitino.metalake` is unset.
 
 
 Alter catalog:
 
 ```sql
-alter_catalog(CATALOG varchar, SET_PROPERTIES MAP(VARCHAR, VARCHAR), 
REMOVE_PROPERTIES ARRY[VARCHAR]);
+alter_catalog(CATALOG varchar, SET_PROPERTIES MAP(VARCHAR, VARCHAR), 
REMOVE_PROPERTIES ARRY[VARCHAR], METALAKE varchar);
 ```
 
 - CATALOG: The catalog name to be altered.
 - SET_PROPERTIES: The properties to be set.
 - REMOVE_PROPERTIES: The properties to be removed.
+- METALAKE: The metalake the catalog belongs to. It's optional, the default 
value is the configured `gravitino.metalake`; it is required when 
`gravitino.metalake` is unset.
+
+A metalake other than the configured one can only be targeted when every 
metalake is loaded
+(`gravitino.metalake` unset or `gravitino.catalog-name-with-metalake=true`). 
With unqualified
+catalog names, the procedures look the catalog up by name and metalake, so a 
catalog of another
+metalake holding the same Trino catalog name is not affected.
 
 These stored procedures are under the `gravitino` connector and the `system` 
schema.
-So you need to use the following SQL to call them in the `trino-cli`:
+So you need to use the following SQL to call them in the `trino-cli`, passing 
the metalake by name
+when `gravitino.metalake` is unset:
 
+```sql
+call gravitino.system.create_catalog(
+    catalog => 'gt_hive',
+    provider => 'hive',
+    properties => map(array['metastore.uris'], 
array['thrift://trino-ci-hive:9083']),
+    metalake => 'test'
+);
+```
 
 Describe catalogs:
 
-The system table `gravitino.system.catalog` is used to describe all the 
catalogs.
+The system table `gravitino.system.catalog` is used to describe all the 
catalogs of the configured
+metalake, or of every metalake when `gravitino.metalake` is unset.
 
 ```sql
 select * from gravitino.system.catalog;
@@ -75,11 +93,14 @@ select * from gravitino.system.catalog;
 The result is like:
 
 ```test
-     name     | provider |                                                 
properties
---------------+----------+-------------------------------------------------------------------------------------------------------------
- gt_hive      | hive     | 
{gravitino.bypass.hive.metastore.client.capability.check=false, 
metastore.uris=thrift://trino-ci-hive:9083}
+     name     | provider |                                                 
properties                                                  | metalake
+--------------+----------+-------------------------------------------------------------------------------------------------------------+----------
+ gt_hive      | hive     | 
{gravitino.bypass.hive.metastore.client.capability.check=false, 
metastore.uris=thrift://trino-ci-hive:9083} | test
 ```
 
+The `metalake` column tells apart catalogs of different metalakes that share a 
name when
+`gravitino.metalake` is unset.
+
 Check catalog registration status:
 
 `gravitino.system.catalog` lists the relational catalogs the Gravitino server 
knows about, minus any
@@ -254,6 +275,7 @@ Registration happens in the background, so a catalog that 
fails to register simp
 | A catalog is missing from `SHOW CATALOGS`                            | Query 
`gravitino.system.catalog_status` and read `status` and `last_error`, then 
follow the rows below              |
 | `status = FAILED`, `last_error` mentions `Access Denied`             | The 
`trino.jdbc.user` lacks a Trino system role permitted to run `CREATE CATALOG`   
                           |
 | `status = FAILED`, `last_error` mentions a configuration property    | A 
`trino.bypass.` property is not accepted by the underlying Trino connector      
                                  |
+| `status = FAILED`, `last_error` mentions `already registered by metalake` | 
Another metalake owns the same Trino catalog name. Rename the catalog or set 
`gravitino.catalog-name-with-metalake=true` |
 | `status = UNSUPPORTED`                                               | The 
catalog is not relational, or its provider is outside the supported list. 
`last_error` names the supported providers |
 | `status = SKIPPED`                                                   | The 
catalog matches `gravitino.trino.skip-catalog-patterns`                         
                                |
 | The catalog has no row in `catalog_status` at all                    | The 
load loop never reached it. Check `gravitino.system.load_status`                
                                |
diff --git 
a/trino-connector/integration-test/src/test/resources/trino-ci-testset/testsets/jdbc-mysql/00008_alter_catalog.txt
 
b/trino-connector/integration-test/src/test/resources/trino-ci-testset/testsets/jdbc-mysql/00008_alter_catalog.txt
index 501fa0d8aa..608e8a55cb 100644
--- 
a/trino-connector/integration-test/src/test/resources/trino-ci-testset/testsets/jdbc-mysql/00008_alter_catalog.txt
+++ 
b/trino-connector/integration-test/src/test/resources/trino-ci-testset/testsets/jdbc-mysql/00008_alter_catalog.txt
@@ -1,17 +1,17 @@
 CALL
 
-"gt_mysql_xxx1","jdbc-mysql","{""in-use"":""true"",""jdbc-driver"":""com.mysql.cj.jdbc.Driver"",""jdbc-url"":""jdbc:mysql://%/?useSSL=false""}"
+"gt_mysql_xxx1","jdbc-mysql","{""in-use"":""true"",""jdbc-driver"":""com.mysql.cj.jdbc.Driver"",""jdbc-url"":""jdbc:mysql://%/?useSSL=false""}","test"
 
 CALL
 
-"gt_mysql_xxx1","jdbc-mysql","{""in-use"":""true"",""jdbc-driver"":""com.mysql.cj.jdbc.Driver"",""jdbc-url"":""jdbc:mysql://%/?useSSL=false"",""test_key"":""test_value"",""trino.bypass.join-pushdown.strategy"":""EAGER""}"
+"gt_mysql_xxx1","jdbc-mysql","{""in-use"":""true"",""jdbc-driver"":""com.mysql.cj.jdbc.Driver"",""jdbc-url"":""jdbc:mysql://%/?useSSL=false"",""test_key"":""test_value"",""trino.bypass.join-pushdown.strategy"":""EAGER""}","test"
 
 CALL
 
-"gt_mysql_xxx1","jdbc-mysql","{""in-use"":""true"",""jdbc-driver"":""com.mysql.cj.jdbc.Driver"",""jdbc-url"":""jdbc:mysql://%/?useSSL=false"",""test_key"":""test_value""}"
+"gt_mysql_xxx1","jdbc-mysql","{""in-use"":""true"",""jdbc-driver"":""com.mysql.cj.jdbc.Driver"",""jdbc-url"":""jdbc:mysql://%/?useSSL=false"",""test_key"":""test_value""}","test"
 
 CALL
 
-"gt_mysql_xxx1","jdbc-mysql","{""in-use"":""true"",""jdbc-driver"":""com.mysql.cj.jdbc.Driver"",""jdbc-url"":""jdbc:mysql://%/?useSSL=false"",""trino.bypass.join-pushdown.strategy"":""EAGER""}"
+"gt_mysql_xxx1","jdbc-mysql","{""in-use"":""true"",""jdbc-driver"":""com.mysql.cj.jdbc.Driver"",""jdbc-url"":""jdbc:mysql://%/?useSSL=false"",""trino.bypass.join-pushdown.strategy"":""EAGER""}","test"
 
 CALL
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConfig.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConfig.java
index 107cde4b82..4113101418 100644
--- 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConfig.java
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConfig.java
@@ -104,9 +104,10 @@ public class GravitinoConfig {
   private static final ConfigEntry GRAVITINO_METALAKE =
       new ConfigEntry(
           "gravitino.metalake",
-          "The name of the metalake (top-level namespace) to connect to",
+          "The name of the metalake (top-level namespace) to connect to. "
+              + "When unset, the catalogs of every metalake are loaded.",
           "",
-          true);
+          false);
 
   private static final ConfigEntry GRAVITINO_USER =
       new ConfigEntry(
@@ -116,25 +117,39 @@ public class GravitinoConfig {
           false);
 
   /**
-   * @deprecated Please use {@code gravitino.use-single-metalake} instead.
+   * @deprecated Please use {@code gravitino.catalog-name-with-metalake} 
instead.
    */
   @Deprecated
   @SuppressWarnings("UnusedVariable")
   private static final ConfigEntry GRAVITINO_SIMPLIFY_CATALOG_NAMES =
       new ConfigEntry(
           "gravitino.simplify-catalog-names",
-          "Deprecated: omits the metalake prefix from catalog names. Use 
gravitino.use-single-metalake instead.",
+          "Deprecated: omits the metalake prefix from catalog names. Use 
gravitino.catalog-name-with-metalake instead.",
           "true",
           false);
 
+  /**
+   * Legacy switch, read only when {@code 
gravitino.catalog-name-with-metalake} is unset.
+   *
+   * @deprecated Please use {@code gravitino.catalog-name-with-metalake} 
instead.
+   */
+  @Deprecated
   private static final ConfigEntry GRAVITINO_SINGLE_METALAKE_MODE =
       new ConfigEntry(
           "gravitino.use-single-metalake",
-          "If true, only one metalake is supported in this connector; identify 
the catalog by <catalog_name>. "
-              + "If false, multiple metalakes are supported; identify the 
catalog by <metalake_name>.<catalog_name>.",
+          "Deprecated: if false, identify the catalog by 
<metalake_name>.<catalog_name> and load "
+              + "every metalake. Use gravitino.catalog-name-with-metalake 
instead.",
           "true",
           false);
 
+  private static final ConfigEntry GRAVITINO_CATALOG_NAME_WITH_METALAKE =
+      new ConfigEntry(
+          "gravitino.catalog-name-with-metalake",
+          "If true, identify the catalog by <metalake_name>.<catalog_name> and 
load every metalake. "
+              + "If false, identify the catalog by <catalog_name>.",
+          "false",
+          false);
+
   private static final ConfigEntry GRAVITINO_CLOUD_REGION_CODE =
       new ConfigEntry(
           "gravitino.cloud.region-code",
@@ -347,9 +362,10 @@ public class GravitinoConfig {
   }
 
   /**
-   * Retrieves the metalake name for used.
+   * Retrieves the configured metalake name.
    *
-   * @return the metalake name for used
+   * @return the trimmed metalake name, or an empty string when {@code 
gravitino.metalake} is unset;
+   *     see {@link #hasMetalake()}
    */
   public String getMetalake() {
     // Trimmed so a stray leading/trailing space in the catalog properties 
file does not make this
@@ -358,6 +374,25 @@ public class GravitinoConfig {
     return config.getOrDefault(GRAVITINO_METALAKE.key, 
GRAVITINO_METALAKE.defaultValue).trim();
   }
 
+  /**
+   * Whether a metalake is configured.
+   *
+   * @return true if {@code gravitino.metalake} is set to a non-blank value
+   */
+  public boolean hasMetalake() {
+    return !getMetalake().isEmpty();
+  }
+
+  /**
+   * Whether the catalogs of every metalake are loaded. This is the case when 
no metalake is
+   * configured, or when catalog names carry the metalake.
+   *
+   * @return true if all metalakes are loaded
+   */
+  public boolean loadAllMetalakes() {
+    return !hasMetalake() || catalogNameWithMetalake();
+  }
+
   /**
    * Retrieves the username for simple authentication.
    *
@@ -379,14 +414,33 @@ public class GravitinoConfig {
   }
 
   /**
-   * Retrieves the single metalake mode.
+   * Whether Trino catalog names carry the metalake, as {@code 
"<metalake>.<catalog>"}. The
+   * deprecated {@code gravitino.use-single-metalake=false} is honored when 
the new key is unset.
    *
-   * @return the single metalake mode
+   * @return true if catalog names are qualified with the metalake
    */
-  public boolean singleMetalakeMode() {
-    return Boolean.parseBoolean(
-        config.getOrDefault(
-            GRAVITINO_SINGLE_METALAKE_MODE.key, 
GRAVITINO_SINGLE_METALAKE_MODE.defaultValue));
+  public boolean catalogNameWithMetalake() {
+    String value = config.get(GRAVITINO_CATALOG_NAME_WITH_METALAKE.key);
+    if (value != null) {
+      return parseBooleanConfig(GRAVITINO_CATALOG_NAME_WITH_METALAKE.key, 
value.trim());
+    }
+    return !parseBooleanConfig(
+        GRAVITINO_SINGLE_METALAKE_MODE.key,
+        config
+            .getOrDefault(
+                GRAVITINO_SINGLE_METALAKE_MODE.key, 
GRAVITINO_SINGLE_METALAKE_MODE.defaultValue)
+            .trim());
+  }
+
+  /**
+   * Whether the deprecated {@code gravitino.use-single-metalake} key is 
present and still decides
+   * the catalog naming, so that its use can be reported.
+   *
+   * @return true if the deprecated key is set and the replacing key is not
+   */
+  public boolean usesDeprecatedSingleMetalakeKey() {
+    return config.containsKey(GRAVITINO_SINGLE_METALAKE_MODE.key)
+        && !config.containsKey(GRAVITINO_CATALOG_NAME_WITH_METALAKE.key);
   }
 
   boolean isDynamicConnector() {
@@ -647,15 +701,17 @@ public class GravitinoConfig {
         stringList.add(String.format("\"%s\"='%s'", entry.getKey(), value));
       }
     }
-    // copy the configuration by the prefix of GRAVITINO_CLIENT_CONFIG_PREFIX 
and
-    // GRAVITINO_ICEBERG_REST_CATALOG_CONFIG_PREFIX
+    // Copy configuration with prefixes that are not represented by exact 
entries in
+    // CONFIG_DEFINITIONS. In particular, scoped Iceberg REST URIs must reach 
dynamic catalogs so
+    // that workers use the same per-metalake endpoint as the coordinator.
     config.entrySet().stream()
         .filter(
             entry ->
                 (entry.getKey().startsWith(GRAVITINO_CLIENT_CONFIG_PREFIX.key)
                         || entry
                             .getKey()
-                            
.startsWith(GRAVITINO_ICEBERG_REST_CATALOG_CONFIG_PREFIX.key))
+                            
.startsWith(GRAVITINO_ICEBERG_REST_CATALOG_CONFIG_PREFIX.key)
+                        || 
entry.getKey().startsWith(GRAVITINO_ICEBERG_REST_URI.key + "."))
                     && 
!GravitinoConnectorFactory.isSecuritySensitivePropertyName(entry.getKey()))
         .forEach(
             entry ->
@@ -802,10 +858,8 @@ public class GravitinoConfig {
    * Unlike the discovered endpoint, this is plain local file configuration 
and is therefore
    * identical and valid on every node — coordinator and workers alike.
    *
-   * <p>{@code gravitino.iceberg.rest-uri.<metalake>} is checked first. The 
unscoped {@code
-   * gravitino.iceberg.rest-uri} is honored only in single-metalake mode, 
where it is unambiguous;
-   * in multi-metalake mode it is ignored, since a single Iceberg REST server 
serves exactly one
-   * metalake and applying it to every metalake would misroute the others.
+   * <p>{@code gravitino.iceberg.rest-uri.<metalake>} is checked first and 
overrides the unscoped
+   * {@code gravitino.iceberg.rest-uri}, which is the default for every 
metalake.
    *
    * @param metalake the metalake to resolve the override for
    * @return the manually configured Iceberg REST server endpoint, or an empty 
string when unset
@@ -815,11 +869,8 @@ public class GravitinoConfig {
     if (StringUtils.isNotBlank(scopedValue)) {
       return scopedValue;
     }
-    if (singleMetalakeMode()) {
-      return config.getOrDefault(
-          GRAVITINO_ICEBERG_REST_URI.key, 
GRAVITINO_ICEBERG_REST_URI.defaultValue);
-    }
-    return GRAVITINO_ICEBERG_REST_URI.defaultValue;
+    return config.getOrDefault(
+        GRAVITINO_ICEBERG_REST_URI.key, 
GRAVITINO_ICEBERG_REST_URI.defaultValue);
   }
 
   /**
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConnectorFactory.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConnectorFactory.java
index 739a7f1d50..f5d7266d20 100644
--- 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConnectorFactory.java
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConnectorFactory.java
@@ -24,7 +24,6 @@ import static 
org.apache.gravitino.trino.connector.GravitinoErrorCode.GRAVITINO_
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
-import com.google.common.base.Strings;
 import io.airlift.log.Logger;
 import io.trino.spi.HostAddress;
 import io.trino.spi.TrinoException;
@@ -179,13 +178,10 @@ public class GravitinoConnectorFactory implements 
ConnectorFactory {
     } else {
       // The static connector is an instance of GravitinoSystemConnector. It 
is loaded by Trino
       // using the connector configuration.
-      String metalake = config.getMetalake();
-      if (Strings.isNullOrEmpty(metalake)) {
-        throw new TrinoException(
-            GravitinoErrorCode.GRAVITINO_METALAKE_NOT_EXISTS, "No gravitino 
metalake selected");
-      }
       // Built per entry catalog, like the stored procedures: both are scoped 
to this catalog's
-      // metalake even though the underlying manager is shared.
+      // metalake even though the underlying manager is shared. Without a 
configured metalake
+      // they cover every metalake the manager loads.
+      String metalake = config.hasMetalake() ? config.getMetalake() : null;
       GravitinoStoredProcedureFactory gravitinoStoredProcedureFactory =
           new GravitinoStoredProcedureFactory(catalogConnectorManager, 
metalake);
       GravitinoSystemTableFactory systemTableFactory =
@@ -224,10 +220,10 @@ public class GravitinoConnectorFactory implements 
ConnectorFactory {
     trinoVersion = parseTrinoSpiVersion(spiVersion);
 
     // check catalog name with metalake are supported in this trino version
-    if (!config.singleMetalakeMode() && !supportCatalogNameWithMetalake()) {
+    if (config.catalogNameWithMetalake() && !supportCatalogNameWithMetalake()) 
{
       LOG.warn(
           "The trino-connector-%s-%s does not fully support catalog name with 
metalake. "
-              + "The DROP CATALOG operation may not work correctly in 
multi-metalake mode.",
+              + "The DROP CATALOG operation may not work correctly when 
catalog names carry the metalake.",
           getMinSupportTrinoSpiVersion(), getMaxSupportTrinoSpiVersion());
     }
 
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/CatalogConnectorManager.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/CatalogConnectorManager.java
index fdc369c559..d7fed43f55 100644
--- 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/CatalogConnectorManager.java
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/CatalogConnectorManager.java
@@ -26,6 +26,7 @@ import io.trino.spi.TrinoException;
 import io.trino.spi.connector.ConnectorContext;
 import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -75,9 +76,11 @@ public class CatalogConnectorManager {
   private final ConcurrentHashMap<String, CatalogConnectorContext> 
catalogConnectors =
       new ConcurrentHashMap<>();
 
-  // The registration state of every catalog seen by the load loop, keyed by 
the Trino catalog
-  // name. Written only by the load loop thread, read by query threads through 
the system tables
-  // and by stored procedure threads through describeRegistrationFailure().
+  // The registration state of every catalog seen by the load loop, keyed by 
the metalake and the
+  // Trino catalog name (see stateKey), since two metalakes can report the 
same Trino catalog name
+  // when every metalake is loaded with unqualified names. Written only by the 
load loop thread,
+  // read by query threads through the system tables and by stored procedure 
threads through
+  // describeRegistrationFailure().
   private final ConcurrentHashMap<String, CatalogRegistrationState> 
catalogStates =
       new ConcurrentHashMap<>();
 
@@ -183,6 +186,14 @@ public class CatalogConnectorManager {
     // Parsed eagerly so a misconfigured value fails startup instead of 
surfacing every poll as an
     // unrelated "Load Metalake failed" error.
     config.isIcebergRestRoutingEnabled();
+    if (config.usesDeprecatedSingleMetalakeKey()) {
+      LOG.warn(
+          "gravitino.use-single-metalake is deprecated, use 
gravitino.catalog-name-with-metalake "
+              + "instead.");
+    }
+    if (!config.hasMetalake()) {
+      LOG.info("gravitino.metalake is not set, the catalogs of every metalake 
are loaded.");
+    }
   }
 
   /**
@@ -228,8 +239,12 @@ public class CatalogConnectorManager {
       }
       trinoReachable = true;
 
-      Set<String> usedMetalakes = new HashSet<>();
-      if (config.singleMetalakeMode()) {
+      // Iterated in the server's listing order, so that with unqualified 
catalog names a clash
+      // between two metalakes first seen in the same cycle follows that 
order. Once a metalake
+      // has registered a Trino name it keeps it across cycles (see the 
ownership check in
+      // loadCatalogs), whatever the order becomes later.
+      Set<String> usedMetalakes = new LinkedHashSet<>();
+      if (!config.loadAllMetalakes()) {
         usedMetalakes.add(targetMetalake);
         metalakes.computeIfAbsent(targetMetalake, this::retrieveMetalake);
       } else {
@@ -307,9 +322,7 @@ public class CatalogConnectorManager {
     catalogStates
         .values()
         .removeIf(
-            state ->
-                !usedMetalakes.contains(state.getMetalake())
-                    && 
!catalogConnectors.containsKey(state.getTrinoCatalogName()));
+            state -> !usedMetalakes.contains(state.getMetalake()) && 
!hasLiveConnector(state));
     metalakeErrors.keySet().removeIf(metalakeName -> 
!usedMetalakes.contains(metalakeName));
     metalakes.keySet().removeIf(metalakeName -> 
!usedMetalakes.contains(metalakeName));
   }
@@ -517,13 +530,31 @@ public class CatalogConnectorManager {
             state ->
                 state.getMetalake().equals(metalakeName)
                     && !presentTrinoNames.contains(state.getTrinoCatalogName())
-                    && 
!catalogConnectors.containsKey(state.getTrinoCatalogName()));
+                    && !hasLiveConnector(state));
 
     // Load new catalogs belows to the metalake.
     for (String catalogName : catalogNames) {
       String trinoCatalogName = getTrinoCatalogName(metalakeName, catalogName);
+      // With unqualified names a catalog of another metalake may already hold 
this Trino name.
+      // The first one registered keeps it; this one is reported and left 
alone, so that it is
+      // neither loaded over the existing connector nor treated as a refresh 
of it.
+      CatalogConnectorContext existing = 
catalogConnectors.get(trinoCatalogName);
+      if (existing != null && 
!existing.getCatalog().getMetalake().equals(metalakeName)) {
+        recordCatalogState(
+            CatalogRegistrationState.failed(
+                metalakeName,
+                catalogName,
+                trinoCatalogName,
+                null,
+                String.format(
+                    "Trino catalog name %s is already registered by metalake 
%s. Rename the "
+                        + "catalog or set 
gravitino.catalog-name-with-metalake=true",
+                    trinoCatalogName, existing.getCatalog().getMetalake())),
+            null);
+        continue;
+      }
       // Known before the catalog is even loaded, since it only depends on the 
name.
-      boolean alreadyRegistered = 
catalogConnectors.containsKey(trinoCatalogName);
+      boolean alreadyRegistered = existing != null;
       // Tracked outside the try so that a failure can still report the 
provider it knows about.
       String provider = null;
       try {
@@ -598,7 +629,7 @@ public class CatalogConnectorManager {
     CatalogRegistrationState[] seen = new CatalogRegistrationState[1];
     CatalogRegistrationState state =
         catalogStates.compute(
-            newState.getTrinoCatalogName(),
+            stateKey(newState.getMetalake(), newState.getTrinoCatalogName()),
             (name, previous) -> {
               seen[0] = previous;
               return newState.withHistoryOf(previous);
@@ -738,6 +769,21 @@ public class CatalogConnectorManager {
     return catalogConnectors.get(catalogName);
   }
 
+  /**
+   * Retrieves the catalog connector context of a Gravitino catalog. With 
unqualified catalog names
+   * the Trino name may be held by a catalog of another metalake, which is not 
the one asked for.
+   *
+   * @param metalake the metalake the catalog belongs to
+   * @param catalogName the name of the catalog in Gravitino
+   * @return the catalog connector context, or null if this catalog is not 
registered
+   */
+  @Nullable
+  public CatalogConnectorContext getCatalogConnector(String metalake, String 
catalogName) {
+    CatalogConnectorContext context =
+        catalogConnectors.get(getTrinoCatalogName(metalake, catalogName));
+    return context != null && 
context.getCatalog().getMetalake().equals(metalake) ? context : null;
+  }
+
   /**
    * Checks if a catalog connector exists for the specified catalog name.
    *
@@ -779,7 +825,7 @@ public class CatalogConnectorManager {
    * @return the Trino catalog name
    */
   public String getTrinoCatalogName(String metalake, String catalog) {
-    return config.singleMetalakeMode()
+    return !config.catalogNameWithMetalake()
         ? catalog
         : trinoCatalogNameHandler.getCatalogName(metalake, catalog);
   }
@@ -796,13 +842,11 @@ public class CatalogConnectorManager {
 
   /**
    * Retrieves a snapshot of the registration state of every Gravitino catalog 
seen by the load
-   * loop. Package-private: production callers always know which metalake they 
report on and use
-   * {@link #getCatalogRegistrationStates(String)}; this exists for tests that 
assert on the whole
-   * set of tracked catalogs.
+   * loop.
    *
    * @return the registration states
    */
-  List<CatalogRegistrationState> getCatalogRegistrationStates() {
+  public List<CatalogRegistrationState> getCatalogRegistrationStates() {
     return List.copyOf(catalogStates.values());
   }
 
@@ -810,15 +854,29 @@ public class CatalogConnectorManager {
    * Retrieves a snapshot of the registration state of every Gravitino catalog 
seen by the load loop
    * that belongs to the given metalake.
    *
-   * @param metalake the metalake to filter by
+   * @param metalake the metalake to filter by, or null for every metalake
    * @return the registration states belonging to that metalake
    */
-  public List<CatalogRegistrationState> getCatalogRegistrationStates(String 
metalake) {
+  public List<CatalogRegistrationState> getCatalogRegistrationStates(@Nullable 
String metalake) {
+    if (metalake == null) {
+      return getCatalogRegistrationStates();
+    }
     return catalogStates.values().stream()
         .filter(state -> state.getMetalake().equals(metalake))
         .toList();
   }
 
+  private static String stateKey(String metalake, String trinoCatalogName) {
+    return metalake + "." + trinoCatalogName;
+  }
+
+  // Whether the catalog this state describes still has its connector in 
Trino. The connector under
+  // the same Trino name may belong to another metalake, which says nothing 
about this catalog.
+  private boolean hasLiveConnector(CatalogRegistrationState state) {
+    CatalogConnectorContext live = 
catalogConnectors.get(state.getTrinoCatalogName());
+    return live != null && 
live.getCatalog().getMetalake().equals(state.getMetalake());
+  }
+
   /**
    * Checks whether the Trino server answered the last time the load loop 
probed it. No catalog can
    * be registered while it does not.
@@ -866,7 +924,7 @@ public class CatalogConnectorManager {
    * @return a human readable explanation
    */
   public String describeRegistrationFailure(String metalake, String 
trinoCatalogName) {
-    CatalogRegistrationState state = catalogStates.get(trinoCatalogName);
+    CatalogRegistrationState state = catalogStates.get(stateKey(metalake, 
trinoCatalogName));
     if (state != null && state.getLastError() != null) {
       return String.format("%s: %s", state.getStatus(), state.getLastError());
     }
@@ -913,12 +971,15 @@ public class CatalogConnectorManager {
       String catalogConfig = config.getCatalogConfig();
 
       GravitinoCatalog catalog = GravitinoCatalog.fromJson(catalogConfig);
-      if (this.config.singleMetalakeMode()
+      if (!this.config.catalogNameWithMetalake()
           && StringUtils.isNotBlank(targetMetalake)
           && !targetMetalake.equals(catalog.getMetalake())) {
         throw new TrinoException(
             GravitinoErrorCode.GRAVITINO_UNSUPPORTED_OPERATION,
-            "Multiple metalakes are not supported");
+            String.format(
+                "Catalog %s belongs to metalake %s but this connector is 
configured for metalake "
+                    + "%s; set gravitino.catalog-name-with-metalake=true to 
serve several metalakes",
+                catalog.getName(), catalog.getMetalake(), targetMetalake));
       }
       GravitinoMetalake metalake =
           metalakes.computeIfAbsent(catalog.getMetalake(), 
this::retrieveMetalake);
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/storedprocedure/AlterCatalogStoredProcedure.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/storedprocedure/AlterCatalogStoredProcedure.java
index 43f53933e9..f0dcf7cf0e 100644
--- 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/storedprocedure/AlterCatalogStoredProcedure.java
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/storedprocedure/AlterCatalogStoredProcedure.java
@@ -33,6 +33,7 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import javax.annotation.Nullable;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.CatalogChange;
 import org.apache.gravitino.NameIdentifier;
@@ -53,29 +54,30 @@ public class AlterCatalogStoredProcedure extends 
GravitinoStoredProcedure {
   private static final Logger LOG = 
Logger.get(AlterCatalogStoredProcedure.class);
 
   private final CatalogConnectorManager catalogConnectorManager;
-  private final String metalake;
+  @Nullable private final String configuredMetalake;
 
   /**
    * Constructs a new AlterCatalogStoredProcedure.
    *
    * @param catalogConnectorManager the catalog connector manager
-   * @param metalake the metalake name
+   * @param configuredMetalake the metalake name, or null when the connector 
is not configured with
+   *     one
    */
   public AlterCatalogStoredProcedure(
-      CatalogConnectorManager catalogConnectorManager, String metalake) {
+      CatalogConnectorManager catalogConnectorManager, @Nullable String 
configuredMetalake) {
     this.catalogConnectorManager = catalogConnectorManager;
-    this.metalake = metalake;
+    this.configuredMetalake = configuredMetalake;
   }
 
   @Override
   public Procedure createStoredProcedure() throws NoSuchMethodException, 
IllegalAccessException {
-    // call gravitino.system.alter_catalog(catalogName, set_properties, 
remove_properties
+    // call gravitino.system.alter_catalog(catalogName, set_properties, 
remove_properties, metalake)
 
     MethodHandle dropCatalog =
         MethodHandles.lookup()
             .unreflect(
                 AlterCatalogStoredProcedure.class.getMethod(
-                    "alterCatalog", String.class, Map.class, List.class))
+                    "alterCatalog", String.class, Map.class, List.class, 
String.class))
             .bindTo(this);
     List<Procedure.Argument> arguments =
         List.of(
@@ -87,7 +89,8 @@ public class AlterCatalogStoredProcedure extends 
GravitinoStoredProcedure {
                 new ArrayType(VARCHAR),
                 false,
                 ArrayBlock.fromElementBlock(
-                    0, Optional.empty(), new int[1], 
VARCHAR.createBlockBuilder(null, 1).build())));
+                    0, Optional.empty(), new int[1], 
VARCHAR.createBlockBuilder(null, 1).build())),
+            new Procedure.Argument(METALAKE_ARGUMENT, VARCHAR, false, null));
     return new Procedure(
         GravitinoSystemTable.SYSTEM_TABLE_SCHEMA_NAME, "alter_catalog", 
arguments, dropCatalog);
   }
@@ -98,14 +101,27 @@ public class AlterCatalogStoredProcedure extends 
GravitinoStoredProcedure {
    * @param catalogName the name of the catalog to alter
    * @param setProperties the properties to set
    * @param removeProperties the properties to remove
+   * @param metalakeArgument the metalake the catalog belongs to, null to use 
the configured one
    * @throws TrinoException if the catalog does not exist or the operation 
fails
    */
   public void alterCatalog(
-      String catalogName, Map<String, String> setProperties, List<String> 
removeProperties) {
+      String catalogName,
+      Map<String, String> setProperties,
+      List<String> removeProperties,
+      @Nullable String metalakeArgument) {
+    String metalake = resolveMetalake(configuredMetalake, metalakeArgument);
     try {
       CatalogConnectorContext catalogConnectorContext =
-          catalogConnectorManager.getCatalogConnector(
-              catalogConnectorManager.getTrinoCatalogName(metalake, 
catalogName));
+          catalogConnectorManager.getCatalogConnector(metalake, catalogName);
+      if (catalogConnectorContext == null) {
+        throw new TrinoException(
+            GravitinoErrorCode.GRAVITINO_CATALOG_NOT_EXISTS,
+            String.format(
+                "Catalog %s is not registered in Trino. %s",
+                NameIdentifier.of(metalake, catalogName),
+                catalogConnectorManager.describeRegistrationFailure(
+                    metalake, 
catalogConnectorManager.getTrinoCatalogName(metalake, catalogName))));
+      }
       GravitinoCatalog oldCatalog = catalogConnectorContext.getCatalog();
 
       List<CatalogChange> changes = new ArrayList<>();
@@ -136,15 +152,15 @@ public class AlterCatalogStoredProcedure extends 
GravitinoStoredProcedure {
           .alterCatalog(catalogName, changes.toArray(changes.toArray(new 
CatalogChange[0])));
 
       catalogConnectorManager.loadMetalakeSync();
-      String trinoCatalogName = 
catalogConnectorManager.getTrinoCatalogName(metalake, catalogName);
-      catalogConnectorContext = 
catalogConnectorManager.getCatalogConnector(trinoCatalogName);
+      catalogConnectorContext = 
catalogConnectorManager.getCatalogConnector(metalake, catalogName);
       if (catalogConnectorContext == null
           || catalogConnectorContext.getCatalog().getLastModifiedTime()
               == oldCatalog.getLastModifiedTime()) {
         throw new TrinoException(
             GravitinoErrorCode.GRAVITINO_OPERATION_FAILED,
             "Update catalog failed due to the reloading process fails. "
-                + 
catalogConnectorManager.describeRegistrationFailure(metalake, 
trinoCatalogName));
+                + catalogConnectorManager.describeRegistrationFailure(
+                    metalake, 
catalogConnectorManager.getTrinoCatalogName(metalake, catalogName)));
       }
       LOG.info("Alter catalog %s in metalake %s successfully.", catalogName, 
metalake);
 
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/storedprocedure/CreateCatalogStoredProcedure.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/storedprocedure/CreateCatalogStoredProcedure.java
index 2bc7180d8a..405ef0ce73 100644
--- 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/storedprocedure/CreateCatalogStoredProcedure.java
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/storedprocedure/CreateCatalogStoredProcedure.java
@@ -30,6 +30,7 @@ import java.lang.invoke.MethodHandle;
 import java.lang.invoke.MethodHandles;
 import java.util.List;
 import java.util.Map;
+import javax.annotation.Nullable;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.Catalog;
 import org.apache.gravitino.NameIdentifier;
@@ -49,28 +50,34 @@ public class CreateCatalogStoredProcedure extends 
GravitinoStoredProcedure {
   private static final Logger LOG = 
Logger.get(CreateCatalogStoredProcedure.class);
 
   private final CatalogConnectorManager catalogConnectorManager;
-  private final String metalake;
+  @Nullable private final String configuredMetalake;
 
   /**
    * Constructs a new CreateCatalogStoredProcedure.
    *
    * @param catalogConnectorManager the catalog connector manager
-   * @param metalake the metalake name
+   * @param configuredMetalake the metalake name, or null when the connector 
is not configured with
+   *     one
    */
   public CreateCatalogStoredProcedure(
-      CatalogConnectorManager catalogConnectorManager, String metalake) {
+      CatalogConnectorManager catalogConnectorManager, @Nullable String 
configuredMetalake) {
     this.catalogConnectorManager = catalogConnectorManager;
-    this.metalake = metalake;
+    this.configuredMetalake = configuredMetalake;
   }
 
   @Override
   public Procedure createStoredProcedure() throws NoSuchMethodException, 
IllegalAccessException {
-    // call gravitino.system.create_catalog(catalog, provider, properties, 
ignore_exist)
+    // call gravitino.system.create_catalog(catalog, provider, properties, 
ignore_exist, metalake)
     MethodHandle createCatalog =
         MethodHandles.lookup()
             .unreflect(
                 CreateCatalogStoredProcedure.class.getMethod(
-                    "createCatalog", String.class, String.class, Map.class, 
boolean.class))
+                    "createCatalog",
+                    String.class,
+                    String.class,
+                    Map.class,
+                    boolean.class,
+                    String.class))
             .bindTo(this);
 
     List<Procedure.Argument> arguments =
@@ -79,7 +86,8 @@ public class CreateCatalogStoredProcedure extends 
GravitinoStoredProcedure {
             new Procedure.Argument("PROVIDER", VARCHAR),
             new Procedure.Argument(
                 "PROPERTIES", new MapType(VARCHAR, VARCHAR, new 
TypeOperators())),
-            new Procedure.Argument("IGNORE_EXIST", BOOLEAN, false, false));
+            new Procedure.Argument("IGNORE_EXIST", BOOLEAN, false, false),
+            new Procedure.Argument(METALAKE_ARGUMENT, VARCHAR, false, null));
 
     return new Procedure(
         GravitinoSystemTable.SYSTEM_TABLE_SCHEMA_NAME, "create_catalog", 
arguments, createCatalog);
@@ -92,13 +100,17 @@ public class CreateCatalogStoredProcedure extends 
GravitinoStoredProcedure {
    * @param provider the provider of the catalog
    * @param properties the properties of the catalog
    * @param ignoreExist whether to ignore if the catalog already exists
+   * @param metalakeArgument the metalake to create the catalog in, null to 
use the configured one
    * @throws TrinoException if the catalog already exists and ignoreExist is 
false
    */
   public void createCatalog(
-      String catalogName, String provider, Map<String, String> properties, 
boolean ignoreExist) {
-    boolean exists =
-        catalogConnectorManager.catalogConnectorExist(
-            catalogConnectorManager.getTrinoCatalogName(metalake, 
catalogName));
+      String catalogName,
+      String provider,
+      Map<String, String> properties,
+      boolean ignoreExist,
+      @Nullable String metalakeArgument) {
+    String metalake = resolveMetalake(configuredMetalake, metalakeArgument);
+    boolean exists = catalogConnectorManager.getCatalogConnector(metalake, 
catalogName) != null;
     if (exists) {
       if (!ignoreExist) {
         throw new TrinoException(
@@ -115,12 +127,12 @@ public class CreateCatalogStoredProcedure extends 
GravitinoStoredProcedure {
               catalogName, Catalog.Type.RELATIONAL, provider, "Trino created", 
properties);
 
       catalogConnectorManager.loadMetalakeSync();
-      String trinoCatalogName = 
catalogConnectorManager.getTrinoCatalogName(metalake, catalogName);
-      if (!catalogConnectorManager.catalogConnectorExist(trinoCatalogName)) {
+      if (catalogConnectorManager.getCatalogConnector(metalake, catalogName) 
== null) {
         throw new TrinoException(
             GravitinoErrorCode.GRAVITINO_OPERATION_FAILED,
             "Create catalog failed due to the loading process fails. "
-                + 
catalogConnectorManager.describeRegistrationFailure(metalake, 
trinoCatalogName));
+                + catalogConnectorManager.describeRegistrationFailure(
+                    metalake, 
catalogConnectorManager.getTrinoCatalogName(metalake, catalogName)));
       }
 
       LOG.info("Create catalog %s in metalake %s successfully.", catalogName, 
metalake);
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/storedprocedure/DropCatalogStoredProcedure.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/storedprocedure/DropCatalogStoredProcedure.java
index eea2fd7815..a813a35df1 100644
--- 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/storedprocedure/DropCatalogStoredProcedure.java
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/storedprocedure/DropCatalogStoredProcedure.java
@@ -27,6 +27,7 @@ import io.trino.spi.procedure.Procedure;
 import java.lang.invoke.MethodHandle;
 import java.lang.invoke.MethodHandles;
 import java.util.List;
+import javax.annotation.Nullable;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.exceptions.NoSuchMetalakeException;
@@ -45,33 +46,35 @@ public class DropCatalogStoredProcedure extends 
GravitinoStoredProcedure {
   private static final Logger LOG = 
Logger.get(DropCatalogStoredProcedure.class);
 
   private final CatalogConnectorManager catalogConnectorManager;
-  private final String metalake;
+  @Nullable private final String configuredMetalake;
 
   /**
    * Constructs a new DropCatalogStoredProcedure.
    *
    * @param catalogConnectorManager the catalog connector manager
-   * @param metalake the metalake name
+   * @param configuredMetalake the metalake name, or null when the connector 
is not configured with
+   *     one
    */
   public DropCatalogStoredProcedure(
-      CatalogConnectorManager catalogConnectorManager, String metalake) {
+      CatalogConnectorManager catalogConnectorManager, @Nullable String 
configuredMetalake) {
     this.catalogConnectorManager = catalogConnectorManager;
-    this.metalake = metalake;
+    this.configuredMetalake = configuredMetalake;
   }
 
   @Override
   public Procedure createStoredProcedure() throws NoSuchMethodException, 
IllegalAccessException {
-    // call gravitino.system.drop_catalog(catalog, ignore_not_exist)
+    // call gravitino.system.drop_catalog(catalog, ignore_not_exist, metalake)
     MethodHandle dropCatalog =
         MethodHandles.lookup()
             .unreflect(
                 DropCatalogStoredProcedure.class.getMethod(
-                    "dropCatalog", String.class, boolean.class))
+                    "dropCatalog", String.class, boolean.class, String.class))
             .bindTo(this);
     List<Procedure.Argument> arguments =
         List.of(
             new Procedure.Argument("CATALOG", VARCHAR),
-            new Procedure.Argument("IGNORE_NOT_EXIST", BOOLEAN, false, false));
+            new Procedure.Argument("IGNORE_NOT_EXIST", BOOLEAN, false, false),
+            new Procedure.Argument(METALAKE_ARGUMENT, VARCHAR, false, null));
     return new Procedure(
         GravitinoSystemTable.SYSTEM_TABLE_SCHEMA_NAME, "drop_catalog", 
arguments, dropCatalog);
   }
@@ -84,13 +87,15 @@ public class DropCatalogStoredProcedure extends 
GravitinoStoredProcedure {
    *
    * @param catalogName the name of the catalog to drop
    * @param ignoreNotExist whether to ignore if the catalog does not exist
+   * @param metalakeArgument the metalake to drop the catalog from, null to 
use the configured one
    * @throws TrinoException if the catalog does not exist and ignoreNotExist 
is false
    */
-  public void dropCatalog(String catalogName, boolean ignoreNotExist) {
+  public void dropCatalog(
+      String catalogName, boolean ignoreNotExist, @Nullable String 
metalakeArgument) {
+    String metalake = resolveMetalake(configuredMetalake, metalakeArgument);
     try {
       CatalogConnectorContext catalogConnector =
-          catalogConnectorManager.getCatalogConnector(
-              catalogConnectorManager.getTrinoCatalogName(metalake, 
catalogName));
+          catalogConnectorManager.getCatalogConnector(metalake, catalogName);
       if (catalogConnector == null) {
         boolean dropped =
             
catalogConnectorManager.getMetalake(metalake).dropCatalog(catalogName, true);
@@ -126,8 +131,7 @@ public class DropCatalogStoredProcedure extends 
GravitinoStoredProcedure {
 
       catalogConnectorManager.loadMetalakeSync();
 
-      if (catalogConnectorManager.catalogConnectorExist(
-          catalogConnectorManager.getTrinoCatalogName(metalake, catalogName))) 
{
+      if (catalogConnectorManager.getCatalogConnector(metalake, catalogName) 
!= null) {
         throw new TrinoException(
             GravitinoErrorCode.GRAVITINO_OPERATION_FAILED,
             "Drop catalog failed due to the reloading process fails");
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/storedprocedure/GravitinoStoredProcedure.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/storedprocedure/GravitinoStoredProcedure.java
index 487cecf4ce..045ce9f5cf 100644
--- 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/storedprocedure/GravitinoStoredProcedure.java
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/storedprocedure/GravitinoStoredProcedure.java
@@ -18,11 +18,18 @@
  */
 package org.apache.gravitino.trino.connector.system.storedprocedure;
 
+import io.trino.spi.TrinoException;
 import io.trino.spi.procedure.Procedure;
+import javax.annotation.Nullable;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.trino.connector.GravitinoErrorCode;
 
 /** Gravitino System stored procedure interfaces */
 public abstract class GravitinoStoredProcedure {
 
+  /** The name of the optional argument selecting the metalake a procedure 
operates on. */
+  protected static final String METALAKE_ARGUMENT = "METALAKE";
+
   /**
    * Return the definition of the stored procedure.
    *
@@ -30,4 +37,26 @@ public abstract class GravitinoStoredProcedure {
    * @throws Exception if creation of the procedure fails
    */
   public abstract Procedure createStoredProcedure() throws Exception;
+
+  /**
+   * Resolves the metalake a procedure call operates on: the METALAKE argument 
when given, otherwise
+   * the metalake the connector is configured with.
+   *
+   * @param configuredMetalake the metalake from {@code gravitino.metalake}, 
null when unset
+   * @param argument the METALAKE argument of the call, null when not passed
+   * @return the metalake name
+   * @throws TrinoException if neither is available
+   */
+  protected static String resolveMetalake(
+      @Nullable String configuredMetalake, @Nullable String argument) {
+    if (StringUtils.isNotBlank(argument)) {
+      return argument.trim();
+    }
+    if (configuredMetalake != null) {
+      return configuredMetalake;
+    }
+    throw new TrinoException(
+        GravitinoErrorCode.GRAVITINO_MISSING_CONFIG,
+        "No metalake specified: pass the METALAKE argument or set 
gravitino.metalake");
+  }
 }
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/storedprocedure/GravitinoStoredProcedureFactory.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/storedprocedure/GravitinoStoredProcedureFactory.java
index 7b920b6155..dae7c70c85 100644
--- 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/storedprocedure/GravitinoStoredProcedureFactory.java
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/storedprocedure/GravitinoStoredProcedureFactory.java
@@ -24,6 +24,7 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
 import java.util.stream.Collectors;
+import javax.annotation.Nullable;
 import org.apache.gravitino.trino.connector.GravitinoErrorCode;
 import org.apache.gravitino.trino.connector.catalog.CatalogConnectorManager;
 
@@ -31,7 +32,7 @@ import 
org.apache.gravitino.trino.connector.catalog.CatalogConnectorManager;
 public class GravitinoStoredProcedureFactory {
 
   private final CatalogConnectorManager catalogConnectorManager;
-  private final String metalake;
+  @Nullable private final String configuredMetalake;
 
   /** List of all registered Gravitino stored procedures */
   public final List<GravitinoStoredProcedure> procedures = new ArrayList<>();
@@ -40,21 +41,22 @@ public class GravitinoStoredProcedureFactory {
    * Constructs a new GravitinoStoredProcedureFactory.
    *
    * @param catalogConnectorManager the catalog connector manager
-   * @param metalake the metalake name
+   * @param configuredMetalake the metalake name, or null when the connector 
is not configured with
+   *     one
    */
   public GravitinoStoredProcedureFactory(
-      CatalogConnectorManager catalogConnectorManager, String metalake) {
+      CatalogConnectorManager catalogConnectorManager, @Nullable String 
configuredMetalake) {
     this.catalogConnectorManager = catalogConnectorManager;
-    this.metalake = metalake;
+    this.configuredMetalake = configuredMetalake;
 
     registerStoredProcedure();
   }
 
   /** Register all the stored procedures * */
   private void registerStoredProcedure() {
-    procedures.add(new CreateCatalogStoredProcedure(catalogConnectorManager, 
metalake));
-    procedures.add(new DropCatalogStoredProcedure(catalogConnectorManager, 
metalake));
-    procedures.add(new AlterCatalogStoredProcedure(catalogConnectorManager, 
metalake));
+    procedures.add(new CreateCatalogStoredProcedure(catalogConnectorManager, 
configuredMetalake));
+    procedures.add(new DropCatalogStoredProcedure(catalogConnectorManager, 
configuredMetalake));
+    procedures.add(new AlterCatalogStoredProcedure(catalogConnectorManager, 
configuredMetalake));
   }
 
   /**
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/table/GravitinoSystemTableCatalog.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/table/GravitinoSystemTableCatalog.java
index 55e4376ee9..07e8550ff2 100644
--- 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/table/GravitinoSystemTableCatalog.java
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/table/GravitinoSystemTableCatalog.java
@@ -32,6 +32,7 @@ import io.trino.spi.connector.SchemaTableName;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.TreeMap;
+import javax.annotation.Nullable;
 import org.apache.gravitino.Catalog;
 import org.apache.gravitino.client.GravitinoMetalake;
 import org.apache.gravitino.trino.connector.GravitinoErrorCode;
@@ -53,19 +54,20 @@ public class GravitinoSystemTableCatalog extends 
GravitinoSystemTable {
           List.of(
               
ColumnMetadata.builder().setName("name").setType(VARCHAR).build(),
               
ColumnMetadata.builder().setName("provider").setType(VARCHAR).build(),
-              
ColumnMetadata.builder().setName("properties").setType(VARCHAR).build()));
+              
ColumnMetadata.builder().setName("properties").setType(VARCHAR).build(),
+              
ColumnMetadata.builder().setName("metalake").setType(VARCHAR).build()));
 
   private final CatalogConnectorManager catalogConnectorManager;
-  private final String metalake;
+  @Nullable private final String metalake;
 
   /**
    * Constructs a new GravitinoSystemTableCatalog.
    *
    * @param catalogConnectorManager the manager for catalog connectors
-   * @param metalake the metalake to report on
+   * @param metalake the metalake to report on, or null for every metalake
    */
   public GravitinoSystemTableCatalog(
-      CatalogConnectorManager catalogConnectorManager, String metalake) {
+      CatalogConnectorManager catalogConnectorManager, @Nullable String 
metalake) {
     this.catalogConnectorManager = catalogConnectorManager;
     this.metalake = metalake;
   }
@@ -75,17 +77,19 @@ public class GravitinoSystemTableCatalog extends 
GravitinoSystemTable {
     List<GravitinoCatalog> gravitinoCatalogs = new ArrayList<>();
     // retrieve catalogs form the Gravitino server with the configuration 
metalakes,
     // the catalogConnectorManager does not manager catalogs in worker nodes
-    // Only the metalake this connector is configured with: the manager is 
shared by every entry
-    // catalog in this Trino.
+    // The manager is shared by every entry catalog in this Trino, so narrow 
to the metalake this
+    // connector is configured with; without one, report every metalake.
     catalogConnectorManager.getUsedMetalakes().stream()
-        .filter(metalake::equals)
+        .filter(metalakeName -> metalake == null || 
metalake.equals(metalakeName))
         .forEach(
             (metalakeName) -> {
-              GravitinoMetalake metalake = 
catalogConnectorManager.getMetalake(metalakeName);
-              Catalog[] catalogs = metalake.listCatalogsInfo();
+              GravitinoMetalake gravitinoMetalake =
+                  catalogConnectorManager.getMetalake(metalakeName);
+              Catalog[] catalogs = gravitinoMetalake.listCatalogsInfo();
               for (Catalog catalog : catalogs) {
                 // Must match against the same Trino-qualified name the load 
loop skips against
-                // (quoted "metalake.catalog" in multi-metalake mode), or a 
skip pattern written
+                // (quoted "metalake.catalog" when catalog names carry the 
metalake), or a skip
+                // pattern written
                 // against the qualified name never matches here.
                 String trinoCatalogName =
                     catalogConnectorManager.getTrinoCatalogName(metalakeName, 
catalog.name());
@@ -102,6 +106,7 @@ public class GravitinoSystemTableCatalog extends 
GravitinoSystemTable {
     BlockBuilder nameColumnBuilder = VARCHAR.createBlockBuilder(null, size);
     BlockBuilder providerColumnBuilder = VARCHAR.createBlockBuilder(null, 
size);
     BlockBuilder propertyColumnBuilder = VARCHAR.createBlockBuilder(null, 
size);
+    BlockBuilder metalakeColumnBuilder = VARCHAR.createBlockBuilder(null, 
size);
 
     for (GravitinoCatalog catalog : gravitinoCatalogs) {
       Preconditions.checkArgument(catalog != null, "catalog should not be 
null");
@@ -116,12 +121,14 @@ public class GravitinoSystemTableCatalog extends 
GravitinoSystemTable {
         throw new TrinoException(
             GravitinoErrorCode.GRAVITINO_ILLEGAL_ARGUMENT, "Invalid property 
format", e); //
       }
+      VARCHAR.writeString(metalakeColumnBuilder, catalog.getMetalake());
     }
     return new Page(
         size,
         nameColumnBuilder.build(),
         providerColumnBuilder.build(),
-        propertyColumnBuilder.build());
+        propertyColumnBuilder.build(),
+        metalakeColumnBuilder.build());
   }
 
   @Override
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/table/GravitinoSystemTableCatalogStatus.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/table/GravitinoSystemTableCatalogStatus.java
index de4ed697c7..9f3e4d9e3d 100644
--- 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/table/GravitinoSystemTableCatalogStatus.java
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/table/GravitinoSystemTableCatalogStatus.java
@@ -27,6 +27,7 @@ import io.trino.spi.connector.ColumnMetadata;
 import io.trino.spi.connector.ConnectorTableMetadata;
 import io.trino.spi.connector.SchemaTableName;
 import java.util.List;
+import javax.annotation.Nullable;
 import org.apache.gravitino.trino.connector.catalog.CatalogConnectorManager;
 import org.apache.gravitino.trino.connector.catalog.CatalogRegistrationState;
 
@@ -57,16 +58,16 @@ public class GravitinoSystemTableCatalogStatus extends 
GravitinoSystemTable {
               
ColumnMetadata.builder().setName("failure_count").setType(BIGINT).build()));
 
   private final CatalogConnectorManager catalogConnectorManager;
-  private final String metalake;
+  @Nullable private final String metalake;
 
   /**
    * Constructs a new GravitinoSystemTableCatalogStatus.
    *
    * @param catalogConnectorManager the manager for catalog connectors
-   * @param metalake the metalake to report on
+   * @param metalake the metalake to report on, or null for every metalake
    */
   public GravitinoSystemTableCatalogStatus(
-      CatalogConnectorManager catalogConnectorManager, String metalake) {
+      CatalogConnectorManager catalogConnectorManager, @Nullable String 
metalake) {
     this.catalogConnectorManager = catalogConnectorManager;
     this.metalake = metalake;
   }
@@ -76,7 +77,7 @@ public class GravitinoSystemTableCatalogStatus extends 
GravitinoSystemTable {
     // Take a snapshot first, the load loop writes these states concurrently 
and the column
     // builders must all end up with the same number of positions.
     // The load loop is shared by every entry catalog in this Trino, so report 
only the metalake
-    // this connector is configured with.
+    // this connector is configured with, or every metalake when none is 
configured.
     List<CatalogRegistrationState> states =
         catalogConnectorManager.getCatalogRegistrationStates(metalake);
     int size = states.size();
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/table/GravitinoSystemTableFactory.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/table/GravitinoSystemTableFactory.java
index 4b41c8d623..8ee9254832 100644
--- 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/table/GravitinoSystemTableFactory.java
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/table/GravitinoSystemTableFactory.java
@@ -25,13 +25,14 @@ import io.trino.spi.connector.SchemaTableName;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import javax.annotation.Nullable;
 import org.apache.gravitino.trino.connector.catalog.CatalogConnectorManager;
 
 /** This class managed all the system tables */
 public class GravitinoSystemTableFactory {
 
   private final CatalogConnectorManager catalogConnectorManager;
-  private final String metalake;
+  @Nullable private final String metalake;
 
   // Per instance, not static: the tables are bound to one 
CatalogConnectorManager, and only the
   // manager on the coordinator runs the load loop that fills in the 
registration state. A shared
@@ -43,10 +44,11 @@ public class GravitinoSystemTableFactory {
    *
    * @param catalogConnectorManager the manager for catalog connectors
    * @param metalake the metalake this connector is configured with; the 
tables only report on it,
-   *     so that two entry catalogs pointed at different metalakes do not 
report each other's state
+   *     so that two entry catalogs pointed at different metalakes do not 
report each other's state.
+   *     Null when no metalake is configured, the tables then report on every 
metalake
    */
   public GravitinoSystemTableFactory(
-      CatalogConnectorManager catalogConnectorManager, String metalake) {
+      CatalogConnectorManager catalogConnectorManager, @Nullable String 
metalake) {
     this.catalogConnectorManager = catalogConnectorManager;
     this.metalake = metalake;
 
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/table/GravitinoSystemTableLoadStatus.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/table/GravitinoSystemTableLoadStatus.java
index df87741e35..34e11fb30b 100644
--- 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/table/GravitinoSystemTableLoadStatus.java
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/table/GravitinoSystemTableLoadStatus.java
@@ -33,6 +33,7 @@ import io.trino.spi.connector.SchemaTableName;
 import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
+import javax.annotation.Nullable;
 import org.apache.gravitino.trino.connector.catalog.CatalogConnectorManager;
 import 
org.apache.gravitino.trino.connector.catalog.CatalogConnectorManager.LoadOutcome;
 
@@ -64,16 +65,16 @@ public class GravitinoSystemTableLoadStatus extends 
GravitinoSystemTable {
               
ColumnMetadata.builder().setName("metalake_errors").setType(VARCHAR).build()));
 
   private final CatalogConnectorManager catalogConnectorManager;
-  private final String metalake;
+  @Nullable private final String metalake;
 
   /**
    * Constructs a new GravitinoSystemTableLoadStatus.
    *
    * @param catalogConnectorManager the manager for catalog connectors
-   * @param metalake the metalake to report errors for
+   * @param metalake the metalake to report errors for, or null for every 
metalake
    */
   public GravitinoSystemTableLoadStatus(
-      CatalogConnectorManager catalogConnectorManager, String metalake) {
+      CatalogConnectorManager catalogConnectorManager, @Nullable String 
metalake) {
     this.catalogConnectorManager = catalogConnectorManager;
     this.metalake = metalake;
   }
@@ -99,10 +100,16 @@ public class GravitinoSystemTableLoadStatus extends 
GravitinoSystemTable {
     writeNullableString(lastErrorColumnBuilder, loadOutcome.getLastError());
 
     // The load loop itself is shared by every entry catalog, so the columns 
above are global.
-    // Only the per metalake errors are narrowed to the metalake this 
connector reports on.
+    // Only the per metalake errors are narrowed to the metalake this 
connector reports on; without
+    // a configured metalake every metalake's error is shown.
     Map<String, String> allErrors = loadOutcome.getMetalakeErrors();
-    Map<String, String> metalakeErrors =
-        allErrors.containsKey(metalake) ? Map.of(metalake, 
allErrors.get(metalake)) : Map.of();
+    Map<String, String> metalakeErrors;
+    if (metalake == null) {
+      metalakeErrors = allErrors;
+    } else {
+      metalakeErrors =
+          allErrors.containsKey(metalake) ? Map.of(metalake, 
allErrors.get(metalake)) : Map.of();
+    }
     if (metalakeErrors.isEmpty()) {
       metalakeErrorsColumnBuilder.appendNull();
     } else {
diff --git 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConfig.java
 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConfig.java
index 0c55d5a26e..78a8431da6 100644
--- 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConfig.java
+++ 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConfig.java
@@ -18,7 +18,7 @@
  */
 package org.apache.gravitino.trino.connector;
 
-import static 
org.apache.gravitino.trino.connector.GravitinoErrorCode.GRAVITINO_MISSING_CONFIG;
+import static 
org.apache.gravitino.trino.connector.GravitinoErrorCode.GRAVITINO_ILLEGAL_ARGUMENT;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -57,17 +57,92 @@ public class TestGravitinoConfig {
   }
 
   @Test
-  public void testMissingConfig() {
+  public void testMetalakeIsOptional() {
     String gravitinoUrl = "http://127.0.0.1:8000";;
-    ImmutableMap<String, String> configMap = ImmutableMap.of("gravitino.uri", 
gravitinoUrl);
-    try {
-      GravitinoConfig config = new GravitinoConfig(configMap);
-      assertEquals(gravitinoUrl, config.getURI());
-    } catch (TrinoException e) {
-      if (!GRAVITINO_MISSING_CONFIG.toErrorCode().equals(e.getErrorCode())) {
-        throw e;
-      }
-    }
+    GravitinoConfig config = new 
GravitinoConfig(ImmutableMap.of("gravitino.uri", gravitinoUrl));
+
+    assertEquals(gravitinoUrl, config.getURI());
+    assertFalse(config.hasMetalake());
+    assertEquals("", config.getMetalake());
+    // A blank value is the same as an unset one.
+    assertFalse(new GravitinoConfig(ImmutableMap.of("gravitino.metalake", "  
")).hasMetalake());
+    assertTrue(new GravitinoConfig(ImmutableMap.of("gravitino.metalake", 
"test")).hasMetalake());
+  }
+
+  @Test
+  public void testLoadAllMetalakes() {
+    // Without a metalake every metalake is loaded, whatever the naming mode 
says.
+    assertTrue(new GravitinoConfig(ImmutableMap.of()).loadAllMetalakes());
+    assertTrue(
+        new 
GravitinoConfig(ImmutableMap.of("gravitino.catalog-name-with-metalake", "true"))
+            .loadAllMetalakes());
+    // With a metalake only the qualified naming mode loads every metalake.
+    assertFalse(
+        new GravitinoConfig(ImmutableMap.of("gravitino.metalake", 
"test")).loadAllMetalakes());
+    assertTrue(
+        new GravitinoConfig(
+                ImmutableMap.of(
+                    "gravitino.metalake", "test", 
"gravitino.catalog-name-with-metalake", "true"))
+            .loadAllMetalakes());
+  }
+
+  @Test
+  public void testCatalogNameWithMetalake() {
+    assertFalse(new 
GravitinoConfig(ImmutableMap.of()).catalogNameWithMetalake());
+    assertTrue(
+        new 
GravitinoConfig(ImmutableMap.of("gravitino.catalog-name-with-metalake", "true"))
+            .catalogNameWithMetalake());
+    // The deprecated key still works when the new one is unset ...
+    assertTrue(
+        new GravitinoConfig(ImmutableMap.of("gravitino.use-single-metalake", 
"false"))
+            .catalogNameWithMetalake());
+    assertFalse(
+        new GravitinoConfig(ImmutableMap.of("gravitino.use-single-metalake", 
"true"))
+            .catalogNameWithMetalake());
+    // ... and is ignored once the new one is set.
+    assertFalse(
+        new GravitinoConfig(
+                ImmutableMap.of(
+                    "gravitino.use-single-metalake", "false",
+                    "gravitino.catalog-name-with-metalake", "false"))
+            .catalogNameWithMetalake());
+
+    assertTrue(
+        new GravitinoConfig(
+                ImmutableMap.of(
+                    "gravitino.use-single-metalake", "true",
+                    "gravitino.catalog-name-with-metalake", "true"))
+            .catalogNameWithMetalake());
+
+    TrinoException error =
+        assertThrows(
+            TrinoException.class,
+            () ->
+                new 
GravitinoConfig(ImmutableMap.of("gravitino.catalog-name-with-metalake", "yes"))
+                    .catalogNameWithMetalake());
+    assertEquals(GRAVITINO_ILLEGAL_ARGUMENT.toErrorCode(), 
error.getErrorCode());
+    // The deprecated key is parsed just as strictly: a typo must not flip the 
naming mode.
+    error =
+        assertThrows(
+            TrinoException.class,
+            () ->
+                new 
GravitinoConfig(ImmutableMap.of("gravitino.use-single-metalake", "yes"))
+                    .catalogNameWithMetalake());
+    assertEquals(GRAVITINO_ILLEGAL_ARGUMENT.toErrorCode(), 
error.getErrorCode());
+  }
+
+  @Test
+  public void testUsesDeprecatedSingleMetalakeKey() {
+    assertFalse(new 
GravitinoConfig(ImmutableMap.of()).usesDeprecatedSingleMetalakeKey());
+    assertTrue(
+        new GravitinoConfig(ImmutableMap.of("gravitino.use-single-metalake", 
"true"))
+            .usesDeprecatedSingleMetalakeKey());
+    assertFalse(
+        new GravitinoConfig(
+                ImmutableMap.of(
+                    "gravitino.use-single-metalake", "false",
+                    "gravitino.catalog-name-with-metalake", "true"))
+            .usesDeprecatedSingleMetalakeKey());
   }
 
   @Test
@@ -179,6 +254,28 @@ public class TestGravitinoConfig {
     assertTrue(catalogConfig.contains("\"gravitino.user\"='admin'"));
   }
 
+  @Test
+  public void testToCatalogConfigIncludesScopedIcebergRestUris() {
+    GravitinoConfig config =
+        new GravitinoConfig(
+            ImmutableMap.of(
+                "gravitino.iceberg.rest-uri",
+                "http://default-irc:9001/iceberg";,
+                "gravitino.iceberg.rest-uri.prod",
+                "http://prod-irc:9001/iceberg";,
+                "gravitino.iceberg.rest-uri.dev",
+                "http://dev-irc:9001/iceberg";));
+
+    String catalogConfig = config.toCatalogConfig();
+    assertTrue(
+        
catalogConfig.contains("\"gravitino.iceberg.rest-uri\"='http://default-irc:9001/iceberg'"));
+    assertTrue(
+        catalogConfig.contains(
+            
"\"gravitino.iceberg.rest-uri.prod\"='http://prod-irc:9001/iceberg'"));
+    assertTrue(
+        
catalogConfig.contains("\"gravitino.iceberg.rest-uri.dev\"='http://dev-irc:9001/iceberg'"));
+  }
+
   @Test
   public void testTrinoJdbcConfigDefaults() {
     GravitinoConfig config =
@@ -355,8 +452,9 @@ public class TestGravitinoConfig {
                 "gravitino.iceberg.rest-catalog.security", "OAUTH2",
                 "gravitino.iceberg.rest-catalog.oauth2.credential", 
"client_id:client_secret"));
 
-    // The unscoped URI is honored as-is in single-metalake mode.
+    // The unscoped URI is the default for every metalake.
     assertEquals("http://127.0.0.1:9001/iceberg";, 
config.getManualIcebergRestUri("user_001"));
+    assertEquals("http://127.0.0.1:9001/iceberg";, 
config.getManualIcebergRestUri("user_002"));
 
     Map<String, String> restCatalogConfig = 
config.getIcebergRestCatalogConfig();
     assertEquals(2, restCatalogConfig.size());
@@ -439,24 +537,25 @@ public class TestGravitinoConfig {
   }
 
   @Test
-  public void testIcebergRestConfigScopedToMetalakeInMultiMetalakeMode() {
-    ImmutableMap<String, String> configMap =
-        ImmutableMap.of(
-            "gravitino.metalake",
-            "metalake_a",
-            "gravitino.use-single-metalake",
-            "false",
-            "gravitino.iceberg.rest-uri",
-            "http://unscoped:9001/iceberg";,
-            "gravitino.iceberg.rest-uri.metalake_a",
-            "http://metalake-a:9001/iceberg";);
-    GravitinoConfig config = new GravitinoConfig(configMap);
-
-    // The scoped key wins for the metalake it names.
+  public void testScopedIcebergRestUriOverridesUnscopedPerMetalake() {
+    GravitinoConfig config =
+        new GravitinoConfig(
+            ImmutableMap.of(
+                "gravitino.catalog-name-with-metalake",
+                "true",
+                "gravitino.iceberg.rest-uri",
+                "http://unscoped:9001/iceberg";,
+                "gravitino.iceberg.rest-uri.metalake_a",
+                "http://metalake-a:9001/iceberg";));
+
+    // The scoped key wins for the metalake it names, the others fall back to 
the unscoped one,
+    // whether or not gravitino.metalake is configured.
     assertEquals("http://metalake-a:9001/iceberg";, 
config.getManualIcebergRestUri("metalake_a"));
-    // The unscoped key is ignored in multi-metalake mode, since it would 
otherwise misroute every
-    // metalake other than the one the Iceberg REST server actually serves.
-    assertEquals("", config.getManualIcebergRestUri("metalake_b"));
+    assertEquals("http://unscoped:9001/iceberg";, 
config.getManualIcebergRestUri("metalake_b"));
+    assertEquals(
+        "",
+        new GravitinoConfig(ImmutableMap.of("gravitino.metalake", 
"metalake_a"))
+            .getManualIcebergRestUri("metalake_a"));
   }
 
   @Test
diff --git 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConnectorFactoryStart.java
 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConnectorFactoryStart.java
index 7123554efe..6500bc8200 100644
--- 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConnectorFactoryStart.java
+++ 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConnectorFactoryStart.java
@@ -108,6 +108,17 @@ public class TestGravitinoConnectorFactoryStart {
     assertTrue(factory.isCatalogConnectorManagerStartTriggered());
   }
 
+  @Test
+  public void testStaticConnectorStartsWithoutMetalake() {
+    CoordinatorFactory factory = newFactory();
+    Map<String, String> config = staticConfig();
+    config.remove("gravitino.metalake");
+
+    // Without a metalake the entry catalog serves every metalake instead of 
refusing to start.
+    assertNotNull(factory.create("gravitino", config, mockContext()));
+    assertTrue(factory.isCatalogConnectorManagerStartTriggered());
+  }
+
   @Test
   public void testDynamicConnectorDoesNotStartTheManager() {
     CoordinatorFactory factory = newFactory();
diff --git 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/catalog/TestCatalogConnectorManager.java
 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/catalog/TestCatalogConnectorManager.java
index acf389e7c0..8bea849ee3 100644
--- 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/catalog/TestCatalogConnectorManager.java
+++ 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/catalog/TestCatalogConnectorManager.java
@@ -26,6 +26,7 @@ import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -113,7 +114,7 @@ public class TestCatalogConnectorManager {
                     "test1", otherConnectorConfig, mockContext()));
     assertEquals(
         GravitinoErrorCode.GRAVITINO_UNSUPPORTED_OPERATION.toErrorCode(), 
error.getErrorCode());
-    assertTrue(error.getMessage().contains("Multiple metalakes are not 
supported"));
+    assertTrue(error.getMessage().contains("belongs to metalake test2"), 
error.getMessage());
   }
 
   @Test
@@ -683,6 +684,185 @@ public class TestCatalogConnectorManager {
     assertTrue(manager.getUsedMetalakes().isEmpty());
   }
 
+  @Test
+  public void testUnsetMetalakeLoadsEveryMetalake() throws Exception {
+    LoadFixture fixture = new LoadFixture().withoutMetalake();
+    fixture.withCatalogs(mockCatalog("memory", "memory", 
Catalog.Type.RELATIONAL));
+    fixture.withSecondMetalake("dev", mockCatalog("sandbox", "memory", 
Catalog.Type.RELATIONAL));
+
+    // The default naming mode, which loads only the configured metalake when 
there is one.
+    CatalogConnectorManager manager = fixture.createManager(ImmutableMap.of());
+    manager.loadMetalakeSync();
+
+    verify(fixture.client, times(1)).listMetalakes();
+    verify(fixture.client, never()).loadMetalake(any());
+    // Unqualified names: the naming mode is independent from which metalakes 
are loaded.
+    verify(fixture.catalogRegister, times(1)).registerCatalog(eq("memory"), 
any());
+    verify(fixture.catalogRegister, times(1)).registerCatalog(eq("sandbox"), 
any());
+    assertEquals(ImmutableSet.of("test", "dev"), 
ImmutableSet.copyOf(manager.getUsedMetalakes()));
+    assertEquals(2, manager.getCatalogRegistrationStates().size());
+    assertEquals(2, manager.getCatalogRegistrationStates(null).size());
+    assertEquals(1, manager.getCatalogRegistrationStates("dev").size());
+    assertNull(manager.getLoadOutcome().getLastError());
+  }
+
+  @Test
+  public void testConfiguredMetalakeStillLoadsOnlyItself() throws Exception {
+    LoadFixture fixture = new LoadFixture();
+    fixture.withCatalogs(mockCatalog("memory", "memory", 
Catalog.Type.RELATIONAL));
+    fixture.withSecondMetalake("dev", mockCatalog("sandbox", "memory", 
Catalog.Type.RELATIONAL));
+
+    CatalogConnectorManager manager = fixture.createManager(ImmutableMap.of());
+    manager.loadMetalakeSync();
+
+    verify(fixture.client, never()).listMetalakes();
+    verify(fixture.catalogRegister, never()).registerCatalog(eq("sandbox"), 
any());
+    assertEquals(ImmutableSet.of("test"), 
ImmutableSet.copyOf(manager.getUsedMetalakes()));
+  }
+
+  @Test
+  public void testSameCatalogNameInAnotherMetalakeIsReportedNotRegistered() 
throws Exception {
+    LoadFixture fixture = new LoadFixture().withoutMetalake();
+    Catalog catalog = mockCatalog("memory", "memory", Catalog.Type.RELATIONAL);
+    fixture.withCatalogs(catalog);
+    fixture.withSecondMetalake("dev", mockCatalog("memory", "memory", 
Catalog.Type.RELATIONAL));
+
+    CatalogConnectorManager manager = fixture.createManager(ImmutableMap.of());
+    // Built from the same definition the server reports, so that the next 
cycle sees it as up to
+    // date. Built outside the answer below: touching the catalog mock while a 
stubbing is in
+    // progress would break that stubbing.
+    GravitinoCatalog registered = new GravitinoCatalog("test", catalog);
+    // What Trino does while CREATE CATALOG runs: the first registration 
creates the connector, so
+    // the manager knows which metalake owns the Trino catalog name from then 
on.
+    Mockito.doAnswer(
+            invocation -> {
+              CatalogConnectorContext context =
+                  manager.createCatalogConnectorContext(
+                      "memory",
+                      createConnectorConfig(catalogConfigJson("test", 
"memory")),
+                      mockContext());
+              when(context.getCatalog()).thenReturn(registered);
+              return null;
+            })
+        .when(fixture.catalogRegister)
+        .registerCatalog(eq("memory"), any());
+    manager.loadMetalakeSync();
+
+    // Registered once, by the metalake listed first.
+    verify(fixture.catalogRegister, times(1)).registerCatalog(eq("memory"), 
any());
+    verify(fixture.catalogRegister, never()).unregisterCatalog(any());
+    assertEquals(2, manager.getCatalogRegistrationStates().size());
+    CatalogRegistrationState winner = 
manager.getCatalogRegistrationStates("test").get(0);
+    assertEquals(CatalogRegistrationState.Status.REGISTERED, 
winner.getStatus());
+    CatalogRegistrationState loser = 
manager.getCatalogRegistrationStates("dev").get(0);
+    assertEquals(CatalogRegistrationState.Status.FAILED, loser.getStatus());
+    assertEquals("memory", loser.getTrinoCatalogName());
+    assertTrue(
+        loser.getLastError().contains("already registered by metalake test"), 
loser.getLastError());
+    assertTrue(manager.describeRegistrationFailure("dev", 
"memory").contains("already registered"));
+    // The other metalake's failure does not fail the run as a whole.
+    assertTrue(manager.getMetalakeErrors().isEmpty());
+
+    // The next cycle neither refreshes the first over the second nor 
unregisters it.
+    manager.loadMetalakeSync();
+    verify(fixture.catalogRegister, times(1)).registerCatalog(eq("memory"), 
any());
+    verify(fixture.catalogRegister, never()).unregisterCatalog(any());
+    assertEquals(
+        CatalogRegistrationState.Status.REGISTERED,
+        manager.getCatalogRegistrationStates("test").get(0).getStatus());
+    assertEquals(
+        CatalogRegistrationState.Status.FAILED,
+        manager.getCatalogRegistrationStates("dev").get(0).getStatus());
+  }
+
+  @Test
+  public void testLoserStateIsPrunedWhenItsCatalogIsDeleted() throws Exception 
{
+    LoadFixture fixture = new LoadFixture().withoutMetalake();
+    Catalog catalog = mockCatalog("memory", "memory", Catalog.Type.RELATIONAL);
+    fixture.withCatalogs(catalog);
+    GravitinoMetalake dev =
+        fixture.withSecondMetalake("dev", mockCatalog("memory", "memory", 
Catalog.Type.RELATIONAL));
+    CatalogConnectorManager manager = fixture.createManager(ImmutableMap.of());
+    String[] owner = {"test"};
+    registerOwnerOnCreate(fixture, manager, owner, catalog);
+    manager.loadMetalakeSync();
+    assertEquals(
+        CatalogRegistrationState.Status.FAILED,
+        manager.getCatalogRegistrationStates("dev").get(0).getStatus());
+
+    // The losing catalog is deleted in Gravitino. The winner still holds the 
Trino name, which
+    // must not keep the loser's row alive.
+    Mockito.doReturn(new String[0]).when(dev).listCatalogs();
+    manager.loadMetalakeSync();
+
+    assertTrue(manager.getCatalogRegistrationStates("dev").isEmpty());
+    assertEquals(
+        CatalogRegistrationState.Status.REGISTERED,
+        manager.getCatalogRegistrationStates("test").get(0).getStatus());
+    verify(fixture.catalogRegister, never()).unregisterCatalog(any());
+
+    // The same holds when the losing metalake disappears altogether.
+    Mockito.doReturn(new GravitinoMetalake[] {fixture.metalake})
+        .when(fixture.client)
+        .listMetalakes();
+    manager.loadMetalakeSync();
+    assertTrue(manager.getCatalogRegistrationStates("dev").isEmpty());
+    assertEquals(1, manager.getCatalogRegistrationStates().size());
+    verify(fixture.catalogRegister, never()).unregisterCatalog(any());
+  }
+
+  @Test
+  public void testLoserTakesOverWhenWinnerIsDropped() throws Exception {
+    LoadFixture fixture = new LoadFixture().withoutMetalake();
+    Catalog catalog = mockCatalog("memory", "memory", Catalog.Type.RELATIONAL);
+    fixture.withCatalogs(catalog);
+    fixture.withSecondMetalake("dev", catalog);
+    CatalogConnectorManager manager = fixture.createManager(ImmutableMap.of());
+    String[] owner = {"test"};
+    registerOwnerOnCreate(fixture, manager, owner, catalog);
+    manager.loadMetalakeSync();
+    assertNotNull(manager.getCatalogConnector("test", "memory"));
+    assertNull(manager.getCatalogConnector("dev", "memory"));
+
+    // The winning catalog is deleted in Gravitino, which frees the Trino name 
for the loser.
+    Mockito.doReturn(new String[0]).when(fixture.metalake).listCatalogs();
+    owner[0] = "dev";
+    manager.loadMetalakeSync();
+
+    verify(fixture.catalogRegister, times(1)).unregisterCatalog("memory");
+    verify(fixture.catalogRegister, times(2)).registerCatalog(eq("memory"), 
any());
+    assertTrue(manager.getCatalogRegistrationStates("test").isEmpty());
+    assertEquals(
+        CatalogRegistrationState.Status.REGISTERED,
+        manager.getCatalogRegistrationStates("dev").get(0).getStatus());
+    assertNull(manager.getCatalogConnector("test", "memory"));
+    assertNotNull(manager.getCatalogConnector("dev", "memory"));
+  }
+
+  // What Trino does while CREATE CATALOG runs: registering creates the 
connector, whose owning
+  // metalake is whatever owner[0] names at that time.
+  private static void registerOwnerOnCreate(
+      LoadFixture fixture, CatalogConnectorManager manager, String[] owner, 
Catalog catalog) {
+    Mockito.doAnswer(
+            invocation -> {
+              String metalake = owner[0];
+              // Built before the stubbing: touching the catalog mock while a 
stubbing is in
+              // progress would break that stubbing.
+              GravitinoCatalog registered = new GravitinoCatalog(metalake, 
catalog);
+              CatalogConnectorContext context =
+                  manager.createCatalogConnectorContext(
+                      "memory",
+                      createConnectorConfig(catalogConfigJson(metalake, 
"memory")),
+                      mockContext());
+              GravitinoMetalake handle = manager.getMetalake(metalake);
+              when(context.getCatalog()).thenReturn(registered);
+              when(context.getMetalake()).thenReturn(handle);
+              return null;
+            })
+        .when(fixture.catalogRegister)
+        .registerCatalog(eq("memory"), any());
+  }
+
   @Test
   public void testCatalogStatesAreFilteredPerMetalake() throws Exception {
     LoadFixture fixture = new LoadFixture();
@@ -1276,8 +1456,7 @@ public class TestCatalogConnectorManager {
       CatalogConnectorFactory catalogFactory, ImmutableMap<String, String> 
configMap) {
     CatalogRegister catalogRegister = mock(CatalogRegister.class);
 
-    boolean singleMetalakeMode =
-        configMap.getOrDefault("gravitino.use-single-metalake", 
"true").equals("true");
+    boolean singleMetalakeMode = !new 
GravitinoConfig(configMap).catalogNameWithMetalake();
     CatalogConnectorManager manager =
         new CatalogConnectorManager(
             catalogRegister,
@@ -1355,6 +1534,7 @@ public class TestCatalogConnectorManager {
     private final GravitinoAdminClient client = 
mock(GravitinoAdminClient.class);
     private final GravitinoMetalake metalake = mock(GravitinoMetalake.class);
     private final CatalogConnectorFactory catalogFactory = 
mock(CatalogConnectorFactory.class);
+    private boolean metalakeConfigured = true;
 
     LoadFixture() throws Exception {
       when(catalogRegister.isTrinoReachable()).thenReturn(true);
@@ -1369,6 +1549,11 @@ public class TestCatalogConnectorManager {
       when(builder.build()).thenReturn(mock(CatalogConnectorContext.class));
     }
 
+    LoadFixture withoutMetalake() {
+      metalakeConfigured = false;
+      return this;
+    }
+
     void withCatalogs(Catalog... catalogs) {
       String[] names = new String[catalogs.length];
       for (int i = 0; i < catalogs.length; i++) {
@@ -1395,10 +1580,27 @@ public class TestCatalogConnectorManager {
     GravitinoConfig config(Map<String, String> extraConfig) {
       Map<String, String> defaults = new HashMap<>();
       defaults.put("gravitino.uri", "http://127.0.0.1:8090";);
-      defaults.put("gravitino.metalake", "test");
+      if (metalakeConfigured) {
+        defaults.put("gravitino.metalake", "test");
+      }
       defaults.put("gravitino.use-single-metalake", "true");
       defaults.putAll(extraConfig);
       return new GravitinoConfig(ImmutableMap.copyOf(defaults));
     }
+
+    /** A second metalake the mock client lists after the fixture's own. */
+    GravitinoMetalake withSecondMetalake(String name, Catalog... catalogs) {
+      GravitinoMetalake second = mock(GravitinoMetalake.class);
+      when(second.name()).thenReturn(name);
+      String[] names = new String[catalogs.length];
+      for (int i = 0; i < catalogs.length; i++) {
+        names[i] = catalogs[i].name();
+        Mockito.doReturn(catalogs[i]).when(second).loadCatalog(names[i]);
+      }
+      Mockito.doReturn(names).when(second).listCatalogs();
+      Mockito.doReturn(second).when(client).loadMetalake(name);
+      Mockito.doReturn(new GravitinoMetalake[] {metalake, 
second}).when(client).listMetalakes();
+      return second;
+    }
   }
 }
diff --git 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/system/storedprocedure/TestAlterCatalogStoredProcedure.java
 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/system/storedprocedure/TestAlterCatalogStoredProcedure.java
new file mode 100644
index 0000000000..5ec6fe7133
--- /dev/null
+++ 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/system/storedprocedure/TestAlterCatalogStoredProcedure.java
@@ -0,0 +1,108 @@
+/*
+ * 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.gravitino.trino.connector.system.storedprocedure;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableMap;
+import io.trino.spi.TrinoException;
+import java.util.List;
+import org.apache.gravitino.client.GravitinoMetalake;
+import org.apache.gravitino.trino.connector.GravitinoErrorCode;
+import org.apache.gravitino.trino.connector.catalog.CatalogConnectorContext;
+import org.apache.gravitino.trino.connector.catalog.CatalogConnectorManager;
+import org.apache.gravitino.trino.connector.metadata.GravitinoCatalog;
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for {@link AlterCatalogStoredProcedure} around the METALAKE 
argument. */
+public class TestAlterCatalogStoredProcedure {
+
+  private static final String METALAKE = "test_metalake";
+  private static final String CATALOG = "test_catalog";
+
+  @Test
+  public void testMetalakeArgumentOverridesConfiguredMetalake() {
+    CatalogConnectorManager manager = mock(CatalogConnectorManager.class);
+    CatalogConnectorContext context = mock(CatalogConnectorContext.class);
+    GravitinoMetalake other = mock(GravitinoMetalake.class);
+    when(manager.getTrinoCatalogName(anyString(), 
eq(CATALOG))).thenReturn(CATALOG);
+    when(manager.getCatalogConnector("other", CATALOG)).thenReturn(context);
+    when(context.getMetalake()).thenReturn(other);
+    when(context.getCatalog())
+        .thenReturn(new GravitinoCatalog("other", "memory", CATALOG, 
ImmutableMap.of(), 1L))
+        .thenReturn(
+            new GravitinoCatalog("other", "memory", CATALOG, 
ImmutableMap.of("k", "v"), 2L));
+
+    AlterCatalogStoredProcedure procedure = new 
AlterCatalogStoredProcedure(manager, METALAKE);
+
+    assertDoesNotThrow(
+        () -> procedure.alterCatalog(CATALOG, ImmutableMap.of("k", "v"), 
List.of(), "other"));
+
+    verify(other, times(1)).alterCatalog(eq(CATALOG), any());
+    verify(manager, never()).getCatalogConnector(METALAKE, CATALOG);
+  }
+
+  @Test
+  public void testMetalakeArgumentRequiredWithoutConfiguredMetalake() {
+    CatalogConnectorManager manager = mock(CatalogConnectorManager.class);
+    AlterCatalogStoredProcedure procedure = new 
AlterCatalogStoredProcedure(manager, null);
+
+    TrinoException error =
+        assertThrows(
+            TrinoException.class,
+            () -> procedure.alterCatalog(CATALOG, ImmutableMap.of("k", "v"), 
List.of(), null));
+    assertEquals(GravitinoErrorCode.GRAVITINO_MISSING_CONFIG.toErrorCode(), 
error.getErrorCode());
+    verify(manager, never()).getCatalogConnector(anyString(), anyString());
+  }
+
+  @Test
+  public void testTrinoNameHeldByAnotherMetalakeIsNotAltered() {
+    // With unqualified names another metalake's catalog may hold the Trino 
name. It is not the
+    // catalog asked for, so nothing is altered and the caller learns why it 
is not registered.
+    CatalogConnectorManager manager = mock(CatalogConnectorManager.class);
+    CatalogConnectorContext otherContext = mock(CatalogConnectorContext.class);
+    GravitinoMetalake otherMetalake = mock(GravitinoMetalake.class);
+    when(manager.getTrinoCatalogName(anyString(), 
eq(CATALOG))).thenReturn(CATALOG);
+    when(manager.getCatalogConnector(CATALOG)).thenReturn(otherContext);
+    when(manager.getCatalogConnector(METALAKE, CATALOG)).thenReturn(null);
+    when(otherContext.getMetalake()).thenReturn(otherMetalake);
+    when(manager.describeRegistrationFailure(METALAKE, CATALOG))
+        .thenReturn("FAILED: already registered by metalake other");
+
+    AlterCatalogStoredProcedure procedure = new 
AlterCatalogStoredProcedure(manager, METALAKE);
+
+    TrinoException error =
+        assertThrows(
+            TrinoException.class,
+            () -> procedure.alterCatalog(CATALOG, ImmutableMap.of("k", "v"), 
List.of(), null));
+    assertTrue(error.getMessage().contains("already registered"), 
error.getMessage());
+    verify(otherMetalake, never()).alterCatalog(anyString(), any());
+  }
+}
diff --git 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/system/storedprocedure/TestCreateCatalogStoredProcedure.java
 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/system/storedprocedure/TestCreateCatalogStoredProcedure.java
new file mode 100644
index 0000000000..9a451f386a
--- /dev/null
+++ 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/system/storedprocedure/TestCreateCatalogStoredProcedure.java
@@ -0,0 +1,108 @@
+/*
+ * 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.gravitino.trino.connector.system.storedprocedure;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableMap;
+import io.trino.spi.TrinoException;
+import java.util.Map;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.client.GravitinoMetalake;
+import org.apache.gravitino.trino.connector.GravitinoErrorCode;
+import org.apache.gravitino.trino.connector.catalog.CatalogConnectorContext;
+import org.apache.gravitino.trino.connector.catalog.CatalogConnectorManager;
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for {@link CreateCatalogStoredProcedure} around the METALAKE 
argument. */
+public class TestCreateCatalogStoredProcedure {
+
+  private static final String METALAKE = "test_metalake";
+  private static final String CATALOG = "test_catalog";
+  private static final Map<String, String> PROPERTIES = ImmutableMap.of("k", 
"v");
+
+  @Test
+  public void testMetalakeArgumentOverridesConfiguredMetalake() {
+    CatalogConnectorManager manager = mock(CatalogConnectorManager.class);
+    GravitinoMetalake other = mock(GravitinoMetalake.class);
+    when(manager.getTrinoCatalogName(anyString(), 
eq(CATALOG))).thenReturn(CATALOG);
+    when(manager.getMetalake("other")).thenReturn(other);
+    // Registered by the load loop once created.
+    when(manager.getCatalogConnector("other", CATALOG))
+        .thenReturn(null)
+        .thenReturn(mock(CatalogConnectorContext.class));
+
+    CreateCatalogStoredProcedure procedure = new 
CreateCatalogStoredProcedure(manager, METALAKE);
+
+    assertDoesNotThrow(
+        () -> procedure.createCatalog(CATALOG, "memory", PROPERTIES, false, 
"other"));
+
+    verify(other, times(1))
+        .createCatalog(CATALOG, Catalog.Type.RELATIONAL, "memory", "Trino 
created", PROPERTIES);
+    verify(manager, never()).getMetalake(METALAKE);
+  }
+
+  @Test
+  public void testMetalakeArgumentRequiredWithoutConfiguredMetalake() {
+    CatalogConnectorManager manager = mock(CatalogConnectorManager.class);
+    CreateCatalogStoredProcedure procedure = new 
CreateCatalogStoredProcedure(manager, null);
+
+    TrinoException error =
+        assertThrows(
+            TrinoException.class,
+            () -> procedure.createCatalog(CATALOG, "memory", PROPERTIES, 
false, null));
+    assertEquals(GravitinoErrorCode.GRAVITINO_MISSING_CONFIG.toErrorCode(), 
error.getErrorCode());
+    verify(manager, never()).getMetalake(anyString());
+  }
+
+  @Test
+  public void testTrinoNameHeldByAnotherMetalakeIsNotAnExistingCatalog() {
+    // With unqualified names another metalake's catalog may hold the Trino 
name; that is not the
+    // catalog being created, so it is created on the server rather than 
reported as existing.
+    CatalogConnectorManager manager = mock(CatalogConnectorManager.class);
+    GravitinoMetalake requested = mock(GravitinoMetalake.class);
+    when(manager.getTrinoCatalogName(anyString(), 
eq(CATALOG))).thenReturn(CATALOG);
+    
when(manager.getCatalogConnector(CATALOG)).thenReturn(mock(CatalogConnectorContext.class));
+    when(manager.getCatalogConnector(METALAKE, CATALOG)).thenReturn(null);
+    when(manager.getMetalake(METALAKE)).thenReturn(requested);
+    when(manager.describeRegistrationFailure(METALAKE, CATALOG))
+        .thenReturn("FAILED: already registered by metalake other");
+
+    CreateCatalogStoredProcedure procedure = new 
CreateCatalogStoredProcedure(manager, METALAKE);
+
+    // Created on the server, but the name stays with the other metalake so 
registration fails.
+    TrinoException error =
+        assertThrows(
+            TrinoException.class,
+            () -> procedure.createCatalog(CATALOG, "memory", PROPERTIES, 
false, null));
+    verify(requested, times(1)).createCatalog(eq(CATALOG), any(), 
eq("memory"), any(), any());
+    assertTrue(error.getMessage().contains("already registered"), 
error.getMessage());
+  }
+}
diff --git 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/system/storedprocedure/TestDropCatalogStoredProcedure.java
 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/system/storedprocedure/TestDropCatalogStoredProcedure.java
index c4b770c16e..cffd2d5a9f 100644
--- 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/system/storedprocedure/TestDropCatalogStoredProcedure.java
+++ 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/system/storedprocedure/TestDropCatalogStoredProcedure.java
@@ -22,7 +22,9 @@ import static 
org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
@@ -32,6 +34,7 @@ import static org.mockito.Mockito.when;
 import io.trino.spi.TrinoException;
 import org.apache.gravitino.client.GravitinoMetalake;
 import org.apache.gravitino.trino.connector.GravitinoErrorCode;
+import org.apache.gravitino.trino.connector.catalog.CatalogConnectorContext;
 import org.apache.gravitino.trino.connector.catalog.CatalogConnectorManager;
 import org.junit.jupiter.api.Test;
 
@@ -45,6 +48,61 @@ public class TestDropCatalogStoredProcedure {
   private static final String CATALOG = "test_catalog";
   private static final String TRINO_CATALOG = "test_metalake.test_catalog";
 
+  @Test
+  public void testMetalakeArgumentOverridesConfiguredMetalake() {
+    CatalogConnectorManager manager = mock(CatalogConnectorManager.class);
+    GravitinoMetalake other = mock(GravitinoMetalake.class);
+    when(manager.getTrinoCatalogName("other", CATALOG)).thenReturn("other." + 
CATALOG);
+    when(manager.getCatalogConnector("other." + CATALOG)).thenReturn(null);
+    when(manager.getMetalake("other")).thenReturn(other);
+    when(other.dropCatalog(CATALOG, true)).thenReturn(true);
+
+    DropCatalogStoredProcedure procedure = new 
DropCatalogStoredProcedure(manager, METALAKE);
+
+    assertDoesNotThrow(() -> procedure.dropCatalog(CATALOG, false, "other"));
+
+    verify(other, times(1)).dropCatalog(CATALOG, true);
+    verify(manager, never()).getMetalake(METALAKE);
+  }
+
+  @Test
+  public void testTrinoNameHeldByAnotherMetalakeIsNotDropped() {
+    // With unqualified names another metalake's catalog may hold the Trino 
name. The lookup is by
+    // metalake, so the procedure falls back to the server for the metalake it 
was asked for and
+    // the other metalake is never touched.
+    CatalogConnectorManager manager = mock(CatalogConnectorManager.class);
+    CatalogConnectorContext otherContext = mock(CatalogConnectorContext.class);
+    GravitinoMetalake otherMetalake = mock(GravitinoMetalake.class);
+    GravitinoMetalake requested = mock(GravitinoMetalake.class);
+    when(manager.getTrinoCatalogName(anyString(), 
eq(CATALOG))).thenReturn(CATALOG);
+    when(manager.getCatalogConnector(CATALOG)).thenReturn(otherContext);
+    when(manager.getCatalogConnector("other", 
CATALOG)).thenReturn(otherContext);
+    when(otherContext.getMetalake()).thenReturn(otherMetalake);
+    when(manager.getMetalake(METALAKE)).thenReturn(requested);
+    when(requested.dropCatalog(CATALOG, true)).thenReturn(true);
+
+    DropCatalogStoredProcedure procedure = new 
DropCatalogStoredProcedure(manager, METALAKE);
+
+    assertDoesNotThrow(() -> procedure.dropCatalog(CATALOG, false, null));
+
+    verify(requested, times(1)).dropCatalog(CATALOG, true);
+    verify(otherMetalake, never()).dropCatalog(anyString(), anyBoolean());
+  }
+
+  @Test
+  public void testMetalakeArgumentRequiredWithoutConfiguredMetalake() {
+    CatalogConnectorManager manager = mock(CatalogConnectorManager.class);
+    DropCatalogStoredProcedure procedure = new 
DropCatalogStoredProcedure(manager, null);
+
+    TrinoException error =
+        assertThrows(TrinoException.class, () -> 
procedure.dropCatalog(CATALOG, false, null));
+    assertEquals(GravitinoErrorCode.GRAVITINO_MISSING_CONFIG.toErrorCode(), 
error.getErrorCode());
+    assertTrue(error.getMessage().contains("METALAKE"), error.getMessage());
+    // Blank counts as not passed.
+    assertThrows(TrinoException.class, () -> procedure.dropCatalog(CATALOG, 
false, " "));
+    verify(manager, never()).getMetalake(anyString());
+  }
+
   @Test
   public void 
testDropCatalogFallsBackToServerWhenLocalCacheMissesAndServerHasIt() {
     // The local connector cache does not have the catalog, but the Gravitino 
server does.
@@ -59,7 +117,7 @@ public class TestDropCatalogStoredProcedure {
 
     DropCatalogStoredProcedure procedure = new 
DropCatalogStoredProcedure(manager, METALAKE);
 
-    assertDoesNotThrow(() -> procedure.dropCatalog(CATALOG, false));
+    assertDoesNotThrow(() -> procedure.dropCatalog(CATALOG, false, null));
 
     verify(metalake, times(1)).dropCatalog(CATALOG, true);
   }
@@ -83,7 +141,7 @@ public class TestDropCatalogStoredProcedure {
     // The contract we verify is: the user-visible message still tells them 
the catalog does not
     // exist, and the root cause carries the GRAVITINO_CATALOG_NOT_EXISTS 
error code.
     TrinoException error =
-        assertThrows(TrinoException.class, () -> 
procedure.dropCatalog(CATALOG, false));
+        assertThrows(TrinoException.class, () -> 
procedure.dropCatalog(CATALOG, false, null));
     assertTrue(
         error.getMessage().contains("not exists"),
         () -> "Expected message to contain 'not exists' but was: " + 
error.getMessage());
@@ -108,7 +166,7 @@ public class TestDropCatalogStoredProcedure {
 
     DropCatalogStoredProcedure procedure = new 
DropCatalogStoredProcedure(manager, METALAKE);
 
-    assertDoesNotThrow(() -> procedure.dropCatalog(CATALOG, true));
+    assertDoesNotThrow(() -> procedure.dropCatalog(CATALOG, true, null));
 
     // We DID consult the server, but no exception was thrown.
     verify(metalake, times(1)).dropCatalog(CATALOG, true);
diff --git 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/system/storedprocedure/TestGravitinoStoredProcedureFactory.java
 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/system/storedprocedure/TestGravitinoStoredProcedureFactory.java
new file mode 100644
index 0000000000..f1e35d57a7
--- /dev/null
+++ 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/system/storedprocedure/TestGravitinoStoredProcedureFactory.java
@@ -0,0 +1,55 @@
+/*
+ * 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.gravitino.trino.connector.system.storedprocedure;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.Mockito.mock;
+
+import io.trino.spi.procedure.Procedure;
+import java.util.List;
+import java.util.Set;
+import org.apache.gravitino.trino.connector.catalog.CatalogConnectorManager;
+import org.junit.jupiter.api.Test;
+
+public class TestGravitinoStoredProcedureFactory {
+
+  @Test
+  public void testProceduresDeclareOptionalMetalakeArgument() {
+    // Trino validates a procedure's arguments against its method handle when 
the catalog loads,
+    // so a mismatch surfaces as a failure to load the entry catalog rather 
than in a call.
+    for (String metalake : new String[] {"test", null}) {
+      GravitinoStoredProcedureFactory factory =
+          new 
GravitinoStoredProcedureFactory(mock(CatalogConnectorManager.class), metalake);
+      Set<Procedure> procedures = factory.getStoredProcedures();
+
+      assertEquals(
+          Set.of("create_catalog", "drop_catalog", "alter_catalog"),
+          
Set.of(procedures.stream().map(Procedure::getName).toArray(String[]::new)));
+      for (Procedure procedure : procedures) {
+        List<Procedure.Argument> arguments = procedure.getArguments();
+        Procedure.Argument last = arguments.get(arguments.size() - 1);
+        assertEquals("METALAKE", last.getName(), procedure.getName());
+        assertFalse(last.isRequired(), procedure.getName());
+        assertNull(last.getDefaultValue(), procedure.getName());
+      }
+    }
+  }
+}
diff --git 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/system/table/TestGravitinoSystemStatusTables.java
 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/system/table/TestGravitinoSystemStatusTables.java
index c8984861e6..0fea4b1992 100644
--- 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/system/table/TestGravitinoSystemStatusTables.java
+++ 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/system/table/TestGravitinoSystemStatusTables.java
@@ -85,6 +85,71 @@ public class TestGravitinoSystemStatusTables {
     assertEquals("memory", varchar(page, 0));
   }
 
+  @Test
+  public void testCatalogTableReportsEveryMetalakeWithoutConfiguredMetalake() {
+    CatalogConnectorManager manager = mock(CatalogConnectorManager.class);
+    when(manager.getUsedMetalakes()).thenReturn(Set.of("prod", "dev"));
+    for (String metalakeName : List.of("prod", "dev")) {
+      GravitinoMetalake metalake = mock(GravitinoMetalake.class);
+      Catalog catalog = mock(Catalog.class);
+      when(catalog.name()).thenReturn(metalakeName + "_memory");
+      when(catalog.provider()).thenReturn("memory");
+      when(catalog.type()).thenReturn(Catalog.Type.RELATIONAL);
+      when(catalog.properties()).thenReturn(Map.of());
+      Audit audit = mock(Audit.class);
+      when(audit.createTime()).thenReturn(Instant.now());
+      when(catalog.auditInfo()).thenReturn(audit);
+      when(metalake.listCatalogsInfo()).thenReturn(new Catalog[] {catalog});
+      when(manager.getMetalake(metalakeName)).thenReturn(metalake);
+      when(manager.getTrinoCatalogName(metalakeName, metalakeName + "_memory"))
+          .thenReturn(metalakeName + "_memory");
+    }
+
+    Page page = new GravitinoSystemTableCatalog(manager, null).loadPageData();
+
+    assertEquals(2, page.getPositionCount());
+    assertEquals(
+        Set.of("prod_memory", "dev_memory"), Set.of(varchar(page, 0), 
varchar(page, 0, 1)));
+    // The metalake column tells same-named catalogs of different metalakes 
apart.
+    for (int position = 0; position < 2; position++) {
+      String name = varchar(page, 0, position);
+      assertEquals(name.substring(0, name.indexOf('_')), varchar(page, 3, 
position));
+    }
+  }
+
+  @Test
+  public void 
testCatalogStatusTableReportsEveryMetalakeWithoutConfiguredMetalake() {
+    CatalogConnectorManager manager = mock(CatalogConnectorManager.class);
+    List<CatalogRegistrationState> states =
+        List.of(
+            CatalogRegistrationState.failed("prod", "memory", "memory", null, 
"boom"),
+            CatalogRegistrationState.failed("dev", "memory", "memory", null, 
"boom"));
+    when(manager.getCatalogRegistrationStates(null)).thenReturn(states);
+
+    Page page = new GravitinoSystemTableCatalogStatus(manager, 
null).loadPageData();
+
+    assertEquals(2, page.getPositionCount());
+    assertEquals(Set.of("prod", "dev"), Set.of(varchar(page, 0), varchar(page, 
0, 1)));
+  }
+
+  @Test
+  public void 
testLoadStatusTableReportsEveryMetalakeErrorWithoutConfiguredMetalake() {
+    CatalogConnectorManager manager = mock(CatalogConnectorManager.class);
+    when(manager.getLastLoadAttemptTimeMs()).thenReturn(2000L);
+    when(manager.getLoadOutcome())
+        .thenReturn(
+            new CatalogConnectorManager.LoadOutcome(
+                true,
+                0L,
+                "1 of 2 metalakes failed",
+                1L,
+                Map.of("test", "Connection refused", "dev", "Access Denied")));
+
+    Page page = new GravitinoSystemTableLoadStatus(manager, 
null).loadPageData();
+
+    assertEquals("{\"dev\":\"Access Denied\",\"test\":\"Connection 
refused\"}", varchar(page, 5));
+  }
+
   @Test
   public void testCatalogStatusTableRendersRegisteredCatalog() {
     GravitinoCatalog catalog =
@@ -313,7 +378,11 @@ public class TestGravitinoSystemStatusTables {
   }
 
   private static String varchar(Page page, int channel) {
+    return varchar(page, channel, 0);
+  }
+
+  private static String varchar(Page page, int channel, int position) {
     Block block = page.getBlock(channel);
-    return VARCHAR.getSlice(block, 0).toStringUtf8();
+    return VARCHAR.getSlice(block, position).toStringUtf8();
   }
 }

Reply via email to