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
