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 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
diff --git a/regress/expected/cypher_match.out b/regress/expected/cypher_match.out
index ff2825a..94315f1 100644
--- a/regress/expected/cypher_match.out
+++ b/regress/expected/cypher_match.out
@@ -3633,6 +3633,110 @@
(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 @@
(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 ebcd67b..d14f45f 100644
--- a/regress/sql/cypher_match.sql
+++ b/regress/sql/cypher_match.sql
@@ -1491,6 +1491,72 @@
$$) 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('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 446e97b..6f06bbb 100644
--- a/src/backend/parser/cypher_clause.c
+++ b/src/backend/parser/cypher_clause.c
@@ -4261,6 +4261,38 @@
}
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 @@
-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 @@
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);