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);
 

Reply via email to