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

starocean999 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new befa4fda0a2 [fix](fe) Fix row policy bypass when leading hint rebuilds 
the join (#67776)
befa4fda0a2 is described below

commit befa4fda0a222f639770567ee3ac691b11ecc33f
Author: starocean999 <[email protected]>
AuthorDate: Thu Sep 17 18:48:36 2026 +0800

    [fix](fe) Fix row policy bypass when leading hint rebuilds the join (#67776)
    
    Problem Summary:
    Reproduction:
    A normal user has a restrictive row policy `USING(k = 1)` on table `t1`.
    Reading the table directly and joining it without a hint only returns
    the
    allowed row, but adding a leading hint leaks the protected row:
    
        -- returns only k = 1
    SELECT t1.k, t1.v, t2.v FROM t1 JOIN t2 ON t1.k = t2.k ORDER BY t1.k;
    
        -- returns k = 1 and k = 2, the row policy is bypassed
        SELECT /*+ leading(t1 t2) */ t1.k, t1.v, t2.v
        FROM t1 JOIN t2 ON t1.k = t2.k ORDER BY t1.k;
    
    `EXPLAIN VERBOSE` shows that the scan of `t1` in the hinted plan has no
    `k = 1` predicate, while the un-hinted plan has one. It is an access
    control
    issue: the hint only changes the join order, so it must never change
    which rows
    a user is allowed to read.
    
    Root cause:
    `CheckPolicy` materializes a row policy as a `LogicalFilter` on the
    relation
    (and a data mask as a `LogicalProject` above it). The analysis rule
    `CollectJoinConstraint` only remembered the scan itself, or the
    `Project(OlapScan)` directly above it, in
    `LeadingHint.relationIdToScanMap`.
    `LeadingHint.generateLeadingJoinPlan` rebuilds the whole join from the
    plans
    remembered in that map, so every node that was not remembered - the row
    policy
    filter, the data mask project, a binder filter, and the pre-aggregation
    of a
    random distribution aggregate table - was silently dropped when the join
    was
    rebuilt.
    
    Fix:
    `CollectJoinConstraint` now remembers the whole plan below each side of
    a join
    instead of only the relation or the project above the relation
    (`collectLeafPlan()`), so rebuilding the join reuses exactly the
    original
    leaves. `LeadingHint.getBitmap()` is generalized accordingly, so that a
    leaf
    which is built on one relation (e.g. the `LogicalAggregate` generated
    for a
    random distribution aggregate table) is still resolved to its table
    bitmap.
    
    Before the fix, the hinted query returned `(1, 10, 100)` and `(2, 20,
    200)`;
    after the fix it returns only `(1, 10, 100)`, the same as the un-hinted
    query.
    As a side effect, `SELECT /*+ leading(...) */ ...` on a random
    distribution
    aggregate table no longer loses its pre-aggregation, which previously
    returned
    un-merged rows or failed the `CheckAfterRewrite` slot validation.
---
 .../org/apache/doris/nereids/hint/LeadingHint.java |   8 +-
 .../rules/analysis/CollectJoinConstraint.java      |  41 +++--
 .../rules/analysis/LeadingHintRowPolicyTest.java   | 195 +++++++++++++++++++++
 .../data/query_p0/hint/test_leading_row_policy.out |  22 +++
 .../query_p0/hint/test_leading_row_policy.groovy   | 173 ++++++++++++++++++
 5 files changed, 421 insertions(+), 18 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/hint/LeadingHint.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/hint/LeadingHint.java
index 7f67311204d..8ffb9a77559 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/hint/LeadingHint.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/hint/LeadingHint.java
@@ -624,7 +624,13 @@ public class LeadingHint extends Hint {
         } else if (root instanceof LogicalSubQueryAlias) {
             return LongBitmap.set(0L, (((LogicalSubQueryAlias) 
root).getRelationId().asInt()));
         } else {
-            return null;
+            Set<RelationId> inputRelations = root.getInputRelations();
+            if (inputRelations.size() != 1) {
+                return null;
+            }
+            // the leaf could be a plan which is built on one relation, e.g. 
the row policy filter, the data
+            // mask project or the aggregate which is generated for the random 
distribution aggregate table
+            return LongBitmap.set(0L, 
inputRelations.iterator().next().asInt());
         }
     }
 }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/CollectJoinConstraint.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/CollectJoinConstraint.java
