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]