This is an automated email from the ASF dual-hosted git repository.
jgemignani pushed a commit to branch Dev_Multiple_Labels
in repository https://gitbox.apache.org/repos/asf/age.git
The following commit(s) were added to refs/heads/Dev_Multiple_Labels by this
push:
new 72a4bca7 Add index-based lookups and oid caching (#2280)
72a4bca7 is described below
commit 72a4bca7ab94a8a98aae457332f1eb415edeeb2e
Author: John Gemignani <[email protected]>
AuthorDate: Sat Dec 20 09:06:35 2025 -0800
Add index-based lookups and oid caching (#2280)
NOTE: This work was done with an AI coding tool and a human.
Replace sequential table scans with index-based lookups for single-row
vertex operations in the unified vertex table architecture. This improves
performance from O(n) to O(log n) for vertex existence checks, retrievals,
updates, and deletions.
Changes:
* vertex_exists() in cypher_utils.c: Use systable_beginscan() with the
primary key index instead of table_beginscan()
* get_vertex() in agtype.c: Use index scan for startNode()/endNode()
vertex retrieval
* process_delete_list() in cypher_delete.c: Use index scan for vertex
lookups; fix scan key comparison from F_GRAPHIDEQ to F_INT8EQ since
unified vertex table stores id as bigint, not graphid
* process_update_list() in cypher_set.c: Use index scan for SET/REMOVE
operations
Add cache-first lookup optimization:
* _label_name_from_table_oid() in ag_label.c: Check label relation cache
before falling back to syscache lookup, reducing catalog overhead for
repeated label name lookups
All changes use RelationGetIndexList() with rd_pkindex to obtain the
primary key index OID for systable_beginscan().
Added regression tests.
modified: regress/expected/unified_vertex_table.out
modified: regress/sql/unified_vertex_table.sql
modified: src/backend/catalog/ag_label.c
modified: src/backend/executor/cypher_delete.c
modified: src/backend/executor/cypher_set.c
modified: src/backend/executor/cypher_utils.c
modified: src/backend/utils/adt/agtype.c
---
regress/expected/unified_vertex_table.out | 406 +++++++++++++++++++++++++++++-
regress/sql/unified_vertex_table.sql | 224 +++++++++++++++++
src/backend/catalog/ag_label.c | 21 +-
src/backend/executor/cypher_delete.c | 25 +-
src/backend/executor/cypher_set.c | 25 +-
src/backend/executor/cypher_utils.c | 24 +-
src/backend/utils/adt/agtype.c | 23 +-
7 files changed, 724 insertions(+), 24 deletions(-)
diff --git a/regress/expected/unified_vertex_table.out
b/regress/expected/unified_vertex_table.out
index 38fac52f..b6037b61 100644
--- a/regress/expected/unified_vertex_table.out
+++ b/regress/expected/unified_vertex_table.out
@@ -392,11 +392,406 @@ WHERE properties::text LIKE '%val%';
3
(1 row)
+--
+-- Test 12: Index scan optimization for vertex_exists()
+-- This exercises the systable_beginscan path in vertex_exists()
+--
+SELECT * FROM cypher('unified_test', $$
+ CREATE (:IndexTest {id: 100})
+$$) AS (v agtype);
+ v
+---
+(0 rows)
+
+SELECT * FROM cypher('unified_test', $$
+ CREATE (:IndexTest {id: 101})
+$$) AS (v agtype);
+ v
+---
+(0 rows)
+
+SELECT * FROM cypher('unified_test', $$
+ CREATE (:IndexTest {id: 102})
+$$) AS (v agtype);
+ v
+---
+(0 rows)
+
+-- DETACH DELETE exercises vertex_exists() to check vertex validity
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:IndexTest {id: 100})
+ DETACH DELETE n
+$$) AS (v agtype);
+ v
+---
+(0 rows)
+
+-- Verify vertex was deleted and others remain
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:IndexTest)
+ RETURN n.id ORDER BY n.id
+$$) AS (id agtype);
+ id
+-----
+ 101
+ 102
+(2 rows)
+
+-- Multiple deletes to exercise index scan repeatedly
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:IndexTest)
+ DELETE n
+$$) AS (v agtype);
+ v
+---
+(0 rows)
+
+-- Verify all IndexTest vertices are gone
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:IndexTest)
+ RETURN count(n)
+$$) AS (cnt agtype);
+ cnt
+-----
+ 0
+(1 row)
+
+--
+-- Test 13: Index scan optimization for get_vertex() via startNode/endNode
+-- This exercises the systable_beginscan path in get_vertex()
+--
+SELECT * FROM cypher('unified_test', $$
+ CREATE (a:GetVertexTest {name: 'source1'})-[:LINK]->(b:GetVertexTest
{name: 'target1'})
+$$) AS (v agtype);
+ v
+---
+(0 rows)
+
+SELECT * FROM cypher('unified_test', $$
+ CREATE (a:GetVertexTest {name: 'source2'})-[:LINK]->(b:GetVertexTest
{name: 'target2'})
+$$) AS (v agtype);
+ v
+---
+(0 rows)
+
+SELECT * FROM cypher('unified_test', $$
+ CREATE (a:GetVertexTest {name: 'source3'})-[:LINK]->(b:GetVertexTest
{name: 'target3'})
+$$) AS (v agtype);
+ v
+---
+(0 rows)
+
+-- Multiple startNode/endNode calls exercise get_vertex() with index scans
+SELECT * FROM cypher('unified_test', $$
+ MATCH ()-[e:LINK]->()
+ RETURN startNode(e).name AS src, endNode(e).name AS tgt,
+ label(startNode(e)) AS src_label, label(endNode(e)) AS tgt_label
+ ORDER BY src
+$$) AS (src agtype, tgt agtype, src_label agtype, tgt_label agtype);
+ src | tgt | src_label | tgt_label
+-----------+-----------+-----------------+-----------------
+ "source1" | "target1" | "GetVertexTest" | "GetVertexTest"
+ "source2" | "target2" | "GetVertexTest" | "GetVertexTest"
+ "source3" | "target3" | "GetVertexTest" | "GetVertexTest"
+(3 rows)
+
+-- Chain of edges to test repeated get_vertex calls
+SELECT * FROM cypher('unified_test', $$
+ MATCH (a:GetVertexTest {name: 'target1'})
+ CREATE (a)-[:CHAIN]->(:GetVertexTest {name:
'chain1'})-[:CHAIN]->(:GetVertexTest {name: 'chain2'})
+$$) AS (v agtype);
+ v
+---
+(0 rows)
+
+SELECT * FROM cypher('unified_test', $$
+ MATCH ()-[e:CHAIN]->()
+ RETURN startNode(e).name, endNode(e).name
+ ORDER BY startNode(e).name
+$$) AS (src agtype, tgt agtype);
+ src | tgt
+-----------+----------
+ "chain1" | "chain2"
+ "target1" | "chain1"
+(2 rows)
+
+--
+-- Test 14: Index scan optimization for process_delete_list()
+-- This exercises the F_INT8EQ fix and systable_beginscan in DELETE
+--
+SELECT * FROM cypher('unified_test', $$
+ CREATE (:DeleteTest {seq: 1}), (:DeleteTest {seq: 2}), (:DeleteTest {seq:
3}),
+ (:DeleteTest {seq: 4}), (:DeleteTest {seq: 5})
+$$) AS (v agtype);
+ v
+---
+(0 rows)
+
+-- Verify vertices exist
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:DeleteTest)
+ RETURN n.seq ORDER BY n.seq
+$$) AS (seq agtype);
+ seq
+-----
+ 1
+ 2
+ 3
+ 4
+ 5
+(5 rows)
+
+-- Delete specific vertex by property (exercises index lookup)
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:DeleteTest {seq: 3})
+ DELETE n
+$$) AS (v agtype);
+ v
+---
+(0 rows)
+
+-- Verify correct vertex was deleted
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:DeleteTest)
+ RETURN n.seq ORDER BY n.seq
+$$) AS (seq agtype);
+ seq
+-----
+ 1
+ 2
+ 4
+ 5
+(4 rows)
+
+-- Delete with edges (exercises process_delete_list with edge cleanup)
+SELECT * FROM cypher('unified_test', $$
+ MATCH (a:DeleteTest {seq: 1})
+ CREATE (a)-[:DEL_EDGE]->(:DeleteTest {seq: 10})
+$$) AS (v agtype);
+ v
+---
+(0 rows)
+
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:DeleteTest {seq: 1})
+ DETACH DELETE n
+$$) AS (v agtype);
+ v
+---
+(0 rows)
+
+-- Verify vertex and edge were deleted
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:DeleteTest)
+ RETURN n.seq ORDER BY n.seq
+$$) AS (seq agtype);
+ seq
+-----
+ 2
+ 4
+ 5
+ 10
+(4 rows)
+
+--
+-- Test 15: Index scan optimization for process_update_list()
+-- This exercises the systable_beginscan in SET/REMOVE operations
+--
+SELECT * FROM cypher('unified_test', $$
+ CREATE (:UpdateTest {id: 1, val: 'original1'}),
+ (:UpdateTest {id: 2, val: 'original2'}),
+ (:UpdateTest {id: 3, val: 'original3'})
+$$) AS (v agtype);
+ v
+---
+(0 rows)
+
+-- Single SET operation
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:UpdateTest {id: 1})
+ SET n.val = 'updated1'
+ RETURN n.id, n.val
+$$) AS (id agtype, val agtype);
+ id | val
+----+------------
+ 1 | "updated1"
+(1 row)
+
+-- Multiple SET operations in one query (exercises repeated index lookups)
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:UpdateTest)
+ SET n.modified = true
+ RETURN n.id, n.val, n.modified ORDER BY n.id
+$$) AS (id agtype, val agtype, modified agtype);
+ id | val | modified
+----+-------------+----------
+ 1 | "updated1" | true
+ 2 | "original2" | true
+ 3 | "original3" | true
+(3 rows)
+
+-- SET with property addition
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:UpdateTest {id: 2})
+ SET n.extra = 'new_property', n.val = 'updated2'
+ RETURN n.id, n.val, n.extra
+$$) AS (id agtype, val agtype, extra agtype);
+ id | val | extra
+----+------------+----------------
+ 2 | "updated2" | "new_property"
+(1 row)
+
+-- REMOVE property operation
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:UpdateTest {id: 3})
+ REMOVE n.val
+ RETURN n.id, n.val, n.modified
+$$) AS (id agtype, val agtype, modified agtype);
+ id | val | modified
+----+-----+----------
+ 3 | | true
+(1 row)
+
+-- Verify final state of all UpdateTest vertices
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:UpdateTest)
+ RETURN n ORDER BY n.id
+$$) AS (n agtype);
+ n
+------------------------------------------------------------------------------------------------------------------------------------------------
+ {"id": 5910974510923777, "label": "UpdateTest", "properties": {"id": 1,
"val": "updated1", "modified": true}}::vertex
+ {"id": 5910974510923778, "label": "UpdateTest", "properties": {"id": 2,
"val": "updated2", "extra": "new_property", "modified": true}}::vertex
+ {"id": 5910974510923779, "label": "UpdateTest", "properties": {"id": 3,
"modified": true}}::vertex
+(3 rows)
+
+--
+-- Test 16: OID caching in _label_name_from_table_oid()
+-- Repeated calls should use cache after first lookup
+--
+-- Call multiple times to exercise cache hit path
+SELECT
ag_catalog._label_name_from_table_oid('unified_test."Person"'::regclass::oid);
+ _label_name_from_table_oid
+----------------------------
+ Person
+(1 row)
+
+SELECT
ag_catalog._label_name_from_table_oid('unified_test."Person"'::regclass::oid);
+ _label_name_from_table_oid
+----------------------------
+ Person
+(1 row)
+
+SELECT
ag_catalog._label_name_from_table_oid('unified_test."Company"'::regclass::oid);
+ _label_name_from_table_oid
+----------------------------
+ Company
+(1 row)
+
+SELECT
ag_catalog._label_name_from_table_oid('unified_test."Company"'::regclass::oid);
+ _label_name_from_table_oid
+----------------------------
+ Company
+(1 row)
+
+SELECT
ag_catalog._label_name_from_table_oid('unified_test."Location"'::regclass::oid);
+ _label_name_from_table_oid
+----------------------------
+ Location
+(1 row)
+
+SELECT
ag_catalog._label_name_from_table_oid('unified_test."Location"'::regclass::oid);
+ _label_name_from_table_oid
+----------------------------
+ Location
+(1 row)
+
+-- Call with unified table OID (default vertex label case)
+SELECT
ag_catalog._label_name_from_table_oid('unified_test._ag_label_vertex'::regclass::oid);
+ _label_name_from_table_oid
+----------------------------
+
+(1 row)
+
+-- Verify label function also benefits from caching (exercises full path)
+SELECT * FROM cypher('unified_test', $$
+ MATCH (p:Person)
+ RETURN label(p), label(p), label(p)
+$$) AS (l1 agtype, l2 agtype, l3 agtype);
+ l1 | l2 | l3
+----------+----------+----------
+ "Person" | "Person" | "Person"
+ "Person" | "Person" | "Person"
+ "Person" | "Person" | "Person"
+(3 rows)
+
+--
+-- Test 17: Combined operations stress test
+-- Multiple operations in sequence to verify optimizations work together
+--
+SELECT * FROM cypher('unified_test', $$
+ CREATE (a:StressTest {id: 1})-[:ST_EDGE]->(b:StressTest {id: 2})
+$$) AS (v agtype);
+ v
+---
+(0 rows)
+
+-- startNode/endNode (get_vertex index scan)
+SELECT * FROM cypher('unified_test', $$
+ MATCH ()-[e:ST_EDGE]->()
+ RETURN startNode(e).id, endNode(e).id
+$$) AS (start_id agtype, end_id agtype);
+ start_id | end_id
+----------+--------
+ 1 | 2
+(1 row)
+
+-- SET (process_update_list index scan)
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:StressTest)
+ SET n.updated = true
+ RETURN n.id, n.updated ORDER BY n.id
+$$) AS (id agtype, updated agtype);
+ id | updated
+----+---------
+ 1 | true
+ 2 | true
+(2 rows)
+
+-- label() calls (OID cache)
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:StressTest)
+ RETURN n.id, label(n) ORDER BY n.id
+$$) AS (id agtype, lbl agtype);
+ id | lbl
+----+--------------
+ 1 | "StressTest"
+ 2 | "StressTest"
+(2 rows)
+
+-- DETACH DELETE (vertex_exists + process_delete_list index scans)
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:StressTest)
+ DETACH DELETE n
+$$) AS (v agtype);
+ v
+---
+(0 rows)
+
+-- Verify cleanup
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:StressTest)
+ RETURN count(n)
+$$) AS (cnt agtype);
+ cnt
+-----
+ 0
+(1 row)
+
--
-- Cleanup
--
SELECT drop_graph('unified_test', true);
-NOTICE: drop cascades to 14 other objects
+NOTICE: drop cascades to 23 other objects
DETAIL: drop cascades to table unified_test._ag_label_vertex
drop cascades to table unified_test._ag_label_edge
drop cascades to table unified_test."Person"
@@ -411,6 +806,15 @@ drop cascades to table unified_test."CONNECTED"
drop cascades to table unified_test."LabelA"
drop cascades to table unified_test."LabelB"
drop cascades to table unified_test."LabelC"
+drop cascades to table unified_test."IndexTest"
+drop cascades to table unified_test."GetVertexTest"
+drop cascades to table unified_test."LINK"
+drop cascades to table unified_test."CHAIN"
+drop cascades to table unified_test."DeleteTest"
+drop cascades to table unified_test."DEL_EDGE"
+drop cascades to table unified_test."UpdateTest"
+drop cascades to table unified_test."StressTest"
+drop cascades to table unified_test."ST_EDGE"
NOTICE: graph "unified_test" has been dropped
drop_graph
------------
diff --git a/regress/sql/unified_vertex_table.sql
b/regress/sql/unified_vertex_table.sql
index 84145e38..f9f30f66 100644
--- a/regress/sql/unified_vertex_table.sql
+++ b/regress/sql/unified_vertex_table.sql
@@ -238,6 +238,230 @@ $$) AS (v agtype);
SELECT COUNT(DISTINCT labels) AS distinct_labels FROM
unified_test._ag_label_vertex
WHERE properties::text LIKE '%val%';
+--
+-- Test 12: Index scan optimization for vertex_exists()
+-- This exercises the systable_beginscan path in vertex_exists()
+--
+SELECT * FROM cypher('unified_test', $$
+ CREATE (:IndexTest {id: 100})
+$$) AS (v agtype);
+
+SELECT * FROM cypher('unified_test', $$
+ CREATE (:IndexTest {id: 101})
+$$) AS (v agtype);
+
+SELECT * FROM cypher('unified_test', $$
+ CREATE (:IndexTest {id: 102})
+$$) AS (v agtype);
+
+-- DETACH DELETE exercises vertex_exists() to check vertex validity
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:IndexTest {id: 100})
+ DETACH DELETE n
+$$) AS (v agtype);
+
+-- Verify vertex was deleted and others remain
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:IndexTest)
+ RETURN n.id ORDER BY n.id
+$$) AS (id agtype);
+
+-- Multiple deletes to exercise index scan repeatedly
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:IndexTest)
+ DELETE n
+$$) AS (v agtype);
+
+-- Verify all IndexTest vertices are gone
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:IndexTest)
+ RETURN count(n)
+$$) AS (cnt agtype);
+
+--
+-- Test 13: Index scan optimization for get_vertex() via startNode/endNode
+-- This exercises the systable_beginscan path in get_vertex()
+--
+SELECT * FROM cypher('unified_test', $$
+ CREATE (a:GetVertexTest {name: 'source1'})-[:LINK]->(b:GetVertexTest
{name: 'target1'})
+$$) AS (v agtype);
+
+SELECT * FROM cypher('unified_test', $$
+ CREATE (a:GetVertexTest {name: 'source2'})-[:LINK]->(b:GetVertexTest
{name: 'target2'})
+$$) AS (v agtype);
+
+SELECT * FROM cypher('unified_test', $$
+ CREATE (a:GetVertexTest {name: 'source3'})-[:LINK]->(b:GetVertexTest
{name: 'target3'})
+$$) AS (v agtype);
+
+-- Multiple startNode/endNode calls exercise get_vertex() with index scans
+SELECT * FROM cypher('unified_test', $$
+ MATCH ()-[e:LINK]->()
+ RETURN startNode(e).name AS src, endNode(e).name AS tgt,
+ label(startNode(e)) AS src_label, label(endNode(e)) AS tgt_label
+ ORDER BY src
+$$) AS (src agtype, tgt agtype, src_label agtype, tgt_label agtype);
+
+-- Chain of edges to test repeated get_vertex calls
+SELECT * FROM cypher('unified_test', $$
+ MATCH (a:GetVertexTest {name: 'target1'})
+ CREATE (a)-[:CHAIN]->(:GetVertexTest {name:
'chain1'})-[:CHAIN]->(:GetVertexTest {name: 'chain2'})
+$$) AS (v agtype);
+
+SELECT * FROM cypher('unified_test', $$
+ MATCH ()-[e:CHAIN]->()
+ RETURN startNode(e).name, endNode(e).name
+ ORDER BY startNode(e).name
+$$) AS (src agtype, tgt agtype);
+
+--
+-- Test 14: Index scan optimization for process_delete_list()
+-- This exercises the F_INT8EQ fix and systable_beginscan in DELETE
+--
+SELECT * FROM cypher('unified_test', $$
+ CREATE (:DeleteTest {seq: 1}), (:DeleteTest {seq: 2}), (:DeleteTest {seq:
3}),
+ (:DeleteTest {seq: 4}), (:DeleteTest {seq: 5})
+$$) AS (v agtype);
+
+-- Verify vertices exist
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:DeleteTest)
+ RETURN n.seq ORDER BY n.seq
+$$) AS (seq agtype);
+
+-- Delete specific vertex by property (exercises index lookup)
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:DeleteTest {seq: 3})
+ DELETE n
+$$) AS (v agtype);
+
+-- Verify correct vertex was deleted
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:DeleteTest)
+ RETURN n.seq ORDER BY n.seq
+$$) AS (seq agtype);
+
+-- Delete with edges (exercises process_delete_list with edge cleanup)
+SELECT * FROM cypher('unified_test', $$
+ MATCH (a:DeleteTest {seq: 1})
+ CREATE (a)-[:DEL_EDGE]->(:DeleteTest {seq: 10})
+$$) AS (v agtype);
+
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:DeleteTest {seq: 1})
+ DETACH DELETE n
+$$) AS (v agtype);
+
+-- Verify vertex and edge were deleted
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:DeleteTest)
+ RETURN n.seq ORDER BY n.seq
+$$) AS (seq agtype);
+
+--
+-- Test 15: Index scan optimization for process_update_list()
+-- This exercises the systable_beginscan in SET/REMOVE operations
+--
+SELECT * FROM cypher('unified_test', $$
+ CREATE (:UpdateTest {id: 1, val: 'original1'}),
+ (:UpdateTest {id: 2, val: 'original2'}),
+ (:UpdateTest {id: 3, val: 'original3'})
+$$) AS (v agtype);
+
+-- Single SET operation
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:UpdateTest {id: 1})
+ SET n.val = 'updated1'
+ RETURN n.id, n.val
+$$) AS (id agtype, val agtype);
+
+-- Multiple SET operations in one query (exercises repeated index lookups)
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:UpdateTest)
+ SET n.modified = true
+ RETURN n.id, n.val, n.modified ORDER BY n.id
+$$) AS (id agtype, val agtype, modified agtype);
+
+-- SET with property addition
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:UpdateTest {id: 2})
+ SET n.extra = 'new_property', n.val = 'updated2'
+ RETURN n.id, n.val, n.extra
+$$) AS (id agtype, val agtype, extra agtype);
+
+-- REMOVE property operation
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:UpdateTest {id: 3})
+ REMOVE n.val
+ RETURN n.id, n.val, n.modified
+$$) AS (id agtype, val agtype, modified agtype);
+
+-- Verify final state of all UpdateTest vertices
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:UpdateTest)
+ RETURN n ORDER BY n.id
+$$) AS (n agtype);
+
+--
+-- Test 16: OID caching in _label_name_from_table_oid()
+-- Repeated calls should use cache after first lookup
+--
+-- Call multiple times to exercise cache hit path
+SELECT
ag_catalog._label_name_from_table_oid('unified_test."Person"'::regclass::oid);
+SELECT
ag_catalog._label_name_from_table_oid('unified_test."Person"'::regclass::oid);
+SELECT
ag_catalog._label_name_from_table_oid('unified_test."Company"'::regclass::oid);
+SELECT
ag_catalog._label_name_from_table_oid('unified_test."Company"'::regclass::oid);
+SELECT
ag_catalog._label_name_from_table_oid('unified_test."Location"'::regclass::oid);
+SELECT
ag_catalog._label_name_from_table_oid('unified_test."Location"'::regclass::oid);
+
+-- Call with unified table OID (default vertex label case)
+SELECT
ag_catalog._label_name_from_table_oid('unified_test._ag_label_vertex'::regclass::oid);
+
+-- Verify label function also benefits from caching (exercises full path)
+SELECT * FROM cypher('unified_test', $$
+ MATCH (p:Person)
+ RETURN label(p), label(p), label(p)
+$$) AS (l1 agtype, l2 agtype, l3 agtype);
+
+--
+-- Test 17: Combined operations stress test
+-- Multiple operations in sequence to verify optimizations work together
+--
+SELECT * FROM cypher('unified_test', $$
+ CREATE (a:StressTest {id: 1})-[:ST_EDGE]->(b:StressTest {id: 2})
+$$) AS (v agtype);
+
+-- startNode/endNode (get_vertex index scan)
+SELECT * FROM cypher('unified_test', $$
+ MATCH ()-[e:ST_EDGE]->()
+ RETURN startNode(e).id, endNode(e).id
+$$) AS (start_id agtype, end_id agtype);
+
+-- SET (process_update_list index scan)
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:StressTest)
+ SET n.updated = true
+ RETURN n.id, n.updated ORDER BY n.id
+$$) AS (id agtype, updated agtype);
+
+-- label() calls (OID cache)
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:StressTest)
+ RETURN n.id, label(n) ORDER BY n.id
+$$) AS (id agtype, lbl agtype);
+
+-- DETACH DELETE (vertex_exists + process_delete_list index scans)
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:StressTest)
+ DETACH DELETE n
+$$) AS (v agtype);
+
+-- Verify cleanup
+SELECT * FROM cypher('unified_test', $$
+ MATCH (n:StressTest)
+ RETURN count(n)
+$$) AS (cnt agtype);
+
--
-- Cleanup
--
diff --git a/src/backend/catalog/ag_label.c b/src/backend/catalog/ag_label.c
index 9578276d..5e3d004b 100644
--- a/src/backend/catalog/ag_label.c
+++ b/src/backend/catalog/ag_label.c
@@ -227,10 +227,14 @@ PG_FUNCTION_INFO_V1(_label_name_from_table_oid);
/*
* Given a label table OID, return the label name.
* Returns empty string for the default vertex/edge table.
+ *
+ * This function first checks the AGE label cache for fast lookups,
+ * then falls back to PostgreSQL's syscache if not found.
*/
Datum _label_name_from_table_oid(PG_FUNCTION_ARGS)
{
Oid label_table_oid;
+ label_cache_data *cache_data;
char *label_name;
if (PG_ARGISNULL(0))
@@ -241,7 +245,22 @@ Datum _label_name_from_table_oid(PG_FUNCTION_ARGS)
label_table_oid = PG_GETARG_OID(0);
- /* Get the relation name from the OID */
+ /* Try the AGE label cache first for fast lookup */
+ cache_data = search_label_relation_cache(label_table_oid);
+ if (cache_data != NULL)
+ {
+ label_name = NameStr(cache_data->name);
+
+ /* Return empty string for default labels */
+ if (IS_AG_DEFAULT_LABEL(label_name))
+ {
+ PG_RETURN_CSTRING("");
+ }
+
+ PG_RETURN_CSTRING(pstrdup(label_name));
+ }
+
+ /* Fallback to PostgreSQL syscache */
label_name = get_rel_name(label_table_oid);
if (label_name == NULL)
diff --git a/src/backend/executor/cypher_delete.c
b/src/backend/executor/cypher_delete.c
index ddfed8ed..ee97343c 100644
--- a/src/backend/executor/cypher_delete.c
+++ b/src/backend/executor/cypher_delete.c
@@ -19,8 +19,11 @@
#include "postgres.h"
+#include "access/genam.h"
#include "storage/bufmgr.h"
#include "common/hashfn.h"
+#include "utils/rel.h"
+#include "utils/relcache.h"
#include "catalog/ag_label.h"
#include "executor/cypher_executor.h"
@@ -376,12 +379,13 @@ static void process_delete_list(CustomScanState *node)
cypher_delete_item *item;
agtype_value *original_entity_value, *id, *label;
ScanKeyData scan_keys[1];
- TableScanDesc scan_desc;
+ SysScanDesc scan_desc;
ResultRelInfo *resultRelInfo;
HeapTuple heap_tuple;
char *label_name;
Integer *pos;
int entity_position;
+ Oid pk_index_oid;
item = lfirst(lc);
@@ -424,16 +428,25 @@ static void process_delete_list(CustomScanState *node)
errmsg("DELETE clause can only delete vertices and
edges")));
}
+ /*
+ * Get primary key index OID from relation cache for index scan.
+ * For vertices, this enables fast index lookups on the unified table.
+ */
+ (void) RelationGetIndexList(resultRelInfo->ri_RelationDesc);
+ pk_index_oid = resultRelInfo->ri_RelationDesc->rd_pkindex;
+
/*
* Setup the scan description, with the correct snapshot and scan keys.
+ * Use systable_beginscan for index-based lookup when available.
*/
estate->es_snapshot->curcid = GetCurrentCommandId(false);
estate->es_output_cid = GetCurrentCommandId(false);
- scan_desc = table_beginscan(resultRelInfo->ri_RelationDesc,
- estate->es_snapshot, 1, scan_keys);
+ scan_desc = systable_beginscan(resultRelInfo->ri_RelationDesc,
+ pk_index_oid, true,
+ estate->es_snapshot, 1, scan_keys);
/* Retrieve the tuple. */
- heap_tuple = heap_getnext(scan_desc, ForwardScanDirection);
+ heap_tuple = systable_getnext(scan_desc);
/*
* If the heap tuple still exists (It wasn't deleted after this
variable
@@ -442,7 +455,7 @@ static void process_delete_list(CustomScanState *node)
*/
if (!HeapTupleIsValid(heap_tuple))
{
- table_endscan(scan_desc);
+ systable_endscan(scan_desc);
destroy_entity_result_rel_info(resultRelInfo);
continue;
@@ -464,7 +477,7 @@ static void process_delete_list(CustomScanState *node)
delete_entity(estate, resultRelInfo, heap_tuple);
/* Close the scan and the relation. */
- table_endscan(scan_desc);
+ systable_endscan(scan_desc);
destroy_entity_result_rel_info(resultRelInfo);
}
}
diff --git a/src/backend/executor/cypher_set.c
b/src/backend/executor/cypher_set.c
index fb2625ba..c13cee04 100644
--- a/src/backend/executor/cypher_set.c
+++ b/src/backend/executor/cypher_set.c
@@ -19,7 +19,10 @@
#include "postgres.h"
+#include "access/genam.h"
#include "storage/bufmgr.h"
+#include "utils/rel.h"
+#include "utils/relcache.h"
#include "catalog/ag_label.h"
#include "catalog/namespace.h"
@@ -398,7 +401,7 @@ static void process_update_list(CustomScanState *node)
TupleTableSlot *slot;
ResultRelInfo *resultRelInfo;
ScanKeyData scan_keys[1];
- TableScanDesc scan_desc;
+ SysScanDesc scan_desc;
bool remove_property;
char *label_name;
cypher_update_item *update_item;
@@ -406,6 +409,7 @@ static void process_update_list(CustomScanState *node)
HeapTuple heap_tuple;
char *clause_name = css->set_list->clause_name;
int cid;
+ Oid pk_index_oid;
update_item = (cypher_update_item *)lfirst(lc);
@@ -610,14 +614,23 @@ static void process_update_list(CustomScanState *node)
ScanKeyInit(&scan_keys[0], 1, BTEqualStrategyNumber,
F_GRAPHIDEQ,
GRAPHID_GET_DATUM(id->val.int_value));
}
+
+ /*
+ * Get primary key index OID from relation cache for index scan.
+ * This enables fast index lookups instead of sequential scans.
+ */
+ (void) RelationGetIndexList(resultRelInfo->ri_RelationDesc);
+ pk_index_oid = resultRelInfo->ri_RelationDesc->rd_pkindex;
+
/*
* Setup the scan description, with the correct snapshot and scan
- * keys.
+ * keys. Use systable_beginscan for index-based lookup when
available.
*/
- scan_desc = table_beginscan(resultRelInfo->ri_RelationDesc,
- estate->es_snapshot, 1, scan_keys);
+ scan_desc = systable_beginscan(resultRelInfo->ri_RelationDesc,
+ pk_index_oid, true,
+ estate->es_snapshot, 1, scan_keys);
/* Retrieve the tuple. */
- heap_tuple = heap_getnext(scan_desc, ForwardScanDirection);
+ heap_tuple = systable_getnext(scan_desc);
/*
* If the heap tuple still exists (It wasn't deleted between the
@@ -629,7 +642,7 @@ static void process_update_list(CustomScanState *node)
heap_tuple);
}
/* close the ScanDescription */
- table_endscan(scan_desc);
+ systable_endscan(scan_desc);
}
estate->es_snapshot->curcid = cid;
diff --git a/src/backend/executor/cypher_utils.c
b/src/backend/executor/cypher_utils.c
index a2dfc6b6..20aada90 100644
--- a/src/backend/executor/cypher_utils.c
+++ b/src/backend/executor/cypher_utils.c
@@ -24,10 +24,13 @@
#include "postgres.h"
+#include "access/genam.h"
#include "catalog/namespace.h"
#include "nodes/makefuncs.h"
#include "parser/parse_relation.h"
#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/relcache.h"
#include "catalog/ag_graph.h"
#include "catalog/ag_label.h"
@@ -194,18 +197,20 @@ TupleTableSlot *populate_edge_tts(
* Find out if the vertex still exists. This is for 'implicit' deletion
* of a vertex - checking if a vertex was deleted by another variable.
*
- * NOTE: This function scans the unified _ag_label_vertex table directly.
+ * NOTE: This function scans the unified _ag_label_vertex table directly
+ * using an index scan on the primary key for efficiency.
* No information is extracted from the graphid - the graphid is only used
* as the search key to find the vertex row.
*/
bool vertex_exists(EState *estate, Oid graph_oid, graphid id)
{
ScanKeyData scan_keys[1];
- TableScanDesc scan_desc;
+ SysScanDesc scan_desc;
HeapTuple tuple;
Relation rel;
bool result = true;
Oid vertex_table_oid;
+ Oid pk_index_oid;
/* Get the unified vertex table OID directly from graph_oid */
vertex_table_oid = get_label_relation(AG_DEFAULT_LABEL_VERTEX, graph_oid);
@@ -216,8 +221,17 @@ bool vertex_exists(EState *estate, Oid graph_oid, graphid
id)
rel = table_open(vertex_table_oid, RowExclusiveLock);
- scan_desc = table_beginscan(rel, estate->es_snapshot, 1, scan_keys);
- tuple = heap_getnext(scan_desc, ForwardScanDirection);
+ /* Get primary key index OID from relation cache for index scan */
+ (void) RelationGetIndexList(rel);
+ pk_index_oid = rel->rd_pkindex;
+
+ /*
+ * Use systable_beginscan which will use the primary key index if
available.
+ * This is much faster than a sequential scan for single-row lookups.
+ */
+ scan_desc = systable_beginscan(rel, pk_index_oid, true,
+ estate->es_snapshot, 1, scan_keys);
+ tuple = systable_getnext(scan_desc);
/*
* If a single tuple was returned, the tuple is still valid, otherwise
@@ -228,7 +242,7 @@ bool vertex_exists(EState *estate, Oid graph_oid, graphid
id)
result = false;
}
- table_endscan(scan_desc);
+ systable_endscan(scan_desc);
table_close(rel, RowExclusiveLock);
return result;
diff --git a/src/backend/utils/adt/agtype.c b/src/backend/utils/adt/agtype.c
index 20ccdcd3..7163cb7d 100644
--- a/src/backend/utils/adt/agtype.c
+++ b/src/backend/utils/adt/agtype.c
@@ -47,6 +47,8 @@
#include "utils/builtins.h"
#include "utils/float.h"
#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/relcache.h"
#include "utils/snapmgr.h"
#include "utils/typcache.h"
#include "utils/age_vle.h"
@@ -5544,7 +5546,7 @@ static Datum get_vertex(const char *graph, int64 graphid)
{
ScanKeyData scan_keys[1];
Relation graph_vertex_table;
- TableScanDesc scan_desc;
+ SysScanDesc scan_desc;
HeapTuple tuple;
TupleDesc tupdesc;
Datum id, properties, labels_oid_datum;
@@ -5552,6 +5554,7 @@ static Datum get_vertex(const char *graph, int64 graphid)
Oid label_table_oid;
label_cache_data *label_cache;
char *label_name;
+ Oid pk_index_oid;
/* get the specific graph namespace (schema) */
Oid graph_namespace_oid = get_namespace_oid(graph, false);
@@ -5565,10 +5568,20 @@ static Datum get_vertex(const char *graph, int64
graphid)
ScanKeyInit(&scan_keys[0], 1, BTEqualStrategyNumber, F_INT8EQ,
Int64GetDatum(graphid));
- /* open the unified vertex table, begin the scan, and get the tuple */
+ /* open the unified vertex table */
graph_vertex_table = table_open(vertex_table_oid, ShareLock);
- scan_desc = table_beginscan(graph_vertex_table, snapshot, 1, scan_keys);
- tuple = heap_getnext(scan_desc, ForwardScanDirection);
+
+ /* Get primary key index OID from relation cache for index scan */
+ (void) RelationGetIndexList(graph_vertex_table);
+ pk_index_oid = graph_vertex_table->rd_pkindex;
+
+ /*
+ * Use systable_beginscan which will use the primary key index if
available.
+ * This is much faster than a sequential scan for single-row lookups.
+ */
+ scan_desc = systable_beginscan(graph_vertex_table, pk_index_oid, true,
+ snapshot, 1, scan_keys);
+ tuple = systable_getnext(scan_desc);
/* bail if the tuple isn't valid */
if (!HeapTupleIsValid(tuple))
@@ -5614,7 +5627,7 @@ static Datum get_vertex(const char *graph, int64 graphid)
result = DirectFunctionCall3(_agtype_build_vertex, id,
CStringGetDatum(label_name), properties);
/* end the scan and close the relation */
- table_endscan(scan_desc);
+ systable_endscan(scan_desc);
table_close(graph_vertex_table, ShareLock);
/* return the vertex datum */
return result;