Add optional parameter '=' in property constraints (#1516)
- 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.
- Added changes in sql files to version update template file.
diff --git a/age--1.5.0--y.y.y.sql b/age--1.5.0--y.y.y.sql
index 9ec64bf..23287a8 100644
--- a/age--1.5.0--y.y.y.sql
+++ b/age--1.5.0--y.y.y.sql
@@ -28,3 +28,63 @@
-- Please add all additions, deletions, and modifications to the end of this
-- file. We need to keep the order of these changes.
+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
+);
+
+/*
+ * Since there is no option to add or drop operator from class,
+ * we have to drop and recreate the whole operator class.
+ * Reference: https://www.postgresql.org/docs/current/sql-alteropclass.html
+ */
+
+DROP OPERATOR CLASS ag_catalog.gin_agtype_ops;
+
+CREATE OPERATOR CLASS ag_catalog.gin_agtype_ops
+DEFAULT FOR TYPE agtype USING gin AS
+ OPERATOR 7 @>(agtype, agtype),
+ OPERATOR 8 <@(agtype, agtype),
+ 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,
+ internal, internal),
+ FUNCTION 4 ag_catalog.gin_consistent_agtype(internal, int2, agtype, int4,
+ internal, internal),
+ FUNCTION 6 ag_catalog.gin_triconsistent_agtype(internal, int2, agtype, int4,
+ internal, internal, internal),
+STORAGE text;
diff --git a/regress/expected/cypher_match.out b/regress/expected/cypher_match.out
index f00c47c..461e924 100644
--- a/regress/expected/cypher_match.out
+++ b/regress/expected/cypher_match.out
@@ -3294,6 +3294,248 @@
(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
--
SELECT drop_graph('cypher_match', true);
@@ -3335,10 +3577,12 @@
(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 318764c..d1531be 100644
--- a/regress/sql/cypher_match.sql
+++ b/regress/sql/cypher_match.sql
@@ -1383,6 +1383,61 @@
$$) 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
--
SELECT drop_graph('cypher_match', true);
diff --git a/sql/agtype_gin.sql b/sql/agtype_gin.sql
index a409d11..36c5119 100644
--- a/sql/agtype_gin.sql
+++ b/sql/agtype_gin.sql
@@ -70,6 +70,8 @@
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 aaff16d..3fbc52f 100644
--- a/sql/agtype_operators.sql
+++ b/sql/agtype_operators.sql
@@ -53,3 +53,37 @@
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 c164e19..818328c 100644
--- a/src/backend/parser/cypher_clause.c
+++ b/src/backend/parser/cypher_clause.c
@@ -163,6 +163,9 @@
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 @@
/*
* 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 @@
* 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 @@
* 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)
@@ -3727,6 +3749,69 @@
}
/*
+ * 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.
*/
static Node *create_property_constraints(cypher_parsestate *cpstate,
@@ -3740,6 +3825,8 @@
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 @@
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 a579fe7..761f0ce 100644
--- a/src/backend/parser/cypher_gram.y
+++ b/src/backend/parser/cypher_gram.y
@@ -1268,11 +1268,27 @@
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;
+ }
;
path_relationship:
@@ -1316,10 +1332,26 @@
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;
+ }
|
/* empty */
{
@@ -1331,6 +1363,7 @@
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 7d106ac..6e3464a 100644
--- a/src/backend/utils/adt/age_vle.c
+++ b/src/backend/utils/adt/age_vle.c
@@ -379,7 +379,7 @@
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 d260fd9..246dbca 100644
--- a/src/backend/utils/adt/agtype_gin.c
+++ b/src/backend/utils/adt/agtype_gin.c
@@ -195,7 +195,8 @@
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 @@
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 @@
* 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 33a32de..35d4d29 100644
--- a/src/backend/utils/adt/agtype_ops.c
+++ b/src/backend/utils/adt/agtype_ops.c
@@ -326,11 +326,11 @@
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 @@
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 @@
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 8dbdca0..eef916f 100644
--- a/src/backend/utils/adt/agtype_util.c
+++ b/src/backend/utils/adt/agtype_util.c
@@ -1121,7 +1121,9 @@
* "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 @@
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 @@
* 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 @@
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 a3022fe..fc882da 100644
--- a/src/include/nodes/cypher_nodes.h
+++ b/src/include/nodes/cypher_nodes.h
@@ -139,6 +139,7 @@
char *parsed_name;
char *label;
char *parsed_label;
+ bool use_equals;
Node *props; // map or parameter
int location;
} cypher_node;
@@ -158,6 +159,7 @@
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 c2b0939..c5a2fe9 100644
--- a/src/include/utils/agtype.h
+++ b/src/include/utils/agtype.h
@@ -57,6 +57,7 @@
#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 @@
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);