Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions age--1.7.0--y.y.y.sql
Original file line number Diff line number Diff line change
Expand Up @@ -408,3 +408,13 @@ $function$;

COMMENT ON FUNCTION ag_catalog.age_pg_upgrade_status() IS
'Returns the current pg_upgrade readiness status of the AGE installation.';

--
-- VLE cache invalidation trigger function
-- Installed on graph label tables to catch SQL-level mutations
-- and increment the per-graph version counter for VLE cache invalidation.
--
CREATE FUNCTION ag_catalog.age_invalidate_graph_cache()
RETURNS trigger
LANGUAGE c
AS 'MODULE_PATHNAME';
174 changes: 174 additions & 0 deletions regress/expected/age_global_graph.out
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,180 @@ NOTICE: graph "ag_graph_3" has been dropped

(1 row)

-----------------------------------------------------------------------------------------------------------------------------
--
-- VLE cache invalidation tests
--
-- These tests verify that the graph version counter properly invalidates
-- the VLE hash table cache when the graph is mutated, and that thin
-- entry lazy property fetch returns correct data.
--
-- Setup: create a graph with a chain a->b->c->d
SELECT * FROM create_graph('vle_cache_test');
NOTICE: graph "vle_cache_test" has been created
create_graph
--------------

(1 row)

SELECT * FROM cypher('vle_cache_test', $$
CREATE (a:Node {name: 'a'})-[:Edge]->(b:Node {name: 'b'})-[:Edge]->(c:Node {name: 'c'})-[:Edge]->(d:Node {name: 'd'})
$$) AS (v agtype);
v
---
(0 rows)

-- VLE query: find all paths from a's neighbors (should find b, b->c, b->c->d)
SELECT * FROM cypher('vle_cache_test', $$
MATCH (a:Node {name: 'a'})-[:Edge*1..3]->(n:Node)
RETURN n.name
ORDER BY n.name
$$) AS (name agtype);
name
------
"b"
"c"
"d"
(3 rows)

-- Now add a new node e connected to d. This should invalidate the cache.
SELECT * FROM cypher('vle_cache_test', $$
MATCH (d:Node {name: 'd'})
CREATE (d)-[:Edge]->(:Node {name: 'e'})
$$) AS (v agtype);
v
---
(0 rows)

-- VLE query again: should now also find e via a->b->c->d->e (4 hops won't reach,
-- but d->e is 1 hop from d, and a->b->c->d->e would be 4 hops from a).
-- Increase range to *1..4 to include e
SELECT * FROM cypher('vle_cache_test', $$
MATCH (a:Node {name: 'a'})-[:Edge*1..4]->(n:Node)
RETURN n.name
ORDER BY n.name
$$) AS (name agtype);
name
------
"b"
"c"
"d"
"e"
(4 rows)

-- Test cache invalidation on DELETE: remove node c and its edges
SELECT * FROM cypher('vle_cache_test', $$
MATCH (c:Node {name: 'c'})
DETACH DELETE c
$$) AS (v agtype);
v
---
(0 rows)

-- VLE query: should only find b now (c is gone, so b->c path is broken)
SELECT * FROM cypher('vle_cache_test', $$
MATCH (a:Node {name: 'a'})-[:Edge*1..4]->(n:Node)
RETURN n.name
ORDER BY n.name
$$) AS (name agtype);
name
------
"b"
(1 row)

-- Test cache invalidation on SET: change b's name property
SELECT * FROM cypher('vle_cache_test', $$
MATCH (b:Node {name: 'b'})
SET b.name = 'b_modified'
RETURN b.name
$$) AS (name agtype);
name
--------------
"b_modified"
(1 row)

-- VLE query: verify the updated property is returned via lazy fetch
SELECT * FROM cypher('vle_cache_test', $$
MATCH (a:Node {name: 'a'})-[:Edge*1..4]->(n:Node)
RETURN n.name
ORDER BY n.name
$$) AS (name agtype);
name
--------------
"b_modified"
(1 row)

-- Test VLE with edge properties (exercises thin entry edge property fetch)
SELECT * FROM drop_graph('vle_cache_test', true);
NOTICE: drop cascades to 4 other objects
DETAIL: drop cascades to table vle_cache_test._ag_label_vertex
drop cascades to table vle_cache_test._ag_label_edge
drop cascades to table vle_cache_test."Node"
drop cascades to table vle_cache_test."Edge"
NOTICE: graph "vle_cache_test" has been dropped
drop_graph
------------

(1 row)

SELECT * FROM create_graph('vle_cache_test2');
NOTICE: graph "vle_cache_test2" has been created
create_graph
--------------

(1 row)

