This is an automated email from the ASF dual-hosted git repository.
mtaha pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/age.git
The following commit(s) were added to refs/heads/master by this push:
new b9d09828 Optimize vertex/edge field access with direct array indexing
(#2302)
b9d09828 is described below
commit b9d0982892306abff0013dd8f336e153684b02e9
Author: John Gemignani <[email protected]>
AuthorDate: Fri Jan 16 15:12:22 2026 -0800
Optimize vertex/edge field access with direct array indexing (#2302)
NOTE: This PR was created using AI tools and a human.
Leverage deterministic key ordering from uniqueify_agtype_object() to
access vertex/edge fields in O(1) instead of O(log n) binary search.
Fields are sorted by key length, giving fixed positions:
- Vertex: id(0), label(1), properties(2)
- Edge: id(0), label(1), end_id(2), start_id(3), properties(4)
Changes:
- Add field index constants and accessor macros to agtype.h
- Update age_id(), age_start_id(), age_end_id(), age_label(),
age_properties() to use direct field access
- Add fill_agtype_value_no_copy() for read-only scalar extraction
without memory allocation
- Add compare_agtype_scalar_containers() fast path for scalar comparison
- Update hash_agtype_value(), equals_agtype_scalar_value(), and
compare_agtype_scalar_values() to use direct field access macros
- Add fast path in get_one_agtype_from_variadic_args() bypassing
extract_variadic_args() for single argument case
- Add comprehensive regression test (30 tests)
Performance impact: Improves ORDER BY, hash joins, aggregations, and
Cypher functions (id, start_id, end_id, label, properties) on vertices
and edges.
All previous regression tests were not impacted.
Additional regression test added to enhance coverage.
modified: Makefile
new file: regress/expected/direct_field_access.out
new file: regress/sql/direct_field_access.sql
modified: src/backend/utils/adt/agtype.c
modified: src/backend/utils/adt/agtype_util.c
modified: src/include/utils/agtype.h
---
Makefile | 3 +-
regress/expected/direct_field_access.out | 535 +++++++++++++++++++++++++++++++
regress/sql/direct_field_access.sql | 319 ++++++++++++++++++
src/backend/utils/adt/agtype.c | 136 ++++++--
src/backend/utils/adt/agtype_util.c | 237 +++++++++++++-
src/include/utils/agtype.h | 103 ++++++
6 files changed, 1304 insertions(+), 29 deletions(-)
diff --git a/Makefile b/Makefile
index 17f2ed65..ffad7d6a 100644
--- a/Makefile
+++ b/Makefile
@@ -112,7 +112,8 @@ REGRESS = scan \
name_validation \
jsonb_operators \
list_comprehension \
- map_projection
+ map_projection \
+ direct_field_access
ifneq ($(EXTRA_TESTS),)
REGRESS += $(EXTRA_TESTS)
diff --git a/regress/expected/direct_field_access.out
b/regress/expected/direct_field_access.out
new file mode 100644
index 00000000..0a059cdd
--- /dev/null
+++ b/regress/expected/direct_field_access.out
@@ -0,0 +1,535 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+/*
+ * Direct Field Access Optimizations Test
+ *
+ * Tests for optimizations that directly access agtype fields without
+ * using the full iterator machinery or binary search:
+ *
+ * 1. fill_agtype_value_no_copy() - Read-only access without memory allocation
+ * 2. compare_agtype_scalar_containers() - Fast path for scalar comparisons
+ * 3. Direct pairs[0] access for vertex/edge id comparison
+ * 4. Fast path in get_one_agtype_from_variadic_args()
+ */
+LOAD 'age';
+SET search_path TO ag_catalog;
+SELECT create_graph('direct_access');
+NOTICE: graph "direct_access" has been created
+ create_graph
+--------------
+
+(1 row)
+
+--
+-- Section 1: Scalar Comparison Fast Path Tests
+--
+-- These tests exercise the compare_agtype_scalar_containers() fast path
+-- which uses fill_agtype_value_no_copy() for read-only comparisons.
+--
+-- Integer comparisons
+SELECT * FROM cypher('direct_access', $$
+ RETURN 1 < 2, 2 > 1, 1 = 1, 1 <> 2
+$$) AS (lt agtype, gt agtype, eq agtype, ne agtype);
+ lt | gt | eq | ne
+------+------+------+------
+ true | true | true | true
+(1 row)
+
+SELECT * FROM cypher('direct_access', $$
+ RETURN 100 < 50, 100 > 50, 100 = 100, 100 <> 100
+$$) AS (lt agtype, gt agtype, eq agtype, ne agtype);
+ lt | gt | eq | ne
+-------+------+------+-------
+ false | true | true | false
+(1 row)
+
+-- Float comparisons
+SELECT * FROM cypher('direct_access', $$
+ RETURN 1.5 < 2.5, 2.5 > 1.5, 1.5 = 1.5, 1.5 <> 2.5
+$$) AS (lt agtype, gt agtype, eq agtype, ne agtype);
+ lt | gt | eq | ne
+------+------+------+------
+ true | true | true | true
+(1 row)
+
+-- String comparisons (tests no-copy string pointer)
+SELECT * FROM cypher('direct_access', $$
+ RETURN 'abc' < 'abd', 'abd' > 'abc', 'abc' = 'abc', 'abc' <> 'abd'
+$$) AS (lt agtype, gt agtype, eq agtype, ne agtype);
+ lt | gt | eq | ne
+------+------+------+------
+ true | true | true | true
+(1 row)
+
+SELECT * FROM cypher('direct_access', $$
+ RETURN 'hello world' < 'hello worlds', 'test' > 'TEST'
+$$) AS (lt agtype, gt agtype);
+ lt | gt
+------+------
+ true | true
+(1 row)
+
+-- Boolean comparisons
+SELECT * FROM cypher('direct_access', $$
+ RETURN false < true, true > false, true = true, false <> true
+$$) AS (lt agtype, gt agtype, eq agtype, ne agtype);
+ lt | gt | eq | ne
+------+------+------+------
+ true | true | true | true
+(1 row)
+
+-- Null comparisons
+SELECT * FROM cypher('direct_access', $$
+ RETURN null = null, null <> null
+$$) AS (eq agtype, ne agtype);
+ eq | ne
+----+----
+ |
+(1 row)
+
+-- Mixed numeric type comparisons (integer vs float)
+SELECT * FROM cypher('direct_access', $$
+ RETURN 1 < 1.5, 2.0 > 1, 1.0 = 1
+$$) AS (lt agtype, gt agtype, eq agtype);
+ lt | gt | eq
+------+------+------
+ true | true | true
+(1 row)
+
+-- Numeric type comparisons
+SELECT * FROM cypher('direct_access', $$
+ RETURN 1.234::numeric < 1.235::numeric,
+ 1.235::numeric > 1.234::numeric,
+ 1.234::numeric = 1.234::numeric
+$$) AS (lt agtype, gt agtype, eq agtype);
+ lt | gt | eq
+------+------+------
+ true | true | true
+(1 row)
+
+--
+-- Section 2: ORDER BY Tests (exercises comparison fast path)
+--
+-- ORDER BY uses compare_agtype_containers_orderability which now has
+-- a fast path for scalar comparisons.
+--
+-- Integer ORDER BY
+SELECT * FROM cypher('direct_access', $$
+ UNWIND [5, 3, 8, 1, 9, 2, 7, 4, 6] AS n
+ RETURN n ORDER BY n
+$$) AS (n agtype);
+ n
+---
+ 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+(9 rows)
+
+SELECT * FROM cypher('direct_access', $$
+ UNWIND [5, 3, 8, 1, 9, 2, 7, 4, 6] AS n
+ RETURN n ORDER BY n DESC
+$$) AS (n agtype);
+ n
+---
+ 9
+ 8
+ 7
+ 6
+ 5
+ 4
+ 3
+ 2
+ 1
+(9 rows)
+
+-- String ORDER BY
+SELECT * FROM cypher('direct_access', $$
+ UNWIND ['banana', 'apple', 'cherry', 'date'] AS s
+ RETURN s ORDER BY s
+$$) AS (s agtype);
+ s
+----------
+ "apple"
+ "banana"
+ "cherry"
+ "date"
+(4 rows)
+
+-- Float ORDER BY
+SELECT * FROM cypher('direct_access', $$
+ UNWIND [3.14, 2.71, 1.41, 1.73] AS f
+ RETURN f ORDER BY f
+$$) AS (f agtype);
+ f
+------
+ 1.41
+ 1.73
+ 2.71
+ 3.14
+(4 rows)
+
+-- Boolean ORDER BY
+SELECT * FROM cypher('direct_access', $$
+ UNWIND [true, false, true, false] AS b
+ RETURN b ORDER BY b
+$$) AS (b agtype);
+ b
+-------
+ false
+ false
+ true
+ true
+(4 rows)
+
+--
+-- Section 3: Vertex/Edge Direct ID Access Tests
+--
+-- These tests exercise the direct pairs[0] access optimization for
+-- extracting graphid from vertices and edges during comparison.
+--
+-- Create test data
+SELECT * FROM cypher('direct_access', $$
+ CREATE (a:Person {name: 'Alice', age: 30}),
+ (b:Person {name: 'Bob', age: 25}),
+ (c:Person {name: 'Charlie', age: 35}),
+ (d:Person {name: 'Diana', age: 28}),
+ (e:Person {name: 'Eve', age: 32}),
+ (a)-[:KNOWS {since: 2020}]->(b),
+ (b)-[:KNOWS {since: 2019}]->(c),
+ (c)-[:KNOWS {since: 2021}]->(d),
+ (d)-[:KNOWS {since: 2018}]->(e),
+ (e)-[:KNOWS {since: 2022}]->(a)
+$$) AS (result agtype);
+ result
+--------
+(0 rows)
+
+-- Test max() on vertices (uses compare_agtype_scalar_values with AGTV_VERTEX)
+SELECT * FROM cypher('direct_access', $$
+ MATCH (p:Person)
+ RETURN max(p)
+$$) AS (max_vertex agtype);
+ max_vertex
+----------------------------------------------------------------------------------------------
+ {"id": 844424930131973, "label": "Person", "properties": {"age": 32, "name":
"Eve"}}::vertex
+(1 row)
+
+-- Test min() on vertices
+SELECT * FROM cypher('direct_access', $$
+ MATCH (p:Person)
+ RETURN min(p)
+$$) AS (min_vertex agtype);
+ min_vertex
+------------------------------------------------------------------------------------------------
+ {"id": 844424930131969, "label": "Person", "properties": {"age": 30, "name":
"Alice"}}::vertex
+(1 row)
+
+-- Test max() on edges (uses compare_agtype_scalar_values with AGTV_EDGE)
+SELECT * FROM cypher('direct_access', $$
+ MATCH ()-[r:KNOWS]->()
+ RETURN max(r)
+$$) AS (max_edge agtype);
+ max_edge
+-----------------------------------------------------------------------------------------------------------------------------------------
+ {"id": 1125899906842629, "label": "KNOWS", "end_id": 844424930131969,
"start_id": 844424930131973, "properties": {"since": 2022}}::edge
+(1 row)
+
+-- Test min() on edges
+SELECT * FROM cypher('direct_access', $$
+ MATCH ()-[r:KNOWS]->()
+ RETURN min(r)
+$$) AS (min_edge agtype);
+ min_edge
+-----------------------------------------------------------------------------------------------------------------------------------------
+ {"id": 1125899906842625, "label": "KNOWS", "end_id": 844424930131970,
"start_id": 844424930131969, "properties": {"since": 2020}}::edge
+(1 row)
+
+-- ORDER BY on vertices (uses direct id comparison)
+SELECT * FROM cypher('direct_access', $$
+ MATCH (p:Person)
+ RETURN p.name ORDER BY p
+$$) AS (name agtype);
+ name
+-----------
+ "Alice"
+ "Bob"
+ "Charlie"
+ "Diana"
+ "Eve"
+(5 rows)
+
+SELECT * FROM cypher('direct_access', $$
+ MATCH (p:Person)
+ RETURN p.name ORDER BY p DESC
+$$) AS (name agtype);
+ name
+-----------
+ "Eve"
+ "Diana"
+ "Charlie"
+ "Bob"
+ "Alice"
+(5 rows)
+
+-- ORDER BY on edges
+SELECT * FROM cypher('direct_access', $$
+ MATCH ()-[r:KNOWS]->()
+ RETURN r.since ORDER BY r
+$$) AS (since agtype);
+ since
+-------
+ 2020
+ 2019
+ 2021
+ 2018
+ 2022
+(5 rows)
+
+-- Vertex comparison in WHERE
+SELECT * FROM cypher('direct_access', $$
+ MATCH (a:Person), (b:Person)
+ WHERE a < b
+ RETURN a.name, b.name
+$$) AS (a_name agtype, b_name agtype);
+ a_name | b_name
+-----------+-----------
+ "Alice" | "Bob"
+ "Alice" | "Charlie"
+ "Alice" | "Diana"
+ "Alice" | "Eve"
+ "Bob" | "Charlie"
+ "Bob" | "Diana"
+ "Bob" | "Eve"
+ "Charlie" | "Diana"
+ "Charlie" | "Eve"
+ "Diana" | "Eve"
+(10 rows)
+
+--
+-- Section 4: Fast Path for get_one_agtype_from_variadic_args
+--
+-- These tests exercise the fast path that bypasses extract_variadic_args
+-- when the argument is already agtype.
+--
+-- Direct agtype comparison operators (use the fast path)
+SELECT * FROM cypher('direct_access', $$
+ RETURN 42 = 42, 42 <> 43, 42 < 100, 42 > 10
+$$) AS (eq agtype, ne agtype, lt agtype, gt agtype);
+ eq | ne | lt | gt
+------+------+------+------
+ true | true | true | true
+(1 row)
+
+-- Arithmetic operators (also use the fast path)
+SELECT * FROM cypher('direct_access', $$
+ RETURN 10 + 5, 10 - 5, 10 * 5, 10 / 5
+$$) AS (add agtype, sub agtype, mul agtype, div agtype);
+ add | sub | mul | div
+-----+-----+-----+-----
+ 15 | 5 | 50 | 2
+(1 row)
+
+-- String functions that take agtype args
+SELECT * FROM cypher('direct_access', $$
+ RETURN toUpper('hello'), toLower('WORLD'), size('test')
+$$) AS (upper agtype, lower agtype, sz agtype);
+ upper | lower | sz
+---------+---------+----
+ "HELLO" | "world" | 4
+(1 row)
+
+-- Type checking functions
+SELECT * FROM cypher('direct_access', $$
+ RETURN toInteger('42'), toFloat('3.14'), toString(42)
+$$) AS (int_val agtype, float_val agtype, str_val agtype);
+ int_val | float_val | str_val
+---------+-----------+---------
+ 42 | 3.14 | "42"
+(1 row)
+
+--
+-- Section 5: Direct Field Access for Accessor Functions
+--
+-- These tests exercise the direct field access macros in id(), start_id(),
+-- end_id(), label(), and properties() functions.
+--
+-- Test id() on vertices (uses AGTYPE_VERTEX_GET_ID macro - index 0)
+SELECT * FROM cypher('direct_access', $$
+ MATCH (p:Person {name: 'Alice'})
+ RETURN id(p)
+$$) AS (vertex_id agtype);
+ vertex_id
+-----------------
+ 844424930131969
+(1 row)
+
+-- Test id() on edges (uses AGTYPE_EDGE_GET_ID macro - index 0)
+SELECT * FROM cypher('direct_access', $$
+ MATCH (a:Person {name: 'Alice'})-[r:KNOWS]->(b:Person {name: 'Bob'})
+ RETURN id(r)
+$$) AS (edge_id agtype);
+ edge_id
+------------------
+ 1125899906842625
+(1 row)
+
+-- Test start_id() on edges (uses AGTYPE_EDGE_GET_START_ID macro - index 3)
+SELECT * FROM cypher('direct_access', $$
+ MATCH (a:Person {name: 'Alice'})-[r:KNOWS]->(b:Person {name: 'Bob'})
+ RETURN start_id(r), id(a)
+$$) AS (start_id agtype, alice_id agtype);
+ start_id | alice_id
+-----------------+-----------------
+ 844424930131969 | 844424930131969
+(1 row)
+
+-- Test end_id() on edges (uses AGTYPE_EDGE_GET_END_ID macro - index 2)
+SELECT * FROM cypher('direct_access', $$
+ MATCH (a:Person {name: 'Alice'})-[r:KNOWS]->(b:Person {name: 'Bob'})
+ RETURN end_id(r), id(b)
+$$) AS (end_id agtype, bob_id agtype);
+ end_id | bob_id
+-----------------+-----------------
+ 844424930131970 | 844424930131970
+(1 row)
+
+-- Test label() on vertices (uses AGTYPE_VERTEX_GET_LABEL macro - index 1)
+SELECT * FROM cypher('direct_access', $$
+ MATCH (p:Person {name: 'Alice'})
+ RETURN label(p)
+$$) AS (vertex_label agtype);
+ vertex_label
+--------------
+ "Person"
+(1 row)
+
+-- Test label() on edges (uses AGTYPE_EDGE_GET_LABEL macro - index 1)
+SELECT * FROM cypher('direct_access', $$
+ MATCH ()-[r:KNOWS]->()
+ RETURN DISTINCT label(r)
+$$) AS (edge_label agtype);
+ edge_label
+------------
+ "KNOWS"
+(1 row)
+
+-- Test properties() on vertices (uses AGTYPE_VERTEX_GET_PROPERTIES macro -
index 2)
+SELECT * FROM cypher('direct_access', $$
+ MATCH (p:Person {name: 'Alice'})
+ RETURN properties(p)
+$$) AS (vertex_props agtype);
+ vertex_props
+------------------------------
+ {"age": 30, "name": "Alice"}
+(1 row)
+
+-- Test properties() on edges (uses AGTYPE_EDGE_GET_PROPERTIES macro - index 4)
+SELECT * FROM cypher('direct_access', $$
+ MATCH (a:Person {name: 'Alice'})-[r:KNOWS]->(b:Person {name: 'Bob'})
+ RETURN properties(r)
+$$) AS (edge_props agtype);
+ edge_props
+-----------------
+ {"since": 2020}
+(1 row)
+
+-- Combined accessor test - verify all fields are accessible
+SELECT * FROM cypher('direct_access', $$
+ MATCH (a:Person {name: 'Alice'})-[r:KNOWS]->(b:Person)
+ RETURN id(a), label(a), properties(a).name,
+ id(r), start_id(r), end_id(r), label(r), properties(r).since,
+ id(b), label(b), properties(b).name
+$$) AS (a_id agtype, a_label agtype, a_name agtype,
+ r_id agtype, r_start agtype, r_end agtype, r_label agtype, r_since
agtype,
+ b_id agtype, b_label agtype, b_name agtype);
+ a_id | a_label | a_name | r_id | r_start |
r_end | r_label | r_since | b_id | b_label | b_name
+-----------------+----------+---------+------------------+-----------------+-----------------+---------+---------+-----------------+----------+--------
+ 844424930131969 | "Person" | "Alice" | 1125899906842625 | 844424930131969 |
844424930131970 | "KNOWS" | 2020 | 844424930131970 | "Person" | "Bob"
+(1 row)
+
+--
+-- Section 6: Mixed Comparisons and Edge Cases
+--
+-- Array comparisons (should NOT use scalar fast path)
+SELECT * FROM cypher('direct_access', $$
+ RETURN [1,2,3] = [1,2,3], [1,2,3] < [1,2,4]
+$$) AS (eq agtype, lt agtype);
+ eq | lt
+------+------
+ true | true
+(1 row)
+
+-- Object comparisons (should NOT use scalar fast path)
+SELECT * FROM cypher('direct_access', $$
+ RETURN {a:1, b:2} = {a:1, b:2}
+$$) AS (eq agtype);
+ eq
+------
+ true
+(1 row)
+
+-- Large integer comparisons
+SELECT * FROM cypher('direct_access', $$
+ RETURN 9223372036854775807 > 9223372036854775806,
+ -9223372036854775808 < -9223372036854775807
+$$) AS (big_gt agtype, neg_lt agtype);
+ big_gt | neg_lt
+--------+--------
+ true | true
+(1 row)
+
+-- Empty string comparison
+SELECT * FROM cypher('direct_access', $$
+ RETURN '' < 'a', '' = ''
+$$) AS (lt agtype, eq agtype);
+ lt | eq
+------+------
+ true | true
+(1 row)
+
+-- Special float values
+SELECT * FROM cypher('direct_access', $$
+ RETURN 0.0 = -0.0
+$$) AS (zero_eq agtype);
+ zero_eq
+---------
+ true
+(1 row)
+
+--
+-- Cleanup
+--
+SELECT drop_graph('direct_access', true);
+NOTICE: drop cascades to 4 other objects
+DETAIL: drop cascades to table direct_access._ag_label_vertex
+drop cascades to table direct_access._ag_label_edge
+drop cascades to table direct_access."Person"
+drop cascades to table direct_access."KNOWS"
+NOTICE: graph "direct_access" has been dropped
+ drop_graph
+------------
+
+(1 row)
+
diff --git a/regress/sql/direct_field_access.sql
b/regress/sql/direct_field_access.sql
new file mode 100644
index 00000000..c8060be4
--- /dev/null
+++ b/regress/sql/direct_field_access.sql
@@ -0,0 +1,319 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*
+ * Direct Field Access Optimizations Test
+ *
+ * Tests for optimizations that directly access agtype fields without
+ * using the full iterator machinery or binary search:
+ *
+ * 1. fill_agtype_value_no_copy() - Read-only access without memory allocation
+ * 2. compare_agtype_scalar_containers() - Fast path for scalar comparisons
+ * 3. Direct pairs[0] access for vertex/edge id comparison
+ * 4. Fast path in get_one_agtype_from_variadic_args()
+ */
+
+LOAD 'age';
+SET search_path TO ag_catalog;
+
+SELECT create_graph('direct_access');
+
+--
+-- Section 1: Scalar Comparison Fast Path Tests
+--
+-- These tests exercise the compare_agtype_scalar_containers() fast path
+-- which uses fill_agtype_value_no_copy() for read-only comparisons.
+--
+
+-- Integer comparisons
+SELECT * FROM cypher('direct_access', $$
+ RETURN 1 < 2, 2 > 1, 1 = 1, 1 <> 2
+$$) AS (lt agtype, gt agtype, eq agtype, ne agtype);
+
+SELECT * FROM cypher('direct_access', $$
+ RETURN 100 < 50, 100 > 50, 100 = 100, 100 <> 100
+$$) AS (lt agtype, gt agtype, eq agtype, ne agtype);
+
+-- Float comparisons
+SELECT * FROM cypher('direct_access', $$
+ RETURN 1.5 < 2.5, 2.5 > 1.5, 1.5 = 1.5, 1.5 <> 2.5
+$$) AS (lt agtype, gt agtype, eq agtype, ne agtype);
+
+-- String comparisons (tests no-copy string pointer)
+SELECT * FROM cypher('direct_access', $$
+ RETURN 'abc' < 'abd', 'abd' > 'abc', 'abc' = 'abc', 'abc' <> 'abd'
+$$) AS (lt agtype, gt agtype, eq agtype, ne agtype);
+
+SELECT * FROM cypher('direct_access', $$
+ RETURN 'hello world' < 'hello worlds', 'test' > 'TEST'
+$$) AS (lt agtype, gt agtype);
+
+-- Boolean comparisons
+SELECT * FROM cypher('direct_access', $$
+ RETURN false < true, true > false, true = true, false <> true
+$$) AS (lt agtype, gt agtype, eq agtype, ne agtype);
+
+-- Null comparisons
+SELECT * FROM cypher('direct_access', $$
+ RETURN null = null, null <> null
+$$) AS (eq agtype, ne agtype);
+
+-- Mixed numeric type comparisons (integer vs float)
+SELECT * FROM cypher('direct_access', $$
+ RETURN 1 < 1.5, 2.0 > 1, 1.0 = 1
+$$) AS (lt agtype, gt agtype, eq agtype);
+
+-- Numeric type comparisons
+SELECT * FROM cypher('direct_access', $$
+ RETURN 1.234::numeric < 1.235::numeric,
+ 1.235::numeric > 1.234::numeric,
+ 1.234::numeric = 1.234::numeric
+$$) AS (lt agtype, gt agtype, eq agtype);
+
+--
+-- Section 2: ORDER BY Tests (exercises comparison fast path)
+--
+-- ORDER BY uses compare_agtype_containers_orderability which now has
+-- a fast path for scalar comparisons.
+--
+
+-- Integer ORDER BY
+SELECT * FROM cypher('direct_access', $$
+ UNWIND [5, 3, 8, 1, 9, 2, 7, 4, 6] AS n
+ RETURN n ORDER BY n
+$$) AS (n agtype);
+
+SELECT * FROM cypher('direct_access', $$
+ UNWIND [5, 3, 8, 1, 9, 2, 7, 4, 6] AS n
+ RETURN n ORDER BY n DESC
+$$) AS (n agtype);
+
+-- String ORDER BY
+SELECT * FROM cypher('direct_access', $$
+ UNWIND ['banana', 'apple', 'cherry', 'date'] AS s
+ RETURN s ORDER BY s
+$$) AS (s agtype);
+
+-- Float ORDER BY
+SELECT * FROM cypher('direct_access', $$
+ UNWIND [3.14, 2.71, 1.41, 1.73] AS f
+ RETURN f ORDER BY f
+$$) AS (f agtype);
+
+-- Boolean ORDER BY
+SELECT * FROM cypher('direct_access', $$
+ UNWIND [true, false, true, false] AS b
+ RETURN b ORDER BY b
+$$) AS (b agtype);
+
+--
+-- Section 3: Vertex/Edge Direct ID Access Tests
+--
+-- These tests exercise the direct pairs[0] access optimization for
+-- extracting graphid from vertices and edges during comparison.
+--
+
+-- Create test data
+SELECT * FROM cypher('direct_access', $$
+ CREATE (a:Person {name: 'Alice', age: 30}),
+ (b:Person {name: 'Bob', age: 25}),
+ (c:Person {name: 'Charlie', age: 35}),
+ (d:Person {name: 'Diana', age: 28}),
+ (e:Person {name: 'Eve', age: 32}),
+ (a)-[:KNOWS {since: 2020}]->(b),
+ (b)-[:KNOWS {since: 2019}]->(c),
+ (c)-[:KNOWS {since: 2021}]->(d),
+ (d)-[:KNOWS {since: 2018}]->(e),
+ (e)-[:KNOWS {since: 2022}]->(a)
+$$) AS (result agtype);
+
+-- Test max() on vertices (uses compare_agtype_scalar_values with AGTV_VERTEX)
+SELECT * FROM cypher('direct_access', $$
+ MATCH (p:Person)
+ RETURN max(p)
+$$) AS (max_vertex agtype);
+
+-- Test min() on vertices
+SELECT * FROM cypher('direct_access', $$
+ MATCH (p:Person)
+ RETURN min(p)
+$$) AS (min_vertex agtype);
+
+-- Test max() on edges (uses compare_agtype_scalar_values with AGTV_EDGE)
+SELECT * FROM cypher('direct_access', $$
+ MATCH ()-[r:KNOWS]->()
+ RETURN max(r)
+$$) AS (max_edge agtype);
+
+-- Test min() on edges
+SELECT * FROM cypher('direct_access', $$
+ MATCH ()-[r:KNOWS]->()
+ RETURN min(r)
+$$) AS (min_edge agtype);
+
+-- ORDER BY on vertices (uses direct id comparison)
+SELECT * FROM cypher('direct_access', $$
+ MATCH (p:Person)
+ RETURN p.name ORDER BY p
+$$) AS (name agtype);
+
+SELECT * FROM cypher('direct_access', $$
+ MATCH (p:Person)
+ RETURN p.name ORDER BY p DESC
+$$) AS (name agtype);
+
+-- ORDER BY on edges
+SELECT * FROM cypher('direct_access', $$
+ MATCH ()-[r:KNOWS]->()
+ RETURN r.since ORDER BY r
+$$) AS (since agtype);
+
+-- Vertex comparison in WHERE
+SELECT * FROM cypher('direct_access', $$
+ MATCH (a:Person), (b:Person)
+ WHERE a < b
+ RETURN a.name, b.name
+$$) AS (a_name agtype, b_name agtype);
+
+--
+-- Section 4: Fast Path for get_one_agtype_from_variadic_args
+--
+-- These tests exercise the fast path that bypasses extract_variadic_args
+-- when the argument is already agtype.
+--
+
+-- Direct agtype comparison operators (use the fast path)
+SELECT * FROM cypher('direct_access', $$
+ RETURN 42 = 42, 42 <> 43, 42 < 100, 42 > 10
+$$) AS (eq agtype, ne agtype, lt agtype, gt agtype);
+
+-- Arithmetic operators (also use the fast path)
+SELECT * FROM cypher('direct_access', $$
+ RETURN 10 + 5, 10 - 5, 10 * 5, 10 / 5
+$$) AS (add agtype, sub agtype, mul agtype, div agtype);
+
+-- String functions that take agtype args
+SELECT * FROM cypher('direct_access', $$
+ RETURN toUpper('hello'), toLower('WORLD'), size('test')
+$$) AS (upper agtype, lower agtype, sz agtype);
+
+-- Type checking functions
+SELECT * FROM cypher('direct_access', $$
+ RETURN toInteger('42'), toFloat('3.14'), toString(42)
+$$) AS (int_val agtype, float_val agtype, str_val agtype);
+
+--
+-- Section 5: Direct Field Access for Accessor Functions
+--
+-- These tests exercise the direct field access macros in id(), start_id(),
+-- end_id(), label(), and properties() functions.
+--
+
+-- Test id() on vertices (uses AGTYPE_VERTEX_GET_ID macro - index 0)
+SELECT * FROM cypher('direct_access', $$
+ MATCH (p:Person {name: 'Alice'})
+ RETURN id(p)
+$$) AS (vertex_id agtype);
+
+-- Test id() on edges (uses AGTYPE_EDGE_GET_ID macro - index 0)
+SELECT * FROM cypher('direct_access', $$
+ MATCH (a:Person {name: 'Alice'})-[r:KNOWS]->(b:Person {name: 'Bob'})
+ RETURN id(r)
+$$) AS (edge_id agtype);
+
+-- Test start_id() on edges (uses AGTYPE_EDGE_GET_START_ID macro - index 3)
+SELECT * FROM cypher('direct_access', $$
+ MATCH (a:Person {name: 'Alice'})-[r:KNOWS]->(b:Person {name: 'Bob'})
+ RETURN start_id(r), id(a)
+$$) AS (start_id agtype, alice_id agtype);
+
+-- Test end_id() on edges (uses AGTYPE_EDGE_GET_END_ID macro - index 2)
+SELECT * FROM cypher('direct_access', $$
+ MATCH (a:Person {name: 'Alice'})-[r:KNOWS]->(b:Person {name: 'Bob'})
+ RETURN end_id(r), id(b)
+$$) AS (end_id agtype, bob_id agtype);
+
+-- Test label() on vertices (uses AGTYPE_VERTEX_GET_LABEL macro - index 1)
+SELECT * FROM cypher('direct_access', $$
+ MATCH (p:Person {name: 'Alice'})
+ RETURN label(p)
+$$) AS (vertex_label agtype);
+
+-- Test label() on edges (uses AGTYPE_EDGE_GET_LABEL macro - index 1)
+SELECT * FROM cypher('direct_access', $$
+ MATCH ()-[r:KNOWS]->()
+ RETURN DISTINCT label(r)
+$$) AS (edge_label agtype);
+
+-- Test properties() on vertices (uses AGTYPE_VERTEX_GET_PROPERTIES macro -
index 2)
+SELECT * FROM cypher('direct_access', $$
+ MATCH (p:Person {name: 'Alice'})
+ RETURN properties(p)
+$$) AS (vertex_props agtype);
+
+-- Test properties() on edges (uses AGTYPE_EDGE_GET_PROPERTIES macro - index 4)
+SELECT * FROM cypher('direct_access', $$
+ MATCH (a:Person {name: 'Alice'})-[r:KNOWS]->(b:Person {name: 'Bob'})
+ RETURN properties(r)
+$$) AS (edge_props agtype);
+
+-- Combined accessor test - verify all fields are accessible
+SELECT * FROM cypher('direct_access', $$
+ MATCH (a:Person {name: 'Alice'})-[r:KNOWS]->(b:Person)
+ RETURN id(a), label(a), properties(a).name,
+ id(r), start_id(r), end_id(r), label(r), properties(r).since,
+ id(b), label(b), properties(b).name
+$$) AS (a_id agtype, a_label agtype, a_name agtype,
+ r_id agtype, r_start agtype, r_end agtype, r_label agtype, r_since
agtype,
+ b_id agtype, b_label agtype, b_name agtype);
+
+--
+-- Section 6: Mixed Comparisons and Edge Cases
+--
+
+-- Array comparisons (should NOT use scalar fast path)
+SELECT * FROM cypher('direct_access', $$
+ RETURN [1,2,3] = [1,2,3], [1,2,3] < [1,2,4]
+$$) AS (eq agtype, lt agtype);
+
+-- Object comparisons (should NOT use scalar fast path)
+SELECT * FROM cypher('direct_access', $$
+ RETURN {a:1, b:2} = {a:1, b:2}
+$$) AS (eq agtype);
+
+-- Large integer comparisons
+SELECT * FROM cypher('direct_access', $$
+ RETURN 9223372036854775807 > 9223372036854775806,
+ -9223372036854775808 < -9223372036854775807
+$$) AS (big_gt agtype, neg_lt agtype);
+
+-- Empty string comparison
+SELECT * FROM cypher('direct_access', $$
+ RETURN '' < 'a', '' = ''
+$$) AS (lt agtype, eq agtype);
+
+-- Special float values
+SELECT * FROM cypher('direct_access', $$
+ RETURN 0.0 = -0.0
+$$) AS (zero_eq agtype);
+
+--
+-- Cleanup
+--
+SELECT drop_graph('direct_access', true);
diff --git a/src/backend/utils/adt/agtype.c b/src/backend/utils/adt/agtype.c
index 02fc3221..f2458a30 100644
--- a/src/backend/utils/adt/agtype.c
+++ b/src/backend/utils/adt/agtype.c
@@ -5409,10 +5409,24 @@ Datum age_id(PG_FUNCTION_ARGS)
ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("id() argument must be a vertex, an edge or
null")));
- agtv_result = GET_AGTYPE_VALUE_OBJECT_VALUE(agtv_object, "id");
-
- Assert(agtv_result != NULL);
- Assert(agtv_result->type = AGTV_INTEGER);
+ /*
+ * Direct field access optimization: id is at a fixed index for both
+ * vertex and edge objects due to key length sorting.
+ */
+ if (agtv_object->type == AGTV_VERTEX)
+ {
+ agtv_result = AGTYPE_VERTEX_GET_ID(agtv_object);
+ }
+ else if (agtv_object->type == AGTV_EDGE)
+ {
+ agtv_result = AGTYPE_EDGE_GET_ID(agtv_object);
+ }
+ else
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("id() unexpected argument type")));
+ }
PG_RETURN_POINTER(agtype_value_to_agtype(agtv_result));
}
@@ -5447,10 +5461,11 @@ Datum age_start_id(PG_FUNCTION_ARGS)
ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("start_id() argument must be an edge or
null")));
- agtv_result = GET_AGTYPE_VALUE_OBJECT_VALUE(agtv_object, "start_id");
-
- Assert(agtv_result != NULL);
- Assert(agtv_result->type = AGTV_INTEGER);
+ /*
+ * Direct field access optimization: start_id is at index 3 for edge
+ * objects due to key length sorting (id=0, label=1, end_id=2, start_id=3).
+ */
+ agtv_result = AGTYPE_EDGE_GET_START_ID(agtv_object);
PG_RETURN_POINTER(agtype_value_to_agtype(agtv_result));
}
@@ -5485,10 +5500,11 @@ Datum age_end_id(PG_FUNCTION_ARGS)
ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("end_id() argument must be an edge or null")));
- agtv_result = GET_AGTYPE_VALUE_OBJECT_VALUE(agtv_object, "end_id");
-
- Assert(agtv_result != NULL);
- Assert(agtv_result->type = AGTV_INTEGER);
+ /*
+ * Direct field access optimization: end_id is at index 2 for edge
+ * objects due to key length sorting (id=0, label=1, end_id=2).
+ */
+ agtv_result = AGTYPE_EDGE_GET_END_ID(agtv_object);
PG_RETURN_POINTER(agtype_value_to_agtype(agtv_result));
}
@@ -6038,10 +6054,25 @@ Datum age_properties(PG_FUNCTION_ARGS)
errmsg("properties() argument must be a vertex, an
edge or null")));
}
- agtv_result = GET_AGTYPE_VALUE_OBJECT_VALUE(agtv_object, "properties");
-
- Assert(agtv_result != NULL);
- Assert(agtv_result->type = AGTV_OBJECT);
+ /*
+ * Direct field access optimization: properties is at index 2 for vertex
+ * (id=0, label=1, properties=2) and index 4 for edge (id=0, label=1,
+ * end_id=2, start_id=3, properties=4) due to key length sorting.
+ */
+ if (agtv_object->type == AGTV_VERTEX)
+ {
+ agtv_result = AGTYPE_VERTEX_GET_PROPERTIES(agtv_object);
+ }
+ else if (agtv_object->type == AGTV_EDGE)
+ {
+ agtv_result = AGTYPE_EDGE_GET_PROPERTIES(agtv_object);
+ }
+ else
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("properties() unexpected argument type")));
+ }
PG_RETURN_POINTER(agtype_value_to_agtype(agtv_result));
}
@@ -7170,8 +7201,24 @@ Datum age_label(PG_FUNCTION_ARGS)
}
- /* extract the label agtype value from the vertex or edge */
- label = GET_AGTYPE_VALUE_OBJECT_VALUE(agtv_value, "label");
+ /*
+ * Direct field access optimization: label is at a fixed index for both
+ * vertex and edge objects due to key length sorting.
+ */
+ if (agtv_value->type == AGTV_VERTEX)
+ {
+ label = AGTYPE_VERTEX_GET_LABEL(agtv_value);
+ }
+ else if (agtv_value->type == AGTV_EDGE)
+ {
+ label = AGTYPE_EDGE_GET_LABEL(agtv_value);
+ }
+ else
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("label() unexpected argument type")));
+ }
PG_RETURN_POINTER(agtype_value_to_agtype(label));
}
@@ -10507,6 +10554,59 @@ agtype
*get_one_agtype_from_variadic_args(FunctionCallInfo fcinfo,
Oid *types = NULL;
agtype *agtype_result = NULL;
+ /*
+ * Fast path optimization: For non-variadic calls where the argument
+ * is already an agtype, we can avoid the overhead of extract_variadic_args
+ * which allocates three arrays. This is the common case for most agtype
+ * comparison and arithmetic operators.
+ */
+ if (!get_fn_expr_variadic(fcinfo->flinfo))
+ {
+ int total_args = PG_NARGS();
+ int actual_nargs = total_args - variadic_offset;
+
+ /* Verify expected number of arguments */
+ if (actual_nargs != expected_nargs)
+ {
+ ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("number of args %d does not match expected
%d",
+ actual_nargs, expected_nargs)));
+ }
+
+ /* Check for SQL NULL */
+ if (PG_ARGISNULL(variadic_offset))
+ {
+ return NULL;
+ }
+
+ /* Check if the argument is already an agtype */
+ if (get_fn_expr_argtype(fcinfo->flinfo, variadic_offset) == AGTYPEOID)
+ {
+ agtype_container *agtc;
+
+ agtype_result =
DATUM_GET_AGTYPE_P(PG_GETARG_DATUM(variadic_offset));
+ agtc = &agtype_result->root;
+
+ /*
+ * Is this a scalar (scalars are stored as one element arrays)?
+ * If so, test for agtype NULL.
+ */
+ if (AGTYPE_CONTAINER_IS_SCALAR(agtc) &&
+ AGTE_IS_NULL(agtc->children[0]))
+ {
+ return NULL;
+ }
+
+ return agtype_result;
+ }
+
+ /*
+ * Not an agtype, need to convert. Fall through to use
+ * extract_variadic_args for type conversion handling.
+ */
+ }
+
+ /* Standard path using extract_variadic_args */
nargs = extract_variadic_args(fcinfo, variadic_offset, false, &args,
&types,
&nulls);
/* throw an error if the number of args is not the expected number */
diff --git a/src/backend/utils/adt/agtype_util.c
b/src/backend/utils/adt/agtype_util.c
index 01a965cd..b3972341 100644
--- a/src/backend/utils/adt/agtype_util.c
+++ b/src/backend/utils/adt/agtype_util.c
@@ -41,6 +41,14 @@
#include "utils/agtype_ext.h"
+/*
+ * Extended type header macros - must match definitions in agtype_ext.c.
+ * These are used for deserializing extended agtype values (INTEGER, FLOAT,
+ * VERTEX, EDGE, PATH) from their binary representation.
+ */
+#define AGT_HEADER_TYPE uint32
+#define AGT_HEADER_SIZE sizeof(AGT_HEADER_TYPE)
+
/*
* Maximum number of elements in an array (or key/value pairs in an object).
* This is limited by two things: the size of the agtentry array must fit
@@ -56,6 +64,11 @@
static void fill_agtype_value(agtype_container *container, int index,
char *base_addr, uint32 offset,
agtype_value *result);
+static void fill_agtype_value_no_copy(agtype_container *container, int index,
+ char *base_addr, uint32 offset,
+ agtype_value *result);
+static int compare_agtype_scalar_containers(agtype_container *a,
+ agtype_container *b);
static bool equals_agtype_scalar_value(agtype_value *a, agtype_value *b);
static agtype *convert_to_agtype(agtype_value *val);
static void convert_agtype_value(StringInfo buffer, agtentry *header,
@@ -264,6 +277,24 @@ int
compare_agtype_containers_orderability(agtype_container *a,
agtype_iterator *itb;
int res = 0;
+ /*
+ * Fast path optimization for scalar values.
+ *
+ * The most common case in ORDER BY and comparison operations is comparing
+ * scalar values (integers, strings, floats, etc.). For these cases, we can
+ * avoid the overhead of the full iterator machinery by directly extracting
+ * and comparing the scalar values.
+ *
+ * This provides significant performance improvement because:
+ * 1. We avoid allocating two agtype_iterator structures
+ * 2. We avoid the iterator state machine overhead
+ * 3. We use no-copy extraction where possible
+ */
+ if (AGTYPE_CONTAINER_IS_SCALAR(a) && AGTYPE_CONTAINER_IS_SCALAR(b))
+ {
+ return compare_agtype_scalar_containers(a, b);
+ }
+
ita = agtype_iterator_init(a);
itb = agtype_iterator_init(b);
@@ -751,6 +782,173 @@ static void fill_agtype_value(agtype_container
*container, int index,
}
}
+/*
+ * A helper function to fill in an agtype_value WITHOUT making deep copies.
+ * This is used for read-only comparison operations where the agtype_value
+ * will not outlive the container data. The caller MUST NOT free the
+ * agtype_value content or use it after the container is freed.
+ *
+ * This function provides significant performance improvements for comparison
+ * operations by avoiding palloc/memcpy for strings and numerics.
+ *
+ * Note: For AGTV_STRING, val.string.val points directly into container data.
+ * Note: For AGTV_NUMERIC, val.numeric points directly into container data.
+ * Note: Extended types (VERTEX, EDGE, PATH) still require deserialization,
+ * so they use the standard fill_agtype_value path.
+ */
+static void fill_agtype_value_no_copy(agtype_container *container, int index,
+ char *base_addr, uint32 offset,
+ agtype_value *result)
+{
+ agtentry entry = container->children[index];
+
+ if (AGTE_IS_NULL(entry))
+ {
+ result->type = AGTV_NULL;
+ }
+ else if (AGTE_IS_STRING(entry))
+ {
+ result->type = AGTV_STRING;
+ /* Point directly into the container data - no copy */
+ result->val.string.val = base_addr + offset;
+ result->val.string.len = get_agtype_length(container, index);
+ }
+ else if (AGTE_IS_NUMERIC(entry))
+ {
+ result->type = AGTV_NUMERIC;
+ /* Point directly into the container data - no copy */
+ result->val.numeric = (Numeric)(base_addr + INTALIGN(offset));
+ }
+ else if (AGTE_IS_AGTYPE(entry))
+ {
+ /*
+ * For extended types (INTEGER, FLOAT, VERTEX, EDGE, PATH), we need
+ * to deserialize. INTEGER and FLOAT don't allocate, but composite
+ * types (VERTEX, EDGE, PATH) do. For simple scalar comparisons,
+ * we handle INTEGER and FLOAT directly here.
+ */
+ char *base = base_addr + INTALIGN(offset);
+ AGT_HEADER_TYPE agt_header = *((AGT_HEADER_TYPE *)base);
+
+ switch (agt_header)
+ {
+ case AGT_HEADER_INTEGER:
+ result->type = AGTV_INTEGER;
+ result->val.int_value = *((int64 *)(base + AGT_HEADER_SIZE));
+ break;
+
+ case AGT_HEADER_FLOAT:
+ result->type = AGTV_FLOAT;
+ result->val.float_value = *((float8 *)(base + AGT_HEADER_SIZE));
+ break;
+
+ default:
+ /*
+ * For VERTEX, EDGE, PATH - use standard deserialization.
+ * These are composite types that require full parsing.
+ */
+ ag_deserialize_extended_type(base_addr, offset, result);
+ break;
+ }
+ }
+ else if (AGTE_IS_BOOL_TRUE(entry))
+ {
+ result->type = AGTV_BOOL;
+ result->val.boolean = true;
+ }
+ else if (AGTE_IS_BOOL_FALSE(entry))
+ {
+ result->type = AGTV_BOOL;
+ result->val.boolean = false;
+ }
+ else
+ {
+ Assert(AGTE_IS_CONTAINER(entry));
+ result->type = AGTV_BINARY;
+ /* Remove alignment padding from data pointer and length */
+ result->val.binary.data =
+ (agtype_container *)(base_addr + INTALIGN(offset));
+ result->val.binary.len = get_agtype_length(container, index) -
+ (INTALIGN(offset) - offset);
+ }
+}
+
+/*
+ * Fast path comparison for scalar agtype containers.
+ *
+ * This function compares two scalar containers directly without the overhead
+ * of the full iterator machinery. It extracts the scalar values using no-copy
+ * fill and compares them directly.
+ *
+ * Returns: negative if a < b, 0 if a == b, positive if a > b
+ */
+static int compare_agtype_scalar_containers(agtype_container *a,
+ agtype_container *b)
+{
+ agtype_value va;
+ agtype_value vb;
+ char *base_addr_a;
+ char *base_addr_b;
+ int result;
+ bool need_free_a = false;
+ bool need_free_b = false;
+
+ Assert(AGTYPE_CONTAINER_IS_SCALAR(a));
+ Assert(AGTYPE_CONTAINER_IS_SCALAR(b));
+
+ /* Scalars are stored as single-element arrays */
+ base_addr_a = (char *)&a->children[1];
+ base_addr_b = (char *)&b->children[1];
+
+ /* Use no-copy fill to avoid allocations for simple types */
+ fill_agtype_value_no_copy(a, 0, base_addr_a, 0, &va);
+ fill_agtype_value_no_copy(b, 0, base_addr_b, 0, &vb);
+
+ /*
+ * Check if we need to free the values after comparison.
+ * Only VERTEX, EDGE, and PATH types allocate memory in no-copy mode.
+ */
+ if (va.type == AGTV_VERTEX || va.type == AGTV_EDGE || va.type == AGTV_PATH)
+ {
+ need_free_a = true;
+ }
+ if (vb.type == AGTV_VERTEX || vb.type == AGTV_EDGE || vb.type == AGTV_PATH)
+ {
+ need_free_b = true;
+ }
+
+ /*
+ * Compare the scalar values. If types match or are numeric compatible,
+ * use scalar comparison. Otherwise, use type-based ordering.
+ */
+ if ((va.type == vb.type) ||
+ ((va.type == AGTV_INTEGER || va.type == AGTV_FLOAT ||
+ va.type == AGTV_NUMERIC) &&
+ (vb.type == AGTV_INTEGER || vb.type == AGTV_FLOAT ||
+ vb.type == AGTV_NUMERIC)))
+ {
+ result = compare_agtype_scalar_values(&va, &vb);
+ }
+ else
+ {
+ /* Type-defined order */
+ result = (get_type_sort_priority(va.type) <
+ get_type_sort_priority(vb.type)) ? -1 : 1;
+ }
+
+ /* Free any allocated memory from composite types */
+ if (need_free_a)
+ {
+ pfree_agtype_value_content(&va);
+ }
+ if (need_free_b)
+ {
+ pfree_agtype_value_content(&vb);
+ }
+
+ return result;
+}
+
/*
* Push agtype_value into agtype_parse_state.
*
@@ -1597,7 +1795,8 @@ void agtype_hash_scalar_value_extended(const agtype_value
*scalar_val,
case AGTV_VERTEX:
{
graphid id;
- agtype_value *id_agt = GET_AGTYPE_VALUE_OBJECT_VALUE(scalar_val, "id");
+ agtype_value *id_agt;
+ id_agt = AGTYPE_VERTEX_GET_ID(scalar_val);
id = id_agt->val.int_value;
tmp = DatumGetUInt64(DirectFunctionCall2(
hashint8extended, Float8GetDatum(id), UInt64GetDatum(seed)));
@@ -1606,7 +1805,8 @@ void agtype_hash_scalar_value_extended(const agtype_value
*scalar_val,
case AGTV_EDGE:
{
graphid id;
- agtype_value *id_agt = GET_AGTYPE_VALUE_OBJECT_VALUE(scalar_val, "id");
+ agtype_value *id_agt;
+ id_agt = AGTYPE_EDGE_GET_ID(scalar_val);
id = id_agt->val.int_value;
tmp = DatumGetUInt64(DirectFunctionCall2(
hashint8extended, Float8GetDatum(id), UInt64GetDatum(seed)));
@@ -1704,8 +1904,8 @@ static bool equals_agtype_scalar_value(agtype_value *a,
agtype_value *b)
case AGTV_VERTEX:
{
graphid a_graphid, b_graphid;
- a_graphid = a->val.object.pairs[0].value.val.int_value;
- b_graphid = b->val.object.pairs[0].value.val.int_value;
+ a_graphid = AGTYPE_VERTEX_GET_ID(a)->val.int_value;
+ b_graphid = AGTYPE_VERTEX_GET_ID(b)->val.int_value;
return a_graphid == b_graphid;
}
@@ -1790,16 +1990,33 @@ int compare_agtype_scalar_values(agtype_value *a,
agtype_value *b)
return compare_two_floats_orderability(a->val.float_value,
b->val.float_value);
case AGTV_VERTEX:
- case AGTV_EDGE:
{
- agtype_value *a_id, *b_id;
graphid a_graphid, b_graphid;
- a_id = GET_AGTYPE_VALUE_OBJECT_VALUE(a, "id");
- b_id = GET_AGTYPE_VALUE_OBJECT_VALUE(b, "id");
+ /* Direct field access optimization using macros defined in
agtype.h. */
+ a_graphid = AGTYPE_VERTEX_GET_ID(a)->val.int_value;
+ b_graphid = AGTYPE_VERTEX_GET_ID(b)->val.int_value;
+
+ if (a_graphid == b_graphid)
+ {
+ return 0;
+ }
+ else if (a_graphid > b_graphid)
+ {
+ return 1;
+ }
+ else
+ {
+ return -1;
+ }
+ }
+ case AGTV_EDGE:
+ {
+ graphid a_graphid, b_graphid;
- a_graphid = a_id->val.int_value;
- b_graphid = b_id->val.int_value;
+ /* Direct field access optimization using macros defined in
agtype.h. */
+ a_graphid = AGTYPE_EDGE_GET_ID(a)->val.int_value;
+ b_graphid = AGTYPE_EDGE_GET_ID(b)->val.int_value;
if (a_graphid == b_graphid)
{
diff --git a/src/include/utils/agtype.h b/src/include/utils/agtype.h
index ab2ba08c..ec912507 100644
--- a/src/include/utils/agtype.h
+++ b/src/include/utils/agtype.h
@@ -322,6 +322,109 @@ enum agtype_value_type
AGTV_BINARY
};
+/*
+ * Direct field access indices for vertex and edge objects.
+ *
+ * Vertex and edge objects are serialized with keys sorted by length first,
+ * then lexicographically (via uniqueify_agtype_object). This means field
+ * positions are deterministic and can be accessed directly without binary
+ * search, providing O(1) access instead of O(log n).
+ *
+ * Vertex keys by length: "id"(2), "label"(5), "properties"(10)
+ * Edge keys by length: "id"(2), "label"(5), "end_id"(6), "start_id"(8),
"properties"(10)
+ */
+#define VERTEX_FIELD_ID 0
+#define VERTEX_FIELD_LABEL 1
+#define VERTEX_FIELD_PROPERTIES 2
+#define VERTEX_NUM_FIELDS 3
+
+#define EDGE_FIELD_ID 0
+#define EDGE_FIELD_LABEL 1
+#define EDGE_FIELD_END_ID 2
+#define EDGE_FIELD_START_ID 3
+#define EDGE_FIELD_PROPERTIES 4
+#define EDGE_NUM_FIELDS 5
+
+/*
+ * Macros for direct field access from vertex/edge agtype_value objects.
+ * These avoid the binary search overhead of GET_AGTYPE_VALUE_OBJECT_VALUE.
+ * Validation is integrated - macros will error if field count is incorrect.
+ * Uses GCC statement expressions to allow validation within expressions.
+ */
+#define AGTYPE_VERTEX_GET_ID(v) \
+ ({ \
+ if ((v)->val.object.num_pairs != VERTEX_NUM_FIELDS) \
+ ereport(ERROR, \
+ (errcode(ERRCODE_DATA_CORRUPTED), \
+ errmsg("invalid vertex structure: expected %d fields,
found %d", \
+ VERTEX_NUM_FIELDS, (v)->val.object.num_pairs))); \
+ &(v)->val.object.pairs[VERTEX_FIELD_ID].value; \
+ })
+#define AGTYPE_VERTEX_GET_LABEL(v) \
+ ({ \
+ if ((v)->val.object.num_pairs != VERTEX_NUM_FIELDS) \
+ ereport(ERROR, \
+ (errcode(ERRCODE_DATA_CORRUPTED), \
+ errmsg("invalid vertex structure: expected %d fields,
found %d", \
+ VERTEX_NUM_FIELDS, (v)->val.object.num_pairs))); \
+ &(v)->val.object.pairs[VERTEX_FIELD_LABEL].value; \
+ })
+#define AGTYPE_VERTEX_GET_PROPERTIES(v) \
+ ({ \
+ if ((v)->val.object.num_pairs != VERTEX_NUM_FIELDS) \
+ ereport(ERROR, \
+ (errcode(ERRCODE_DATA_CORRUPTED), \
+ errmsg("invalid vertex structure: expected %d fields,
found %d", \
+ VERTEX_NUM_FIELDS, (v)->val.object.num_pairs))); \
+ &(v)->val.object.pairs[VERTEX_FIELD_PROPERTIES].value; \
+ })
+
+#define AGTYPE_EDGE_GET_ID(e) \
+ ({ \
+ if ((e)->val.object.num_pairs != EDGE_NUM_FIELDS) \
+ ereport(ERROR, \
+ (errcode(ERRCODE_DATA_CORRUPTED), \
+ errmsg("invalid edge structure: expected %d fields, found
%d", \
+ EDGE_NUM_FIELDS, (e)->val.object.num_pairs))); \
+ &(e)->val.object.pairs[EDGE_FIELD_ID].value; \
+ })
+#define AGTYPE_EDGE_GET_LABEL(e) \
+ ({ \
+ if ((e)->val.object.num_pairs != EDGE_NUM_FIELDS) \
+ ereport(ERROR, \
+ (errcode(ERRCODE_DATA_CORRUPTED), \
+ errmsg("invalid edge structure: expected %d fields, found
%d", \
+ EDGE_NUM_FIELDS, (e)->val.object.num_pairs))); \
+ &(e)->val.object.pairs[EDGE_FIELD_LABEL].value; \
+ })
+#define AGTYPE_EDGE_GET_END_ID(e) \
+ ({ \
+ if ((e)->val.object.num_pairs != EDGE_NUM_FIELDS) \
+ ereport(ERROR, \
+ (errcode(ERRCODE_DATA_CORRUPTED), \
+ errmsg("invalid edge structure: expected %d fields, found
%d", \
+ EDGE_NUM_FIELDS, (e)->val.object.num_pairs))); \
+ &(e)->val.object.pairs[EDGE_FIELD_END_ID].value; \
+ })
+#define AGTYPE_EDGE_GET_START_ID(e) \
+ ({ \
+ if ((e)->val.object.num_pairs != EDGE_NUM_FIELDS) \
+ ereport(ERROR, \
+ (errcode(ERRCODE_DATA_CORRUPTED), \
+ errmsg("invalid edge structure: expected %d fields, found
%d", \
+ EDGE_NUM_FIELDS, (e)->val.object.num_pairs))); \
+ &(e)->val.object.pairs[EDGE_FIELD_START_ID].value; \
+ })
+#define AGTYPE_EDGE_GET_PROPERTIES(e) \
+ ({ \
+ if ((e)->val.object.num_pairs != EDGE_NUM_FIELDS) \
+ ereport(ERROR, \
+ (errcode(ERRCODE_DATA_CORRUPTED), \
+ errmsg("invalid edge structure: expected %d fields, found
%d", \
+ EDGE_NUM_FIELDS, (e)->val.object.num_pairs))); \
+ &(e)->val.object.pairs[EDGE_FIELD_PROPERTIES].value; \
+ })
+
/*
* agtype_value: In-memory representation of agtype. This is a convenient
* deserialized representation, that can easily support using the "val"