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