This is an automated email from the ASF dual-hosted git repository.
morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new c76ff7b61b8 [opt](catalog) support nested namespaces of iceberg
(#56415)
c76ff7b61b8 is described below
commit c76ff7b61b8f92b0ba376a04e4d08145d8d59896
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Tue Sep 30 17:44:34 2025 -0700
[opt](catalog) support nested namespaces of iceberg (#56415)
### What problem does this PR solve?
Iceberg has 3 levels of metadata: catalog, namespace and table, mapping
to Doris' catalog, database and table.
Iceberg support nested namespaces, which means the following namespaces
are valid:
```
ns1
ns1.ns2
ns1.ns2.ns3
```
So we need to support mapping nested namespace to Doris' database.
This PR add a global variable `enable_nested_namespace` to control this
behavior.
Default is `false`, and no logic is changed.
If set to true, Doris can support following statments:
```
mysql> switch iceberg;
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| nested |
| nested.db1 |
| nested.db2 |
+--------------------+
mysql> use iceberg.nested.db1;
ERROR 1049 (42000): Only one dot can be in the name: iceberg.nested.db1
mysql> use iceberg.`nested.db1`;
ERROR 5086 (42000): errCode = 2, detailMessage = Unknown catalog 'nested'
mysql> set global enable_nested_namespace=true;
mysql> use iceberg.nested.db1;
Database changed
mysql> select k1 from iceberg.`nested.db1`.nested1;
mysql> select nested1.k1 from `nested.db1`.nested1;
mysql> select `nested.db1`.nested1.k1 from iceberg.`nested.db1`.nested1;
mysql> select iceberg.`nested.db1`.nested1.k1 from nested1;
+------+
| k1 |
+------+
| 1 |
+------+
mysql> refresh catalog iceberg;
mysql> refresh database iceberg.`nested.db1`;
mysql> refresh table iceberg.`nested.db1`.nested1;
Query OK, 0 rows affected (0.01 sec)
```
But, I can execute statement like:
```
use iceberg.`nested.db1`;
```
I don't know why, there is a very strange behavior in MySQL client, when
adding back quota,
the INIT_DB command can only receive `nested.db1` part, but expect
`iceberg.nested.db1`.
Also support creating nested database name in internal catalog:
```
create database `db1.db2`
```
---
.../create_preinstalled_scripts/iceberg/run20.sql | 2 +
.../main/java/org/apache/doris/catalog/Env.java | 2 +-
.../java/org/apache/doris/common/FeNameFormat.java | 26 +-
.../datasource/iceberg/IcebergMetadataOps.java | 74 +++--
.../property/metastore/IcebergRestProperties.java | 7 +-
.../java/org/apache/doris/mysql/MysqlProto.java | 43 +--
.../doris/nereids/parser/LogicalPlanBuilder.java | 15 +-
.../trees/plans/commands/DropCatalogCommand.java | 2 +-
.../org/apache/doris/qe/ConnectContextUtil.java | 55 ++++
.../java/org/apache/doris/qe/ConnectProcessor.java | 44 +--
.../java/org/apache/doris/qe/GlobalVariable.java | 12 +
.../org/apache/doris/qe/MysqlConnectProcessor.java | 50 +---
.../org/apache/doris/common/FeNameFormatTest.java | 147 +++++++++-
.../org/apache/doris/mysql/MysqlProtoTest.java | 9 -
.../org/apache/doris/qe/ConnectContextTest.java | 179 ++++++++++++-
.../iceberg_and_internal_nested_namespace.out | 128 +++++++++
.../iceberg_and_internal_nested_namespace.groovy | 298 +++++++++++++++++++++
17 files changed, 929 insertions(+), 164 deletions(-)
diff --git
a/docker/thirdparties/docker-compose/iceberg/scripts/create_preinstalled_scripts/iceberg/run20.sql
b/docker/thirdparties/docker-compose/iceberg/scripts/create_preinstalled_scripts/iceberg/run20.sql
new file mode 100644
index 00000000000..dba9680f456
--- /dev/null
+++
b/docker/thirdparties/docker-compose/iceberg/scripts/create_preinstalled_scripts/iceberg/run20.sql
@@ -0,0 +1,2 @@
+create database if not exists nested.db1;
+
diff --git a/fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java
b/fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java
index f22b7b0acdc..c75b870d91e 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java
@@ -3375,7 +3375,7 @@ public class Env {
if (StringUtils.isEmpty(command.getCtlName())) {
catalogIf = getCurrentCatalog();
} else {
- catalogIf = catalogMgr.getCatalog(command.getCtlName());
+ catalogIf =
catalogMgr.getCatalogOrDdlException(command.getCtlName());
}
catalogIf.createDb(command.getDbName(), command.isIfNotExists(),
command.getProperties());
}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/common/FeNameFormat.java
b/fe/fe-core/src/main/java/org/apache/doris/common/FeNameFormat.java
index 82db6ac659b..9917f7c7030 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/common/FeNameFormat.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/common/FeNameFormat.java
@@ -24,6 +24,7 @@ import org.apache.doris.datasource.InternalCatalog;
import org.apache.doris.mysql.privilege.Role;
import org.apache.doris.mysql.privilege.RoleManager;
import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.GlobalVariable;
import org.apache.doris.qe.VariableMgr;
import com.google.common.base.Strings;
@@ -53,6 +54,10 @@ public class FeNameFormat {
public static final String TEMPORARY_TABLE_SIGN = "_#TEMP#_";
+ private static final String NESTED_DB_NAME_REGEX =
"^[a-zA-Z][a-zA-Z0-9\\-_]*(\\.([a-zA-Z0-9\\-_]+))*$";
+ private static final String NESTED_UNICODE_DB_NAME_REGEX
+ =
"^[a-zA-Z\\p{L}][a-zA-Z0-9\\-_\\p{L}]*(\\.([a-zA-Z0-9\\-_\\p{L}]+))*$";
+
public static void checkCatalogName(String catalogName) throws
AnalysisException {
if (!InternalCatalog.INTERNAL_CATALOG_NAME.equals(catalogName) &&
(Strings.isNullOrEmpty(catalogName)
|| !catalogName.matches(getCommonNameRegex()))) {
@@ -61,7 +66,10 @@ public class FeNameFormat {
}
public static void checkDbName(String dbName) throws AnalysisException {
- if (Strings.isNullOrEmpty(dbName) ||
!dbName.matches(getCommonNameRegex())) {
+ if (Strings.isNullOrEmpty(dbName)) {
+ ErrorReport.reportAnalysisException(ErrorCode.ERR_WRONG_DB_NAME,
dbName);
+ }
+ if (!dbName.matches(getDbNameRegex())) {
ErrorReport.reportAnalysisException(ErrorCode.ERR_WRONG_DB_NAME,
dbName);
}
}
@@ -257,6 +265,22 @@ public class FeNameFormat {
}
}
+ public static String getDbNameRegex() {
+ if (GlobalVariable.enableNestedNamespace) {
+ if (FeNameFormat.isEnableUnicodeNameSupport()) {
+ return NESTED_UNICODE_DB_NAME_REGEX;
+ } else {
+ return NESTED_DB_NAME_REGEX;
+ }
+ } else {
+ if (FeNameFormat.isEnableUnicodeNameSupport()) {
+ return UNICODE_COMMON_NAME_REGEX;
+ } else {
+ return COMMON_NAME_REGEX;
+ }
+ }
+ }
+
public static String getOutfileSuccessFileNameRegex() {
if (FeNameFormat.isEnableUnicodeNameSupport()) {
return UNICODE_UNDERSCORE_COMMON_NAME_REGEX;
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergMetadataOps.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergMetadataOps.java
index f57392a4f4c..d722183caf9 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergMetadataOps.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergMetadataOps.java
@@ -33,6 +33,8 @@ import org.apache.doris.datasource.ExternalCatalog;
import org.apache.doris.datasource.ExternalDatabase;
import org.apache.doris.datasource.ExternalTable;
import org.apache.doris.datasource.operations.ExternalMetadataOps;
+import org.apache.doris.datasource.property.metastore.IcebergRestProperties;
+import org.apache.doris.datasource.property.metastore.MetastoreProperties;
import org.apache.doris.nereids.trees.plans.commands.info.BranchOptions;
import
org.apache.doris.nereids.trees.plans.commands.info.CreateOrReplaceBranchInfo;
import
org.apache.doris.nereids.trees.plans.commands.info.CreateOrReplaceTagInfo;
@@ -54,12 +56,14 @@ import org.apache.iceberg.catalog.Namespace;
import org.apache.iceberg.catalog.SupportsNamespaces;
import org.apache.iceberg.catalog.TableIdentifier;
import org.apache.iceberg.catalog.ViewCatalog;
+import org.apache.iceberg.exceptions.NoSuchNamespaceException;
import org.apache.iceberg.expressions.Literal;
import org.apache.iceberg.types.Type;
import org.apache.iceberg.types.Types.NestedField;
import org.apache.iceberg.view.View;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collections;
@@ -68,6 +72,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
public class IcebergMetadataOps implements ExternalMetadataOps {
@@ -129,15 +134,36 @@ public class IcebergMetadataOps implements
ExternalMetadataOps {
public List<String> listDatabaseNames() {
try {
- return executionAuthenticator.execute(() ->
nsCatalog.listNamespaces(getNamespace())
- .stream()
- .map(n -> n.level(n.length() - 1))
- .collect(Collectors.toList()));
+ return executionAuthenticator.execute(() ->
listNestedNamespaces(getNamespace()));
} catch (Exception e) {
throw new RuntimeException("Failed to list database names, error
message is:" + e.getMessage(), e);
}
}
+ @NotNull
+ private List<String> listNestedNamespaces(Namespace parentNs) {
+ // Handle nested namespaces for Iceberg REST catalog,
+ // only if "iceberg.rest.nested-namespace-enabled" is true.
+ if (dorisCatalog instanceof IcebergRestExternalCatalog) {
+ IcebergRestExternalCatalog restCatalog =
(IcebergRestExternalCatalog) dorisCatalog;
+ MetastoreProperties metaProps =
restCatalog.getCatalogProperty().getMetastoreProperties();
+ if (metaProps instanceof IcebergRestProperties
+ && ((IcebergRestProperties)
metaProps).isIcebergRestNestedNamespaceEnabled()) {
+ return nsCatalog.listNamespaces(parentNs)
+ .stream()
+ .flatMap(childNs -> Stream.concat(
+ Stream.of(childNs.toString()),
+ listNestedNamespaces(childNs).stream()
+ )).collect(Collectors.toList());
+ }
+ }
+
+ return nsCatalog.listNamespaces(parentNs)
+ .stream()
+ .map(n -> n.level(n.length() - 1))
+ .collect(Collectors.toList());
+ }
+
@Override
public List<String> listTableNames(String dbName) {
try {
@@ -209,7 +235,7 @@ public class IcebergMetadataOps implements
ExternalMetadataOps {
public void dropDbImpl(String dbName, boolean ifExists, boolean force)
throws DdlException {
try {
executionAuthenticator.execute(() -> {
- preformDropDb(dbName, ifExists, force);
+ performDropDb(dbName, ifExists, force);
return null;
});
} catch (Exception e) {
@@ -218,7 +244,7 @@ public class IcebergMetadataOps implements
ExternalMetadataOps {
}
}
- private void preformDropDb(String dbName, boolean ifExists, boolean force)
throws DdlException {
+ private void performDropDb(String dbName, boolean ifExists, boolean force)
throws DdlException {
ExternalDatabase dorisDb = dorisCatalog.getDbNullable(dbName);
if (dorisDb == null) {
if (ifExists) {
@@ -229,21 +255,27 @@ public class IcebergMetadataOps implements
ExternalMetadataOps {
}
}
if (force) {
- // try to drop all tables in the database
- List<String> remoteTableNames =
listTableNames(dorisDb.getRemoteName());
- for (String remoteTableName : remoteTableNames) {
- performDropTable(dorisDb.getRemoteName(), remoteTableName,
true);
- }
- if (!remoteTableNames.isEmpty()) {
- LOG.info("drop database[{}] with force, drop all tables, num:
{}", dbName, remoteTableNames.size());
- }
- // try to drop all views in the database
- List<String> remoteViewNames =
listViewNames(dorisDb.getRemoteName());
- for (String remoteViewName : remoteViewNames) {
- performDropView(dorisDb.getRemoteName(), remoteViewName);
- }
- if (!remoteViewNames.isEmpty()) {
- LOG.info("drop database[{}] with force, drop all views, num:
{}", dbName, remoteViewNames.size());
+ try {
+ // try to drop all tables in the database
+ List<String> remoteTableNames =
listTableNames(dorisDb.getRemoteName());
+ for (String remoteTableName : remoteTableNames) {
+ performDropTable(dorisDb.getRemoteName(), remoteTableName,
true);
+ }
+ if (!remoteTableNames.isEmpty()) {
+ LOG.info("drop database[{}] with force, drop all tables,
num: {}", dbName, remoteTableNames.size());
+ }
+ // try to drop all views in the database
+ List<String> remoteViewNames =
listViewNames(dorisDb.getRemoteName());
+ for (String remoteViewName : remoteViewNames) {
+ performDropView(dorisDb.getRemoteName(), remoteViewName);
+ }
+ if (!remoteViewNames.isEmpty()) {
+ LOG.info("drop database[{}] with force, drop all views,
num: {}", dbName, remoteViewNames.size());
+ }
+ } catch (NoSuchNamespaceException e) {
+ // just ignore
+ LOG.info("drop database[{}] force which does not exist",
dbName);
+ return;
}
}
nsCatalog.dropNamespace(getNamespace(dorisDb.getRemoteName()));
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/property/metastore/IcebergRestProperties.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/property/metastore/IcebergRestProperties.java
index ed9abc84e9f..faf5d736af1 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/datasource/property/metastore/IcebergRestProperties.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/datasource/property/metastore/IcebergRestProperties.java
@@ -110,9 +110,8 @@ public class IcebergRestProperties extends
AbstractIcebergProperties {
@ConnectorProperty(names = {"iceberg.rest.nested-namespace-enabled"},
required = false,
- supported = false,
description = "Enable nested namespace for the iceberg rest
catalog service.")
- private String icebergRestNestedNamespaceEnabled = "true";
+ private String icebergRestNestedNamespaceEnabled = "false";
@ConnectorProperty(names = {"iceberg.rest.case-insensitive-name-matching"},
required = false,
@@ -306,6 +305,10 @@ public class IcebergRestProperties extends
AbstractIcebergProperties {
return Boolean.parseBoolean(icebergRestVendedCredentialsEnabled);
}
+ public boolean isIcebergRestNestedNamespaceEnabled() {
+ return Boolean.parseBoolean(icebergRestNestedNamespaceEnabled);
+ }
+
/**
* Unified method to configure FileIO properties for Iceberg catalog.
* This method handles all storage types (HDFS, S3, MinIO, etc.) and
populates
diff --git a/fe/fe-core/src/main/java/org/apache/doris/mysql/MysqlProto.java
b/fe/fe-core/src/main/java/org/apache/doris/mysql/MysqlProto.java
index 3b0a5bd8c44..8a570e6bc99 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/mysql/MysqlProto.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/MysqlProto.java
@@ -18,14 +18,15 @@
package org.apache.doris.mysql;
import org.apache.doris.catalog.Env;
-import org.apache.doris.cloud.catalog.CloudEnv;
import org.apache.doris.common.Config;
import org.apache.doris.common.DdlException;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
+import org.apache.doris.common.Pair;
import org.apache.doris.datasource.CatalogIf;
import org.apache.doris.datasource.InternalCatalog;
import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.ConnectContextUtil;
import com.google.common.base.Strings;
import org.apache.logging.log4j.LogManager;
@@ -33,6 +34,7 @@ import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.nio.ByteBuffer;
+import java.util.Optional;
// MySQL protocol util
public class MysqlProto {
@@ -224,42 +226,9 @@ public class MysqlProto {
// set database
String db = authPacket.getDb();
if (!Strings.isNullOrEmpty(db)) {
- String catalogName = null;
- String dbName = null;
- String[] dbNames = db.split("\\.");
- if (dbNames.length == 1) {
- dbName = db;
- } else if (dbNames.length == 2) {
- catalogName = dbNames[0];
- dbName = dbNames[1];
- } else if (dbNames.length > 2) {
- context.getState().setError(ErrorCode.ERR_BAD_DB_ERROR, "Only
one dot can be in the name: " + db);
- sendResponsePacket(context);
- return false;
- }
-
- // mysql -d
- if (Config.isCloudMode()) {
- try {
- dbName = ((CloudEnv)
Env.getCurrentEnv()).analyzeCloudCluster(dbName, context);
- } catch (DdlException e) {
- context.getState().setError(e.getMysqlErrorCode(),
e.getMessage());
- sendResponsePacket(context);
- return false;
- }
-
- if (dbName == null || dbName.isEmpty()) {
- return true;
- }
- }
-
- try {
- if (catalogName != null) {
- context.getEnv().changeCatalog(context, catalogName);
- }
- Env.getCurrentEnv().changeDb(context, dbName);
- } catch (DdlException e) {
- context.getState().setError(e.getMysqlErrorCode(),
e.getMessage());
+ Optional<Pair<ErrorCode, String>> res =
ConnectContextUtil.initCatalogAndDb(context, db);
+ if (res.isPresent()) {
+ context.getState().setError(res.get().first, res.get().second);
sendResponsePacket(context);
return false;
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
index 9fbc6838fd3..5feda8b6240 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
@@ -1043,6 +1043,7 @@ import org.apache.doris.nereids.util.Utils;
import org.apache.doris.policy.FilterType;
import org.apache.doris.policy.PolicyTypeEnum;
import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.GlobalVariable;
import org.apache.doris.qe.SqlModeHelper;
import org.apache.doris.resource.workloadschedpolicy.WorkloadActionMeta;
import org.apache.doris.resource.workloadschedpolicy.WorkloadConditionMeta;
@@ -5270,15 +5271,21 @@ public class LogicalPlanBuilder extends
DorisParserBaseVisitor<Object> {
if (size == 0) {
throw new ParseException("database name can't be empty");
}
- String dbName = parts.get(size - 1);
// [db].
if (size == 1) {
- return new RefreshDatabaseCommand(dbName, properties);
+ return new RefreshDatabaseCommand(parts.get(0), properties);
} else if (parts.size() == 2) { // [ctl,db].
- return new RefreshDatabaseCommand(parts.get(0), dbName,
properties);
+ return new RefreshDatabaseCommand(parts.get(0), parts.get(1),
properties);
+ } else {
+ if (GlobalVariable.enableNestedNamespace) {
+ // use the first part as catalog name, the rest part as db name
+ return new RefreshDatabaseCommand(parts.get(0),
+ String.join(".", parts.subList(1, size)), properties);
+ } else {
+ throw new ParseException("Only one dot can be in the name: " +
String.join(".", parts));
+ }
}
- throw new ParseException("Only one dot can be in the name: " +
String.join(".", parts));
}
@Override
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropCatalogCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropCatalogCommand.java
index 034ecb1053a..8a0cb73f75e 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropCatalogCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropCatalogCommand.java
@@ -50,7 +50,7 @@ public class DropCatalogCommand extends DropCommand {
Util.checkCatalogAllRules(catalogName);
if (catalogName.equals(InternalCatalog.INTERNAL_CATALOG_NAME)) {
- throw new AnalysisException("Internal catalog can't be drop.");
+ throw new AnalysisException("Internal catalog can't be dropped.");
}
if (!Env.getCurrentEnv().getAccessManager().checkCtlPriv(
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/qe/ConnectContextUtil.java
b/fe/fe-core/src/main/java/org/apache/doris/qe/ConnectContextUtil.java
index b0a705599ca..48c7744b570 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/qe/ConnectContextUtil.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/qe/ConnectContextUtil.java
@@ -19,8 +19,17 @@ package org.apache.doris.qe;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.catalog.Env;
+import org.apache.doris.cloud.catalog.CloudEnv;
+import org.apache.doris.common.Config;
+import org.apache.doris.common.DdlException;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.Pair;
+import org.apache.doris.common.util.Util;
import org.apache.doris.nereids.StatementContext;
+import java.util.Optional;
+import java.util.stream.Stream;
+
public class ConnectContextUtil {
// Sometimes it's necessary to parse SQL, but not in a user thread where
no ConnectContext exists.
@@ -39,4 +48,50 @@ public class ConnectContextUtil {
ctx.setThreadLocalInfo();
return ctx;
}
+
+ public static Optional<Pair<ErrorCode, String>>
initCatalogAndDb(ConnectContext ctx, String fullDbName) {
+ String catalogName = null;
+ String dbName = null;
+ String[] dbNames = fullDbName.split("\\.");
+ if (dbNames.length == 1) {
+ dbName = fullDbName;
+ } else if (dbNames.length == 2) {
+ catalogName = dbNames[0];
+ dbName = dbNames[1];
+ } else if (dbNames.length > 2) {
+ if (GlobalVariable.enableNestedNamespace) {
+ // use the first part as catalog name, the rest part as db name
+ catalogName = dbNames[0];
+ dbName = Stream.of(dbNames).skip(1).reduce((a, b) -> a + "." +
b).get();
+ } else {
+ return Optional.of(
+ Pair.of(ErrorCode.ERR_BAD_DB_ERROR, "Only one dot can
be in the name: " + fullDbName));
+ }
+ }
+
+ // mysql client
+ if (Config.isCloudMode()) {
+ try {
+ dbName = ((CloudEnv) ctx.getEnv()).analyzeCloudCluster(dbName,
ctx);
+ } catch (DdlException e) {
+ return Optional.of(Pair.of(e.getMysqlErrorCode(),
e.getMessage()));
+ }
+ if (dbName == null || dbName.isEmpty()) {
+ return Optional.empty();
+ }
+ }
+
+ try {
+ if (catalogName != null) {
+ ctx.getEnv().changeCatalog(ctx, catalogName);
+ }
+ ctx.getEnv().changeDb(ctx, dbName);
+ } catch (DdlException e) {
+ return Optional.of(Pair.of(e.getMysqlErrorCode(), e.getMessage()));
+ } catch (Throwable t) {
+ return Optional.of(Pair.of(ErrorCode.ERR_INTERNAL_ERROR,
Util.getRootCauseMessage(t)));
+ }
+ ctx.getState().setOk();
+ return Optional.empty();
+ }
}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/qe/ConnectProcessor.java
b/fe/fe-core/src/main/java/org/apache/doris/qe/ConnectProcessor.java
index f285fd6a811..1f9e0bb49a0 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/qe/ConnectProcessor.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/qe/ConnectProcessor.java
@@ -25,21 +25,18 @@ import org.apache.doris.catalog.Column;
import org.apache.doris.catalog.DatabaseIf;
import org.apache.doris.catalog.Env;
import org.apache.doris.catalog.TableIf;
-import org.apache.doris.cloud.catalog.CloudEnv;
import org.apache.doris.cloud.proto.Cloud;
import org.apache.doris.cloud.qe.ComputeGroupException;
import org.apache.doris.cloud.system.CloudSystemInfoService;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.Config;
import org.apache.doris.common.ConnectionException;
-import org.apache.doris.common.DdlException;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.NotImplementedException;
import org.apache.doris.common.Pair;
import org.apache.doris.common.UserException;
import org.apache.doris.common.util.DebugUtil;
import org.apache.doris.common.util.SqlUtils;
-import org.apache.doris.common.util.Util;
import org.apache.doris.datasource.CatalogIf;
import org.apache.doris.datasource.InternalCatalog;
import org.apache.doris.metric.MetricRepo;
@@ -118,45 +115,10 @@ public abstract class ConnectProcessor {
// change current database of this session.
protected void handleInitDb(String fullDbName) {
- String catalogName = null;
- String dbName = null;
- String[] dbNames = fullDbName.split("\\.");
- if (dbNames.length == 1) {
- dbName = fullDbName;
- } else if (dbNames.length == 2) {
- catalogName = dbNames[0];
- dbName = dbNames[1];
- } else if (dbNames.length > 2) {
- ctx.getState().setError(ErrorCode.ERR_BAD_DB_ERROR, "Only one dot
can be in the name: " + fullDbName);
- return;
- }
-
- // mysql client
- if (Config.isCloudMode()) {
- try {
- dbName = ((CloudEnv) ctx.getEnv()).analyzeCloudCluster(dbName,
ctx);
- } catch (DdlException e) {
- ctx.getState().setError(e.getMysqlErrorCode(), e.getMessage());
- return;
- }
- if (dbName == null || dbName.isEmpty()) {
- return;
- }
+ Optional<Pair<ErrorCode, String>> res =
ConnectContextUtil.initCatalogAndDb(ctx, fullDbName);
+ if (res.isPresent()) {
+ ctx.getState().setError(res.get().first, res.get().second);
}
-
- try {
- if (catalogName != null) {
- ctx.getEnv().changeCatalog(ctx, catalogName);
- }
- ctx.getEnv().changeDb(ctx, dbName);
- } catch (DdlException e) {
- ctx.getState().setError(e.getMysqlErrorCode(), e.getMessage());
- return;
- } catch (Throwable t) {
- ctx.getState().setError(ErrorCode.ERR_INTERNAL_ERROR,
Util.getRootCauseMessage(t));
- return;
- }
- ctx.getState().setOk();
}
// set killed flag
diff --git a/fe/fe-core/src/main/java/org/apache/doris/qe/GlobalVariable.java
b/fe/fe-core/src/main/java/org/apache/doris/qe/GlobalVariable.java
index 31901c76c4a..ff2b423b8a1 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/qe/GlobalVariable.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/qe/GlobalVariable.java
@@ -79,6 +79,8 @@ public final class GlobalVariable {
public static final String ENABLE_FETCH_ICEBERG_STATS =
"enable_fetch_iceberg_stats";
+ public static final String ENABLE_NESTED_NAMESPACE =
"enable_nested_namespace";
+
public static final String ENABLE_ANSI_QUERY_ORGANIZATION_BEHAVIOR
= "enable_ansi_query_organization_behavior";
public static final String ENABLE_NEW_TYPE_COERCION_BEHAVIOR
@@ -239,6 +241,16 @@ public final class GlobalVariable {
+ " a numeric type and precision cannot be
determined, the DECIMAL type is preferred."})
public static boolean enableNewTypeCoercionBehavior = true;
+ @VariableMgr.VarAttr(name = ENABLE_NESTED_NAMESPACE, flag =
VariableMgr.GLOBAL,
+ description = {
+ "是否允许访问 `ns1.ns2` 这种类型的 database。当前仅适用于 External Catalog
中映射 Database 并访问。"
+ + "不支持创建。",
+ "Whether to allow accessing databases of the form
`ns1.ns2`. "
+ + "Currently, this only applies to mapping
databases in "
+ + "External Catalogs and accessing them. "
+ + "Creation is not supported."})
+ public static boolean enableNestedNamespace = false;
+
// Don't allow creating instance.
private GlobalVariable() {
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/qe/MysqlConnectProcessor.java
b/fe/fe-core/src/main/java/org/apache/doris/qe/MysqlConnectProcessor.java
index 3b309136990..50d3bed474a 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/qe/MysqlConnectProcessor.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/qe/MysqlConnectProcessor.java
@@ -21,14 +21,11 @@ import org.apache.doris.analysis.StatementBase;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.catalog.Env;
import org.apache.doris.catalog.MysqlColType;
-import org.apache.doris.cloud.catalog.CloudEnv;
import org.apache.doris.common.AuthenticationException;
-import org.apache.doris.common.Config;
import org.apache.doris.common.ConnectionException;
-import org.apache.doris.common.DdlException;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
-import org.apache.doris.datasource.CatalogIf;
+import org.apache.doris.common.Pair;
import org.apache.doris.datasource.InternalCatalog;
import org.apache.doris.mysql.MysqlChannel;
import org.apache.doris.mysql.MysqlCommand;
@@ -58,6 +55,7 @@ import java.nio.channels.AsynchronousCloseException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
+import java.util.Optional;
/**
* Process one mysql connection, receive one packet, process, send one packet.
@@ -355,47 +353,9 @@ public class MysqlConnectProcessor extends
ConnectProcessor {
if (Strings.isNullOrEmpty(db)) {
ctx.changeDefaultCatalog(InternalCatalog.INTERNAL_CATALOG_NAME);
} else {
- String catalogName = null;
- String dbName = null;
- String[] dbNames = db.split("\\.");
- if (dbNames.length == 1) {
- dbName = db;
- } else if (dbNames.length == 2) {
- catalogName = dbNames[0];
- dbName = dbNames[1];
- } else if (dbNames.length > 2) {
- ctx.getState().setError(ErrorCode.ERR_BAD_DB_ERROR, "Only one
dot can be in the name: " + db);
- return;
- }
-
- if (Config.isCloudMode()) {
- try {
- dbName = ((CloudEnv)
Env.getCurrentEnv()).analyzeCloudCluster(dbName, ctx);
- } catch (DdlException e) {
- ctx.getState().setError(e.getMysqlErrorCode(),
e.getMessage());
- return;
- }
- }
-
- // check catalog and db exists
- if (catalogName != null) {
- CatalogIf catalogIf =
ctx.getEnv().getCatalogMgr().getCatalog(catalogName);
- if (catalogIf == null) {
- ctx.getState().setError(ErrorCode.ERR_BAD_DB_ERROR, "No
match catalog in doris: " + db);
- return;
- }
- if (catalogIf.getDbNullable(dbName) == null) {
- ctx.getState().setError(ErrorCode.ERR_BAD_DB_ERROR, "No
match database in doris: " + db);
- return;
- }
- }
- try {
- if (catalogName != null) {
- ctx.getEnv().changeCatalog(ctx, catalogName);
- }
- Env.getCurrentEnv().changeDb(ctx, dbName);
- } catch (DdlException e) {
- ctx.getState().setError(e.getMysqlErrorCode(), e.getMessage());
+ Optional<Pair<ErrorCode, String>> res =
ConnectContextUtil.initCatalogAndDb(ctx, db);
+ if (res.isPresent()) {
+ ctx.getState().setError(res.get().first, res.get().second);
return;
}
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/common/FeNameFormatTest.java
b/fe/fe-core/src/test/java/org/apache/doris/common/FeNameFormatTest.java
index 124604bff8d..336c693d82f 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/common/FeNameFormatTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/common/FeNameFormatTest.java
@@ -17,6 +17,7 @@
package org.apache.doris.common;
+import org.apache.doris.qe.GlobalVariable;
import org.apache.doris.qe.VariableMgr;
import com.google.common.collect.Lists;
@@ -235,6 +236,150 @@ public class FeNameFormatTest {
test(FeNameFormat::checkUserName, alwaysValid, alwaysInvalid,
unicodeValid);
}
+ @Test
+ void testDbName() {
+ boolean defaultUnicode =
VariableMgr.getDefaultSessionVariable().enableUnicodeNameSupport;
+ boolean defaultNestedNamespace = GlobalVariable.enableNestedNamespace;
+ List<Boolean> enableUnicode = Lists.newArrayList(false, true);
+ List<Boolean> enableNestedNamespace = Lists.newArrayList(false, true);
+
+ // Names that are always valid regardless of nested namespace setting
+ List<String> alwaysValid = Arrays.asList(
+ "abc123", // ASCII letters + numbers
+ "A-1_b", // with allowed symbols (-_)
+ "Z", // single ASCII letter
+ "a1b2c3", // alphanumeric
+ "x_y-z", // underscore and hyphen
+ "test", // letters only
+ "a-b-c", // multiple hyphens
+ "a_b", // underscore
+ "a-1", // hyphen + number
+ "B2" // uppercase + number
+ );
+
+ // Names that are always invalid regardless of settings
+ List<String> alwaysInvalid = Arrays.asList(
+ "1abc", // starts with number
+ "@test", // contains invalid symbol @
+ "", // empty string
+ "a b", // contains space
+ "abc!", // contains invalid symbol !
+ "a\nb", // contains newline
+ "abc$", // contains invalid symbol $
+ "-abc", // starts with hyphen
+ "_abc", // starts with underscore
+ "a*b", // contains asterisk
+ "a#b" // contains hash symbol
+ );
+
+ // Names with dots - only valid when nested namespace is enabled
+ List<String> dotNames = Arrays.asList(
+ "db1.db2", // database name with dot in middle
+ "db1.db2.db3", // multiple dots in middle
+ "a.b.c.d", // multiple segments with dots
+ "test.prod", // simple dot notation
+ "system.user.profile" // nested database name
+ );
+
+ // Names with dots that are always invalid (start/end with dot,
consecutive dots)
+ List<String> invalidDotNames = Arrays.asList(
+ ".abc", // starts with dot
+ "abc.", // ends with dot
+ ".abc.def", // starts with dot
+ "abc.def.", // ends with dot
+ "a..b", // consecutive dots
+ "a.b.", // ends with dot after valid segment
+ ".a.b" // starts with dot before valid segment
+ );
+
+ // Unicode names that are always valid
+ List<String> unicodeValid = Lists.newArrayList(
+ "éclair", // French letters
+ "über", // German umlaut
+ "北京", // Chinese characters
+ "東京123", // Japanese + numbers
+ "München", // German umlaut
+ "Beyoncé", // French accent
+ "αβγ", // Greek letters
+ "русский", // Cyrillic letters
+ "øre", // Nordic letter
+ "ção", // Portuguese letter
+ "naïve", // French diacritic
+ "Ḥello", // special diacritic
+ "ẞig" // German sharp S
+ );
+
+ // Unicode names with dots - only valid when both unicode and nested
namespace are enabled
+ List<String> unicodeDotNames = Lists.newArrayList(
+ "北京.東京", // Chinese and Japanese with dot
+ "café.système", // French words with dot
+ "über.München", // German words with dot
+ "αβγ.русский" // Greek and Cyrillic with dot
+ );
+
+ try {
+ for (Boolean unicode : enableUnicode) {
+ for (Boolean nestedNamespace : enableNestedNamespace) {
+
VariableMgr.getDefaultSessionVariable().setEnableUnicodeNameSupport(unicode);
+ GlobalVariable.enableNestedNamespace = nestedNamespace;
+
+ // Test always valid names
+ for (String s : alwaysValid) {
+ ExceptionChecker.expectThrowsNoException(() ->
FeNameFormat.checkDbName(s));
+ }
+
+ // Test always invalid names
+ for (String s : alwaysInvalid) {
+
Assertions.assertThrowsExactly(AnalysisException.class, () ->
FeNameFormat.checkDbName(s),
+ "name should be invalid: " + s
+ + " (unicode=" + unicode + ", nested="
+ nestedNamespace + ")");
+ }
+
+ // Test names with invalid dot patterns (always invalid)
+ for (String s : invalidDotNames) {
+
Assertions.assertThrowsExactly(AnalysisException.class, () ->
FeNameFormat.checkDbName(s),
+ "name should be invalid: " + s
+ + " (unicode=" + unicode + ", nested="
+ nestedNamespace + ")");
+ }
+
+ // Test names with dots (valid only when nested namespace
is enabled)
+ for (String s : dotNames) {
+ if (nestedNamespace) {
+ ExceptionChecker.expectThrowsNoException(() ->
FeNameFormat.checkDbName(s));
+ } else {
+
Assertions.assertThrowsExactly(AnalysisException.class, () ->
FeNameFormat.checkDbName(s),
+ "name should be invalid when nested
namespace is disabled: " + s);
+ }
+ }
+
+ // Test unicode names
+ for (String s : unicodeValid) {
+ if (unicode) {
+ ExceptionChecker.expectThrowsNoException(() ->
FeNameFormat.checkDbName(s));
+ } else {
+
Assertions.assertThrowsExactly(AnalysisException.class, () ->
FeNameFormat.checkDbName(s),
+ "unicode name should be invalid when
unicode issh bi disabled: " + s);
+ }
+ }
+
+ // Test unicode names with dots (valid only when both
unicode and nested namespace are enabled)
+ for (String s : unicodeDotNames) {
+ if (unicode && nestedNamespace) {
+ ExceptionChecker.expectThrowsNoException(() ->
FeNameFormat.checkDbName(s));
+ } else {
+
Assertions.assertThrowsExactly(AnalysisException.class, () ->
FeNameFormat.checkDbName(s),
+ "unicode dot name should be invalid: " + s
+ + " (unicode=" + unicode + ",
nested=" + nestedNamespace + ")");
+ }
+ }
+ }
+ }
+ } finally {
+
VariableMgr.getDefaultSessionVariable().setEnableUnicodeNameSupport(defaultUnicode);
+ GlobalVariable.enableNestedNamespace = defaultNestedNamespace;
+ }
+ }
+
@Test
void testCommonName() {
List<String> alwaysValid = Arrays.asList(
@@ -261,7 +406,7 @@ public class FeNameFormatTest {
"_abc", // starts with underscore
StringUtils.repeat("a", 65), // exceeds length limit (64)
"a*b", // contains asterisk
- "a.b", // contains dot (if not allowed)
+ "a.b", // contains dot (not allowed)
"a#b" // contains hash symbol
);
List<String> unicodeValid = Lists.newArrayList(
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/mysql/MysqlProtoTest.java
b/fe/fe-core/src/test/java/org/apache/doris/mysql/MysqlProtoTest.java
index 0142b285dd7..18a7117e920 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/mysql/MysqlProtoTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/mysql/MysqlProtoTest.java
@@ -314,15 +314,6 @@ public class MysqlProtoTest {
Assert.assertFalse(MysqlProto.negotiate(context));
}
- @Test
- public void testNegotiateInvalidPasswd() throws Exception {
- mockChannel("user", true);
- mockPassword(false);
- mockAccess();
- ConnectContext context = new ConnectContext(streamConnection);
- Assert.assertTrue(MysqlProto.negotiate(context));
- }
-
@Test
public void testNegotiateNoUser() throws Exception {
mockChannel("", true);
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/qe/ConnectContextTest.java
b/fe/fe-core/src/test/java/org/apache/doris/qe/ConnectContextTest.java
index cc522138ca0..874c30e9cbe 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/qe/ConnectContextTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/qe/ConnectContextTest.java
@@ -19,6 +19,9 @@ package org.apache.doris.qe;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.catalog.Env;
+import org.apache.doris.common.DdlException;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.Pair;
import org.apache.doris.mysql.MysqlCapability;
import org.apache.doris.mysql.MysqlCommand;
import org.apache.doris.mysql.privilege.Auth;
@@ -103,7 +106,7 @@ public class ConnectContextTest {
// Thread info
Assert.assertNotNull(ctx.toThreadInfo(false));
- List<String> row = ctx.toThreadInfo(false).toRow(101, 1000,
Optional.empty());
+ List<String> row = ctx.toThreadInfo(false).toRow(101, 1000,
Optional.of("+08:00"));
Assert.assertEquals(15, row.size());
Assert.assertEquals("Yes", row.get(0));
Assert.assertEquals("101", row.get(1));
@@ -298,4 +301,178 @@ public class ConnectContextTest {
Assert.assertEquals(queryId2, context.queryId);
Assert.assertEquals(queryId, context.lastQueryId);
}
+
+ @Test
+ public void testInitCatalogAndDbSinglePart() throws Exception {
+ ConnectContext ctx = new ConnectContext();
+ ctx.setEnv(env);
+
+ new Expectations() {
+ {
+ env.changeDb(ctx, "testDb");
+ minTimes = 0;
+ }
+ };
+
+ Optional<Pair<ErrorCode, String>> result =
ConnectContextUtil.initCatalogAndDb(ctx, "testDb");
+ Assert.assertFalse(result.isPresent());
+ }
+
+ @Test
+ public void testInitCatalogAndDbTwoParts() throws Exception {
+ ConnectContext ctx = new ConnectContext();
+ ctx.setEnv(env);
+
+ new Expectations() {
+ {
+ env.changeCatalog(ctx, "catalog1");
+ minTimes = 0;
+ env.changeDb(ctx, "testDb");
+ minTimes = 0;
+ }
+ };
+
+ Optional<Pair<ErrorCode, String>> result =
ConnectContextUtil.initCatalogAndDb(ctx, "catalog1.testDb");
+ Assert.assertFalse(result.isPresent());
+ }
+
+ @Test
+ public void testInitCatalogAndDbMultiplePartsWithNestedNamespaceEnabled()
throws Exception {
+ // Temporarily set the field value
+ boolean originalValue = GlobalVariable.enableNestedNamespace;
+ GlobalVariable.enableNestedNamespace = true;
+
+ try {
+ ConnectContext ctx = new ConnectContext();
+ ctx.setEnv(env);
+
+ new Expectations() {
+ {
+ env.changeCatalog(ctx, "catalog1");
+ minTimes = 0;
+ env.changeDb(ctx, "ns1.ns2.testDb");
+ minTimes = 0;
+ }
+ };
+
+ Optional<Pair<ErrorCode, String>> result =
ConnectContextUtil.initCatalogAndDb(ctx,
+ "catalog1.ns1.ns2.testDb");
+ Assert.assertFalse(result.isPresent());
+ } finally {
+ GlobalVariable.enableNestedNamespace = originalValue;
+ }
+ }
+
+ @Test
+ public void testInitCatalogAndDbMultiplePartsWithNestedNamespaceDisabled()
throws Exception {
+ // Ensure GlobalVariable.enableNestedNamespace is false (default)
+ boolean originalValue = GlobalVariable.enableNestedNamespace;
+ GlobalVariable.enableNestedNamespace = false;
+
+ try {
+ ConnectContext ctx = new ConnectContext();
+ ctx.setEnv(env);
+
+ Optional<Pair<ErrorCode, String>> result =
ConnectContextUtil.initCatalogAndDb(ctx,
+ "catalog1.ns1.ns2.testDb");
+ Assert.assertTrue(result.isPresent());
+ Assert.assertEquals(ErrorCode.ERR_BAD_DB_ERROR,
result.get().first);
+ Assert.assertTrue(result.get().second.contains("Only one dot can
be in the name"));
+ } finally {
+ GlobalVariable.enableNestedNamespace = originalValue;
+ }
+ }
+
+ @Test
+ public void testInitCatalogAndDbWithFourPartsNestedNamespaceEnabled()
throws Exception {
+ // Temporarily set GlobalVariable.enableNestedNamespace to be true
+ boolean originalValue = GlobalVariable.enableNestedNamespace;
+ GlobalVariable.enableNestedNamespace = true;
+
+ try {
+ ConnectContext ctx = new ConnectContext();
+ ctx.setEnv(env);
+
+ new Expectations() {
+ {
+ env.changeCatalog(ctx, "catalog1");
+ minTimes = 0;
+ env.changeDb(ctx, "ns1.ns2.ns3.testDb");
+ minTimes = 0;
+ }
+ };
+
+ Optional<Pair<ErrorCode, String>> result =
ConnectContextUtil.initCatalogAndDb(ctx,
+ "catalog1.ns1.ns2.ns3.testDb");
+ Assert.assertFalse(result.isPresent());
+ } finally {
+ GlobalVariable.enableNestedNamespace = originalValue;
+ }
+ }
+
+ @Test
+ public void testInitCatalogAndDbWithChangeCatalogException() throws
Exception {
+ ConnectContext ctx = new ConnectContext();
+ ctx.setEnv(env);
+
+ new Expectations() {
+ {
+ env.changeCatalog(ctx, "invalidCatalog");
+ result = new DdlException("Catalog not found");
+ minTimes = 0;
+ }
+ };
+
+ Optional<Pair<ErrorCode, String>> result =
ConnectContextUtil.initCatalogAndDb(ctx, "invalidCatalog.testDb");
+ Assert.assertTrue(result.isPresent());
+ Assert.assertTrue(result.get().second.contains("Catalog not found"));
+ }
+
+ @Test
+ public void testInitCatalogAndDbWithChangeDbException() throws Exception {
+ ConnectContext ctx = new ConnectContext();
+ ctx.setEnv(env);
+
+ new Expectations() {
+ {
+ env.changeDb(ctx, "invalidDb");
+ result = new DdlException("Database not found");
+ minTimes = 0;
+ }
+ };
+
+ Optional<Pair<ErrorCode, String>> result =
ConnectContextUtil.initCatalogAndDb(ctx, "invalidDb");
+ Assert.assertTrue(result.isPresent());
+ Assert.assertTrue(result.get().second.contains("Database not found"));
+ }
+
+ @Test
+ public void testInitCatalogAndDbEmptyString() throws Exception {
+ ConnectContext ctx = new ConnectContext();
+ ctx.setEnv(env);
+
+ new Expectations() {
+ {
+ env.changeDb(ctx, "");
+ minTimes = 0;
+ }
+ };
+
+ Optional<Pair<ErrorCode, String>> result =
ConnectContextUtil.initCatalogAndDb(ctx, "");
+ Assert.assertFalse(result.isPresent());
+ }
+
+ @Test
+ public void testInitCatalogAndDbNullString() {
+ ConnectContext ctx = new ConnectContext();
+ ctx.setEnv(env);
+
+ // This should cause a NullPointerException when calling split on null
+ try {
+ ConnectContextUtil.initCatalogAndDb(ctx, null);
+ Assert.fail("Expected NullPointerException");
+ } catch (NullPointerException e) {
+ // Expected behavior
+ }
+ }
}
diff --git
a/regression-test/data/external_table_p0/iceberg/iceberg_and_internal_nested_namespace.out
b/regression-test/data/external_table_p0/iceberg/iceberg_and_internal_nested_namespace.out
new file mode 100644
index 00000000000..4432b08854c
--- /dev/null
+++
b/regression-test/data/external_table_p0/iceberg/iceberg_and_internal_nested_namespace.out
@@ -0,0 +1,128 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !sql01 --
+
+-- !sql02 --
+
+-- !sql03 --
+ns1
+
+-- !sql04 --
+ns1.ns2
+
+-- !sql05 --
+ns1.ns2.ns3
+
+-- !sql06 --
+101
+
+-- !sql07 --
+102
+
+-- !sql08 --
+103
+
+-- !sql09 --
+101
+
+-- !sql10 --
+102
+
+-- !sql11 --
+101
+102
+103
+
+-- !sql12 --
+nested_tbl1
+
+-- !sql13 --
+nested_tbl2
+
+-- !sql14 --
+nested_tbl3
+
+-- !sql15 --
+
+-- !sql16 --
+ns1.ns2
+
+-- !sql17 --
+ns1.ns2
+
+-- !sql18 --
+
+-- !sql19 --
+
+-- !sql20 --
+
+-- !sql21 --
+ns1.ns2
+
+-- !sql22 --
+ns1.ns2.ns3
+
+-- !sql23 --
+
+-- !sql24 --
+
+-- !sql25 --
+ns1.ns2
+
+-- !sql26 --
+ns1.ns2.ns3
+
+-- !sql261 --
+104
+
+-- !sql27 --
+ns1.ns2
+
+-- !sql28 --
+
+-- !sql29 --
+104
+
+-- !sql30 --
+
+-- !sql31 --
+105
+
+-- !sql32 --
+
+-- !sql33 --
+nsa
+
+-- !sql34 --
+
+-- !sql35 --
+106
+
+-- !sql1001 --
+idb1
+
+-- !sql1002 --
+idb1.idb2
+
+-- !sql1003 --
+idb1.idb2.idb3
+
+-- !sql101 --
+201
+
+-- !sql103 --
+202
+
+-- !sql104 --
+201
+202
+203
+
+-- !sql2001 --
+idb1
+
+-- !sql2002 --
+idb1.idb2
+
+-- !sql2003 --
+idb1.idb2.idb3
+
diff --git
a/regression-test/suites/external_table_p0/iceberg/iceberg_and_internal_nested_namespace.groovy
b/regression-test/suites/external_table_p0/iceberg/iceberg_and_internal_nested_namespace.groovy
new file mode 100644
index 00000000000..f7eb1f5e8b6
--- /dev/null
+++
b/regression-test/suites/external_table_p0/iceberg/iceberg_and_internal_nested_namespace.groovy
@@ -0,0 +1,298 @@
+// 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.
+
+suite("iceberg_and_internal_nested_namespace",
"p0,external,doris,external_docker,external_docker_doris") {
+
+ String enabled = context.config.otherConfigs.get("enableIcebergTest")
+ if (enabled == null || !enabled.equalsIgnoreCase("true")) {
+ logger.info("disable iceberg test.")
+ return
+ }
+
+ String rest_port = context.config.otherConfigs.get("iceberg_rest_uri_port")
+ String minio_port = context.config.otherConfigs.get("iceberg_minio_port")
+ String externalEnvIp = context.config.otherConfigs.get("externalEnvIp")
+ String catalog_name = "iceberg_nested_namespace"
+
+ sql """drop catalog if exists ${catalog_name}"""
+ // 1.
+ // iceberg.rest.nested-namespace-enabled = false
+ // set global enable_nested_namespace = false
+ sql """set global enable_nested_namespace=false"""
+ sql """
+ CREATE CATALOG ${catalog_name} PROPERTIES (
+ 'type'='iceberg',
+ 'iceberg.catalog.type'='rest',
+ 'uri' = 'http://${externalEnvIp}:${rest_port}',
+ "s3.access_key" = "admin",
+ "s3.secret_key" = "password",
+ "s3.endpoint" = "http://${externalEnvIp}:${minio_port}",
+ "s3.region" = "us-east-1",
+ "iceberg.rest.nested-namespace-enabled" = "false"
+ );"""
+
+ logger.info("catalog " + catalog_name + " created")
+ sql """switch ${catalog_name};"""
+ logger.info("switched to catalog " + catalog_name)
+
+ // there is already a nested namespace "nested.db1", but can only see
"nested"
+ sql """show tables from `nested`;"""
+ test {
+ sql """show tables from `nested.db1`;"""
+ exception """Unknown database 'nested.db1'"""
+ }
+ test {
+ sql """drop database `nested.db1`;"""
+ exception """Can't drop database 'nested.db1'; database doesn't
exist"""
+ }
+ test {
+ sql """select * from `nested.db1`.tbl1;"""
+ exception """Database [nested.db1] does not exist"""
+ }
+ // can not create nested ns
+ test {
+ sql """create database `ns1.ns2`"""
+ exception """Incorrect database name 'ns1.ns2'"""
+ }
+
+ // 2.
+ // iceberg.rest.nested-namespace-enabled = true
+ // set global enable_nested_namespace = false
+ sql """set global enable_nested_namespace = false"""
+ sql """alter catalog ${catalog_name} set
properties("iceberg.rest.nested-namespace-enabled" = "true");"""
+ sql """switch ${catalog_name}"""
+ // can see the nested ns, with back quote
+ sql """show tables from `nested`;"""
+ sql """show tables from `nested.db1`;"""
+ test {
+ sql """show tables from nested.db1"""
+ exception """Unknown catalog 'nested'"""
+ }
+ // for "use" stmt, back quote is not necessary
+ // sql """use ${catalog_name}.nested.db1"""
+ // sql """use ${catalog_name}.`nested.db1`"""
+ // can not create nested ns
+ test {
+ sql """create database `ns1.ns2`"""
+ exception """Incorrect database name 'ns1.ns2'"""
+ }
+
+ // 3.
+ // iceberg.rest.nested-namespace-enabled = true
+ // set global enable_nested_namespace = true
+ sql """set global enable_nested_namespace = true"""
+ sql """alter catalog ${catalog_name} set
properties("iceberg.rest.nested-namespace-enabled" = "true");"""
+ sql """switch ${catalog_name}"""
+ // can see the nested ns, with back quote
+ sql """show tables from `nested`;"""
+ sql """show tables from `nested.db1`;"""
+ test {
+ sql """show tables from nested.db1"""
+ exception """Unknown catalog 'nested'"""
+ }
+ // for "use" stmt, back quote is not necessary
+ sql """use ${catalog_name}.nested;"""
+ sql """use ${catalog_name}.`nested.db1`"""
+ // drop and create nested db1
+ sql """drop database if exists `ns1.ns2.ns3` force"""
+ sql """drop database if exists `ns1.ns2` force"""
+ sql """drop database if exists `ns1` force"""
+ qt_sql01 """show databases like 'ns1.ns2.ns3'""" // empty
+ sql """refresh catalog ${catalog_name}"""
+ qt_sql02 """show databases like 'ns1.ns2.ns3'""" // empty
+
+ sql """create database `ns1.ns2.ns3`"""
+ // will see 3 ns, flat
+ qt_sql03 """show databases like 'ns1'""" // 1
+ qt_sql04 """show databases like 'ns1.ns2'""" // 1
+ qt_sql05 """show databases like 'ns1.ns2.ns3'""" // 1
+ // can create database in each ns
+ sql """create table ns1.nested_tbl1 (k1 int)""";
+ sql """insert into ns1.nested_tbl1 values(101)"""
+ qt_sql06 """select * from ns1.nested_tbl1"""
+
+ sql """create table `ns1.ns2`.nested_tbl2 (k1 int)""";
+ sql """insert into `ns1.ns2`.nested_tbl2 values(102)"""
+ qt_sql07 """select * from `ns1.ns2`.nested_tbl2"""
+
+ sql """use ${catalog_name}.`ns1.ns2.ns3`"""
+ sql """create table nested_tbl3 (k1 int)""";
+ sql """insert into nested_tbl3 values(103)"""
+ qt_sql08 """select * from nested_tbl3"""
+
+ // test select column in diff qualified names
+ qt_sql09 """select ${catalog_name}.ns1.nested_tbl1.k1 from
${catalog_name}.ns1.nested_tbl1"""
+ qt_sql10 """select ${catalog_name}.`ns1.ns2`.nested_tbl2.k1 from
${catalog_name}.`ns1.ns2`.nested_tbl2"""
+ sql """use ${catalog_name}.`ns1.ns2`"""
+ order_qt_sql11 """select ${catalog_name}.ns1.nested_tbl1.k1 from
${catalog_name}.ns1.nested_tbl1
+ union all
+ select k1 from nested_tbl2
+ union all
+ select `ns1.ns2.ns3`.nested_tbl3.k1 from
`ns1.ns2.ns3`.nested_tbl3;
+ """
+ // test table exist in each ns
+ qt_sql12 """show tables from ns1""";
+ qt_sql13 """show tables from `ns1.ns2`""";
+ qt_sql14 """show tables from `ns1.ns2.ns3`""";
+ test {
+ sql """drop database ns1"""
+ exception """Namespace ns1 is not empty. 1 tables exist"""
+ }
+ test {
+ sql """drop database `ns1.ns2`"""
+ exception """Namespace ns1.ns2 is not empty. 1 tables exist"""
+ }
+ test {
+ sql """drop database ${catalog_name}.`ns1.ns2.ns3`"""
+ exception """Namespace ns1.ns2.ns3 is not empty. 1 tables exist"""
+ }
+ // test refresh database and table
+ sql """refresh database ${catalog_name}.`ns1.ns2`"""
+ sql """refresh database `ns1.ns2`"""
+ sql """refresh table ${catalog_name}.`ns1.ns2`.nested_tbl2"""
+ sql """refresh table `ns1.ns2`.nested_tbl2"""
+ test {
+ sql """refresh table `ns1.ns2`.nested_tbl2xxx"""
+ exception """Table nested_tbl2xxx does not exist in db ns1.ns2"""
+ }
+ // drop ns1.ns2 first, we can still see it after refresh, because
ns1.ns2.ns3 still exists
+ sql """drop database `ns1.ns2` force"""
+ qt_sql15 """show databases like "ns1.ns2"""" // empty
+ sql """refresh catalog ${catalog_name}"""
+ qt_sql16 """show databases like "ns1.ns2"""" // 1
+ // then we drop ns1.ns2.ns3, after refresh, ns1.ns2 also disappear
+ sql """drop database `ns1.ns2.ns3` force"""
+ qt_sql17 """show databases like "ns1.ns2"""" // 1
+ qt_sql18 """show databases like "ns1.ns2.ns3"""" // empty
+ sql """refresh catalog ${catalog_name}"""
+ qt_sql19 """show databases like "ns1.ns2"""" // empty
+ qt_sql20 """show databases like "ns1.ns2.ns3"""" // empty
+
+ // recreate ns1.ns2.ns3
+ sql """create database `ns1.ns2.ns3`;"""
+ qt_sql21 """show databases like "ns1.ns2"""" // 1
+ qt_sql22 """show databases like "ns1.ns2.ns3"""" // 1
+ // drop ns1.ns2.ns3, and ns1.ns2 will disappear too
+ sql """drop database `ns1.ns2.ns3`"""
+ sql """refresh catalog ${catalog_name}"""
+ qt_sql23 """show databases like "ns1.ns2"""" // empty
+ qt_sql24 """show databases like "ns1.ns2.ns3"""" // empty
+
+ // recreate ns1.ns2.ns3, and create table in ns1.ns2
+ sql """create database `ns1.ns2.ns3`;"""
+ qt_sql25 """show databases like "ns1.ns2"""" // 1
+ qt_sql26 """show databases like "ns1.ns2.ns3"""" // 1
+ sql """create table `ns1.ns2`.test_table2(k1 int);"""
+ sql """insert into `ns1.ns2`.test_table2 values(104)"""
+ qt_sql261 """select * from `ns1.ns2`.test_table2"""
+ // drop ns1.ns2.ns3, ns1.ns2 will still exist
+ sql """drop database `ns1.ns2.ns3`"""
+ sql """refresh catalog ${catalog_name}"""
+ qt_sql27 """show databases like "ns1.ns2"""" // 1
+ qt_sql28 """show databases like "ns1.ns2.ns3"""" // empty
+ qt_sql29 """select * from `ns1.ns2`.test_table2"""
+ // drop `ns1.ns2`.test_table2, and then ns1.ns2 will disappeal
+ sql """drop table `ns1.ns2`.test_table2"""
+ sql """refresh catalog ${catalog_name}"""
+ qt_sql30 """show databases like "ns1.ns2"""" // empty
+
+ // test dropping and creating table in nested ns spark created
+ sql """drop table if exists `nested.db1`.spark_table"""
+ sql """create table `nested.db1`.spark_table (k1 int)"""
+ sql """insert into `nested.db1`.spark_table values(105)"""
+ qt_sql31 """select * from `nested.db1`.spark_table"""
+
+
+ // 4.
+ // iceberg.rest.nested-namespace-enabled = false
+ // set global enable_nested_namespace = true
+ sql """set global enable_nested_namespace = true"""
+ sql """alter catalog ${catalog_name} set
properties("iceberg.rest.nested-namespace-enabled" = "false");"""
+ sql """switch ${catalog_name}"""
+ // can not see the nested ns
+ qt_sql32 """show databases like "nested.db1";""" // empty
+ test {
+ sql """use ${catalog_name}.`nested.db1`"""
+ exception """Unknown database 'nested.db1'"""
+ }
+
+ // can create nested ns, but can not drop because nested ns can not be seen
+ test {
+ sql """drop database `nested.db1`"""
+ exception """Can't drop database 'nested.db1'; database doesn't
exist"""
+ }
+ sql """create database if not exists `nsa.nsb`"""
+ sql """create database if not exists `nsa.nsb.nsc`"""
+ // can only see nsa
+ qt_sql33 """show databases like "nsa"""" // 1
+ qt_sql34 """show databases like "nsa.nsb"""" // empty
+ // can create and drop table in nsa
+ sql """drop table if exists nsa.nsa_tbl1"""
+ sql """create table nsa.nsa_tbl1 (k1 int);"""
+ sql """insert into nsa.nsa_tbl1 values(106)"""
+ qt_sql35 """select * from nsa.nsa_tbl1"""
+ sql """drop table nsa.nsa_tbl1"""
+ test {
+ sql """select * from nsa.nsa_tbl1"""
+ exception """Table [nsa_tbl1] does not exist in database [nsa]"""
+ }
+
+ // 5. test internal
+ sql """switch internal"""
+ sql """set global enable_nested_namespace = true"""
+ // create nested ns
+ sql """drop database if exists `idb1.idb2.idb3`"""
+ sql """drop database if exists `idb1.idb2`"""
+ sql """drop database if exists `idb1`"""
+ sql """create database `idb1`"""
+ sql """create database `idb1.idb2`"""
+ sql """create database `idb1.idb2.idb3`"""
+ qt_sql1001 """show databases like "idb1""""
+ qt_sql1002 """show databases like "idb1.idb2""""
+ qt_sql1003 """show databases like "idb1.idb2.idb3""""
+
+ // create table
+ sql """create table idb1.itbl1 (k1 int) properties("replication_num" =
"1")"""
+ sql """create table `idb1.idb2`.itbl2 (k1 int)
properties("replication_num" = "1")"""
+ sql """use internal.`idb1.idb2.idb3`"""
+ sql """create table itbl3 (k1 int) properties("replication_num" = "1")"""
+
+ // insert
+ sql """insert into idb1.itbl1 values(201)"""
+ sql """insert into `idb1.idb2`.itbl2 values(202)"""
+ sql """use internal.`idb1.idb2.idb3`"""
+ sql """insert into itbl3 values(203)"""
+
+ // query
+ qt_sql101 """select * from idb1.itbl1"""
+ qt_sql103 """select `idb1.idb2`.itbl2.k1 from `idb1.idb2`.itbl2"""
+ sql """use internal.`idb1.idb2.idb3`"""
+ order_qt_sql104 """select `idb1.idb2`.itbl2.k1 from `idb1.idb2`.itbl2
+ union all
+ select idb1.itbl1.k1 from idb1.itbl1
+ union all
+ select itbl3.k1 from itbl3
+ """
+ // disable
+ sql """set global enable_nested_namespace = false"""
+ // still can see nested ns
+ qt_sql2001 """show databases like "idb1""""
+ qt_sql2002 """show databases like "idb1.idb2""""
+ qt_sql2003 """show databases like "idb1.idb2.idb3""""
+
+ sql """ unset global variable enable_nested_namespace;"""
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]