This is an automated email from the ASF dual-hosted git repository.
jgemignani 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 23146a44 Fix crash in PREPARE with property parameter when
enable_containment is off (#2339)
23146a44 is described below
commit 23146a44c36c0ece003b7b91f7942385ae8ac5e7
Author: Greg Felice <[email protected]>
AuthorDate: Tue Mar 3 12:52:48 2026 -0500
Fix crash in PREPARE with property parameter when enable_containment is off
(#2339)
* Fix crash in PREPARE with property parameter when enable_containment is
off
When age.enable_containment is set to off, executing a PREPARE statement
with a property parameter (e.g., MATCH (n $props) RETURN n) causes a
segfault. The crash occurs in transform_map_to_ind_recursive because
the property_constraints node is a cypher_param, not a cypher_map, but
is blindly cast to cypher_map and its keyvals field is dereferenced.
Three fixes:
- In create_property_constraints, when enable_containment is off and the
constraint is a cypher_param, fall back to the containment operator
(@>) since map decomposition requires known keys at parse time.
- In transform_match_entities, guard the keep_null assignment for both
vertex and edge property constraints with is_ag_node checks to avoid
writing to the wrong struct layout.
Fixes #1964
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Fix @> vs @>> for =properties form with PREPARE and add tests
When MATCH uses the =properties form (e.g., MATCH (n = $props)), the
enable_containment=on path correctly uses @>> (top-level containment).
The parameter fallback path unconditionally used @> (deep containment),
ignoring the use_equals flag. Fix the fallback to mirror the
enable_containment path by selecting @>> when use_equals is set.
Add regression tests for =properties form with PREPARE for both
vertices and edges, with enable_containment on and off.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
---
regress/expected/cypher_match.out | 116 +++++++++++++++++++++++++++++++++++++
regress/sql/cypher_match.sql | 67 +++++++++++++++++++++
src/backend/parser/cypher_clause.c | 42 +++++++++++++-
3 files changed, 223 insertions(+), 2 deletions(-)
diff --git a/regress/expected/cypher_match.out
b/regress/expected/cypher_match.out
index ff2825ae..94315f13 100644
--- a/regress/expected/cypher_match.out
+++ b/regress/expected/cypher_match.out
@@ -3633,6 +3633,110 @@ NOTICE: graph "issue_2308" has been dropped
(1 row)
+-- Issue 1964
+--
+-- PREPARE with property parameter ($props) crashed the server when
+-- age.enable_containment was set to off. The crash was in
+-- transform_map_to_ind_recursive which blindly cast cypher_param
+-- nodes to cypher_map, accessing invalid memory.
+--
+SELECT create_graph('issue_1964');
+NOTICE: graph "issue_1964" has been created
+ create_graph
+--------------
+
+(1 row)
+
+SELECT * FROM cypher('issue_1964', $$
+ CREATE (:Person {name: 'Alice', age: 30}),
+ (:Person {name: 'Bob', age: 25})
+$$) AS (result agtype);
+ result
+--------
+(0 rows)
+
+SELECT * FROM cypher('issue_1964', $$
+ CREATE (:Person {name: 'Alice'})-[:KNOWS {since: 2020}]->(:Person {name:
'Bob'})
+$$) AS (result agtype);
+ result
+--------
+(0 rows)
+
+-- Test PREPARE with enable_containment off (was crashing)
+SET age.enable_containment = off;
+PREPARE issue_1964_vertex(agtype) AS
+ SELECT * FROM cypher('issue_1964',
+ $$MATCH (n $props) RETURN n $$, $1) AS (p agtype);
+EXECUTE issue_1964_vertex('{"props": {"name": "Alice"}}');
+ p
+------------------------------------------------------------------------------------------------
+ {"id": 844424930131969, "label": "Person", "properties": {"age": 30, "name":
"Alice"}}::vertex
+ {"id": 844424930131971, "label": "Person", "properties": {"name":
"Alice"}}::vertex
+(2 rows)
+
+EXECUTE issue_1964_vertex('{"props": {"age": 25}}');
+ p
+----------------------------------------------------------------------------------------------
+ {"id": 844424930131970, "label": "Person", "properties": {"age": 25, "name":
"Bob"}}::vertex
+(1 row)
+
+DEALLOCATE issue_1964_vertex;
+-- Test edge property parameter with enable_containment off
+PREPARE issue_1964_edge(agtype) AS
+ SELECT * FROM cypher('issue_1964',
+ $$MATCH ()-[r $props]->() RETURN r $$, $1) AS (p agtype);
+EXECUTE issue_1964_edge('{"props": {"since": 2020}}');
+ p
+-----------------------------------------------------------------------------------------------------------------------------------------
+ {"id": 1125899906842625, "label": "KNOWS", "end_id": 844424930131972,
"start_id": 844424930131971, "properties": {"since": 2020}}::edge
+(1 row)
+
+DEALLOCATE issue_1964_edge;
+-- Verify enable_containment on still works with PREPARE
+SET age.enable_containment = on;
+PREPARE issue_1964_vertex_on(agtype) AS
+ SELECT * FROM cypher('issue_1964',
+ $$MATCH (n $props) RETURN n $$, $1) AS (p agtype);
+EXECUTE issue_1964_vertex_on('{"props": {"name": "Alice"}}');
+ p
+------------------------------------------------------------------------------------------------
+ {"id": 844424930131969, "label": "Person", "properties": {"age": 30, "name":
"Alice"}}::vertex
+ {"id": 844424930131971, "label": "Person", "properties": {"name":
"Alice"}}::vertex
+(2 rows)
+
+DEALLOCATE issue_1964_vertex_on;
+-- Test =properties form with PREPARE (uses @>> top-level containment)
+SET age.enable_containment = off;
+PREPARE issue_1964_vertex_eq(agtype) AS
+ SELECT * FROM cypher('issue_1964',
+ $$MATCH (n = $props) RETURN n $$, $1) AS (p agtype);
+EXECUTE issue_1964_vertex_eq('{"props": {"name": "Alice", "age": 25}}');
+ p
+---
+(0 rows)
+
+DEALLOCATE issue_1964_vertex_eq;
+PREPARE issue_1964_edge_eq(agtype) AS
+ SELECT * FROM cypher('issue_1964',
+ $$MATCH ()-[r = $props]->() RETURN r $$, $1) AS (p agtype);
+EXECUTE issue_1964_edge_eq('{"props": {"since": 2020}}');
+ p
+-----------------------------------------------------------------------------------------------------------------------------------------
+ {"id": 1125899906842625, "label": "KNOWS", "end_id": 844424930131972,
"start_id": 844424930131971, "properties": {"since": 2020}}::edge
+(1 row)
+
+DEALLOCATE issue_1964_edge_eq;
+-- Same with enable_containment on
+SET age.enable_containment = on;
+PREPARE issue_1964_vertex_eq_on(agtype) AS
+ SELECT * FROM cypher('issue_1964',
+ $$MATCH (n = $props) RETURN n $$, $1) AS (p agtype);
+EXECUTE issue_1964_vertex_eq_on('{"props": {"name": "Alice", "age": 25}}');
+ p
+---
+(0 rows)
+
+DEALLOCATE issue_1964_vertex_eq_on;
--
-- Clean up
--
@@ -3721,6 +3825,18 @@ NOTICE: graph "issue_1393" has been dropped
(1 row)
+SELECT drop_graph('issue_1964', true);
+NOTICE: drop cascades to 4 other objects
+DETAIL: drop cascades to table issue_1964._ag_label_vertex
+drop cascades to table issue_1964._ag_label_edge
+drop cascades to table issue_1964."Person"
+drop cascades to table issue_1964."KNOWS"
+NOTICE: graph "issue_1964" has been dropped
+ drop_graph
+------------
+
+(1 row)
+
--
-- End
--
diff --git a/regress/sql/cypher_match.sql b/regress/sql/cypher_match.sql
index ebcd67b8..d14f45f1 100644
--- a/regress/sql/cypher_match.sql
+++ b/regress/sql/cypher_match.sql
@@ -1491,6 +1491,72 @@ SELECT * FROM cypher('issue_2308', $$
$$) AS (val agtype);
SELECT drop_graph('issue_2308', true);
+-- Issue 1964
+--
+-- PREPARE with property parameter ($props) crashed the server when
+-- age.enable_containment was set to off. The crash was in
+-- transform_map_to_ind_recursive which blindly cast cypher_param
+-- nodes to cypher_map, accessing invalid memory.
+--
+
+SELECT create_graph('issue_1964');
+SELECT * FROM cypher('issue_1964', $$
+ CREATE (:Person {name: 'Alice', age: 30}),
+ (:Person {name: 'Bob', age: 25})
+$$) AS (result agtype);
+SELECT * FROM cypher('issue_1964', $$
+ CREATE (:Person {name: 'Alice'})-[:KNOWS {since: 2020}]->(:Person {name:
'Bob'})
+$$) AS (result agtype);
+
+-- Test PREPARE with enable_containment off (was crashing)
+SET age.enable_containment = off;
+
+PREPARE issue_1964_vertex(agtype) AS
+ SELECT * FROM cypher('issue_1964',
+ $$MATCH (n $props) RETURN n $$, $1) AS (p agtype);
+EXECUTE issue_1964_vertex('{"props": {"name": "Alice"}}');
+EXECUTE issue_1964_vertex('{"props": {"age": 25}}');
+DEALLOCATE issue_1964_vertex;
+
+-- Test edge property parameter with enable_containment off
+PREPARE issue_1964_edge(agtype) AS
+ SELECT * FROM cypher('issue_1964',
+ $$MATCH ()-[r $props]->() RETURN r $$, $1) AS (p agtype);
+EXECUTE issue_1964_edge('{"props": {"since": 2020}}');
+DEALLOCATE issue_1964_edge;
+
+-- Verify enable_containment on still works with PREPARE
+SET age.enable_containment = on;
+
+PREPARE issue_1964_vertex_on(agtype) AS
+ SELECT * FROM cypher('issue_1964',
+ $$MATCH (n $props) RETURN n $$, $1) AS (p agtype);
+EXECUTE issue_1964_vertex_on('{"props": {"name": "Alice"}}');
+DEALLOCATE issue_1964_vertex_on;
+
+-- Test =properties form with PREPARE (uses @>> top-level containment)
+SET age.enable_containment = off;
+
+PREPARE issue_1964_vertex_eq(agtype) AS
+ SELECT * FROM cypher('issue_1964',
+ $$MATCH (n = $props) RETURN n $$, $1) AS (p agtype);
+EXECUTE issue_1964_vertex_eq('{"props": {"name": "Alice", "age": 25}}');
+DEALLOCATE issue_1964_vertex_eq;
+
+PREPARE issue_1964_edge_eq(agtype) AS
+ SELECT * FROM cypher('issue_1964',
+ $$MATCH ()-[r = $props]->() RETURN r $$, $1) AS (p agtype);
+EXECUTE issue_1964_edge_eq('{"props": {"since": 2020}}');
+DEALLOCATE issue_1964_edge_eq;
+
+-- Same with enable_containment on
+SET age.enable_containment = on;
+
+PREPARE issue_1964_vertex_eq_on(agtype) AS
+ SELECT * FROM cypher('issue_1964',
+ $$MATCH (n = $props) RETURN n $$, $1) AS (p agtype);
+EXECUTE issue_1964_vertex_eq_on('{"props": {"name": "Alice", "age": 25}}');
+DEALLOCATE issue_1964_vertex_eq_on;
--
-- Clean up
@@ -1501,6 +1567,7 @@ SELECT drop_graph('test_enable_containment', true);
SELECT drop_graph('issue_945', true);
SELECT drop_graph('issue_1399', true);
SELECT drop_graph('issue_1393', true);
+SELECT drop_graph('issue_1964', true);
--
-- End
diff --git a/src/backend/parser/cypher_clause.c
b/src/backend/parser/cypher_clause.c
index 446e97b3..6f06bbb8 100644
--- a/src/backend/parser/cypher_clause.c
+++ b/src/backend/parser/cypher_clause.c
@@ -4261,6 +4261,38 @@ static Node
*create_property_constraints(cypher_parsestate *cpstate,
}
else
{
+ /*
+ * Map decomposition into individual index lookups requires known
+ * keys at parse time. When the property constraint is a parameter
+ * (cypher_param), the keys are not available until execution, so
+ * fall back to the containment operator.
+ */
+ if (is_ag_node(property_constraints, cypher_param))
+ {
+ /*
+ * Use @>> (top-level containment) for =properties form,
+ * @> (deep containment) otherwise — matching the
+ * enable_containment=on path above.
+ */
+ if ((entity->type == ENT_VERTEX &&
+ entity->entity.node->use_equals) ||
+ ((entity->type == ENT_EDGE ||
+ entity->type == ENT_VLE_EDGE) &&
+ entity->entity.rel->use_equals))
+ {
+ return (Node *)make_op(pstate,
+ list_make1(makeString("@>>")),
+ prop_expr, const_expr,
+ last_srf, -1);
+ }
+ else
+ {
+ return (Node *)make_op(pstate,
+ list_make1(makeString("@>")),
+ prop_expr, const_expr,
+ last_srf, -1);
+ }
+ }
return (Node *)transform_map_to_ind(
cpstate, entity, (cypher_map *)property_constraints);
}
@@ -4690,7 +4722,10 @@ static List *transform_match_entities(cypher_parsestate
*cpstate, Query *query,
-1);
}
- ((cypher_map*)node->props)->keep_null = true;
+ if (is_ag_node(node->props, cypher_map))
+ {
+ ((cypher_map*)node->props)->keep_null = true;
+ }
n = create_property_constraints(cpstate, entity, node->props,
prop_expr);
@@ -4819,7 +4854,10 @@ static List *transform_match_entities(cypher_parsestate
*cpstate, Query *query,
false, -1);
}
- ((cypher_map*)rel->props)->keep_null = true;
+ if (is_ag_node(rel->props, cypher_map))
+ {
+ ((cypher_map*)rel->props)->keep_null = true;
+ }
r = create_property_constraints(cpstate, entity,
rel->props,
prop_expr);