index eacdd3fdd41..60402be1024 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/CollectJoinConstraint.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/CollectJoinConstraint.java
@@ -30,8 +30,7 @@ import org.apache.doris.nereids.trees.expressions.Slot;
 import org.apache.doris.nereids.trees.plans.JoinType;
 import org.apache.doris.nereids.trees.plans.RelationId;
 import org.apache.doris.nereids.trees.plans.logical.LogicalJoin;
-import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
-import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
+import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
 
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
@@ -54,6 +53,8 @@ public class CollectJoinConstraint implements 
RewriteRuleFactory {
                 LeadingHint leading = (LeadingHint) ctx.cascadesContext
                             .getHintMap().get("Leading");
                 LogicalJoin join = ctx.root;
+                collectLeafPlan(leading, (LogicalPlan) join.left());
+                collectLeafPlan(leading, (LogicalPlan) join.right());
                 if (join.getJoinType().isNullAwareLeftAntiJoin()) {
                     leading.setStatus(Hint.HintStatus.UNUSED);
                     leading.setErrorMessage("condition does not matched 
joinType");
@@ -99,24 +100,30 @@ public class CollectJoinConstraint implements 
RewriteRuleFactory {
                         leading, leftHand, rightHand, joinType, 
totalFilterBitMap, nonNullableSlotBitMap);
 
                 return ctx.root;
-            }).toRule(RuleType.COLLECT_JOIN_CONSTRAINT),
-
-            logicalProject(logicalOlapScan()).thenApply(
-                ctx -> {
-                    if (!ctx.cascadesContext.isLeadingJoin()) {
-                        return ctx.root;
-                    }
-                    LeadingHint leading = (LeadingHint) ctx.cascadesContext
-                            .getHintMap().get("Leading");
-                    LogicalProject<LogicalOlapScan> project = ctx.root;
-                    LogicalOlapScan scan = project.child();
-                    leading.getRelationIdToScanMap().put(scan.getRelationId(), 
project);
-                    return ctx.root;
-                }
-            ).toRule(RuleType.COLLECT_JOIN_CONSTRAINT)
+            }).toRule(RuleType.COLLECT_JOIN_CONSTRAINT)
         );
     }
 