SELECT * FROM cypher('vle_cache_test2', $$
CREATE (a:N {name: 'a'})-[:E {weight: 1}]->(b:N {name: 'b'})-[:E {weight: 2}]->(c:N {name: 'c'})
$$) AS (v agtype);
v
---
(0 rows)

-- VLE path output to verify edge properties are fetched correctly via
-- thin entry lazy fetch. Returning the full path forces build_path()
-- to call get_edge_entry_properties() for each edge in the result.
-- The output must contain the correct weight values (1 and 2).
SELECT * FROM cypher('vle_cache_test2', $$
MATCH p=(a:N {name: 'a'})-[:E *1..2]->(n:N)
RETURN p
ORDER BY n.name
$$) AS (p agtype);
p
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
[{"id": 844424930131969, "label": "N", "properties": {"name": "a"}}::vertex, {"id": 1125899906842626, "label": "E", "end_id": 844424930131970, "start_id": 844424930131969, "properties": {"weight": 1}}::edge, {"id": 844424930131970, "label": "N", "properties": {"name": "b"}}::vertex]::path
[{"id": 844424930131969, "label": "N", "properties": {"name": "a"}}::vertex, {"id": 1125899906842626, "label": "E", "end_id": 844424930131970, "start_id": 844424930131969, "properties": {"weight": 1}}::edge, {"id": 844424930131970, "label": "N", "properties": {"name": "b"}}::vertex, {"id": 1125899906842625, "label": "E", "end_id": 844424930131971, "start_id": 844424930131970, "properties": {"weight": 2}}::edge, {"id": 844424930131971, "label": "N", "properties": {"name": "c"}}::vertex]::path
(2 rows)

-- VLE edge properties via UNWIND + relationships() to individually verify
-- each edge's properties are correctly fetched from the heap via TID.
SELECT * FROM cypher('vle_cache_test2', $$
MATCH p=(a:N {name: 'a'})-[:E *1..2]->(n:N)
WITH p, n
UNWIND relationships(p) AS e
RETURN n.name, e.weight
ORDER BY n.name, e.weight
$$) AS (name agtype, weight agtype);
name | weight
------+--------
"b" | 1
"c" | 1
"c" | 2
(3 rows)

-- Cleanup
SELECT * FROM drop_graph('vle_cache_test2', true);
NOTICE: drop cascades to 4 other objects
DETAIL: drop cascades to table vle_cache_test2._ag_label_vertex
drop cascades to table vle_cache_test2._ag_label_edge
drop cascades to table vle_cache_test2."N"
drop cascades to table vle_cache_test2."E"
NOTICE: graph "vle_cache_test2" has been dropped
drop_graph
------------

(1 row)

-----------------------------------------------------------------------------------------------------------------------------
--
-- End of tests
Expand Down
97 changes: 97 additions & 0 deletions regress/sql/age_global_graph.sql
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,103 @@ RESET client_min_messages;
SELECT * FROM drop_graph('ag_graph_1', true);
SELECT * FROM drop_graph('ag_graph_2', true);
SELECT * FROM drop_graph('ag_graph_3', true);

-----------------------------------------------------------------------------------------------------------------------------
--
-- VLE cache invalidation tests
--
-- These tests verify that the graph version counter properly invalidates
-- the VLE hash table cache when the graph is mutated, and that thin
-- entry lazy property fetch returns correct data.
--

-- Setup: create a graph with a chain a->b->c->d
SELECT * FROM create_graph('vle_cache_test');

SELECT * FROM cypher('vle_cache_test', $$
CREATE (a:Node {name: 'a'})-[:Edge]->(b:Node {name: 'b'})-[:Edge]->(c:Node {name: 'c'})-[:Edge]->(d:Node {name: 'd'})
$$) AS (v agtype);

-- VLE query: find all paths from a's neighbors (should find b, b->c, b->c->d)
SELECT * FROM cypher('vle_cache_test', $$
MATCH (a:Node {name: 'a'})-[:Edge*1..3]->(n:Node)
RETURN n.name
ORDER BY n.name
$$) AS (name agtype);

-- Now add a new node e connected to d. This should invalidate the cache.
SELECT * FROM cypher('vle_cache_test', $$
MATCH (d:Node {name: 'd'})
CREATE (d)-[:Edge]->(:Node {name: 'e'})
$$) AS (v agtype);

-- VLE query again: should now also find e via a->b->c->d->e (4 hops won't reach,
-- but d->e is 1 hop from d, and a->b->c->d->e would be 4 hops from a).
-- Increase range to *1..4 to include e
SELECT * FROM cypher('vle_cache_test', $$
MATCH (a:Node {name: 'a'})-[:Edge*1..4]->(n:Node)
RETURN n.name
ORDER BY n.name
$$) AS (name agtype);

