From 6bc158710832b6e4895954067bdcd8aa5c40e9bd Mon Sep 17 00:00:00 2001 From: John Gemignani Date: Thu, 2 Apr 2026 14:57:46 -0700 Subject: [PATCH] Fix nondeterministic age_global_graph regression test The age_global_graph test had two issues that could cause intermittent failures: 1. Nondeterministic warning output: The graph_stats() call on a graph with deliberately deleted vertices produces WARNING messages for dangling edges. These warnings are emitted by iterating edge label tables (knows, stalks), and the iteration order is not guaranteed. Since PostgreSQL WARNING messages cannot be caught or counted from SQL (only ERROR and above are catchable via PL/pgSQL exception handling), we suppress them with SET client_min_messages = error. The suppressed warnings are documented verbatim in comments. The graph_stats() result row still validates correct dangling-edge handling. 2. Nondeterministic row ordering: Multiple MATCH...RETURN queries returned multi-row results without ORDER BY, relying on scan order. Added ORDER BY id(u), id(v), id(e), id(n), or id(a) as appropriate to all MATCH...RETURN queries for future-proofing, even those currently returning a single row. Files changed: regress/sql/age_global_graph.sql regress/expected/age_global_graph.out Co-authored-by: GitHub Copilot --- regress/expected/age_global_graph.out | 59 +++++++++++++++------------ regress/sql/age_global_graph.sql | 43 +++++++++++++------ 2 files changed, 64 insertions(+), 38 deletions(-) diff --git a/regress/expected/age_global_graph.out b/regress/expected/age_global_graph.out index f2d9b059f..478637800 100644 --- a/regress/expected/age_global_graph.out +++ b/regress/expected/age_global_graph.out @@ -44,19 +44,19 @@ SELECT * FROM cypher('ag_graph_3', $$ CREATE (v:vertex3) RETURN v $$) AS (v agt (1 row) -- load contexts using the vertex_stats command -SELECT * FROM cypher('ag_graph_3', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype); +SELECT * FROM cypher('ag_graph_3', $$ MATCH (u) RETURN vertex_stats(u) ORDER BY id(u) $$) AS (result agtype); result ----------------------------------------------------------------------------------------------- {"id": 844424930131969, "label": "vertex3", "in_degree": 0, "out_degree": 0, "self_loops": 0} (1 row) -SELECT * FROM cypher('ag_graph_2', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype); +SELECT * FROM cypher('ag_graph_2', $$ MATCH (u) RETURN vertex_stats(u) ORDER BY id(u) $$) AS (result agtype); result ----------------------------------------------------------------------------------------------- {"id": 844424930131969, "label": "vertex2", "in_degree": 0, "out_degree": 0, "self_loops": 0} (1 row) -SELECT * FROM cypher('ag_graph_1', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype); +SELECT * FROM cypher('ag_graph_1', $$ MATCH (u) RETURN vertex_stats(u) ORDER BY id(u) $$) AS (result agtype); result ----------------------------------------------------------------------------------------------- {"id": 844424930131969, "label": "vertex1", "in_degree": 0, "out_degree": 0, "self_loops": 0} @@ -64,7 +64,7 @@ SELECT * FROM cypher('ag_graph_1', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (r --- loading undefined contexts --- should throw exception - graph "ag_graph_4" does not exist -SELECT * FROM cypher('ag_graph_4', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype); +SELECT * FROM cypher('ag_graph_4', $$ MATCH (u) RETURN vertex_stats(u) ORDER BY id(u) $$) AS (result agtype); ERROR: graph "ag_graph_4" does not exist LINE 1: SELECT * FROM cypher('ag_graph_4', $$ MATCH (u) RETURN verte... ^ @@ -130,19 +130,19 @@ LINE 1: SELECT * FROM cypher('ag_graph_4', $$ RETURN delete_global_g... -- delete_GRAPH_global_contexts -- -- load contexts again -SELECT * FROM cypher('ag_graph_3', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype); +SELECT * FROM cypher('ag_graph_3', $$ MATCH (u) RETURN vertex_stats(u) ORDER BY id(u) $$) AS (result agtype); result ----------------------------------------------------------------------------------------------- {"id": 844424930131969, "label": "vertex3", "in_degree": 0, "out_degree": 0, "self_loops": 0} (1 row) -SELECT * FROM cypher('ag_graph_2', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype); +SELECT * FROM cypher('ag_graph_2', $$ MATCH (u) RETURN vertex_stats(u) ORDER BY id(u) $$) AS (result agtype); result ----------------------------------------------------------------------------------------------- {"id": 844424930131969, "label": "vertex2", "in_degree": 0, "out_degree": 0, "self_loops": 0} (1 row) -SELECT * FROM cypher('ag_graph_1', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype); +SELECT * FROM cypher('ag_graph_1', $$ MATCH (u) RETURN vertex_stats(u) ORDER BY id(u) $$) AS (result agtype); result ----------------------------------------------------------------------------------------------- {"id": 844424930131969, "label": "vertex1", "in_degree": 0, "out_degree": 0, "self_loops": 0} @@ -193,14 +193,14 @@ SELECT * FROM cypher('ag_graph_2', $$ CREATE (:Person) $$) as (v agtype); (0 rows) ---adding edges between nodes -SELECT * FROM cypher('ag_graph_2', $$ MATCH (a:Person), (b:Person) WHERE a.name = 'A' AND b.name = 'B' CREATE (a)-[e:RELTYPE]->(b) RETURN e $$) as (e agtype); +SELECT * FROM cypher('ag_graph_2', $$ MATCH (a:Person), (b:Person) WHERE a.name = 'A' AND b.name = 'B' CREATE (a)-[e:RELTYPE]->(b) RETURN e ORDER BY id(e) $$) as (e agtype); e --- (0 rows) --checking if vertex stats have been updated along with the new label --should return 3 vertices -SELECT * FROM cypher('ag_graph_1', $$ MATCH (n) RETURN vertex_stats(n) $$) AS (result agtype); +SELECT * FROM cypher('ag_graph_1', $$ MATCH (n) RETURN vertex_stats(n) ORDER BY id(n) $$) AS (result agtype); result ----------------------------------------------------------------------------------------------- {"id": 281474976710657, "label": "", "in_degree": 0, "out_degree": 0, "self_loops": 0} @@ -209,7 +209,7 @@ SELECT * FROM cypher('ag_graph_1', $$ MATCH (n) RETURN vertex_stats(n) $$) AS (r (3 rows) --should return 1 vertice and 1 label -SELECT * FROM cypher('ag_graph_2', $$ MATCH (a) RETURN vertex_stats(a) $$) AS (result agtype); +SELECT * FROM cypher('ag_graph_2', $$ MATCH (a) RETURN vertex_stats(a) ORDER BY id(a) $$) AS (result agtype); result ----------------------------------------------------------------------------------------------- {"id": 844424930131969, "label": "vertex2", "in_degree": 0, "out_degree": 0, "self_loops": 0} @@ -271,7 +271,7 @@ SELECT * FROM cypher('ag_graph_1', $$ MATCH (u)-[]->(v) SET u.id = id(u) SET v.id = id(v) SET u.name = 'u' SET v.name = 'v' - RETURN u,v $$) AS (u agtype, v agtype); + RETURN u,v ORDER BY id(u), id(v) $$) AS (u agtype, v agtype); u | v --------------------------------------------------------------------------------------------------+-------------------------------------------------------------------------------------------------- {"id": 281474976710659, "label": "", "properties": {"id": 281474976710659, "name": "u"}}::vertex | {"id": 281474976710660, "label": "", "properties": {"id": 281474976710660, "name": "v"}}::vertex @@ -285,17 +285,17 @@ SELECT * FROM cypher('ag_graph_1', $$ MATCH (u)-[]->(v) MERGE (v)-[:stalks]->(u) -------- (0 rows) -SELECT * FROM cypher('ag_graph_1', $$ MATCH (u)-[e]->(v) RETURN u, e, v $$) AS (u agtype, e agtype, v agtype); +SELECT * FROM cypher('ag_graph_1', $$ MATCH (u)-[e]->(v) RETURN u, e, v ORDER BY id(e) $$) AS (u agtype, e agtype, v agtype); u | e | v --------------------------------------------------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------------------------------------------------- - {"id": 281474976710660, "label": "", "properties": {"id": 281474976710660, "name": "v"}}::vertex | {"id": 1407374883553281, "label": "stalks", "end_id": 281474976710659, "start_id": 281474976710660, "properties": {}}::edge | {"id": 281474976710659, "label": "", "properties": {"id": 281474976710659, "name": "u"}}::vertex {"id": 281474976710659, "label": "", "properties": {"id": 281474976710659, "name": "u"}}::vertex | {"id": 1125899906842625, "label": "knows", "end_id": 281474976710660, "start_id": 281474976710659, "properties": {}}::edge | {"id": 281474976710660, "label": "", "properties": {"id": 281474976710660, "name": "v"}}::vertex - {"id": 281474976710662, "label": "", "properties": {"id": 281474976710662, "name": "v"}}::vertex | {"id": 1407374883553282, "label": "stalks", "end_id": 281474976710661, "start_id": 281474976710662, "properties": {}}::edge | {"id": 281474976710661, "label": "", "properties": {"id": 281474976710661, "name": "u"}}::vertex {"id": 281474976710661, "label": "", "properties": {"id": 281474976710661, "name": "u"}}::vertex | {"id": 1125899906842626, "label": "knows", "end_id": 281474976710662, "start_id": 281474976710661, "properties": {}}::edge | {"id": 281474976710662, "label": "", "properties": {"id": 281474976710662, "name": "v"}}::vertex - {"id": 281474976710664, "label": "", "properties": {"id": 281474976710664, "name": "v"}}::vertex | {"id": 1407374883553283, "label": "stalks", "end_id": 281474976710663, "start_id": 281474976710664, "properties": {}}::edge | {"id": 281474976710663, "label": "", "properties": {"id": 281474976710663, "name": "u"}}::vertex {"id": 281474976710663, "label": "", "properties": {"id": 281474976710663, "name": "u"}}::vertex | {"id": 1125899906842627, "label": "knows", "end_id": 281474976710664, "start_id": 281474976710663, "properties": {}}::edge | {"id": 281474976710664, "label": "", "properties": {"id": 281474976710664, "name": "v"}}::vertex - {"id": 281474976710666, "label": "", "properties": {"id": 281474976710666, "name": "v"}}::vertex | {"id": 1407374883553284, "label": "stalks", "end_id": 281474976710665, "start_id": 281474976710666, "properties": {}}::edge | {"id": 281474976710665, "label": "", "properties": {"id": 281474976710665, "name": "u"}}::vertex {"id": 281474976710665, "label": "", "properties": {"id": 281474976710665, "name": "u"}}::vertex | {"id": 1125899906842628, "label": "knows", "end_id": 281474976710666, "start_id": 281474976710665, "properties": {}}::edge | {"id": 281474976710666, "label": "", "properties": {"id": 281474976710666, "name": "v"}}::vertex + {"id": 281474976710660, "label": "", "properties": {"id": 281474976710660, "name": "v"}}::vertex | {"id": 1407374883553281, "label": "stalks", "end_id": 281474976710659, "start_id": 281474976710660, "properties": {}}::edge | {"id": 281474976710659, "label": "", "properties": {"id": 281474976710659, "name": "u"}}::vertex + {"id": 281474976710662, "label": "", "properties": {"id": 281474976710662, "name": "v"}}::vertex | {"id": 1407374883553282, "label": "stalks", "end_id": 281474976710661, "start_id": 281474976710662, "properties": {}}::edge | {"id": 281474976710661, "label": "", "properties": {"id": 281474976710661, "name": "u"}}::vertex + {"id": 281474976710664, "label": "", "properties": {"id": 281474976710664, "name": "v"}}::vertex | {"id": 1407374883553283, "label": "stalks", "end_id": 281474976710663, "start_id": 281474976710664, "properties": {}}::edge | {"id": 281474976710663, "label": "", "properties": {"id": 281474976710663, "name": "u"}}::vertex + {"id": 281474976710666, "label": "", "properties": {"id": 281474976710666, "name": "v"}}::vertex | {"id": 1407374883553284, "label": "stalks", "end_id": 281474976710665, "start_id": 281474976710666, "properties": {}}::edge | {"id": 281474976710665, "label": "", "properties": {"id": 281474976710665, "name": "u"}}::vertex (8 rows) -- what is there now? @@ -351,21 +351,30 @@ SELECT * FROM ag_graph_1._ag_label_edge; 1407374883553284 | 281474976710666 | 281474976710665 | {} (8 rows) --- there should be warning messages +-- The graph_stats query below will produce warnings for the dangling edges +-- created by the DELETE commands above. The warnings appear in nondeterministic +-- order because they come from iterating edge label tables (knows, stalks), +-- so we suppress them with client_min_messages. Without suppression, the +-- output would include these warnings (in some order): +-- +-- WARNING: edge: [id: 1125899906842626, start: 281474976710661, end: 281474976710662, label: knows] start and end vertices not found +-- WARNING: ignored malformed or dangling edge +-- WARNING: edge: [id: 1125899906842627, start: 281474976710663, end: 281474976710664, label: knows] end vertex not found +-- WARNING: ignored malformed or dangling edge +-- WARNING: edge: [id: 1407374883553282, start: 281474976710662, end: 281474976710661, label: stalks] start and end vertices not found +-- WARNING: ignored malformed or dangling edge +-- WARNING: edge: [id: 1407374883553283, start: 281474976710664, end: 281474976710663, label: stalks] start vertex not found +-- WARNING: ignored malformed or dangling edge +-- +-- The result row validates that graph_stats handled the dangling edges correctly. +SET client_min_messages = error; SELECT * FROM cypher('ag_graph_1', $$ RETURN graph_stats('ag_graph_1') $$) AS (result agtype); -WARNING: edge: [id: 1125899906842626, start: 281474976710661, end: 281474976710662, label: knows] start and end vertices not found -WARNING: ignored malformed or dangling edge -WARNING: edge: [id: 1125899906842627, start: 281474976710663, end: 281474976710664, label: knows] end vertex not found -WARNING: ignored malformed or dangling edge -WARNING: edge: [id: 1407374883553282, start: 281474976710662, end: 281474976710661, label: stalks] start and end vertices not found -WARNING: ignored malformed or dangling edge -WARNING: edge: [id: 1407374883553283, start: 281474976710664, end: 281474976710663, label: stalks] start vertex not found -WARNING: ignored malformed or dangling edge result -------------------------------------------------------------------------- {"graph": "ag_graph_1", "num_loaded_edges": 8, "num_loaded_vertices": 8} (1 row) +RESET client_min_messages; --drop graphs SELECT * FROM drop_graph('ag_graph_1', true); NOTICE: drop cascades to 5 other objects diff --git a/regress/sql/age_global_graph.sql b/regress/sql/age_global_graph.sql index acd95fbf0..376681a8b 100644 --- a/regress/sql/age_global_graph.sql +++ b/regress/sql/age_global_graph.sql @@ -16,13 +16,13 @@ SELECT * FROM create_graph('ag_graph_3'); SELECT * FROM cypher('ag_graph_3', $$ CREATE (v:vertex3) RETURN v $$) AS (v agtype); -- load contexts using the vertex_stats command -SELECT * FROM cypher('ag_graph_3', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype); -SELECT * FROM cypher('ag_graph_2', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype); -SELECT * FROM cypher('ag_graph_1', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype); +SELECT * FROM cypher('ag_graph_3', $$ MATCH (u) RETURN vertex_stats(u) ORDER BY id(u) $$) AS (result agtype); +SELECT * FROM cypher('ag_graph_2', $$ MATCH (u) RETURN vertex_stats(u) ORDER BY id(u) $$) AS (result agtype); +SELECT * FROM cypher('ag_graph_1', $$ MATCH (u) RETURN vertex_stats(u) ORDER BY id(u) $$) AS (result agtype); --- loading undefined contexts --- should throw exception - graph "ag_graph_4" does not exist -SELECT * FROM cypher('ag_graph_4', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype); +SELECT * FROM cypher('ag_graph_4', $$ MATCH (u) RETURN vertex_stats(u) ORDER BY id(u) $$) AS (result agtype); --- delete with invalid parameter ---should return false @@ -55,9 +55,9 @@ SELECT * FROM cypher('ag_graph_4', $$ RETURN delete_global_graphs('ag_graph_4') -- -- load contexts again -SELECT * FROM cypher('ag_graph_3', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype); -SELECT * FROM cypher('ag_graph_2', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype); -SELECT * FROM cypher('ag_graph_1', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype); +SELECT * FROM cypher('ag_graph_3', $$ MATCH (u) RETURN vertex_stats(u) ORDER BY id(u) $$) AS (result agtype); +SELECT * FROM cypher('ag_graph_2', $$ MATCH (u) RETURN vertex_stats(u) ORDER BY id(u) $$) AS (result agtype); +SELECT * FROM cypher('ag_graph_1', $$ MATCH (u) RETURN vertex_stats(u) ORDER BY id(u) $$) AS (result agtype); -- delete all graph contexts -- should return true @@ -81,14 +81,14 @@ SELECT * FROM cypher('ag_graph_1', $$ CREATE (n), (m) $$) as (v agtype); SELECT * FROM cypher('ag_graph_2', $$ CREATE (:Person) $$) as (v agtype); ---adding edges between nodes -SELECT * FROM cypher('ag_graph_2', $$ MATCH (a:Person), (b:Person) WHERE a.name = 'A' AND b.name = 'B' CREATE (a)-[e:RELTYPE]->(b) RETURN e $$) as (e agtype); +SELECT * FROM cypher('ag_graph_2', $$ MATCH (a:Person), (b:Person) WHERE a.name = 'A' AND b.name = 'B' CREATE (a)-[e:RELTYPE]->(b) RETURN e ORDER BY id(e) $$) as (e agtype); --checking if vertex stats have been updated along with the new label --should return 3 vertices -SELECT * FROM cypher('ag_graph_1', $$ MATCH (n) RETURN vertex_stats(n) $$) AS (result agtype); +SELECT * FROM cypher('ag_graph_1', $$ MATCH (n) RETURN vertex_stats(n) ORDER BY id(n) $$) AS (result agtype); --should return 1 vertice and 1 label -SELECT * FROM cypher('ag_graph_2', $$ MATCH (a) RETURN vertex_stats(a) $$) AS (result agtype); +SELECT * FROM cypher('ag_graph_2', $$ MATCH (a) RETURN vertex_stats(a) ORDER BY id(a) $$) AS (result agtype); -- -- graph_stats command @@ -109,9 +109,9 @@ SELECT * FROM cypher('ag_graph_1', $$ MATCH (u)-[]->(v) SET u.id = id(u) SET v.id = id(v) SET u.name = 'u' SET v.name = 'v' - RETURN u,v $$) AS (u agtype, v agtype); + RETURN u,v ORDER BY id(u), id(v) $$) AS (u agtype, v agtype); SELECT * FROM cypher('ag_graph_1', $$ MATCH (u)-[]->(v) MERGE (v)-[:stalks]->(u) $$) AS (result agtype); -SELECT * FROM cypher('ag_graph_1', $$ MATCH (u)-[e]->(v) RETURN u, e, v $$) AS (u agtype, e agtype, v agtype); +SELECT * FROM cypher('ag_graph_1', $$ MATCH (u)-[e]->(v) RETURN u, e, v ORDER BY id(e) $$) AS (u agtype, e agtype, v agtype); -- what is there now? SELECT * FROM cypher('ag_graph_1', $$ RETURN graph_stats('ag_graph_1') $$) AS (result agtype); -- remove some vertices @@ -121,8 +121,25 @@ DELETE FROM ag_graph_1._ag_label_vertex WHERE id::text = '281474976710662'; DELETE FROM ag_graph_1._ag_label_vertex WHERE id::text = '281474976710664'; SELECT * FROM ag_graph_1._ag_label_vertex; SELECT * FROM ag_graph_1._ag_label_edge; --- there should be warning messages +-- The graph_stats query below will produce warnings for the dangling edges +-- created by the DELETE commands above. The warnings appear in nondeterministic +-- order because they come from iterating edge label tables (knows, stalks), +-- so we suppress them with client_min_messages. Without suppression, the +-- output would include these warnings (in some order): +-- +-- WARNING: edge: [id: 1125899906842626, start: 281474976710661, end: 281474976710662, label: knows] start and end vertices not found +-- WARNING: ignored malformed or dangling edge +-- WARNING: edge: [id: 1125899906842627, start: 281474976710663, end: 281474976710664, label: knows] end vertex not found +-- WARNING: ignored malformed or dangling edge +-- WARNING: edge: [id: 1407374883553282, start: 281474976710662, end: 281474976710661, label: stalks] start and end vertices not found +-- WARNING: ignored malformed or dangling edge +-- WARNING: edge: [id: 1407374883553283, start: 281474976710664, end: 281474976710663, label: stalks] start vertex not found +-- WARNING: ignored malformed or dangling edge +-- +-- The result row validates that graph_stats handled the dangling edges correctly. +SET client_min_messages = error; SELECT * FROM cypher('ag_graph_1', $$ RETURN graph_stats('ag_graph_1') $$) AS (result agtype); +RESET client_min_messages; --drop graphs