+    /**
+     * Remember the whole plan below one side of the join, so that the leading 
hint can rebuild the join
+     * with exactly the same leaves. The plan could be the relation itself, or 
the plans which are built
+     * on the relation by the previous analysis rules, e.g. the row policy / 
data mask filter which is
+     * materialized by CheckPolicy. If only the relation is remembered, these 
plans are dropped silently
+     * once the join is rebuilt from them.
+     */
+    private void collectLeafPlan(LeadingHint leading, LogicalPlan child) {
+        Set<RelationId> inputRelations = child.getInputRelations();
+        if (inputRelations.size() != 1) {
+            // the child is built on multiple relations, e.g. a join, its own 
join node is processed separately
+            return;
+        }
+        RelationId relationId = inputRelations.iterator().next();
+        if (relationId == null) {
+            return;
+        }
+        leading.getRelationIdToScanMap().put(relationId, child);
+    }
+
     private void collectJoinConstraintList(LeadingHint leading, Long leftHand, 
Long rightHand, JoinType joinType,
                                             Long filterTableBitMap, Long 
nonNullableSlotBitMap) {
         Long totalTables = LongBitmap.or(leftHand, rightHand);
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/LeadingHintRowPolicyTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/LeadingHintRowPolicyTest.java
new file mode 100644
index 00000000000..253f6a1e833
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/LeadingHintRowPolicyTest.java
@@ -0,0 +1,195 @@
+// 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.doris.nereids.rules.analysis;
+
+import org.apache.doris.analysis.TablePattern;
+import org.apache.doris.analysis.UserDesc;
+import org.apache.doris.analysis.UserIdentity;
+import org.apache.doris.authorization.DataMaskSpec;
+import org.apache.doris.catalog.AccessPrivilege;
+import org.apache.doris.catalog.AccessPrivilegeWithCols;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.common.FeConstants;
+import org.apache.doris.common.jmockit.Deencapsulation;
+import org.apache.doris.mysql.privilege.AccessControllerManager;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.commands.CreateUserCommand;
+import 
org.apache.doris.nereids.trees.plans.commands.GrantTablePrivilegeCommand;
+import org.apache.doris.nereids.trees.plans.commands.info.CreateUserInfo;
+import org.apache.doris.nereids.trees.plans.logical.LogicalFilter;
+import org.apache.doris.nereids.trees.plans.logical.LogicalJoin;
+import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
+import org.apache.doris.nereids.util.PlanChecker;
+import org.apache.doris.utframe.TestWithFeService;
+
+import com.google.common.collect.Lists;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * The leading hint rebuilds the join from the plans which are remembered in 
the analysis phase, so the plans
+ * which are built on the table relation, e.g. the row policy filter and the 
data mask project, must be
+ * remembered together with the relation. Otherwise they are dropped silently 
and the user can read the rows
+ * which are protected by the row policy.
+ */
+public class LeadingHintRowPolicyTest extends TestWithFeService {
+
+    private static final String DB_NAME = "leading_hint_row_policy";
+    private static final String TABLE_1 = "leading_hint_t1";
+    private static final String TABLE_2 = "leading_hint_t2";
+    private static final String MASKED_TABLE = "leading_hint_masked";
+    private static final String USER_NAME = "leading_hint_user";
+    private static final String POLICY_NAME = "leading_hint_policy";
+
+    @Override
+    protected void runBeforeAll() throws Exception {
+        FeConstants.runningUnitTest = true;
+        createDatabase(DB_NAME);
+        useDatabase(DB_NAME);
+        createTable("create table " + TABLE_1 + " (k int, v int) distributed 
by hash(k) buckets 1"
+                + " properties(\"replication_num\" = \"1\");");
+        createTable("create table " + TABLE_2 + " (k int, v int) distributed 
by hash(k) buckets 1"
+                + " properties(\"replication_num\" = \"1\");");
+        createTable("create table " + MASKED_TABLE + " (k int, v int) 
distributed by hash(k) buckets 1"
+                + " properties(\"replication_num\" = \"1\");");
+
+        // create user and grant privilege, so that the row policy and the 
data mask policy can be evaluated
+        UserIdentity user = new UserIdentity(USER_NAME, "%");
+        user.analyze();
+        CreateUserCommand createUserCommand = new CreateUserCommand(new 
CreateUserInfo(new UserDesc(user)));
+        createUserCommand.getInfo().validate();
+        Env.getCurrentEnv().getAuth().createUser(createUserCommand.getInfo());
+        List<AccessPrivilegeWithCols> privileges = Lists
+                .newArrayList(new 
AccessPrivilegeWithCols(AccessPrivilege.ADMIN_PRIV));
+        TablePattern tablePattern = new TablePattern("*", "*", "*");
+        tablePattern.analyze();
+        GrantTablePrivilegeCommand grantTablePrivilegeCommand = new 
GrantTablePrivilegeCommand(
+                privileges, tablePattern, Optional.of(user), Optional.empty());
+        grantTablePrivilegeCommand.validate();
+        
Env.getCurrentEnv().getAuth().grantTablePrivilegeCommand(grantTablePrivilegeCommand);
+
+        // the data mask policy is provided by the external auth plugin, mock 
it for the masked table
+        AccessControllerManager spyAcm = 
Mockito.spy(Env.getCurrentEnv().getAccessManager());
+        // Masks are asked for one table at a time, keyed by the lower-cased 
column name - that is the shape
+        // the planner asks in and reads back, so a stub on the per-column 
method would never be reached.
+        Mockito.doAnswer(invocation -> {
+            String tbl = invocation.getArgument(3);
+            Set<String> cols = invocation.getArgument(4);
+            if (!tbl.equalsIgnoreCase(MASKED_TABLE)) {
+                return Collections.<String, DataMaskSpec>emptyMap();
+            }
+            Map<String, DataMaskSpec> masks = new LinkedHashMap<>();
+            for (String col : cols) {
+                String column = col.toLowerCase(Locale.ROOT);
+                masks.put(column, new DataMaskSpec(
+                        String.format("custom policy: concat(%s, '_****_', 
%s)", column, column),
+                        String.format("concat(%s, '_****_', %s)", column, 
column)));
+            }
+            return masks;
+        }).when(spyAcm).evalDataMaskPolicies(
+                Mockito.any(UserIdentity.class), Mockito.anyString(),
+                Mockito.anyString(), Mockito.anyString(), Mockito.anySet());
+        Deencapsulation.setField(Env.getCurrentEnv(), "accessManager", spyAcm);
+    }
+
+    @Test
+    public void testRowPolicyIsKeptByLeadingHint() throws Exception {
+        useUser(USER_NAME);
+        createPolicy("CREATE ROW POLICY " + POLICY_NAME + " ON " + TABLE_1
+                + " AS RESTRICTIVE TO " + USER_NAME + " USING (k = 1)");
+
+        // the hint reverses the join order, so the rebuilt join proves that 
the hint is really applied
+        PlanChecker planChecker = PlanChecker.from(connectContext)
+                .analyze("SELECT /*+ leading(" + TABLE_2 + " " + TABLE_1 + ") 
*/ "
+                        + TABLE_1 + ".k, " + TABLE_1 + ".v, " + TABLE_2 + ".v 
FROM "
+                        + TABLE_1 + " JOIN " + TABLE_2 + " ON " + TABLE_1 + 
".k = " + TABLE_2 + ".k");
+        
Assertions.assertTrue(planChecker.getCascadesContext().getHintMap().get("Leading").isSuccess());
+        Plan plan = planChecker.getPlan();
+
+        LogicalJoin<?, ?> join = findJoin(plan);
+        Assertions.assertNotNull(join, () -> "join is missing in plan:\n" + 
plan.treeString());
+        Assertions.assertInstanceOf(LogicalOlapScan.class, join.left(),
+                () -> "unexpected join order of leading hint:\n" + 
plan.treeString());
+        Assertions.assertEquals(TABLE_2, ((LogicalOlapScan) 
join.left()).getTable().getName());
+        Assertions.assertInstanceOf(LogicalFilter.class, join.right(),
+                () -> "row policy filter is dropped by leading hint:\n" + 
plan.treeString());
+        LogicalFilter<?> policyFilter = (LogicalFilter<?>) join.right();
+        Assertions.assertEquals(1, policyFilter.getConjuncts().size());
+        
Assertions.assertTrue(policyFilter.getConjuncts().toString().contains("= 1"),
+                () -> "unexpected row policy filter: " + 
policyFilter.getConjuncts());
+        Assertions.assertInstanceOf(LogicalOlapScan.class, 
policyFilter.child());
+        Assertions.assertEquals(TABLE_1, ((LogicalOlapScan) 
policyFilter.child()).getTable().getName());
+
+        dropPolicy("DROP ROW POLICY " + POLICY_NAME + " ON " + TABLE_1);
+    }
+
+    @Test
+    public void testRowPolicyAndDataMaskAreKeptByLeadingHint() throws 
Exception {
+        useUser(USER_NAME);
+        createPolicy("CREATE ROW POLICY " + POLICY_NAME + " ON " + MASKED_TABLE
+                + " AS RESTRICTIVE TO " + USER_NAME + " USING (k = 1)");
+
+        PlanChecker planChecker = PlanChecker.from(connectContext)
+                .analyze("SELECT /*+ leading(" + TABLE_2 + " " + MASKED_TABLE 
+ ") */ "
+                        + MASKED_TABLE + ".k, " + MASKED_TABLE + ".v, " + 
TABLE_2 + ".v FROM "
+                        + MASKED_TABLE + " JOIN " + TABLE_2 + " ON " + 
MASKED_TABLE + ".k = " + TABLE_2 + ".k");
+        
Assertions.assertTrue(planChecker.getCascadesContext().getHintMap().get("Leading").isSuccess());
+        Plan plan = planChecker.getPlan();
+
+        // both the data mask project and the row policy filter are kept on 
the leaf of the leading hint
+        LogicalJoin<?, ?> join = findJoin(plan);
+        Assertions.assertNotNull(join, () -> "join is missing in plan:\n" + 
plan.treeString());
+        Assertions.assertInstanceOf(LogicalProject.class, join.right(),
+                () -> "data mask project is dropped by leading hint:\n" + 
plan.treeString());
+        Plan policyLeaf = join.right().child(0);
+        Assertions.assertInstanceOf(LogicalFilter.class, policyLeaf,
+                () -> "row policy filter is dropped by leading hint:\n" + 
plan.treeString());
+        LogicalFilter<?> policyFilter = (LogicalFilter<?>) policyLeaf;
+        Assertions.assertEquals(1, policyFilter.getConjuncts().size());
+        
Assertions.assertTrue(policyFilter.getConjuncts().toString().contains("= 1"),
+                () -> "unexpected row policy filter: " + 
policyFilter.getConjuncts());
+        Assertions.assertInstanceOf(LogicalOlapScan.class, 
policyFilter.child());
+        Assertions.assertEquals(MASKED_TABLE,
+                ((LogicalOlapScan) policyFilter.child()).getTable().getName());
+
+        dropPolicy("DROP ROW POLICY " + POLICY_NAME + " ON " + MASKED_TABLE);
+    }
+
+    private LogicalJoin<?, ?> findJoin(Plan plan) {
+        if (plan instanceof LogicalJoin) {
+            return (LogicalJoin<?, ?>) plan;
+        }
+        for (Plan child : plan.children()) {
+            LogicalJoin<?, ?> join = findJoin(child);
+            if (join != null) {
+                return join;
+            }
+        }
+        return null;
+    }
+}
diff --git a/regression-test/data/query_p0/hint/test_leading_row_policy.out 
b/regression-test/data/query_p0/hint/test_leading_row_policy.out
new file mode 100644
index 00000000000..2d60899c86b
--- /dev/null
+++ b/regression-test/data/query_p0/hint/test_leading_row_policy.out
@@ -0,0 +1,22 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !read_table --
+1      10
+
+-- !join_without_hint --
+1      10      100
+
+-- !join_with_leading --
+1      10      100
+
+-- !join_with_leading_swapped --
+1      10      100
+
+-- !left_join_with_leading --
+1      10      100
+
+-- !agg_table_without_hint --
+1      40      100
+
+-- !agg_table_with_leading --
+1      40      100
+
diff --git 
a/regression-test/suites/query_p0/hint/test_leading_row_policy.groovy 
b/regression-test/suites/query_p0/hint/test_leading_row_policy.groovy
new file mode 100644
index 00000000000..c1705941b9b
--- /dev/null
+++ b/regression-test/suites/query_p0/hint/test_leading_row_policy.groovy
@@ -0,0 +1,173 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("test_leading_row_policy") {
+    String dbName = context.config.getDbNameByFile(context.file)
+    String user = "leading_row_policy_user"
+    String pwd = 'C123_567p'
+    def tokens = context.config.jdbcUrl.split('/')
+    def url = tokens[0] + "//" + tokens[2] + "/" + dbName + "?"
+
+    sql "DROP ROW POLICY IF EXISTS leading_row_policy ON 
${dbName}.leading_row_policy_t1 FOR ${user}"
+    sql "DROP ROW POLICY IF EXISTS leading_row_policy_agg ON 
${dbName}.leading_row_policy_agg FOR ${user}"
+    sql "DROP TABLE IF EXISTS leading_row_policy_t1"
+    sql "DROP TABLE IF EXISTS leading_row_policy_t2"
+    sql "DROP TABLE IF EXISTS leading_row_policy_agg"
+    sql """
+        CREATE TABLE leading_row_policy_t1 (
+            `k` INT,
+            `v` INT
+        ) DUPLICATE KEY (`k`) DISTRIBUTED BY HASH (`k`) BUCKETS 1
+        PROPERTIES ('replication_num' = '1')
+    """
+    sql """
+        CREATE TABLE leading_row_policy_t2 (
+            `k` INT,
+            `v` INT
+        ) DUPLICATE KEY (`k`) DISTRIBUTED BY HASH (`k`) BUCKETS 1
+        PROPERTIES ('replication_num' = '1')
+    """
+    // the aggregate table with random distribution needs an aggregation above 
the scan to merge the rows
+    sql """
+        CREATE TABLE leading_row_policy_agg (
+            `k` INT,
+            `v` INT SUM
+        ) AGGREGATE KEY (`k`) DISTRIBUTED BY RANDOM BUCKETS 1
+        PROPERTIES ('replication_num' = '1')
+    """
+    sql "INSERT INTO leading_row_policy_t1 VALUES (1, 10), (2, 20)"
+    sql "INSERT INTO leading_row_policy_t2 VALUES (1, 100), (2, 200)"
+    sql "INSERT INTO leading_row_policy_agg VALUES (1, 10), (1, 30), (2, 20)"
+
+    sql "DROP USER IF EXISTS ${user}"
+    sql "CREATE USER ${user} IDENTIFIED BY '${pwd}'"
+    sql "GRANT SELECT_PRIV ON internal.${dbName}.leading_row_policy_t1 TO 
${user}"
+    sql "GRANT SELECT_PRIV ON internal.${dbName}.leading_row_policy_t2 TO 
${user}"
+    sql "GRANT SELECT_PRIV ON internal.${dbName}.leading_row_policy_agg TO 
${user}"
+    //cloud-mode
+    // a cloud user is only allowed to use the compute groups it has the usage 
privilege of, without it the
+    // connection is rejected with 
CURRENT_USER_NO_AUTH_TO_USE_ANY_COMPUTE_GROUP before the first query runs
+    if (isCloudMode()) {
+        def clusters = sql " SHOW CLUSTERS; "
+        assertTrue(!clusters.isEmpty())
+        def validCluster = clusters[0][0]
+        sql """GRANT USAGE_PRIV ON CLUSTER `${validCluster}` TO ${user}""";
+    }
+    sql """
+        CREATE ROW POLICY leading_row_policy ON ${dbName}.leading_row_policy_t1
+        AS RESTRICTIVE TO ${user} USING (k = 1)
+    """
+    sql """
+        CREATE ROW POLICY leading_row_policy_agg ON 
${dbName}.leading_row_policy_agg
+        AS RESTRICTIVE TO ${user} USING (k = 1)
+    """
+    sql "SYNC"
+
+    // The tables are referenced by their real name, an alias would add a sub 
query alias node which already
+    // carries the whole plan of the table, so the cases below have to 
exercise the plans which are built
+    // directly on the relation, e.g. the row policy filter.
+    connect(user, "${pwd}", url) {
+        sql "SET enable_sql_cache = false"
+        // the row policy only allows to read the rows of 
leading_row_policy_t1 with k = 1
+        order_qt_read_table "SELECT k, v FROM leading_row_policy_t1 ORDER BY k"
+        // the row policy also applies without any hint
+        order_qt_join_without_hint """
+            SELECT leading_row_policy_t1.k, leading_row_policy_t1.v, 
leading_row_policy_t2.v
+            FROM leading_row_policy_t1 JOIN leading_row_policy_t2
+            ON leading_row_policy_t1.k = leading_row_policy_t2.k
+            ORDER BY leading_row_policy_t1.k
+        """
+        // the leading hint has to be accepted, otherwise the cases below 
exercise nothing
+        explain {
+            sql """
+                SELECT /*+ leading(leading_row_policy_t1 
leading_row_policy_t2) */
+                    leading_row_policy_t1.k, leading_row_policy_t1.v, 
leading_row_policy_t2.v
+                FROM leading_row_policy_t1 JOIN leading_row_policy_t2
+                ON leading_row_policy_t1.k = leading_row_policy_t2.k
+                ORDER BY leading_row_policy_t1.k
+            """
+            contains("Used: leading(leading_row_policy_t1 
leading_row_policy_t2 )")
+        }
+        // a leading hint only changes the join order, it must not change the 
rows allowed by the row policy
+        order_qt_join_with_leading """
+            SELECT /*+ leading(leading_row_policy_t1 leading_row_policy_t2) */
+                leading_row_policy_t1.k, leading_row_policy_t1.v, 
leading_row_policy_t2.v
+            FROM leading_row_policy_t1 JOIN leading_row_policy_t2
+            ON leading_row_policy_t1.k = leading_row_policy_t2.k
+            ORDER BY leading_row_policy_t1.k
+        """
+        explain {
+            sql """
+                SELECT /*+ leading(leading_row_policy_t2 
leading_row_policy_t1) */
+                    leading_row_policy_t1.k, leading_row_policy_t1.v, 
leading_row_policy_t2.v
+                FROM leading_row_policy_t1 JOIN leading_row_policy_t2
+                ON leading_row_policy_t1.k = leading_row_policy_t2.k
+                ORDER BY leading_row_policy_t1.k
+            """
+            contains("Used: leading(leading_row_policy_t2 
leading_row_policy_t1 )")
+        }
+        order_qt_join_with_leading_swapped """
+            SELECT /*+ leading(leading_row_policy_t2 leading_row_policy_t1) */
+                leading_row_policy_t1.k, leading_row_policy_t1.v, 
leading_row_policy_t2.v
+            FROM leading_row_policy_t1 JOIN leading_row_policy_t2
+            ON leading_row_policy_t1.k = leading_row_policy_t2.k
+            ORDER BY leading_row_policy_t1.k
+        """
+        explain {
+            sql """
+                SELECT /*+ leading(leading_row_policy_t1 
leading_row_policy_t2) */
+                    leading_row_policy_t1.k, leading_row_policy_t1.v, 
leading_row_policy_t2.v
+                FROM leading_row_policy_t1 LEFT JOIN leading_row_policy_t2
+                ON leading_row_policy_t1.k = leading_row_policy_t2.k
+                ORDER BY leading_row_policy_t1.k
+            """
+            contains("Used: leading(leading_row_policy_t1 
leading_row_policy_t2 )")
+        }
+        order_qt_left_join_with_leading """
+            SELECT /*+ leading(leading_row_policy_t1 leading_row_policy_t2) */
+                leading_row_policy_t1.k, leading_row_policy_t1.v, 
leading_row_policy_t2.v
+            FROM leading_row_policy_t1 LEFT JOIN leading_row_policy_t2
+            ON leading_row_policy_t1.k = leading_row_policy_t2.k
+            ORDER BY leading_row_policy_t1.k
+        """
+        // the random distribution aggregate table needs the aggregation which 
merges the rows of the table,
+        // both the aggregation and the row policy are built on the relation 
and have to be kept as well
+        explain {
+            sql """
+                SELECT /*+ leading(leading_row_policy_agg 
leading_row_policy_t2) */
+                    leading_row_policy_agg.k, leading_row_policy_agg.v, 
leading_row_policy_t2.v
+                FROM leading_row_policy_agg JOIN leading_row_policy_t2
+                ON leading_row_policy_agg.k = leading_row_policy_t2.k
+                ORDER BY leading_row_policy_agg.k
+            """
+            contains("Used: leading(leading_row_policy_agg 
leading_row_policy_t2 )")
+        }
+        order_qt_agg_table_without_hint """
+            SELECT leading_row_policy_agg.k, leading_row_policy_agg.v, 
leading_row_policy_t2.v
+            FROM leading_row_policy_agg JOIN leading_row_policy_t2
+            ON leading_row_policy_agg.k = leading_row_policy_t2.k
+            ORDER BY leading_row_policy_agg.k
+        """
+        order_qt_agg_table_with_leading """
+            SELECT /*+ leading(leading_row_policy_agg leading_row_policy_t2) */
+                leading_row_policy_agg.k, leading_row_policy_agg.v, 
leading_row_policy_t2.v
+            FROM leading_row_policy_agg JOIN leading_row_policy_t2
+            ON leading_row_policy_agg.k = leading_row_policy_t2.k
+            ORDER BY leading_row_policy_agg.k
+        """
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to