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

yufei pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris.git


The following commit(s) were added to refs/heads/main by this push:
     new 21d1c235d Policy Store: PolicyMappingRecord with Persistence Impl 
(#1104)
21d1c235d is described below

commit 21d1c235d9b0c0fcd1ef1332e96a196f7808ca44
Author: Honah (Jonas) J. <[email protected]>
AuthorDate: Tue Apr 1 15:40:12 2025 -0500

    Policy Store: PolicyMappingRecord with Persistence Impl (#1104)
---
 .../PolarisEclipseLinkMetaStoreSessionImpl.java    |  84 +++++++
 .../impl/eclipselink/PolarisEclipseLinkStore.java  | 116 ++++++++++
 .../src/main/resources/META-INF/persistence.xml    |   1 +
 .../jpa/models/ModelPolicyMappingRecord.java       | 153 +++++++++++++
 .../AtomicOperationMetaStoreManager.java           | 156 +++++++++++++
 .../polaris/core/persistence/BasePersistence.java  |   3 +-
 .../core/persistence/PolarisMetaStoreManager.java  |   6 +-
 .../PolicyMappingAlreadyExistsException.java       |  41 ++++
 .../TransactionWorkspaceMetaStoreManager.java      |  51 +++++
 .../core/persistence/dao/entity/BaseResult.java    |   6 +
 .../dao/entity/LoadPolicyMappingsResult.java       | 102 +++++++++
 .../dao/entity/PolicyAttachmentResult.java         |  77 +++++++
 .../AbstractTransactionalPersistence.java          | 116 ++++++++++
 .../TransactionalMetaStoreManagerImpl.java         | 228 +++++++++++++++++++
 .../transactional/TransactionalPersistence.java    |   4 +-
 .../transactional/TreeMapMetaStore.java            |  42 ++++
 .../TreeMapTransactionalPersistenceImpl.java       |  83 +++++++
 .../core/policy/PolarisPolicyMappingManager.java   | 103 +++++++++
 .../core/policy/PolarisPolicyMappingRecord.java    | 215 ++++++++++++++++++
 .../core/policy/PolicyMappingPersistence.java      | 151 +++++++++++++
 .../TransactionalPolicyMappingPersistence.java     |  98 ++++++++
 .../BasePolarisMetaStoreManagerTest.java           |   6 +
 .../persistence/PolarisTestMetaStoreManager.java   | 250 +++++++++++++++++++++
 23 files changed, 2089 insertions(+), 3 deletions(-)

diff --git 
a/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreSessionImpl.java
 
b/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreSessionImpl.java
index 7005d5cfe..a8492384e 100644
--- 
a/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreSessionImpl.java
+++ 
b/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreSessionImpl.java
@@ -53,6 +53,7 @@ import 
org.apache.polaris.core.persistence.BaseMetaStoreManager;
 import org.apache.polaris.core.persistence.PrincipalSecretsGenerator;
 import org.apache.polaris.core.persistence.RetryOnConcurrencyException;
 import 
org.apache.polaris.core.persistence.transactional.AbstractTransactionalPersistence;
+import org.apache.polaris.core.policy.PolarisPolicyMappingRecord;
 import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo;
 import org.apache.polaris.core.storage.PolarisStorageIntegration;
 import org.apache.polaris.core.storage.PolarisStorageIntegrationProvider;
@@ -60,6 +61,7 @@ import org.apache.polaris.jpa.models.ModelEntity;
 import org.apache.polaris.jpa.models.ModelEntityActive;
 import org.apache.polaris.jpa.models.ModelEntityChangeTracking;
 import org.apache.polaris.jpa.models.ModelGrantRecord;
+import org.apache.polaris.jpa.models.ModelPolicyMappingRecord;
 import org.apache.polaris.jpa.models.ModelPrincipalSecrets;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -664,6 +666,88 @@ public class PolarisEclipseLinkMetaStoreSessionImpl 
extends AbstractTransactiona
     return 
storageIntegrationProvider.getStorageIntegrationForConfig(storageConfig);
   }
 
+  /** {@inheritDoc} */
+  @Override
+  public void writeToPolicyMappingRecordsInCurrentTxn(
+      @Nonnull PolarisCallContext callCtx, @Nonnull PolarisPolicyMappingRecord 
record) {
+
+    this.store.writeToPolicyMappingRecords(localSession.get(), record);
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public void deleteFromPolicyMappingRecordsInCurrentTxn(
+      @Nonnull PolarisCallContext callCtx, @Nonnull PolarisPolicyMappingRecord 
record) {
+    this.store.deleteFromPolicyMappingRecords(localSession.get(), record);
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public void deleteAllEntityPolicyMappingRecordsInCurrentTxn(
+      @Nonnull PolarisCallContext callCtx,
+      @Nonnull PolarisEntityCore entity,
+      @Nonnull List<PolarisPolicyMappingRecord> mappingOnTarget,
+      @Nonnull List<PolarisPolicyMappingRecord> mappingOnPolicy) {
+    this.store.deleteAllEntityPolicyMappingRecords(localSession.get(), entity);
+  }
+
+  /** {@inheritDoc} */
+  @Nullable
+  @Override
+  public PolarisPolicyMappingRecord lookupPolicyMappingRecordInCurrentTxn(
+      @Nonnull PolarisCallContext callCtx,
+      long targetCatalogId,
+      long targetId,
+      int policyTypeCode,
+      long policyCatalogId,
+      long policyId) {
+    return ModelPolicyMappingRecord.toPolicyMappingRecord(
+        this.store.lookupPolicyMappingRecord(
+            localSession.get(),
+            targetCatalogId,
+            targetId,
+            policyTypeCode,
+            policyCatalogId,
+            policyId));
+  }
+
+  /** {@inheritDoc} */
+  @Nonnull
+  @Override
+  public List<PolarisPolicyMappingRecord> 
loadPoliciesOnTargetByTypeInCurrentTxn(
+      @Nonnull PolarisCallContext callCtx,
+      long targetCatalogId,
+      long targetId,
+      int policyTypeCode) {
+    return this.store
+        .loadPoliciesOnTargetByType(localSession.get(), targetCatalogId, 
targetId, policyTypeCode)
+        .stream()
+        .map(ModelPolicyMappingRecord::toPolicyMappingRecord)
+        .toList();
+  }
+
+  /** {@inheritDoc} */
+  @Nonnull
+  @Override
+  public List<PolarisPolicyMappingRecord> loadAllPoliciesOnTargetInCurrentTxn(
+      @Nonnull PolarisCallContext callCtx, long targetCatalogId, long 
targetId) {
+    return this.store
+        .loadAllPoliciesOnTarget(localSession.get(), targetCatalogId, targetId)
+        .stream()
+        .map(ModelPolicyMappingRecord::toPolicyMappingRecord)
+        .toList();
+  }
+
+  /** {@inheritDoc} */
+  @Nonnull
+  @Override
+  public List<PolarisPolicyMappingRecord> loadAllTargetsOnPolicyInCurrentTxn(
+      @Nonnull PolarisCallContext callCtx, long policyCatalogId, long 
policyId) {
+    return this.store.loadAllTargetsOnPolicy(localSession.get(), 
policyCatalogId, policyId).stream()
+        .map(ModelPolicyMappingRecord::toPolicyMappingRecord)
+        .toList();
+  }
+
   @Override
   public void rollback() {
     EntityManager session = localSession.get();
diff --git 
a/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkStore.java
 
b/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkStore.java
index bfc83ae37..fc889656b 100644
--- 
a/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkStore.java
+++ 
b/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkStore.java
@@ -35,10 +35,12 @@ import org.apache.polaris.core.entity.PolarisEntityId;
 import org.apache.polaris.core.entity.PolarisEntityType;
 import org.apache.polaris.core.entity.PolarisGrantRecord;
 import org.apache.polaris.core.entity.PolarisPrincipalSecrets;
+import org.apache.polaris.core.policy.PolarisPolicyMappingRecord;
 import org.apache.polaris.jpa.models.ModelEntity;
 import org.apache.polaris.jpa.models.ModelEntityActive;
 import org.apache.polaris.jpa.models.ModelEntityChangeTracking;
 import org.apache.polaris.jpa.models.ModelGrantRecord;
+import org.apache.polaris.jpa.models.ModelPolicyMappingRecord;
 import org.apache.polaris.jpa.models.ModelPrincipalSecrets;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -411,6 +413,120 @@ public class PolarisEclipseLinkStore {
     session.remove(modelPrincipalSecrets);
   }
 
+  void writeToPolicyMappingRecords(
+      EntityManager session, PolarisPolicyMappingRecord mappingRecord) {
+    diagnosticServices.check(session != null, "session_is_null");
+    checkInitialized();
+
+    
session.persist(ModelPolicyMappingRecord.fromPolicyMappingRecord(mappingRecord));
+  }
+
+  void deleteFromPolicyMappingRecords(
+      EntityManager session, PolarisPolicyMappingRecord mappingRecord) {
+    diagnosticServices.check(session != null, "session_is_null");
+    checkInitialized();
+
+    ModelPolicyMappingRecord lookupPolicyMappingRecord =
+        lookupPolicyMappingRecord(
+            session,
+            mappingRecord.getTargetCatalogId(),
+            mappingRecord.getTargetId(),
+            mappingRecord.getPolicyTypeCode(),
+            mappingRecord.getPolicyCatalogId(),
+            mappingRecord.getPolicyId());
+
+    diagnosticServices.check(lookupPolicyMappingRecord != null, 
"policy_mapping_record_not_found");
+    session.remove(lookupPolicyMappingRecord);
+  }
+
+  void deleteAllEntityPolicyMappingRecords(EntityManager session, 
PolarisEntityCore entity) {
+    diagnosticServices.check(session != null, "session_is_null");
+    checkInitialized();
+
+    loadAllTargetsOnPolicy(session, entity.getCatalogId(), 
entity.getId()).forEach(session::remove);
+    loadAllPoliciesOnTarget(session, entity.getCatalogId(), entity.getId())
+        .forEach(session::remove);
+  }
+
+  ModelPolicyMappingRecord lookupPolicyMappingRecord(
+      EntityManager session,
+      long targetCatalogId,
+      long targetId,
+      long policyTypeCode,
+      long policyCatalogId,
+      long policyId) {
+    diagnosticServices.check(session != null, "session_is_null");
+    checkInitialized();
+
+    return session
+        .createQuery(
+            "SELECT m from ModelPolicyMappingRecord m "
+                + "where m.targetCatalogId=:targetCatalogId "
+                + "and m.targetId=:targetId "
+                + "and m.policyTypeCode=:policyTypeCode "
+                + "and m.policyCatalogId=:policyCatalogId "
+                + "and m.policyId=:policyId",
+            ModelPolicyMappingRecord.class)
+        .setParameter("targetCatalogId", targetCatalogId)
+        .setParameter("targetId", targetId)
+        .setParameter("policyTypeCode", policyTypeCode)
+        .setParameter("policyCatalogId", policyCatalogId)
+        .setParameter("policyId", policyId)
+        .getResultStream()
+        .findFirst()
+        .orElse(null);
+  }
+
+  List<ModelPolicyMappingRecord> loadPoliciesOnTargetByType(
+      EntityManager session, long targetCatalogId, long targetId, int 
policyTypeCode) {
+    diagnosticServices.check(session != null, "session_is_null");
+    checkInitialized();
+
+    return session
+        .createQuery(
+            "SELECT m from ModelPolicyMappingRecord m "
+                + "where m.targetCatalogId=:targetCatalogId "
+                + "and m.targetId=:targetId "
+                + "and m.policyTypeCode=:policyTypeCode",
+            ModelPolicyMappingRecord.class)
+        .setParameter("targetCatalogId", targetCatalogId)
+        .setParameter("targetId", targetId)
+        .setParameter("policyTypeCode", policyTypeCode)
+        .getResultList();
+  }
+
+  List<ModelPolicyMappingRecord> loadAllPoliciesOnTarget(
+      EntityManager session, long targetCatalogId, long targetId) {
+    diagnosticServices.check(session != null, "session_is_null");
+    checkInitialized();
+
+    return session
+        .createQuery(
+            "SELECT m from ModelPolicyMappingRecord m "
+                + " where m.targetCatalogId=:targetCatalogId "
+                + "and m.targetId=:targetId",
+            ModelPolicyMappingRecord.class)
+        .setParameter("targetCatalogId", targetCatalogId)
+        .setParameter("targetId", targetId)
+        .getResultList();
+  }
+
+  List<ModelPolicyMappingRecord> loadAllTargetsOnPolicy(
+      EntityManager session, long policyCatalogId, long policyId) {
+    diagnosticServices.check(session != null, "session_is_null");
+    checkInitialized();
+
+    return session
+        .createQuery(
+            "SELECT m from ModelPolicyMappingRecord m "
+                + "where  m.policyCatalogId=:policyCatalogId "
+                + "and m.policyId=:policyId",
+            ModelPolicyMappingRecord.class)
+        .setParameter("policyCatalogId", policyCatalogId)
+        .setParameter("policyId", policyId)
+        .getResultList();
+  }
+
   private void checkInitialized() {
     diagnosticServices.check(this.initialized.get(), "store_not_initialized");
   }
diff --git 
a/extension/persistence/eclipselink/src/main/resources/META-INF/persistence.xml 
b/extension/persistence/eclipselink/src/main/resources/META-INF/persistence.xml
index d435b3d33..cd8610599 100644
--- 
a/extension/persistence/eclipselink/src/main/resources/META-INF/persistence.xml
+++ 
b/extension/persistence/eclipselink/src/main/resources/META-INF/persistence.xml
@@ -29,6 +29,7 @@
     <class>org.apache.polaris.jpa.models.ModelEntityActive</class>
     <class>org.apache.polaris.jpa.models.ModelEntityChangeTracking</class>
     <class>org.apache.polaris.jpa.models.ModelGrantRecord</class>
+    <class>org.apache.polaris.jpa.models.ModelPolicyMappingRecord</class>
     <class>org.apache.polaris.jpa.models.ModelPrincipalSecrets</class>
     <class>org.apache.polaris.jpa.models.ModelSequenceId</class>
     <shared-cache-mode>NONE</shared-cache-mode>
diff --git 
a/extension/persistence/jpa-model/src/main/java/org/apache/polaris/jpa/models/ModelPolicyMappingRecord.java
 
b/extension/persistence/jpa-model/src/main/java/org/apache/polaris/jpa/models/ModelPolicyMappingRecord.java
new file mode 100644
index 000000000..122eeadb8
--- /dev/null
+++ 
b/extension/persistence/jpa-model/src/main/java/org/apache/polaris/jpa/models/ModelPolicyMappingRecord.java
@@ -0,0 +1,153 @@
+/*
+ * 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.polaris.jpa.models;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Index;
+import jakarta.persistence.Table;
+import jakarta.persistence.Version;
+import org.apache.polaris.core.policy.PolarisPolicyMappingRecord;
+
+@Entity
+@Table(
+    name = "POLICY_MAPPING_RECORDS",
+    indexes = {
+      @Index(
+          name = "POLICY_MAPPING_RECORDS_BY_POLICY_INDEX",
+          columnList = "policyCatalogId,policyId,targetCatalogId,targetId")
+    })
+public class ModelPolicyMappingRecord {
+  // id of the catalog where target entity resides
+  @Id private long targetCatalogId;
+
+  // id of the target entity
+  @Id private long targetId;
+
+  // id associated to the policy type
+  @Id private int policyTypeCode;
+
+  // id of the catalog where the policy entity resides
+  @Id private long policyCatalogId;
+
+  // id of the policy
+  @Id private long policyId;
+
+  // additional parameters of the mapping
+  private String parameters;
+
+  // Used for Optimistic Locking to handle concurrent reads and updates
+  @Version private long version;
+
+  public long getTargetCatalogId() {
+    return targetCatalogId;
+  }
+
+  public long getTargetId() {
+    return targetId;
+  }
+
+  public int getPolicyTypeCode() {
+    return policyTypeCode;
+  }
+
+  public long getPolicyCatalogId() {
+    return policyCatalogId;
+  }
+
+  public long getPolicyId() {
+    return policyId;
+  }
+
+  public String getParameters() {
+    return parameters;
+  }
+
+  public static ModelPolicyMappingRecord.Builder builder() {
+    return new ModelPolicyMappingRecord.Builder();
+  }
+
+  public static final class Builder {
+    private final ModelPolicyMappingRecord policyMappingRecord;
+
+    private Builder() {
+      policyMappingRecord = new ModelPolicyMappingRecord();
+    }
+
+    public Builder targetCatalogId(long targetCatalogId) {
+      policyMappingRecord.targetCatalogId = targetCatalogId;
+      return this;
+    }
+
+    public Builder targetId(long targetId) {
+      policyMappingRecord.targetId = targetId;
+      return this;
+    }
+
+    public Builder policyTypeCode(int policyTypeCode) {
+      policyMappingRecord.policyTypeCode = policyTypeCode;
+      return this;
+    }
+
+    public Builder policyCatalogId(long policyCatalogId) {
+      policyMappingRecord.policyCatalogId = policyCatalogId;
+      return this;
+    }
+
+    public Builder policyId(long policyId) {
+      policyMappingRecord.policyId = policyId;
+      return this;
+    }
+
+    public Builder parameters(String parameters) {
+      policyMappingRecord.parameters = parameters;
+      return this;
+    }
+
+    public ModelPolicyMappingRecord build() {
+      return policyMappingRecord;
+    }
+  }
+
+  public static ModelPolicyMappingRecord fromPolicyMappingRecord(
+      PolarisPolicyMappingRecord record) {
+    if (record == null) return null;
+
+    return ModelPolicyMappingRecord.builder()
+        .targetCatalogId(record.getTargetCatalogId())
+        .targetId(record.getTargetId())
+        .policyTypeCode(record.getPolicyTypeCode())
+        .policyCatalogId(record.getPolicyCatalogId())
+        .policyId(record.getPolicyId())
+        .parameters(record.getParameters())
+        .build();
+  }
+
+  public static PolarisPolicyMappingRecord 
toPolicyMappingRecord(ModelPolicyMappingRecord model) {
+    if (model == null) return null;
+
+    return new PolarisPolicyMappingRecord(
+        model.getTargetCatalogId(),
+        model.getTargetId(),
+        model.getPolicyCatalogId(),
+        model.getPolicyId(),
+        model.getPolicyTypeCode(),
+        model.getParameters());
+  }
+}
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/AtomicOperationMetaStoreManager.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/AtomicOperationMetaStoreManager.java
index 7dcc9f0ac..979607376 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/AtomicOperationMetaStoreManager.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/AtomicOperationMetaStoreManager.java
@@ -56,11 +56,16 @@ import 
org.apache.polaris.core.persistence.dao.entity.EntityResult;
 import org.apache.polaris.core.persistence.dao.entity.EntityWithPath;
 import org.apache.polaris.core.persistence.dao.entity.ListEntitiesResult;
 import org.apache.polaris.core.persistence.dao.entity.LoadGrantsResult;
+import org.apache.polaris.core.persistence.dao.entity.LoadPolicyMappingsResult;
+import org.apache.polaris.core.persistence.dao.entity.PolicyAttachmentResult;
 import org.apache.polaris.core.persistence.dao.entity.PrincipalSecretsResult;
 import org.apache.polaris.core.persistence.dao.entity.PrivilegeResult;
 import org.apache.polaris.core.persistence.dao.entity.ResolvedEntityResult;
 import org.apache.polaris.core.persistence.dao.entity.ScopedCredentialsResult;
 import org.apache.polaris.core.persistence.dao.entity.ValidateAccessResult;
+import org.apache.polaris.core.policy.PolarisPolicyMappingRecord;
+import org.apache.polaris.core.policy.PolicyEntity;
+import org.apache.polaris.core.policy.PolicyType;
 import org.apache.polaris.core.storage.PolarisCredentialProperty;
 import org.apache.polaris.core.storage.PolarisStorageActions;
 import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo;
@@ -1821,4 +1826,155 @@ public class AtomicOperationMetaStoreManager extends 
BaseMetaStoreManager {
     // return the result
     return new ResolvedEntityResult(entity, 
entityVersions.getGrantRecordsVersion(), grantRecords);
   }
+
+  @Override
+  public @Nonnull PolicyAttachmentResult attachPolicyToEntity(
+      @Nonnull PolarisCallContext callCtx,
+      @Nonnull List<PolarisEntityCore> targetCatalogPath,
+      @Nonnull PolarisEntityCore target,
+      @Nonnull List<PolarisEntityCore> policyCatalogPath,
+      @Nonnull PolicyEntity policy,
+      Map<String, String> parameters) {
+    // get metastore we should be using
+    BasePersistence ms = callCtx.getMetaStore();
+
+    return this.persistNewPolicyMappingRecord(callCtx, ms, target, policy, 
parameters);
+  }
+
+  @Override
+  public @Nonnull PolicyAttachmentResult detachPolicyFromEntity(
+      @Nonnull PolarisCallContext callCtx,
+      @Nonnull List<PolarisEntityCore> catalogPath,
+      @Nonnull PolarisEntityCore target,
+      @Nonnull List<PolarisEntityCore> policyCatalogPath,
+      @Nonnull PolicyEntity policy) {
+    // get metastore we should be using
+    BasePersistence ms = callCtx.getMetaStore();
+
+    PolarisPolicyMappingRecord mappingRecord =
+        ms.lookupPolicyMappingRecord(
+            callCtx,
+            target.getCatalogId(),
+            target.getId(),
+            policy.getPolicyTypeCode(),
+            policy.getCatalogId(),
+            policy.getId());
+    if (mappingRecord == null) {
+      return new 
PolicyAttachmentResult(BaseResult.ReturnStatus.POLICY_MAPPING_NOT_FOUND, null);
+    }
+
+    ms.deleteFromPolicyMappingRecords(callCtx, mappingRecord);
+
+    return new PolicyAttachmentResult(mappingRecord);
+  }
+
+  @Override
+  public @Nonnull LoadPolicyMappingsResult loadPoliciesOnEntity(
+      @Nonnull PolarisCallContext callCtx, @Nonnull PolarisEntityCore target) {
+    // get metastore we should be using
+    BasePersistence ms = callCtx.getMetaStore();
+
+    PolarisBaseEntity entity =
+        ms.lookupEntity(callCtx, target.getCatalogId(), target.getId(), 
target.getTypeCode());
+    if (entity == null) {
+      // Target entity does not exist
+      return new 
LoadPolicyMappingsResult(BaseResult.ReturnStatus.ENTITY_NOT_FOUND, null);
+    }
+
+    final List<PolarisPolicyMappingRecord> policyMappingRecords =
+        ms.loadAllPoliciesOnTarget(callCtx, target.getCatalogId(), 
target.getId());
+
+    List<PolarisBaseEntity> policyEntities =
+        loadPoliciesFromMappingRecords(callCtx, ms, policyMappingRecords);
+    return new LoadPolicyMappingsResult(policyMappingRecords, policyEntities);
+  }
+
+  @Override
+  public @Nonnull LoadPolicyMappingsResult loadPoliciesOnEntityByType(
+      @Nonnull PolarisCallContext callCtx,
+      @Nonnull PolarisEntityCore target,
+      @Nonnull PolicyType policyType) {
+    // get metastore we should be using
+    BasePersistence ms = callCtx.getMetaStore();
+
+    PolarisBaseEntity entity =
+        ms.lookupEntity(callCtx, target.getCatalogId(), target.getId(), 
target.getTypeCode());
+    if (entity == null) {
+      // Target entity does not exist
+      return new 
LoadPolicyMappingsResult(BaseResult.ReturnStatus.ENTITY_NOT_FOUND, null);
+    }
+
+    final List<PolarisPolicyMappingRecord> policyMappingRecords =
+        ms.loadPoliciesOnTargetByType(
+            callCtx, target.getCatalogId(), target.getId(), 
policyType.getCode());
+
+    List<PolarisBaseEntity> policyEntities =
+        loadPoliciesFromMappingRecords(callCtx, ms, policyMappingRecords);
+    return new LoadPolicyMappingsResult(policyMappingRecords, policyEntities);
+  }
+
+  /**
+   * Create and persist a new policy mapping record
+   *
+   * @param callCtx call context
+   * @param ms meta store in read/write mode
+   * @param target target
+   * @param policy policy
+   * @param parameters optional parameters
+   * @return new policy mapping record which was created and persisted
+   */
+  private @Nonnull PolicyAttachmentResult persistNewPolicyMappingRecord(
+      @Nonnull PolarisCallContext callCtx,
+      @Nonnull BasePersistence ms,
+      @Nonnull PolarisEntityCore target,
+      @Nonnull PolicyEntity policy,
+      Map<String, String> parameters) {
+    callCtx.getDiagServices().checkNotNull(target, "unexpected_null_target");
+    callCtx.getDiagServices().checkNotNull(policy, "unexpected_null_policy");
+
+    PolarisPolicyMappingRecord mappingRecord =
+        new PolarisPolicyMappingRecord(
+            target.getCatalogId(),
+            target.getId(),
+            policy.getCatalogId(),
+            policy.getId(),
+            policy.getPolicyTypeCode(),
+            parameters);
+    try {
+      ms.writeToPolicyMappingRecords(callCtx, mappingRecord);
+    } catch (IllegalArgumentException e) {
+      return new PolicyAttachmentResult(
+          BaseResult.ReturnStatus.UNEXPECTED_ERROR_SIGNALED, "Unknown policy 
type");
+    } catch (PolicyMappingAlreadyExistsException e) {
+      return new PolicyAttachmentResult(
+          BaseResult.ReturnStatus.POLICY_MAPPING_OF_SAME_TYPE_ALREADY_EXISTS,
+          e.getExistingRecord().getPolicyTypeCode());
+    }
+
+    return new PolicyAttachmentResult(mappingRecord);
+  }
+
+  /**
+   * Load policies from a list of policy mapping records
+   *
+   * @param callCtx call context
+   * @param ms meta store
+   * @param policyMappingRecords a list of policy mapping records
+   * @return a list of policy entities
+   */
+  private List<PolarisBaseEntity> loadPoliciesFromMappingRecords(
+      @Nonnull PolarisCallContext callCtx,
+      @Nonnull BasePersistence ms,
+      @Nonnull List<PolarisPolicyMappingRecord> policyMappingRecords) {
+    List<PolarisEntityId> policyEntityIds =
+        policyMappingRecords.stream()
+            .map(
+                policyMappingRecord ->
+                    new PolarisEntityId(
+                        policyMappingRecord.getPolicyCatalogId(),
+                        policyMappingRecord.getPolicyId()))
+            .distinct()
+            .collect(Collectors.toList());
+    return ms.lookupEntities(callCtx, policyEntityIds);
+  }
 }
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/BasePersistence.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/BasePersistence.java
index 88e5143cf..45460eb46 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/BasePersistence.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/BasePersistence.java
@@ -31,6 +31,7 @@ import org.apache.polaris.core.entity.PolarisEntityCore;
 import org.apache.polaris.core.entity.PolarisEntityId;
 import org.apache.polaris.core.entity.PolarisEntityType;
 import org.apache.polaris.core.entity.PolarisGrantRecord;
+import org.apache.polaris.core.policy.PolicyMappingPersistence;
 
 /**
  * Interface to the Polaris persistence backend, with which to persist and 
retrieve all the data
@@ -41,7 +42,7 @@ import org.apache.polaris.core.entity.PolarisGrantRecord;
  * the underlying data store. The goal is to make it really easy to back this 
using databases like
  * Postgres or simpler KV store.
  */
-public interface BasePersistence {
+public interface BasePersistence extends PolicyMappingPersistence {
   /**
    * The returned id must be fully unique within a realm and never reused once 
generated, whether or
    * not anything ends up committing an entity with the generated id.
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisMetaStoreManager.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisMetaStoreManager.java
index cc082bc27..da2ab521e 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisMetaStoreManager.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisMetaStoreManager.java
@@ -42,6 +42,7 @@ import 
org.apache.polaris.core.persistence.dao.entity.EntityWithPath;
 import org.apache.polaris.core.persistence.dao.entity.GenerateEntityIdResult;
 import org.apache.polaris.core.persistence.dao.entity.ListEntitiesResult;
 import org.apache.polaris.core.persistence.dao.entity.ResolvedEntityResult;
+import org.apache.polaris.core.policy.PolarisPolicyMappingManager;
 import org.apache.polaris.core.storage.PolarisCredentialVendor;
 
 /**
@@ -49,7 +50,10 @@ import 
org.apache.polaris.core.storage.PolarisCredentialVendor;
  * authorization. It uses the underlying persistent metastore to store and 
retrieve Polaris metadata
  */
 public interface PolarisMetaStoreManager
-    extends PolarisSecretsManager, PolarisGrantManager, 
PolarisCredentialVendor {
+    extends PolarisSecretsManager,
+        PolarisGrantManager,
+        PolarisCredentialVendor,
+        PolarisPolicyMappingManager {
 
   /**
    * Bootstrap the Polaris service, creating the root catalog, root principal, 
and associated
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolicyMappingAlreadyExistsException.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolicyMappingAlreadyExistsException.java
new file mode 100644
index 000000000..2cd714f25
--- /dev/null
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolicyMappingAlreadyExistsException.java
@@ -0,0 +1,41 @@
+/*
+ * 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.polaris.core.persistence;
+
+import org.apache.polaris.core.policy.PolarisPolicyMappingRecord;
+
+/**
+ * Exception raised when an existing policy mapping preveents the attempted 
creation of a new policy
+ * mapping record.
+ */
+public class PolicyMappingAlreadyExistsException extends RuntimeException {
+  private PolarisPolicyMappingRecord existingRecord;
+
+  /**
+   * @param existingRecord The conflicting record that caused creation to fail.
+   */
+  public PolicyMappingAlreadyExistsException(PolarisPolicyMappingRecord 
existingRecord) {
+    super("Existing Policy Mapping Record: " + existingRecord);
+    this.existingRecord = existingRecord;
+  }
+
+  public PolarisPolicyMappingRecord getExistingRecord() {
+    return this.existingRecord;
+  }
+}
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/TransactionWorkspaceMetaStoreManager.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/TransactionWorkspaceMetaStoreManager.java
index 4f26c51fa..4c3778e98 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/TransactionWorkspaceMetaStoreManager.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/TransactionWorkspaceMetaStoreManager.java
@@ -44,11 +44,15 @@ import 
org.apache.polaris.core.persistence.dao.entity.EntityWithPath;
 import org.apache.polaris.core.persistence.dao.entity.GenerateEntityIdResult;
 import org.apache.polaris.core.persistence.dao.entity.ListEntitiesResult;
 import org.apache.polaris.core.persistence.dao.entity.LoadGrantsResult;
+import org.apache.polaris.core.persistence.dao.entity.LoadPolicyMappingsResult;
+import org.apache.polaris.core.persistence.dao.entity.PolicyAttachmentResult;
 import org.apache.polaris.core.persistence.dao.entity.PrincipalSecretsResult;
 import org.apache.polaris.core.persistence.dao.entity.PrivilegeResult;
 import org.apache.polaris.core.persistence.dao.entity.ResolvedEntityResult;
 import org.apache.polaris.core.persistence.dao.entity.ScopedCredentialsResult;
 import org.apache.polaris.core.persistence.dao.entity.ValidateAccessResult;
+import org.apache.polaris.core.policy.PolicyEntity;
+import org.apache.polaris.core.policy.PolicyType;
 import org.apache.polaris.core.storage.PolarisStorageActions;
 
 /**
@@ -394,4 +398,51 @@ public class TransactionWorkspaceMetaStoreManager 
implements PolarisMetaStoreMan
         .fail("illegal_method_in_transaction_workspace", 
"refreshResolvedEntity");
     return null;
   }
+
+  @Override
+  public @Nonnull PolicyAttachmentResult attachPolicyToEntity(
+      @Nonnull PolarisCallContext callCtx,
+      @Nonnull List<PolarisEntityCore> targetCatalogPath,
+      @Nonnull PolarisEntityCore target,
+      @Nonnull List<PolarisEntityCore> policyCatalogPath,
+      @Nonnull PolicyEntity policy,
+      Map<String, String> parameters) {
+    callCtx
+        .getDiagServices()
+        .fail("illegal_method_in_transaction_workspace", 
"attachPolicyToEntity");
+    return null;
+  }
+
+  @Override
+  public @Nonnull PolicyAttachmentResult detachPolicyFromEntity(
+      @Nonnull PolarisCallContext callCtx,
+      @Nonnull List<PolarisEntityCore> catalogPath,
+      @Nonnull PolarisEntityCore target,
+      @Nonnull List<PolarisEntityCore> policyCatalogPath,
+      @Nonnull PolicyEntity policy) {
+    callCtx
+        .getDiagServices()
+        .fail("illegal_method_in_transaction_workspace", 
"detachPolicyFromEntity");
+    return null;
+  }
+
+  @Override
+  public @Nonnull LoadPolicyMappingsResult loadPoliciesOnEntity(
+      @Nonnull PolarisCallContext callCtx, @Nonnull PolarisEntityCore target) {
+    callCtx
+        .getDiagServices()
+        .fail("illegal_method_in_transaction_workspace", 
"loadPoliciesOnEntity");
+    return null;
+  }
+
+  @Override
+  public @Nonnull LoadPolicyMappingsResult loadPoliciesOnEntityByType(
+      @Nonnull PolarisCallContext callCtx,
+      @Nonnull PolarisEntityCore target,
+      @Nonnull PolicyType policyType) {
+    callCtx
+        .getDiagServices()
+        .fail("illegal_method_in_transaction_workspace", 
"loadPoliciesOnEntityByType");
+    return null;
+  }
 }
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/dao/entity/BaseResult.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/dao/entity/BaseResult.java
index b4e1757de..a4eee22cc 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/dao/entity/BaseResult.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/dao/entity/BaseResult.java
@@ -111,6 +111,12 @@ public class BaseResult {
 
     // error caught while sub-scoping credentials. Error message will be 
returned
     SUBSCOPE_CREDS_ERROR(13),
+
+    // policy mapping not found
+    POLICY_MAPPING_NOT_FOUND(14),
+
+    // policy mapping of same type already exists
+    POLICY_MAPPING_OF_SAME_TYPE_ALREADY_EXISTS(15),
     ;
 
     // code for the enum
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/dao/entity/LoadPolicyMappingsResult.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/dao/entity/LoadPolicyMappingsResult.java
new file mode 100644
index 000000000..48a0277dd
--- /dev/null
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/dao/entity/LoadPolicyMappingsResult.java
@@ -0,0 +1,102 @@
+/*
+ * 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.polaris.core.persistence.dao.entity;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.polaris.core.entity.PolarisBaseEntity;
+import org.apache.polaris.core.policy.PolarisPolicyMappingRecord;
+
+/** result of a load policy mapping call */
+public class LoadPolicyMappingsResult extends BaseResult {
+  // null if not success. Else set of policy mapping records on a target or 
from a policy
+  private final List<PolarisPolicyMappingRecord> mappingRecords;
+
+  // null if not success. Else, for each policy mapping record, list of target 
or policy entities
+  private final List<PolarisBaseEntity> entities;
+
+  /**
+   * Constructor for an error
+   *
+   * @param errorCode error code, cannot be SUCCESS
+   * @param extraInformation extra information
+   */
+  public LoadPolicyMappingsResult(
+      @Nonnull ReturnStatus errorCode, @Nullable String extraInformation) {
+    super(errorCode, extraInformation);
+    this.mappingRecords = null;
+    this.entities = null;
+  }
+
+  /**
+   * Constructor for success
+   *
+   * @param mappingRecords policy mapping records
+   * @param entities policy entities
+   */
+  public LoadPolicyMappingsResult(
+      @Nonnull List<PolarisPolicyMappingRecord> mappingRecords,
+      @Nonnull List<PolarisBaseEntity> entities) {
+    super(ReturnStatus.SUCCESS);
+    this.mappingRecords = mappingRecords;
+    this.entities = entities;
+  }
+
+  @JsonCreator
+  private LoadPolicyMappingsResult(
+      @JsonProperty("returnStatus") @Nonnull ReturnStatus returnStatus,
+      @JsonProperty("extraInformation") String extraInformation,
+      @JsonProperty("policyMappingRecords") List<PolarisPolicyMappingRecord> 
mappingRecords,
+      @JsonProperty("policyEntities") List<PolarisBaseEntity> entities) {
+    super(returnStatus, extraInformation);
+    this.mappingRecords = mappingRecords;
+    this.entities = entities;
+  }
+
+  public List<PolarisPolicyMappingRecord> getPolicyMappingRecords() {
+    return mappingRecords;
+  }
+
+  public List<PolarisBaseEntity> getEntities() {
+    return entities;
+  }
+
+  @JsonIgnore
+  public Map<Long, PolarisBaseEntity> getEntitiesAsMap() {
+    return entities == null
+        ? null
+        : entities.stream().collect(Collectors.toMap(PolarisBaseEntity::getId, 
entity -> entity));
+  }
+
+  @Override
+  public String toString() {
+    return "LoadPolicyMappingsResult{"
+        + "mappingRecords="
+        + mappingRecords
+        + ", entities="
+        + entities
+        + '}';
+  }
+}
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/dao/entity/PolicyAttachmentResult.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/dao/entity/PolicyAttachmentResult.java
new file mode 100644
index 000000000..ffc7eabdd
--- /dev/null
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/dao/entity/PolicyAttachmentResult.java
@@ -0,0 +1,77 @@
+/*
+ * 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.polaris.core.persistence.dao.entity;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import org.apache.polaris.core.policy.PolarisPolicyMappingRecord;
+
+/** result of an attach/detach operation */
+public class PolicyAttachmentResult extends BaseResult {
+  // null if not success
+  private final PolarisPolicyMappingRecord mappingRecord;
+
+  /**
+   * Constructor for an error
+   *
+   * @param errorStatus error code, cannot be SUCCESS
+   * @param extraInformation extra information
+   */
+  public PolicyAttachmentResult(
+      @Nonnull ReturnStatus errorStatus, @Nullable String extraInformation) {
+    super(errorStatus, extraInformation);
+    this.mappingRecord = null;
+  }
+
+  /**
+   * Constructor for an error
+   *
+   * @param errorStatus error code, cannot be SUCCESS
+   * @param policyTypeCode existing policy mapping record's policy type code
+   */
+  public PolicyAttachmentResult(@Nonnull ReturnStatus errorStatus, int 
policyTypeCode) {
+    super(errorStatus, Integer.toString(policyTypeCode));
+    this.mappingRecord = null;
+  }
+
+  /**
+   * Constructor for success
+   *
+   * @param mappingRecord policy mapping record being attached/detached
+   */
+  public PolicyAttachmentResult(@Nonnull PolarisPolicyMappingRecord 
mappingRecord) {
+    super(ReturnStatus.SUCCESS);
+    this.mappingRecord = mappingRecord;
+  }
+
+  @JsonCreator
+  private PolicyAttachmentResult(
+      @JsonProperty("returnStatus") @Nonnull ReturnStatus returnStatus,
+      @JsonProperty("extraInformation") String extraInformation,
+      @JsonProperty("policyMappingRecord") PolarisPolicyMappingRecord 
mappingRecord) {
+    super(returnStatus, extraInformation);
+    this.mappingRecord = mappingRecord;
+  }
+
+  public PolarisPolicyMappingRecord getPolicyMappingRecord() {
+    return mappingRecord;
+  }
+}
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/AbstractTransactionalPersistence.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/AbstractTransactionalPersistence.java
index c673c4849..e949b33fe 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/AbstractTransactionalPersistence.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/AbstractTransactionalPersistence.java
@@ -18,6 +18,7 @@
  */
 package org.apache.polaris.core.persistence.transactional;
 
+import com.google.common.base.Preconditions;
 import jakarta.annotation.Nonnull;
 import jakarta.annotation.Nullable;
 import java.util.List;
@@ -34,7 +35,10 @@ import org.apache.polaris.core.entity.PolarisEntityType;
 import org.apache.polaris.core.entity.PolarisGrantRecord;
 import org.apache.polaris.core.entity.PolarisPrincipalSecrets;
 import org.apache.polaris.core.persistence.EntityAlreadyExistsException;
+import org.apache.polaris.core.persistence.PolicyMappingAlreadyExistsException;
 import org.apache.polaris.core.persistence.RetryOnConcurrencyException;
+import org.apache.polaris.core.policy.PolarisPolicyMappingRecord;
+import org.apache.polaris.core.policy.PolicyType;
 import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo;
 import org.apache.polaris.core.storage.PolarisStorageIntegration;
 
@@ -660,4 +664,116 @@ public abstract class AbstractTransactionalPersistence 
implements TransactionalP
         new PolarisEntitiesActiveKey(catalogId, parentId, typeCode, name);
     return this.lookupEntityActiveInCurrentTxn(callCtx, entityActiveKey);
   }
+
+  /** {@inheritDoc} */
+  @Override
+  public void writeToPolicyMappingRecords(
+      @Nonnull PolarisCallContext callCtx, @Nonnull PolarisPolicyMappingRecord 
record) {
+    this.runActionInTransaction(
+        callCtx,
+        () -> {
+          
this.checkConditionsForWriteToPolicyMappingRecordsInCurrentTxn(callCtx, record);
+          this.writeToPolicyMappingRecordsInCurrentTxn(callCtx, record);
+        });
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public void checkConditionsForWriteToPolicyMappingRecordsInCurrentTxn(
+      @Nonnull PolarisCallContext callCtx, @Nonnull PolarisPolicyMappingRecord 
record) {
+
+    PolicyType policyType = PolicyType.fromCode(record.getPolicyTypeCode());
+    Preconditions.checkArgument(
+        policyType != null, "Invalid policy type code: %s", 
record.getPolicyTypeCode());
+
+    if (!policyType.isInheritable()) {
+      return;
+    }
+
+    List<PolarisPolicyMappingRecord> existingRecords =
+        this.loadPoliciesOnTargetByTypeInCurrentTxn(
+            callCtx, record.getTargetCatalogId(), record.getTargetId(), 
record.getPolicyTypeCode());
+    if (existingRecords.size() > 1) {
+      throw new PolicyMappingAlreadyExistsException(existingRecords.get(0));
+    } else if (existingRecords.size() == 1) {
+      PolarisPolicyMappingRecord existingRecord = existingRecords.get(0);
+      if (existingRecord.getPolicyCatalogId() != record.getPolicyCatalogId()
+          || existingRecord.getPolicyId() != record.getPolicyId()) {
+        throw new PolicyMappingAlreadyExistsException(existingRecord);
+      }
+    }
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public void deleteFromPolicyMappingRecords(
+      @Nonnull PolarisCallContext callCtx, @Nonnull PolarisPolicyMappingRecord 
record) {
+    this.runActionInTransaction(
+        callCtx, () -> 
this.deleteFromPolicyMappingRecordsInCurrentTxn(callCtx, record));
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public void deleteAllEntityPolicyMappingRecords(
+      @Nonnull PolarisCallContext callCtx,
+      @Nonnull PolarisEntityCore entity,
+      @Nonnull List<PolarisPolicyMappingRecord> mappingOnTarget,
+      @Nonnull List<PolarisPolicyMappingRecord> mappingOnPolicy) {
+    this.runActionInTransaction(
+        callCtx,
+        () ->
+            this.deleteAllEntityPolicyMappingRecordsInCurrentTxn(
+                callCtx, entity, mappingOnTarget, mappingOnPolicy));
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  @Nullable
+  public PolarisPolicyMappingRecord lookupPolicyMappingRecord(
+      @Nonnull PolarisCallContext callCtx,
+      long targetCatalogId,
+      long targetId,
+      int policyTypeCode,
+      long policyCatalogId,
+      long policyId) {
+    return this.runInReadTransaction(
+        callCtx,
+        () ->
+            this.lookupPolicyMappingRecordInCurrentTxn(
+                callCtx, targetCatalogId, targetId, policyTypeCode, 
policyCatalogId, policyId));
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  @Nonnull
+  public List<PolarisPolicyMappingRecord> loadPoliciesOnTargetByType(
+      @Nonnull PolarisCallContext callCtx,
+      long targetCatalogId,
+      long targetId,
+      int policyTypeCode) {
+    return this.runInReadTransaction(
+        callCtx,
+        () ->
+            this.loadPoliciesOnTargetByTypeInCurrentTxn(
+                callCtx, targetCatalogId, targetId, policyTypeCode));
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  @Nonnull
+  public List<PolarisPolicyMappingRecord> loadAllPoliciesOnTarget(
+      @Nonnull PolarisCallContext callCtx, long targetCatalogId, long 
targetId) {
+    return this.runInReadTransaction(
+        callCtx,
+        () -> this.loadAllPoliciesOnTargetInCurrentTxn(callCtx, 
targetCatalogId, targetId));
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  @Nonnull
+  public List<PolarisPolicyMappingRecord> loadAllTargetsOnPolicy(
+      @Nonnull PolarisCallContext callCtx, long policyCatalogId, long 
policyId) {
+    return this.runInReadTransaction(
+        callCtx, () -> this.loadAllTargetsOnPolicyInCurrentTxn(callCtx, 
policyCatalogId, policyId));
+  }
 }
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TransactionalMetaStoreManagerImpl.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TransactionalMetaStoreManagerImpl.java
index cf1e54b53..27b049e91 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TransactionalMetaStoreManagerImpl.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TransactionalMetaStoreManagerImpl.java
@@ -57,11 +57,16 @@ import 
org.apache.polaris.core.persistence.dao.entity.EntityResult;
 import org.apache.polaris.core.persistence.dao.entity.EntityWithPath;
 import org.apache.polaris.core.persistence.dao.entity.ListEntitiesResult;
 import org.apache.polaris.core.persistence.dao.entity.LoadGrantsResult;
+import org.apache.polaris.core.persistence.dao.entity.LoadPolicyMappingsResult;
+import org.apache.polaris.core.persistence.dao.entity.PolicyAttachmentResult;
 import org.apache.polaris.core.persistence.dao.entity.PrincipalSecretsResult;
 import org.apache.polaris.core.persistence.dao.entity.PrivilegeResult;
 import org.apache.polaris.core.persistence.dao.entity.ResolvedEntityResult;
 import org.apache.polaris.core.persistence.dao.entity.ScopedCredentialsResult;
 import org.apache.polaris.core.persistence.dao.entity.ValidateAccessResult;
+import org.apache.polaris.core.policy.PolarisPolicyMappingRecord;
+import org.apache.polaris.core.policy.PolicyEntity;
+import org.apache.polaris.core.policy.PolicyType;
 import org.apache.polaris.core.storage.PolarisCredentialProperty;
 import org.apache.polaris.core.storage.PolarisStorageActions;
 import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo;
@@ -2316,4 +2321,227 @@ public class TransactionalMetaStoreManagerImpl extends 
BaseMetaStoreManager {
                 entityCatalogId,
                 entityId));
   }
+
+  /** {@inheritDoc} */
+  @Override
+  public @Nonnull PolicyAttachmentResult attachPolicyToEntity(
+      @Nonnull PolarisCallContext callCtx,
+      @Nonnull List<PolarisEntityCore> targetCatalogPath,
+      @Nonnull PolarisEntityCore target,
+      @Nonnull List<PolarisEntityCore> policyCatalogPath,
+      @Nonnull PolicyEntity policy,
+      Map<String, String> parameters) {
+    // get metastore we should be using
+    TransactionalPersistence ms = ((TransactionalPersistence) 
callCtx.getMetaStore());
+
+    return ms.runInTransaction(
+        callCtx,
+        () ->
+            this.doAttachPolicyToEntity(
+                callCtx, ms, targetCatalogPath, target, policyCatalogPath, 
policy, parameters));
+  }
+
+  /**
+   * See {@link #attachPolicyToEntity(PolarisCallContext, List, 
PolarisEntityCore, List,
+   * PolicyEntity, Map)}
+   */
+  private @Nonnull PolicyAttachmentResult doAttachPolicyToEntity(
+      @Nonnull PolarisCallContext callCtx,
+      @Nonnull TransactionalPersistence ms,
+      @Nonnull List<PolarisEntityCore> targetCatalogPath,
+      @Nonnull PolarisEntityCore target,
+      @Nonnull List<PolarisEntityCore> policyCatalogPath,
+      @Nonnull PolicyEntity policy,
+      Map<String, String> parameters) {
+    PolarisEntityResolver targetResolver =
+        new PolarisEntityResolver(callCtx, ms, targetCatalogPath, target);
+    PolarisEntityResolver policyResolver =
+        new PolarisEntityResolver(callCtx, ms, policyCatalogPath, policy);
+    if (targetResolver.isFailure() || policyResolver.isFailure()) {
+      return new 
PolicyAttachmentResult(BaseResult.ReturnStatus.ENTITY_CANNOT_BE_RESOLVED, null);
+    }
+
+    return this.persistNewPolicyMappingRecord(callCtx, ms, target, policy, 
parameters);
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public @Nonnull PolicyAttachmentResult detachPolicyFromEntity(
+      @Nonnull PolarisCallContext callCtx,
+      @Nonnull List<PolarisEntityCore> targetCatalogPath,
+      @Nonnull PolarisEntityCore target,
+      @Nonnull List<PolarisEntityCore> policyCatalogPath,
+      @Nonnull PolicyEntity policy) {
+    TransactionalPersistence ms = ((TransactionalPersistence) 
callCtx.getMetaStore());
+    return ms.runInTransaction(
+        callCtx,
+        () ->
+            this.doDetachPolicyFromEntity(
+                callCtx, ms, targetCatalogPath, target, policyCatalogPath, 
policy));
+  }
+
+  /**
+   * See {@link #detachPolicyFromEntity(PolarisCallContext, List, 
PolarisEntityCore,
+   * List,PolicyEntity)}
+   */
+  private PolicyAttachmentResult doDetachPolicyFromEntity(
+      @Nonnull PolarisCallContext callCtx,
+      @Nonnull TransactionalPersistence ms,
+      @Nonnull List<PolarisEntityCore> targetCatalogPath,
+      @Nonnull PolarisEntityCore target,
+      @Nonnull List<PolarisEntityCore> policyCatalogPath,
+      @Nonnull PolicyEntity policy) {
+    PolarisEntityResolver targetResolver =
+        new PolarisEntityResolver(callCtx, ms, targetCatalogPath, target);
+    PolarisEntityResolver policyResolver =
+        new PolarisEntityResolver(callCtx, ms, policyCatalogPath, policy);
+    if (targetResolver.isFailure() || policyResolver.isFailure()) {
+      return new 
PolicyAttachmentResult(BaseResult.ReturnStatus.ENTITY_CANNOT_BE_RESOLVED, null);
+    }
+
+    PolarisPolicyMappingRecord mappingRecord =
+        ms.lookupPolicyMappingRecordInCurrentTxn(
+            callCtx,
+            target.getCatalogId(),
+            target.getId(),
+            policy.getPolicyTypeCode(),
+            policy.getCatalogId(),
+            policy.getId());
+    if (mappingRecord == null) {
+      return new 
PolicyAttachmentResult(BaseResult.ReturnStatus.POLICY_MAPPING_NOT_FOUND, null);
+    }
+
+    ms.deleteFromPolicyMappingRecordsInCurrentTxn(callCtx, mappingRecord);
+
+    return new PolicyAttachmentResult(mappingRecord);
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public @Nonnull LoadPolicyMappingsResult loadPoliciesOnEntity(
+      @Nonnull PolarisCallContext callCtx, @Nonnull PolarisEntityCore target) {
+    TransactionalPersistence ms = ((TransactionalPersistence) 
callCtx.getMetaStore());
+    return ms.runInReadTransaction(callCtx, () -> 
this.doLoadPoliciesOnEntity(callCtx, ms, target));
+  }
+
+  /** See {@link #loadPoliciesOnEntity(PolarisCallContext, PolarisEntityCore)} 
*/
+  private LoadPolicyMappingsResult doLoadPoliciesOnEntity(
+      @Nonnull PolarisCallContext callCtx,
+      @Nonnull TransactionalPersistence ms,
+      @Nonnull PolarisEntityCore target) {
+    PolarisBaseEntity entity =
+        ms.lookupEntityInCurrentTxn(
+            callCtx, target.getCatalogId(), target.getId(), 
target.getTypeCode());
+    if (entity == null) {
+      // Target entity does not exists
+      return new 
LoadPolicyMappingsResult(BaseResult.ReturnStatus.ENTITY_NOT_FOUND, null);
+    }
+
+    final List<PolarisPolicyMappingRecord> policyMappingRecords =
+        ms.loadAllPoliciesOnTargetInCurrentTxn(callCtx, target.getCatalogId(), 
target.getId());
+
+    List<PolarisBaseEntity> policyEntities =
+        loadPoliciesFromMappingRecords(callCtx, ms, policyMappingRecords);
+    return new LoadPolicyMappingsResult(policyMappingRecords, policyEntities);
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public @Nonnull LoadPolicyMappingsResult loadPoliciesOnEntityByType(
+      @Nonnull PolarisCallContext callCtx,
+      @Nonnull PolarisEntityCore target,
+      @Nonnull PolicyType policyType) {
+    TransactionalPersistence ms = ((TransactionalPersistence) 
callCtx.getMetaStore());
+    return ms.runInReadTransaction(
+        callCtx, () -> this.doLoadPoliciesOnEntityByType(callCtx, ms, target, 
policyType));
+  }
+
+  /** See {@link #loadPoliciesOnEntityByType(PolarisCallContext, 
PolarisEntityCore, PolicyType)} */
+  public LoadPolicyMappingsResult doLoadPoliciesOnEntityByType(
+      @Nonnull PolarisCallContext callCtx,
+      @Nonnull TransactionalPersistence ms,
+      @Nonnull PolarisEntityCore target,
+      @Nonnull PolicyType policyType) {
+    PolarisBaseEntity entity =
+        ms.lookupEntityInCurrentTxn(
+            callCtx, target.getCatalogId(), target.getId(), 
target.getTypeCode());
+    if (entity == null) {
+      // Target entity does not exists
+      return new 
LoadPolicyMappingsResult(BaseResult.ReturnStatus.ENTITY_NOT_FOUND, null);
+    }
+
+    final List<PolarisPolicyMappingRecord> policyMappingRecords =
+        ms.loadPoliciesOnTargetByTypeInCurrentTxn(
+            callCtx, target.getCatalogId(), target.getId(), 
policyType.getCode());
+    List<PolarisBaseEntity> policyEntities =
+        loadPoliciesFromMappingRecords(callCtx, ms, policyMappingRecords);
+    return new LoadPolicyMappingsResult(policyMappingRecords, policyEntities);
+  }
+
+  /**
+   * Create and persist a new policy mapping record
+   *
+   * @param callCtx call context
+   * @param ms meta store in read/write mode
+   * @param target target
+   * @param policy policy
+   * @param parameters optional parameters
+   * @return new policy mapping record which was created and persisted
+   */
+  private @Nonnull PolicyAttachmentResult persistNewPolicyMappingRecord(
+      @Nonnull PolarisCallContext callCtx,
+      @Nonnull TransactionalPersistence ms,
+      @Nonnull PolarisEntityCore target,
+      @Nonnull PolicyEntity policy,
+      Map<String, String> parameters) {
+    callCtx.getDiagServices().checkNotNull(target, "unexpected_null_target");
+    callCtx.getDiagServices().checkNotNull(policy, "unexpected_null_policy");
+
+    PolarisPolicyMappingRecord mappingRecord =
+        new PolarisPolicyMappingRecord(
+            target.getCatalogId(),
+            target.getId(),
+            policy.getCatalogId(),
+            policy.getId(),
+            policy.getPolicyTypeCode(),
+            parameters);
+
+    try {
+      ms.checkConditionsForWriteToPolicyMappingRecordsInCurrentTxn(callCtx, 
mappingRecord);
+      ms.writeToPolicyMappingRecordsInCurrentTxn(callCtx, mappingRecord);
+    } catch (IllegalArgumentException e) {
+      return new PolicyAttachmentResult(
+          BaseResult.ReturnStatus.UNEXPECTED_ERROR_SIGNALED, "Unknown policy 
type");
+    } catch (PolicyMappingAlreadyExistsException e) {
+      return new PolicyAttachmentResult(
+          BaseResult.ReturnStatus.POLICY_MAPPING_OF_SAME_TYPE_ALREADY_EXISTS,
+          e.getExistingRecord().getPolicyTypeCode());
+    }
+
+    return new PolicyAttachmentResult(mappingRecord);
+  }
+
+  /**
+   * Load policies from a list of policy mapping records
+   *
+   * @param callCtx call context
+   * @param ms meta store
+   * @param policyMappingRecords a list of policy mapping records
+   * @return a list of policy entities
+   */
+  private List<PolarisBaseEntity> loadPoliciesFromMappingRecords(
+      @Nonnull PolarisCallContext callCtx,
+      @Nonnull TransactionalPersistence ms,
+      @Nonnull List<PolarisPolicyMappingRecord> policyMappingRecords) {
+    List<PolarisEntityId> policyEntityIds =
+        policyMappingRecords.stream()
+            .map(
+                policyMappingRecord ->
+                    new PolarisEntityId(
+                        policyMappingRecord.getPolicyCatalogId(),
+                        policyMappingRecord.getPolicyId()))
+            .distinct()
+            .collect(Collectors.toList());
+    return ms.lookupEntitiesInCurrentTxn(callCtx, policyEntityIds);
+  }
 }
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TransactionalPersistence.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TransactionalPersistence.java
index f3d3842bb..2057991db 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TransactionalPersistence.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TransactionalPersistence.java
@@ -36,6 +36,7 @@ import org.apache.polaris.core.entity.PolarisGrantRecord;
 import org.apache.polaris.core.entity.PolarisPrincipalSecrets;
 import org.apache.polaris.core.persistence.BasePersistence;
 import org.apache.polaris.core.persistence.IntegrationPersistence;
+import org.apache.polaris.core.policy.TransactionalPolicyMappingPersistence;
 import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo;
 import org.apache.polaris.core.storage.PolarisStorageIntegration;
 
@@ -44,7 +45,8 @@ import 
org.apache.polaris.core.storage.PolarisStorageIntegration;
  * which can support a runInTransaction semantic, while providing default 
implementations of some of
  * the BasePersistence methods in terms of lower-level methods that subclasses 
must implement.
  */
-public interface TransactionalPersistence extends BasePersistence, 
IntegrationPersistence {
+public interface TransactionalPersistence
+    extends BasePersistence, IntegrationPersistence, 
TransactionalPolicyMappingPersistence {
 
   /**
    * Run the specified transaction code (a Supplier lambda type) in a database 
read/write
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TreeMapMetaStore.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TreeMapMetaStore.java
index b37f6e840..c91414040 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TreeMapMetaStore.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TreeMapMetaStore.java
@@ -31,6 +31,7 @@ import org.apache.polaris.core.entity.PolarisBaseEntity;
 import org.apache.polaris.core.entity.PolarisEntityCore;
 import org.apache.polaris.core.entity.PolarisGrantRecord;
 import org.apache.polaris.core.entity.PolarisPrincipalSecrets;
+import org.apache.polaris.core.policy.PolarisPolicyMappingRecord;
 
 /** Implements a simple in-memory store for Polaris, using tree-map */
 public class TreeMapMetaStore {
@@ -209,6 +210,10 @@ public class TreeMapMetaStore {
   // slice to store principal secrets
   private final Slice<PolarisPrincipalSecrets> slicePrincipalSecrets;
 
+  private final Slice<PolarisPolicyMappingRecord> slicePolicyMappingRecords;
+
+  private final Slice<PolarisPolicyMappingRecord> 
slicePolicyMappingRecordsByPolicy;
+
   // next id generator
   private final AtomicLong nextId = new AtomicLong();
 
@@ -266,6 +271,29 @@ public class TreeMapMetaStore {
             principalSecrets -> String.format("%s", 
principalSecrets.getPrincipalClientId()),
             PolarisPrincipalSecrets::new);
 
+    this.slicePolicyMappingRecords =
+        new Slice<>(
+            policyMappingRecord ->
+                String.format(
+                    "%d::%d::%d::%d::%d",
+                    policyMappingRecord.getTargetCatalogId(),
+                    policyMappingRecord.getTargetId(),
+                    policyMappingRecord.getPolicyTypeCode(),
+                    policyMappingRecord.getPolicyCatalogId(),
+                    policyMappingRecord.getPolicyId()),
+            PolarisPolicyMappingRecord::new);
+
+    this.slicePolicyMappingRecordsByPolicy =
+        new Slice<>(
+            policyMappingRecord ->
+                String.format(
+                    "%d::%d::%d::%d",
+                    policyMappingRecord.getPolicyCatalogId(),
+                    policyMappingRecord.getPolicyId(),
+                    policyMappingRecord.getTargetCatalogId(),
+                    policyMappingRecord.getTargetId()),
+            PolarisPolicyMappingRecord::new);
+
     // no transaction open yet
     this.diagnosticServices = diagnostics;
     this.tr = null;
@@ -345,6 +373,8 @@ public class TreeMapMetaStore {
     this.sliceGrantRecords.startWriteTransaction();
     this.sliceGrantRecordsByGrantee.startWriteTransaction();
     this.slicePrincipalSecrets.startWriteTransaction();
+    this.slicePolicyMappingRecords.startWriteTransaction();
+    this.slicePolicyMappingRecordsByPolicy.startWriteTransaction();
   }
 
   /** Rollback transaction */
@@ -355,6 +385,8 @@ public class TreeMapMetaStore {
     this.sliceGrantRecords.rollback();
     this.sliceGrantRecordsByGrantee.rollback();
     this.slicePrincipalSecrets.rollback();
+    this.slicePolicyMappingRecords.rollback();
+    this.slicePolicyMappingRecordsByPolicy.rollback();
   }
 
   /** Ensure that a read/write FDB transaction has been started */
@@ -497,6 +529,14 @@ public class TreeMapMetaStore {
     return slicePrincipalSecrets;
   }
 
+  public Slice<PolarisPolicyMappingRecord> getSlicePolicyMappingRecords() {
+    return slicePolicyMappingRecords;
+  }
+
+  public Slice<PolarisPolicyMappingRecord> 
getSlicePolicyMappingRecordsByPolicy() {
+    return slicePolicyMappingRecordsByPolicy;
+  }
+
   /**
    * Next sequence number generator
    *
@@ -516,5 +556,7 @@ public class TreeMapMetaStore {
     this.sliceGrantRecordsByGrantee.deleteAll();
     this.sliceGrantRecords.deleteAll();
     this.slicePrincipalSecrets.deleteAll();
+    this.slicePolicyMappingRecords.deleteAll();
+    this.slicePolicyMappingRecordsByPolicy.deleteAll();
   }
 }
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TreeMapTransactionalPersistenceImpl.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TreeMapTransactionalPersistenceImpl.java
index 8d2e59c96..822045673 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TreeMapTransactionalPersistenceImpl.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TreeMapTransactionalPersistenceImpl.java
@@ -38,6 +38,7 @@ import org.apache.polaris.core.entity.PolarisGrantRecord;
 import org.apache.polaris.core.entity.PolarisPrincipalSecrets;
 import org.apache.polaris.core.persistence.BaseMetaStoreManager;
 import org.apache.polaris.core.persistence.PrincipalSecretsGenerator;
+import org.apache.polaris.core.policy.PolarisPolicyMappingRecord;
 import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo;
 import org.apache.polaris.core.storage.PolarisStorageIntegration;
 import org.apache.polaris.core.storage.PolarisStorageIntegrationProvider;
@@ -551,4 +552,86 @@ public class TreeMapTransactionalPersistenceImpl extends 
AbstractTransactionalPe
   public void rollback() {
     this.store.rollback();
   }
+
+  /** {@inheritDoc} */
+  @Override
+  public void writeToPolicyMappingRecordsInCurrentTxn(
+      @Nonnull PolarisCallContext callCtx, @Nonnull PolarisPolicyMappingRecord 
record) {
+    this.store.getSlicePolicyMappingRecords().write(record);
+    this.store.getSlicePolicyMappingRecordsByPolicy().write(record);
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public void deleteFromPolicyMappingRecordsInCurrentTxn(
+      @Nonnull PolarisCallContext callCtx, @Nonnull PolarisPolicyMappingRecord 
record) {
+    this.store.getSlicePolicyMappingRecords().delete(record);
+    this.store.getSlicePolicyMappingRecordsByPolicy().delete(record);
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public void deleteAllEntityPolicyMappingRecordsInCurrentTxn(
+      @Nonnull PolarisCallContext callCtx,
+      @Nonnull PolarisEntityCore entity,
+      @Nonnull List<PolarisPolicyMappingRecord> mappingOnTarget,
+      @Nonnull List<PolarisPolicyMappingRecord> mappingOnPolicy) {
+    // build composite prefix key and delete policy mapping records on the 
indexed side of each
+    // mapping table
+    String prefix = this.store.buildPrefixKeyComposite(entity.getCatalogId(), 
entity.getId());
+    this.store.getSlicePolicyMappingRecords().deleteRange(prefix);
+    this.store.getSlicePolicyMappingRecordsByPolicy().deleteRange(prefix);
+
+    // also delete the other side. We need to delete these mapping one at a 
time versus doing a
+    // range delete
+    mappingOnTarget.forEach(record -> 
this.store.getSlicePolicyMappingRecords().delete(record));
+    mappingOnPolicy.forEach(
+        record -> 
this.store.getSlicePolicyMappingRecordsByPolicy().delete(record));
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public @Nullable PolarisPolicyMappingRecord 
lookupPolicyMappingRecordInCurrentTxn(
+      @Nonnull PolarisCallContext callCtx,
+      long targetCatalogId,
+      long targetId,
+      int policyTypeCode,
+      long policyCatalogId,
+      long policyId) {
+    return this.store
+        .getSlicePolicyMappingRecords()
+        .read(
+            this.store.buildKeyComposite(
+                targetCatalogId, targetId, policyTypeCode, policyCatalogId, 
policyId));
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public @Nonnull List<PolarisPolicyMappingRecord> 
loadPoliciesOnTargetByTypeInCurrentTxn(
+      @Nonnull PolarisCallContext callCtx,
+      long targetCatalogId,
+      long targetId,
+      int policyTypeCode) {
+    return this.store
+        .getSlicePolicyMappingRecords()
+        .readRange(this.store.buildPrefixKeyComposite(targetCatalogId, 
targetId, policyTypeCode));
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public @Nonnull List<PolarisPolicyMappingRecord> 
loadAllPoliciesOnTargetInCurrentTxn(
+      @Nonnull PolarisCallContext callCtx, long targetCatalogId, long 
targetId) {
+    return this.store
+        .getSlicePolicyMappingRecords()
+        .readRange(this.store.buildPrefixKeyComposite(targetCatalogId, 
targetId));
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public @Nonnull List<PolarisPolicyMappingRecord> 
loadAllTargetsOnPolicyInCurrentTxn(
+      @Nonnull PolarisCallContext callCtx, long policyCatalogId, long 
policyId) {
+    return this.store
+        .getSlicePolicyMappingRecordsByPolicy()
+        .readRange(this.store.buildPrefixKeyComposite(policyCatalogId, 
policyId));
+  }
 }
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/policy/PolarisPolicyMappingManager.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/policy/PolarisPolicyMappingManager.java
new file mode 100644
index 000000000..266d5477e
--- /dev/null
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/policy/PolarisPolicyMappingManager.java
@@ -0,0 +1,103 @@
+/*
+ * 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.polaris.core.policy;
+
+import jakarta.annotation.Nonnull;
+import java.util.List;
+import java.util.Map;
+import org.apache.polaris.core.PolarisCallContext;
+import org.apache.polaris.core.entity.PolarisEntityCore;
+import org.apache.polaris.core.persistence.dao.entity.LoadPolicyMappingsResult;
+import org.apache.polaris.core.persistence.dao.entity.PolicyAttachmentResult;
+
+public interface PolarisPolicyMappingManager {
+
+  /**
+   * Attach a policy to a target entity, for example attach a policy to a 
table.
+   *
+   * <p>For inheritable policy, only one policy of the same type can be 
attached to the target. For
+   * non-inheritable policy, multiple policies of the same type can be 
attached to the target.
+   *
+   * @param callCtx call context
+   * @param targetCatalogPath path to the target entity
+   * @param target target entity
+   * @param policyCatalogPath path to the policy entity
+   * @param policy policy entity
+   * @param parameters additional parameters for the attachment
+   * @return The policy mapping record we created for this attachment. Will 
return ENTITY_NOT_FOUND
+   *     if the specified target or policy does not exist. Will return
+   *     POLICY_OF_SAME_TYPE_ALREADY_ATTACHED if the target already has a 
policy of the same type
+   *     attached and the policy is inheritable.
+   */
+  @Nonnull
+  PolicyAttachmentResult attachPolicyToEntity(
+      @Nonnull PolarisCallContext callCtx,
+      @Nonnull List<PolarisEntityCore> targetCatalogPath,
+      @Nonnull PolarisEntityCore target,
+      @Nonnull List<PolarisEntityCore> policyCatalogPath,
+      @Nonnull PolicyEntity policy,
+      Map<String, String> parameters);
+
+  /**
+   * Detach a policy from a target entity
+   *
+   * @param callCtx call context
+   * @param catalogPath path to the target entity
+   * @param target target entity
+   * @param policyCatalogPath path to the policy entity
+   * @param policy policy entity
+   * @return The policy mapping record we detached. Will return 
ENTITY_NOT_FOUND if the specified
+   *     target or policy does not exist. Will return POLICY_MAPPING_NOT_FOUND 
if the mapping cannot
+   *     be found
+   */
+  @Nonnull
+  PolicyAttachmentResult detachPolicyFromEntity(
+      @Nonnull PolarisCallContext callCtx,
+      @Nonnull List<PolarisEntityCore> catalogPath,
+      @Nonnull PolarisEntityCore target,
+      @Nonnull List<PolarisEntityCore> policyCatalogPath,
+      @Nonnull PolicyEntity policy);
+
+  /**
+   * Load all policies attached to a target entity
+   *
+   * @param callCtx call context
+   * @param target target entity
+   * @return the list of policy mapping records on the target entity. Will 
return ENTITY_NOT_FOUND
+   *     if the specified target does not exist.
+   */
+  @Nonnull
+  LoadPolicyMappingsResult loadPoliciesOnEntity(
+      @Nonnull PolarisCallContext callCtx, @Nonnull PolarisEntityCore target);
+
+  /**
+   * Load all policies of a specific type attached to a target entity
+   *
+   * @param callCtx call context
+   * @param target target entity
+   * @param policyType the type of policy
+   * @return the list of policy mapping records on the target entity. Will 
return ENTITY_NOT_FOUND
+   *     if the specified target does not exist.
+   */
+  @Nonnull
+  LoadPolicyMappingsResult loadPoliciesOnEntityByType(
+      @Nonnull PolarisCallContext callCtx,
+      @Nonnull PolarisEntityCore target,
+      @Nonnull PolicyType policyType);
+}
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/policy/PolarisPolicyMappingRecord.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/policy/PolarisPolicyMappingRecord.java
new file mode 100644
index 000000000..d4bf118b6
--- /dev/null
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/policy/PolarisPolicyMappingRecord.java
@@ -0,0 +1,215 @@
+/*
+ * 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.polaris.core.policy;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+public class PolarisPolicyMappingRecord {
+  // to serialize/deserialize properties
+  public static final String EMPTY_MAP_STRING = "{}";
+  private static final ObjectMapper MAPPER = new ObjectMapper();
+
+  // id of the catalog where target entity resides
+  private long targetCatalogId;
+
+  // id of the target entity
+  private long targetId;
+
+  // id of the catalog where the policy entity resides
+  private long policyCatalogId;
+
+  // id of the policy
+  private long policyId;
+
+  // id associated to the policy type
+  private int policyTypeCode;
+
+  // additional parameters of the mapping
+  private String parameters;
+
+  public PolarisPolicyMappingRecord() {}
+
+  public long getTargetCatalogId() {
+    return targetCatalogId;
+  }
+
+  public void setTargetCatalogId(long targetCatalogId) {
+    this.targetCatalogId = targetCatalogId;
+  }
+
+  public long getTargetId() {
+    return targetId;
+  }
+
+  public void setTargetId(long targetId) {
+    this.targetId = targetId;
+  }
+
+  public long getPolicyId() {
+    return policyId;
+  }
+
+  public void setPolicyId(long policyId) {
+    this.policyId = policyId;
+  }
+
+  public int getPolicyTypeCode() {
+    return policyTypeCode;
+  }
+
+  public void setPolicyTypeCode(int policyTypeCode) {
+    this.policyTypeCode = policyTypeCode;
+  }
+
+  public long getPolicyCatalogId() {
+    return policyCatalogId;
+  }
+
+  public void setPolicyCatalogId(long policyCatalogId) {
+    this.policyCatalogId = policyCatalogId;
+  }
+
+  public String getParameters() {
+    return parameters;
+  }
+
+  public void setParameters(String parameters) {
+    this.parameters = parameters;
+  }
+
+  public Map<String, String> getParametersAsMap() {
+    if (parameters == null) {
+      return new HashMap<>();
+    }
+    try {
+      return MAPPER.readValue(parameters, new TypeReference<>() {});
+    } catch (JsonProcessingException ex) {
+      throw new IllegalStateException(
+          String.format("Failed to deserialize json. parameters %s", 
parameters), ex);
+    }
+  }
+
+  public void setParametersAsMap(Map<String, String> parameters) {
+    try {
+      this.parameters =
+          parameters == null ? EMPTY_MAP_STRING : 
MAPPER.writeValueAsString(parameters);
+    } catch (JsonProcessingException ex) {
+      throw new IllegalStateException(
+          String.format("Failed to serialize json. properties %s", 
parameters), ex);
+    }
+  }
+
+  /**
+   * Constructor
+   *
+   * @param targetCatalogId id of the catalog where target entity resides
+   * @param targetId id of the target entity
+   * @param policyCatalogId id of the catalog where the policy entity resides
+   * @param policyId id of the policy
+   * @param policyTypeCode id associated to the policy type
+   * @param parameters additional parameters of the mapping
+   */
+  @JsonCreator
+  public PolarisPolicyMappingRecord(
+      @JsonProperty("targetCatalogId") long targetCatalogId,
+      @JsonProperty("targetId") long targetId,
+      @JsonProperty("policyCatalogId") long policyCatalogId,
+      @JsonProperty("policyId") long policyId,
+      @JsonProperty("policyTypeCode") int policyTypeCode,
+      @JsonProperty("parameters") String parameters) {
+    this.targetCatalogId = targetCatalogId;
+    this.targetId = targetId;
+    this.policyCatalogId = policyCatalogId;
+    this.policyId = policyId;
+    this.policyTypeCode = policyTypeCode;
+    this.parameters = parameters;
+  }
+
+  public PolarisPolicyMappingRecord(
+      long targetCatalogId,
+      long targetId,
+      long policyCatalogId,
+      long policyId,
+      int policyTypeCode,
+      Map<String, String> parameters) {
+    this.targetCatalogId = targetCatalogId;
+    this.targetId = targetId;
+    this.policyCatalogId = policyCatalogId;
+    this.policyId = policyId;
+    this.policyTypeCode = policyTypeCode;
+    this.setParametersAsMap(parameters);
+  }
+
+  /**
+   * Copy constructor
+   *
+   * @param policyMappingRecord policy mapping rec to copy
+   */
+  public PolarisPolicyMappingRecord(PolarisPolicyMappingRecord 
policyMappingRecord) {
+    this.targetCatalogId = policyMappingRecord.getTargetCatalogId();
+    this.targetId = policyMappingRecord.getTargetId();
+    this.policyCatalogId = policyMappingRecord.getPolicyCatalogId();
+    this.policyId = policyMappingRecord.getPolicyId();
+    this.policyTypeCode = policyMappingRecord.getPolicyTypeCode();
+    this.parameters = policyMappingRecord.getParameters();
+  }
+
+  @Override
+  public String toString() {
+    return "PolarisPolicyMappingRec{"
+        + "targetCatalogId="
+        + targetCatalogId
+        + ", targetId="
+        + targetId
+        + ", policyCatalogId="
+        + policyCatalogId
+        + ", policyId="
+        + policyId
+        + ", policyType='"
+        + policyTypeCode
+        + ", parameters='"
+        + parameters
+        + "}";
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    PolarisPolicyMappingRecord that = (PolarisPolicyMappingRecord) o;
+    return targetCatalogId == that.targetCatalogId
+        && targetId == that.targetId
+        && policyCatalogId == that.policyCatalogId
+        && policyId == that.policyId
+        && policyTypeCode == that.policyTypeCode
+        && Objects.equals(parameters, that.parameters);
+  }
+
+  @Override
+  public int hashCode() {
+    return java.util.Objects.hash(targetId, policyId, policyCatalogId, 
policyTypeCode, parameters);
+  }
+}
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/policy/PolicyMappingPersistence.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/policy/PolicyMappingPersistence.java
new file mode 100644
index 000000000..33a754668
--- /dev/null
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/policy/PolicyMappingPersistence.java
@@ -0,0 +1,151 @@
+/*
+ * 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.polaris.core.policy;
+
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.util.List;
+import org.apache.polaris.core.PolarisCallContext;
+import org.apache.polaris.core.entity.PolarisEntityCore;
+
+/**
+ * Interface for interacting with the Polaris persistence backend for Policy 
Mapping operations.
+ * This interface provides methods to persist and retrieve policy mapping 
records, which define the
+ * relationships between policies and target entities in Polaris.
+ *
+ * <p>Note that APIs to the actual persistence store are very basic, often 
point read or write to
+ * the underlying data store. The goal is to make it really easy to back this 
using databases like
+ * Postgres or simpler KV store. Each API in this interface need to be atomic.
+ */
+public interface PolicyMappingPersistence {
+
+  /**
+   * Write the specified policyMappingRecord to the policy_mapping_records 
table. If there is a
+   * conflict (existing record with the same PK), all attributes of the new 
record will replace the
+   * existing one.
+   *
+   * @param callCtx call context
+   * @param record policy mapping record to write, potentially replacing an 
existing policy mapping
+   *     with the same key
+   */
+  default void writeToPolicyMappingRecords(
+      @Nonnull PolarisCallContext callCtx, @Nonnull PolarisPolicyMappingRecord 
record) {
+    throw new UnsupportedOperationException("Not Implemented");
+  }
+
+  /**
+   * Delete the specified policyMappingRecord to the policy_mapping_records 
table.
+   *
+   * @param callCtx call context
+   * @param record policy mapping record to delete.
+   */
+  default void deleteFromPolicyMappingRecords(
+      @Nonnull PolarisCallContext callCtx, @Nonnull PolarisPolicyMappingRecord 
record) {
+    throw new UnsupportedOperationException("Not Implemented");
+  }
+
+  /**
+   * Delete the all policy mapping records in the policy_mapping_records table 
for the specified
+   * entity. This method will delete all policy mapping records on the entity
+   *
+   * @param callCtx call context
+   * @param entity entity whose policy mapping records should be deleted
+   * @param mappingOnTarget all mappings on that target entity. Empty list if 
that entity is not a
+   *     target
+   * @param mappingOnPolicy all mappings on that policy entity. Empty list if 
that entity is not a
+   *     policy
+   */
+  default void deleteAllEntityPolicyMappingRecords(
+      @Nonnull PolarisCallContext callCtx,
+      @Nonnull PolarisEntityCore entity,
+      @Nonnull List<PolarisPolicyMappingRecord> mappingOnTarget,
+      @Nonnull List<PolarisPolicyMappingRecord> mappingOnPolicy) {
+    throw new UnsupportedOperationException("Not Implemented");
+  }
+
+  /**
+   * Look up the specified policy mapping record from the 
policy_mapping_records table. Return NULL
+   * if not found
+   *
+   * @param callCtx call context
+   * @param targetCatalogId catalog id of the target entity, NULL_ID if the 
entity is top-level
+   * @param targetId id of the target entity
+   * @param policyTypeCode type code of the policy entity
+   * @param policyCatalogId catalog id of the policy entity
+   * @param policyId id of the policy entity
+   * @return the policy mapping record if found, NULL if not found
+   */
+  @Nullable
+  default PolarisPolicyMappingRecord lookupPolicyMappingRecord(
+      @Nonnull PolarisCallContext callCtx,
+      long targetCatalogId,
+      long targetId,
+      int policyTypeCode,
+      long policyCatalogId,
+      long policyId) {
+    throw new UnsupportedOperationException("Not Implemented");
+  }
+
+  /**
+   * Get all policies on the specified target entity with specified policy 
type.
+   *
+   * @param callCtx call context
+   * @param targetCatalogId catalog id of the target entity, NULL_ID if the 
entity is top-level
+   * @param targetId id of the target entity
+   * @param policyTypeCode type code of the policy entity
+   * @return the list of policy mapping records for the specified target 
entity with the specified
+   *     policy type
+   */
+  @Nonnull
+  default List<PolarisPolicyMappingRecord> loadPoliciesOnTargetByType(
+      @Nonnull PolarisCallContext callCtx,
+      long targetCatalogId,
+      long targetId,
+      int policyTypeCode) {
+    throw new UnsupportedOperationException("Not Implemented");
+  }
+
+  /**
+   * Get all policies on the specified target entity.
+   *
+   * @param callCtx call context
+   * @param targetCatalogId catalog id of the target entity, NULL_ID if the 
entity is top-level
+   * @param targetId id of the target entity
+   * @return the list of policy mapping records for the specified target entity
+   */
+  @Nonnull
+  default List<PolarisPolicyMappingRecord> loadAllPoliciesOnTarget(
+      @Nonnull PolarisCallContext callCtx, long targetCatalogId, long 
targetId) {
+    throw new UnsupportedOperationException("Not Implemented");
+  }
+
+  /**
+   * Get all targets of the specified policy entity
+   *
+   * @param callCtx call context
+   * @param policyCatalogId catalog id of the policy entity, NULL_ID if the 
entity is top-level
+   * @param policyId id of the policy entity
+   * @return the list of policy mapping records for the specified policy entity
+   */
+  @Nonnull
+  default List<PolarisPolicyMappingRecord> loadAllTargetsOnPolicy(
+      @Nonnull PolarisCallContext callCtx, long policyCatalogId, long 
policyId) {
+    throw new UnsupportedOperationException("Not Implemented");
+  }
+}
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/policy/TransactionalPolicyMappingPersistence.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/policy/TransactionalPolicyMappingPersistence.java
new file mode 100644
index 000000000..6ad1ac80c
--- /dev/null
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/policy/TransactionalPolicyMappingPersistence.java
@@ -0,0 +1,98 @@
+/*
+ * 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.polaris.core.policy;
+
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.util.List;
+import org.apache.polaris.core.PolarisCallContext;
+import org.apache.polaris.core.entity.PolarisEntityCore;
+
+public interface TransactionalPolicyMappingPersistence {
+  /** See {@link PolicyMappingPersistence#writeToPolicyMappingRecords} */
+  default void writeToPolicyMappingRecordsInCurrentTxn(
+      @Nonnull PolarisCallContext callCtx, @Nonnull PolarisPolicyMappingRecord 
record) {
+    throw new UnsupportedOperationException("Not Implemented");
+  }
+
+  /**
+   * Helpers to check conditions for writing new PolicyMappingRecords in 
current transaction.
+   *
+   * <p>It should throw a PolicyMappingAlreadyExistsException if the new 
record conflicts with an
+   * exising record with same policy type but different policy.
+   *
+   * @param callCtx call context
+   * @param record policy mapping record to write.
+   */
+  default void checkConditionsForWriteToPolicyMappingRecordsInCurrentTxn(
+      @Nonnull PolarisCallContext callCtx, @Nonnull PolarisPolicyMappingRecord 
record) {
+    throw new UnsupportedOperationException("Not Implemented");
+  }
+
+  /** See {@link PolicyMappingPersistence#deleteFromPolicyMappingRecords} */
+  default void deleteFromPolicyMappingRecordsInCurrentTxn(
+      @Nonnull PolarisCallContext callCtx, @Nonnull PolarisPolicyMappingRecord 
record) {
+    throw new UnsupportedOperationException("Not Implemented");
+  }
+
+  /** See {@link PolicyMappingPersistence#deleteAllEntityPolicyMappingRecords} 
*/
+  default void deleteAllEntityPolicyMappingRecordsInCurrentTxn(
+      @Nonnull PolarisCallContext callCtx,
+      @Nonnull PolarisEntityCore entity,
+      @Nonnull List<PolarisPolicyMappingRecord> mappingOnTarget,
+      @Nonnull List<PolarisPolicyMappingRecord> mappingOnPolicy) {
+    throw new UnsupportedOperationException("Not Implemented");
+  }
+
+  /** See {@link PolicyMappingPersistence#lookupPolicyMappingRecord} */
+  @Nullable
+  default PolarisPolicyMappingRecord lookupPolicyMappingRecordInCurrentTxn(
+      @Nonnull PolarisCallContext callCtx,
+      long targetCatalogId,
+      long targetId,
+      int policyTypeCode,
+      long policyCatalogId,
+      long policyId) {
+    throw new UnsupportedOperationException("Not Implemented");
+  }
+
+  /** See {@link PolicyMappingPersistence#loadPoliciesOnTargetByType} */
+  @Nonnull
+  default List<PolarisPolicyMappingRecord> 
loadPoliciesOnTargetByTypeInCurrentTxn(
+      @Nonnull PolarisCallContext callCtx,
+      long targetCatalogId,
+      long targetId,
+      int policyTypeCode) {
+    throw new UnsupportedOperationException("Not Implemented");
+  }
+
+  /** See {@link PolicyMappingPersistence#loadAllPoliciesOnTarget} */
+  @Nonnull
+  default List<PolarisPolicyMappingRecord> loadAllPoliciesOnTargetInCurrentTxn(
+      @Nonnull PolarisCallContext callCtx, long targetCatalogId, long 
targetId) {
+    throw new UnsupportedOperationException("Not Implemented");
+  }
+
+  /** See {@link PolicyMappingPersistence#loadAllTargetsOnPolicy} */
+  @Nonnull
+  default List<PolarisPolicyMappingRecord> loadAllTargetsOnPolicyInCurrentTxn(
+      @Nonnull PolarisCallContext callCtx, long policyCatalogId, long 
policyId) {
+    throw new UnsupportedOperationException("Not Implemented");
+  }
+}
diff --git 
a/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BasePolarisMetaStoreManagerTest.java
 
b/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BasePolarisMetaStoreManagerTest.java
index 81f233825..0f9824fcd 100644
--- 
a/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BasePolarisMetaStoreManagerTest.java
+++ 
b/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BasePolarisMetaStoreManagerTest.java
@@ -280,6 +280,12 @@ public abstract class BasePolarisMetaStoreManagerTest {
     polarisTestMetaStoreManager.testEntityCache();
   }
 
+  /** Test that attaching/detaching policies works well */
+  @Test
+  void testPolicyMapping() {
+    polarisTestMetaStoreManager.testPolicyMapping();
+  }
+
   @Test
   void testLoadTasks() {
     for (int i = 0; i < 20; i++) {
diff --git 
a/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/PolarisTestMetaStoreManager.java
 
b/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/PolarisTestMetaStoreManager.java
index 591c657d1..3e7a0fb2a 100644
--- 
a/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/PolarisTestMetaStoreManager.java
+++ 
b/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/PolarisTestMetaStoreManager.java
@@ -48,7 +48,10 @@ import 
org.apache.polaris.core.persistence.dao.entity.CreatePrincipalResult;
 import org.apache.polaris.core.persistence.dao.entity.DropEntityResult;
 import org.apache.polaris.core.persistence.dao.entity.EntityResult;
 import org.apache.polaris.core.persistence.dao.entity.LoadGrantsResult;
+import org.apache.polaris.core.persistence.dao.entity.LoadPolicyMappingsResult;
+import org.apache.polaris.core.persistence.dao.entity.PolicyAttachmentResult;
 import org.apache.polaris.core.persistence.dao.entity.ResolvedEntityResult;
+import org.apache.polaris.core.policy.PolarisPolicyMappingRecord;
 import org.apache.polaris.core.policy.PolicyEntity;
 import org.apache.polaris.core.policy.PolicyType;
 import org.apache.polaris.core.policy.PredefinedPolicyTypes;
@@ -956,6 +959,195 @@ public class PolarisTestMetaStoreManager {
     this.ensureGrantRecordRemoved(granted, grantee, priv);
   }
 
+  /** attach a policy to a target */
+  void attachPolicyToTarget(
+      List<PolarisEntityCore> targetCatalogPath,
+      PolarisBaseEntity target,
+      List<PolarisEntityCore> policyCatalogPath,
+      PolicyEntity policy) {
+    polarisMetaStoreManager.attachPolicyToEntity(
+        polarisCallContext, targetCatalogPath, target, policyCatalogPath, 
policy, null);
+    ensurePolicyMappingRecordExists(target, policy);
+  }
+
+  /** detach a policy from a target */
+  void detachPolicyFromTarget(
+      List<PolarisEntityCore> targetCatalogPath,
+      PolarisBaseEntity target,
+      List<PolarisEntityCore> policyCatalogPath,
+      PolicyEntity policy) {
+    polarisMetaStoreManager.detachPolicyFromEntity(
+        polarisCallContext, targetCatalogPath, target, policyCatalogPath, 
policy);
+    ensurePolicyMappingRecordRemoved(target, policy);
+  }
+
+  /**
+   * Ensure that the specified policy mapping record exists
+   *
+   * @param target the target
+   * @param policy the policy
+   */
+  void ensurePolicyMappingRecordExists(PolarisBaseEntity target, PolicyEntity 
policy) {
+    target =
+        polarisMetaStoreManager
+            .loadEntity(
+                this.polarisCallContext, target.getCatalogId(), 
target.getId(), target.getType())
+            .getEntity();
+    Assertions.assertThat(target).isNotNull();
+
+    policy =
+        PolicyEntity.of(
+            polarisMetaStoreManager
+                .loadEntity(
+                    this.polarisCallContext,
+                    policy.getCatalogId(),
+                    policy.getId(),
+                    PolarisEntityType.POLICY)
+                .getEntity());
+    Assertions.assertThat(policy).isNotNull();
+
+    LoadPolicyMappingsResult loadPolicyMappingsResult =
+        polarisMetaStoreManager.loadPoliciesOnEntity(this.polarisCallContext, 
target);
+
+    validateLoadedPolicyMappings(loadPolicyMappingsResult);
+
+    checkPolicyMappingRecordExists(
+        loadPolicyMappingsResult.getPolicyMappingRecords(), target, policy);
+
+    // also try load by specific type
+    LoadPolicyMappingsResult loadPolicyMappingsResultByType =
+        polarisMetaStoreManager.loadPoliciesOnEntityByType(
+            this.polarisCallContext, target, policy.getPolicyType());
+    validateLoadedPolicyMappings(loadPolicyMappingsResultByType);
+    checkPolicyMappingRecordExists(
+        loadPolicyMappingsResultByType.getPolicyMappingRecords(), target, 
policy);
+  }
+
+  /**
+   * Ensure that the specified policy mapping record has been removed
+   *
+   * @param target the target
+   * @param policy the policy
+   */
+  void ensurePolicyMappingRecordRemoved(PolarisBaseEntity target, PolicyEntity 
policy) {
+    target =
+        polarisMetaStoreManager
+            .loadEntity(
+                this.polarisCallContext, target.getCatalogId(), 
target.getId(), target.getType())
+            .getEntity();
+    Assertions.assertThat(target).isNotNull();
+
+    policy =
+        PolicyEntity.of(
+            polarisMetaStoreManager
+                .loadEntity(
+                    this.polarisCallContext,
+                    policy.getCatalogId(),
+                    policy.getId(),
+                    PolarisEntityType.POLICY)
+                .getEntity());
+    Assertions.assertThat(policy).isNotNull();
+
+    LoadPolicyMappingsResult loadPolicyMappingsResult =
+        polarisMetaStoreManager.loadPoliciesOnEntity(this.polarisCallContext, 
target);
+
+    validateLoadedPolicyMappings(loadPolicyMappingsResult);
+
+    checkPolicyMappingRecordRemoved(
+        loadPolicyMappingsResult.getPolicyMappingRecords(), target, policy);
+
+    // also try load by specific type
+    LoadPolicyMappingsResult loadPolicyMappingsResultByType =
+        polarisMetaStoreManager.loadPoliciesOnEntityByType(
+            this.polarisCallContext, target, policy.getPolicyType());
+    validateLoadedPolicyMappings(loadPolicyMappingsResultByType);
+    checkPolicyMappingRecordRemoved(
+        loadPolicyMappingsResultByType.getPolicyMappingRecords(), target, 
policy);
+  }
+
+  /**
+   * Validate the return of loadPoliciesOnEntity() or 
LoadPoliciesOnEntityByType()
+   *
+   * @param loadPolicyMappingRecords return from calling
+   *     loadPoliciesOnEntity()/LoadPoliciesOnEntityByType()
+   */
+  void validateLoadedPolicyMappings(LoadPolicyMappingsResult 
loadPolicyMappingRecords) {
+    Assertions.assertThat(loadPolicyMappingRecords).isNotNull();
+
+    Map<Long, PolarisBaseEntity> policyEntities = 
loadPolicyMappingRecords.getEntitiesAsMap();
+    Assertions.assertThat(policyEntities).isNotNull();
+
+    for (PolarisPolicyMappingRecord policyMappingRecord :
+        loadPolicyMappingRecords.getPolicyMappingRecords()) {
+      PolicyEntity entity =
+          PolicyEntity.of(
+              polarisMetaStoreManager
+                  .loadEntity(
+                      this.polarisCallContext,
+                      policyMappingRecord.getPolicyCatalogId(),
+                      policyMappingRecord.getPolicyId(),
+                      PolarisEntityType.POLICY)
+                  .getEntity());
+
+      Assertions.assertThat(entity).isNotNull();
+      
Assertions.assertThat(policyEntities.get(entity.getId())).isEqualTo(entity);
+    }
+  }
+
+  /**
+   * Check that the policy mapping record exists
+   *
+   * @param policyMappingRecords list of policy mapping records
+   * @param target the target
+   * @param policy the policy
+   */
+  void checkPolicyMappingRecordExists(
+      List<PolarisPolicyMappingRecord> policyMappingRecords,
+      PolarisBaseEntity target,
+      PolicyEntity policy) {
+    boolean exists = isPolicyMappingRecordExists(policyMappingRecords, target, 
policy);
+    Assertions.assertThat(exists).isTrue();
+  }
+
+  /**
+   * Check that the policy mapping record has been removed
+   *
+   * @param policyMappingRecords list of policy mapping records
+   * @param target the target
+   * @param policy the policy
+   */
+  void checkPolicyMappingRecordRemoved(
+      List<PolarisPolicyMappingRecord> policyMappingRecords,
+      PolarisBaseEntity target,
+      PolicyEntity policy) {
+    boolean exists = isPolicyMappingRecordExists(policyMappingRecords, target, 
policy);
+    Assertions.assertThat(exists).isFalse();
+  }
+
+  /**
+   * Check if the policy mapping record exists
+   *
+   * @param policyMappingRecords list of policy mapping records
+   * @param target the target
+   * @param policy the policy
+   */
+  boolean isPolicyMappingRecordExists(
+      List<PolarisPolicyMappingRecord> policyMappingRecords,
+      PolarisBaseEntity target,
+      PolicyEntity policy) {
+    long policyMappingCount =
+        policyMappingRecords.stream()
+            .filter(
+                record ->
+                    record.getPolicyCatalogId() == policy.getCatalogId()
+                        && record.getPolicyId() == policy.getId()
+                        && record.getTargetCatalogId() == target.getCatalogId()
+                        && record.getTargetId() == target.getId()
+                        && record.getPolicyTypeCode() == 
policy.getPolicyTypeCode())
+            .count();
+    return policyMappingCount == 1;
+  }
+
   /**
    * Create a test catalog. This is a new catalog which will have the 
following objects (N is for a
    * namespace, T for a table, V for a view, R for a role, P for a principal, 
POL for a policy):
@@ -2443,4 +2635,62 @@ public class PolarisTestMetaStoreManager {
     this.refreshCacheEntry(
         1, 1, PolarisEntityType.TABLE_LIKE, N1.getCatalogId() + 1000, 
N1.getId(), false);
   }
+
+  void testPolicyMapping() {
+    PolarisBaseEntity catalog = this.createTestCatalog("test");
+    Assertions.assertThat(catalog).isNotNull();
+
+    PolarisBaseEntity N1 =
+        this.ensureExistsByName(List.of(catalog), PolarisEntityType.NAMESPACE, 
"N1");
+    PolarisBaseEntity N1_N2 =
+        this.ensureExistsByName(List.of(catalog, N1), 
PolarisEntityType.NAMESPACE, "N2");
+    PolarisBaseEntity N5 =
+        this.ensureExistsByName(List.of(catalog), PolarisEntityType.NAMESPACE, 
"N5");
+
+    PolarisBaseEntity N1_N2_T1 =
+        this.ensureExistsByName(
+            List.of(catalog, N1, N1_N2),
+            PolarisEntityType.TABLE_LIKE,
+            PolarisEntitySubType.ANY_SUBTYPE,
+            "T1");
+
+    PolicyEntity N1_P1 =
+        this.createPolicy(List.of(catalog, N1), "P1", 
PredefinedPolicyTypes.DATA_COMPACTION);
+
+    PolicyEntity N1_P2 =
+        this.createPolicy(List.of(catalog, N1), "P2", 
PredefinedPolicyTypes.DATA_COMPACTION);
+
+    PolicyEntity N5_P3 =
+        this.createPolicy(List.of(catalog, N5), "P3", 
PredefinedPolicyTypes.METADATA_COMPACTION);
+    attachPolicyToTarget(List.of(catalog, N1, N1_N2), N1_N2_T1, 
List.of(catalog, N1), N1_P1);
+    // attach a different policy of different inheritable type to the same 
target, should succeed
+    attachPolicyToTarget(List.of(catalog, N1, N1_N2), N1_N2_T1, 
List.of(catalog, N5), N5_P3);
+
+    // attach a different policy of same inheritable type to the same target, 
should fail
+    PolicyAttachmentResult policyAttachmentResult =
+        polarisMetaStoreManager.attachPolicyToEntity(
+            polarisCallContext,
+            List.of(catalog, N1, N1_N2),
+            N1_N2_T1,
+            List.of(catalog, N1),
+            N1_P2,
+            null);
+
+    Assertions.assertThat(policyAttachmentResult.isSuccess()).isFalse();
+    Assertions.assertThat(policyAttachmentResult.getReturnStatus())
+        
.isEqualTo(BaseResult.ReturnStatus.POLICY_MAPPING_OF_SAME_TYPE_ALREADY_EXISTS);
+
+    LoadPolicyMappingsResult loadPolicyMappingsResult =
+        polarisMetaStoreManager.loadPoliciesOnEntityByType(
+            polarisCallContext, N1_N2_T1, 
PredefinedPolicyTypes.DATA_COMPACTION);
+    Assertions.assertThat(loadPolicyMappingsResult.isSuccess()).isTrue();
+    Assertions.assertThat(loadPolicyMappingsResult.getEntities()).hasSize(1);
+    PolicyEntity policyEntity = 
PolicyEntity.of(loadPolicyMappingsResult.getEntities().get(0));
+    Assertions.assertThat(policyEntity.getId()).isEqualTo(N1_P1.getId());
+    Assertions.assertThat(policyEntity.getPolicyType())
+        .isEqualTo(PredefinedPolicyTypes.DATA_COMPACTION);
+
+    detachPolicyFromTarget(List.of(catalog, N1, N1_N2), N1_N2_T1, 
List.of(catalog, N1), N1_P1);
+    detachPolicyFromTarget(List.of(catalog, N1, N1_N2), N1_N2_T1, 
List.of(catalog, N5), N5_P3);
+  }
 }

Reply via email to