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]

Reply via email to