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;


Reply via email to