From 046ede1e564af2ed161b609d3ed714d38f3a46b3 Mon Sep 17 00:00:00 2001 From: waterWang Date: Mon, 24 Aug 2026 22:52:48 +0800 Subject: [PATCH] fix: handle stale TID in VLE path materialization after DETACH DELETE (#2549) Fixes apache/age#2549 --- regress/expected/cypher_vle.out | 46 ++++++++ regress/sql/cypher_vle.sql | 20 ++++ src/backend/utils/adt/age_global_graph.c | 128 +++++++++++++++++++++++ src/backend/utils/adt/age_vle.c | 6 +- src/include/utils/age_global_graph.h | 5 + 5 files changed, 202 insertions(+), 3 deletions(-) diff --git a/regress/expected/cypher_vle.out b/regress/expected/cypher_vle.out index de85176b6..41b65d876 100644 --- a/regress/expected/cypher_vle.out +++ b/regress/expected/cypher_vle.out @@ -1334,6 +1334,52 @@ NOTICE: graph "issue_2382" has been dropped (1 row) +-- +-- Issue #2549: VLE path + DETACH DELETE + RETURN p should not raise +-- stale TID error. When the path's bound vertices are deleted before +-- the path datum is materialized, build_path must tolerate the stale +-- TID and return the path with empty properties ({}) for the deleted +-- entities. +-- +SELECT create_graph('issue_2549'); +NOTICE: graph "issue_2549" has been created + create_graph +-------------- + +(1 row) + +SELECT * FROM cypher('issue_2549', $$ + CREATE (a {id: 1})<-[:R]-(b {id: 2}), + (a)<-[:R]-(c {id: 3}) + RETURN a +$$) AS (v agtype); + v +----------------------------------------------------------------------- + {"id": 281474976710657, "label": "", "properties": {"id": 1}}::vertex +(1 row) + +SELECT * FROM cypher('issue_2549', $$ + MATCH p = (n0)<-[:R*..2]-(n1) + DETACH DELETE n0, n1 + RETURN p +$$) AS (p agtype); + p +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + [{"id": 281474976710657, "label": "_ag_label_vertex", "properties": {"id": 1}}::vertex, {"id": 844424930131970, "label": "R", "end_id": 281474976710657, "start_id": 281474976710659, "properties": {}}::edge, {"id": 281474976710659, "label": "_ag_label_vertex", "properties": {"id": 3}}::vertex]::path + [{"id": 281474976710657, "label": "_ag_label_vertex", "properties": {}}::vertex, {"id": 844424930131969, "label": "R", "end_id": 281474976710657, "start_id": 281474976710658, "properties": {}}::edge, {"id": 281474976710658, "label": "_ag_label_vertex", "properties": {"id": 2}}::vertex]::path +(2 rows) + +SELECT drop_graph('issue_2549', true); +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to table issue_2549._ag_label_vertex +drop cascades to table issue_2549._ag_label_edge +drop cascades to table issue_2549."R" +NOTICE: graph "issue_2549" has been dropped + drop_graph +------------ + +(1 row) + -- -- End -- diff --git a/regress/sql/cypher_vle.sql b/regress/sql/cypher_vle.sql index 5f3f54ed2..5a5326ff2 100644 --- a/regress/sql/cypher_vle.sql +++ b/regress/sql/cypher_vle.sql @@ -487,6 +487,26 @@ $$) AS (person agtype, friend agtype); SELECT drop_graph('issue_2382', true); +-- +-- Issue #2549: VLE path + DETACH DELETE + RETURN p should not raise +-- stale TID error. When the path's bound vertices are deleted before +-- the path datum is materialized, build_path must tolerate the stale +-- TID and return the path with empty properties ({}) for the deleted +-- entities. +-- +SELECT create_graph('issue_2549'); +SELECT * FROM cypher('issue_2549', $$ + CREATE (a {id: 1})<-[:R]-(b {id: 2}), + (a)<-[:R]-(c {id: 3}) + RETURN a +$$) AS (v agtype); +SELECT * FROM cypher('issue_2549', $$ + MATCH p = (n0)<-[:R*..2]-(n1) + DETACH DELETE n0, n1 + RETURN p +$$) AS (p agtype); +SELECT drop_graph('issue_2549', true); + -- -- End -- diff --git a/src/backend/utils/adt/age_global_graph.c b/src/backend/utils/adt/age_global_graph.c index 397e0511c..613b190ff 100644 --- a/src/backend/utils/adt/age_global_graph.c +++ b/src/backend/utils/adt/age_global_graph.c @@ -41,6 +41,7 @@ #include "utils/age_global_graph.h" #include "utils/agehash.h" +#include "utils/agtype.h" #include "catalog/ag_graph.h" #include "catalog/ag_label.h" #include "utils/ag_cache.h" @@ -1426,6 +1427,74 @@ Datum get_vertex_entry_properties(vertex_entry *ve) return result; } +/* + * Tolerant variant of get_vertex_entry_properties used by the VLE path + * materializer. + * + * The VLE SRF stores paths as interleaved graphid arrays and only lazily + * fetches vertex properties when the path datum is projected (build_path). + * When the path's bound vertices were deleted earlier in the same query + * (e.g. `MATCH p = ... DETACH DELETE ... RETURN p`), the stored TID no + * longer resolves to a visible tuple. The non-VLE path executor builds its + * path datums eagerly (before DELETE runs), so it never hits this case; + * build_path must tolerate it instead of raising an internal error. + * + * Returns an empty agtype object ({}) as the properties when the tuple is + * no longer visible, so the path can still be projected with the vertex + * present but its properties empty. + */ +Datum get_vertex_entry_properties_graceful(vertex_entry *ve) +{ + Relation rel; + HeapTupleData tuple; + Buffer buffer = InvalidBuffer; + Datum result = (Datum) 0; + + rel = table_open(ve->vertex_label_table_oid, AccessShareLock); + tuple.t_self = ve->tid; + + if (heap_fetch(rel, GetActiveSnapshot(), &tuple, &buffer, true)) + { + TupleDesc tupdesc = RelationGetDescr(rel); + bool isnull; + Datum props; + + /* properties is column 2 (1-indexed) */ + props = heap_getattr(&tuple, 2, tupdesc, &isnull); + if (!isnull) + { + result = datumCopy(props, false, -1); + } + + ReleaseBuffer(buffer); + buffer = InvalidBuffer; + } + + table_close(rel, AccessShareLock); + + /* defensive: release any leftover pin (PG 18 heap_fetch may pin + * buffer in some failure paths) */ + if (BufferIsValid(buffer)) + ReleaseBuffer(buffer); + + if (result == (Datum) 0) + { + agtype_in_state result_state; + + /* build an empty object ({}) as the properties */ + memset(&result_state, 0, sizeof(agtype_in_state)); + push_agtype_value(&result_state.parse_state, WAGT_BEGIN_OBJECT, NULL); + result_state.res = push_agtype_value(&result_state.parse_state, + WAGT_END_OBJECT, NULL); + + result = AGTYPE_P_GET_DATUM(agtype_value_to_agtype(result_state.res)); + + pfree_agtype_in_state(&result_state); + } + + return result; +} + /* edge_entry accessor functions */ graphid get_edge_entry_id(edge_entry *ee) { @@ -1486,6 +1555,65 @@ Datum get_edge_entry_properties(edge_entry *ee) return result; } +/* + * Tolerant variant of get_edge_entry_properties used by the VLE path + * materializer. See get_vertex_entry_properties_graceful for rationale: + * when the path's bound edges were deleted earlier in the same query, the + * stored TID no longer resolves to a visible tuple, and the path must be + * projectable with an empty properties object ({}) instead of raising an + * internal error. + */ +Datum get_edge_entry_properties_graceful(edge_entry *ee) +{ + Relation rel; + HeapTupleData tuple; + Buffer buffer = InvalidBuffer; + Datum result = (Datum) 0; + + rel = table_open(ee->edge_label_table_oid, AccessShareLock); + tuple.t_self = ee->tid; + + if (heap_fetch(rel, GetActiveSnapshot(), &tuple, &buffer, true)) + { + TupleDesc tupdesc = RelationGetDescr(rel); + bool isnull; + Datum props; + + /* properties is column 4 (1-indexed) */ + props = heap_getattr(&tuple, 4, tupdesc, &isnull); + if (!isnull) + { + result = datumCopy(props, false, -1); + } + + ReleaseBuffer(buffer); + buffer = InvalidBuffer; + } + + table_close(rel, AccessShareLock); + + /* defensive: release any leftover pin */ + if (BufferIsValid(buffer)) + ReleaseBuffer(buffer); + + if (result == (Datum) 0) + { + agtype_in_state result_state; + + /* build an empty object ({}) as the properties */ + memset(&result_state, 0, sizeof(agtype_in_state)); + push_agtype_value(&result_state.parse_state, WAGT_BEGIN_OBJECT, NULL); + result_state.res = push_agtype_value(&result_state.parse_state, + WAGT_END_OBJECT, NULL); + + result = AGTYPE_P_GET_DATUM(agtype_value_to_agtype(result_state.res)); + + pfree_agtype_in_state(&result_state); + } + + return result; +} + graphid get_edge_entry_start_vertex_id(edge_entry *ee) { return ee->start_vertex_id; diff --git a/src/backend/utils/adt/age_vle.c b/src/backend/utils/adt/age_vle.c index cb036b154..e6cc40a06 100644 --- a/src/backend/utils/adt/age_vle.c +++ b/src/backend/utils/adt/age_vle.c @@ -1782,7 +1782,7 @@ static agtype_value *build_edge_list(VLE_path_container *vpc) agtv_edge = agtype_value_build_edge(get_edge_entry_id(ee), label_name, get_edge_entry_end_vertex_id(ee), get_edge_entry_start_vertex_id(ee), - get_edge_entry_properties(ee)); + get_edge_entry_properties_graceful(ee)); /* push the edge*/ edges_result.res = push_agtype_value(&edges_result.parse_state, WAGT_ELEM, agtv_edge); @@ -1847,7 +1847,7 @@ static agtype_value *build_path(VLE_path_container *vpc) /* reconstruct the vertex */ agtv_vertex = agtype_value_build_vertex(get_vertex_entry_id(ve), label_name, - get_vertex_entry_properties(ve)); + get_vertex_entry_properties_graceful(ve)); /* push the vertex */ path_result.res = push_agtype_value(&path_result.parse_state, WAGT_ELEM, agtv_vertex); @@ -1869,7 +1869,7 @@ static agtype_value *build_path(VLE_path_container *vpc) agtv_edge = agtype_value_build_edge(get_edge_entry_id(ee), label_name, get_edge_entry_end_vertex_id(ee), get_edge_entry_start_vertex_id(ee), - get_edge_entry_properties(ee)); + get_edge_entry_properties_graceful(ee)); /* push the edge*/ path_result.res = push_agtype_value(&path_result.parse_state, WAGT_ELEM, agtv_edge); diff --git a/src/include/utils/age_global_graph.h b/src/include/utils/age_global_graph.h index d68530a91..38db9b1e1 100644 --- a/src/include/utils/age_global_graph.h +++ b/src/include/utils/age_global_graph.h @@ -77,6 +77,11 @@ edge_entry *get_edge_entry_with_hash(GRAPH_global_context *ggctx, graphid get_vertex_entry_id(vertex_entry *ve); Oid get_vertex_entry_label_table_oid(vertex_entry *ve); Datum get_vertex_entry_properties(vertex_entry *ve); +/* tolerant variants for VLE path materialization: return empty object ({}) + * when the referenced tuple is no longer visible instead of raising + * a stale-TID error (see age_vle.c build_path) */ +Datum get_vertex_entry_properties_graceful(vertex_entry *ve); +Datum get_edge_entry_properties_graceful(edge_entry *ee); /* * Flat-array adjacency accessors. Returned pointer is into the entry's