Skip to content

Commit

Permalink
Add optional parameter '=' in property constraints (apache#1516)
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
MuhammadTahaNaveed authored Feb 6, 2024
1 parent c3f8caf commit 1ac037a
Show file tree
Hide file tree
Showing 13 changed files with 660 additions and 21 deletions.
60 changes: 60 additions & 0 deletions age--1.5.0--y.y.y.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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;
246 changes: 245 additions & 1 deletion regress/expected/cypher_match.out
Original file line number Diff line number Diff line change
Expand Up @@ -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
--
Expand Down Expand 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
------------
Expand Down
Loading

0 comments on commit 1ac037a

Please sign in to comment.