From 5a4246822d18bb67c596944f9278a8c3367b6428 Mon Sep 17 00:00:00 2001 From: Muhammad Taha Naveed Date: Mon, 22 Jan 2024 18:23:23 +0500 Subject: [PATCH] Add optional parameter '=' in property constraints - The '=' operator checks if the original property value(as a whole) is equal to the given value. e.g MATCH (n {school:{addr:{city:'Toronto'}}}) tranforms into either(in case age.enable_containment is off) `properties.school.addr.city = 'Toronto'` or(in case age.enable_containment is on) `properties @> {school:{addr:{city:'Toronto'}}}` But MATCH (n ={school:{addr:{city:'Toronto'}}}) will tranform into either(in case age.enable_containment is off) `properties.school = {addr:{city:'Toronto'}}` or(in case age.enable_containment is on) `properties @>> {school:{addr:{city:'Toronto'}}}` - Added @>> and <<@ operators. Unlike @> and <@, these operators does not recurse into sub-objects. - Added regression tests. --- regress/expected/cypher_match.out | 246 +++++++++++++++++++++++++++- regress/sql/cypher_match.sql | 55 +++++++ sql/agtype_gin.sql | 2 + sql/agtype_operators.sql | 34 ++++ src/backend/parser/cypher_clause.c | 115 ++++++++++++- src/backend/parser/cypher_gram.y | 33 ++++ src/backend/utils/adt/age_vle.c | 2 +- src/backend/utils/adt/agtype_gin.c | 7 +- src/backend/utils/adt/agtype_ops.c | 99 ++++++++++- src/backend/utils/adt/agtype_util.c | 23 ++- src/include/nodes/cypher_nodes.h | 2 + src/include/utils/agtype.h | 3 +- 12 files changed, 600 insertions(+), 21 deletions(-) diff --git a/regress/expected/cypher_match.out b/regress/expected/cypher_match.out index cef6dad94..1b1fbaa33 100644 --- a/regress/expected/cypher_match.out +++ b/regress/expected/cypher_match.out @@ -3293,6 +3293,248 @@ $$) AS (n1 agtype, n2 agtype, n3 agtype, e1 agtype); {"id": 844424930131970, "label": "Object", "properties": {}}::vertex | {"id": 844424930131971, "label": "Object", "properties": {}}::vertex | {"id": 844424930131970, "label": "Object", "properties": {}}::vertex | {"id": 1125899906842625, "label": "knows", "end_id": 844424930131971, "start_id": 844424930131970, "properties": {}}::edge (1 row) +-- +-- Issue 1461 +-- +-- Using the test_enable_containment graph for these tests +SELECT * FROM cypher('test_enable_containment', $$ CREATE p=(:Customer)-[:bought {store:'Amazon', addr:{city: 'Vancouver', street: 30}}]->(y:Product) RETURN p $$) as (a agtype); + a +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + [{"id": 844424930131970, "label": "Customer", "properties": {}}::vertex, {"id": 1125899906842625, "label": "bought", "end_id": 1407374883553281, "start_id": 844424930131970, "properties": {"addr": {"city": "Vancouver", "street": 30}, "store": "Amazon"}}::edge, {"id": 1407374883553281, "label": "Product", "properties": {}}::vertex]::path +(1 row) + +-- With enable_containment on +SET age.enable_containment = on; +-- Should return 0 +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={addr:[{city:'Toronto'}]}) RETURN x $$) as (a agtype); + count +------- + 0 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={school:{program:{major:'Psyc'}}}) RETURN x $$) as (a agtype); + count +------- + 0 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={name:'Bob',school:{program:{degree:'BSc'}}}) RETURN x $$) as (a agtype); + count +------- + 0 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={school:{program:{major:'Cs'}}}) RETURN x $$) as (a agtype); + count +------- + 0 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={name:'Bob',school:{program:{degree:'PHd'}}}) RETURN x $$) as (a agtype); + count +------- + 0 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={phone:[987654321]}) RETURN x $$) as (a agtype); + count +------- + 0 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={phone:[654765876]}) RETURN x $$) as (a agtype); + count +------- + 0 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x)-[:bought ={store: 'Amazon', addr:{city: 'Vancouver'}}]->() RETURN x $$) as (a agtype); + count +------- + 0 +(1 row) + +-- Should return 1 +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={addr: [{city: 'Vancouver', street: 30},{city: 'Toronto', street: 40}]}) RETURN x $$) as (a agtype); + count +------- + 1 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={school: { name: 'XYZ College',program: { major: 'Psyc', degree: 'BSc'}}}) RETURN x $$) as (a agtype); + count +------- + 1 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={phone: [ 123456789, 987654321, 456987123 ]}) RETURN x $$) as (a agtype); + count +------- + 1 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={school: { name: 'XYZ College',program: { major: 'Psyc', degree: 'BSc'} },phone: [ 123456789, 987654321, 456987123 ]}) RETURN x $$) as (a agtype); + count +------- + 1 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH p=(x:Customer)-[:bought ={store: 'Amazon'}]->() RETURN p $$) as (a agtype); + count +------- + 1 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH p=(x:Customer)-[:bought ={store: 'Amazon', addr:{city: 'Vancouver', street: 30}}]->() RETURN p $$) as (a agtype); + count +------- + 1 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH p=(x:Customer)-[:bought {store: 'Amazon', addr:{city: 'Vancouver'}}]->() RETURN p $$) as (a agtype); + count +------- + 1 +(1 row) + +SELECT * FROM cypher('test_enable_containment', $$ EXPLAIN (costs off) MATCH (x:Customer)-[:bought ={store: 'Amazon', addr:{city: 'Vancouver', street: 30}}]->(y:Product) RETURN 0 $$) as (a agtype); + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------- + Hash Join + Hash Cond: (y.id = _age_default_alias_0.end_id) + -> Seq Scan on "Product" y + -> Hash + -> Hash Join + Hash Cond: (x.id = _age_default_alias_0.start_id) + -> Seq Scan on "Customer" x + -> Hash + -> Seq Scan on bought _age_default_alias_0 + Filter: (properties @>> '{"addr": {"city": "Vancouver", "street": 30}, "store": "Amazon"}'::agtype) +(10 rows) + +SELECT * FROM cypher('test_enable_containment', $$ EXPLAIN (costs off) MATCH (x:Customer ={school: { name: 'XYZ College',program: { major: 'Psyc', degree: 'BSc'} },phone: [ 123456789, 987654321, 456987123 ]}) RETURN 0 $$) as (a agtype); + QUERY PLAN +--------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Seq Scan on "Customer" x + Filter: (properties @>> '{"phone": [123456789, 987654321, 456987123], "school": {"name": "XYZ College", "program": {"major": "Psyc", "degree": "BSc"}}}'::agtype) +(2 rows) + +-- With enable_containment off +SET age.enable_containment = off; +-- Should return 0 +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={addr:[{city:'Toronto'}]}) RETURN x $$) as (a agtype); + count +------- + 0 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={school:{program:{major:'Psyc'}}}) RETURN x $$) as (a agtype); + count +------- + 0 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={name:'Bob',school:{program:{degree:'BSc'}}}) RETURN x $$) as (a agtype); + count +------- + 0 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={school:{program:{major:'Cs'}}}) RETURN x $$) as (a agtype); + count +------- + 0 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={name:'Bob',school:{program:{degree:'PHd'}}}) RETURN x $$) as (a agtype); + count +------- + 0 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={phone:[987654321]}) RETURN x $$) as (a agtype); + count +------- + 0 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={phone:[654765876]}) RETURN x $$) as (a agtype); + count +------- + 0 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x)-[:bought ={store: 'Amazon', addr:{city: 'Vancouver'}}]->() RETURN x $$) as (a agtype); + count +------- + 0 +(1 row) + +-- Should return 1 +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={addr: [{city: 'Vancouver', street: 30},{city: 'Toronto', street: 40}]}) RETURN x $$) as (a agtype); + count +------- + 1 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={school: { name: 'XYZ College',program: { major: 'Psyc', degree: 'BSc'}}}) RETURN x $$) as (a agtype); + count +------- + 1 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={phone: [ 123456789, 987654321, 456987123 ]}) RETURN x $$) as (a agtype); + count +------- + 1 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={school: { name: 'XYZ College',program: { major: 'Psyc', degree: 'BSc'} },phone: [ 123456789, 987654321, 456987123 ]}) RETURN x $$) as (a agtype); + count +------- + 1 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH p=(x:Customer)-[:bought ={store: 'Amazon'}]->() RETURN p $$) as (a agtype); + count +------- + 1 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH p=(x:Customer)-[:bought ={store: 'Amazon', addr:{city: 'Vancouver', street: 30}}]->() RETURN p $$) as (a agtype); + count +------- + 1 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH p=(x:Customer)-[:bought {store: 'Amazon', addr:{city: 'Vancouver'}}]->() RETURN p $$) as (a agtype); + count +------- + 1 +(1 row) + +SELECT * FROM cypher('test_enable_containment', $$ EXPLAIN (costs off) MATCH (x:Customer)-[:bought ={store: 'Amazon', addr:{city: 'Vancouver', street: 30}}]->(y:Product) RETURN 0 $$) as (a agtype); + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Hash Join + Hash Cond: (y.id = _age_default_alias_0.end_id) + -> Seq Scan on "Product" y + -> Hash + -> Hash Join + Hash Cond: (x.id = _age_default_alias_0.start_id) + -> Seq Scan on "Customer" x + -> Hash + -> Seq Scan on bought _age_default_alias_0 + Filter: ((agtype_access_operator(VARIADIC ARRAY[properties, '"store"'::agtype]) = '"Amazon"'::agtype) AND (agtype_access_operator(VARIADIC ARRAY[properties, '"addr"'::agtype]) = '{"city": "Vancouver", "street": 30}'::agtype)) +(10 rows) + +SELECT * FROM cypher('test_enable_containment', $$ EXPLAIN (costs off) MATCH (x:Customer ={school: { name: 'XYZ College',program: { major: 'Psyc', degree: 'BSc'} },phone: [ 123456789, 987654321, 456987123 ]}) RETURN 0 $$) as (a agtype); + QUERY PLAN +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Seq Scan on "Customer" x + Filter: ((agtype_access_operator(VARIADIC ARRAY[properties, '"school"'::agtype]) = '{"name": "XYZ College", "program": {"major": "Psyc", "degree": "BSc"}}'::agtype) AND (agtype_access_operator(VARIADIC ARRAY[properties, '"phone"'::agtype]) = '[123456789, 987654321, 456987123]'::agtype)) +(2 rows) + -- -- Clean up -- @@ -3335,10 +3577,12 @@ NOTICE: graph "test_retrieve_var" has been dropped (1 row) SELECT drop_graph('test_enable_containment', true); -NOTICE: drop cascades to 3 other objects +NOTICE: drop cascades to 5 other objects DETAIL: drop cascades to table test_enable_containment._ag_label_vertex drop cascades to table test_enable_containment._ag_label_edge drop cascades to table test_enable_containment."Customer" +drop cascades to table test_enable_containment.bought +drop cascades to table test_enable_containment."Product" NOTICE: graph "test_enable_containment" has been dropped drop_graph ------------ diff --git a/regress/sql/cypher_match.sql b/regress/sql/cypher_match.sql index b211439ea..1ac29419a 100644 --- a/regress/sql/cypher_match.sql +++ b/regress/sql/cypher_match.sql @@ -1382,6 +1382,61 @@ SELECT * FROM cypher('issue_1393', $$ WHERE EXISTS((n3:Object)-[e1:knows]->(n2:Object)) RETURN n1,n2,n3,e1 $$) AS (n1 agtype, n2 agtype, n3 agtype, e1 agtype); +-- +-- Issue 1461 +-- + +-- Using the test_enable_containment graph for these tests +SELECT * FROM cypher('test_enable_containment', $$ CREATE p=(:Customer)-[:bought {store:'Amazon', addr:{city: 'Vancouver', street: 30}}]->(y:Product) RETURN p $$) as (a agtype); + +-- With enable_containment on +SET age.enable_containment = on; +-- Should return 0 +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={addr:[{city:'Toronto'}]}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={school:{program:{major:'Psyc'}}}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={name:'Bob',school:{program:{degree:'BSc'}}}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={school:{program:{major:'Cs'}}}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={name:'Bob',school:{program:{degree:'PHd'}}}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={phone:[987654321]}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={phone:[654765876]}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x)-[:bought ={store: 'Amazon', addr:{city: 'Vancouver'}}]->() RETURN x $$) as (a agtype); + +-- Should return 1 +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={addr: [{city: 'Vancouver', street: 30},{city: 'Toronto', street: 40}]}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={school: { name: 'XYZ College',program: { major: 'Psyc', degree: 'BSc'}}}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={phone: [ 123456789, 987654321, 456987123 ]}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={school: { name: 'XYZ College',program: { major: 'Psyc', degree: 'BSc'} },phone: [ 123456789, 987654321, 456987123 ]}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH p=(x:Customer)-[:bought ={store: 'Amazon'}]->() RETURN p $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH p=(x:Customer)-[:bought ={store: 'Amazon', addr:{city: 'Vancouver', street: 30}}]->() RETURN p $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH p=(x:Customer)-[:bought {store: 'Amazon', addr:{city: 'Vancouver'}}]->() RETURN p $$) as (a agtype); + +SELECT * FROM cypher('test_enable_containment', $$ EXPLAIN (costs off) MATCH (x:Customer)-[:bought ={store: 'Amazon', addr:{city: 'Vancouver', street: 30}}]->(y:Product) RETURN 0 $$) as (a agtype); +SELECT * FROM cypher('test_enable_containment', $$ EXPLAIN (costs off) MATCH (x:Customer ={school: { name: 'XYZ College',program: { major: 'Psyc', degree: 'BSc'} },phone: [ 123456789, 987654321, 456987123 ]}) RETURN 0 $$) as (a agtype); + +-- With enable_containment off +SET age.enable_containment = off; +-- Should return 0 +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={addr:[{city:'Toronto'}]}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={school:{program:{major:'Psyc'}}}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={name:'Bob',school:{program:{degree:'BSc'}}}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={school:{program:{major:'Cs'}}}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={name:'Bob',school:{program:{degree:'PHd'}}}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={phone:[987654321]}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={phone:[654765876]}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x)-[:bought ={store: 'Amazon', addr:{city: 'Vancouver'}}]->() RETURN x $$) as (a agtype); + +-- Should return 1 +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={addr: [{city: 'Vancouver', street: 30},{city: 'Toronto', street: 40}]}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={school: { name: 'XYZ College',program: { major: 'Psyc', degree: 'BSc'}}}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={phone: [ 123456789, 987654321, 456987123 ]}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer ={school: { name: 'XYZ College',program: { major: 'Psyc', degree: 'BSc'} },phone: [ 123456789, 987654321, 456987123 ]}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH p=(x:Customer)-[:bought ={store: 'Amazon'}]->() RETURN p $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH p=(x:Customer)-[:bought ={store: 'Amazon', addr:{city: 'Vancouver', street: 30}}]->() RETURN p $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH p=(x:Customer)-[:bought {store: 'Amazon', addr:{city: 'Vancouver'}}]->() RETURN p $$) as (a agtype); + +SELECT * FROM cypher('test_enable_containment', $$ EXPLAIN (costs off) MATCH (x:Customer)-[:bought ={store: 'Amazon', addr:{city: 'Vancouver', street: 30}}]->(y:Product) RETURN 0 $$) as (a agtype); +SELECT * FROM cypher('test_enable_containment', $$ EXPLAIN (costs off) MATCH (x:Customer ={school: { name: 'XYZ College',program: { major: 'Psyc', degree: 'BSc'} },phone: [ 123456789, 987654321, 456987123 ]}) RETURN 0 $$) as (a agtype); + -- -- Clean up -- diff --git a/sql/agtype_gin.sql b/sql/agtype_gin.sql index a409d11b2..36c511920 100644 --- a/sql/agtype_gin.sql +++ b/sql/agtype_gin.sql @@ -70,6 +70,8 @@ DEFAULT FOR TYPE agtype USING gin AS OPERATOR 9 ?(agtype, agtype), OPERATOR 10 ?|(agtype, agtype), OPERATOR 11 ?&(agtype, agtype), + OPERATOR 12 @>>(agtype, agtype), + OPERATOR 13 <<@(agtype, agtype), FUNCTION 1 ag_catalog.gin_compare_agtype(text,text), FUNCTION 2 ag_catalog.gin_extract_agtype(agtype, internal), FUNCTION 3 ag_catalog.gin_extract_agtype_query(agtype, internal, int2, diff --git a/sql/agtype_operators.sql b/sql/agtype_operators.sql index aaff16d55..3fbc52f33 100644 --- a/sql/agtype_operators.sql +++ b/sql/agtype_operators.sql @@ -53,3 +53,37 @@ CREATE OPERATOR <@ ( RESTRICT = contsel, JOIN = contjoinsel ); + +CREATE FUNCTION ag_catalog.agtype_contains_top_level(agtype, agtype) + RETURNS boolean + LANGUAGE c + IMMUTABLE +RETURNS NULL ON NULL INPUT +PARALLEL SAFE +AS 'MODULE_PATHNAME'; + +CREATE OPERATOR @>> ( + LEFTARG = agtype, + RIGHTARG = agtype, + FUNCTION = ag_catalog.agtype_contains_top_level, + COMMUTATOR = '<<@', + RESTRICT = contsel, + JOIN = contjoinsel +); + +CREATE FUNCTION ag_catalog.agtype_contained_by_top_level(agtype, agtype) + RETURNS boolean + LANGUAGE c + IMMUTABLE +RETURNS NULL ON NULL INPUT +PARALLEL SAFE +AS 'MODULE_PATHNAME'; + +CREATE OPERATOR <<@ ( + LEFTARG = agtype, + RIGHTARG = agtype, + FUNCTION = ag_catalog.agtype_contained_by_top_level, + COMMUTATOR = '@>>', + RESTRICT = contsel, + JOIN = contjoinsel +); \ No newline at end of file diff --git a/src/backend/parser/cypher_clause.c b/src/backend/parser/cypher_clause.c index c164e194d..818328c26 100644 --- a/src/backend/parser/cypher_clause.c +++ b/src/backend/parser/cypher_clause.c @@ -163,6 +163,9 @@ static List *transform_map_to_ind_recursive(cypher_parsestate *cpstate, transform_entity *entity, cypher_map *map, List *parent_fields); +static List *transform_map_to_ind_top_level(cypher_parsestate *cpstate, + transform_entity *entity, + cypher_map *map); static Node *create_property_constraints(cypher_parsestate *cpstate, transform_entity *entity, Node *property_constraints, @@ -3580,11 +3583,9 @@ static A_Expr *filter_vertices_on_label_id(cypher_parsestate *cpstate, /* * Makes property constraint using indirection(s). This is an * alternative to using the containment operator (@>). - * - * In case of array and empty map, containment is used instead of equality. - * - * For example, the following query - * + * + * Consider the following query + * * MATCH (x:Label{ * name: 'xyz', * address: { @@ -3598,7 +3599,16 @@ static A_Expr *filter_vertices_on_label_id(cypher_parsestate *cpstate, * parents: {} * }) * - * is transformed to- + * There are two cases: + * + * 1- When use_equals flag is set, the above query is tranformed to- + * + * x.name = 'xyz' AND + * x.address = {"city": "abc", "street": {"name": "pqr", "number": 123}} AND + * x.phone = [9, 8, 7] AND + * x.parents = {} + * + * 2- When use_equals flag is not set, the above query is tranformed to- * * x.name = 'xyz' AND * x.address.city = 'abc' AND @@ -3606,13 +3616,25 @@ static A_Expr *filter_vertices_on_label_id(cypher_parsestate *cpstate, * x.address.street.number = 123 AND * x.phone @> [6, 4, 3] AND * x.parents @> {} + * + * NOTE: In case of array and empty map, containment is used instead of equality. */ static Node *transform_map_to_ind(cypher_parsestate *cpstate, transform_entity *entity, cypher_map *map) { List *quals; // list of equality and/or containment qual node - quals = transform_map_to_ind_recursive(cpstate, entity, map, NIL); + if (entity->entity.node->use_equals) + { + // Case 1 + quals = transform_map_to_ind_top_level(cpstate, entity, map); + } + else + { + // Case 2 + quals = transform_map_to_ind_recursive(cpstate, entity, map, NIL); + } + Assert(quals != NIL); if (list_length(quals) > 1) @@ -3726,6 +3748,69 @@ static List *transform_map_to_ind_recursive(cypher_parsestate *cpstate, return quals; } +/* + * Helper function of `transform_map_to_ind`. + * + * Transforms the map to a list of equality irrespective of + * value type. For example, + * + * x.name = 'xyz' + * x.map = {"city": "abc", "street": {"name": "pqr", "number": 123}} + * x.list = [9, 8, 7] + */ +static List *transform_map_to_ind_top_level(cypher_parsestate *cpstate, + transform_entity *entity, + cypher_map *map) +{ + int i; + ParseState *pstate; + Node *last_srf; + List *quals; + + pstate = (ParseState *)cpstate; + last_srf = pstate->p_last_srf; + quals = NIL; + + Assert(list_length(map->keyvals) != 0); + + for (i = 0; i < map->keyvals->length; i += 2) + { + Node *key; + Node *val; + Node *qual; + Node *lhs; + Node *rhs; + List *op; + A_Indirection *indir; + ColumnRef *variable; + char *keystr; + + key = (Node *)map->keyvals->elements[i].ptr_value; + val = (Node *)map->keyvals->elements[i + 1].ptr_value; + Assert(IsA(key, String)); + keystr = ((String *)key)->sval; + + op = list_make1(makeString("=")); + variable = makeNode(ColumnRef); + variable->fields = + list_make1(makeString(entity->entity.node->name)); + variable->location = -1; + + indir = makeNode(A_Indirection); + indir->arg = (Node *)variable; + indir->indirection = list_make1(makeString(keystr)); + + lhs = transform_cypher_expr(cpstate, (Node *)indir, + EXPR_KIND_WHERE); + rhs = transform_cypher_expr(cpstate, val, EXPR_KIND_WHERE); + + qual = (Node *)make_op(pstate, op, lhs, rhs, last_srf, -1); + quals = lappend(quals, qual); + } + + return quals; +} + /* * Creates the property constraints for a vertex/edge in a MATCH clause. */ @@ -3740,6 +3825,8 @@ static Node *create_property_constraints(cypher_parsestate *cpstate, Node *last_srf = pstate->p_last_srf; ParseNamespaceItem *pnsi; + Assert(entity->type != ENT_PATH); + /* * If the prop_expr node wasn't passed in, create it. Otherwise, skip * the creation step. @@ -3772,8 +3859,18 @@ static Node *create_property_constraints(cypher_parsestate *cpstate, if (age_enable_containment) { - return (Node *)make_op(pstate, list_make1(makeString("@>")), prop_expr, - const_expr, last_srf, -1); + 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); + } } else { diff --git a/src/backend/parser/cypher_gram.y b/src/backend/parser/cypher_gram.y index a579fe76f..761f0ce78 100644 --- a/src/backend/parser/cypher_gram.y +++ b/src/backend/parser/cypher_gram.y @@ -1268,9 +1268,25 @@ path_node: n->parsed_name = $2; n->label = $3; n->parsed_label = $3; + n->use_equals = false; n->props = $4; n->location = @2; + $$ = (Node *)n; + } + | '(' var_name_opt label_opt '='properties_opt ')' + { + cypher_node *n; + + n = make_ag_node(cypher_node); + n->name = $2; + n->parsed_name = $2; + n->label = $3; + n->parsed_label = $3; + n->use_equals = true; + n->props = $5; + n->location = @2; + $$ = (Node *)n; } ; @@ -1316,8 +1332,24 @@ path_relationship_body: n->label = $3; n->parsed_label = $3; n->varlen = $4; + n->use_equals = false; n->props = $5; + $$ = (Node *)n; + } + | '[' var_name_opt label_opt cypher_varlen_opt '='properties_opt ']' + { + cypher_relationship *n; + + n = make_ag_node(cypher_relationship); + n->name = $2; + n->parsed_name = $2; + n->label = $3; + n->parsed_label = $3; + n->varlen = $4; + n->use_equals = true; + n->props = $6; + $$ = (Node *)n; } | @@ -1331,6 +1363,7 @@ path_relationship_body: n->label = NULL; n->parsed_label = NULL; n->varlen = NULL; + n->use_equals = false; n->props = NULL; $$ = (Node *)n; diff --git a/src/backend/utils/adt/age_vle.c b/src/backend/utils/adt/age_vle.c index 7d106ac66..6e3464ad0 100644 --- a/src/backend/utils/adt/age_vle.c +++ b/src/backend/utils/adt/age_vle.c @@ -379,7 +379,7 @@ static bool is_an_edge_match(VLE_local_context *vlelctx, edge_entry *ee) property_it = agtype_iterator_init(agtc_edge_property); /* return the value of deep contains */ - return agtype_deep_contains(&property_it, &constraint_it); + return agtype_deep_contains(&property_it, &constraint_it, false); } /* diff --git a/src/backend/utils/adt/agtype_gin.c b/src/backend/utils/adt/agtype_gin.c index d260fd986..246dbcab6 100644 --- a/src/backend/utils/adt/agtype_gin.c +++ b/src/backend/utils/adt/agtype_gin.c @@ -195,7 +195,8 @@ Datum gin_extract_agtype_query(PG_FUNCTION_ARGS) strategy = PG_GETARG_UINT16(2); searchMode = (int32 *) PG_GETARG_POINTER(6); - if (strategy == AGTYPE_CONTAINS_STRATEGY_NUMBER) + if (strategy == AGTYPE_CONTAINS_STRATEGY_NUMBER || + strategy == AGTYPE_CONTAINS_TOP_LEVEL_STRATEGY_NUMBER) { /* Query is a agtype, so just apply gin_extract_agtype... */ entries = (Datum *) @@ -325,7 +326,8 @@ Datum gin_consistent_agtype(PG_FUNCTION_ARGS) nkeys = PG_GETARG_INT32(3); recheck = (bool *) PG_GETARG_POINTER(5); - if (strategy == AGTYPE_CONTAINS_STRATEGY_NUMBER) + if (strategy == AGTYPE_CONTAINS_STRATEGY_NUMBER || + strategy == AGTYPE_CONTAINS_TOP_LEVEL_STRATEGY_NUMBER) { /* * We must always recheck, since we can't tell from the index whether @@ -426,6 +428,7 @@ Datum gin_triconsistent_agtype(PG_FUNCTION_ARGS) * function, for the reasons listed there. */ if (strategy == AGTYPE_CONTAINS_STRATEGY_NUMBER || + strategy == AGTYPE_CONTAINS_TOP_LEVEL_STRATEGY_NUMBER || strategy == AGTYPE_EXISTS_ALL_STRATEGY_NUMBER) { /* All extracted keys must be present */ diff --git a/src/backend/utils/adt/agtype_ops.c b/src/backend/utils/adt/agtype_ops.c index 33a32deb3..35d4d2931 100644 --- a/src/backend/utils/adt/agtype_ops.c +++ b/src/backend/utils/adt/agtype_ops.c @@ -326,11 +326,11 @@ static agtype *delete_from_array(agtype *agt, agtype *indexes) it_neg_idx = agtype_iterator_init(&neg_idx_agt->root); it_indexes = agtype_iterator_init(&indexes->root); - contains_idx = agtype_deep_contains(&it_indexes, &it_cur_idx); + contains_idx = agtype_deep_contains(&it_indexes, &it_cur_idx, false); // re-initialize indexes array iterator it_indexes = agtype_iterator_init(&indexes->root); - contains_neg_idx = agtype_deep_contains(&it_indexes, &it_neg_idx); + contains_neg_idx = agtype_deep_contains(&it_indexes, &it_neg_idx, false); if (contains_idx || contains_neg_idx) { @@ -1444,7 +1444,49 @@ Datum agtype_contains(PG_FUNCTION_ARGS) property_it = agtype_iterator_init(&properties->root); constraint_it = agtype_iterator_init(&constraints->root); - PG_RETURN_BOOL(agtype_deep_contains(&property_it, &constraint_it)); + PG_RETURN_BOOL(agtype_deep_contains(&property_it, &constraint_it, false)); +} + +PG_FUNCTION_INFO_V1(agtype_contained_by_top_level); +/* + * Function for operator <<@ + * Works similar to <@, but unlike <@, this function does not recurse + * into object values, instead checks if the value of top-level key in + * right agtype is equal to the one on the left. + */ +Datum agtype_contained_by_top_level(PG_FUNCTION_ARGS) +{ + agtype_iterator *constraint_it, *property_it; + agtype *properties, *constraints; + + if (PG_ARGISNULL(0) || PG_ARGISNULL(1)) + { + PG_RETURN_BOOL(false); + } + + properties = AG_GET_ARG_AGTYPE_P(0); + constraints = AG_GET_ARG_AGTYPE_P(1); + + if (AGT_ROOT_IS_SCALAR(properties) + && AGTE_IS_AGTYPE(properties->root.children[0])) + { + properties = + agtype_value_to_agtype(extract_entity_properties(properties, + false)); + } + + if (AGT_ROOT_IS_SCALAR(constraints) + && AGTE_IS_AGTYPE(constraints->root.children[0])) + { + constraints = + agtype_value_to_agtype(extract_entity_properties(constraints, + false)); + } + + constraint_it = agtype_iterator_init(&constraints->root); + property_it = agtype_iterator_init(&properties->root); + + PG_RETURN_BOOL(agtype_deep_contains(&constraint_it, &property_it, true)); } @@ -1485,7 +1527,56 @@ Datum agtype_contained_by(PG_FUNCTION_ARGS) constraint_it = agtype_iterator_init(&constraints->root); property_it = agtype_iterator_init(&properties->root); - PG_RETURN_BOOL(agtype_deep_contains(&constraint_it, &property_it)); + PG_RETURN_BOOL(agtype_deep_contains(&constraint_it, &property_it, false)); +} + +PG_FUNCTION_INFO_V1(agtype_contains_top_level); +/* + * Function for operator @>>. + * Works similar to @>, but unlike @>, this function does not recurse + * into object values, instead checks if the value of top-level key in + * left agtype is equal to the one on the right. + */ +Datum agtype_contains_top_level(PG_FUNCTION_ARGS) +{ + agtype_iterator *constraint_it = NULL; + agtype_iterator *property_it = NULL; + agtype *properties = NULL; + agtype *constraints = NULL; + + if (PG_ARGISNULL(0) || PG_ARGISNULL(1)) + { + PG_RETURN_BOOL(false); + } + + properties = AG_GET_ARG_AGTYPE_P(0); + constraints = AG_GET_ARG_AGTYPE_P(1); + + if (AGT_ROOT_IS_SCALAR(properties) + && AGTE_IS_AGTYPE(properties->root.children[0])) + { + properties = + agtype_value_to_agtype(extract_entity_properties(properties, + false)); + } + + if (AGT_ROOT_IS_SCALAR(constraints) + && AGTE_IS_AGTYPE(constraints->root.children[0])) + { + constraints = + agtype_value_to_agtype(extract_entity_properties(constraints, + false)); + } + + if (AGT_ROOT_IS_OBJECT(properties) != AGT_ROOT_IS_OBJECT(constraints)) + { + PG_RETURN_BOOL(false); + } + + property_it = agtype_iterator_init(&properties->root); + constraint_it = agtype_iterator_init(&constraints->root); + + PG_RETURN_BOOL(agtype_deep_contains(&property_it, &constraint_it, true)); } PG_FUNCTION_INFO_V1(agtype_exists); diff --git a/src/backend/utils/adt/agtype_util.c b/src/backend/utils/adt/agtype_util.c index 8dbdca06d..eef916fdc 100644 --- a/src/backend/utils/adt/agtype_util.c +++ b/src/backend/utils/adt/agtype_util.c @@ -1121,7 +1121,9 @@ static agtype_iterator *free_and_get_parent(agtype_iterator *it) * "val" is lhs agtype, and m_contained is rhs agtype when called from top * level. We determine if m_contained is contained within val. */ -bool agtype_deep_contains(agtype_iterator **val, agtype_iterator **m_contained) +bool agtype_deep_contains(agtype_iterator **val, + agtype_iterator **m_contained, + bool skip_nested) { agtype_value vval; agtype_value vcontained; @@ -1211,6 +1213,19 @@ bool agtype_deep_contains(agtype_iterator **val, agtype_iterator **m_contained) if (!equals_agtype_scalar_value(lhs_val, &vcontained)) return false; } + else if (skip_nested) + { + Assert(lhs_val->type == AGTV_BINARY); + Assert(vcontained.type == AGTV_BINARY); + + // We will just check if the rhs value is equal to lhs + if (compare_agtype_containers_orderability( + lhs_val->val.binary.data, + vcontained.val.binary.data) != 0) + { + return false; + } + } else { /* Nested container value (object or array) */ @@ -1244,8 +1259,10 @@ bool agtype_deep_contains(agtype_iterator **val, agtype_iterator **m_contained) * of containment (plus of course the mapped nodes must be * equal). */ - if (!agtype_deep_contains(&nestval, &nest_contained)) + if (!agtype_deep_contains(&nestval, &nest_contained, false)) + { return false; + } } } } @@ -1337,7 +1354,7 @@ bool agtype_deep_contains(agtype_iterator **val, agtype_iterator **m_contained) nest_contained = agtype_iterator_init(vcontained.val.binary.data); - contains = agtype_deep_contains(&nestval, &nest_contained); + contains = agtype_deep_contains(&nestval, &nest_contained, false); if (nestval) pfree(nestval); diff --git a/src/include/nodes/cypher_nodes.h b/src/include/nodes/cypher_nodes.h index a3022fea1..fc882da66 100644 --- a/src/include/nodes/cypher_nodes.h +++ b/src/include/nodes/cypher_nodes.h @@ -139,6 +139,7 @@ typedef struct cypher_node char *parsed_name; char *label; char *parsed_label; + bool use_equals; Node *props; // map or parameter int location; } cypher_node; @@ -158,6 +159,7 @@ typedef struct cypher_relationship char *parsed_name; char *label; char *parsed_label; + bool use_equals; Node *props; // map or parameter Node *varlen; // variable length relationships (A_Indices) cypher_rel_dir dir; diff --git a/src/include/utils/agtype.h b/src/include/utils/agtype.h index c2b093954..c5a2fe95b 100644 --- a/src/include/utils/agtype.h +++ b/src/include/utils/agtype.h @@ -57,6 +57,7 @@ typedef enum #define AGTYPE_EXISTS_STRATEGY_NUMBER 9 #define AGTYPE_EXISTS_ANY_STRATEGY_NUMBER 10 #define AGTYPE_EXISTS_ALL_STRATEGY_NUMBER 11 +#define AGTYPE_CONTAINS_TOP_LEVEL_STRATEGY_NUMBER 12 /* * In the standard agtype_ops GIN opclass for agtype, we choose to index both @@ -474,7 +475,7 @@ agtype_iterator_token agtype_iterator_next(agtype_iterator **it, bool skip_nested); agtype *agtype_value_to_agtype(agtype_value *val); bool agtype_deep_contains(agtype_iterator **val, - agtype_iterator **m_contained); + agtype_iterator **m_contained, bool skip_nested); void agtype_hash_scalar_value(const agtype_value *scalar_val, uint32 *hash); void agtype_hash_scalar_value_extended(const agtype_value *scalar_val, uint64 *hash, uint64 seed);