This is an automated email from the ASF dual-hosted git repository.
roryqi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new eb8e6d7520 [#11037] feat(core): Hierarchical namespace persistence and
owner batch in relational service (#11036)
eb8e6d7520 is described below
commit eb8e6d75208f0f64c034b48b1b8124c6f9ce697e
Author: roryqi <[email protected]>
AuthorDate: Wed May 13 16:01:38 2026 +0800
[#11037] feat(core): Hierarchical namespace persistence and owner batch in
relational service (#11036)
### What changes were proposed in this pull request?
- Hierarchical schema config/util; schema meta service hierarchical
naming
- Owner batch soft-delete/insert SQL (OwnerRelDeletion) and
OwnerMetaService.batchSetOwners
- Schema/owner mappers and SQL providers; StatisticPO adjustments
- PO converter tests
### Why are the changes needed?
Fix: #11037
### Does this PR introduce _any_ user-facing change?
No.
### How was this patch tested?
Added UTs.
---
.../main/java/org/apache/gravitino/Configs.java | 21 +++
.../storage/relational/mapper/OwnerMetaMapper.java | 10 ++
.../mapper/OwnerMetaSQLProviderFactory.java | 10 ++
.../relational/mapper/SchemaMetaMapper.java | 8 +
.../mapper/SchemaMetaSQLProviderFactory.java | 9 ++
.../provider/base/OwnerMetaBaseSQLProvider.java | 30 ++++
.../provider/base/SchemaMetaBaseSQLProvider.java | 39 +++++
.../postgresql/OwnerMetaPostgreSQLProvider.java | 17 +++
.../postgresql/SchemaMetaPostgreSQLProvider.java | 27 ++++
.../storage/relational/po/OwnerRelForDeletion.java | 30 ++++
.../storage/relational/po/StatisticPO.java | 15 +-
.../relational/service/OwnerMetaService.java | 46 ++++++
.../relational/service/SchemaMetaService.java | 82 +++++++++-
.../relational/service/StatisticMetaService.java | 4 +-
.../gravitino/utils/HierarchicalSchemaUtil.java | 166 +++++++++++++++++++++
.../storage/relational/BackendTestExtension.java | 3 +
.../relational/service/TestOwnerMetaService.java | 149 ++++++++++++++++++
.../relational/service/TestSchemaMetaService.java | 109 ++++++++++++++
.../storage/relational/utils/TestPOConverters.java | 5 +-
.../utils/TestHierarchicalSchemaUtil.java | 144 ++++++++++++++++++
20 files changed, 914 insertions(+), 10 deletions(-)
diff --git a/core/src/main/java/org/apache/gravitino/Configs.java
b/core/src/main/java/org/apache/gravitino/Configs.java
index 3cc9d703e6..2f09aa54c8 100644
--- a/core/src/main/java/org/apache/gravitino/Configs.java
+++ b/core/src/main/java/org/apache/gravitino/Configs.java
@@ -29,6 +29,7 @@ import org.apache.gravitino.config.ConfigBuilder;
import org.apache.gravitino.config.ConfigConstants;
import org.apache.gravitino.config.ConfigEntry;
import org.apache.gravitino.stats.storage.JdbcPartitionStatisticStorageFactory;
+import org.apache.gravitino.utils.HierarchicalSchemaUtil;
public class Configs {
@@ -500,6 +501,26 @@ public class Configs {
.checkValue(value -> value > 0,
ConfigConstants.POSITIVE_NUMBER_ERROR_MSG)
.createWithDefault(5 * 60 * 1000L); // Default is 5 minutes
+ public static final ConfigEntry<String> SCHEMA_SEPARATOR =
+ new ConfigBuilder("gravitino.schema.separator")
+ .doc(
+ "The separator used to represent HierarchicalSchema hierarchy in
schema names at the "
+ + "API boundary (e.g. ':' for 'A:B:C'). Schema names are
stored internally in "
+ + "EntityStore using ASCII-1 (\\u0001) as the physical
separator. The "
+ + "configured separator is only used at external API and
catalog capability "
+ + "validation layer. The internal physical separator and '.'
must not be used "
+ + "as the external separator.")
+ .version(ConfigConstants.VERSION_1_3_0)
+ .stringConf()
+ .checkValue(
+ value ->
+ StringUtils.isNotBlank(value)
+ && !value.contains(".")
+ &&
!value.contains(HierarchicalSchemaUtil.physicalSeparator()),
+ ConfigConstants.NOT_BLANK_ERROR_MSG
+ + " and must not contain '.' or the internal physical
separator (\\u0001)")
+ .createWithDefault(":");
+
public static final ConfigEntry<String>
PARTITION_STATS_STORAGE_FACTORY_CLASS =
new ConfigBuilder("gravitino.stats.partition.storageFactoryClass")
.doc(
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/OwnerMetaMapper.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/OwnerMetaMapper.java
index 3a2d0c459b..b2872ac3b2 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/OwnerMetaMapper.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/OwnerMetaMapper.java
@@ -20,6 +20,7 @@ package org.apache.gravitino.storage.relational.mapper;
import java.util.List;
import org.apache.gravitino.storage.relational.po.GroupPO;
+import org.apache.gravitino.storage.relational.po.OwnerRelForDeletion;
import org.apache.gravitino.storage.relational.po.OwnerRelPO;
import org.apache.gravitino.storage.relational.po.UserOwnerRelPO;
import org.apache.gravitino.storage.relational.po.UserPO;
@@ -66,6 +67,15 @@ public interface OwnerMetaMapper {
@InsertProvider(type = OwnerMetaSQLProviderFactory.class, method =
"insertOwnerRel")
void insertOwnerRel(@Param("ownerRelPO") OwnerRelPO ownerRelPO);
+ @InsertProvider(type = OwnerMetaSQLProviderFactory.class, method =
"batchInsertOwnerRels")
+ void batchInsertOwnerRels(@Param("ownerRelPOs") List<OwnerRelPO>
ownerRelPOs);
+
+ @UpdateProvider(
+ type = OwnerMetaSQLProviderFactory.class,
+ method = "batchSoftDeleteOwnerRelByMetadataObjects")
+ void batchSoftDeleteOwnerRelByMetadataObjects(
+ @Param("deletions") List<OwnerRelForDeletion> deletions);
+
@UpdateProvider(
type = OwnerMetaSQLProviderFactory.class,
method = "softDeleteOwnerRelByMetadataObjectIdAndType")
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/OwnerMetaSQLProviderFactory.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/OwnerMetaSQLProviderFactory.java
index a710583abf..3d805459c8 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/OwnerMetaSQLProviderFactory.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/OwnerMetaSQLProviderFactory.java
@@ -24,6 +24,7 @@ import java.util.Map;
import org.apache.gravitino.storage.relational.JDBCBackend.JDBCBackendType;
import
org.apache.gravitino.storage.relational.mapper.provider.base.OwnerMetaBaseSQLProvider;
import
org.apache.gravitino.storage.relational.mapper.provider.postgresql.OwnerMetaPostgreSQLProvider;
+import org.apache.gravitino.storage.relational.po.OwnerRelForDeletion;
import org.apache.gravitino.storage.relational.po.OwnerRelPO;
import org.apache.gravitino.storage.relational.session.SqlSessionFactoryHelper;
import org.apache.ibatis.annotations.Param;
@@ -69,6 +70,15 @@ public class OwnerMetaSQLProviderFactory {
return getProvider().insertOwnerRel(ownerRelPO);
}
+ public static String batchInsertOwnerRels(@Param("ownerRelPOs")
List<OwnerRelPO> ownerRelPOs) {
+ return getProvider().batchInsertOwnerRels(ownerRelPOs);
+ }
+
+ public static String batchSoftDeleteOwnerRelByMetadataObjects(
+ @Param("deletions") List<OwnerRelForDeletion> deletions) {
+ return getProvider().batchSoftDeleteOwnerRelByMetadataObjects(deletions);
+ }
+
public static String softDeleteOwnerRelByMetadataObjectIdAndType(
@Param("metadataObjectId") Long metadataObjectId,
@Param("metadataObjectType") String metadataObjectType) {
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/SchemaMetaMapper.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/SchemaMetaMapper.java
index 3473395942..d549ad49c6 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/SchemaMetaMapper.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/SchemaMetaMapper.java
@@ -82,6 +82,14 @@ public interface SchemaMetaMapper {
method = "insertSchemaMetaOnDuplicateKeyUpdate")
void insertSchemaMetaOnDuplicateKeyUpdate(@Param("schemaMeta") SchemaPO
schemaPO);
+ @InsertProvider(type = SchemaMetaSQLProviderFactory.class, method =
"batchInsertSchemaMeta")
+ void batchInsertSchemaMeta(@Param("schemaMetas") List<SchemaPO> schemaMetas);
+
+ @InsertProvider(
+ type = SchemaMetaSQLProviderFactory.class,
+ method = "batchInsertSchemaMetaOnDuplicateKeyUpdate")
+ void batchInsertSchemaMetaOnDuplicateKeyUpdate(@Param("schemaMetas")
List<SchemaPO> schemaMetas);
+
@UpdateProvider(type = SchemaMetaSQLProviderFactory.class, method =
"updateSchemaMeta")
Integer updateSchemaMeta(
@Param("newSchemaMeta") SchemaPO newSchemaPO, @Param("oldSchemaMeta")
SchemaPO oldSchemaPO);
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/SchemaMetaSQLProviderFactory.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/SchemaMetaSQLProviderFactory.java
index 0cef49e994..46de84cff9 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/SchemaMetaSQLProviderFactory.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/SchemaMetaSQLProviderFactory.java
@@ -94,6 +94,15 @@ public class SchemaMetaSQLProviderFactory {
return getProvider().insertSchemaMetaOnDuplicateKeyUpdate(schemaPO);
}
+ public static String batchInsertSchemaMeta(@Param("schemaMetas")
List<SchemaPO> schemaMetas) {
+ return getProvider().batchInsertSchemaMeta(schemaMetas);
+ }
+
+ public static String batchInsertSchemaMetaOnDuplicateKeyUpdate(
+ @Param("schemaMetas") List<SchemaPO> schemaMetas) {
+ return
getProvider().batchInsertSchemaMetaOnDuplicateKeyUpdate(schemaMetas);
+ }
+
public static String updateSchemaMeta(
@Param("newSchemaMeta") SchemaPO newSchemaPO, @Param("oldSchemaMeta")
SchemaPO oldSchemaPO) {
return getProvider().updateSchemaMeta(newSchemaPO, oldSchemaPO);
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/OwnerMetaBaseSQLProvider.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/OwnerMetaBaseSQLProvider.java
index 27bcea1b92..5ec2fbdd89 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/OwnerMetaBaseSQLProvider.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/OwnerMetaBaseSQLProvider.java
@@ -31,6 +31,7 @@ import
org.apache.gravitino.storage.relational.mapper.TableMetaMapper;
import org.apache.gravitino.storage.relational.mapper.TopicMetaMapper;
import org.apache.gravitino.storage.relational.mapper.UserMetaMapper;
import org.apache.gravitino.storage.relational.mapper.ViewMetaMapper;
+import org.apache.gravitino.storage.relational.po.OwnerRelForDeletion;
import org.apache.gravitino.storage.relational.po.OwnerRelPO;
import org.apache.ibatis.annotations.Param;
@@ -124,6 +125,35 @@ public class OwnerMetaBaseSQLProvider {
+ ")";
}
+ public String batchInsertOwnerRels(@Param("ownerRelPOs") List<OwnerRelPO>
ownerRelPOs) {
+ return "<script>"
+ + "INSERT INTO "
+ + OWNER_TABLE_NAME
+ + " (metalake_id, metadata_object_id, metadata_object_type, owner_id,
owner_type,"
+ + " audit_info, current_version, last_version, deleted_at, updated_at)
VALUES "
+ + "<foreach collection='ownerRelPOs' item='po' separator=','>"
+ + "(#{po.metalakeId}, #{po.metadataObjectId},
#{po.metadataObjectType},"
+ + " #{po.ownerId}, #{po.ownerType}, #{po.auditInfo},"
+ + " #{po.currentVersion}, #{po.lastVersion}, #{po.deletedAt},
#{po.updatedAt})"
+ + "</foreach>"
+ + "</script>";
+ }
+
+ public String batchSoftDeleteOwnerRelByMetadataObjects(
+ @Param("deletions") List<OwnerRelForDeletion> deletions) {
+ return "<script>"
+ + "UPDATE "
+ + OWNER_TABLE_NAME
+ + " SET deleted_at = (UNIX_TIMESTAMP() * 1000.0)"
+ + " + EXTRACT(MICROSECOND FROM CURRENT_TIMESTAMP(3)) / 1000"
+ + " WHERE deleted_at = 0 AND ("
+ + "<foreach collection='deletions' item='t' separator=' OR '>"
+ + "(metadata_object_id = #{t.metadataObjectId} AND
metadata_object_type = #{t.metadataObjectType})"
+ + "</foreach>"
+ + ")"
+ + "</script>";
+ }
+
public String softDeleteOwnerRelByMetadataObjectIdAndType(
@Param("metadataObjectId") Long metadataObjectId,
@Param("metadataObjectType") String metadataObjectType) {
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/SchemaMetaBaseSQLProvider.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/SchemaMetaBaseSQLProvider.java
index 202dc84593..63f10ae2fc 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/SchemaMetaBaseSQLProvider.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/SchemaMetaBaseSQLProvider.java
@@ -200,6 +200,45 @@ public class SchemaMetaBaseSQLProvider {
+ " deleted_at = #{schemaMeta.deletedAt}";
}
+ public String batchInsertSchemaMeta(@Param("schemaMetas") List<SchemaPO>
schemaMetas) {
+ return "<script>"
+ + "INSERT INTO "
+ + TABLE_NAME
+ + " (schema_id, schema_name, metalake_id, catalog_id, schema_comment,"
+ + " properties, audit_info, current_version, last_version, deleted_at)
VALUES "
+ + "<foreach collection='schemaMetas' item='po' separator=','>"
+ + "(#{po.schemaId}, #{po.schemaName}, #{po.metalakeId},
#{po.catalogId},"
+ + " #{po.schemaComment}, #{po.properties}, #{po.auditInfo},"
+ + " #{po.currentVersion}, #{po.lastVersion}, #{po.deletedAt})"
+ + "</foreach>"
+ + "</script>";
+ }
+
+ public String batchInsertSchemaMetaOnDuplicateKeyUpdate(
+ @Param("schemaMetas") List<SchemaPO> schemaMetas) {
+ return "<script>"
+ + "INSERT INTO "
+ + TABLE_NAME
+ + " (schema_id, schema_name, metalake_id, catalog_id, schema_comment,"
+ + " properties, audit_info, current_version, last_version, deleted_at)
VALUES "
+ + "<foreach collection='schemaMetas' item='po' separator=','>"
+ + "(#{po.schemaId}, #{po.schemaName}, #{po.metalakeId},
#{po.catalogId},"
+ + " #{po.schemaComment}, #{po.properties}, #{po.auditInfo},"
+ + " #{po.currentVersion}, #{po.lastVersion}, #{po.deletedAt})"
+ + "</foreach>"
+ + " ON DUPLICATE KEY UPDATE"
+ + " schema_name = VALUES(schema_name),"
+ + " metalake_id = VALUES(metalake_id),"
+ + " catalog_id = VALUES(catalog_id),"
+ + " schema_comment = VALUES(schema_comment),"
+ + " properties = VALUES(properties),"
+ + " audit_info = VALUES(audit_info),"
+ + " current_version = VALUES(current_version),"
+ + " last_version = VALUES(last_version),"
+ + " deleted_at = VALUES(deleted_at)"
+ + "</script>";
+ }
+
public String updateSchemaMeta(
@Param("newSchemaMeta") SchemaPO newSchemaPO, @Param("oldSchemaMeta")
SchemaPO oldSchemaPO) {
return "UPDATE "
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/OwnerMetaPostgreSQLProvider.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/OwnerMetaPostgreSQLProvider.java
index d0fe369261..c0ba865250 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/OwnerMetaPostgreSQLProvider.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/OwnerMetaPostgreSQLProvider.java
@@ -20,6 +20,7 @@ package
org.apache.gravitino.storage.relational.mapper.provider.postgresql;
import static
org.apache.gravitino.storage.relational.mapper.OwnerMetaMapper.OWNER_TABLE_NAME;
+import java.util.List;
import org.apache.gravitino.storage.relational.mapper.CatalogMetaMapper;
import org.apache.gravitino.storage.relational.mapper.FilesetMetaMapper;
import org.apache.gravitino.storage.relational.mapper.FunctionMetaMapper;
@@ -29,6 +30,7 @@ import
org.apache.gravitino.storage.relational.mapper.TableMetaMapper;
import org.apache.gravitino.storage.relational.mapper.TopicMetaMapper;
import org.apache.gravitino.storage.relational.mapper.ViewMetaMapper;
import
org.apache.gravitino.storage.relational.mapper.provider.base.OwnerMetaBaseSQLProvider;
+import org.apache.gravitino.storage.relational.po.OwnerRelForDeletion;
import org.apache.ibatis.annotations.Param;
public class OwnerMetaPostgreSQLProvider extends OwnerMetaBaseSQLProvider {
@@ -148,6 +150,21 @@ public class OwnerMetaPostgreSQLProvider extends
OwnerMetaBaseSQLProvider {
+ ")";
}
+ @Override
+ public String batchSoftDeleteOwnerRelByMetadataObjects(
+ @Param("deletions") List<OwnerRelForDeletion> deletions) {
+ return "<script>"
+ + "UPDATE "
+ + OWNER_TABLE_NAME
+ + " SET deleted_at = CAST(EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000
AS BIGINT)"
+ + " WHERE deleted_at = 0 AND ("
+ + "<foreach collection='deletions' item='t' separator=' OR '>"
+ + "(metadata_object_id = #{t.metadataObjectId} AND
metadata_object_type = #{t.metadataObjectType})"
+ + "</foreach>"
+ + ")"
+ + "</script>";
+ }
+
@Override
public String deleteOwnerMetasByLegacyTimeline(
@Param("legacyTimeline") Long legacyTimeline, @Param("limit") int limit)
{
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/SchemaMetaPostgreSQLProvider.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/SchemaMetaPostgreSQLProvider.java
index 1e23111c3f..e272c0a806 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/SchemaMetaPostgreSQLProvider.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/SchemaMetaPostgreSQLProvider.java
@@ -20,6 +20,7 @@ package
org.apache.gravitino.storage.relational.mapper.provider.postgresql;
import static
org.apache.gravitino.storage.relational.mapper.SchemaMetaMapper.TABLE_NAME;
+import java.util.List;
import
org.apache.gravitino.storage.relational.mapper.provider.base.SchemaMetaBaseSQLProvider;
import org.apache.gravitino.storage.relational.po.SchemaPO;
import org.apache.ibatis.annotations.Param;
@@ -56,6 +57,32 @@ public class SchemaMetaPostgreSQLProvider extends
SchemaMetaBaseSQLProvider {
+ " deleted_at = #{schemaMeta.deletedAt}";
}
+ @Override
+ public String batchInsertSchemaMetaOnDuplicateKeyUpdate(
+ @Param("schemaMetas") List<SchemaPO> schemaMetas) {
+ return "<script>"
+ + "INSERT INTO "
+ + TABLE_NAME
+ + " (schema_id, schema_name, metalake_id, catalog_id, schema_comment,"
+ + " properties, audit_info, current_version, last_version, deleted_at)
VALUES "
+ + "<foreach collection='schemaMetas' item='po' separator=','>"
+ + "(#{po.schemaId}, #{po.schemaName}, #{po.metalakeId},
#{po.catalogId},"
+ + " #{po.schemaComment}, #{po.properties}, #{po.auditInfo},"
+ + " #{po.currentVersion}, #{po.lastVersion}, #{po.deletedAt})"
+ + "</foreach>"
+ + " ON CONFLICT(schema_id) DO UPDATE SET"
+ + " schema_name = EXCLUDED.schema_name,"
+ + " metalake_id = EXCLUDED.metalake_id,"
+ + " catalog_id = EXCLUDED.catalog_id,"
+ + " schema_comment = EXCLUDED.schema_comment,"
+ + " properties = EXCLUDED.properties,"
+ + " audit_info = EXCLUDED.audit_info,"
+ + " current_version = EXCLUDED.current_version,"
+ + " last_version = EXCLUDED.last_version,"
+ + " deleted_at = EXCLUDED.deleted_at"
+ + "</script>";
+ }
+
@Override
public String updateSchemaMeta(
@Param("newSchemaMeta") SchemaPO newSchemaPO, @Param("oldSchemaMeta")
SchemaPO oldSchemaPO) {
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/po/OwnerRelForDeletion.java
b/core/src/main/java/org/apache/gravitino/storage/relational/po/OwnerRelForDeletion.java
new file mode 100644
index 0000000000..3933b74a1f
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/po/OwnerRelForDeletion.java
@@ -0,0 +1,30 @@
+/*
+ * 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.storage.relational.po;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/** Key for batch soft-deleting owner rows by metadata object. */
+@Getter
+@AllArgsConstructor
+public class OwnerRelForDeletion {
+ private final Long metadataObjectId;
+ private final String metadataObjectType;
+}
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/po/StatisticPO.java
b/core/src/main/java/org/apache/gravitino/storage/relational/po/StatisticPO.java
index fa3de359b2..0b6117d4c0 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/po/StatisticPO.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/po/StatisticPO.java
@@ -20,11 +20,15 @@ package org.apache.gravitino.storage.relational.po;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.base.Preconditions;
+import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
import lombok.Getter;
import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.Namespace;
import org.apache.gravitino.json.JsonUtils;
import org.apache.gravitino.meta.AuditInfo;
import org.apache.gravitino.meta.StatisticEntity;
@@ -55,13 +59,22 @@ public class StatisticPO {
return new Builder();
}
- public static StatisticEntity fromStatisticPO(StatisticPO statisticPO) {
+ public static StatisticEntity fromStatisticPO(
+ StatisticPO statisticPO, NameIdentifier metadataObjectIdentifier) {
+ Preconditions.checkArgument(
+ metadataObjectIdentifier != null, "`metadataObjectIdentifier` is
required");
try {
return StatisticEntity.builder(
StatisticEntity.getStatisticType(
MetadataObject.Type.valueOf(statisticPO.metadataObjectType)))
.withId(statisticPO.getStatisticId())
.withName(statisticPO.getStatisticName())
+ .withNamespace(
+ Namespace.of(
+ Stream.concat(
+
Arrays.stream(metadataObjectIdentifier.namespace().levels()),
+ Stream.of(metadataObjectIdentifier.name()))
+ .toArray(String[]::new)))
.withValue(
JsonUtils.anyFieldMapper()
.readValue(statisticPO.getStatisticValue(),
StatisticValue.class))
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/service/OwnerMetaService.java
b/core/src/main/java/org/apache/gravitino/storage/relational/service/OwnerMetaService.java
index 2a209e654f..012d00abc4 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/service/OwnerMetaService.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/service/OwnerMetaService.java
@@ -39,6 +39,7 @@ import org.apache.gravitino.meta.UserEntity;
import org.apache.gravitino.metrics.Monitored;
import org.apache.gravitino.storage.relational.mapper.OwnerMetaMapper;
import org.apache.gravitino.storage.relational.po.GroupPO;
+import org.apache.gravitino.storage.relational.po.OwnerRelForDeletion;
import org.apache.gravitino.storage.relational.po.OwnerRelPO;
import org.apache.gravitino.storage.relational.po.UserOwnerRelPO;
import org.apache.gravitino.storage.relational.po.UserPO;
@@ -172,4 +173,49 @@ public class OwnerMetaService {
SessionUtils.doWithoutCommit(
OwnerMetaMapper.class, mapper ->
mapper.insertOwnerRel(ownerRelPO)));
}
+
+ @Monitored(
+ metricsSource = GRAVITINO_RELATIONAL_STORE_METRIC_NAME,
+ baseMetricName = "batchSetOwner")
+ public void batchSetOwners(
+ List<NameIdentifier> ownedObjects,
+ Entity.EntityType ownedObjectType,
+ NameIdentifier ownerIdent,
+ Entity.EntityType ownerType) {
+ if (CollectionUtils.isEmpty(ownedObjects)) {
+ return;
+ }
+
+ String metalake = NameIdentifierUtil.getMetalake(ownedObjects.get(0));
+ for (NameIdentifier entity : ownedObjects) {
+ Preconditions.checkArgument(
+ Objects.equals(NameIdentifierUtil.getMetalake(entity), metalake),
+ "All owned objects must be in the same metalake");
+ }
+
+ long metalakeId =
MetalakeMetaService.getInstance().getMetalakeIdByName(metalake);
+ Long ownerId = EntityIdService.getEntityId(ownerIdent, ownerType);
+
+ List<OwnerRelForDeletion> deletions = new ArrayList<>(ownedObjects.size());
+ List<OwnerRelPO> ownerRelPOs = new ArrayList<>(ownedObjects.size());
+ for (NameIdentifier entity : ownedObjects) {
+ Long entityId = EntityIdService.getEntityId(entity, ownedObjectType);
+ deletions.add(
+ new OwnerRelForDeletion(
+ entityId,
+ NameIdentifierUtil.toMetadataObject(entity,
ownedObjectType).type().name()));
+ ownerRelPOs.add(
+ POConverters.initializeOwnerRelPOsWithVersion(
+ metalakeId, ownerType.name(), ownerId, ownedObjectType.name(),
entityId));
+ }
+
+ SessionUtils.doMultipleWithCommit(
+ () ->
+ SessionUtils.doWithoutCommit(
+ OwnerMetaMapper.class,
+ mapper ->
mapper.batchSoftDeleteOwnerRelByMetadataObjects(deletions)),
+ () ->
+ SessionUtils.doWithoutCommit(
+ OwnerMetaMapper.class, mapper ->
mapper.batchInsertOwnerRels(ownerRelPOs)));
+ }
}
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/service/SchemaMetaService.java
b/core/src/main/java/org/apache/gravitino/storage/relational/service/SchemaMetaService.java
index 8127ae4c11..c7edb6d26f 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/service/SchemaMetaService.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/service/SchemaMetaService.java
@@ -22,10 +22,15 @@ import static
org.apache.gravitino.metrics.source.MetricsSource.GRAVITINO_RELATI
import com.google.common.base.Preconditions;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import java.util.Objects;
+import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.gravitino.Entity;
import org.apache.gravitino.Entity.EntityType;
@@ -43,6 +48,7 @@ import org.apache.gravitino.meta.SchemaEntity;
import org.apache.gravitino.meta.TableEntity;
import org.apache.gravitino.meta.TopicEntity;
import org.apache.gravitino.metrics.Monitored;
+import org.apache.gravitino.storage.IdGenerator;
import org.apache.gravitino.storage.relational.helper.SchemaIds;
import org.apache.gravitino.storage.relational.mapper.EntityChangeLogMapper;
import org.apache.gravitino.storage.relational.mapper.FilesetMetaMapper;
@@ -67,6 +73,7 @@ import
org.apache.gravitino.storage.relational.po.cache.OperateType;
import org.apache.gravitino.storage.relational.utils.ExceptionUtils;
import org.apache.gravitino.storage.relational.utils.POConverters;
import org.apache.gravitino.storage.relational.utils.SessionUtils;
+import org.apache.gravitino.utils.HierarchicalSchemaUtil;
import org.apache.gravitino.utils.NameIdentifierUtil;
import org.apache.gravitino.utils.NamespaceUtil;
@@ -162,18 +169,71 @@ public class SchemaMetaService {
public void insertSchema(SchemaEntity schemaEntity, boolean overwrite)
throws IOException {
try {
NameIdentifierUtil.checkSchema(schemaEntity.nameIdentifier());
-
- SchemaPO.Builder builder = SchemaPO.builder();
- fillSchemaPOBuilderParentEntityId(builder, schemaEntity.namespace());
+ // Callers above this service (e.g. JDBCBackend + naming bridge) pass
storage-form schema
+ // names: nested paths use the internal physical separator, not the
external logical one.
+ String physicalSep = HierarchicalSchemaUtil.physicalSeparator();
+ String schemaName = schemaEntity.name();
+ List<SchemaEntity> rowsToInsert = new ArrayList<>();
+ if (schemaName == null || !schemaName.contains(physicalSep)) {
+ rowsToInsert.add(schemaEntity);
+ } else {
+ // Segments of the storage-form name; e.g. [A, B, C] -> ancestor rows
"A", "A"+sep+"B", then
+ // leaf.
+ String[] parts = schemaName.split(Pattern.quote(physicalSep), -1);
+ for (int nSeg = 1; nSeg < parts.length; nSeg++) {
+ String ancestorPhysical = String.join(physicalSep,
Arrays.copyOf(parts, nSeg));
+ SchemaEntity ancestor =
+ SchemaEntity.builder()
+ .withId(nextIdForNestedAncestor())
+ .withName(ancestorPhysical)
+ .withNamespace(schemaEntity.namespace())
+ .withComment(null)
+ .withProperties(Collections.emptyMap())
+ .withAuditInfo(schemaEntity.auditInfo())
+ .build();
+ rowsToInsert.add(ancestor);
+ }
+ rowsToInsert.add(schemaEntity);
+ }
SessionUtils.doWithCommit(
SchemaMetaMapper.class,
mapper -> {
- SchemaPO po =
POConverters.initializeSchemaPOWithVersion(schemaEntity, builder);
+ int n = rowsToInsert.size();
+ List<SchemaPO> missingAncestorPOs = new ArrayList<>();
+ if (n > 1) {
+ SchemaEntity firstAncestor = rowsToInsert.get(0);
+ Namespace ancestorNs = firstAncestor.namespace();
+ List<String> ancestorPhysicalNames =
+ rowsToInsert.subList(0, n - 1).stream()
+ .map(SchemaEntity::name)
+ .collect(Collectors.toList());
+ Set<String> existingAncestorNames =
+ mapper
+ .batchSelectSchemaByIdentifier(
+ ancestorNs.level(0), ancestorNs.level(1),
ancestorPhysicalNames)
+ .stream()
+ .map(SchemaPO::getSchemaName)
+ .collect(Collectors.toSet());
+ for (SchemaEntity row : rowsToInsert.subList(0, n - 1)) {
+ if (existingAncestorNames.contains(row.name())) {
+ continue;
+ }
+ SchemaPO.Builder builder = SchemaPO.builder();
+ fillSchemaPOBuilderParentEntityId(builder, row.namespace());
+
missingAncestorPOs.add(POConverters.initializeSchemaPOWithVersion(row,
builder));
+ }
+ }
+ SchemaEntity leafRow = rowsToInsert.get(n - 1);
+ SchemaPO.Builder leafBuilder = SchemaPO.builder();
+ fillSchemaPOBuilderParentEntityId(leafBuilder,
leafRow.namespace());
+ SchemaPO leafPO =
POConverters.initializeSchemaPOWithVersion(leafRow, leafBuilder);
+ List<SchemaPO> schemaPosToInsert = new
ArrayList<>(missingAncestorPOs);
+ schemaPosToInsert.add(leafPO);
if (overwrite) {
- mapper.insertSchemaMetaOnDuplicateKeyUpdate(po);
+
mapper.batchInsertSchemaMetaOnDuplicateKeyUpdate(schemaPosToInsert);
} else {
- mapper.insertSchemaMeta(po);
+ mapper.batchInsertSchemaMeta(schemaPosToInsert);
}
});
} catch (RuntimeException re) {
@@ -189,7 +249,6 @@ public class SchemaMetaService {
public <E extends Entity & HasIdentifier> SchemaEntity updateSchema(
NameIdentifier identifier, Function<E, E> updater) throws IOException {
SchemaPO oldSchemaPO = getSchemaPOByIdentifier(identifier);
-
SchemaEntity oldSchemaEntity = POConverters.fromSchemaPO(oldSchemaPO,
identifier.namespace());
SchemaEntity newEntity = (SchemaEntity) updater.apply((E) oldSchemaEntity);
Preconditions.checkArgument(
@@ -555,4 +614,13 @@ public class SchemaMetaService {
return POConverters.fromSchemaPOs(schemaPOs, firstIdent.namespace());
});
}
+
+ private static long nextIdForNestedAncestor() {
+ IdGenerator generator = GravitinoEnv.getInstance().idGenerator();
+ if (generator == null) {
+ throw new IllegalStateException(
+ "IdGenerator is not initialized in GravitinoEnv; ensure it is set up
before inserting nested schemas");
+ }
+ return generator.nextId();
+ }
}
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/service/StatisticMetaService.java
b/core/src/main/java/org/apache/gravitino/storage/relational/service/StatisticMetaService.java
index 81880859f3..31fd741e39 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/service/StatisticMetaService.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/service/StatisticMetaService.java
@@ -58,7 +58,9 @@ public class StatisticMetaService {
mapper ->
mapper.listStatisticPOsByEntityId(
namespacedEntityId.namespaceIds()[0],
namespacedEntityId.entityId()));
- return
statisticPOs.stream().map(StatisticPO::fromStatisticPO).collect(Collectors.toList());
+ return statisticPOs.stream()
+ .map(po -> StatisticPO.fromStatisticPO(po, identifier))
+ .collect(Collectors.toList());
}
@Monitored(
diff --git
a/core/src/main/java/org/apache/gravitino/utils/HierarchicalSchemaUtil.java
b/core/src/main/java/org/apache/gravitino/utils/HierarchicalSchemaUtil.java
new file mode 100644
index 0000000000..4d6a3fae91
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/utils/HierarchicalSchemaUtil.java
@@ -0,0 +1,166 @@
+/*
+ * 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.utils;
+
+import com.google.common.base.Preconditions;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Pattern;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.Configs;
+import org.apache.gravitino.GravitinoEnv;
+
+/**
+ * Utility class for hierarchical schema name conversions.
+ *
+ * <p>Gravitino supports HierarchicalSchema semantics where a logical schema
name like {@code A:B:C}
+ * (using a configurable external separator, default {@code :}) is mapped to a
physical schema name
+ * {@code A\u0001B\u0001C} stored in {@code EntityStore} using ASCII-1 ({@code
\u0001}) as the
+ * internal separator.
+ *
+ * <p>Schema names in {@code EntityStore} never contain the external
separator; the external
+ * separator is only used at the API boundary and in catalog capability
validation.
+ */
+public final class HierarchicalSchemaUtil {
+
+ /** The internal separator used in EntityStore for HierarchicalSchema names.
*/
+ private static final String PHYSICAL_SEPARATOR = "\u0001";
+
+ private HierarchicalSchemaUtil() {}
+
+ /**
+ * Returns the configured external schema separator from the server
configuration. All code that
+ * needs the separator should use this method instead of reading the config
directly.
+ *
+ * @return the configured separator string (default {@code ":"})
+ */
+ public static String schemaSeparator() {
+ Config config = GravitinoEnv.getInstance().config();
+ if (config == null) {
+ return Configs.SCHEMA_SEPARATOR.getDefaultValue();
+ }
+ String separator = config.get(Configs.SCHEMA_SEPARATOR);
+ return StringUtils.defaultIfBlank(separator,
Configs.SCHEMA_SEPARATOR.getDefaultValue());
+ }
+
+ /** Returns the internal physical separator used in EntityStore. */
+ public static String physicalSeparator() {
+ return PHYSICAL_SEPARATOR;
+ }
+
+ /**
+ * Converts a logical schema path (using the external separator) to a
physical schema name (using
+ * ASCII-1 ({@code \u0001}) as separator) suitable for storage in
EntityStore.
+ *
+ * <p>Example: {@code "A:B:C"} with separator {@code ":"} → {@code
"A\u0001B\u0001C"}
+ *
+ * @param logicalPath the logical schema path using the external separator
+ * @param separator the external separator configured on the server
+ * @return the physical schema name using ASCII-1 as separator
+ */
+ public static String logicalToPhysical(String logicalPath, String separator)
{
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(logicalPath), "logicalPath must not be blank");
+ Preconditions.checkArgument(StringUtils.isNotBlank(separator), "separator
must not be blank");
+ return logicalPath.replace(separator, PHYSICAL_SEPARATOR);
+ }
+
+ /**
+ * Converts a physical schema name (using ASCII-1 as separator) back to the
logical schema path
+ * using the configured external separator.
+ *
+ * <p>Example: {@code "A\u0001B\u0001C"} with separator {@code ":"} → {@code
"A:B:C"}
+ *
+ * @param physicalName the physical schema name using ASCII-1 as separator
+ * @param separator the external separator configured on the server
+ * @return the logical schema path using the external separator
+ */
+ public static String physicalToLogical(String physicalName, String
separator) {
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(physicalName), "physicalName must not be
blank");
+ Preconditions.checkArgument(StringUtils.isNotBlank(separator), "separator
must not be blank");
+ return physicalName.replace(PHYSICAL_SEPARATOR, separator);
+ }
+
+ /**
+ * Returns whether a schema name is a hierarchical path (contains the
external separator).
+ *
+ * @param name the schema name to test
+ * @param separator the external separator
+ * @return {@code true} if the name contains the external separator
+ */
+ public static boolean isHierarchical(String name, String separator) {
+ return StringUtils.isNotBlank(name) && name.contains(separator);
+ }
+
+ /**
+ * Returns all ancestor schema names of the given schema name, ordered from
outermost to innermost
+ * (but excluding the name itself). Returns an empty list for top-level
(non-HierarchicalSchema)
+ * schemas.
+ *
+ * <p>Example: {@code "A:B:C"} with separator {@code ":"} → {@code ["A",
"A:B"]}
+ *
+ * @param schemaName the schema name to find ancestors for
+ * @param separator the external separator
+ * @return ancestor names from outermost to innermost, or empty list if not
HierarchicalSchema
+ */
+ public static List<String> getAncestorNames(String schemaName, String
separator) {
+ Preconditions.checkArgument(StringUtils.isNotBlank(schemaName),
"schemaName must not be blank");
+ Preconditions.checkArgument(StringUtils.isNotBlank(separator), "separator
must not be blank");
+ String[] parts = schemaName.split(Pattern.quote(separator), -1);
+ List<String> ancestors = new ArrayList<>();
+ for (int i = 1; i < parts.length; i++) {
+ ancestors.add(String.join(separator, Arrays.copyOf(parts, i)));
+ }
+ return ancestors;
+ }
+
+ /**
+ * Returns the schema name and all its ancestor schema names, ordered from
the schema itself to
+ * the outermost ancestor. Returns a single-element list for top-level
(non-HierarchicalSchema)
+ * schemas.
+ *
+ * <p>Example: {@code "A:B:C"} with separator {@code ":"} → {@code ["A:B:C",
"A:B", "A"]}
+ *
+ * <p>This is used for privilege inheritance: a privilege on an ancestor
schema is inherited by
+ * all descendant schemas.
+ *
+ * @param schemaName the schema name to compute scopes for
+ * @param separator the external separator
+ * @return the schema name and all ancestor names, from most specific to
outermost
+ */
+ public static List<String> allScopes(String schemaName, String separator) {
+ Preconditions.checkArgument(StringUtils.isNotBlank(schemaName),
"schemaName must not be blank");
+ Preconditions.checkArgument(StringUtils.isNotBlank(separator), "separator
must not be blank");
+ List<String> result = new ArrayList<>();
+ result.add(schemaName);
+ String current = schemaName;
+ while (current.contains(separator)) {
+ int lastIdx = current.lastIndexOf(separator);
+ current = current.substring(0, lastIdx);
+ if (current.isEmpty()) {
+ break;
+ }
+ result.add(current);
+ }
+ return result;
+ }
+}
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/BackendTestExtension.java
b/core/src/test/java/org/apache/gravitino/storage/relational/BackendTestExtension.java
index 03c7c43b5a..a9377c8b59 100644
---
a/core/src/test/java/org/apache/gravitino/storage/relational/BackendTestExtension.java
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/BackendTestExtension.java
@@ -46,6 +46,7 @@ import org.apache.gravitino.Config;
import org.apache.gravitino.Configs;
import org.apache.gravitino.GravitinoEnv;
import org.apache.gravitino.integration.test.util.BaseIT;
+import org.apache.gravitino.storage.RandomIdGenerator;
import org.apache.gravitino.storage.relational.service.EntityIdService;
import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
@@ -178,6 +179,8 @@ public class BackendTestExtension
Mockito.when(config.get(CACHE_ENABLED)).thenReturn(true);
FieldUtils.writeField(GravitinoEnv.getInstance(), "config", config,
true);
+ FieldUtils.writeField(
+ GravitinoEnv.getInstance(), "idGenerator",
RandomIdGenerator.INSTANCE, true);
RelationalBackend backend = new JDBCBackend();
if ("mysql".equals(type)) {
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestOwnerMetaService.java
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestOwnerMetaService.java
index 46de145356..fc57a734ee 100644
---
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestOwnerMetaService.java
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestOwnerMetaService.java
@@ -28,6 +28,7 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.gravitino.Entity;
+import org.apache.gravitino.HasIdentifier;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.RelationalEntity;
@@ -735,6 +736,154 @@ class TestOwnerMetaService extends TestJDBCBackend {
Assertions.assertTrue(relations.isEmpty());
}
+ @TestTemplate
+ void testBatchSetOwnersWithEmptyInput() {
+ OwnerMetaService.getInstance()
+ .batchSetOwners(
+ Collections.emptyList(),
+ Entity.EntityType.TABLE,
+ NameIdentifier.of(METALAKE_NAME, "u"),
+ Entity.EntityType.USER);
+ }
+
+ @TestTemplate
+ void testBatchSetOwnersAssignsSameUserToMultipleTables() throws IOException {
+ createAndInsertMakeLake(METALAKE_NAME);
+ createAndInsertCatalog(METALAKE_NAME, CATALOG_NAME);
+ createAndInsertSchema(METALAKE_NAME, CATALOG_NAME, SCHEMA_NAME);
+ TableEntity table1 =
+ createTableEntity(
+ RandomIdGenerator.INSTANCE.nextId(),
+ Namespace.of(METALAKE_NAME, CATALOG_NAME, SCHEMA_NAME),
+ TABLE_NAME,
+ AUDIT_INFO);
+ TableEntity table2 =
+ createTableEntity(
+ RandomIdGenerator.INSTANCE.nextId(),
+ Namespace.of(METALAKE_NAME, CATALOG_NAME, SCHEMA_NAME),
+ TABLE_NAME + "_2",
+ AUDIT_INFO);
+ backend.insert(table1, false);
+ backend.insert(table2, false);
+
+ UserEntity user =
+ createUserEntity(
+ RandomIdGenerator.INSTANCE.nextId(),
+ AuthorizationUtils.ofUserNamespace(METALAKE_NAME),
+ "batch_owner",
+ AUDIT_INFO);
+ backend.insert(user, false);
+
+ OwnerMetaService.getInstance()
+ .batchSetOwners(
+ List.of(table1.nameIdentifier(), table2.nameIdentifier()),
+ Entity.EntityType.TABLE,
+ user.nameIdentifier(),
+ user.type());
+
+ Assertions.assertEquals(
+ user.nameIdentifier(),
+ ((HasIdentifier)
+ OwnerMetaService.getInstance()
+ .getOwner(table1.nameIdentifier(), table1.type())
+ .get())
+ .nameIdentifier());
+ Assertions.assertEquals(
+ user.nameIdentifier(),
+ ((HasIdentifier)
+ OwnerMetaService.getInstance()
+ .getOwner(table2.nameIdentifier(), table2.type())
+ .get())
+ .nameIdentifier());
+ }
+
+ @TestTemplate
+ void testBatchSetOwnersReplacesPriorOwner() throws IOException {
+ createAndInsertMakeLake(METALAKE_NAME);
+ createAndInsertCatalog(METALAKE_NAME, CATALOG_NAME);
+ createAndInsertSchema(METALAKE_NAME, CATALOG_NAME, SCHEMA_NAME);
+ TableEntity table =
+ createTableEntity(
+ RandomIdGenerator.INSTANCE.nextId(),
+ Namespace.of(METALAKE_NAME, CATALOG_NAME, SCHEMA_NAME),
+ TABLE_NAME,
+ AUDIT_INFO);
+ backend.insert(table, false);
+
+ UserEntity user1 =
+ createUserEntity(
+ RandomIdGenerator.INSTANCE.nextId(),
+ AuthorizationUtils.ofUserNamespace(METALAKE_NAME),
+ "u1",
+ AUDIT_INFO);
+ UserEntity user2 =
+ createUserEntity(
+ RandomIdGenerator.INSTANCE.nextId(),
+ AuthorizationUtils.ofUserNamespace(METALAKE_NAME),
+ "u2",
+ AUDIT_INFO);
+ backend.insert(user1, false);
+ backend.insert(user2, false);
+
+ OwnerMetaService.getInstance()
+ .setOwner(table.nameIdentifier(), table.type(),
user1.nameIdentifier(), user1.type());
+ OwnerMetaService.getInstance()
+ .batchSetOwners(
+ List.of(table.nameIdentifier()),
+ Entity.EntityType.TABLE,
+ user2.nameIdentifier(),
+ user2.type());
+
+ Assertions.assertEquals(
+ user2.nameIdentifier(),
+ ((HasIdentifier)
+
OwnerMetaService.getInstance().getOwner(table.nameIdentifier(),
table.type()).get())
+ .nameIdentifier());
+ }
+
+ @TestTemplate
+ void testBatchSetOwnersRequiresSingleMetalake() throws IOException {
+ String ml2 = METALAKE_NAME + "_other";
+ createAndInsertMakeLake(METALAKE_NAME);
+ createAndInsertMakeLake(ml2);
+ createAndInsertCatalog(METALAKE_NAME, CATALOG_NAME);
+ createAndInsertCatalog(ml2, CATALOG_NAME);
+ createAndInsertSchema(METALAKE_NAME, CATALOG_NAME, SCHEMA_NAME);
+ createAndInsertSchema(ml2, CATALOG_NAME, SCHEMA_NAME);
+ TableEntity table1 =
+ createTableEntity(
+ RandomIdGenerator.INSTANCE.nextId(),
+ Namespace.of(METALAKE_NAME, CATALOG_NAME, SCHEMA_NAME),
+ TABLE_NAME,
+ AUDIT_INFO);
+ TableEntity table2 =
+ createTableEntity(
+ RandomIdGenerator.INSTANCE.nextId(),
+ Namespace.of(ml2, CATALOG_NAME, SCHEMA_NAME),
+ TABLE_NAME,
+ AUDIT_INFO);
+ backend.insert(table1, false);
+ backend.insert(table2, false);
+
+ UserEntity user =
+ createUserEntity(
+ RandomIdGenerator.INSTANCE.nextId(),
+ AuthorizationUtils.ofUserNamespace(METALAKE_NAME),
+ "u",
+ AUDIT_INFO);
+ backend.insert(user, false);
+
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ OwnerMetaService.getInstance()
+ .batchSetOwners(
+ List.of(table1.nameIdentifier(), table2.nameIdentifier()),
+ Entity.EntityType.TABLE,
+ user.nameIdentifier(),
+ user.type()));
+ }
+
@TestTemplate
void testBatchGetOwnerNoneHaveOwners() throws IOException {
createAndInsertMakeLake(METALAKE_NAME);
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestSchemaMetaService.java
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestSchemaMetaService.java
index ad806f28f4..2c19502caa 100644
---
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestSchemaMetaService.java
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestSchemaMetaService.java
@@ -24,14 +24,21 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
+import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
import org.apache.gravitino.Entity;
import org.apache.gravitino.EntityAlreadyExistsException;
+import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.exceptions.NonEmptyEntityException;
import org.apache.gravitino.meta.SchemaEntity;
import org.apache.gravitino.meta.TopicEntity;
import org.apache.gravitino.storage.RandomIdGenerator;
import org.apache.gravitino.storage.relational.TestJDBCBackend;
+import org.apache.gravitino.utils.HierarchicalSchemaUtil;
import org.apache.gravitino.utils.NameIdentifierUtil;
import org.apache.gravitino.utils.NamespaceUtil;
import org.junit.jupiter.api.Assertions;
@@ -205,4 +212,106 @@ public class TestSchemaMetaService extends
TestJDBCBackend {
topicMetaService.deleteTopic(topic.nameIdentifier());
schemaMetaService.deleteSchema(schema.nameIdentifier(), false);
}
+
+ @TestTemplate
+ public void testInsertHierarchicalSchemaCreatesAncestorsAndLeaf() throws
IOException {
+ createAndInsertMakeLake(metalakeName);
+ createAndInsertCatalog(metalakeName, catalogName);
+
+ SchemaMetaService schemaMetaService = SchemaMetaService.getInstance();
+ String logicalLeaf = "ns_a:ns_b:leaf";
+ String sep = HierarchicalSchemaUtil.schemaSeparator();
+ String physicalLeaf =
HierarchicalSchemaUtil.logicalToPhysical(logicalLeaf, sep);
+ SchemaEntity hierarchical =
+ SchemaEntity.builder()
+ .withId(RandomIdGenerator.INSTANCE.nextId())
+ .withName(physicalLeaf)
+ .withNamespace(NamespaceUtil.ofSchema(metalakeName, catalogName))
+ .withComment("nested")
+ .withProperties(Collections.emptyMap())
+ .withAuditInfo(AUDIT_INFO)
+ .build();
+ schemaMetaService.insertSchema(hierarchical, false);
+
+ List<SchemaEntity> schemas =
+
schemaMetaService.listSchemasByNamespace(NamespaceUtil.ofSchema(metalakeName,
catalogName));
+ Set<String> logicalNames =
+ schemas.stream()
+ .map(SchemaEntity::name)
+ .map(
+ n -> {
+ if (n != null &&
n.contains(HierarchicalSchemaUtil.physicalSeparator())) {
+ return HierarchicalSchemaUtil.physicalToLogical(n, sep);
+ }
+ return n;
+ })
+ .collect(Collectors.toSet());
+
+ Assertions.assertTrue(logicalNames.contains("ns_a"));
+ Assertions.assertTrue(logicalNames.contains("ns_a:ns_b"));
+ Assertions.assertTrue(logicalNames.contains(logicalLeaf));
+
+ SchemaEntity loaded =
+ schemaMetaService.getSchemaByIdentifier(
+ NameIdentifier.of(metalakeName, catalogName, physicalLeaf));
+ Assertions.assertEquals(physicalLeaf, loaded.name());
+ Assertions.assertEquals("nested", loaded.comment());
+ }
+
+ @TestTemplate
+ public void testInsertHierarchicalSecondLeafReusesAncestorsWithoutUpsert()
throws IOException {
+ createAndInsertMakeLake(metalakeName);
+ createAndInsertCatalog(metalakeName, catalogName);
+
+ SchemaMetaService schemaMetaService = SchemaMetaService.getInstance();
+ String sep = HierarchicalSchemaUtil.schemaSeparator();
+ String physSep = HierarchicalSchemaUtil.physicalSeparator();
+ String physicalLeaf1 =
HierarchicalSchemaUtil.logicalToPhysical("ns_a:ns_b:leaf1", sep);
+ String physicalLeaf2 =
HierarchicalSchemaUtil.logicalToPhysical("ns_a:ns_b:leaf2", sep);
+ String[] parts = physicalLeaf1.split(Pattern.quote(physSep), -1);
+ String ancestorA = parts[0];
+ String ancestorAB = String.join(physSep, Arrays.copyOfRange(parts, 0, 2));
+
+ SchemaEntity first =
+ SchemaEntity.builder()
+ .withId(RandomIdGenerator.INSTANCE.nextId())
+ .withName(physicalLeaf1)
+ .withNamespace(NamespaceUtil.ofSchema(metalakeName, catalogName))
+ .withComment("first")
+ .withProperties(Collections.emptyMap())
+ .withAuditInfo(AUDIT_INFO)
+ .build();
+ schemaMetaService.insertSchema(first, false);
+
+ long idA =
+ schemaMetaService
+ .getSchemaByIdentifier(NameIdentifier.of(metalakeName,
catalogName, ancestorA))
+ .id();
+ long idAB =
+ schemaMetaService
+ .getSchemaByIdentifier(NameIdentifier.of(metalakeName,
catalogName, ancestorAB))
+ .id();
+
+ SchemaEntity second =
+ SchemaEntity.builder()
+ .withId(RandomIdGenerator.INSTANCE.nextId())
+ .withName(physicalLeaf2)
+ .withNamespace(NamespaceUtil.ofSchema(metalakeName, catalogName))
+ .withComment("second")
+ .withProperties(Collections.emptyMap())
+ .withAuditInfo(AUDIT_INFO)
+ .build();
+ schemaMetaService.insertSchema(second, false);
+
+ Assertions.assertEquals(
+ idA,
+ schemaMetaService
+ .getSchemaByIdentifier(NameIdentifier.of(metalakeName,
catalogName, ancestorA))
+ .id());
+ Assertions.assertEquals(
+ idAB,
+ schemaMetaService
+ .getSchemaByIdentifier(NameIdentifier.of(metalakeName,
catalogName, ancestorAB))
+ .id());
+ }
}
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/utils/TestPOConverters.java
b/core/src/test/java/org/apache/gravitino/storage/relational/utils/TestPOConverters.java
index 93e0de7fd1..bff96b2bd6 100644
---
a/core/src/test/java/org/apache/gravitino/storage/relational/utils/TestPOConverters.java
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/utils/TestPOConverters.java
@@ -1317,10 +1317,13 @@ public class TestPOConverters {
.withCreateTime(FIX_INSTANT)
.build()))
.build();
- StatisticEntity entity = StatisticPO.fromStatisticPO(statisticPO);
+ NameIdentifier parentTableIdent =
+ NameIdentifier.of("test_metalake", "test_catalog", "test_schema",
"test_table");
+ StatisticEntity entity = StatisticPO.fromStatisticPO(statisticPO,
parentTableIdent);
Assertions.assertEquals(1L, entity.id());
Assertions.assertEquals("test", entity.name());
Assertions.assertEquals("test", entity.value().value());
+ Assertions.assertEquals(Namespace.fromString(parentTableIdent.toString()),
entity.namespace());
}
private static BaseMetalake createMetalake(Long id, String name, String
comment) {
diff --git
a/core/src/test/java/org/apache/gravitino/utils/TestHierarchicalSchemaUtil.java
b/core/src/test/java/org/apache/gravitino/utils/TestHierarchicalSchemaUtil.java
new file mode 100644
index 0000000000..e1b65bb66f
--- /dev/null
+++
b/core/src/test/java/org/apache/gravitino/utils/TestHierarchicalSchemaUtil.java
@@ -0,0 +1,144 @@
+/*
+ * 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.utils;
+
+import java.util.List;
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.Configs;
+import org.apache.gravitino.GravitinoEnv;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+public class TestHierarchicalSchemaUtil {
+
+ private static final String PHYS = "\u0001";
+
+ @Test
+ public void testPhysicalSeparator() {
+ Assertions.assertEquals(PHYS, HierarchicalSchemaUtil.physicalSeparator());
+ }
+
+ @Test
+ public void testLogicalToPhysicalAndBack() {
+ String logical = "A:B:C";
+ String sep = ":";
+ String physical = HierarchicalSchemaUtil.logicalToPhysical(logical, sep);
+ Assertions.assertEquals("A" + PHYS + "B" + PHYS + "C", physical);
+ Assertions.assertEquals(logical,
HierarchicalSchemaUtil.physicalToLogical(physical, sep));
+ }
+
+ @Test
+ public void testLogicalToPhysicalMultiCharSeparator() {
+ String logical = "ns|sub|leaf";
+ String physical = HierarchicalSchemaUtil.logicalToPhysical(logical, "|");
+ Assertions.assertEquals("ns" + PHYS + "sub" + PHYS + "leaf", physical);
+ Assertions.assertEquals(logical,
HierarchicalSchemaUtil.physicalToLogical(physical, "|"));
+ }
+
+ @Test
+ public void testLogicalToPhysicalSingleSegment() {
+ Assertions.assertEquals("sales",
HierarchicalSchemaUtil.logicalToPhysical("sales", ":"));
+ }
+
+ @Test
+ public void testLogicalToPhysicalBlankArguments() {
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () ->
HierarchicalSchemaUtil.logicalToPhysical("", ":"));
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () ->
HierarchicalSchemaUtil.logicalToPhysical("A", ""));
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () ->
HierarchicalSchemaUtil.logicalToPhysical("A", " "));
+ }
+
+ @Test
+ public void testPhysicalToLogicalBlankArguments() {
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () ->
HierarchicalSchemaUtil.physicalToLogical("", ":"));
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> HierarchicalSchemaUtil.physicalToLogical("A" + PHYS + "B", ""));
+ }
+
+ @Test
+ public void testIsHierarchical() {
+ Assertions.assertTrue(HierarchicalSchemaUtil.isHierarchical("A:B", ":"));
+ Assertions.assertFalse(HierarchicalSchemaUtil.isHierarchical("AB", ":"));
+ Assertions.assertFalse(HierarchicalSchemaUtil.isHierarchical("A", ":"));
+ Assertions.assertFalse(HierarchicalSchemaUtil.isHierarchical("", ":"));
+ Assertions.assertFalse(HierarchicalSchemaUtil.isHierarchical(" ", ":"));
+ Assertions.assertFalse(HierarchicalSchemaUtil.isHierarchical(null, ":"));
+ }
+
+ @Test
+ public void testGetAncestorNames() {
+ Assertions.assertEquals(
+ List.of("A", "A:B"), HierarchicalSchemaUtil.getAncestorNames("A:B:C",
":"));
+ Assertions.assertEquals(List.of(),
HierarchicalSchemaUtil.getAncestorNames("root", ":"));
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () ->
HierarchicalSchemaUtil.getAncestorNames("", ":"));
+ }
+
+ @Test
+ public void testAllScopes() {
+ Assertions.assertEquals(
+ List.of("A:B:C", "A:B", "A"),
HierarchicalSchemaUtil.allScopes("A:B:C", ":"));
+ Assertions.assertEquals(List.of("root"),
HierarchicalSchemaUtil.allScopes("root", ":"));
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () ->
HierarchicalSchemaUtil.allScopes("", ":"));
+ }
+
+ @Test
+ public void testSchemaSeparatorWhenConfigNull() throws
IllegalAccessException {
+ Config saved = GravitinoEnv.getInstance().config();
+ try {
+ FieldUtils.writeField(GravitinoEnv.getInstance(), "config", null, true);
+ Assertions.assertEquals(":", HierarchicalSchemaUtil.schemaSeparator());
+ } finally {
+ FieldUtils.writeField(GravitinoEnv.getInstance(), "config", saved, true);
+ }
+ }
+
+ @Test
+ public void testSchemaSeparatorFromConfig() throws IllegalAccessException {
+ Config saved = GravitinoEnv.getInstance().config();
+ Config mockConfig = Mockito.mock(Config.class);
+ Mockito.when(mockConfig.get(Configs.SCHEMA_SEPARATOR)).thenReturn("|");
+ try {
+ FieldUtils.writeField(GravitinoEnv.getInstance(), "config", mockConfig,
true);
+ Assertions.assertEquals("|", HierarchicalSchemaUtil.schemaSeparator());
+ } finally {
+ FieldUtils.writeField(GravitinoEnv.getInstance(), "config", saved, true);
+ }
+ }
+
+ @Test
+ public void testSchemaSeparatorBlankFallsBackToDefault() throws
IllegalAccessException {
+ Config saved = GravitinoEnv.getInstance().config();
+ Config mockConfig = Mockito.mock(Config.class);
+ Mockito.when(mockConfig.get(Configs.SCHEMA_SEPARATOR)).thenReturn(" ");
+ try {
+ FieldUtils.writeField(GravitinoEnv.getInstance(), "config", mockConfig,
true);
+ Assertions.assertEquals(":", HierarchicalSchemaUtil.schemaSeparator());
+ } finally {
+ FieldUtils.writeField(GravitinoEnv.getInstance(), "config", saved, true);
+ }
+ }
+}