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

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

commit 01bffce3bd8bc74aba55938459b27e1b54cd2482
Author: John Gemignani <[email protected]>
AuthorDate: Thu Oct 8 05:31:22 2026 +0000

    Return no paths when RLS hides every vertex
    
    A traversal raised "age_vle: empty graph" when the graph held no
    vertices. Since #2561 the traversal graph holds only the vertices the
    caller may see, so a policy that hides every vertex turned an ordinary
    query into an error, although MATCH over the same labels returns no
    rows (review issue 4 on #2561).
    
    Return no paths instead. The check runs before the traversal state is
    allocated, which reduces what a cached call on this path leaves in
    long-lived memory from 352 to 64 bytes. The remaining bytes come from
    parsing the graph name argument in the long-lived context. That predates
    PR #2561, also happens on every cache miss, and is left to separate
    hardening.
    
    The new rls_vle part hides every vertex and checks a variable-length
    MATCH, a zero-length lower bound and a direct age_vle() call.
    
    Co-authored-by: Copilot <[email protected]>
---
 regress/expected/rls_vle.out    | 43 +++++++++++++++++++++++++++++++++++++++++
 regress/sql/rls_vle.sql         | 25 ++++++++++++++++++++++++
 src/backend/utils/adt/age_vle.c | 17 +++++++++++-----
 3 files changed, 80 insertions(+), 5 deletions(-)

diff --git a/regress/expected/rls_vle.out b/regress/expected/rls_vle.out
index f8edf71e..3a6f7265 100644
--- a/regress/expected/rls_vle.out
+++ b/regress/expected/rls_vle.out
@@ -1341,6 +1341,49 @@ GRANT SELECT ON rls_vle."Node" TO rls_vle_user;
 DROP POLICY node_all ON rls_vle."Node";
 ALTER TABLE rls_vle."Node" DISABLE ROW LEVEL SECURITY;
 -- ============================================================================
+-- PART 11: No visible vertices. When a policy hides every vertex, a traversal
+-- has nothing to traverse and returns no paths instead of an error. The
+-- untyped MATCH still returns the hidden vertices from the parent table, so
+-- each traversal starts from a vertex outside the visible graph.
+-- ============================================================================
+ALTER TABLE rls_vle."Node" ENABLE ROW LEVEL SECURITY;
+CREATE POLICY node_none ON rls_vle."Node" FOR SELECT USING (false);
+SET ROLE rls_vle_user;
+SELECT * FROM cypher('rls_vle', $$
+    MATCH (n:Node) RETURN count(n)
+$$) AS (count agtype);
+ count 
+-------
+ 0
+(1 row)
+
+SELECT * FROM cypher('rls_vle', $$
+    MATCH (a)-[:LINK*1..3]->(b) RETURN b.name
+$$) AS (name agtype);
+ name 
+------
+(0 rows)
+
+SELECT * FROM cypher('rls_vle', $$
+    MATCH (a)-[:LINK*0..1]->(b) RETURN count(*)
+$$) AS (count agtype);
+ count 
+-------
+ 0
+(1 row)
+
+SELECT count(*) AS paths
+FROM age_vle('"rls_vle"', NULL, NULL,
+             age_build_vle_match_edge('"LINK"', NULL), '1', '3', '1');
+ paths 
+-------
+     0
+(1 row)
+
+RESET ROLE;
+DROP POLICY node_none ON rls_vle."Node";
+ALTER TABLE rls_vle."Node" DISABLE ROW LEVEL SECURITY;
+-- ============================================================================
 -- cleanup
 -- ============================================================================
 SELECT drop_graph('rls_vle', true);
diff --git a/regress/sql/rls_vle.sql b/regress/sql/rls_vle.sql
index ebb9bc5e..b2738a62 100644
--- a/regress/sql/rls_vle.sql
+++ b/regress/sql/rls_vle.sql
@@ -848,6 +848,31 @@ GRANT SELECT ON rls_vle."Node" TO rls_vle_user;
 DROP POLICY node_all ON rls_vle."Node";
 ALTER TABLE rls_vle."Node" DISABLE ROW LEVEL SECURITY;
 
+-- ============================================================================
+-- PART 11: No visible vertices. When a policy hides every vertex, a traversal
+-- has nothing to traverse and returns no paths instead of an error. The
+-- untyped MATCH still returns the hidden vertices from the parent table, so
+-- each traversal starts from a vertex outside the visible graph.
+-- ============================================================================
+ALTER TABLE rls_vle."Node" ENABLE ROW LEVEL SECURITY;
+CREATE POLICY node_none ON rls_vle."Node" FOR SELECT USING (false);
+SET ROLE rls_vle_user;
+SELECT * FROM cypher('rls_vle', $$
+    MATCH (n:Node) RETURN count(n)
+$$) AS (count agtype);
+SELECT * FROM cypher('rls_vle', $$
+    MATCH (a)-[:LINK*1..3]->(b) RETURN b.name
+$$) AS (name agtype);
+SELECT * FROM cypher('rls_vle', $$
+    MATCH (a)-[:LINK*0..1]->(b) RETURN count(*)
+$$) AS (count agtype);
+SELECT count(*) AS paths
+FROM age_vle('"rls_vle"', NULL, NULL,
+             age_build_vle_match_edge('"LINK"', NULL), '1', '3', '1');
+RESET ROLE;
+DROP POLICY node_none ON rls_vle."Node";
+ALTER TABLE rls_vle."Node" DISABLE ROW LEVEL SECURITY;
+
 -- ============================================================================
 -- cleanup
 -- ============================================================================
diff --git a/src/backend/utils/adt/age_vle.c b/src/backend/utils/adt/age_vle.c
index d3f22de5..c4dc7efd 100644
--- a/src/backend/utils/adt/age_vle.c
+++ b/src/backend/utils/adt/age_vle.c
@@ -776,6 +776,18 @@ static VLE_local_context 
*build_local_vle_context(FunctionCallInfo fcinfo,
     ggctx = acquire_GRAPH_global_context(graph_name, graph_oid, query_owner,
                                          fcinfo->flinfo);
 
+    /*
+     * The graph can have no vertices the caller may see, for example when
+     * row-level security hides all of them. There is nothing to traverse,
+     * so there are no paths.
+     */
+    if (peek_stack_head(get_graph_vertices(ggctx)) == NULL)
+    {
+        pfree_if_not_null(graph_name);
+        MemoryContextSwitchTo(oldctx);
+        return NULL;
+    }
+
     /* allocate and initialize local VLE context */
     vlelctx = palloc0(sizeof(VLE_local_context));
 
@@ -798,11 +810,6 @@ static VLE_local_context 
*build_local_vle_context(FunctionCallInfo fcinfo,
     /* initialize the next vertex, in this case the first */
     vlelctx->next_vertex = peek_stack_head(get_graph_vertices(ggctx));
 
-    /* if there isn't one, the graph is empty */
-    if (vlelctx->next_vertex == NULL)
-    {
-        elog(ERROR, "age_vle: empty graph");
-    }
     /*
      * Get the start vertex id - this is an optional parameter and determines
      * which path function is used. If a start vertex isn't provided, we

Reply via email to