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 2bcbd2014f [#12578] feat(core): add policy-to-tag relation storage
(#12579)
2bcbd2014f is described below
commit 2bcbd2014f2aca5f473e46b246eadf8b418d25b0
Author: roryqi <[email protected]>
AuthorDate: Fri Aug 28 00:23:13 2026 +0800
[#12578] feat(core): add policy-to-tag relation storage (#12579)
### What changes were proposed in this pull request?
This PR extracts the relational storage part of policy-on-tag from
#12536.
The changes include:
1. Add the `policy_tag_relation_meta` table and upgrade scripts for H2,
MySQL, and PostgreSQL.
2. Persist the optional selector JSON on each policy-to-tag relation.
3. Support atomic and idempotent relation updates.
4. Support bidirectional batch reads without resolving every anchor ID
separately.
5. Clean up relations when a policy, tag, metalake, or legacy record is
deleted.
REST APIs, clients, authorization, events, and policy resolution are
intentionally excluded from this PR.
### Why are the changes needed?
Policy-on-tag needs a persistent `Policy -> Tag` relation before
higher-level APIs and policy resolution can be implemented.
Splitting the storage layer from #12536 keeps the schema, transaction
behavior, and lifecycle cleanup independently reviewable.
Fix: #12578
Part of #12179 and #12176.
### Does this PR introduce _any_ user-facing change?
No. This PR only adds internal relational storage and schema support. It
does not add or change REST or client APIs.
### How was this patch tested?
- `./gradlew :core:spotlessApply`
- `./gradlew :core:test --tests
org.apache.gravitino.storage.relational.service.TestPolicyTagRelService
-PskipDockerTests=false`
- Covers H2, MySQL, and PostgreSQL.
- Covers atomic rollback, idempotent updates, bidirectional batch reads,
metalake isolation, and cascade cleanup.
- Related relational entity store, metalake, policy, and tag service
tests.
- `git diff --check`
---
.../org/apache/gravitino/RelationalEntity.java | 32 ++
.../gravitino/SupportsRelationOperations.java | 6 +
.../gravitino/storage/relational/JDBCBackend.java | 33 ++
.../storage/relational/RelationalEntityStore.java | 2 +
.../relational/mapper/PolicyTagRelMapper.java | 104 ++++
.../mapper/PolicyTagRelSQLProviderFactory.java | 94 ++++
.../storage/relational/mapper/TagMetaMapper.java | 9 +
.../mapper/TagMetaSQLProviderFactory.java | 5 +
.../provider/DefaultMapperPackageProvider.java | 2 +
.../provider/base/PolicyTagRelBaseSQLProvider.java | 150 +++++
.../provider/base/TagMetaBaseSQLProvider.java | 5 +
.../postgresql/PolicyTagRelPostgreSQLProvider.java | 55 ++
.../storage/relational/po/PolicyTagRelPO.java | 154 ++++++
.../relational/service/MetalakeMetaService.java | 7 +
.../relational/service/PolicyTagRelService.java | 408 ++++++++++++++
.../storage/relational/service/TagMetaService.java | 11 +-
.../apache/gravitino/storage/TestSQLScripts.java | 59 +-
.../service/TestPolicyTagRelService.java | 610 +++++++++++++++++++++
scripts/h2/schema-2.0.0-h2.sql | 14 +
scripts/h2/upgrade-1.3.0-to-2.0.0-h2.sql | 14 +
scripts/mysql/schema-2.0.0-mysql.sql | 14 +
scripts/mysql/upgrade-1.3.0-to-2.0.0-mysql.sql | 14 +
scripts/postgresql/schema-2.0.0-postgresql.sql | 24 +
.../upgrade-1.3.0-to-2.0.0-postgresql.sql | 24 +
24 files changed, 1834 insertions(+), 16 deletions(-)
diff --git a/core/src/main/java/org/apache/gravitino/RelationalEntity.java
b/core/src/main/java/org/apache/gravitino/RelationalEntity.java
index 4c9cd20bcc..54cf1d41f1 100644
--- a/core/src/main/java/org/apache/gravitino/RelationalEntity.java
+++ b/core/src/main/java/org/apache/gravitino/RelationalEntity.java
@@ -17,6 +17,9 @@
package org.apache.gravitino;
+import java.util.Optional;
+import javax.annotation.Nullable;
+
/**
* Represents a directed relation between two entities. The source is
identified by a {@link
* NameIdentifier} and an {@link Entity.EntityType}; the target is the actual
resolved entity,
@@ -29,6 +32,7 @@ public class RelationalEntity<T extends Entity &
HasIdentifier> {
private final NameIdentifier source;
private final Entity.EntityType sourceType;
private final T targetEntity;
+ @Nullable private final String relationValue;
/**
* Constructs a RelationalEntity.
@@ -43,10 +47,29 @@ public class RelationalEntity<T extends Entity &
HasIdentifier> {
NameIdentifier source,
Entity.EntityType sourceType,
T targetEntity) {
+ this(type, source, sourceType, targetEntity, null);
+ }
+
+ /**
+ * Constructs a RelationalEntity with an optional value stored on the
relation edge.
+ *
+ * @param type the relation type
+ * @param source the source identifier
+ * @param sourceType the entity type of the source
+ * @param targetEntity the resolved target entity
+ * @param relationValue the optional value carried by the relation edge
+ */
+ public RelationalEntity(
+ SupportsRelationOperations.Type type,
+ NameIdentifier source,
+ Entity.EntityType sourceType,
+ T targetEntity,
+ @Nullable String relationValue) {
this.type = type;
this.source = source;
this.sourceType = sourceType;
this.targetEntity = targetEntity;
+ this.relationValue = relationValue;
}
/**
@@ -84,4 +107,13 @@ public class RelationalEntity<T extends Entity &
HasIdentifier> {
public T targetEntity() {
return targetEntity;
}
+
+ /**
+ * Gets the optional value stored on this relation edge.
+ *
+ * @return the relation value, or empty when the edge has no value
+ */
+ public Optional<String> relationValue() {
+ return Optional.ofNullable(relationValue);
+ }
}
diff --git
a/core/src/main/java/org/apache/gravitino/SupportsRelationOperations.java
b/core/src/main/java/org/apache/gravitino/SupportsRelationOperations.java
index ca1ba1c230..6158c6c3bd 100644
--- a/core/src/main/java/org/apache/gravitino/SupportsRelationOperations.java
+++ b/core/src/main/java/org/apache/gravitino/SupportsRelationOperations.java
@@ -43,6 +43,8 @@ public interface SupportsRelationOperations {
POLICY_METADATA_OBJECT_REL,
/** Metadata object to tag relationship */
TAG_METADATA_OBJECT_REL,
+ /** Policy to tag relationship */
+ POLICY_TAG_REL,
}
/**
@@ -291,6 +293,10 @@ public interface SupportsRelationOperations {
return Entity.EntityType.POLICY;
case TAG_METADATA_OBJECT_REL:
return Entity.EntityType.TAG;
+ case POLICY_TAG_REL:
+ return srcEntityType == Entity.EntityType.TAG
+ ? Entity.EntityType.POLICY
+ : Entity.EntityType.TAG;
default:
return srcEntityType;
}
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java
b/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java
index 6661d0014d..a0f3701d2e 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java
@@ -84,6 +84,7 @@ import
org.apache.gravitino.storage.relational.service.ModelVersionMetaService;
import
org.apache.gravitino.storage.relational.service.OrphanedMetadataObjectRelationService;
import org.apache.gravitino.storage.relational.service.OwnerMetaService;
import org.apache.gravitino.storage.relational.service.PolicyMetaService;
+import org.apache.gravitino.storage.relational.service.PolicyTagRelService;
import org.apache.gravitino.storage.relational.service.RoleMetaService;
import org.apache.gravitino.storage.relational.service.SchemaMetaService;
import org.apache.gravitino.storage.relational.service.StatisticMetaService;
@@ -737,6 +738,13 @@ public class JDBCBackend implements RelationalBackend,
SupportsOrphanedRelationC
return (List<E>)
TagMetaService.getInstance().listTagsForMetadataObject(nameIdentifier,
identType);
}
+ case POLICY_TAG_REL:
+ return (List<E>)
+ PolicyTagRelService.getInstance()
+ .listRelations(List.of(nameIdentifier), identType)
+ .stream()
+ .map(RelationalEntity::targetEntity)
+ .collect(Collectors.toList());
default:
throw new IllegalArgumentException(
String.format("Doesn't support the relation type %s", relType));
@@ -750,6 +758,8 @@ public class JDBCBackend implements RelationalBackend,
SupportsOrphanedRelationC
switch (relType) {
case OWNER_REL:
return OwnerMetaService.getInstance().batchGetOwner(nameIdentifiers,
identType);
+ case POLICY_TAG_REL:
+ return
PolicyTagRelService.getInstance().listRelations(nameIdentifiers, identType);
default:
throw new IllegalArgumentException(
String.format("Doesn't support the relation type %s", relType));
@@ -862,6 +872,14 @@ public class JDBCBackend implements RelationalBackend,
SupportsOrphanedRelationC
update.sourceEntityType(),
toTagValues(update.targetsToAdd()),
toTagValues(update.targetsToRemove()));
+ case POLICY_TAG_REL:
+ Preconditions.checkArgument(
+ update.sourceEntityType() == Entity.EntityType.TAG,
+ "Policy-to-tag relation updates must use a tag as the source
entity");
+ return (List<E>)
+ PolicyTagRelService.getInstance()
+ .updateRelations(
+ update.sourceIdentifier(), update.targetsToAdd(),
update.targetsToRemove());
default:
Preconditions.checkArgument(
!update.hasRelationValues(),
@@ -921,6 +939,21 @@ public class JDBCBackend implements RelationalBackend,
SupportsOrphanedRelationC
return (E)
TagMetaService.getInstance()
.getTagForMetadataObject(srcIdentifier, srcType,
destEntityIdent);
+ case POLICY_TAG_REL:
+ return (E)
+ PolicyTagRelService.getInstance()
+ .listRelations(List.of(srcIdentifier), srcType)
+ .stream()
+ .filter(
+ relation ->
relation.targetEntity().nameIdentifier().equals(destEntityIdent))
+ .map(RelationalEntity::targetEntity)
+ .findFirst()
+ .orElseThrow(
+ () ->
+ new NoSuchEntityException(
+ NoSuchEntityException.NO_SUCH_ENTITY_MESSAGE,
+ srcType == Entity.EntityType.TAG ? "policy" :
"tag",
+ destEntityIdent.name()));
default:
throw new IllegalArgumentException(
String.format("Doesn't support the relation type %s", relType));
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java
b/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java
index bdac4f92b8..bf6c50ee09 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java
@@ -565,6 +565,8 @@ public class RelationalEntityStore
return Entity.EntityType.POLICY;
case TAG_METADATA_OBJECT_REL:
return Entity.EntityType.TAG;
+ case POLICY_TAG_REL:
+ return Entity.EntityType.POLICY;
default:
throw new IllegalArgumentException(
String.format("Doesn't support the relation type %s", relType));
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyTagRelMapper.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyTagRelMapper.java
new file mode 100644
index 0000000000..370c2b2f5e
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyTagRelMapper.java
@@ -0,0 +1,104 @@
+/*
+ * 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.mapper;
+
+import java.util.List;
+import org.apache.gravitino.storage.relational.po.PolicyTagRelPO;
+import org.apache.ibatis.annotations.DeleteProvider;
+import org.apache.ibatis.annotations.InsertProvider;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.SelectProvider;
+import org.apache.ibatis.annotations.UpdateProvider;
+
+/** MyBatis mapper for policy-to-tag relations. */
+public interface PolicyTagRelMapper {
+ /** The policy-to-tag relation table name. */
+ String POLICY_TAG_RELATION_TABLE_NAME = "policy_tag_relation_meta";
+
+ /**
+ * Lists active policy-to-tag relations for the given tag names in a
metalake.
+ *
+ * @param metalakeName The metalake name.
+ * @param tagNames The tag names.
+ * @return The active policy-to-tag relations.
+ */
+ @SelectProvider(type = PolicyTagRelSQLProviderFactory.class, method =
"listByTagNames")
+ List<PolicyTagRelPO> listByTagNames(
+ @Param("metalakeName") String metalakeName, @Param("tagNames")
List<String> tagNames);
+
+ /**
+ * Lists active policy-to-tag relations for the given policy names in a
metalake.
+ *
+ * @param metalakeName The metalake name.
+ * @param policyNames The policy names.
+ * @return The active policy-to-tag relations.
+ */
+ @SelectProvider(type = PolicyTagRelSQLProviderFactory.class, method =
"listByPolicyNames")
+ List<PolicyTagRelPO> listByPolicyNames(
+ @Param("metalakeName") String metalakeName, @Param("policyNames")
List<String> policyNames);
+
+ /**
+ * Gets one active relation by policy ID and tag ID.
+ *
+ * @param policyId The policy ID.
+ * @param tagId The tag ID.
+ * @return The active relation, or null if no relation exists.
+ */
+ @SelectProvider(type = PolicyTagRelSQLProviderFactory.class, method =
"getByPolicyIdAndTagId")
+ PolicyTagRelPO getByPolicyIdAndTagId(
+ @Param("policyId") Long policyId, @Param("tagId") Long tagId);
+
+ /**
+ * Inserts a policy-to-tag relation if the active pair does not exist.
+ *
+ * @param relation The relation to insert.
+ * @return The number of inserted rows.
+ */
+ @InsertProvider(type = PolicyTagRelSQLProviderFactory.class, method =
"insertIfAbsent")
+ int insertIfAbsent(@Param("relation") PolicyTagRelPO relation);
+
+ /**
+ * Soft-deletes one active relation and advances its optimistic-concurrency
version.
+ *
+ * @param relation The observed relation and version.
+ * @return The number of updated rows.
+ */
+ @UpdateProvider(type = PolicyTagRelSQLProviderFactory.class, method =
"softDeleteByIdAndVersion")
+ int softDeleteByIdAndVersion(@Param("relation") PolicyTagRelPO relation);
+
+ /**
+ * Soft-deletes active relations in a metalake.
+ *
+ * @param metalakeId The metalake ID.
+ * @return The number of updated rows.
+ */
+ @UpdateProvider(type = PolicyTagRelSQLProviderFactory.class, method =
"softDeleteByMetalakeId")
+ int softDeleteByMetalakeId(@Param("metalakeId") Long metalakeId);
+
+ /**
+ * Physically deletes expired relation rows.
+ *
+ * @param legacyTimeline The exclusive deletion timestamp upper bound.
+ * @param limit The maximum number of rows to delete.
+ * @return The number of deleted rows.
+ */
+ @DeleteProvider(type = PolicyTagRelSQLProviderFactory.class, method =
"deleteByLegacyTimeline")
+ int deleteByLegacyTimeline(
+ @Param("legacyTimeline") Long legacyTimeline, @Param("limit") int limit);
+}
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyTagRelSQLProviderFactory.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyTagRelSQLProviderFactory.java
new file mode 100644
index 0000000000..d9cdb827ce
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/PolicyTagRelSQLProviderFactory.java
@@ -0,0 +1,94 @@
+/*
+ * 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.mapper;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.gravitino.storage.relational.JDBCBackend.JDBCBackendType;
+import
org.apache.gravitino.storage.relational.mapper.provider.base.PolicyTagRelBaseSQLProvider;
+import
org.apache.gravitino.storage.relational.mapper.provider.postgresql.PolicyTagRelPostgreSQLProvider;
+import org.apache.gravitino.storage.relational.po.PolicyTagRelPO;
+import org.apache.gravitino.storage.relational.session.SqlSessionFactoryHelper;
+import org.apache.ibatis.annotations.Param;
+
+/** Selects the policy-to-tag relation SQL provider for the active JDBC
backend. */
+public class PolicyTagRelSQLProviderFactory {
+
+ private static final Map<JDBCBackendType, PolicyTagRelBaseSQLProvider>
PROVIDERS =
+ ImmutableMap.of(
+ JDBCBackendType.MYSQL, new PolicyTagRelMySQLProvider(),
+ JDBCBackendType.H2, new PolicyTagRelH2Provider(),
+ JDBCBackendType.POSTGRESQL, new PolicyTagRelPostgreSQLProvider());
+
+ /**
+ * @return The SQL provider for the active backend.
+ */
+ public static PolicyTagRelBaseSQLProvider getProvider() {
+ String databaseId =
+ SqlSessionFactoryHelper.getInstance()
+ .getSqlSessionFactory()
+ .getConfiguration()
+ .getDatabaseId();
+ return PROVIDERS.get(JDBCBackendType.fromString(databaseId));
+ }
+
+ /** Delegates a tag-anchored list query. */
+ public static String listByTagNames(
+ @Param("metalakeName") String metalakeName, @Param("tagNames")
List<String> tagNames) {
+ return getProvider().listByTagNames(metalakeName, tagNames);
+ }
+
+ /** Delegates a policy-anchored list query. */
+ public static String listByPolicyNames(
+ @Param("metalakeName") String metalakeName, @Param("policyNames")
List<String> policyNames) {
+ return getProvider().listByPolicyNames(metalakeName, policyNames);
+ }
+
+ /** Delegates a single relation query. */
+ public static String getByPolicyIdAndTagId(
+ @Param("policyId") Long policyId, @Param("tagId") Long tagId) {
+ return getProvider().getByPolicyIdAndTagId(policyId, tagId);
+ }
+
+ /** Delegates an insert-if-absent operation. */
+ public static String insertIfAbsent(@Param("relation") PolicyTagRelPO
relation) {
+ return getProvider().insertIfAbsent(relation);
+ }
+
+ /** Delegates a relation soft delete. */
+ public static String softDeleteByIdAndVersion(@Param("relation")
PolicyTagRelPO relation) {
+ return getProvider().softDeleteByIdAndVersion(relation);
+ }
+
+ /** Delegates metalake deletion cleanup. */
+ public static String softDeleteByMetalakeId(@Param("metalakeId") Long
metalakeId) {
+ return getProvider().softDeleteByMetalakeId(metalakeId);
+ }
+
+ /** Delegates expired relation cleanup. */
+ public static String deleteByLegacyTimeline(
+ @Param("legacyTimeline") Long legacyTimeline, @Param("limit") int limit)
{
+ return getProvider().deleteByLegacyTimeline(legacyTimeline, limit);
+ }
+
+ static class PolicyTagRelMySQLProvider extends PolicyTagRelBaseSQLProvider {}
+
+ static class PolicyTagRelH2Provider extends PolicyTagRelBaseSQLProvider {}
+}
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/TagMetaMapper.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/TagMetaMapper.java
index 68cb3d721f..2d6095bf03 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/TagMetaMapper.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/TagMetaMapper.java
@@ -80,6 +80,15 @@ public interface TagMetaMapper {
@SelectProvider(type = TagMetaSQLProviderFactory.class, method =
"selectTagByTagId")
TagPO selectTagByTagId(@Param("tagId") Long tagId);
+ /**
+ * Selects and exclusively locks an active tag by ID.
+ *
+ * @param tagId The tag ID.
+ * @return The locked tag, or null if it is not active.
+ */
+ @SelectProvider(type = TagMetaSQLProviderFactory.class, method =
"selectTagByTagIdForUpdate")
+ TagPO selectTagByTagIdForUpdate(@Param("tagId") Long tagId);
+
@SelectProvider(type = TagMetaSQLProviderFactory.class, method =
"listTagPOsByTagIds")
List<TagPO> listTagPOsByTagIds(@Param("tagIds") List<Long> tagIds);
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/TagMetaSQLProviderFactory.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/TagMetaSQLProviderFactory.java
index 1dde368e69..400bcc0e08 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/TagMetaSQLProviderFactory.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/TagMetaSQLProviderFactory.java
@@ -106,6 +106,11 @@ public class TagMetaSQLProviderFactory {
return getProvider().selectTagByTagId(tagId);
}
+ /** Delegates an exclusive-lock tag query. */
+ public static String selectTagByTagIdForUpdate(@Param("tagId") Long tagId) {
+ return getProvider().selectTagByTagIdForUpdate(tagId);
+ }
+
public static String listTagPOsByTagIds(@Param("tagIds") List<Long> tagIds) {
return getProvider().listTagPOsByTagIds(tagIds);
}
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/DefaultMapperPackageProvider.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/DefaultMapperPackageProvider.java
index 8065ecfa3b..6fdf25d142 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/DefaultMapperPackageProvider.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/DefaultMapperPackageProvider.java
@@ -38,6 +38,7 @@ import
org.apache.gravitino.storage.relational.mapper.OrphanedMetadataObjectRela
import org.apache.gravitino.storage.relational.mapper.OwnerMetaMapper;
import org.apache.gravitino.storage.relational.mapper.PolicyMetaMapper;
import
org.apache.gravitino.storage.relational.mapper.PolicyMetadataObjectRelMapper;
+import org.apache.gravitino.storage.relational.mapper.PolicyTagRelMapper;
import org.apache.gravitino.storage.relational.mapper.PolicyVersionMapper;
import org.apache.gravitino.storage.relational.mapper.RoleMetaMapper;
import org.apache.gravitino.storage.relational.mapper.SchemaMetaMapper;
@@ -77,6 +78,7 @@ public class DefaultMapperPackageProvider implements
MapperPackageProvider {
OrphanedMetadataObjectRelationMapper.class,
OwnerMetaMapper.class,
PolicyMetadataObjectRelMapper.class,
+ PolicyTagRelMapper.class,
PolicyMetaMapper.class,
PolicyVersionMapper.class,
RoleMetaMapper.class,
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/PolicyTagRelBaseSQLProvider.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/PolicyTagRelBaseSQLProvider.java
new file mode 100644
index 0000000000..b81e6b9ec6
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/PolicyTagRelBaseSQLProvider.java
@@ -0,0 +1,150 @@
+/*
+ * 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.mapper.provider.base;
+
+import static
org.apache.gravitino.storage.relational.mapper.PolicyTagRelMapper.POLICY_TAG_RELATION_TABLE_NAME;
+
+import java.util.List;
+import org.apache.gravitino.storage.relational.mapper.MetalakeMetaMapper;
+import org.apache.gravitino.storage.relational.mapper.PolicyMetaMapper;
+import org.apache.gravitino.storage.relational.mapper.TagMetaMapper;
+import org.apache.gravitino.storage.relational.po.PolicyTagRelPO;
+import org.apache.ibatis.annotations.Param;
+
+/** Base SQL provider for policy-to-tag relations. */
+public class PolicyTagRelBaseSQLProvider {
+
+ /** Returns SQL for listing relations anchored by tag names. */
+ public String listByTagNames(
+ @Param("metalakeName") String metalakeName, @Param("tagNames")
List<String> tagNames) {
+ return listRelations("tm.metalake_id", "tm.tag_name", "tagNames",
"tagName");
+ }
+
+ /** Returns SQL for listing relations anchored by policy names. */
+ public String listByPolicyNames(
+ @Param("metalakeName") String metalakeName, @Param("policyNames")
List<String> policyNames) {
+ return listRelations("pm.metalake_id", "pm.policy_name", "policyNames",
"policyName");
+ }
+
+ /** Returns SQL for getting one active relation. */
+ public String getByPolicyIdAndTagId(
+ @Param("policyId") Long policyId, @Param("tagId") Long tagId) {
+ return selectColumns()
+ + joins()
+ + " WHERE ptr.policy_id = #{policyId} AND ptr.tag_id = #{tagId}"
+ + activePredicates();
+ }
+
+ /** Returns SQL for inserting one relation if the active pair does not
exist. */
+ public String insertIfAbsent(@Param("relation") PolicyTagRelPO relation) {
+ return "INSERT IGNORE INTO "
+ + POLICY_TAG_RELATION_TABLE_NAME
+ + " (policy_id, tag_id, selector, audit_info, current_version,
last_version, deleted_at)"
+ + " VALUES (#{relation.policyId}, #{relation.tagId},
#{relation.selector},"
+ + " #{relation.auditInfo}, #{relation.currentVersion},
#{relation.lastVersion},"
+ + " #{relation.deletedAt})";
+ }
+
+ /** Returns SQL for soft-deleting one relation with a version CAS. */
+ public String softDeleteByIdAndVersion(@Param("relation") PolicyTagRelPO
relation) {
+ return "UPDATE "
+ + POLICY_TAG_RELATION_TABLE_NAME
+ // Advance the OCC token in the same statement as the soft delete.
Assign last_version
+ // first so MySQL computes both columns from the version observed
before this update.
+ + " SET last_version = current_version + 1,"
+ + " current_version = current_version + 1,"
+ + " deleted_at = "
+ + deletedAtNowExpression()
+ + " WHERE id = #{relation.id} AND current_version =
#{relation.currentVersion}"
+ + " AND deleted_at = 0";
+ }
+
+ /** Returns SQL for soft-deleting relations when a metalake is deleted. */
+ public String softDeleteByMetalakeId(@Param("metalakeId") Long metalakeId) {
+ return "UPDATE "
+ + POLICY_TAG_RELATION_TABLE_NAME
+ + " SET deleted_at = "
+ + deletedAtNowExpression()
+ + " WHERE EXISTS (SELECT * FROM "
+ + PolicyMetaMapper.POLICY_META_TABLE_NAME
+ + " pm WHERE pm.metalake_id = #{metalakeId} AND pm.policy_id = "
+ + POLICY_TAG_RELATION_TABLE_NAME
+ + ".policy_id)"
+ + " AND deleted_at = 0";
+ }
+
+ /** Returns SQL for physically deleting expired relation rows. */
+ public String deleteByLegacyTimeline(
+ @Param("legacyTimeline") Long legacyTimeline, @Param("limit") int limit)
{
+ return "DELETE FROM "
+ + POLICY_TAG_RELATION_TABLE_NAME
+ + " WHERE deleted_at > 0 AND deleted_at < #{legacyTimeline} LIMIT
#{limit}";
+ }
+
+ /** Returns the database expression for the current epoch-millisecond
timestamp. */
+ protected String deletedAtNowExpression() {
+ return "(UNIX_TIMESTAMP() * 1000.0)"
+ + " + EXTRACT(MICROSECOND FROM CURRENT_TIMESTAMP(3)) / 1000";
+ }
+
+ private String listRelations(
+ String metalakeIdColumn, String nameColumn, String collection, String
item) {
+ return "<script>"
+ + selectColumns()
+ + joins()
+ + " WHERE "
+ + metalakeIdColumn
+ + " IN (SELECT mm.metalake_id FROM "
+ + MetalakeMetaMapper.TABLE_NAME
+ + " mm WHERE mm.metalake_name = #{metalakeName} AND mm.deleted_at = 0)"
+ + " AND "
+ + nameColumn
+ + " IN <foreach item='"
+ + item
+ + "' collection='"
+ + collection
+ + "' open='(' separator=',' close=')'>#{"
+ + item
+ + "}</foreach>"
+ + activePredicates()
+ + " ORDER BY tm.tag_name, pm.policy_name"
+ + "</script>";
+ }
+
+ private String selectColumns() {
+ return "SELECT ptr.id AS id, ptr.policy_id AS policyId, pm.policy_name AS
policyName,"
+ + " ptr.tag_id AS tagId, tm.tag_name AS tagName, ptr.selector,"
+ + " ptr.audit_info AS auditInfo, ptr.current_version AS
currentVersion,"
+ + " ptr.last_version AS lastVersion, ptr.deleted_at AS deletedAt";
+ }
+
+ private String joins() {
+ return " FROM "
+ + POLICY_TAG_RELATION_TABLE_NAME
+ + " ptr JOIN "
+ + PolicyMetaMapper.POLICY_META_TABLE_NAME
+ + " pm ON ptr.policy_id = pm.policy_id JOIN "
+ + TagMetaMapper.TAG_TABLE_NAME
+ + " tm ON ptr.tag_id = tm.tag_id";
+ }
+
+ private String activePredicates() {
+ return " AND ptr.deleted_at = 0 AND pm.deleted_at = 0 AND tm.deleted_at =
0";
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/TagMetaBaseSQLProvider.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/TagMetaBaseSQLProvider.java
index e33f2080c9..8ab7a8fe06 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/TagMetaBaseSQLProvider.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/TagMetaBaseSQLProvider.java
@@ -233,6 +233,11 @@ public class TagMetaBaseSQLProvider {
+ " WHERE tag_id = #{tagId} and deleted_at = 0";
}
+ /** Returns SQL that selects and locks an active tag by ID. */
+ public String selectTagByTagIdForUpdate(@Param("tagId") Long tagId) {
+ return selectTagByTagId(tagId) + " FOR UPDATE";
+ }
+
public String listTagPOsByTagIds(@Param("tagIds") List<Long> tagIds) {
return "<script>"
+ "SELECT tag_id as tagId, tag_name as tagName,"
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/PolicyTagRelPostgreSQLProvider.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/PolicyTagRelPostgreSQLProvider.java
new file mode 100644
index 0000000000..101a3014ad
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/PolicyTagRelPostgreSQLProvider.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.storage.relational.mapper.provider.postgresql;
+
+import static
org.apache.gravitino.storage.relational.mapper.PolicyTagRelMapper.POLICY_TAG_RELATION_TABLE_NAME;
+
+import
org.apache.gravitino.storage.relational.mapper.provider.base.PolicyTagRelBaseSQLProvider;
+import org.apache.gravitino.storage.relational.po.PolicyTagRelPO;
+import org.apache.ibatis.annotations.Param;
+
+/** PostgreSQL SQL provider for policy-to-tag relations. */
+public class PolicyTagRelPostgreSQLProvider extends
PolicyTagRelBaseSQLProvider {
+
+ @Override
+ public String insertIfAbsent(@Param("relation") PolicyTagRelPO relation) {
+ return "INSERT INTO "
+ + POLICY_TAG_RELATION_TABLE_NAME
+ + " (policy_id, tag_id, selector, audit_info, current_version,
last_version, deleted_at)"
+ + " VALUES (#{relation.policyId}, #{relation.tagId},
#{relation.selector},"
+ + " #{relation.auditInfo}, #{relation.currentVersion},
#{relation.lastVersion},"
+ + " #{relation.deletedAt})"
+ + " ON CONFLICT (policy_id, tag_id, deleted_at) DO NOTHING";
+ }
+
+ @Override
+ public String deleteByLegacyTimeline(
+ @Param("legacyTimeline") Long legacyTimeline, @Param("limit") int limit)
{
+ return "DELETE FROM "
+ + POLICY_TAG_RELATION_TABLE_NAME
+ + " WHERE id IN (SELECT id FROM "
+ + POLICY_TAG_RELATION_TABLE_NAME
+ + " WHERE deleted_at > 0 AND deleted_at < #{legacyTimeline} LIMIT
#{limit})";
+ }
+
+ @Override
+ protected String deletedAtNowExpression() {
+ return "CAST(EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000 AS BIGINT)";
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/po/PolicyTagRelPO.java
b/core/src/main/java/org/apache/gravitino/storage/relational/po/PolicyTagRelPO.java
new file mode 100644
index 0000000000..29d8bd17e0
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/po/PolicyTagRelPO.java
@@ -0,0 +1,154 @@
+/*
+ * 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 com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import javax.annotation.Nullable;
+import lombok.Getter;
+
+/** Persistent object for a policy-to-tag relation row. */
+@Getter
+public class PolicyTagRelPO {
+ private Long id;
+ private Long policyId;
+ private String policyName;
+ private Long tagId;
+ private String tagName;
+ @Nullable private String selector;
+ private String auditInfo;
+ private Long currentVersion;
+ private Long lastVersion;
+ private Long deletedAt;
+
+ /**
+ * @return A builder for a policy-to-tag relation persistent object.
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof PolicyTagRelPO)) {
+ return false;
+ }
+ PolicyTagRelPO that = (PolicyTagRelPO) o;
+ return Objects.equal(id, that.id)
+ && Objects.equal(policyId, that.policyId)
+ && Objects.equal(policyName, that.policyName)
+ && Objects.equal(tagId, that.tagId)
+ && Objects.equal(tagName, that.tagName)
+ && Objects.equal(selector, that.selector)
+ && Objects.equal(auditInfo, that.auditInfo)
+ && Objects.equal(currentVersion, that.currentVersion)
+ && Objects.equal(lastVersion, that.lastVersion)
+ && Objects.equal(deletedAt, that.deletedAt);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(
+ id,
+ policyId,
+ policyName,
+ tagId,
+ tagName,
+ selector,
+ auditInfo,
+ currentVersion,
+ lastVersion,
+ deletedAt);
+ }
+
+ /** Builder for {@link PolicyTagRelPO}. */
+ public static class Builder {
+ private final PolicyTagRelPO relation;
+
+ private Builder() {
+ relation = new PolicyTagRelPO();
+ }
+
+ /** Sets the relation row ID. */
+ public Builder withId(Long id) {
+ relation.id = id;
+ return this;
+ }
+
+ /** Sets the policy ID. */
+ public Builder withPolicyId(Long policyId) {
+ relation.policyId = policyId;
+ return this;
+ }
+
+ /** Sets the tag ID. */
+ public Builder withTagId(Long tagId) {
+ relation.tagId = tagId;
+ return this;
+ }
+
+ /** Sets the selector JSON. */
+ public Builder withSelector(@Nullable String selector) {
+ relation.selector = selector;
+ return this;
+ }
+
+ /** Sets the audit information JSON. */
+ public Builder withAuditInfo(String auditInfo) {
+ relation.auditInfo = auditInfo;
+ return this;
+ }
+
+ /** Sets the current version. */
+ public Builder withCurrentVersion(Long currentVersion) {
+ relation.currentVersion = currentVersion;
+ return this;
+ }
+
+ /** Sets the last version. */
+ public Builder withLastVersion(Long lastVersion) {
+ relation.lastVersion = lastVersion;
+ return this;
+ }
+
+ /** Sets the deletion timestamp. */
+ public Builder withDeletedAt(Long deletedAt) {
+ relation.deletedAt = deletedAt;
+ return this;
+ }
+
+ /**
+ * Builds the persistent object.
+ *
+ * @return The persistent object.
+ */
+ public PolicyTagRelPO build() {
+ Preconditions.checkArgument(relation.policyId != null, "Policy id is
required");
+ Preconditions.checkArgument(relation.tagId != null, "Tag id is
required");
+ Preconditions.checkArgument(relation.auditInfo != null, "Audit info is
required");
+ Preconditions.checkArgument(relation.currentVersion != null, "Current
version is required");
+ Preconditions.checkArgument(relation.lastVersion != null, "Last version
is required");
+ Preconditions.checkArgument(relation.deletedAt != null, "Deleted at is
required");
+ return relation;
+ }
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/service/MetalakeMetaService.java
b/core/src/main/java/org/apache/gravitino/storage/relational/service/MetalakeMetaService.java
index 58c8c054fe..fb63973758 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/service/MetalakeMetaService.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/service/MetalakeMetaService.java
@@ -49,6 +49,7 @@ import
org.apache.gravitino.storage.relational.mapper.ModelVersionAliasRelMapper
import org.apache.gravitino.storage.relational.mapper.ModelVersionMetaMapper;
import org.apache.gravitino.storage.relational.mapper.OwnerMetaMapper;
import org.apache.gravitino.storage.relational.mapper.PolicyMetaMapper;
+import org.apache.gravitino.storage.relational.mapper.PolicyTagRelMapper;
import org.apache.gravitino.storage.relational.mapper.PolicyVersionMapper;
import org.apache.gravitino.storage.relational.mapper.RoleMetaMapper;
import org.apache.gravitino.storage.relational.mapper.SchemaMetaMapper;
@@ -283,6 +284,9 @@ public class MetalakeMetaService {
SessionUtils.doWithoutCommit(
TagMetadataObjectRelMapper.class,
mapper ->
mapper.softDeleteTagMetadataObjectRelsByMetalakeId(metalakeId)),
+ () ->
+ SessionUtils.doWithoutCommit(
+ PolicyTagRelMapper.class, mapper ->
mapper.softDeleteByMetalakeId(metalakeId)),
() ->
SessionUtils.doWithoutCommit(
PolicyMetaMapper.class,
@@ -374,6 +378,9 @@ public class MetalakeMetaService {
SessionUtils.doWithoutCommit(
TagMetadataObjectRelMapper.class,
mapper ->
mapper.softDeleteTagMetadataObjectRelsByMetalakeId(metalakeId)),
+ () ->
+ SessionUtils.doWithoutCommit(
+ PolicyTagRelMapper.class, mapper ->
mapper.softDeleteByMetalakeId(metalakeId)),
() ->
SessionUtils.doWithoutCommit(
OwnerMetaMapper.class,
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/service/PolicyTagRelService.java
b/core/src/main/java/org/apache/gravitino/storage/relational/service/PolicyTagRelService.java
new file mode 100644
index 0000000000..fc011456c2
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/service/PolicyTagRelService.java
@@ -0,0 +1,408 @@
+/*
+ * 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.service;
+
+import com.google.common.base.Preconditions;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.EntityAlreadyExistsException;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.RelationEdgeTarget;
+import org.apache.gravitino.RelationalEntity;
+import org.apache.gravitino.SupportsRelationOperations;
+import org.apache.gravitino.exceptions.NoSuchEntityException;
+import org.apache.gravitino.exceptions.OptimisticLockException;
+import org.apache.gravitino.json.JsonUtils;
+import org.apache.gravitino.meta.AuditInfo;
+import org.apache.gravitino.meta.PolicyEntity;
+import org.apache.gravitino.meta.TagEntity;
+import org.apache.gravitino.storage.relational.mapper.PolicyMetaMapper;
+import org.apache.gravitino.storage.relational.mapper.PolicyTagRelMapper;
+import org.apache.gravitino.storage.relational.mapper.TagMetaMapper;
+import org.apache.gravitino.storage.relational.po.PolicyPO;
+import org.apache.gravitino.storage.relational.po.PolicyTagRelPO;
+import org.apache.gravitino.storage.relational.po.TagPO;
+import org.apache.gravitino.storage.relational.utils.SessionUtils;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+import org.apache.gravitino.utils.PrincipalUtils;
+
+/** JDBC metadata service for policy-to-tag relations. */
+public class PolicyTagRelService {
+
+ private static final PolicyTagRelService INSTANCE = new
PolicyTagRelService();
+
+ /**
+ * @return The singleton service instance.
+ */
+ public static PolicyTagRelService getInstance() {
+ return INSTANCE;
+ }
+
+ private PolicyTagRelService() {}
+
+ /**
+ * Lists policy-to-tag relation edges from policy or tag anchors.
+ *
+ * @param anchors The policy or tag identifiers to query.
+ * @param anchorType The entity type of every anchor.
+ * @return The relation edges, including selector JSON as the relation value.
+ */
+ public List<RelationalEntity<?>> listRelations(
+ List<NameIdentifier> anchors, Entity.EntityType anchorType) {
+ if (anchors == null || anchors.isEmpty()) {
+ return Collections.emptyList();
+ }
+ Preconditions.checkArgument(
+ anchorType == Entity.EntityType.TAG || anchorType ==
Entity.EntityType.POLICY,
+ "Policy-to-tag relations do not support anchor type %s",
+ anchorType);
+ validateSameMetalake(anchors);
+
+ String metalake = anchors.get(0).namespace().level(0);
+ List<String> anchorNames =
+
anchors.stream().map(NameIdentifier::name).distinct().collect(Collectors.toList());
+ List<PolicyTagRelPO> relations =
+ SessionUtils.getWithoutCommit(
+ PolicyTagRelMapper.class,
+ mapper ->
+ anchorType == Entity.EntityType.TAG
+ ? mapper.listByTagNames(metalake, anchorNames)
+ : mapper.listByPolicyNames(metalake, anchorNames));
+ if (relations.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ return anchorType == Entity.EntityType.TAG
+ ? policyTargets(metalake, relations)
+ : tagTargets(metalake, relations);
+ }
+
+ /**
+ * Creates or removes policy-to-tag relations for one tag.
+ *
+ * <p>An add for an existing policy and tag pair conflicts regardless of its
selector. Removing a
+ * missing pair is an idempotent no-op. The same pair cannot be added and
removed in one update.
+ *
+ * @param tagIdentifier The source tag identifier.
+ * @param targetsToAdd Policy targets to create.
+ * @param targetsToRemove Policy targets to remove.
+ * @return All active policy targets for the tag after the update.
+ * @throws IOException If selector audit information cannot be serialized.
+ * @throws EntityAlreadyExistsException If a relation to add already exists.
+ * @throws IllegalArgumentException If the same relation is both added and
removed.
+ */
+ public List<PolicyEntity> updateRelations(
+ NameIdentifier tagIdentifier,
+ RelationEdgeTarget[] targetsToAdd,
+ RelationEdgeTarget[] targetsToRemove)
+ throws IOException {
+ NameIdentifierUtil.checkTag(tagIdentifier);
+ String metalake = tagIdentifier.namespace().level(0);
+ RelationEdgeTarget[] targetsToAddOrEmpty = nullToEmpty(targetsToAdd);
+ RelationEdgeTarget[] targetsToRemoveOrEmpty = nullToEmpty(targetsToRemove);
+ validatePolicyTargets(metalake, targetsToAddOrEmpty);
+ validatePolicyTargets(metalake, targetsToRemoveOrEmpty);
+ validateNoOverlappingTargets(targetsToAddOrEmpty, targetsToRemoveOrEmpty);
+
+ List<PolicyEntity> updatedPolicies = new ArrayList<>();
+ try {
+ SessionUtils.doMultipleWithCommit(
+ () -> {
+ try {
+ updatedPolicies.addAll(
+ updateRelationsWithoutCommit(
+ tagIdentifier, targetsToAddOrEmpty,
targetsToRemoveOrEmpty));
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ });
+ } catch (UncheckedIOException e) {
+ throw e.getCause();
+ }
+ return updatedPolicies;
+ }
+
+ private List<PolicyEntity> updateRelationsWithoutCommit(
+ NameIdentifier tagIdentifier,
+ RelationEdgeTarget[] targetsToAdd,
+ RelationEdgeTarget[] targetsToRemove)
+ throws IOException {
+ String metalake = tagIdentifier.namespace().level(0);
+ TagPO tagPO = lockTag(tagIdentifier);
+ long tagId = tagPO.getTagId();
+ Map<String, Long> policyIds = resolvePolicyIds(metalake, targetsToAdd,
targetsToRemove);
+
+ for (RelationEdgeTarget target : targetsToRemove) {
+ long policyId = policyIds.get(target.nameIdentifier().name());
+ PolicyTagRelPO existing =
+ SessionUtils.getWithoutCommit(
+ PolicyTagRelMapper.class, mapper ->
mapper.getByPolicyIdAndTagId(policyId, tagId));
+ if (existing != null) {
+ int deleted =
+ SessionUtils.getWithoutCommit(
+ PolicyTagRelMapper.class, mapper ->
mapper.softDeleteByIdAndVersion(existing));
+ if (deleted != 1) {
+ throw relationConflict(tagIdentifier);
+ }
+ }
+ }
+
+ for (RelationEdgeTarget target : targetsToAdd) {
+ long policyId = policyIds.get(target.nameIdentifier().name());
+ insertIfAbsent(
+ tagIdentifier,
+ target.nameIdentifier(),
+ policyId,
+ tagId,
+ target.relationValue().orElse(null));
+ }
+
+ return listRelations(Collections.singletonList(tagIdentifier),
Entity.EntityType.TAG).stream()
+ .map(relation -> (PolicyEntity) relation.targetEntity())
+ .collect(Collectors.toList());
+ }
+
+ private static List<RelationalEntity<?>> policyTargets(
+ String metalake, List<PolicyTagRelPO> relations) {
+ Set<String> policyNames =
+ relations.stream()
+ .map(PolicyTagRelPO::getPolicyName)
+ .collect(Collectors.toCollection(LinkedHashSet::new));
+ List<NameIdentifier> policyIdentifiers =
+ policyNames.stream()
+ .map(name -> NameIdentifierUtil.ofPolicy(metalake, name))
+ .collect(Collectors.toList());
+ Map<String, PolicyEntity> policies =
+
PolicyMetaService.getInstance().batchGetPolicyByIdentifier(policyIdentifiers).stream()
+ .collect(Collectors.toMap(PolicyEntity::name, policy -> policy));
+
+ List<RelationalEntity<?>> result = new ArrayList<>();
+ for (PolicyTagRelPO relation : relations) {
+ PolicyEntity policy = policies.get(relation.getPolicyName());
+ if (policy != null) {
+ result.add(
+ new RelationalEntity<>(
+ SupportsRelationOperations.Type.POLICY_TAG_REL,
+ NameIdentifierUtil.ofTag(metalake, relation.getTagName()),
+ Entity.EntityType.TAG,
+ policy,
+ relation.getSelector()));
+ }
+ }
+ return result;
+ }
+
+ private static List<RelationalEntity<?>> tagTargets(
+ String metalake, List<PolicyTagRelPO> relations) {
+ Set<String> tagNames =
+ relations.stream()
+ .map(PolicyTagRelPO::getTagName)
+ .collect(Collectors.toCollection(LinkedHashSet::new));
+ List<NameIdentifier> tagIdentifiers =
+ tagNames.stream()
+ .map(name -> NameIdentifierUtil.ofTag(metalake, name))
+ .collect(Collectors.toList());
+ Map<String, TagEntity> tags =
+
TagMetaService.getInstance().batchGetTagByIdentifier(tagIdentifiers).stream()
+ .collect(Collectors.toMap(TagEntity::name, tag -> tag));
+
+ List<RelationalEntity<?>> result = new ArrayList<>();
+ for (PolicyTagRelPO relation : relations) {
+ TagEntity tag = tags.get(relation.getTagName());
+ if (tag != null) {
+ result.add(
+ new RelationalEntity<>(
+ SupportsRelationOperations.Type.POLICY_TAG_REL,
+ NameIdentifierUtil.ofPolicy(metalake,
relation.getPolicyName()),
+ Entity.EntityType.POLICY,
+ tag,
+ relation.getSelector()));
+ }
+ }
+ return result;
+ }
+
+ private static void insertIfAbsent(
+ NameIdentifier tagIdentifier,
+ NameIdentifier policyIdentifier,
+ long policyId,
+ long tagId,
+ String selector)
+ throws IOException {
+ PolicyTagRelPO relation =
+ PolicyTagRelPO.builder()
+ .withPolicyId(policyId)
+ .withTagId(tagId)
+ .withSelector(selector)
+ .withAuditInfo(auditInfo())
+ .withCurrentVersion(1L)
+ .withLastVersion(1L)
+ .withDeletedAt(0L)
+ .build();
+ int inserted =
+ SessionUtils.getWithoutCommit(
+ PolicyTagRelMapper.class, mapper ->
mapper.insertIfAbsent(relation));
+ if (inserted == 1) {
+ return;
+ }
+
+ // A zero-row insert means another writer won the active-pair uniqueness
race. Re-read the
+ // pair to distinguish an existing relation from another concurrent state
transition.
+ PolicyTagRelPO winner =
+ SessionUtils.getWithoutCommit(
+ PolicyTagRelMapper.class, mapper ->
mapper.getByPolicyIdAndTagId(policyId, tagId));
+ if (winner != null) {
+ throw relationAlreadyExists(tagIdentifier, policyIdentifier);
+ }
+ throw relationConflict(tagIdentifier);
+ }
+
+ private static String auditInfo() throws IOException {
+ String principal = PrincipalUtils.getCurrentPrincipal().getName();
+ Instant now = Instant.now();
+ AuditInfo auditInfo =
AuditInfo.builder().withCreator(principal).withCreateTime(now).build();
+ return JsonUtils.anyFieldMapper().writeValueAsString(auditInfo);
+ }
+
+ private static OptimisticLockException relationConflict(NameIdentifier
tagIdentifier) {
+ return new OptimisticLockException(
+ "A policy-to-tag relation for tag %s was modified concurrently; retry
the operation",
+ tagIdentifier);
+ }
+
+ private static EntityAlreadyExistsException relationAlreadyExists(
+ NameIdentifier tagIdentifier, NameIdentifier policyIdentifier) {
+ return new EntityAlreadyExistsException(
+ "The policy-to-tag relation between tag %s and policy %s already
exists",
+ tagIdentifier, policyIdentifier);
+ }
+
+ private static void validatePolicyTarget(String metalake, RelationEdgeTarget
target) {
+ Preconditions.checkArgument(target != null, "Policy relation target cannot
be null");
+ Preconditions.checkArgument(
+ target.entityType() == Entity.EntityType.POLICY,
+ "Policy-to-tag relation target must be POLICY, but is %s",
+ target.entityType());
+ Preconditions.checkArgument(
+ target.nameIdentifier().namespace().length() > 0
+ && metalake.equals(target.nameIdentifier().namespace().level(0)),
+ "Policy and tag must belong to the same metalake");
+ }
+
+ private static void validatePolicyTargets(String metalake,
RelationEdgeTarget[] targets) {
+ for (RelationEdgeTarget target : targets) {
+ validatePolicyTarget(metalake, target);
+ }
+ }
+
+ private static void validateNoOverlappingTargets(
+ RelationEdgeTarget[] targetsToAdd, RelationEdgeTarget[] targetsToRemove)
{
+ Set<String> policyNamesToAdd =
+ Arrays.stream(targetsToAdd)
+ .map(target -> target.nameIdentifier().name())
+ .collect(Collectors.toSet());
+ for (RelationEdgeTarget target : targetsToRemove) {
+ Preconditions.checkArgument(
+ !policyNamesToAdd.contains(target.nameIdentifier().name()),
+ "Policy-to-tag relation target %s cannot be both added and removed",
+ target.nameIdentifier());
+ }
+ }
+
+ private static TagPO lockTag(NameIdentifier tagIdentifier) {
+ String metalake = tagIdentifier.namespace().level(0);
+ return SessionUtils.getWithoutCommit(
+ TagMetaMapper.class,
+ mapper -> {
+ TagPO observed = mapper.selectTagMetaByMetalakeAndName(metalake,
tagIdentifier.name());
+ if (observed == null) {
+ throw noSuchEntity(Entity.EntityType.TAG, tagIdentifier.name());
+ }
+ TagPO locked = mapper.selectTagByTagIdForUpdate(observed.getTagId());
+ if (locked == null
+ || !Objects.equals(locked.getTagName(), tagIdentifier.name())
+ || !Objects.equals(locked.getMetalakeId(),
observed.getMetalakeId())) {
+ throw noSuchEntity(Entity.EntityType.TAG, tagIdentifier.name());
+ }
+ return locked;
+ });
+ }
+
+ private static Map<String, Long> resolvePolicyIds(
+ String metalake, RelationEdgeTarget[] targetsToAdd, RelationEdgeTarget[]
targetsToRemove) {
+ Set<String> policyNames = new LinkedHashSet<>();
+ Arrays.stream(targetsToAdd)
+ .map(target -> target.nameIdentifier().name())
+ .forEach(policyNames::add);
+ Arrays.stream(targetsToRemove)
+ .map(target -> target.nameIdentifier().name())
+ .forEach(policyNames::add);
+ if (policyNames.isEmpty()) {
+ return Collections.emptyMap();
+ }
+
+ List<PolicyPO> policies =
+ SessionUtils.getWithoutCommit(
+ PolicyMetaMapper.class,
+ mapper ->
+ mapper.listPolicyPOsByMetalakeAndPolicyNames(
+ metalake, new ArrayList<>(policyNames)));
+ Map<String, Long> policyIds =
+ policies.stream().collect(Collectors.toMap(PolicyPO::getPolicyName,
PolicyPO::getPolicyId));
+
+ for (String policyName : policyNames) {
+ if (!policyIds.containsKey(policyName)) {
+ throw noSuchEntity(Entity.EntityType.POLICY, policyName);
+ }
+ }
+ return policyIds;
+ }
+
+ private static NoSuchEntityException noSuchEntity(Entity.EntityType type,
String name) {
+ return new NoSuchEntityException(
+ NoSuchEntityException.NO_SUCH_ENTITY_MESSAGE,
type.name().toLowerCase(), name);
+ }
+
+ private static void validateSameMetalake(List<NameIdentifier> identifiers) {
+ Preconditions.checkArgument(
+ identifiers.stream()
+ .allMatch(identifier -> identifier != null &&
identifier.namespace().length() > 0),
+ "All policy-to-tag relation anchors must have a metalake namespace");
+ String metalake = identifiers.get(0).namespace().level(0);
+ Preconditions.checkArgument(
+ identifiers.stream()
+ .allMatch(identifier ->
metalake.equals(identifier.namespace().level(0))),
+ "All policy-to-tag relation anchors must belong to the same metalake");
+ }
+
+ private static RelationEdgeTarget[] nullToEmpty(RelationEdgeTarget[]
targets) {
+ return targets == null ? new RelationEdgeTarget[0] :
Arrays.copyOf(targets, targets.length);
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/service/TagMetaService.java
b/core/src/main/java/org/apache/gravitino/storage/relational/service/TagMetaService.java
index 92b6362379..6726bc8de7 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/service/TagMetaService.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/service/TagMetaService.java
@@ -49,6 +49,7 @@ import org.apache.gravitino.json.JsonUtils;
import org.apache.gravitino.meta.GenericEntity;
import org.apache.gravitino.meta.TagEntity;
import org.apache.gravitino.metrics.Monitored;
+import org.apache.gravitino.storage.relational.mapper.PolicyTagRelMapper;
import org.apache.gravitino.storage.relational.mapper.TagMetaMapper;
import
org.apache.gravitino.storage.relational.mapper.TagMetadataObjectRelMapper;
import org.apache.gravitino.storage.relational.po.TagMetadataObjectRelPO;
@@ -504,6 +505,7 @@ public class TagMetaService {
public int deleteTagMetasByLegacyTimeline(long legacyTimeline, int limit) {
int[] tagDeletedCount = new int[] {0};
int[] tagMetadataObjectRelDeletedCount = new int[] {0};
+ int[] policyTagRelDeletedCount = new int[] {0};
SessionUtils.doMultipleWithCommit(
() ->
@@ -515,9 +517,14 @@ public class TagMetaService {
tagMetadataObjectRelDeletedCount[0] =
SessionUtils.getWithoutCommit(
TagMetadataObjectRelMapper.class,
- mapper ->
mapper.deleteTagEntityRelsByLegacyTimeline(legacyTimeline, limit)));
+ mapper ->
mapper.deleteTagEntityRelsByLegacyTimeline(legacyTimeline, limit)),
+ () ->
+ policyTagRelDeletedCount[0] =
+ SessionUtils.getWithoutCommit(
+ PolicyTagRelMapper.class,
+ mapper -> mapper.deleteByLegacyTimeline(legacyTimeline,
limit)));
- return tagDeletedCount[0] + tagMetadataObjectRelDeletedCount[0];
+ return tagDeletedCount[0] + tagMetadataObjectRelDeletedCount[0] +
policyTagRelDeletedCount[0];
}
private static List<TagEntity> tagPOsToTagEntities(List<TagPO> tagPOs,
Namespace namespace) {
diff --git
a/core/src/test/java/org/apache/gravitino/storage/TestSQLScripts.java
b/core/src/test/java/org/apache/gravitino/storage/TestSQLScripts.java
index d3aa2fbbcc..ba06be927c 100644
--- a/core/src/test/java/org/apache/gravitino/storage/TestSQLScripts.java
+++ b/core/src/test/java/org/apache/gravitino/storage/TestSQLScripts.java
@@ -91,20 +91,51 @@ public class TestSQLScripts extends TestJDBCBackend {
for (List<File> scripts : versionScrips.values()) {
dropAllTables();
for (File scriptFile : scripts) {
- List<String> ddls = extractStatements(scriptFile.toPath());
-
- try (SqlSession sqlSession =
-
SqlSessionFactoryHelper.getInstance().getSqlSessionFactory().openSession(true))
{
- try (Connection connection = sqlSession.getConnection()) {
- try (Statement statement = connection.createStatement()) {
- for (String ddl : ddls) {
- Assertions.assertDoesNotThrow(
- () -> statement.execute(ddl),
- "Failed to execute DDL in file " + scriptFile.getName() +
"ddl: " + ddl);
- }
- }
- }
- }
+ executeScript(scriptFile);
+ }
+ }
+ }
+
+ @TestTemplate
+ public void testUpgradeSQLScripts() throws SQLException, IOException {
+ String gravitinoHome = System.getenv("GRAVITINO_HOME");
+ Assertions.assertNotNull(gravitinoHome, "GRAVITINO_HOME environment
variable is not set");
+ Path scriptDir = Path.of(gravitinoHome, "scripts",
backendType.toLowerCase());
+ File[] scriptFiles = scriptDir.toFile().listFiles();
+ Assertions.assertNotNull(scriptFiles, "No script files found in " +
scriptDir);
+ Arrays.sort(scriptFiles, Comparator.comparing(File::getName));
+
+ Pattern upgradePattern =
+ Pattern.compile("upgrade-([\\d.]+)-to-([\\d.]+)-" +
backendType.toLowerCase() + "\\.sql");
+ for (File upgradeScript : scriptFiles) {
+ Matcher upgradeMatcher = upgradePattern.matcher(upgradeScript.getName());
+ if (!upgradeMatcher.matches()) {
+ continue;
+ }
+
+ String fromVersion = upgradeMatcher.group(1);
+ File sourceSchema =
+ scriptDir
+ .resolve("schema-" + fromVersion + "-" +
backendType.toLowerCase() + ".sql")
+ .toFile();
+ Assertions.assertTrue(
+ sourceSchema.isFile(), "No source schema found for " +
upgradeScript.getName());
+ dropAllTables();
+ executeScript(sourceSchema);
+ executeScript(upgradeScript);
+ }
+ }
+
+ private void executeScript(File scriptFile) throws IOException, SQLException
{
+ List<String> ddls = extractStatements(scriptFile.toPath());
+ try (SqlSession sqlSession =
+
SqlSessionFactoryHelper.getInstance().getSqlSessionFactory().openSession(true);
+ Connection connection = sqlSession.getConnection();
+ Statement statement = connection.createStatement()) {
+ for (String ddl : ddls) {
+ Assertions.assertDoesNotThrow(
+ () -> statement.execute(ddl),
+ "Failed to execute DDL in file " + scriptFile.getName() + " ddl: "
+ ddl);
}
}
}
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestPolicyTagRelService.java
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestPolicyTagRelService.java
new file mode 100644
index 0000000000..3d2911f68e
--- /dev/null
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestPolicyTagRelService.java
@@ -0,0 +1,610 @@
+/*
+ * 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.service;
+
+import java.io.IOException;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.EntityAlreadyExistsException;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.RelationEdgeTarget;
+import org.apache.gravitino.RelationUpdate;
+import org.apache.gravitino.RelationalEntity;
+import org.apache.gravitino.SupportsRelationOperations;
+import org.apache.gravitino.exceptions.NoSuchEntityException;
+import org.apache.gravitino.meta.BaseMetalake;
+import org.apache.gravitino.meta.PolicyEntity;
+import org.apache.gravitino.meta.TagEntity;
+import org.apache.gravitino.storage.RandomIdGenerator;
+import org.apache.gravitino.storage.relational.TestJDBCBackend;
+import org.apache.gravitino.storage.relational.mapper.PolicyTagRelMapper;
+import org.apache.gravitino.storage.relational.po.PolicyTagRelPO;
+import org.apache.gravitino.storage.relational.session.SqlSessionFactoryHelper;
+import org.apache.gravitino.storage.relational.utils.SessionUtils;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+import org.apache.gravitino.utils.NamespaceUtil;
+import org.apache.ibatis.session.SqlSession;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.TestTemplate;
+
+/** Tests policy-to-tag relation persistence and lifecycle behavior. */
+public class TestPolicyTagRelService extends TestJDBCBackend {
+
+ private static final String METALAKE = "policy_tag_relation_metalake";
+ private static final String FINANCE_SELECTOR =
"{\"type\":\"TAG_VALUE\",\"value\":\"finance\"}";
+ private static final String RISK_SELECTOR =
"{\"type\":\"TAG_VALUE\",\"value\":\"risk\"}";
+
+ @TestTemplate
+ public void testSelectorCreateConflictBidirectionalReadAndIdempotentDelete()
throws IOException {
+ createAndInsertMakeLake(METALAKE);
+ TagEntity tag =
+ TagEntity.builder()
+ .withId(RandomIdGenerator.INSTANCE.nextId())
+ .withName("domain")
+ .withNamespace(NamespaceUtil.ofTag(METALAKE))
+ .withProperties(Collections.emptyMap())
+ .withAuditInfo(AUDIT_INFO)
+ .build();
+ PolicyEntity policy =
+ createPolicy(
+ RandomIdGenerator.INSTANCE.nextId(),
+ NamespaceUtil.ofPolicy(METALAKE),
+ "retention",
+ AUDIT_INFO);
+ backend.insert(tag, false);
+ backend.insert(policy, false);
+
+ RelationEdgeTarget financeTarget =
+ RelationEdgeTarget.of(policy.nameIdentifier(),
Entity.EntityType.POLICY, FINANCE_SELECTOR);
+ RelationUpdate financeUpdate =
+ RelationUpdate.of(
+ SupportsRelationOperations.Type.POLICY_TAG_REL,
+ tag.nameIdentifier(),
+ Entity.EntityType.TAG,
+ new RelationEdgeTarget[] {financeTarget},
+ new RelationEdgeTarget[0]);
+ backend.updateEntityRelations(financeUpdate);
+ Assertions.assertThrows(
+ EntityAlreadyExistsException.class, () ->
backend.updateEntityRelations(financeUpdate));
+
+ RelationEdgeTarget riskTarget =
+ RelationEdgeTarget.of(policy.nameIdentifier(),
Entity.EntityType.POLICY, RISK_SELECTOR);
+ Assertions.assertThrows(
+ EntityAlreadyExistsException.class,
+ () ->
+ backend.updateEntityRelations(
+ RelationUpdate.of(
+ SupportsRelationOperations.Type.POLICY_TAG_REL,
+ tag.nameIdentifier(),
+ Entity.EntityType.TAG,
+ new RelationEdgeTarget[] {riskTarget},
+ new RelationEdgeTarget[0])));
+
+ List<RelationalEntity<?>> byTag =
+ backend.batchListEntitiesByRelation(
+ SupportsRelationOperations.Type.POLICY_TAG_REL,
+ Collections.singletonList(tag.nameIdentifier()),
+ Entity.EntityType.TAG);
+ Assertions.assertEquals(1, byTag.size());
+ Assertions.assertEquals(SupportsRelationOperations.Type.POLICY_TAG_REL,
byTag.get(0).type());
+ Assertions.assertEquals(tag.nameIdentifier(), byTag.get(0).source());
+ Assertions.assertEquals(Entity.EntityType.TAG, byTag.get(0).sourceType());
+ Assertions.assertEquals(policy, byTag.get(0).targetEntity());
+ Assertions.assertEquals(FINANCE_SELECTOR,
byTag.get(0).relationValue().orElse(null));
+ Assertions.assertEquals(
+ policy,
+ backend.getEntityByRelation(
+ SupportsRelationOperations.Type.POLICY_TAG_REL,
+ tag.nameIdentifier(),
+ Entity.EntityType.TAG,
+ policy.nameIdentifier()));
+
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ backend.updateEntityRelations(
+ RelationUpdate.of(
+ SupportsRelationOperations.Type.POLICY_TAG_REL,
+ tag.nameIdentifier(),
+ Entity.EntityType.TAG,
+ new RelationEdgeTarget[] {riskTarget},
+ new RelationEdgeTarget[] {financeTarget})));
+ List<RelationalEntity<?>> byPolicy =
+ backend.batchListEntitiesByRelation(
+ SupportsRelationOperations.Type.POLICY_TAG_REL,
+ Collections.singletonList(policy.nameIdentifier()),
+ Entity.EntityType.POLICY);
+ Assertions.assertEquals(1, byPolicy.size());
+ Assertions.assertEquals(policy.nameIdentifier(), byPolicy.get(0).source());
+ Assertions.assertEquals(Entity.EntityType.POLICY,
byPolicy.get(0).sourceType());
+ Assertions.assertEquals(tag, byPolicy.get(0).targetEntity());
+ Assertions.assertEquals(FINANCE_SELECTOR,
byPolicy.get(0).relationValue().orElse(null));
+
+ RelationUpdate removeUpdate =
+ RelationUpdate.of(
+ SupportsRelationOperations.Type.POLICY_TAG_REL,
+ tag.nameIdentifier(),
+ Entity.EntityType.TAG,
+ new RelationEdgeTarget[0],
+ new RelationEdgeTarget[] {financeTarget});
+ backend.updateEntityRelations(removeUpdate);
+ backend.updateEntityRelations(removeUpdate);
+ Assertions.assertTrue(
+ backend
+ .batchListEntitiesByRelation(
+ SupportsRelationOperations.Type.POLICY_TAG_REL,
+ Collections.singletonList(NameIdentifierUtil.ofTag(METALAKE,
tag.name())),
+ Entity.EntityType.TAG)
+ .isEmpty());
+ }
+
+ @TestTemplate
+ public void testBatchListRelationsByMultipleAnchors() throws IOException {
+ createAndInsertMakeLake(METALAKE);
+ TagEntity firstTag = createAssociation(METALAKE, "domain_a",
"retention_a");
+ TagEntity secondTag = createAssociation(METALAKE, "domain_b",
"retention_b");
+ String otherMetalake = METALAKE + "_other";
+ createAndInsertMakeLake(otherMetalake);
+ createAssociation(otherMetalake, "domain_a", "retention_a");
+
+ List<RelationalEntity<?>> byTags =
+ backend.batchListEntitiesByRelation(
+ SupportsRelationOperations.Type.POLICY_TAG_REL,
+ Arrays.asList(
+ firstTag.nameIdentifier(),
+ secondTag.nameIdentifier(),
+ NameIdentifierUtil.ofTag(METALAKE, "missing_tag")),
+ Entity.EntityType.TAG);
+ Assertions.assertEquals(2, byTags.size());
+ Assertions.assertEquals(
+ Set.of("domain_a", "domain_b"),
+ byTags.stream().map(relation ->
relation.source().name()).collect(Collectors.toSet()));
+
+ List<RelationalEntity<?>> byPolicies =
+ backend.batchListEntitiesByRelation(
+ SupportsRelationOperations.Type.POLICY_TAG_REL,
+ Arrays.asList(
+ NameIdentifierUtil.ofPolicy(METALAKE, "retention_a"),
+ NameIdentifierUtil.ofPolicy(METALAKE, "retention_b"),
+ NameIdentifierUtil.ofPolicy(METALAKE, "missing_policy")),
+ Entity.EntityType.POLICY);
+ Assertions.assertEquals(2, byPolicies.size());
+ Assertions.assertEquals(
+ Set.of("retention_a", "retention_b"),
+ byPolicies.stream().map(relation ->
relation.source().name()).collect(Collectors.toSet()));
+ }
+
+ @TestTemplate
+ public void testUpdateRelationsRollsBackOnFailure() throws IOException {
+ createAndInsertMakeLake(METALAKE);
+ TagEntity tag = createAssociation(METALAKE, "domain", "retention");
+ RelationEdgeTarget existingTarget =
+ RelationEdgeTarget.of(
+ NameIdentifierUtil.ofPolicy(METALAKE, "retention"),
Entity.EntityType.POLICY, null);
+ RelationEdgeTarget missingTarget =
+ RelationEdgeTarget.of(
+ NameIdentifierUtil.ofPolicy(METALAKE, "missing_policy"),
+ Entity.EntityType.POLICY,
+ null);
+
+ Assertions.assertThrows(
+ NoSuchEntityException.class,
+ () ->
+ backend.updateEntityRelations(
+ RelationUpdate.of(
+ SupportsRelationOperations.Type.POLICY_TAG_REL,
+ tag.nameIdentifier(),
+ Entity.EntityType.TAG,
+ new RelationEdgeTarget[] {missingTarget},
+ new RelationEdgeTarget[] {existingTarget})));
+
+ List<RelationalEntity<?>> relations =
+ backend.batchListEntitiesByRelation(
+ SupportsRelationOperations.Type.POLICY_TAG_REL,
+ Collections.singletonList(tag.nameIdentifier()),
+ Entity.EntityType.TAG);
+ Assertions.assertEquals(1, relations.size());
+ Assertions.assertEquals("retention",
relations.get(0).targetEntity().name());
+ }
+
+ @TestTemplate
+ public void testDuplicateAddRollsBackAllRelationWrites() throws IOException {
+ createAndInsertMakeLake(METALAKE);
+ TagEntity tag = createAssociation(METALAKE, "domain", "retention");
+ PolicyEntity newPolicy =
+ createPolicy(
+ RandomIdGenerator.INSTANCE.nextId(),
+ NamespaceUtil.ofPolicy(METALAKE),
+ "new_policy",
+ AUDIT_INFO);
+ backend.insert(newPolicy, false);
+
+ RelationEdgeTarget newTarget =
+ RelationEdgeTarget.of(newPolicy.nameIdentifier(),
Entity.EntityType.POLICY, null);
+ RelationEdgeTarget existingTarget =
+ RelationEdgeTarget.of(
+ NameIdentifierUtil.ofPolicy(METALAKE, "retention"),
Entity.EntityType.POLICY, null);
+ Assertions.assertThrows(
+ EntityAlreadyExistsException.class,
+ () ->
+ backend.updateEntityRelations(
+ RelationUpdate.of(
+ SupportsRelationOperations.Type.POLICY_TAG_REL,
+ tag.nameIdentifier(),
+ Entity.EntityType.TAG,
+ new RelationEdgeTarget[] {newTarget, existingTarget},
+ new RelationEdgeTarget[0])));
+
+ List<RelationalEntity<?>> relations =
+ backend.batchListEntitiesByRelation(
+ SupportsRelationOperations.Type.POLICY_TAG_REL,
+ Collections.singletonList(tag.nameIdentifier()),
+ Entity.EntityType.TAG);
+ Assertions.assertEquals(1, relations.size());
+ Assertions.assertEquals("retention",
relations.get(0).targetEntity().name());
+ Assertions.assertNull(
+ SessionUtils.getWithoutCommit(
+ PolicyTagRelMapper.class,
+ mapper -> mapper.getByPolicyIdAndTagId(newPolicy.id(), tag.id())));
+ }
+
+ @TestTemplate
+ public void testEntityDeletesCascadePolicyTagRelations() throws IOException {
+ createAndInsertMakeLake(METALAKE);
+ TagEntity policyDeletedTag =
+ createAssociation(METALAKE, "policy_deleted_tag", "deleted_policy");
+ backend.delete(
+ NameIdentifierUtil.ofPolicy(METALAKE, "deleted_policy"),
Entity.EntityType.POLICY, false);
+ Assertions.assertTrue(
+ backend
+ .batchListEntitiesByRelation(
+ SupportsRelationOperations.Type.POLICY_TAG_REL,
+ Collections.singletonList(policyDeletedTag.nameIdentifier()),
+ Entity.EntityType.TAG)
+ .isEmpty());
+
+ TagEntity deletedTag = createAssociation(METALAKE, "deleted_tag",
"surviving_policy");
+ backend.delete(deletedTag.nameIdentifier(), Entity.EntityType.TAG, false);
+ Assertions.assertTrue(
+ backend
+ .batchListEntitiesByRelation(
+ SupportsRelationOperations.Type.POLICY_TAG_REL,
+ Collections.singletonList(
+ NameIdentifierUtil.ofPolicy(METALAKE, "surviving_policy")),
+ Entity.EntityType.POLICY)
+ .isEmpty());
+ }
+
+ @TestTemplate
+ public void testMetalakeCascadeDoesNotDeleteRelationsFromOtherMetalakes()
throws IOException {
+ BaseMetalake deletedMetalake = createAndInsertMakeLake(METALAKE);
+ createAssociation(METALAKE, "deleted_domain", "deleted_retention");
+ String survivingMetalake = METALAKE + "_surviving";
+ createAndInsertMakeLake(survivingMetalake);
+ TagEntity survivingTag =
+ createAssociation(survivingMetalake, "surviving_domain",
"surviving_retention");
+
+ backend.delete(deletedMetalake.nameIdentifier(),
Entity.EntityType.METALAKE, true);
+
+ List<RelationalEntity<?>> survivingRelations =
+ backend.batchListEntitiesByRelation(
+ SupportsRelationOperations.Type.POLICY_TAG_REL,
+ Collections.singletonList(survivingTag.nameIdentifier()),
+ Entity.EntityType.TAG);
+ Assertions.assertEquals(1, survivingRelations.size());
+ Assertions.assertEquals("surviving_retention",
survivingRelations.get(0).targetEntity().name());
+ }
+
+ @TestTemplate
+ public void testConcurrentInsertsHaveOneWinner() throws Exception {
+ createAndInsertMakeLake(METALAKE);
+ RelationEndpoints endpoints = createEndpoints(METALAKE, "domain",
"retention");
+ PolicyTagRelPO relation = newRelation(endpoints, null);
+ CyclicBarrier bothObservedAbsent = new CyclicBarrier(2);
+ ExecutorService executor = Executors.newFixedThreadPool(2);
+ List<Integer> affectedRows;
+ try {
+ Future<Integer> first = executor.submit(concurrentInsertTask(relation,
bothObservedAbsent));
+ Future<Integer> second = executor.submit(concurrentInsertTask(relation,
bothObservedAbsent));
+ affectedRows =
+ Arrays.asList(first.get(20, TimeUnit.SECONDS), second.get(20,
TimeUnit.SECONDS));
+ } finally {
+ executor.shutdownNow();
+ }
+ Collections.sort(affectedRows);
+
+ Assertions.assertEquals(Arrays.asList(0, 1), affectedRows);
+ PolicyTagRelPO persisted = getRelation(endpoints);
+ Assertions.assertNotNull(persisted);
+ Assertions.assertNotNull(persisted.getId());
+ Assertions.assertEquals(1L, persisted.getCurrentVersion());
+ Assertions.assertEquals(
+ 1L,
+ queryForLong(
+ "SELECT COUNT(*) FROM policy_tag_relation_meta WHERE policy_id = "
+ + endpoints.policy.id()
+ + " AND tag_id = "
+ + endpoints.tag.id()
+ + " AND deleted_at = 0"));
+ }
+
+ @TestTemplate
+ public void testStaleRelationDeleteAffectsNoRows() throws Exception {
+ createAndInsertMakeLake(METALAKE);
+ RelationEndpoints endpoints = createEndpoints(METALAKE, "domain",
"retention");
+ backend.updateEntityRelations(relationUpdate(endpoints, null, true));
+ PolicyTagRelPO observed = getRelation(endpoints);
+ PolicyTagRelPO replacement = copyWithVersion(observed, 2L);
+
+ Assertions.assertEquals(
+ Integer.valueOf(0),
+ SessionUtils.doWithCommitAndFetchResult(
+ PolicyTagRelMapper.class, mapper ->
mapper.softDeleteByIdAndVersion(replacement)));
+
+ PolicyTagRelPO current = getRelation(endpoints);
+ Assertions.assertEquals(
+ Integer.valueOf(1),
+ SessionUtils.doWithCommitAndFetchResult(
+ PolicyTagRelMapper.class, mapper ->
mapper.softDeleteByIdAndVersion(current)));
+ Assertions.assertEquals(
+ 2L,
+ queryForLong(
+ "SELECT current_version FROM policy_tag_relation_meta WHERE id = "
+ observed.getId()));
+ Assertions.assertEquals(
+ 2L,
+ queryForLong(
+ "SELECT last_version FROM policy_tag_relation_meta WHERE id = " +
observed.getId()));
+ }
+
+ @TestTemplate
+ public void testStaleRelationCannotDeleteRecreatedRow() throws Exception {
+ createAndInsertMakeLake(METALAKE);
+ RelationEndpoints endpoints = createEndpoints(METALAKE, "domain",
"retention");
+ backend.updateEntityRelations(relationUpdate(endpoints, null, true));
+ PolicyTagRelPO stale = getRelation(endpoints);
+
+ backend.updateEntityRelations(relationUpdate(endpoints, null, false));
+ backend.updateEntityRelations(relationUpdate(endpoints, null, true));
+ PolicyTagRelPO recreated = getRelation(endpoints);
+ Assertions.assertNotEquals(stale.getId(), recreated.getId());
+
+ Assertions.assertEquals(
+ Integer.valueOf(0),
+ SessionUtils.doWithCommitAndFetchResult(
+ PolicyTagRelMapper.class, mapper ->
mapper.softDeleteByIdAndVersion(stale)));
+
+ PolicyTagRelPO current = getRelation(endpoints);
+ Assertions.assertNotNull(current);
+ Assertions.assertEquals(recreated.getId(), current.getId());
+ }
+
+ @TestTemplate
+ public void testDeleteReAddDeleteRetainsHistory() throws Exception {
+ createAndInsertMakeLake(METALAKE);
+ RelationEndpoints endpoints = createEndpoints(METALAKE, "domain",
"retention");
+ backend.updateEntityRelations(relationUpdate(endpoints, null, true));
+ backend.updateEntityRelations(relationUpdate(endpoints, null, false));
+ backend.updateEntityRelations(relationUpdate(endpoints, null, true));
+ backend.updateEntityRelations(relationUpdate(endpoints, null, false));
+
+ String pairPredicate =
+ "policy_id = "
+ + endpoints.policy.id()
+ + " AND tag_id = "
+ + endpoints.tag.id()
+ + " AND deleted_at > 0";
+ Assertions.assertEquals(
+ 2L, queryForLong("SELECT COUNT(*) FROM policy_tag_relation_meta WHERE
" + pairPredicate));
+ }
+
+ @TestTemplate
+ public void testLegacyCleanupHonorsCutoffAndLimit() throws Exception {
+ createAndInsertMakeLake(METALAKE);
+ RelationEndpoints first = createEndpoints(METALAKE, "domain_a",
"retention_a");
+ RelationEndpoints second = createEndpoints(METALAKE, "domain_b",
"retention_b");
+ backend.updateEntityRelations(relationUpdate(first, null, true));
+ backend.updateEntityRelations(relationUpdate(second, null, true));
+ backend.updateEntityRelations(relationUpdate(first, null, false));
+ backend.updateEntityRelations(relationUpdate(second, null, false));
+ executeUpdate("UPDATE policy_tag_relation_meta SET deleted_at = 100 WHERE
deleted_at > 0");
+
+ Assertions.assertEquals(
+ 0, TagMetaService.getInstance().deleteTagMetasByLegacyTimeline(100L,
10));
+ Assertions.assertEquals(
+ 1, TagMetaService.getInstance().deleteTagMetasByLegacyTimeline(101L,
1));
+ Assertions.assertEquals(
+ 1L, queryForLong("SELECT COUNT(*) FROM policy_tag_relation_meta WHERE
deleted_at > 0"));
+ Assertions.assertEquals(
+ 1, TagMetaService.getInstance().deleteTagMetasByLegacyTimeline(101L,
10));
+ }
+
+ @TestTemplate
+ public void testRelationIdentifiersAndTypesAreValidated() throws Exception {
+ createAndInsertMakeLake(METALAKE);
+ RelationEndpoints endpoints = createEndpoints(METALAKE, "domain",
"retention");
+
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ backend.batchListEntitiesByRelation(
+ SupportsRelationOperations.Type.POLICY_TAG_REL,
+ Collections.singletonList(NameIdentifier.of("domain")),
+ Entity.EntityType.TAG));
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ backend.updateEntityRelations(
+ RelationUpdate.of(
+ SupportsRelationOperations.Type.POLICY_TAG_REL,
+ endpoints.tag.nameIdentifier(),
+ Entity.EntityType.TAG,
+ new RelationEdgeTarget[] {
+ RelationEdgeTarget.of(
+ NameIdentifierUtil.ofPolicy(METALAKE + "_other",
"retention"),
+ Entity.EntityType.POLICY,
+ null)
+ },
+ new RelationEdgeTarget[0])));
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ backend.updateEntityRelations(
+ RelationUpdate.of(
+ SupportsRelationOperations.Type.POLICY_TAG_REL,
+ endpoints.tag.nameIdentifier(),
+ Entity.EntityType.TAG,
+ new RelationEdgeTarget[] {
+ RelationEdgeTarget.of(
+ endpoints.policy.nameIdentifier(),
Entity.EntityType.TAG, null)
+ },
+ new RelationEdgeTarget[0])));
+ }
+
+ private TagEntity createAssociation(String metalake, String tagName, String
policyName)
+ throws IOException {
+ RelationEndpoints endpoints = createEndpoints(metalake, tagName,
policyName);
+ backend.updateEntityRelations(relationUpdate(endpoints, null, true));
+ return endpoints.tag;
+ }
+
+ private RelationEndpoints createEndpoints(String metalake, String tagName,
String policyName)
+ throws IOException {
+ TagEntity tag =
+ TagEntity.builder()
+ .withId(RandomIdGenerator.INSTANCE.nextId())
+ .withName(tagName)
+ .withNamespace(NamespaceUtil.ofTag(metalake))
+ .withProperties(Collections.emptyMap())
+ .withAuditInfo(AUDIT_INFO)
+ .build();
+ PolicyEntity policy =
+ createPolicy(
+ RandomIdGenerator.INSTANCE.nextId(),
+ NamespaceUtil.ofPolicy(metalake),
+ policyName,
+ AUDIT_INFO);
+ backend.insert(tag, false);
+ backend.insert(policy, false);
+ return new RelationEndpoints(tag, policy);
+ }
+
+ private RelationUpdate relationUpdate(RelationEndpoints endpoints, String
selector, boolean add) {
+ RelationEdgeTarget target =
+ RelationEdgeTarget.of(
+ endpoints.policy.nameIdentifier(), Entity.EntityType.POLICY,
selector);
+ return RelationUpdate.of(
+ SupportsRelationOperations.Type.POLICY_TAG_REL,
+ endpoints.tag.nameIdentifier(),
+ Entity.EntityType.TAG,
+ add ? new RelationEdgeTarget[] {target} : new RelationEdgeTarget[0],
+ add ? new RelationEdgeTarget[0] : new RelationEdgeTarget[] {target});
+ }
+
+ private PolicyTagRelPO getRelation(RelationEndpoints endpoints) {
+ return SessionUtils.getWithoutCommit(
+ PolicyTagRelMapper.class,
+ mapper -> mapper.getByPolicyIdAndTagId(endpoints.policy.id(),
endpoints.tag.id()));
+ }
+
+ private PolicyTagRelPO copyWithVersion(PolicyTagRelPO relation, long
version) {
+ return PolicyTagRelPO.builder()
+ .withId(relation.getId())
+ .withPolicyId(relation.getPolicyId())
+ .withTagId(relation.getTagId())
+ .withSelector(relation.getSelector())
+ .withAuditInfo(relation.getAuditInfo())
+ .withCurrentVersion(version)
+ .withLastVersion(version)
+ .withDeletedAt(0L)
+ .build();
+ }
+
+ private PolicyTagRelPO newRelation(RelationEndpoints endpoints, String
selector) {
+ return PolicyTagRelPO.builder()
+ .withPolicyId(endpoints.policy.id())
+ .withTagId(endpoints.tag.id())
+ .withSelector(selector)
+ .withAuditInfo("{}")
+ .withCurrentVersion(1L)
+ .withLastVersion(1L)
+ .withDeletedAt(0L)
+ .build();
+ }
+
+ private Callable<Integer> concurrentInsertTask(
+ PolicyTagRelPO relation, CyclicBarrier bothObservedAbsent) {
+ return () -> {
+ try (SqlSession sqlSession =
+
SqlSessionFactoryHelper.getInstance().getSqlSessionFactory().openSession(false))
{
+ PolicyTagRelMapper mapper =
sqlSession.getMapper(PolicyTagRelMapper.class);
+ PolicyTagRelPO observed =
+ mapper.getByPolicyIdAndTagId(relation.getPolicyId(),
relation.getTagId());
+ bothObservedAbsent.await(10, TimeUnit.SECONDS);
+ Assertions.assertNull(observed);
+ int inserted = mapper.insertIfAbsent(relation);
+ sqlSession.commit();
+ return inserted;
+ }
+ };
+ }
+
+ private long queryForLong(String sql) throws SQLException {
+ try (SqlSession sqlSession =
+
SqlSessionFactoryHelper.getInstance().getSqlSessionFactory().openSession(true);
+ Connection connection = sqlSession.getConnection();
+ Statement statement = connection.createStatement();
+ ResultSet resultSet = statement.executeQuery(sql)) {
+ Assertions.assertTrue(resultSet.next());
+ return resultSet.getLong(1);
+ }
+ }
+
+ private int executeUpdate(String sql) throws SQLException {
+ try (SqlSession sqlSession =
+
SqlSessionFactoryHelper.getInstance().getSqlSessionFactory().openSession(true);
+ Connection connection = sqlSession.getConnection();
+ Statement statement = connection.createStatement()) {
+ return statement.executeUpdate(sql);
+ }
+ }
+
+ private static class RelationEndpoints {
+ private final TagEntity tag;
+ private final PolicyEntity policy;
+
+ private RelationEndpoints(TagEntity tag, PolicyEntity policy) {
+ this.tag = tag;
+ this.policy = policy;
+ }
+ }
+}
diff --git a/scripts/h2/schema-2.0.0-h2.sql b/scripts/h2/schema-2.0.0-h2.sql
index ff8aaa3d50..37e9732884 100644
--- a/scripts/h2/schema-2.0.0-h2.sql
+++ b/scripts/h2/schema-2.0.0-h2.sql
@@ -431,6 +431,20 @@ CREATE TABLE IF NOT EXISTS `policy_relation_meta` (
KEY `idx_prmid` (`metadata_object_id`)
) ENGINE=InnoDB;
+CREATE TABLE IF NOT EXISTS `policy_tag_relation_meta` (
+ `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'auto increment
id',
+ `policy_id` BIGINT(20) UNSIGNED NOT NULL COMMENT 'policy id',
+ `tag_id` BIGINT(20) UNSIGNED NOT NULL COMMENT 'tag id',
+ `selector` CLOB DEFAULT NULL COMMENT 'policy tag selector JSON, NULL
matches tag presence',
+ `audit_info` CLOB NOT NULL COMMENT 'policy tag relation audit info',
+ `current_version` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 'policy tag
relation current version',
+ `last_version` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 'policy tag
relation last version',
+ `deleted_at` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'policy tag
relation deleted at',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `policy_tag_relation_meta_uk_pid_tid_del` (`policy_id`,
`tag_id`, `deleted_at`),
+ KEY `policy_tag_relation_meta_idx_tag_id` (`tag_id`)
+) ENGINE=InnoDB;
+
CREATE TABLE IF NOT EXISTS `statistic_meta` (
`id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'auto increment
id',
`statistic_id` BIGINT(20) UNSIGNED NOT NULL COMMENT 'statistic id',
diff --git a/scripts/h2/upgrade-1.3.0-to-2.0.0-h2.sql
b/scripts/h2/upgrade-1.3.0-to-2.0.0-h2.sql
index 677341c49c..19d7f054dc 100644
--- a/scripts/h2/upgrade-1.3.0-to-2.0.0-h2.sql
+++ b/scripts/h2/upgrade-1.3.0-to-2.0.0-h2.sql
@@ -41,6 +41,20 @@ CREATE INDEX IF NOT EXISTS `idx_tid_value` ON
`tag_relation_meta` (`tag_id`, `ta
ALTER TABLE `job_run_meta` ADD COLUMN `job_started_at` BIGINT(20) UNSIGNED NOT
NULL DEFAULT 0 COMMENT 'job started at' AFTER `job_run_status`;
+CREATE TABLE IF NOT EXISTS `policy_tag_relation_meta` (
+ `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'auto increment
id',
+ `policy_id` BIGINT(20) UNSIGNED NOT NULL COMMENT 'policy id',
+ `tag_id` BIGINT(20) UNSIGNED NOT NULL COMMENT 'tag id',
+ `selector` CLOB DEFAULT NULL COMMENT 'policy tag selector JSON, NULL
matches tag presence',
+ `audit_info` CLOB NOT NULL COMMENT 'policy tag relation audit info',
+ `current_version` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 'policy tag
relation current version',
+ `last_version` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 'policy tag
relation last version',
+ `deleted_at` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'policy tag
relation deleted at',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `policy_tag_relation_meta_uk_pid_tid_del` (`policy_id`,
`tag_id`, `deleted_at`),
+ KEY `policy_tag_relation_meta_idx_tag_id` (`tag_id`)
+) ENGINE=InnoDB;
+
CREATE TABLE IF NOT EXISTS `semantic_model_meta` (
`semantic_model_id` BIGINT(20) UNSIGNED NOT NULL COMMENT 'semantic model
id',
`semantic_model_name` VARCHAR(128) NOT NULL COMMENT 'semantic model name',
diff --git a/scripts/mysql/schema-2.0.0-mysql.sql
b/scripts/mysql/schema-2.0.0-mysql.sql
index 7f0c35a060..d6bdfb8f5e 100644
--- a/scripts/mysql/schema-2.0.0-mysql.sql
+++ b/scripts/mysql/schema-2.0.0-mysql.sql
@@ -422,6 +422,20 @@ CREATE TABLE IF NOT EXISTS `policy_relation_meta` (
KEY `policy_relation_meta_idx_mid` (`metadata_object_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT 'policy
metadata object relation';
+CREATE TABLE IF NOT EXISTS `policy_tag_relation_meta` (
+ `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'auto increment
id',
+ `policy_id` BIGINT(20) UNSIGNED NOT NULL COMMENT 'policy id',
+ `tag_id` BIGINT(20) UNSIGNED NOT NULL COMMENT 'tag id',
+ `selector` MEDIUMTEXT DEFAULT NULL COMMENT 'policy tag selector JSON, NULL
matches tag presence',
+ `audit_info` MEDIUMTEXT NOT NULL COMMENT 'policy tag relation audit info',
+ `current_version` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 'policy tag
relation current version',
+ `last_version` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 'policy tag
relation last version',
+ `deleted_at` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'policy tag
relation deleted at',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `policy_tag_relation_meta_uk_pid_tid_del` (`policy_id`,
`tag_id`, `deleted_at`),
+ KEY `policy_tag_relation_meta_idx_tag_id` (`tag_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT 'policy
tag relation';
+
CREATE TABLE IF NOT EXISTS `statistic_meta` (
`id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'auto increment
id',
`statistic_id` BIGINT(20) UNSIGNED NOT NULL COMMENT 'statistic id',
diff --git a/scripts/mysql/upgrade-1.3.0-to-2.0.0-mysql.sql
b/scripts/mysql/upgrade-1.3.0-to-2.0.0-mysql.sql
index 654f6bb45a..d59ef44bcc 100644
--- a/scripts/mysql/upgrade-1.3.0-to-2.0.0-mysql.sql
+++ b/scripts/mysql/upgrade-1.3.0-to-2.0.0-mysql.sql
@@ -100,6 +100,20 @@ ALTER TABLE `table_version_info`
ALTER TABLE `job_run_meta`
ADD COLUMN `job_started_at` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT
'job started at' AFTER `job_run_status`;
+CREATE TABLE IF NOT EXISTS `policy_tag_relation_meta` (
+ `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'auto increment
id',
+ `policy_id` BIGINT(20) UNSIGNED NOT NULL COMMENT 'policy id',
+ `tag_id` BIGINT(20) UNSIGNED NOT NULL COMMENT 'tag id',
+ `selector` MEDIUMTEXT DEFAULT NULL COMMENT 'policy tag selector JSON, NULL
matches tag presence',
+ `audit_info` MEDIUMTEXT NOT NULL COMMENT 'policy tag relation audit info',
+ `current_version` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 'policy tag
relation current version',
+ `last_version` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 'policy tag
relation last version',
+ `deleted_at` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'policy tag
relation deleted at',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `policy_tag_relation_meta_uk_pid_tid_del` (`policy_id`,
`tag_id`, `deleted_at`),
+ KEY `policy_tag_relation_meta_idx_tag_id` (`tag_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT 'policy
tag relation';
+
CREATE TABLE IF NOT EXISTS `semantic_model_meta` (
`semantic_model_id` BIGINT(20) UNSIGNED NOT NULL COMMENT 'semantic model
id',
`semantic_model_name` VARCHAR(128) NOT NULL COMMENT 'semantic model name',
diff --git a/scripts/postgresql/schema-2.0.0-postgresql.sql
b/scripts/postgresql/schema-2.0.0-postgresql.sql
index 28bbdf77d3..c0b0c0f6e8 100644
--- a/scripts/postgresql/schema-2.0.0-postgresql.sql
+++ b/scripts/postgresql/schema-2.0.0-postgresql.sql
@@ -750,6 +750,30 @@ COMMENT ON COLUMN policy_relation_meta.current_version IS
'policy relation curre
COMMENT ON COLUMN policy_relation_meta.last_version IS 'policy relation last
version';
COMMENT ON COLUMN policy_relation_meta.deleted_at IS 'policy relation deleted
at';
+CREATE TABLE IF NOT EXISTS policy_tag_relation_meta (
+ id BIGSERIAL NOT NULL,
+ policy_id BIGINT NOT NULL,
+ tag_id BIGINT NOT NULL,
+ selector TEXT DEFAULT NULL,
+ audit_info TEXT NOT NULL,
+ current_version INT NOT NULL DEFAULT 1,
+ last_version INT NOT NULL DEFAULT 1,
+ deleted_at BIGINT NOT NULL DEFAULT 0,
+ PRIMARY KEY (id),
+ UNIQUE (policy_id, tag_id, deleted_at)
+);
+
+CREATE INDEX IF NOT EXISTS policy_tag_relation_meta_idx_tag_id ON
policy_tag_relation_meta (tag_id);
+COMMENT ON TABLE policy_tag_relation_meta IS 'policy tag relation';
+COMMENT ON COLUMN policy_tag_relation_meta.id IS 'auto increment id';
+COMMENT ON COLUMN policy_tag_relation_meta.policy_id IS 'policy id';
+COMMENT ON COLUMN policy_tag_relation_meta.tag_id IS 'tag id';
+COMMENT ON COLUMN policy_tag_relation_meta.selector IS 'policy tag selector
JSON, NULL matches tag presence';
+COMMENT ON COLUMN policy_tag_relation_meta.audit_info IS 'policy tag relation
audit info';
+COMMENT ON COLUMN policy_tag_relation_meta.current_version IS 'policy tag
relation current version';
+COMMENT ON COLUMN policy_tag_relation_meta.last_version IS 'policy tag
relation last version';
+COMMENT ON COLUMN policy_tag_relation_meta.deleted_at IS 'policy tag relation
deleted at';
+
CREATE TABLE IF NOT EXISTS statistic_meta (
id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY,
statistic_id BIGINT NOT NULL,
diff --git a/scripts/postgresql/upgrade-1.3.0-to-2.0.0-postgresql.sql
b/scripts/postgresql/upgrade-1.3.0-to-2.0.0-postgresql.sql
index 59b4c9e7ae..ea6edd173b 100644
--- a/scripts/postgresql/upgrade-1.3.0-to-2.0.0-postgresql.sql
+++ b/scripts/postgresql/upgrade-1.3.0-to-2.0.0-postgresql.sql
@@ -49,6 +49,30 @@ CREATE INDEX IF NOT EXISTS
tag_relation_meta_idx_tag_id_value ON tag_relation_me
ALTER TABLE job_run_meta ADD COLUMN IF NOT EXISTS job_started_at BIGINT NOT
NULL DEFAULT 0;
COMMENT ON COLUMN job_run_meta.job_started_at IS 'job run started at';
+CREATE TABLE IF NOT EXISTS policy_tag_relation_meta (
+ id BIGSERIAL NOT NULL,
+ policy_id BIGINT NOT NULL,
+ tag_id BIGINT NOT NULL,
+ selector TEXT DEFAULT NULL,
+ audit_info TEXT NOT NULL,
+ current_version INT NOT NULL DEFAULT 1,
+ last_version INT NOT NULL DEFAULT 1,
+ deleted_at BIGINT NOT NULL DEFAULT 0,
+ PRIMARY KEY (id),
+ UNIQUE (policy_id, tag_id, deleted_at)
+);
+
+CREATE INDEX IF NOT EXISTS policy_tag_relation_meta_idx_tag_id ON
policy_tag_relation_meta (tag_id);
+COMMENT ON TABLE policy_tag_relation_meta IS 'policy tag relation';
+COMMENT ON COLUMN policy_tag_relation_meta.id IS 'auto increment id';
+COMMENT ON COLUMN policy_tag_relation_meta.policy_id IS 'policy id';
+COMMENT ON COLUMN policy_tag_relation_meta.tag_id IS 'tag id';
+COMMENT ON COLUMN policy_tag_relation_meta.selector IS 'policy tag selector
JSON, NULL matches tag presence';
+COMMENT ON COLUMN policy_tag_relation_meta.audit_info IS 'policy tag relation
audit info';
+COMMENT ON COLUMN policy_tag_relation_meta.current_version IS 'policy tag
relation current version';
+COMMENT ON COLUMN policy_tag_relation_meta.last_version IS 'policy tag
relation last version';
+COMMENT ON COLUMN policy_tag_relation_meta.deleted_at IS 'policy tag relation
deleted at';
+
CREATE TABLE IF NOT EXISTS semantic_model_meta (
semantic_model_id BIGINT NOT NULL,
semantic_model_name VARCHAR(128) NOT NULL,