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 bc57f62b215 [fix](fe) Fix SHOW ROW POLICY failing on a predicate with
a user variable (#68795)
bc57f62b215 is described below
commit bc57f62b2152ebfa52eca22458f837b02d5cfb34
Author: Mr.Vitvara Varavithya <[email protected]>
AuthorDate: Thu Oct 29 13:40:38 2026 +0700
[fix](fe) Fix SHOW ROW POLICY failing on a predicate with a user variable
(#68795)
A row policy may compare a column against a user variable, so each
session decides which rows it sees:
```sql
CREATE ROW POLICY p ON db.t AS RESTRICTIVE TO ROLE r
USING (restaurant_id = @authorized_restaurant);
```
The policy is created, but `SHOW ROW POLICY` then fails with `Invalid
call to sql on unbound object`, for every listing that includes it.
Root cause: `RowPolicy#getShowInfo` renders the stored predicate with
`toSql()`. The variable is still an `UnboundVariable` (it is bound per
query, not at create time), and `UnboundVariable` does not override
`computeToSql`, so the base implementation throws.
Fix: implement `UnboundVariable#computeToSql`. A user variable is always
rendered backquoted, with any backtick inside the name doubled (the
standard identifier escape), because the parser strips the backticks of
a quoted name such as ``@`allowed-region` `` and a bare rendering would
read back as a subtraction. System variables render as `@@name`,
`@@session.name` or `@@global.name`. The output re-parses to the same
variable for every name the parser accepts. The bound `Variable`
renderer is left unchanged: a bound variable is never re-parsed.
Before: `SHOW ROW POLICY` → `ERROR: Invalid call to sql on unbound
object`
After: `SHOW ROW POLICY` → ``WherePredicate = (restaurant_id =
@`authorized_restaurant`)``
Hit on 4.1.3 in production. Please add `dev/4.1.x`; the automatic pick
might conflict on a test file that only exists on master, so I'll open
the backport PR manually after merge.
---
.../doris/nereids/analyzer/UnboundVariable.java | 14 ++++
.../nereids/analyzer/UnboundVariableTest.java | 59 +++++++++++++++
.../doris/policy/RowPolicyFilterSqlTest.java | 29 +++++++-
.../account_p0/test_row_policy_user_variable.out | 18 +++++
.../test_row_policy_user_variable.groovy | 83 ++++++++++++++++++++++
5 files changed, 202 insertions(+), 1 deletion(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/UnboundVariable.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/UnboundVariable.java
index 66e19e486c1..fadd5a6defb 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/UnboundVariable.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/UnboundVariable.java
@@ -80,6 +80,20 @@ public class UnboundVariable extends Expression implements
Unbound {
}
}
+ @Override
+ public String computeToSql() {
+ switch (type) {
+ case USER:
+ return "@`" + name.replace("`", "``") + "`";
+ case GLOBAL:
+ return "@@global." + name;
+ case SESSION:
+ return "@@session." + name;
+ default:
+ return "@@" + name;
+ }
+ }
+
@Override
public String toDigest() {
return toString();
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/analyzer/UnboundVariableTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/analyzer/UnboundVariableTest.java
new file mode 100644
index 00000000000..da20f102ebd
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/analyzer/UnboundVariableTest.java
@@ -0,0 +1,59 @@
+// 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.analyzer;
+
+import org.apache.doris.nereids.analyzer.UnboundVariable.VariableType;
+import org.apache.doris.nereids.parser.NereidsParser;
+import org.apache.doris.nereids.trees.expressions.Expression;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+/**
+ * An unbound variable renders as SQL the parser reads back as the same
variable. A user variable name is
+ * stored without the backticks it may have been written with, and may need
them to be read back, so it is
+ * always rendered backquoted.
+ */
+public class UnboundVariableTest {
+
+ private static final NereidsParser PARSER = new NereidsParser();
+
+ @ParameterizedTest(name = "{0} -> {1}")
+ @CsvSource({
+ "@authorized_region, @`authorized_region`, USER",
+ "@`authorized_region`, @`authorized_region`, USER",
+ "@`authorized-region`, @`authorized-region`, USER",
+ "@`select`, @`select`, USER",
+ "@`authorized``region`, @`authorized``region`, USER",
+ "@`authorized````region`, @`authorized````region`, USER",
+ "@@k1_limit, @@k1_limit, DEFAULT",
+ "@@session.k1_limit, @@session.k1_limit, SESSION",
+ "@@global.k1_limit, @@global.k1_limit, GLOBAL"
+ })
+ public void testToSqlReadsBackAsTheSameVariable(String sql, String
expected, VariableType type) {
+ Expression parsed = PARSER.parseExpression(sql);
+ Assertions.assertInstanceOf(UnboundVariable.class, parsed);
+ Assertions.assertEquals(type, ((UnboundVariable) parsed).getType());
+
+ String rendered = parsed.toSql();
+
+ Assertions.assertEquals(expected, rendered);
+ Assertions.assertEquals(parsed, PARSER.parseExpression(rendered));
+ }
+}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/policy/RowPolicyFilterSqlTest.java
b/fe/fe-core/src/test/java/org/apache/doris/policy/RowPolicyFilterSqlTest.java
index c592f1769a6..d316f9e6083 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/policy/RowPolicyFilterSqlTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/policy/RowPolicyFilterSqlTest.java
@@ -69,7 +69,10 @@ public class RowPolicyFilterSqlTest {
"concat(a, b) = 'ab'",
"substr(phone, 1, 3) = '138'",
"k1 + 1 > 2",
- "k1 = 1 and region in ('cn', 'us') and name like 'x%'"
+ "k1 = 1 and region in ('cn', 'us') and name like 'x%'",
+ "region = @authorized_region",
+ "k1 = @@session.k1_limit",
+ "region = @`authorized-region`"
})
public void testPredicateSurvivesTheRoundTripToThePlanner(String original)
throws AnalysisException {
RowPolicy policy = policyOver(original);
@@ -142,6 +145,30 @@ public class RowPolicyFilterSqlTest {
PARSER.parseExpression(replayed.getFilterSql()));
}
+ /** SHOW ROW POLICY renders a predicate on an unbound user variable
instead of failing. */
+ @Test
+ public void testShowInfoRendersAPredicateOnAUserVariable() throws
AnalysisException {
+ RowPolicy policy = policyOver("region = @authorized_region");
+
+ String shownPredicate = policy.getShowInfo().get(6);
+
+ Assertions.assertEquals(PARSER.parseExpression("region =
@authorized_region"),
+ PARSER.parseExpression(shownPredicate),
+ "SHOW ROW POLICY renders a different predicate than the policy
holds: " + shownPredicate);
+ }
+
+ /** SHOW ROW POLICY keeps the quoting of a user variable name: bare,
@authorized-region reads as a subtraction. */
+ @Test
+ public void testShowInfoKeepsTheQuotingOfAUserVariableName() throws
AnalysisException {
+ RowPolicy policy = policyOver("region = @`authorized-region`");
+
+ String shownPredicate = policy.getShowInfo().get(6);
+
+ Assertions.assertEquals(PARSER.parseExpression("region =
@`authorized-region`"),
+ PARSER.parseExpression(shownPredicate),
+ "SHOW ROW POLICY renders a different predicate than the policy
holds: " + shownPredicate);
+ }
+
/** Builds the policy the way CREATE ROW POLICY does: statement text plus
the predicate parsed from it. */
private RowPolicy policyOver(String predicate) {
String statement = "CREATE ROW POLICY p1 ON db1.t1 AS RESTRICTIVE TO
root USING (" + predicate + ")";
diff --git a/regression-test/data/account_p0/test_row_policy_user_variable.out
b/regression-test/data/account_p0/test_row_policy_user_variable.out
new file mode 100644
index 00000000000..7f3a9870176
--- /dev/null
+++ b/regression-test/data/account_p0/test_row_policy_user_variable.out
@@ -0,0 +1,18 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !show_user_var --
+p_user_var internal regression_test_account_p0
row_policy_user_var_tbl ROW RESTRICTIVE (region = @`authorized_region`)
row_policy_user_var_user \N CREATE ROW POLICY p_user_var ON
regression_test_account_p0.row_policy_user_var_tbl AS RESTRICTIVE TO
row_policy_user_var_user USING (region = @authorized_region)
+
+-- !cn_session --
+cn 1
+
+-- !us_session --
+us 2
+
+-- !unset_session --
+
+-- !show_quoted_var --
+p_quoted_var internal regression_test_account_p0
row_policy_user_var_tbl ROW RESTRICTIVE (region = @`authorized-region`)
row_policy_user_var_user \N CREATE ROW POLICY p_quoted_var ON
regression_test_account_p0.row_policy_user_var_tbl AS RESTRICTIVE TO
row_policy_user_var_user USING (region = @`authorized-region`)
+
+-- !quoted_session --
+de 3
+
diff --git
a/regression-test/suites/account_p0/test_row_policy_user_variable.groovy
b/regression-test/suites/account_p0/test_row_policy_user_variable.groovy
new file mode 100644
index 00000000000..abda528b518
--- /dev/null
+++ b/regression-test/suites/account_p0/test_row_policy_user_variable.groovy
@@ -0,0 +1,83 @@
+// 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.
+
+// A row policy may compare a column against a user variable, so one policy
serves every session and each
+// session says which rows it is allowed to see: USING (region =
@authorized_region). The predicate keeps the
+// variable unbound until a query binds it to the session's value, and SHOW
ROW POLICY used to fail on it
+// with an unbound-expression error - for every policy it listed, not only
this one.
+suite("test_row_policy_user_variable") {
+ def dbName = context.config.getDbNameByFile(context.file)
+ def user = 'row_policy_user_var_user'
+ def pwd = '123abc!@#'
+ def tokens = context.config.jdbcUrl.split('/')
+ def url = tokens[0] + "//" + tokens[2] + "/" + dbName + "?"
+
+ sql "DROP TABLE IF EXISTS row_policy_user_var_tbl"
+ sql """
+ CREATE TABLE row_policy_user_var_tbl (region varchar(8), v int)
+ DISTRIBUTED BY HASH(region) BUCKETS 1 PROPERTIES("replication_num" =
"1")
+ """
+ sql """INSERT INTO row_policy_user_var_tbl VALUES ('cn', 1), ('us', 2),
('de', 3)"""
+
+ sql "DROP USER IF EXISTS ${user}"
+ sql "CREATE USER ${user} IDENTIFIED BY '${pwd}'"
+ sql "GRANT SELECT_PRIV ON ${dbName}.row_policy_user_var_tbl TO ${user}"
+
+ def cloudMode = isCloudMode()
+ if (cloudMode) {
+ def clusters = sql " SHOW CLUSTERS; "
+ assertTrue(!clusters.isEmpty())
+ sql """GRANT USAGE_PRIV ON CLUSTER `${clusters[0][0]}` TO ${user}"""
+ }
+
+ sql "DROP ROW POLICY IF EXISTS p_user_var ON
${dbName}.row_policy_user_var_tbl FOR ${user}"
+ sql "DROP ROW POLICY IF EXISTS p_quoted_var ON
${dbName}.row_policy_user_var_tbl FOR ${user}"
+ sql "CREATE ROW POLICY p_user_var ON ${dbName}.row_policy_user_var_tbl " +
+ "AS RESTRICTIVE TO ${user} USING (region = @authorized_region)"
+
+ // Both the listing that names the policy's grantee and the unfiltered one
include this policy, and each
+ // has to render it rather than fail. The name is rendered backquoted,
which is how any name reads back.
+ order_qt_show_user_var "SHOW ROW POLICY FOR ${user}"
+ assertTrue(sql("SHOW ROW POLICY").any { it[0] == 'p_user_var' },
+ "SHOW ROW POLICY without a filter does not list the policy")
+
+ // The variable is bound per session, so the same policy admits different
rows to different sessions,
+ // and a session that never set it reads nothing: an unset user variable
is NULL.
+ connectToDoris(user, pwd, url) {
+ sql "SET @authorized_region = 'cn'"
+ order_qt_cn_session "SELECT region, v FROM row_policy_user_var_tbl
ORDER BY region"
+ }
+ connectToDoris(user, pwd, url) {
+ sql "SET @authorized_region = 'us'"
+ order_qt_us_session "SELECT region, v FROM row_policy_user_var_tbl
ORDER BY region"
+ }
+ connectToDoris(user, pwd, url) {
+ order_qt_unset_session "SELECT region, v FROM row_policy_user_var_tbl
ORDER BY region"
+ }
+
+ // A quoted variable name is stored without its backticks, so SHOW has to
put them back: rendered bare,
+ // @authorized-region reads as a subtraction, not the variable the policy
enforces. p_user_var is dropped
+ // first: restrictive policies combine with AND, and this session never
sets @authorized_region.
+ sql "DROP ROW POLICY IF EXISTS p_user_var ON
${dbName}.row_policy_user_var_tbl FOR ${user}"
+ sql "CREATE ROW POLICY p_quoted_var ON ${dbName}.row_policy_user_var_tbl "
+
+ "AS RESTRICTIVE TO ${user} USING (region = @`authorized-region`)"
+ order_qt_show_quoted_var "SHOW ROW POLICY FOR ${user}"
+ connectToDoris(user, pwd, url) {
+ sql "SET @`authorized-region` = 'de'"
+ order_qt_quoted_session "SELECT region, v FROM row_policy_user_var_tbl
ORDER BY region"
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]