Skip to content
Merged
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
51 changes: 51 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,54 @@ $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';
Comment thread
jrgemignani marked this conversation as resolved.

--
-- Install the cache invalidation trigger on all pre-existing label tables.
-- New label tables created after this upgrade will get the trigger
-- automatically via label_commands.c. This DO block handles tables
-- that were created before the upgrade.
--
DO $$
DECLARE
r RECORD;
BEGIN
FOR r IN
SELECT n.nspname AS schema_name, c.relname AS table_name
FROM ag_catalog.ag_label l
JOIN pg_catalog.pg_class c ON c.oid = l.relation
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE l.name != '_ag_label_vertex'
AND l.name != '_ag_label_edge'
LOOP
-- Skip if trigger already exists on this table
IF NOT EXISTS (
SELECT 1 FROM pg_catalog.pg_trigger t
JOIN pg_catalog.pg_class c ON c.oid = t.tgrelid
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = r.schema_name
AND c.relname = r.table_name
AND t.tgname = '_age_cache_invalidate'
)
THEN
EXECUTE format(
'CREATE TRIGGER _age_cache_invalidate '
'AFTER INSERT OR UPDATE OR DELETE OR TRUNCATE '
'ON %I.%I '
'FOR EACH STATEMENT '
'EXECUTE FUNCTION ag_catalog.age_invalidate_graph_cache()',
r.schema_name, r.table_name
);
END IF;
END LOOP;
END;
$$;
288 changes: 288 additions & 0 deletions regress/expected/age_global_graph.out
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,294 @@ 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)

-----------------------------------------------------------------------------------------------------------------------------
--
-- VLE cache invalidation via direct SQL (trigger tests)
--
-- These tests verify that the SQL trigger (age_invalidate_graph_cache)
-- properly invalidates the VLE cache when label tables are mutated
-- via direct SQL INSERT, UPDATE, DELETE, and TRUNCATE — not via Cypher.
--
-- Setup: create graph with a chain a->b->c using Cypher
SELECT * FROM create_graph('vle_trigger_test');
NOTICE: graph "vle_trigger_test" has been created
create_graph
--------------

(1 row)

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

-- Prime the VLE cache: find all nodes reachable from a
SELECT * FROM cypher('vle_trigger_test', $$
MATCH (a:Node {name: 'a'})-[:Edge*1..3]->(n:Node)
RETURN n.name
ORDER BY n.name
$$) AS (name agtype);
name
------
"b"
"c"
(2 rows)

-- Direct SQL INSERT on vertex: add a new vertex via SQL.
-- This should fire the trigger and invalidate the VLE cache.
-- Use _graphid() with the label's id and nextval for the entry id.
INSERT INTO vle_trigger_test."Node" (id, properties)
SELECT ag_catalog._graphid(l.id,
nextval('vle_trigger_test."Node_id_seq"'::regclass)),
'{"name": "d"}'::agtype
FROM ag_catalog.ag_label l
JOIN ag_catalog.ag_graph g ON g.graphid = l.graph
WHERE g.name = 'vle_trigger_test' AND l.name = 'Node';
-- VLE query: results should be unchanged (d has no edges) but cache was rebuilt
SELECT * FROM cypher('vle_trigger_test', $$
MATCH (a:Node {name: 'a'})-[:Edge*1..3]->(n:Node)
RETURN n.name
ORDER BY n.name
$$) AS (name agtype);
name
------
"b"
"c"
(2 rows)

-- Direct SQL UPDATE on vertex: change b's name to 'b_updated'
-- This should fire the trigger and invalidate the VLE cache.
UPDATE vle_trigger_test."Node"
SET properties = '{"name": "b_updated"}'::agtype
WHERE properties @> '{"name": "b"}'::agtype;
-- VLE query: verify updated property is visible (cache invalidated by trigger)
SELECT * FROM cypher('vle_trigger_test', $$
MATCH (a:Node {name: 'a'})-[:Edge*1..3]->(n:Node)
RETURN n.name
ORDER BY n.name
$$) AS (name agtype);
name
-------------
"b_updated"
"c"
(2 rows)

-- Direct SQL DELETE on edge: remove the edge from b_updated to c
DELETE FROM vle_trigger_test."Edge"
WHERE end_id = (SELECT id FROM vle_trigger_test."Node"
WHERE properties @> '{"name": "c"}'::agtype);
-- VLE query: only b_updated reachable now (edge to c is gone)
SELECT * FROM cypher('vle_trigger_test', $$
MATCH (a:Node {name: 'a'})-[:Edge*1..3]->(n:Node)
RETURN n.name
ORDER BY n.name
$$) AS (name agtype);
name
-------------
"b_updated"
(1 row)

-- Direct SQL TRUNCATE on edge table: remove all edges
TRUNCATE vle_trigger_test."Edge";
-- VLE query: no edges exist, should return no rows
SELECT * FROM cypher('vle_trigger_test', $$
MATCH (a:Node {name: 'a'})-[:Edge*1..3]->(n:Node)
RETURN n.name
ORDER BY n.name
$$) AS (name agtype);
name
------
(0 rows)

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

(1 row)

-----------------------------------------------------------------------------------------------------------------------------
--
-- End of tests
Expand Down
Loading
Loading