-- Test cache invalidation on DELETE: remove node c and its edges
SELECT * FROM cypher('vle_cache_test', $$
MATCH (c:Node {name: 'c'})
DETACH DELETE c
$$) AS (v agtype);

-- VLE query: should only find b now (c is gone, so b->c path is broken)
SELECT * FROM cypher('vle_cache_test', $$
MATCH (a:Node {name: 'a'})-[:Edge*1..4]->(n:Node)
RETURN n.name
ORDER BY n.name
$$) AS (name agtype);

-- Test cache invalidation on SET: change b's name property
SELECT * FROM cypher('vle_cache_test', $$
MATCH (b:Node {name: 'b'})
SET b.name = 'b_modified'
RETURN b.name
$$) AS (name agtype);

-- VLE query: verify the updated property is returned via lazy fetch
SELECT * FROM cypher('vle_cache_test', $$
MATCH (a:Node {name: 'a'})-[:Edge*1..4]->(n:Node)
RETURN n.name
ORDER BY n.name
$$) AS (name agtype);

-- Test VLE with edge properties (exercises thin entry edge property fetch)
SELECT * FROM drop_graph('vle_cache_test', true);
SELECT * FROM create_graph('vle_cache_test2');

SELECT * FROM cypher('vle_cache_test2', $$
CREATE (a:N {name: 'a'})-[:E {weight: 1}]->(b:N {name: 'b'})-[:E {weight: 2}]->(c:N {name: 'c'})
$$) AS (v agtype);

-- VLE path output to verify edge properties are fetched correctly via
-- thin entry lazy fetch. Returning the full path forces build_path()
-- to call get_edge_entry_properties() for each edge in the result.
-- The output must contain the correct weight values (1 and 2).
SELECT * FROM cypher('vle_cache_test2', $$
MATCH p=(a:N {name: 'a'})-[:E *1..2]->(n:N)
RETURN p
ORDER BY n.name
$$) AS (p agtype);

-- VLE edge properties via UNWIND + relationships() to individually verify
-- each edge's properties are correctly fetched from the heap via TID.
SELECT * FROM cypher('vle_cache_test2', $$
MATCH p=(a:N {name: 'a'})-[:E *1..2]->(n:N)
WITH p, n
UNWIND relationships(p) AS e
RETURN n.name, e.weight
ORDER BY n.name, e.weight
$$) AS (name agtype, weight agtype);

-- Cleanup
SELECT * FROM drop_graph('vle_cache_test2', true);

-----------------------------------------------------------------------------------------------------------------------------
--
-- End of tests
Expand Down
11 changes: 11 additions & 0 deletions sql/age_main.sql
Original file line number Diff line number Diff line change
Expand Up @@ -381,3 +381,14 @@ CREATE FUNCTION ag_catalog._extract_label_id(graphid)
STABLE
PARALLEL SAFE
AS 'MODULE_PATHNAME';

--
-- VLE cache invalidation trigger function.
-- Installed on graph label tables to catch SQL-level mutations
-- (INSERT/UPDATE/DELETE/TRUNCATE) and increment the graph's
-- version counter so VLE caches are properly invalidated.
--
CREATE FUNCTION ag_catalog.age_invalidate_graph_cache()
RETURNS trigger
LANGUAGE c
AS 'MODULE_PATHNAME';
32 changes: 32 additions & 0 deletions src/backend/age.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,29 @@
#include "optimizer/cypher_paths.h"
#include "parser/cypher_analyze.h"
#include "utils/ag_guc.h"
#include "utils/age_global_graph.h"

#if PG_VERSION_NUM < 170000
#include "miscadmin.h"

/* saved hook pointers for PG < 17 shmem path */
static shmem_request_hook_type prev_shmem_request_hook = NULL;
static shmem_startup_hook_type prev_shmem_startup_hook = NULL;

static void age_shmem_request_hook(void)
{
if (prev_shmem_request_hook)
prev_shmem_request_hook();
age_graph_version_shmem_request();
}

static void age_shmem_startup_hook(void)
{
if (prev_shmem_startup_hook)
prev_shmem_startup_hook();
age_graph_version_shmem_startup();
}
#endif /* PG_VERSION_NUM < 170000 */

PG_MODULE_MAGIC;

Expand All @@ -35,6 +58,15 @@ void _PG_init(void)
process_utility_hook_init();
post_parse_analyze_init();
define_config_params();

#if PG_VERSION_NUM < 170000
/* Register shared memory hooks for graph version tracking.
* On PG 17+, DSM is used instead (no hooks needed). */
prev_shmem_request_hook = shmem_request_hook;
shmem_request_hook = age_shmem_request_hook;
prev_shmem_startup_hook = shmem_startup_hook;
shmem_startup_hook = age_shmem_startup_hook;
#endif
}

void _PG_fini(void);
Expand Down
Loading
Loading