Add support for operators in cypher query (#2172)
- Fixed some operator signatures in .sql
- Added support for PG operators in cypher. Some
hardcoded operators are removed, since they are
now covered by the general operator handling.
- Added full typecast syntax that allows for type
modifiers.
- These changes also improve interoperability with
other extensions, as reflected in the regression
tests.
- Added a new function to check if graph_oid exists.
diff --git a/age--1.5.0--y.y.y.sql b/age--1.5.0--y.y.y.sql
index 6b7560a..d04a6a2 100644
--- a/age--1.5.0--y.y.y.sql
+++ b/age--1.5.0--y.y.y.sql
@@ -41,8 +41,8 @@
RIGHTARG = agtype,
FUNCTION = ag_catalog.agtype_contains_top_level,
COMMUTATOR = '<<@',
- RESTRICT = contsel,
- JOIN = contjoinsel
+ RESTRICT = matchingsel,
+ JOIN = matchingjoinsel
);
CREATE FUNCTION ag_catalog.agtype_contained_by_top_level(agtype, agtype)
@@ -58,17 +58,114 @@
RIGHTARG = agtype,
FUNCTION = ag_catalog.agtype_contained_by_top_level,
COMMUTATOR = '@>>',
- RESTRICT = contsel,
- JOIN = contjoinsel
+ RESTRICT = matchingsel,
+ JOIN = matchingjoinsel
);
/*
+ * We have to drop and recreate the operators, because
+ * commutator is not modifiable using ALTER OPERATOR.
+ */
+ALTER EXTENSION age
+ DROP OPERATOR ? (agtype, agtype);
+ALTER EXTENSION age
+ DROP OPERATOR ? (agtype, text);
+ALTER EXTENSION age
+ DROP OPERATOR ?| (agtype, agtype);
+ALTER EXTENSION age
+ DROP OPERATOR ?| (agtype, text[]);
+ALTER EXTENSION age
+ DROP OPERATOR ?& (agtype, agtype[]);
+ALTER EXTENSION age
+ DROP OPERATOR ?& (agtype, text);
+
+DROP OPERATOR ? (agtype, agtype), ? (agtype, text),
+ ?| (agtype, agtype), ?| (agtype, text[]),
+ ?& (agtype, agtype[]), ?& (agtype, text);
+
+CREATE OPERATOR ? (
+ LEFTARG = agtype,
+ RIGHTARG = agtype,
+ FUNCTION = ag_catalog.agtype_exists_agtype,
+ RESTRICT = matchingsel,
+ JOIN = matchingjoinsel
+);
+
+CREATE OPERATOR ? (
+ LEFTARG = agtype,
+ RIGHTARG = text,
+ FUNCTION = ag_catalog.agtype_exists,
+ RESTRICT = matchingsel,
+ JOIN = matchingjoinsel
+);
+
+CREATE OPERATOR ?| (
+ LEFTARG = agtype,
+ RIGHTARG = agtype,
+ FUNCTION = ag_catalog.agtype_exists_any_agtype,
+ RESTRICT = matchingsel,
+ JOIN = matchingjoinsel
+);
+
+CREATE OPERATOR ?| (
+ LEFTARG = agtype,
+ RIGHTARG = text[],
+ FUNCTION = ag_catalog.agtype_exists_any,
+ RESTRICT = matchingsel,
+ JOIN = matchingjoinsel
+);
+
+CREATE OPERATOR ?& (
+ LEFTARG = agtype,
+ RIGHTARG = agtype,
+ FUNCTION = ag_catalog.agtype_exists_all_agtype,
+ RESTRICT = matchingsel,
+ JOIN = matchingjoinsel
+);
+
+CREATE OPERATOR ?& (
+ LEFTARG = agtype,
+ RIGHTARG = text[],
+ FUNCTION = ag_catalog.agtype_exists_all,
+ RESTRICT = matchingsel,
+ JOIN = matchingjoinsel
+);
+
+ALTER EXTENSION age
+ ADD OPERATOR ? (agtype, agtype);
+ALTER EXTENSION age
+ ADD OPERATOR ? (agtype, text);
+ALTER EXTENSION age
+ ADD OPERATOR ?| (agtype, agtype);
+ALTER EXTENSION age
+ ADD OPERATOR ?| (agtype, text[]);
+ALTER EXTENSION age
+ ADD OPERATOR ?& (agtype, agtype[]);
+ALTER EXTENSION age
+ ADD OPERATOR ?& (agtype, text);
+
+ALTER OPERATOR @> (agtype, agtype)
+ SET (RESTRICT = matchingsel, JOIN = matchingjoinsel);
+
+ALTER OPERATOR @> (agtype, agtype)
+ SET (RESTRICT = matchingsel, JOIN = matchingjoinsel);
+
+ALTER OPERATOR <@ (agtype, agtype)
+ SET (RESTRICT = matchingsel, JOIN = matchingjoinsel);
+
+ALTER OPERATOR <@ (agtype, agtype)
+ SET (RESTRICT = matchingsel, JOIN = matchingjoinsel);
+
+/*
* 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;
+ALTER EXTENSION age
+ DROP OPERATOR CLASS ag_catalog.gin_agtype_ops USING gin;
+
+DROP OPERATOR CLASS ag_catalog.gin_agtype_ops USING gin;
CREATE OPERATOR CLASS ag_catalog.gin_agtype_ops
DEFAULT FOR TYPE agtype USING gin AS
@@ -89,6 +186,9 @@
internal, internal, internal),
STORAGE text;
+ALTER EXTENSION age
+ ADD OPERATOR CLASS ag_catalog.gin_agtype_ops USING gin;
+
-- this function went from variadic "any" to just "any" type
CREATE OR REPLACE FUNCTION ag_catalog.age_tostring("any")
RETURNS agtype
@@ -148,4 +248,10 @@
AS 'MODULE_PATHNAME';
CREATE CAST (agtype[] AS agtype)
- WITH FUNCTION ag_catalog.agtype_array_to_agtype(agtype[]);
\ No newline at end of file
+ WITH FUNCTION ag_catalog.agtype_array_to_agtype(agtype[]);
+
+CREATE OPERATOR =~ (
+ LEFTARG = agtype,
+ RIGHTARG = agtype,
+ FUNCTION = ag_catalog.age_eq_tilde
+);
diff --git a/regress/expected/cypher_match.out b/regress/expected/cypher_match.out
index e255847..e83ba3b 100644
--- a/regress/expected/cypher_match.out
+++ b/regress/expected/cypher_match.out
@@ -2407,22 +2407,22 @@
SELECT * FROM cypher('cypher_match', $$ MATCH p=(a)-[u {relationship: u.relationship}]->(b) RETURN p $$) as (a agtype);
a
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- [{"id": 281474976710661, "label": "", "properties": {"age": 4, "name": "T"}}::vertex, {"id": 4785074604081153, "label": "knows", "end_id": 281474976710666, "start_id": 281474976710661, "properties": {"years": 3, "relationship": "friends"}}::edge, {"id": 281474976710666, "label": "", "properties": {"age": 6}}::vertex]::path
[{"id": 281474976710659, "label": "", "properties": {"age": 3, "name": "orphan"}}::vertex, {"id": 4785074604081154, "label": "knows", "end_id": 281474976710666, "start_id": 281474976710659, "properties": {"years": 4, "relationship": "enemies"}}::edge, {"id": 281474976710666, "label": "", "properties": {"age": 6}}::vertex]::path
+ [{"id": 281474976710661, "label": "", "properties": {"age": 4, "name": "T"}}::vertex, {"id": 4785074604081153, "label": "knows", "end_id": 281474976710666, "start_id": 281474976710661, "properties": {"years": 3, "relationship": "friends"}}::edge, {"id": 281474976710666, "label": "", "properties": {"age": 6}}::vertex]::path
(2 rows)
SELECT * FROM cypher('cypher_match', $$ MATCH p=(a)-[u {relationship: u.relationship, years: u.years}]->(b) RETURN p $$) as (a agtype);
a
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- [{"id": 281474976710661, "label": "", "properties": {"age": 4, "name": "T"}}::vertex, {"id": 4785074604081153, "label": "knows", "end_id": 281474976710666, "start_id": 281474976710661, "properties": {"years": 3, "relationship": "friends"}}::edge, {"id": 281474976710666, "label": "", "properties": {"age": 6}}::vertex]::path
[{"id": 281474976710659, "label": "", "properties": {"age": 3, "name": "orphan"}}::vertex, {"id": 4785074604081154, "label": "knows", "end_id": 281474976710666, "start_id": 281474976710659, "properties": {"years": 4, "relationship": "enemies"}}::edge, {"id": 281474976710666, "label": "", "properties": {"age": 6}}::vertex]::path
+ [{"id": 281474976710661, "label": "", "properties": {"age": 4, "name": "T"}}::vertex, {"id": 4785074604081153, "label": "knows", "end_id": 281474976710666, "start_id": 281474976710661, "properties": {"years": 3, "relationship": "friends"}}::edge, {"id": 281474976710666, "label": "", "properties": {"age": 6}}::vertex]::path
(2 rows)
SELECT * FROM cypher('cypher_match', $$ MATCH p=(a {name:a.name})-[u {relationship: u.relationship}]->(b {age:b.age}) RETURN p $$) as (a agtype);
a
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- [{"id": 281474976710661, "label": "", "properties": {"age": 4, "name": "T"}}::vertex, {"id": 4785074604081153, "label": "knows", "end_id": 281474976710666, "start_id": 281474976710661, "properties": {"years": 3, "relationship": "friends"}}::edge, {"id": 281474976710666, "label": "", "properties": {"age": 6}}::vertex]::path
[{"id": 281474976710659, "label": "", "properties": {"age": 3, "name": "orphan"}}::vertex, {"id": 4785074604081154, "label": "knows", "end_id": 281474976710666, "start_id": 281474976710659, "properties": {"years": 4, "relationship": "enemies"}}::edge, {"id": 281474976710666, "label": "", "properties": {"age": 6}}::vertex]::path
+ [{"id": 281474976710661, "label": "", "properties": {"age": 4, "name": "T"}}::vertex, {"id": 4785074604081153, "label": "knows", "end_id": 281474976710666, "start_id": 281474976710661, "properties": {"years": 3, "relationship": "friends"}}::edge, {"id": 281474976710666, "label": "", "properties": {"age": 6}}::vertex]::path
(2 rows)
SELECT * FROM cypher('cypher_match', $$ CREATE () WITH * MATCH (x{n0:x.n1}) RETURN 0 $$) as (a agtype);
diff --git a/regress/expected/cypher_vle.out b/regress/expected/cypher_vle.out
index b3cada6..9cbb342 100644
--- a/regress/expected/cypher_vle.out
+++ b/regress/expected/cypher_vle.out
@@ -726,8 +726,8 @@
SELECT * FROM show_list_use_vle('list01');
node
-----------------------------------------------------------------------------------
- {"id": 1407374883553281, "label": "node", "properties": {"content": "a"}}::vertex
{"id": 1407374883553282, "label": "node", "properties": {"content": "b"}}::vertex
+ {"id": 1407374883553281, "label": "node", "properties": {"content": "a"}}::vertex
(2 rows)
-- prepend a node 'c'
@@ -741,9 +741,9 @@
SELECT * FROM show_list_use_vle('list01');
node
-----------------------------------------------------------------------------------
- {"id": 1407374883553281, "label": "node", "properties": {"content": "a"}}::vertex
- {"id": 1407374883553282, "label": "node", "properties": {"content": "b"}}::vertex
{"id": 1407374883553283, "label": "node", "properties": {"content": "c"}}::vertex
+ {"id": 1407374883553282, "label": "node", "properties": {"content": "b"}}::vertex
+ {"id": 1407374883553281, "label": "node", "properties": {"content": "a"}}::vertex
(3 rows)
DROP FUNCTION show_list_use_vle;
diff --git a/regress/expected/pgvector.out b/regress/expected/pgvector.out
index f1bd53e..bbc5583 100644
--- a/regress/expected/pgvector.out
+++ b/regress/expected/pgvector.out
@@ -61,6 +61,58 @@
{1:1.22,2:2.22,3:3.33}/3
(1 row)
+SELECT * FROM cypher('graph', $$ RETURN [1.22,2.22,3.33]::vector $$) AS (n vector);
+ n
+------------------
+ [1.22,2.22,3.33]
+(1 row)
+
+SELECT * FROM cypher('graph', $$ RETURN [1.22,2.22,3.33]::vector $$) AS (n halfvec);
+ n
+---------------------------------
+ [1.2197266,2.2207031,3.3300781]
+(1 row)
+
+SELECT * FROM cypher('graph', $$ RETURN [1.22,2.22,3.33]::vector $$) AS (n sparsevec);
+ n
+--------------------------
+ {1:1.22,2:2.22,3:3.33}/3
+(1 row)
+
+SELECT * FROM cypher('graph', $$ RETURN [1.22,2.22,3.33]::vector $$) AS (n vector(3));
+ n
+------------------
+ [1.22,2.22,3.33]
+(1 row)
+
+SELECT * FROM cypher('graph', $$ RETURN [1.22,2.22,3.33]::vector $$) AS (n halfvec(3));
+ n
+---------------------------------
+ [1.2197266,2.2207031,3.3300781]
+(1 row)
+
+SELECT * FROM cypher('graph', $$ RETURN [1.22,2.22,3.33]::vector $$) AS (n sparsevec(3));
+ n
+--------------------------
+ {1:1.22,2:2.22,3:3.33}/3
+(1 row)
+
+-- Should error out
+SELECT * FROM cypher('graph', $$ RETURN [1.22,2.22,3.33]::vector $$) AS (n vector(2));
+ERROR: expected 2 dimensions, not 3
+SELECT * FROM cypher('graph', $$ RETURN [1.22,2.22,3.33]::vector $$) AS (n halfvec(2));
+ERROR: expected 2 dimensions, not 3
+SELECT * FROM cypher('graph', $$ RETURN [1.22,2.22,3.33]::vector $$) AS (n sparsevec(2));
+ERROR: expected 2 dimensions, not 3
+SELECT * FROM cypher('graph', $$ RETURN [1.22,2.22,3.33]::vector(3) $$) AS (n vector(4));
+ERROR: expected 4 dimensions, not 3
+SELECT * FROM cypher('graph', $$ RETURN [1.22,2.22,3.33]::vector(3) $$) AS (n halfvec(4));
+ERROR: expected 4 dimensions, not 3
+SELECT * FROM cypher('graph', $$ RETURN [1.22,2.22,3.33]::vector(3) $$) AS (n sparsevec(4));
+ERROR: expected 4 dimensions, not 3
+--
+-- Test functions
+--
SELECT * FROM cypher('graph', $$ RETURN l2_distance("[1,2,3]", "[1,2,4]") $$) AS (n agtype);
n
-----
@@ -121,32 +173,186 @@
[2, 3, 4, 5]
(1 row)
-SELECT * FROM cypher('graph', $$ RETURN binary_quantize("[1,2,4]") $$) AS (n bit);
+SELECT * FROM cypher('graph', $$ RETURN binary_quantize("[1,2,4]") $$) AS (n bit(3));
n
-----
111
(1 row)
+--
+-- Test operators
+--
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector + [1,2,4]::vector $$) AS (n vector);
+ n
+---------
+ [2,4,7]
+(1 row)
+
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector - [1,2,4]::vector $$) AS (n vector);
+ n
+----------
+ [0,0,-1]
+(1 row)
+
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector * [1,2,4]::vector $$) AS (n vector);
+ n
+----------
+ [1,4,12]
+(1 row)
+
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector || [1,2,4]::vector $$) AS (n vector);
+ n
+---------------
+ [1,2,3,1,2,4]
+(1 row)
+
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector <#> [1,2,4]::vector $$) AS (n agtype);
+ n
+-------
+ -17.0
+(1 row)
+
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector <=> [1,2,4]::vector $$) AS (n agtype);
+ n
+---------------------
+ 0.00853986601633272
+(1 row)
+
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector <+> [1,2,4]::vector $$) AS (n agtype);
+ n
+-----
+ 1.0
+(1 row)
+
+--
+-- Due to issues with pattern matching syntax, '-' is not allowed
+-- as an operator character, so we have to use the OPERATOR syntax.
+--
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (`<->`) [1,2,4]::vector $$) AS (n agtype);
+ n
+-----
+ 1.0
+(1 row)
+
+-- Using OPERATOR () syntax
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (+) [1,2,4]::vector $$) AS (n vector);
+ n
+---------
+ [2,4,7]
+(1 row)
+
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (-) [1,2,4]::vector $$) AS (n vector);
+ n
+----------
+ [0,0,-1]
+(1 row)
+
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (*) [1,2,4]::vector $$) AS (n vector);
+ n
+----------
+ [1,4,12]
+(1 row)
+
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (||) [1,2,4]::vector $$) AS (n vector);
+ n
+---------------
+ [1,2,3,1,2,4]
+(1 row)
+
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (`<->`) [1,2,4]::vector $$) AS (n agtype);
+ n
+-----
+ 1.0
+(1 row)
+
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (<#>) [1,2,4]::vector $$) AS (n agtype);
+ n
+-------
+ -17.0
+(1 row)
+
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (<=>) [1,2,4]::vector $$) AS (n agtype);
+ n
+---------------------
+ 0.00853986601633272
+(1 row)
+
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (<+>) [1,2,4]::vector $$) AS (n agtype);
+ n
+-----
+ 1.0
+(1 row)
+
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (public.+) [1,2,4]::vector $$) AS (n vector);
+ n
+---------
+ [2,4,7]
+(1 row)
+
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (public.-) [1,2,4]::vector $$) AS (n vector);
+ n
+----------
+ [0,0,-1]
+(1 row)
+
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (public.*) [1,2,4]::vector $$) AS (n vector);
+ n
+----------
+ [1,4,12]
+(1 row)
+
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (public.||) [1,2,4]::vector $$) AS (n vector);
+ n
+---------------
+ [1,2,3,1,2,4]
+(1 row)
+
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (public.`<->`) [1,2,4]::vector $$) AS (n agtype);
+ n
+-----
+ 1.0
+(1 row)
+
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (public.<#>) [1,2,4]::vector $$) AS (n agtype);
+ n
+-------
+ -17.0
+(1 row)
+
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (public.<=>) [1,2,4]::vector $$) AS (n agtype);
+ n
+---------------------
+ 0.00853986601633272
+(1 row)
+
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (public.<+>) [1,2,4]::vector $$) AS (n agtype);
+ n
+-----
+ 1.0
+(1 row)
+
+--
-- An example usage
+--
SELECT * FROM cypher('graph', $$
- CREATE (:Movie {title: "The Matrix", year: 1999, genre: "Action", plot: "A computer hacker learns about the true nature of reality and joins a rebellion to free humanity from a simulated world controlled by machines.", embedding: "[-0.07594558, 0.04081754, 0.29592122, -0.11921061]"}),
- (:Movie {title: "The Matrix Reloaded", year: 2003, genre: "Action", plot: "The rebels continue their fight against the machines, uncovering deeper truths about the Matrix and the nature of their mission.", embedding: "[0.30228977, -0.22839354, 0.35070436, 0.01262819]"}),
- (:Movie {title: "The Matrix Revolutions", year: 2003, genre: "Action", plot: "The final battle between humans and machines reaches its climax as the fate of both worlds hangs in the balance.", embedding: "[ 0.12240622, -0.29752459, 0.22620453, 0.24454723]"}),
- (:Movie {title: "The Matrix Resurrections", year: 2021, genre: "Action", plot: "Neo returns to a new version of the Matrix and must once again fight to save the people from the control of the machines.", embedding: "[ 0.34717246, -0.13820869, 0.29214213, 0.08090488]"}),
- (:Movie {title: "Inception", year: 2010, genre: "Sci-Fi", plot: "A skilled thief is given a chance at redemption if he can successfully perform an inception: planting an idea into someone’s subconscious.", embedding: "[ 0.03923657, 0.39284106, -0.20927092, -0.17770818]"}),
- (:Movie {title: "Interstellar", year: 2014, genre: "Sci-Fi", plot: "A group of explorers travel through a wormhole in space in an attempt to ensure humanity’s survival.", embedding: "[-0.29302418, -0.39615033, -0.23393948, -0.09601383]"}),
- (:Movie {title: "Avatar", year: 2009, genre: "Sci-Fi", plot: "A paraplegic Marine is sent to the moon Pandora, where he becomes torn between following orders and protecting the world he feels is his home.", embedding: "[-0.13663386, 0.00635589, -0.03038832, -0.08252723]"}),
- (:Movie {title: "Blade Runner", year: 1982, genre: "Sci-Fi", plot: "A blade runner must pursue and terminate four replicants who have stolen a ship in space and returned to Earth.", embedding: "[ 0.27215557, -0.1479577, -0.09972772, -0.08234394]"}),
- (:Movie {title: "Blade Runner 2049", year: 2017, genre: "Sci-Fi", plot: "A new blade runner unearths a long-buried secret that has the potential to plunge what’s left of society into chaos.", embedding: "[ 0.21560573, -0.07505179, -0.01331814, 0.13403069]"}),
- (:Movie {title: "Minority Report", year: 2002, genre: "Sci-Fi", plot: "In a future where a special police unit can arrest murderers before they commit their crimes, a top officer is accused of a future murder.", embedding: "[ 0.24008012, 0.44954908, -0.30905488, 0.15195407]"}),
- (:Movie {title: "Total Recall", year: 1990, genre: "Sci-Fi", plot: "A construction worker discovers that his memories have been implanted and becomes embroiled in a conspiracy on Mars.", embedding: "[-0.17471036, 0.14695261, -0.06272433, -0.21795064]"}),
- (:Movie {title: "Elysium", year: 2013, genre: "Sci-Fi", plot: "In a future where the rich live on a luxurious space station while the rest of humanity lives in squalor, a man fights to bring equality.", embedding: "[-0.33280967, 0.07733926, 0.11015328, 0.53382836]"}),
- (:Movie {title: "Gattaca", year: 1997, genre: "Sci-Fi", plot: "In a future where genetic engineering determines social class, a man defies his fate to achieve his dreams.", embedding: "[-0.21629286, 0.31114665, 0.08303899, 0.46199759]"}),
- (:Movie {title: "The Fifth Element", year: 1997, genre: "Sci-Fi", plot: "In a futuristic world, a cab driver becomes the key to saving humanity from an impending cosmic threat.", embedding: "[-0.11528205, -0.0208782, -0.0735215, 0.14327449]"}),
- (:Movie {title: "The Terminator", year: 1984, genre: "Action", plot: "A cyborg assassin is sent back in time to kill the mother of the future resistance leader.", embedding: "[ 0.33666933, 0.18040994, -0.01075103, -0.11117851]"}),
- (:Movie {title: "Terminator 2: Judgment Day", year: 1991, genre: "Action", plot: "A reprogrammed Terminator is sent to protect the future leader of the human resistance from a more advanced Terminator.", embedding: "[ 0.34698868, 0.06439331, 0.06232323, -0.19534876]"}),
- (:Movie {title: "Jurassic Park", year: 1993, genre: "Adventure", plot: "Scientists clone dinosaurs to create a theme park, but things go awry when the creatures escape.", embedding: "[ 0.01794725, -0.11434246, -0.46831815, -0.01049593]"}),
- (:Movie {title: "The Avengers", year: 2012, genre: "Action", plot: "Superheroes assemble to face a global threat from an alien invasion led by Loki.", embedding: "[ 0.00546514, -0.37005171, -0.42612838, 0.07968612]"})
+ CREATE (:Movie {title: "The Matrix", year: 1999, genre: "Action", plot: "A computer hacker learns about the true nature of reality and joins a rebellion to free humanity from a simulated world controlled by machines.", embedding: [-0.07594558, 0.04081754, 0.29592122, -0.11921061]}),
+ (:Movie {title: "The Matrix Reloaded", year: 2003, genre: "Action", plot: "The rebels continue their fight against the machines, uncovering deeper truths about the Matrix and the nature of their mission.", embedding: [0.30228977, -0.22839354, 0.35070436, 0.01262819]}),
+ (:Movie {title: "The Matrix Revolutions", year: 2003, genre: "Action", plot: "The final battle between humans and machines reaches its climax as the fate of both worlds hangs in the balance.", embedding: [ 0.12240622, -0.29752459, 0.22620453, 0.24454723]}),
+ (:Movie {title: "The Matrix Resurrections", year: 2021, genre: "Action", plot: "Neo returns to a new version of the Matrix and must once again fight to save the people from the control of the machines.", embedding: [ 0.34717246, -0.13820869, 0.29214213, 0.08090488]}),
+ (:Movie {title: "Inception", year: 2010, genre: "Sci-Fi", plot: "A skilled thief is given a chance at redemption if he can successfully perform an inception: planting an idea into someone’s subconscious.", embedding: [ 0.03923657, 0.39284106, -0.20927092, -0.17770818]}),
+ (:Movie {title: "Interstellar", year: 2014, genre: "Sci-Fi", plot: "A group of explorers travel through a wormhole in space in an attempt to ensure humanity’s survival.", embedding: [-0.29302418, -0.39615033, -0.23393948, -0.09601383]}),
+ (:Movie {title: "Avatar", year: 2009, genre: "Sci-Fi", plot: "A paraplegic Marine is sent to the moon Pandora, where he becomes torn between following orders and protecting the world he feels is his home.", embedding: [-0.13663386, 0.00635589, -0.03038832, -0.08252723]}),
+ (:Movie {title: "Blade Runner", year: 1982, genre: "Sci-Fi", plot: "A blade runner must pursue and terminate four replicants who have stolen a ship in space and returned to Earth.", embedding: [ 0.27215557, -0.1479577, -0.09972772, -0.08234394]}),
+ (:Movie {title: "Blade Runner 2049", year: 2017, genre: "Sci-Fi", plot: "A new blade runner unearths a long-buried secret that has the potential to plunge what’s left of society into chaos.", embedding: [ 0.21560573, -0.07505179, -0.01331814, 0.13403069]}),
+ (:Movie {title: "Minority Report", year: 2002, genre: "Sci-Fi", plot: "In a future where a special police unit can arrest murderers before they commit their crimes, a top officer is accused of a future murder.", embedding: [ 0.24008012, 0.44954908, -0.30905488, 0.15195407]}),
+ (:Movie {title: "Total Recall", year: 1990, genre: "Sci-Fi", plot: "A construction worker discovers that his memories have been implanted and becomes embroiled in a conspiracy on Mars.", embedding: [-0.17471036, 0.14695261, -0.06272433, -0.21795064]}),
+ (:Movie {title: "Elysium", year: 2013, genre: "Sci-Fi", plot: "In a future where the rich live on a luxurious space station while the rest of humanity lives in squalor, a man fights to bring equality.", embedding: [-0.33280967, 0.07733926, 0.11015328, 0.53382836]}),
+ (:Movie {title: "Gattaca", year: 1997, genre: "Sci-Fi", plot: "In a future where genetic engineering determines social class, a man defies his fate to achieve his dreams.", embedding: [-0.21629286, 0.31114665, 0.08303899, 0.46199759]}),
+ (:Movie {title: "The Fifth Element", year: 1997, genre: "Sci-Fi", plot: "In a futuristic world, a cab driver becomes the key to saving humanity from an impending cosmic threat.", embedding: [-0.11528205, -0.0208782, -0.0735215, 0.14327449]}),
+ (:Movie {title: "The Terminator", year: 1984, genre: "Action", plot: "A cyborg assassin is sent back in time to kill the mother of the future resistance leader.", embedding: [ 0.33666933, 0.18040994, -0.01075103, -0.11117851]}),
+ (:Movie {title: "Terminator 2: Judgment Day", year: 1991, genre: "Action", plot: "A reprogrammed Terminator is sent to protect the future leader of the human resistance from a more advanced Terminator.", embedding: [ 0.34698868, 0.06439331, 0.06232323, -0.19534876]}),
+ (:Movie {title: "Jurassic Park", year: 1993, genre: "Adventure", plot: "Scientists clone dinosaurs to create a theme park, but things go awry when the creatures escape.", embedding: [ 0.01794725, -0.11434246, -0.46831815, -0.01049593]}),
+ (:Movie {title: "The Avengers", year: 2012, genre: "Action", plot: "Superheroes assemble to face a global threat from an alien invasion led by Loki.", embedding: [ 0.00546514, -0.37005171, -0.42612838, 0.07968612]})
$$) AS (result agtype);
result
--------
@@ -201,7 +407,20 @@
-- Get top 4 most similar movies to The Terminator using cosine distance
SELECT * FROM cypher('graph', $$ MATCH (m:Movie), (search:Movie {title: "The Terminator"})
- RETURN m.title ORDER BY cosine_distance(m.embedding, search.embedding) ASC LIMIT 4
+ RETURN m.title ORDER BY cosine_distance(m.embedding, search.embedding)
+ ASC LIMIT 4
+$$) AS (title agtype);
+ title
+------------------------------
+ "The Terminator"
+ "Terminator 2: Judgment Day"
+ "Minority Report"
+ "Blade Runner"
+(4 rows)
+
+SELECT * FROM cypher('graph', $$ MATCH (m:Movie), (search:Movie {title: "The Terminator"})
+ RETURN m.title ORDER BY m.embedding::vector <=> search.embedding::vector
+ ASC LIMIT 4
$$) AS (title agtype);
title
------------------------------
@@ -213,7 +432,20 @@
-- Get top 4 most similar movies to The Matrix using cosine distance
SELECT * FROM cypher('graph', $$ MATCH (m:Movie), (search:Movie {title: "The Matrix"})
- RETURN m.title ORDER BY cosine_distance(m.embedding, search.embedding) ASC LIMIT 4
+ RETURN m.title ORDER BY cosine_distance(m.embedding, search.embedding)
+ ASC LIMIT 4
+$$) AS (title agtype);
+ title
+----------------------------
+ "The Matrix"
+ "The Matrix Reloaded"
+ "The Matrix Resurrections"
+ "Total Recall"
+(4 rows)
+
+SELECT * FROM cypher('graph', $$ MATCH (m:Movie), (search:Movie {title: "The Matrix"})
+ RETURN m.title ORDER BY m.embedding::vector <=> search.embedding::vector
+ ASC LIMIT 4
$$) AS (title agtype);
title
----------------------------
@@ -224,27 +456,27 @@
(4 rows)
-- l2 norm of the embedding
-SELECT * FROM cypher('graph', $$ MATCH (m:Movie) set m.embedding=(l2_normalize(m.embedding))::text return m.title, m.embedding $$) AS (title agtype, embedding agtype);
- title | embedding
-------------------------------+----------------------------------------------------
- "The Matrix" | "[-0.22980669,0.12351139,0.89543957,-0.36072403]"
- "The Matrix Reloaded" | "[0.58534974,-0.44225806,0.6790991,0.024453051]"
- "The Matrix Revolutions" | "[0.26431033,-0.6424414,0.4884408,0.528048]"
- "The Matrix Resurrections" | "[0.72151977,-0.28723562,0.60715157,0.16814256]"
- "Inception" | "[0.08159459,0.81693435,-0.43519026,-0.3695538]"
- "Interstellar" | "[-0.5290723,-0.71527255,-0.4223914,-0.17335857]"
- "Avatar" | "[-0.84023285,0.039085682,-0.18687363,-0.507503]"
- "Blade Runner" | "[0.81074023,-0.44075987,-0.29708475,-0.2452992]"
- "Blade Runner 2049" | "[0.8134027,-0.28314334,-0.05024454,0.50564945]"
- "Minority Report" | "[0.39031598,0.7308651,-0.5024533,0.24704295]"
- "Total Recall" | "[-0.54291505,0.4566574,-0.19491677,-0.67728484]"
- "Elysium" | "[-0.517338,0.12022049,0.17122844,0.82981277]"
- "Gattaca" | "[-0.35853538,0.51576865,0.13764863,0.765825]"
- "The Fifth Element" | "[-0.5788842,-0.10483904,-0.36918527,0.7194471]"
- "The Terminator" | "[0.84599304,0.45333964,-0.02701552,-0.27937278]"
- "Terminator 2: Judgment Day" | "[0.8501332,0.15776564,0.15269388,-0.4786106]"
- "Jurassic Park" | "[0.037194606,-0.23696794,-0.9705615,-0.02175219]"
- "The Avengers" | "[0.009587915,-0.6492101,-0.7475897,0.13979948]"
+SELECT * FROM cypher('graph', $$ MATCH (m:Movie) set m.embedding=l2_normalize(m.embedding)::agtype return m.title, m.embedding $$) AS (title agtype, embedding agtype);
+ title | embedding
+------------------------------+-----------------------------------------------------
+ "The Matrix" | [-0.22980669, 0.12351139, 0.89543957, -0.36072403]
+ "The Matrix Reloaded" | [0.58534974, -0.44225806, 0.6790991, 0.024453051]
+ "The Matrix Revolutions" | [0.26431033, -0.6424414, 0.4884408, 0.528048]
+ "The Matrix Resurrections" | [0.72151977, -0.28723562, 0.60715157, 0.16814256]
+ "Inception" | [0.08159459, 0.81693435, -0.43519026, -0.3695538]
+ "Interstellar" | [-0.5290723, -0.71527255, -0.4223914, -0.17335857]
+ "Avatar" | [-0.84023285, 0.039085682, -0.18687363, -0.507503]
+ "Blade Runner" | [0.81074023, -0.44075987, -0.29708475, -0.2452992]
+ "Blade Runner 2049" | [0.8134027, -0.28314334, -0.05024454, 0.50564945]
+ "Minority Report" | [0.39031598, 0.7308651, -0.5024533, 0.24704295]
+ "Total Recall" | [-0.54291505, 0.4566574, -0.19491677, -0.67728484]
+ "Elysium" | [-0.517338, 0.12022049, 0.17122844, 0.82981277]
+ "Gattaca" | [-0.35853538, 0.51576865, 0.13764863, 0.765825]
+ "The Fifth Element" | [-0.5788842, -0.10483904, -0.36918527, 0.7194471]
+ "The Terminator" | [0.84599304, 0.45333964, -0.02701552, -0.27937278]
+ "Terminator 2: Judgment Day" | [0.8501332, 0.15776564, 0.15269388, -0.4786106]
+ "Jurassic Park" | [0.037194606, -0.23696794, -0.9705615, -0.02175219]
+ "The Avengers" | [0.009587915, -0.6492101, -0.7475897, 0.13979948]
(18 rows)
-- Get top 4 most similar movies to The Terminator using l2 distance
@@ -259,6 +491,18 @@
"Blade Runner"
(4 rows)
+SELECT * FROM cypher('graph', $$ MATCH (m:Movie), (search:Movie {title: "The Terminator"})
+ RETURN m.title ORDER BY m.embedding::vector OPERATOR (`<->`) search.embedding::vector
+ ASC LIMIT 4
+$$) AS (title agtype);
+ title
+------------------------------
+ "The Terminator"
+ "Terminator 2: Judgment Day"
+ "Minority Report"
+ "Blade Runner"
+(4 rows)
+
-- Get top 4 most similar movies to The Matrix using l2 distance
SELECT * FROM cypher('graph', $$ MATCH (m:Movie), (search:Movie {title: "The Matrix"})
RETURN m.title ORDER BY l2_distance(m.embedding, search.embedding) ASC LIMIT 4
@@ -271,6 +515,186 @@
"Total Recall"
(4 rows)
+SELECT * FROM cypher('graph', $$ MATCH (m:Movie), (search:Movie {title: "The Matrix"})
+ RETURN m.title ORDER BY m.embedding::vector OPERATOR (`<->`) search.embedding::vector
+ ASC LIMIT 4
+$$) AS (title agtype);
+ title
+----------------------------
+ "The Matrix"
+ "The Matrix Reloaded"
+ "The Matrix Resurrections"
+ "Total Recall"
+(4 rows)
+
+--
+-- Test vector index
+--
+-- This function will be used to check if index scan
+-- is used successfully. We cannot simply have EXPLAIN
+-- in the upcoming queries because it produces some
+-- hardcoded oids in sort node, which may change in
+-- future and break the tests.
+CREATE OR REPLACE FUNCTION plan_has_index_scan(sql text)
+RETURNS boolean
+LANGUAGE plpgsql AS
+$$
+DECLARE
+ plan_lines text[];
+ plan_text text;
+BEGIN
+ EXECUTE format('EXPLAIN (FORMAT JSON, COSTS OFF) %s', sql) INTO plan_text;
+
+ -- Return true if 'Index Scan' appears anywhere
+ RETURN position('"Index Scan"' in plan_text) > 0;
+END;
+$$;
+SELECT * FROM cypher('graph', $$ MATCH (m:Movie), (search:Movie {title: "The Matrix"})
+ RETURN m.title ORDER BY m.embedding::vector(4) <=> search.embedding::vector(4)
+ ASC LIMIT 4
+$$) AS (title agtype);
+ title
+----------------------------
+ "The Matrix"
+ "The Matrix Reloaded"
+ "The Matrix Resurrections"
+ "Total Recall"
+(4 rows)
+
+-- The index expression below matches the expression
+-- seen in the EXPLAIN plan of above query
+DO $$
+DECLARE
+ graph_oid oid;
+BEGIN
+ SELECT graphid INTO graph_oid
+ FROM ag_catalog.ag_graph
+ WHERE name = 'graph';
+
+ EXECUTE format($f$
+ CREATE INDEX movie_vector_idx ON graph."Movie"
+ USING hnsw (((
+ agtype_access_operator(
+ VARIADIC ARRAY[
+ _agtype_build_vertex(id, _label_name(%L::oid, id), properties),
+ '"embedding"'::agtype
+ ]
+ )::text
+ )::vector(4)) vector_cosine_ops);
+ $f$, graph_oid);
+END;
+$$;
+-- Disable seqscan just to test the index
+SET enable_seqscan = off;
+SELECT plan_has_index_scan($f$
+ SELECT * FROM cypher('graph', $$
+ MATCH (m:Movie)
+ RETURN m.title
+ ORDER BY m.embedding::vector(4) <=> [-0.07594558, 0.04081754, 0.29592122, -0.11921061]::vector(4)
+ ASC LIMIT 4
+ $$) AS (title agtype);
+$f$);
+ plan_has_index_scan
+---------------------
+ t
+(1 row)
+
+SELECT * FROM cypher('graph', $$ MATCH (m:Movie)
+ RETURN m.title
+ ORDER BY m.embedding::vector(4) <=> [-0.07594558, 0.04081754, 0.29592122, -0.11921061]::vector(4)
+ ASC LIMIT 4
+$$) AS (title agtype);
+ title
+----------------------------
+ "The Matrix"
+ "The Matrix Reloaded"
+ "The Matrix Resurrections"
+ "Total Recall"
+(4 rows)
+
+DROP INDEX graph.movie_vector_idx;
+SET enable_seqscan = on;
+-- Test a direct implicit cast
+CREATE CAST (agtype AS vector)
+ WITH INOUT AS implicit;
+SELECT * FROM cypher('graph', $$ MATCH (m:Movie), (search:Movie {title: "The Matrix"})
+ RETURN m.title ORDER BY m.embedding <=> search.embedding
+ ASC LIMIT 4
+$$) AS (title agtype);
+ title
+----------------------------
+ "The Matrix"
+ "The Matrix Reloaded"
+ "The Matrix Resurrections"
+ "Total Recall"
+(4 rows)
+
+SELECT * FROM cypher('graph', $$ MATCH (m:Movie), (search:Movie {title: "The Matrix"})
+ RETURN m.title ORDER BY m.embedding OPERATOR (`<->`) search.embedding
+ ASC LIMIT 4
+$$) AS (title agtype);
+ title
+----------------------------
+ "The Matrix"
+ "The Matrix Reloaded"
+ "The Matrix Resurrections"
+ "Total Recall"
+(4 rows)
+
+DO $$
+DECLARE
+ graph_oid oid;
+BEGIN
+ SELECT graphid INTO graph_oid
+ FROM ag_catalog.ag_graph
+ WHERE name = 'graph';
+
+ EXECUTE format($f$
+ CREATE INDEX movie_vector_idx ON graph."Movie"
+ USING hnsw ((
+ agtype_access_operator(
+ VARIADIC ARRAY[
+ _agtype_build_vertex(id, _label_name(%L::oid, id), properties),
+ '"embedding"'::agtype
+ ]
+ )::vector(4)) vector_cosine_ops);
+ $f$, graph_oid);
+END;
+$$;
+-- Disable seqscan just to test the index
+SET enable_seqscan = off;
+SELECT plan_has_index_scan($f$
+ SELECT * FROM cypher('graph', $$
+ MATCH (m:Movie)
+ RETURN m.title
+ ORDER BY m.embedding::vector(4) <=> [-0.07594558, 0.04081754, 0.29592122, -0.11921061]::vector(4)
+ ASC LIMIT 4
+ $$) AS (title agtype);
+$f$);
+ plan_has_index_scan
+---------------------
+ t
+(1 row)
+
+SELECT * FROM cypher('graph', $$ MATCH (m:Movie)
+ RETURN m.title
+ ORDER BY m.embedding::vector(4) <=> [-0.07594558, 0.04081754, 0.29592122, -0.11921061]::vector(4)
+ ASC LIMIT 4
+$$) AS (title agtype);
+ title
+----------------------------
+ "The Matrix"
+ "The Matrix Reloaded"
+ "The Matrix Resurrections"
+ "Total Recall"
+(4 rows)
+
+SET enable_seqscan = on;
+--
+-- Clean up
+--
+DROP FUNCTION plan_has_index_scan(text);
+DROP CAST (agtype AS vector);
SELECT drop_graph('graph', true);
NOTICE: drop cascades to 3 other objects
DETAIL: drop cascades to table graph._ag_label_vertex
diff --git a/regress/expected/scan.out b/regress/expected/scan.out
index d96d800..d8105a0 100644
--- a/regress/expected/scan.out
+++ b/regress/expected/scan.out
@@ -52,7 +52,7 @@
"
LINE 2: /* unterminated /* comment
^
--- recover syntax highlighting */
+-- recover syntax highlighting */ */
--
-- single-line comment
--
@@ -208,9 +208,9 @@
SELECT * FROM cypher('scan', $$
RETURN 0xF~
$$) AS t(a int);
-ERROR: unexpected character at or near "~"
-LINE 2: RETURN 0xF~
- ^
+ERROR: syntax error at end of input
+LINE 3: $$) AS t(a int);
+ ^
-- an invalid character after the leading "0x"
SELECT * FROM cypher('scan', $$
RETURN 0x~
diff --git a/regress/sql/pgvector.sql b/regress/sql/pgvector.sql
index 816d6eb..677e785 100644
--- a/regress/sql/pgvector.sql
+++ b/regress/sql/pgvector.sql
@@ -39,6 +39,26 @@
SELECT * FROM cypher('graph', $$ RETURN "[1.22,2.22,3.33]"::vector $$) AS (n halfvec);
SELECT * FROM cypher('graph', $$ RETURN "[1.22,2.22,3.33]"::vector $$) AS (n sparsevec);
+SELECT * FROM cypher('graph', $$ RETURN [1.22,2.22,3.33]::vector $$) AS (n vector);
+SELECT * FROM cypher('graph', $$ RETURN [1.22,2.22,3.33]::vector $$) AS (n halfvec);
+SELECT * FROM cypher('graph', $$ RETURN [1.22,2.22,3.33]::vector $$) AS (n sparsevec);
+
+SELECT * FROM cypher('graph', $$ RETURN [1.22,2.22,3.33]::vector $$) AS (n vector(3));
+SELECT * FROM cypher('graph', $$ RETURN [1.22,2.22,3.33]::vector $$) AS (n halfvec(3));
+SELECT * FROM cypher('graph', $$ RETURN [1.22,2.22,3.33]::vector $$) AS (n sparsevec(3));
+
+-- Should error out
+SELECT * FROM cypher('graph', $$ RETURN [1.22,2.22,3.33]::vector $$) AS (n vector(2));
+SELECT * FROM cypher('graph', $$ RETURN [1.22,2.22,3.33]::vector $$) AS (n halfvec(2));
+SELECT * FROM cypher('graph', $$ RETURN [1.22,2.22,3.33]::vector $$) AS (n sparsevec(2));
+
+SELECT * FROM cypher('graph', $$ RETURN [1.22,2.22,3.33]::vector(3) $$) AS (n vector(4));
+SELECT * FROM cypher('graph', $$ RETURN [1.22,2.22,3.33]::vector(3) $$) AS (n halfvec(4));
+SELECT * FROM cypher('graph', $$ RETURN [1.22,2.22,3.33]::vector(3) $$) AS (n sparsevec(4));
+
+--
+-- Test functions
+--
SELECT * FROM cypher('graph', $$ RETURN l2_distance("[1,2,3]", "[1,2,4]") $$) AS (n agtype);
SELECT * FROM cypher('graph', $$ RETURN inner_product("[1,2,3]", "[1,2,4]") $$) AS (n agtype);
SELECT * FROM cypher('graph', $$ RETURN cosine_distance("[1,2,3]", "[1,2,4]") $$) AS (n agtype);
@@ -49,28 +69,65 @@
SELECT * FROM cypher('graph', $$ RETURN l2_normalize("[1,2,3]")::text $$) AS (n agtype);
SELECT * FROM cypher('graph', $$ RETURN subvector("[1,2,3,4,5,6]", 2, 4) $$) AS (n vector);
SELECT * FROM cypher('graph', $$ RETURN subvector("[1,2,3,4,5,6]", 2, 4)::text $$) AS (n agtype);
-SELECT * FROM cypher('graph', $$ RETURN binary_quantize("[1,2,4]") $$) AS (n bit);
+SELECT * FROM cypher('graph', $$ RETURN binary_quantize("[1,2,4]") $$) AS (n bit(3));
+--
+-- Test operators
+--
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector + [1,2,4]::vector $$) AS (n vector);
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector - [1,2,4]::vector $$) AS (n vector);
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector * [1,2,4]::vector $$) AS (n vector);
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector || [1,2,4]::vector $$) AS (n vector);
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector <#> [1,2,4]::vector $$) AS (n agtype);
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector <=> [1,2,4]::vector $$) AS (n agtype);
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector <+> [1,2,4]::vector $$) AS (n agtype);
+--
+-- Due to issues with pattern matching syntax, '-' is not allowed
+-- as an operator character, so we have to use the OPERATOR syntax.
+--
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (`<->`) [1,2,4]::vector $$) AS (n agtype);
+
+-- Using OPERATOR () syntax
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (+) [1,2,4]::vector $$) AS (n vector);
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (-) [1,2,4]::vector $$) AS (n vector);
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (*) [1,2,4]::vector $$) AS (n vector);
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (||) [1,2,4]::vector $$) AS (n vector);
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (`<->`) [1,2,4]::vector $$) AS (n agtype);
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (<#>) [1,2,4]::vector $$) AS (n agtype);
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (<=>) [1,2,4]::vector $$) AS (n agtype);
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (<+>) [1,2,4]::vector $$) AS (n agtype);
+
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (public.+) [1,2,4]::vector $$) AS (n vector);
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (public.-) [1,2,4]::vector $$) AS (n vector);
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (public.*) [1,2,4]::vector $$) AS (n vector);
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (public.||) [1,2,4]::vector $$) AS (n vector);
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (public.`<->`) [1,2,4]::vector $$) AS (n agtype);
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (public.<#>) [1,2,4]::vector $$) AS (n agtype);
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (public.<=>) [1,2,4]::vector $$) AS (n agtype);
+SELECT * FROM cypher('graph', $$ RETURN [1,2,3]::vector OPERATOR (public.<+>) [1,2,4]::vector $$) AS (n agtype);
+
+--
-- An example usage
+--
SELECT * FROM cypher('graph', $$
- CREATE (:Movie {title: "The Matrix", year: 1999, genre: "Action", plot: "A computer hacker learns about the true nature of reality and joins a rebellion to free humanity from a simulated world controlled by machines.", embedding: "[-0.07594558, 0.04081754, 0.29592122, -0.11921061]"}),
- (:Movie {title: "The Matrix Reloaded", year: 2003, genre: "Action", plot: "The rebels continue their fight against the machines, uncovering deeper truths about the Matrix and the nature of their mission.", embedding: "[0.30228977, -0.22839354, 0.35070436, 0.01262819]"}),
- (:Movie {title: "The Matrix Revolutions", year: 2003, genre: "Action", plot: "The final battle between humans and machines reaches its climax as the fate of both worlds hangs in the balance.", embedding: "[ 0.12240622, -0.29752459, 0.22620453, 0.24454723]"}),
- (:Movie {title: "The Matrix Resurrections", year: 2021, genre: "Action", plot: "Neo returns to a new version of the Matrix and must once again fight to save the people from the control of the machines.", embedding: "[ 0.34717246, -0.13820869, 0.29214213, 0.08090488]"}),
- (:Movie {title: "Inception", year: 2010, genre: "Sci-Fi", plot: "A skilled thief is given a chance at redemption if he can successfully perform an inception: planting an idea into someone’s subconscious.", embedding: "[ 0.03923657, 0.39284106, -0.20927092, -0.17770818]"}),
- (:Movie {title: "Interstellar", year: 2014, genre: "Sci-Fi", plot: "A group of explorers travel through a wormhole in space in an attempt to ensure humanity’s survival.", embedding: "[-0.29302418, -0.39615033, -0.23393948, -0.09601383]"}),
- (:Movie {title: "Avatar", year: 2009, genre: "Sci-Fi", plot: "A paraplegic Marine is sent to the moon Pandora, where he becomes torn between following orders and protecting the world he feels is his home.", embedding: "[-0.13663386, 0.00635589, -0.03038832, -0.08252723]"}),
- (:Movie {title: "Blade Runner", year: 1982, genre: "Sci-Fi", plot: "A blade runner must pursue and terminate four replicants who have stolen a ship in space and returned to Earth.", embedding: "[ 0.27215557, -0.1479577, -0.09972772, -0.08234394]"}),
- (:Movie {title: "Blade Runner 2049", year: 2017, genre: "Sci-Fi", plot: "A new blade runner unearths a long-buried secret that has the potential to plunge what’s left of society into chaos.", embedding: "[ 0.21560573, -0.07505179, -0.01331814, 0.13403069]"}),
- (:Movie {title: "Minority Report", year: 2002, genre: "Sci-Fi", plot: "In a future where a special police unit can arrest murderers before they commit their crimes, a top officer is accused of a future murder.", embedding: "[ 0.24008012, 0.44954908, -0.30905488, 0.15195407]"}),
- (:Movie {title: "Total Recall", year: 1990, genre: "Sci-Fi", plot: "A construction worker discovers that his memories have been implanted and becomes embroiled in a conspiracy on Mars.", embedding: "[-0.17471036, 0.14695261, -0.06272433, -0.21795064]"}),
- (:Movie {title: "Elysium", year: 2013, genre: "Sci-Fi", plot: "In a future where the rich live on a luxurious space station while the rest of humanity lives in squalor, a man fights to bring equality.", embedding: "[-0.33280967, 0.07733926, 0.11015328, 0.53382836]"}),
- (:Movie {title: "Gattaca", year: 1997, genre: "Sci-Fi", plot: "In a future where genetic engineering determines social class, a man defies his fate to achieve his dreams.", embedding: "[-0.21629286, 0.31114665, 0.08303899, 0.46199759]"}),
- (:Movie {title: "The Fifth Element", year: 1997, genre: "Sci-Fi", plot: "In a futuristic world, a cab driver becomes the key to saving humanity from an impending cosmic threat.", embedding: "[-0.11528205, -0.0208782, -0.0735215, 0.14327449]"}),
- (:Movie {title: "The Terminator", year: 1984, genre: "Action", plot: "A cyborg assassin is sent back in time to kill the mother of the future resistance leader.", embedding: "[ 0.33666933, 0.18040994, -0.01075103, -0.11117851]"}),
- (:Movie {title: "Terminator 2: Judgment Day", year: 1991, genre: "Action", plot: "A reprogrammed Terminator is sent to protect the future leader of the human resistance from a more advanced Terminator.", embedding: "[ 0.34698868, 0.06439331, 0.06232323, -0.19534876]"}),
- (:Movie {title: "Jurassic Park", year: 1993, genre: "Adventure", plot: "Scientists clone dinosaurs to create a theme park, but things go awry when the creatures escape.", embedding: "[ 0.01794725, -0.11434246, -0.46831815, -0.01049593]"}),
- (:Movie {title: "The Avengers", year: 2012, genre: "Action", plot: "Superheroes assemble to face a global threat from an alien invasion led by Loki.", embedding: "[ 0.00546514, -0.37005171, -0.42612838, 0.07968612]"})
+ CREATE (:Movie {title: "The Matrix", year: 1999, genre: "Action", plot: "A computer hacker learns about the true nature of reality and joins a rebellion to free humanity from a simulated world controlled by machines.", embedding: [-0.07594558, 0.04081754, 0.29592122, -0.11921061]}),
+ (:Movie {title: "The Matrix Reloaded", year: 2003, genre: "Action", plot: "The rebels continue their fight against the machines, uncovering deeper truths about the Matrix and the nature of their mission.", embedding: [0.30228977, -0.22839354, 0.35070436, 0.01262819]}),
+ (:Movie {title: "The Matrix Revolutions", year: 2003, genre: "Action", plot: "The final battle between humans and machines reaches its climax as the fate of both worlds hangs in the balance.", embedding: [ 0.12240622, -0.29752459, 0.22620453, 0.24454723]}),
+ (:Movie {title: "The Matrix Resurrections", year: 2021, genre: "Action", plot: "Neo returns to a new version of the Matrix and must once again fight to save the people from the control of the machines.", embedding: [ 0.34717246, -0.13820869, 0.29214213, 0.08090488]}),
+ (:Movie {title: "Inception", year: 2010, genre: "Sci-Fi", plot: "A skilled thief is given a chance at redemption if he can successfully perform an inception: planting an idea into someone’s subconscious.", embedding: [ 0.03923657, 0.39284106, -0.20927092, -0.17770818]}),
+ (:Movie {title: "Interstellar", year: 2014, genre: "Sci-Fi", plot: "A group of explorers travel through a wormhole in space in an attempt to ensure humanity’s survival.", embedding: [-0.29302418, -0.39615033, -0.23393948, -0.09601383]}),
+ (:Movie {title: "Avatar", year: 2009, genre: "Sci-Fi", plot: "A paraplegic Marine is sent to the moon Pandora, where he becomes torn between following orders and protecting the world he feels is his home.", embedding: [-0.13663386, 0.00635589, -0.03038832, -0.08252723]}),
+ (:Movie {title: "Blade Runner", year: 1982, genre: "Sci-Fi", plot: "A blade runner must pursue and terminate four replicants who have stolen a ship in space and returned to Earth.", embedding: [ 0.27215557, -0.1479577, -0.09972772, -0.08234394]}),
+ (:Movie {title: "Blade Runner 2049", year: 2017, genre: "Sci-Fi", plot: "A new blade runner unearths a long-buried secret that has the potential to plunge what’s left of society into chaos.", embedding: [ 0.21560573, -0.07505179, -0.01331814, 0.13403069]}),
+ (:Movie {title: "Minority Report", year: 2002, genre: "Sci-Fi", plot: "In a future where a special police unit can arrest murderers before they commit their crimes, a top officer is accused of a future murder.", embedding: [ 0.24008012, 0.44954908, -0.30905488, 0.15195407]}),
+ (:Movie {title: "Total Recall", year: 1990, genre: "Sci-Fi", plot: "A construction worker discovers that his memories have been implanted and becomes embroiled in a conspiracy on Mars.", embedding: [-0.17471036, 0.14695261, -0.06272433, -0.21795064]}),
+ (:Movie {title: "Elysium", year: 2013, genre: "Sci-Fi", plot: "In a future where the rich live on a luxurious space station while the rest of humanity lives in squalor, a man fights to bring equality.", embedding: [-0.33280967, 0.07733926, 0.11015328, 0.53382836]}),
+ (:Movie {title: "Gattaca", year: 1997, genre: "Sci-Fi", plot: "In a future where genetic engineering determines social class, a man defies his fate to achieve his dreams.", embedding: [-0.21629286, 0.31114665, 0.08303899, 0.46199759]}),
+ (:Movie {title: "The Fifth Element", year: 1997, genre: "Sci-Fi", plot: "In a futuristic world, a cab driver becomes the key to saving humanity from an impending cosmic threat.", embedding: [-0.11528205, -0.0208782, -0.0735215, 0.14327449]}),
+ (:Movie {title: "The Terminator", year: 1984, genre: "Action", plot: "A cyborg assassin is sent back in time to kill the mother of the future resistance leader.", embedding: [ 0.33666933, 0.18040994, -0.01075103, -0.11117851]}),
+ (:Movie {title: "Terminator 2: Judgment Day", year: 1991, genre: "Action", plot: "A reprogrammed Terminator is sent to protect the future leader of the human resistance from a more advanced Terminator.", embedding: [ 0.34698868, 0.06439331, 0.06232323, -0.19534876]}),
+ (:Movie {title: "Jurassic Park", year: 1993, genre: "Adventure", plot: "Scientists clone dinosaurs to create a theme park, but things go awry when the creatures escape.", embedding: [ 0.01794725, -0.11434246, -0.46831815, -0.01049593]}),
+ (:Movie {title: "The Avengers", year: 2012, genre: "Action", plot: "Superheroes assemble to face a global threat from an alien invasion led by Loki.", embedding: [ 0.00546514, -0.37005171, -0.42612838, 0.07968612]})
$$) AS (result agtype);
SELECT * FROM cypher('graph', $$ MATCH (m:Movie) RETURN m.title, (m.embedding)::vector $$) AS (title agtype, embedding vector);
@@ -79,23 +136,174 @@
-- Get top 4 most similar movies to The Terminator using cosine distance
SELECT * FROM cypher('graph', $$ MATCH (m:Movie), (search:Movie {title: "The Terminator"})
- RETURN m.title ORDER BY cosine_distance(m.embedding, search.embedding) ASC LIMIT 4
+ RETURN m.title ORDER BY cosine_distance(m.embedding, search.embedding)
+ ASC LIMIT 4
$$) AS (title agtype);
+SELECT * FROM cypher('graph', $$ MATCH (m:Movie), (search:Movie {title: "The Terminator"})
+ RETURN m.title ORDER BY m.embedding::vector <=> search.embedding::vector
+ ASC LIMIT 4
+$$) AS (title agtype);
+
-- Get top 4 most similar movies to The Matrix using cosine distance
SELECT * FROM cypher('graph', $$ MATCH (m:Movie), (search:Movie {title: "The Matrix"})
- RETURN m.title ORDER BY cosine_distance(m.embedding, search.embedding) ASC LIMIT 4
+ RETURN m.title ORDER BY cosine_distance(m.embedding, search.embedding)
+ ASC LIMIT 4
$$) AS (title agtype);
+SELECT * FROM cypher('graph', $$ MATCH (m:Movie), (search:Movie {title: "The Matrix"})
+ RETURN m.title ORDER BY m.embedding::vector <=> search.embedding::vector
+ ASC LIMIT 4
+$$) AS (title agtype);
+
-- l2 norm of the embedding
-SELECT * FROM cypher('graph', $$ MATCH (m:Movie) set m.embedding=(l2_normalize(m.embedding))::text return m.title, m.embedding $$) AS (title agtype, embedding agtype);
+SELECT * FROM cypher('graph', $$ MATCH (m:Movie) set m.embedding=l2_normalize(m.embedding)::agtype return m.title, m.embedding $$) AS (title agtype, embedding agtype);
-- Get top 4 most similar movies to The Terminator using l2 distance
SELECT * FROM cypher('graph', $$ MATCH (m:Movie), (search:Movie {title: "The Terminator"})
RETURN m.title ORDER BY l2_distance(m.embedding, search.embedding) ASC LIMIT 4
$$) AS (title agtype);
+SELECT * FROM cypher('graph', $$ MATCH (m:Movie), (search:Movie {title: "The Terminator"})
+ RETURN m.title ORDER BY m.embedding::vector OPERATOR (`<->`) search.embedding::vector
+ ASC LIMIT 4
+$$) AS (title agtype);
+
-- Get top 4 most similar movies to The Matrix using l2 distance
SELECT * FROM cypher('graph', $$ MATCH (m:Movie), (search:Movie {title: "The Matrix"})
RETURN m.title ORDER BY l2_distance(m.embedding, search.embedding) ASC LIMIT 4
$$) AS (title agtype);
+SELECT * FROM cypher('graph', $$ MATCH (m:Movie), (search:Movie {title: "The Matrix"})
+ RETURN m.title ORDER BY m.embedding::vector OPERATOR (`<->`) search.embedding::vector
+ ASC LIMIT 4
+$$) AS (title agtype);
+--
+-- Test vector index
+--
+
+-- This function will be used to check if index scan
+-- is used successfully. We cannot simply have EXPLAIN
+-- in the upcoming queries because it produces some
+-- hardcoded oids in sort node, which may change in
+-- future and break the tests.
+CREATE OR REPLACE FUNCTION plan_has_index_scan(sql text)
+RETURNS boolean
+LANGUAGE plpgsql AS
+$$
+DECLARE
+ plan_lines text[];
+ plan_text text;
+BEGIN
+ EXECUTE format('EXPLAIN (FORMAT JSON, COSTS OFF) %s', sql) INTO plan_text;
+
+ -- Return true if 'Index Scan' appears anywhere
+ RETURN position('"Index Scan"' in plan_text) > 0;
+END;
+$$;
+
+SELECT * FROM cypher('graph', $$ MATCH (m:Movie), (search:Movie {title: "The Matrix"})
+ RETURN m.title ORDER BY m.embedding::vector(4) <=> search.embedding::vector(4)
+ ASC LIMIT 4
+$$) AS (title agtype);
+
+-- The index expression below matches the expression
+-- seen in the EXPLAIN plan of above query
+DO $$
+DECLARE
+ graph_oid oid;
+BEGIN
+ SELECT graphid INTO graph_oid
+ FROM ag_catalog.ag_graph
+ WHERE name = 'graph';
+
+ EXECUTE format($f$
+ CREATE INDEX movie_vector_idx ON graph."Movie"
+ USING hnsw (((
+ agtype_access_operator(
+ VARIADIC ARRAY[
+ _agtype_build_vertex(id, _label_name(%L::oid, id), properties),
+ '"embedding"'::agtype
+ ]
+ )::text
+ )::vector(4)) vector_cosine_ops);
+ $f$, graph_oid);
+END;
+$$;
+
+-- Disable seqscan just to test the index
+SET enable_seqscan = off;
+SELECT plan_has_index_scan($f$
+ SELECT * FROM cypher('graph', $$
+ MATCH (m:Movie)
+ RETURN m.title
+ ORDER BY m.embedding::vector(4) <=> [-0.07594558, 0.04081754, 0.29592122, -0.11921061]::vector(4)
+ ASC LIMIT 4
+ $$) AS (title agtype);
+$f$);
+SELECT * FROM cypher('graph', $$ MATCH (m:Movie)
+ RETURN m.title
+ ORDER BY m.embedding::vector(4) <=> [-0.07594558, 0.04081754, 0.29592122, -0.11921061]::vector(4)
+ ASC LIMIT 4
+$$) AS (title agtype);
+
+DROP INDEX graph.movie_vector_idx;
+SET enable_seqscan = on;
+
+-- Test a direct implicit cast
+CREATE CAST (agtype AS vector)
+ WITH INOUT AS implicit;
+
+SELECT * FROM cypher('graph', $$ MATCH (m:Movie), (search:Movie {title: "The Matrix"})
+ RETURN m.title ORDER BY m.embedding <=> search.embedding
+ ASC LIMIT 4
+$$) AS (title agtype);
+
+SELECT * FROM cypher('graph', $$ MATCH (m:Movie), (search:Movie {title: "The Matrix"})
+ RETURN m.title ORDER BY m.embedding OPERATOR (`<->`) search.embedding
+ ASC LIMIT 4
+$$) AS (title agtype);
+
+DO $$
+DECLARE
+ graph_oid oid;
+BEGIN
+ SELECT graphid INTO graph_oid
+ FROM ag_catalog.ag_graph
+ WHERE name = 'graph';
+
+ EXECUTE format($f$
+ CREATE INDEX movie_vector_idx ON graph."Movie"
+ USING hnsw ((
+ agtype_access_operator(
+ VARIADIC ARRAY[
+ _agtype_build_vertex(id, _label_name(%L::oid, id), properties),
+ '"embedding"'::agtype
+ ]
+ )::vector(4)) vector_cosine_ops);
+ $f$, graph_oid);
+END;
+$$;
+
+-- Disable seqscan just to test the index
+SET enable_seqscan = off;
+SELECT plan_has_index_scan($f$
+ SELECT * FROM cypher('graph', $$
+ MATCH (m:Movie)
+ RETURN m.title
+ ORDER BY m.embedding::vector(4) <=> [-0.07594558, 0.04081754, 0.29592122, -0.11921061]::vector(4)
+ ASC LIMIT 4
+ $$) AS (title agtype);
+$f$);
+SELECT * FROM cypher('graph', $$ MATCH (m:Movie)
+ RETURN m.title
+ ORDER BY m.embedding::vector(4) <=> [-0.07594558, 0.04081754, 0.29592122, -0.11921061]::vector(4)
+ ASC LIMIT 4
+$$) AS (title agtype);
+
+SET enable_seqscan = on;
+
+--
+-- Clean up
+--
+DROP FUNCTION plan_has_index_scan(text);
+DROP CAST (agtype AS vector);
SELECT drop_graph('graph', true);
DROP EXTENSION vector CASCADE;
\ No newline at end of file
diff --git a/regress/sql/scan.sql b/regress/sql/scan.sql
index 840a822..4d35fe0 100644
--- a/regress/sql/scan.sql
+++ b/regress/sql/scan.sql
@@ -41,7 +41,7 @@
/* unterminated /* comment
RETURN 0
$$) AS t(a int);
--- recover syntax highlighting */
+-- recover syntax highlighting */ */
--
-- single-line comment
diff --git a/sql/agtype_coercions.sql b/sql/agtype_coercions.sql
index c7895fa..933375f 100644
--- a/sql/agtype_coercions.sql
+++ b/sql/agtype_coercions.sql
@@ -78,7 +78,7 @@
CREATE CAST (float8 AS agtype)
WITH FUNCTION ag_catalog.float8_to_agtype(float8);
--- agtype -> float8 (exmplicit)
+-- agtype -> float8 (explicit)
CREATE FUNCTION ag_catalog.agtype_to_float8(agtype)
RETURNS float8
LANGUAGE c
diff --git a/sql/agtype_exists.sql b/sql/agtype_exists.sql
index fe6150d..441af17 100644
--- a/sql/agtype_exists.sql
+++ b/sql/agtype_exists.sql
@@ -32,9 +32,8 @@
LEFTARG = agtype,
RIGHTARG = text,
FUNCTION = ag_catalog.agtype_exists,
- COMMUTATOR = '?',
- RESTRICT = contsel,
- JOIN = contjoinsel
+ RESTRICT = matchingsel,
+ JOIN = matchingjoinsel
);
CREATE FUNCTION ag_catalog.agtype_exists_agtype(agtype, agtype)
@@ -49,9 +48,8 @@
LEFTARG = agtype,
RIGHTARG = agtype,
FUNCTION = ag_catalog.agtype_exists_agtype,
- COMMUTATOR = '?',
- RESTRICT = contsel,
- JOIN = contjoinsel
+ RESTRICT = matchingsel,
+ JOIN = matchingjoinsel
);
CREATE FUNCTION ag_catalog.agtype_exists_any(agtype, text[])
@@ -66,8 +64,8 @@
LEFTARG = agtype,
RIGHTARG = text[],
FUNCTION = ag_catalog.agtype_exists_any,
- RESTRICT = contsel,
- JOIN = contjoinsel
+ RESTRICT = matchingsel,
+ JOIN = matchingjoinsel
);
CREATE FUNCTION ag_catalog.agtype_exists_any_agtype(agtype, agtype)
@@ -82,8 +80,8 @@
LEFTARG = agtype,
RIGHTARG = agtype,
FUNCTION = ag_catalog.agtype_exists_any_agtype,
- RESTRICT = contsel,
- JOIN = contjoinsel
+ RESTRICT = matchingsel,
+ JOIN = matchingjoinsel
);
CREATE FUNCTION ag_catalog.agtype_exists_all(agtype, text[])
@@ -98,8 +96,8 @@
LEFTARG = agtype,
RIGHTARG = text[],
FUNCTION = ag_catalog.agtype_exists_all,
- RESTRICT = contsel,
- JOIN = contjoinsel
+ RESTRICT = matchingsel,
+ JOIN = matchingjoinsel
);
CREATE FUNCTION ag_catalog.agtype_exists_all_agtype(agtype, agtype)
@@ -114,6 +112,6 @@
LEFTARG = agtype,
RIGHTARG = agtype,
FUNCTION = ag_catalog.agtype_exists_all_agtype,
- RESTRICT = contsel,
- JOIN = contjoinsel
+ RESTRICT = matchingsel,
+ JOIN = matchingjoinsel
);
diff --git a/sql/agtype_operators.sql b/sql/agtype_operators.sql
index 3fbc52f..36fedfe 100644
--- a/sql/agtype_operators.sql
+++ b/sql/agtype_operators.sql
@@ -33,8 +33,8 @@
RIGHTARG = agtype,
FUNCTION = ag_catalog.agtype_contains,
COMMUTATOR = '<@',
- RESTRICT = contsel,
- JOIN = contjoinsel
+ RESTRICT = matchingsel,
+ JOIN = matchingjoinsel
);
CREATE FUNCTION ag_catalog.agtype_contained_by(agtype, agtype)
@@ -50,8 +50,8 @@
RIGHTARG = agtype,
FUNCTION = ag_catalog.agtype_contained_by,
COMMUTATOR = '@>',
- RESTRICT = contsel,
- JOIN = contjoinsel
+ RESTRICT = matchingsel,
+ JOIN = matchingjoinsel
);
CREATE FUNCTION ag_catalog.agtype_contains_top_level(agtype, agtype)
@@ -67,8 +67,8 @@
RIGHTARG = agtype,
FUNCTION = ag_catalog.agtype_contains_top_level,
COMMUTATOR = '<<@',
- RESTRICT = contsel,
- JOIN = contjoinsel
+ RESTRICT = matchingsel,
+ JOIN = matchingjoinsel
);
CREATE FUNCTION ag_catalog.agtype_contained_by_top_level(agtype, agtype)
@@ -84,6 +84,6 @@
RIGHTARG = agtype,
FUNCTION = ag_catalog.agtype_contained_by_top_level,
COMMUTATOR = '@>>',
- RESTRICT = contsel,
- JOIN = contjoinsel
+ RESTRICT = matchingsel,
+ JOIN = matchingjoinsel
);
\ No newline at end of file
diff --git a/sql/agtype_string.sql b/sql/agtype_string.sql
index e748576..0d7b201 100644
--- a/sql/agtype_string.sql
+++ b/sql/agtype_string.sql
@@ -52,6 +52,12 @@
PARALLEL SAFE
AS 'MODULE_PATHNAME';
+CREATE OPERATOR =~ (
+ LEFTARG = agtype,
+ RIGHTARG = agtype,
+ FUNCTION = ag_catalog.age_eq_tilde
+);
+
CREATE FUNCTION ag_catalog.age_is_valid_label_name(agtype)
RETURNS boolean
LANGUAGE c
diff --git a/src/backend/catalog/ag_graph.c b/src/backend/catalog/ag_graph.c
index 495c652..833cba2 100644
--- a/src/backend/catalog/ag_graph.c
+++ b/src/backend/catalog/ag_graph.c
@@ -173,3 +173,16 @@
{
return get_namespace_name(get_graph_namespace(graph_name));
}
+
+bool graph_namespace_exists(Oid graph_oid)
+{
+ graph_cache_data *cache_data;
+
+ cache_data = search_graph_namespace_cache(graph_oid);
+ if (cache_data)
+ {
+ return true;
+ }
+
+ return false;
+}
diff --git a/src/backend/catalog/ag_label.c b/src/backend/catalog/ag_label.c
index 3c242a0..b6dcf77 100644
--- a/src/backend/catalog/ag_label.c
+++ b/src/backend/catalog/ag_label.c
@@ -186,6 +186,13 @@
}
graph = PG_GETARG_OID(0);
+
+ /* Check if the graph OID is valid */
+ if (!graph_namespace_exists(graph))
+ {
+ ereport(ERROR, (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("graph with oid %u does not exist", graph)));
+ }
label_id = (int32)(((uint64)AG_GETARG_GRAPHID(1)) >> ENTRY_ID_BITS);
@@ -193,6 +200,14 @@
label_name = NameStr(label_cache->name);
+ /* If label_name is not found, error out */
+ if (label_name == NULL)
+ {
+ ereport(ERROR, (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("label with id %d does not exist in graph %u",
+ label_id, graph)));
+ }
+
if (IS_AG_DEFAULT_LABEL(label_name))
PG_RETURN_CSTRING("");
diff --git a/src/backend/nodes/cypher_outfuncs.c b/src/backend/nodes/cypher_outfuncs.c
index 5bc824f..4772621 100644
--- a/src/backend/nodes/cypher_outfuncs.c
+++ b/src/backend/nodes/cypher_outfuncs.c
@@ -320,7 +320,7 @@
DEFINE_AG_NODE(cypher_typecast);
WRITE_NODE_FIELD(expr);
- WRITE_STRING_FIELD(typecast);
+ WRITE_NODE_FIELD(typname);
WRITE_LOCATION_FIELD(location);
}
diff --git a/src/backend/parser/ag_scanner.l b/src/backend/parser/ag_scanner.l
index 45ccdac..d5d72b9 100644
--- a/src/backend/parser/ag_scanner.l
+++ b/src/backend/parser/ag_scanner.l
@@ -124,7 +124,7 @@
* Therefore, the rule has been modified so that it can match such comments.
*/
%x mlcomment
-mlcstart "/*"
+mlcstart \/\*{op_chars}*
mlcchars [^*]+|\*+
mlcstop \*+\/
slcomment "//"[^\n\r]*
@@ -228,20 +228,15 @@
* These are tokens that are used as operators and language constructs in
* Cypher, and some of them are structural characters in JSON.
*/
-left_contains "<@"
-right_contains "@>"
-any_exists "?|"
-all_exists "?&"
-concat "||"
-access_path "#>"
lt_gt "<>"
lt_eq "<="
gt_eq ">="
dot_dot ".."
plus_eq "+="
-eq_tilde "=~"
typecast "::"
-self [?%()*+,\-./:;<=>[\]^{|}]
+self [%()*+,\-./:;<=>[\]^{|}]
+op_chars [\!\@\#\^\&\|\~\?\+\*\/\%\<\>\=]
+operator {op_chars}+
other .
@@ -339,6 +334,11 @@
/* update location in case of unterminated comment */
update_location();
BEGIN(mlcomment);
+ yyless(2);
+}
+
+<mlcomment>{mlcstart} {
+ yyless(2);
}
<mlcomment>{mlcchars} {
@@ -649,54 +649,6 @@
return token;
}
-{concat} {
- update_location();
- token.type = AG_TOKEN_CONCAT;
- token.value.s = yytext;
- token.location = get_location();
- return token;
-}
-
-{access_path} {
- update_location();
- token.type = AG_TOKEN_ACCESS_PATH;
- token.value.s = yytext;
- token.location = get_location();
- return token;
-}
-
-{any_exists} {
- update_location();
- token.type = AG_TOKEN_ANY_EXISTS;
- token.value.s = yytext;
- token.location = get_location();
- return token;
-}
-
-{left_contains} {
- update_location();
- token.type = AG_TOKEN_LEFT_CONTAINS;
- token.value.s = yytext;
- token.location = get_location();
- return token;
-}
-
-{right_contains} {
- update_location();
- token.type = AG_TOKEN_RIGHT_CONTAINS;
- token.value.s = yytext;
- token.location = get_location();
- return token;
-}
-
-{all_exists} {
- update_location();
- token.type = AG_TOKEN_ALL_EXISTS;
- token.value.s = yytext;
- token.location = get_location();
- return token;
-}
-
{lt_gt} {
update_location();
token.type = AG_TOKEN_LT_GT;
@@ -737,14 +689,6 @@
return token;
}
-{eq_tilde} {
- update_location();
- token.type = AG_TOKEN_EQ_TILDE;
- token.value.s = yytext;
- token.location = get_location();
- return token;
-}
-
{typecast} {
update_location();
token.type = AG_TOKEN_TYPECAST;
@@ -761,6 +705,151 @@
return token;
}
+{op_chars} {
+ update_location();
+ token.type = AG_TOKEN_OP;
+ token.value.s = yytext;
+ token.location = get_location();
+ return token;
+}
+
+{operator} {
+ /* Borrowed from PG and adjusted for our scanner */
+
+ /*
+ * Check for embedded slash-star or slash-slash; those
+ * are comment starts, so operator must stop there.
+ * Note that slash-star or slash-slash at the first
+ * character will match a prior rule, not this one.
+ */
+ int nchars = yyleng;
+ char *slashstar = strstr(yytext, "/*");
+ char *slashslash = strstr(yytext, "//");
+
+ if (slashstar && slashslash)
+ {
+ /* if both appear, take the first one */
+ if (slashstar > slashslash)
+ slashstar = slashslash;
+ }
+ else if (!slashstar)
+ slashstar = slashslash;
+ if (slashstar)
+ nchars = slashstar - yytext;
+
+ /*
+ * For SQL compatibility, '+' and '-' cannot be the
+ * last char of a multi-char operator unless the operator
+ * contains chars that are not in SQL operators.
+ * The idea is to lex '=-' as two operators, but not
+ * to forbid operator names like '?-' that could not be
+ * sequences of SQL operators.
+ */
+ if (nchars > 1 &&
+ (yytext[nchars - 1] == '+' ||
+ yytext[nchars - 1] == '-'))
+ {
+ int ic;
+
+ for (ic = nchars - 2; ic >= 0; ic--)
+ {
+ char c = yytext[ic];
+ if (c == '~' || c == '!' || c == '@' ||
+ c == '#' || c == '^' || c == '&' ||
+ c == '|' || c == '`' || c == '?' ||
+ c == '%')
+ break;
+ }
+ if (ic < 0)
+ {
+ /*
+ * didn't find a qualifying character, so remove
+ * all trailing [+-]
+ */
+ do {
+ nchars--;
+ } while (nchars > 1 &&
+ (yytext[nchars - 1] == '+' ||
+ yytext[nchars - 1] == '-'));
+ }
+ }
+
+ update_location();
+
+ if (nchars < yyleng)
+ {
+ /* Strip the unwanted chars from the token */
+ yyless(nchars);
+ /*
+ * If what we have left is only one char, and it's
+ * one of the characters matching "self", then
+ * return it as a character token the same way
+ * that the "self" rule would have.
+ */
+ if (nchars == 1 &&
+ strchr("%()*+,-./:;<=>[\\]^{|}", yytext[0]))
+ {
+ token.type = AG_TOKEN_CHAR;
+ token.value.c = yytext[0];
+ token.location = get_location();
+ return token;
+ }
+
+ /*
+ * Likewise, if what we have left is two chars, and
+ * those match the tokens ">=", "<=", "=>", "<>" or
+ * "!=", then we must return the appropriate token
+ * rather than the generic Op.
+ */
+ if (nchars == 2)
+ {
+ if (yytext[0] == '>' && yytext[1] == '=')
+ token.type = AG_TOKEN_GT_EQ;
+ else if (yytext[0] == '<' && yytext[1] == '=')
+ token.type = AG_TOKEN_LT_EQ;
+ else if (yytext[0] == '<' && yytext[1] == '>')
+ token.type = AG_TOKEN_LT_GT;
+ else if (yytext[0] == '+' && yytext[1] == '=')
+ token.type = AG_TOKEN_PLUS_EQ;
+ /*
+ * These operators (!=, =>) are not allowed as user-defined
+ * operators in PG because they are reserved as valid tokens
+ * with predefined semantics. As a result, we also reject
+ * them here. However, if a specific use case arises, we
+ * could allow them with custom handling.
+ */
+ else if ((yytext[0] == '!' && yytext[1] == '=') ||
+ (yytext[0] == '=' && yytext[1] == '>'))
+ ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR),
+ scan_errmsg("unexpected character"),
+ scan_errposition()));
+ else
+ goto handle_generic_op;
+
+ token.value.s = yytext;
+ token.location = get_location();
+ return token;
+ }
+ }
+
+handle_generic_op:
+ /*
+ * Complain if operator is too long. Unlike the case
+ * for identifiers, we make this an error not a notice-
+ * and-truncate, because the odds are we are looking at
+ * a syntactic mistake anyway.
+ */
+ if (nchars >= NAMEDATALEN)
+ ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR),
+ scan_errmsg("operator too long"),
+ scan_errposition()));
+
+ token.type = AG_TOKEN_OP;
+ token.value.s = yytext;
+ token.location = get_location();
+ return token;
+}
+
{other} {
update_location();
ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR),
diff --git a/src/backend/parser/cypher_analyze.c b/src/backend/parser/cypher_analyze.c
index 4bba104..d53fba3 100644
--- a/src/backend/parser/cypher_analyze.c
+++ b/src/backend/parser/cypher_analyze.c
@@ -1055,15 +1055,20 @@
TargetEntry *te = lfirst(lt);
Node *expr = (Node *)te->expr;
Oid current_type;
+ int32 current_typmod;
Oid target_type;
+ int32 target_typmod;
Assert(!te->resjunk);
current_type = exprType(expr);
+ current_typmod = exprTypmod(expr);
target_type = lfirst_oid(lc2);
- if (current_type != target_type)
+ target_typmod = lfirst_int(lc3);
+
+ if ((current_type != target_type) ||
+ (current_typmod != target_typmod))
{
- int32 target_typmod = lfirst_int(lc3);
Node *new_expr;
/*
diff --git a/src/backend/parser/cypher_expr.c b/src/backend/parser/cypher_expr.c
index 19bc71d..390bfb3 100644
--- a/src/backend/parser/cypher_expr.c
+++ b/src/backend/parser/cypher_expr.c
@@ -106,16 +106,16 @@
Form_pg_proc procform,
List *fargs,
Oid *target_types);
-static Node *cast_to_target_type(cypher_parsestate *cpstate, Node *expr,
- Oid source_oid, Oid target_oid);
static Node *wrap_text_output_to_agtype(cypher_parsestate *cpstate,
FuncExpr *fexpr);
static Form_pg_proc get_procform(FuncCall *fn, bool err_not_found);
static char *get_mapped_extension(Oid func_oid);
static bool is_extension_external(char *extension);
-static bool is_pgvector_datatype(char *typename);
static char *construct_age_function_name(char *funcname);
static bool function_exists(char *funcname, char *extension);
+static Node *coerce_expr_flexible(ParseState *pstate, Node *expr,
+ Oid source_oid, Oid target_oid,
+ int32 t_typemod, bool error_out);
/* transform a cypher expression */
Node *transform_cypher_expr(cypher_parsestate *cpstate, Node *expr,
@@ -1540,6 +1540,7 @@
List *fname;
FuncCall *fnode;
ParseState *pstate;
+ TypeName *target_typ;
/* verify input parameter */
Assert (cpstate != NULL);
@@ -1548,98 +1549,137 @@
/* create the qualified function name, schema first */
fname = list_make1(makeString("ag_catalog"));
pstate = &cpstate->pstate;
+ target_typ = ctypecast->typname;
+
+ if (list_length(target_typ->names) == 1)
+ {
+ char *typecast = strVal(linitial(target_typ->names));
- /* append the name of the requested typecast function */
- if (pg_strcasecmp(ctypecast->typecast, "edge") == 0)
- {
- fname = lappend(fname, makeString(FUNC_AGTYPE_TYPECAST_EDGE));
+ /* append the name of the requested typecast function */
+ if (pg_strcasecmp(typecast, "edge") == 0)
+ {
+ fname = lappend(fname, makeString(FUNC_AGTYPE_TYPECAST_EDGE));
+ }
+ else if (pg_strcasecmp(typecast, "path") == 0)
+ {
+ fname = lappend(fname, makeString(FUNC_AGTYPE_TYPECAST_PATH));
+ }
+ else if (pg_strcasecmp(typecast, "vertex") == 0)
+ {
+ fname = lappend(fname, makeString(FUNC_AGTYPE_TYPECAST_VERTEX));
+ }
+ else if (pg_strcasecmp(typecast, "numeric") == 0)
+ {
+ fname = lappend(fname, makeString(FUNC_AGTYPE_TYPECAST_NUMERIC));
+ }
+ else if (pg_strcasecmp(typecast, "float") == 0)
+ {
+ fname = lappend(fname, makeString(FUNC_AGTYPE_TYPECAST_FLOAT));
+ }
+ else if (pg_strcasecmp(typecast, "int") == 0 ||
+ pg_strcasecmp(typecast, "integer") == 0)
+ {
+ fname = lappend(fname, makeString(FUNC_AGTYPE_TYPECAST_INT));
+ }
+ else if (pg_strcasecmp(typecast, "pg_float8") == 0)
+ {
+ fname = lappend(fname, makeString(FUNC_AGTYPE_TYPECAST_PG_FLOAT8));
+ }
+ else if (pg_strcasecmp(typecast, "pg_bigint") == 0)
+ {
+ fname = lappend(fname, makeString(FUNC_AGTYPE_TYPECAST_PG_BIGINT));
+ }
+ else if ((pg_strcasecmp(typecast, "bool") == 0 ||
+ pg_strcasecmp(typecast, "boolean") == 0))
+ {
+ fname = lappend(fname, makeString(FUNC_AGTYPE_TYPECAST_BOOL));
+ }
+ else if (pg_strcasecmp(typecast, "pg_text") == 0)
+ {
+ fname = lappend(fname, makeString(FUNC_AGTYPE_TYPECAST_PG_TEXT));
+ }
+ else
+ {
+ goto fallback_coercion;
+ }
+
+ /* make a function call node */
+ fnode = makeFuncCall(fname, list_make1(ctypecast->expr), COERCE_SQL_SYNTAX,
+ ctypecast->location);
+
+ /* return the transformed function */
+ return transform_FuncCall(cpstate, fnode);
}
- else if (pg_strcasecmp(ctypecast->typecast, "path") == 0)
+
+fallback_coercion:
{
- fname = lappend(fname, makeString(FUNC_AGTYPE_TYPECAST_PATH));
- }
- else if (pg_strcasecmp(ctypecast->typecast, "vertex") == 0)
- {
- fname = lappend(fname, makeString(FUNC_AGTYPE_TYPECAST_VERTEX));
- }
- else if (pg_strcasecmp(ctypecast->typecast, "numeric") == 0)
- {
- fname = lappend(fname, makeString(FUNC_AGTYPE_TYPECAST_NUMERIC));
- }
- else if (pg_strcasecmp(ctypecast->typecast, "float") == 0)
- {
- fname = lappend(fname, makeString(FUNC_AGTYPE_TYPECAST_FLOAT));
- }
- else if (pg_strcasecmp(ctypecast->typecast, "int") == 0 ||
- pg_strcasecmp(ctypecast->typecast, "integer") == 0)
- {
- fname = lappend(fname, makeString(FUNC_AGTYPE_TYPECAST_INT));
- }
- else if (pg_strcasecmp(ctypecast->typecast, "pg_float8") == 0)
- {
- fname = lappend(fname, makeString(FUNC_AGTYPE_TYPECAST_PG_FLOAT8));
- }
- else if (pg_strcasecmp(ctypecast->typecast, "pg_bigint") == 0)
- {
- fname = lappend(fname, makeString(FUNC_AGTYPE_TYPECAST_PG_BIGINT));
- }
- else if ((pg_strcasecmp(ctypecast->typecast, "bool") == 0 ||
- pg_strcasecmp(ctypecast->typecast, "boolean") == 0))
- {
- fname = lappend(fname, makeString(FUNC_AGTYPE_TYPECAST_BOOL));
- }
- else if (pg_strcasecmp(ctypecast->typecast, "pg_text") == 0)
- {
- fname = lappend(fname, makeString(FUNC_AGTYPE_TYPECAST_PG_TEXT));
- }
- else if (is_pgvector_datatype(ctypecast->typecast))
- {
- TypeName *target_typname;
Oid source_oid;
Oid target_oid;
+ int32 t_typmod = -1;
Node *expr;
/* transform the expr before casting */
expr = transform_cypher_expr_recurse(cpstate,
ctypecast->expr);
- /* get the source and target oids */
- target_typname = makeTypeNameFromNameList(list_make1(
- makeString(ctypecast->typecast)));
- target_oid = typenameTypeId(pstate, target_typname);
+ typenameTypeIdAndMod(pstate, target_typ, &target_oid, &t_typmod);
source_oid = exprType(expr);
- if (source_oid == AGTYPEOID)
- {
- /*
- * Cast to text and then to target type, since we cant
- * directly cast agtype to pgvector datatypes.
- */
- expr = cast_to_target_type(cpstate, expr, source_oid, TEXTOID);
- expr = cast_to_target_type(cpstate, expr, TEXTOID, target_oid);
- }
- else
- {
- /* try a direct cast, it will error out if not possible */
- expr = cast_to_target_type(cpstate, expr, source_oid, target_oid);
- }
+ /* errors out if cast not possible */
+ expr = coerce_expr_flexible(pstate, expr, source_oid, target_oid,
+ t_typmod, true);
return expr;
}
- /* if none was found, error out */
- else
+}
+
+/*
+ * Helper function to coerce an expression to the target type. If
+ * no direct cast exists, it attempts to cast through text if the
+ * source or target type is agtype. This improves interoperability
+ * with types from other extensions.
+ */
+static Node *coerce_expr_flexible(ParseState *pstate, Node *expr,
+ Oid source_oid, Oid target_oid,
+ int32 t_typmod, bool error_out)
+{
+ const Oid text_oid = TEXTOID;
+ Node *result;
+
+ if (expr == NULL)
+ return NULL;
+
+ /* Try a direct cast */
+ result = coerce_to_target_type(pstate, expr, source_oid, target_oid,
+ t_typmod, COERCION_EXPLICIT,
+ COERCE_EXPLICIT_CAST, -1);
+ if (result != NULL)
+ return result;
+
+ /* Try cast via TEXT if either side is AGTYPE */
+ if (source_oid == AGTYPEOID || target_oid == AGTYPEOID)
+ {
+ Node *to_text = coerce_to_target_type(pstate, expr, source_oid, text_oid,
+ -1, COERCION_EXPLICIT,
+ COERCE_EXPLICIT_CAST, -1);
+ if (to_text != NULL)
+ {
+ result = coerce_to_target_type(pstate, to_text, text_oid, target_oid,
+ t_typmod, COERCION_EXPLICIT,
+ COERCE_EXPLICIT_CAST, -1);
+ if (result != NULL)
+ return result;
+ }
+ }
+
+ if (error_out)
{
ereport(ERROR,
(errmsg_internal("typecast \'%s\' not supported",
- ctypecast->typecast)));
+ format_type_be(target_oid))));
}
- /* make a function call node */
- fnode = makeFuncCall(fname, list_make1(ctypecast->expr), COERCE_SQL_SYNTAX,
- ctypecast->location);
-
- /* return the transformed function */
- return transform_FuncCall(cpstate, fnode);
+ return NULL;
}
static Node *transform_external_ext_FuncCall(cypher_parsestate *cpstate,
@@ -1704,7 +1744,6 @@
char *funcname = NameStr(procform->proname);
int nargs = procform->pronargs;
ListCell *lc = NULL;
- int i = 0;
/* verify the length of args are same */
if (list_length(fargs) != nargs)
@@ -1718,68 +1757,21 @@
/* iterate through the function's args */
foreach (lc, fargs)
{
- char *target_typname;
Node *expr = lfirst(lc);
Oid source_oid = exprType(expr);
- Oid target_oid = target_types[i];
+ Oid target_oid = target_types[foreach_current_index(lc)];
- /* get the typename from target_oid */
- target_typname = format_type_be(target_oid);
-
- /* cast the agtype to the target type */
- if (source_oid == AGTYPEOID && is_pgvector_datatype(target_typname))
- {
- /*
- * There is no cast from agtype to vector, so we first
- * cast agtype to text and then text to vector.
- */
- expr = cast_to_target_type(cpstate, expr, source_oid, TEXTOID);
- expr = cast_to_target_type(cpstate, expr, TEXTOID, target_oid);
- }
- /* additional casts can be added here for other types */
- else
- {
- /* try a direct cast, it will error out if not possible */
- expr = cast_to_target_type(cpstate, expr, source_oid, target_oid);
- }
+ /* errors out if cast not possible */
+ expr = coerce_expr_flexible(&cpstate->pstate, expr, source_oid,
+ target_oid, -1, true);
lfirst(lc) = expr;
- i++;
}
return fargs;
}
/*
- * Cast an input type to an output type, error out if not possible.
- * Thanks to Taha for this idea.
- */
-static Node *cast_to_target_type(cypher_parsestate *cpstate, Node *expr,
- Oid source_oid, Oid target_oid)
-{
- ParseState *pstate = &cpstate->pstate;
-
- /* can we cast from source to target oid? */
- if (can_coerce_type(1, &source_oid, &target_oid, COERCION_EXPLICIT))
- {
- /* coerce the source to the target */
- expr = coerce_type(pstate, expr, source_oid, target_oid, -1,
- COERCION_EXPLICIT, COERCE_EXPLICIT_CAST, -1);
- }
- /* error out if we can't cast */
- else
- {
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_FUNCTION),
- errmsg("cannot cast type %s to %s", format_type_be(source_oid),
- format_type_be(target_oid))));
- }
-
- /* return the casted expression */
- return expr;
-}
-
-/*
* Due to issues with creating a cast from text to agtype, we need to wrap a
* function that outputs text with text_to_agtype.
*/
@@ -1912,13 +1904,6 @@
(pg_strcasecmp(extension, "age") != 0));
}
-static bool is_pgvector_datatype(char *typename)
-{
- return (pg_strcasecmp(typename, "vector") ||
- pg_strcasecmp(typename, "halfvec") ||
- pg_strcasecmp(typename, "sparsevec"));
-}
-
/* Returns age_ prefiexed lower case function name */
static char *construct_age_function_name(char *funcname)
{
diff --git a/src/backend/parser/cypher_gram.y b/src/backend/parser/cypher_gram.y
index a4d8f0f..0bafefe 100644
--- a/src/backend/parser/cypher_gram.y
+++ b/src/backend/parser/cypher_gram.y
@@ -69,11 +69,11 @@
%token <string> IDENTIFIER
%token <string> PARAMETER
%token <string> BQIDENT
+%token <string> OP
%token <character> CHAR
/* operators that have more than 1 character */
-%token NOT_EQ LT_EQ GT_EQ DOT_DOT TYPECAST PLUS_EQ EQ_TILDE CONCAT
-%token ACCESS_PATH LEFT_CONTAINS RIGHT_CONTAINS ANY_EXISTS ALL_EXISTS
+%token NOT_EQ LT_EQ GT_EQ DOT_DOT TYPECAST PLUS_EQ
/* keywords in alphabetical order */
%token <keyword> ALL ANALYZE AND AS ASC ASCENDING
@@ -86,7 +86,7 @@
LIMIT
MATCH MERGE
NOT NULL_P
- OPTIONAL OR ORDER
+ OPERATOR OPTIONAL OR ORDER
REMOVE RETURN
SET SKIP STARTS
THEN TRUE_P
@@ -168,10 +168,18 @@
/* names */
%type <string> property_key_name var_name var_name_opt label_name
-%type <string> symbolic_name schema_name
+%type <string> symbolic_name schema_name type_name
%type <keyword> reserved_keyword safe_keywords conflicted_keywords
%type <list> func_name
+/* types */
+%type <node> generic_type
+%type <list> opt_type_modifiers
+
+/* operator */
+%type <string> all_op math_op
+%type <list> qual_op any_operator
+
/* precedence: lowest to highest */
%left UNION
%left OR
@@ -179,8 +187,8 @@
%left XOR
%right NOT
%left '=' NOT_EQ '<' LT_EQ '>' GT_EQ
-%left '@' '|' '&' '?' LEFT_CONTAINS RIGHT_CONTAINS ANY_EXISTS ALL_EXISTS
-%left '+' '-' CONCAT
+%left '+' '-'
+%left OP OPERATOR
%left '*' '/' '%'
%left '^'
%nonassoc IN IS
@@ -235,7 +243,7 @@
static Node *make_null_const(int location);
/* typecast */
-static Node *make_typecast_expr(Node *expr, char *typecast, int location);
+static Node *make_typecast_expr(Node *expr, Node *typname, int location);
/* functions */
static Node *make_function_expr(List *func_name, List *exprs, int location);
@@ -499,7 +507,6 @@
}
;
-
semicolon_opt:
/* empty */
| ';'
@@ -1562,33 +1569,13 @@
{
$$ = build_comparison_expression($1, $3, ">=", @2);
}
- | expr LEFT_CONTAINS expr
+ | expr qual_op expr %prec OP
{
- $$ = (Node *)makeSimpleA_Expr(AEXPR_OP, "<@", $1, $3, @2);
+ $$ = (Node *) makeA_Expr(AEXPR_OP, $2, $1, $3, @2);
}
- | expr RIGHT_CONTAINS expr
+ | qual_op expr %prec OP
{
- $$ = (Node *)makeSimpleA_Expr(AEXPR_OP, "@>", $1, $3, @2);
- }
- | expr '?' expr %prec '.'
- {
- $$ = (Node *)makeSimpleA_Expr(AEXPR_OP, "?", $1, $3, @2);
- }
- | expr ANY_EXISTS expr
- {
- $$ = (Node *)makeSimpleA_Expr(AEXPR_OP, "?|", $1, $3, @2);
- }
- | expr ALL_EXISTS expr
- {
- $$ = (Node *)makeSimpleA_Expr(AEXPR_OP, "?&", $1, $3, @2);
- }
- | expr CONCAT expr
- {
- $$ = (Node *)makeSimpleA_Expr(AEXPR_OP, "||", $1, $3, @2);
- }
- | expr ACCESS_PATH expr
- {
- $$ = (Node *)makeSimpleA_Expr(AEXPR_OP, "#>", $1, $3, @2);
+ $$ = (Node *) makeA_Expr(AEXPR_OP, $1, NULL, $2, @1);
}
| expr '+' expr
{
@@ -1680,11 +1667,6 @@
$$ = (Node *)n;
}
- | expr EQ_TILDE expr
- {
- $$ = make_function_expr(list_make1(makeString("eq_tilde")),
- list_make2($1, $3), @2);
- }
| expr '[' expr ']'
{
A_Indices *i;
@@ -1775,6 +1757,31 @@
$$ = append_indirection($1, (Node*)string);
}
+ /* allow indirection with a typecast */
+ else if ((IsA($1, ColumnRef) || IsA($1, A_Indirection)) &&
+ (IsA($3, ExtensibleNode) &&
+ is_ag_node($3, cypher_typecast)))
+ {
+ cypher_typecast *tc = (cypher_typecast *)$3;
+
+ if (IsA(tc->expr, ColumnRef))
+ {
+ ColumnRef *cr = (ColumnRef *)tc->expr;
+ List *fields = cr->fields;
+ String *string = linitial(fields);
+
+ tc->expr = append_indirection($1, (Node *)string);
+
+ $$ = (Node *)tc;
+ }
+ else
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("invalid indirection syntax"),
+ ag_scanner_errposition(@1, scanner)));
+ }
+ }
else if (IsA($1, FuncCall) && IsA($3, A_Indirection))
{
ereport(ERROR,
@@ -1798,7 +1805,7 @@
{
$$ = (Node *)makeSimpleA_Expr(AEXPR_OP, "->", $1, $4, @2);
}
- | expr TYPECAST symbolic_name
+ | expr TYPECAST generic_type
{
$$ = make_typecast_expr($1, $3, @2);
}
@@ -2338,6 +2345,10 @@
schema_name
;
+type_name:
+ schema_name
+ ;
+
symbolic_name:
IDENTIFIER
;
@@ -2357,6 +2368,82 @@
;
/*
+ * types
+ */
+generic_type:
+ type_name opt_type_modifiers
+ {
+ TypeName *typname;
+
+ typname = makeTypeName($1);
+ typname->typmods = $2;
+ typname->location = @1;
+
+ $$ = (Node *) typname;
+ }
+ ;
+
+opt_type_modifiers:
+ '(' expr_list ')'
+ {
+ $$ = $2;
+ }
+ | /* empty */
+ {
+ $$ = NIL;
+ }
+ ;
+
+/*
+ * operators
+ */
+any_operator:
+ all_op
+ {
+ $$ = list_make1(makeString($1));
+ }
+ | symbolic_name
+ {
+ $$ = list_make1(makeString($1));
+ }
+ | schema_name '.' any_operator
+ {
+ $$ = lcons(makeString($1), $3);
+ }
+ ;
+
+all_op:
+ OP
+ | math_op
+ ;
+
+math_op:
+ '+' { $$ = "+"; }
+ | '-' { $$ = "-"; }
+ | '*' { $$ = "*"; }
+ | '/' { $$ = "/"; }
+ | '%' { $$ = "%"; }
+ | '^' { $$ = "^"; }
+ | '<' { $$ = "<"; }
+ | '>' { $$ = ">"; }
+ | '=' { $$ = "="; }
+ | LT_EQ { $$ = "<="; }
+ | GT_EQ { $$ = ">="; }
+ | NOT_EQ { $$ = "<>"; }
+ ;
+
+qual_op:
+ OP
+ {
+ $$ = list_make1(makeString($1));
+ }
+ | OPERATOR '(' any_operator ')'
+ {
+ $$ = $3;
+ }
+ ;
+
+/*
* All keywords need to be copied and properly terminated with a null before
* using them, pnstrdup effectively does this for us.
*/
@@ -2390,6 +2477,7 @@
| MATCH { $$ = pnstrdup($1, 6); }
| MERGE { $$ = pnstrdup($1, 6); }
| NOT { $$ = pnstrdup($1, 3); }
+ | OPERATOR { $$ = pnstrdup($1, 8); }
| OPTIONAL { $$ = pnstrdup($1, 8); }
| OR { $$ = pnstrdup($1, 2); }
| ORDER { $$ = pnstrdup($1, 5); }
@@ -2658,13 +2746,13 @@
/*
* typecast
*/
-static Node *make_typecast_expr(Node *expr, char *typecast, int location)
+static Node *make_typecast_expr(Node *expr, Node *typname, int location)
{
cypher_typecast *node;
node = make_ag_node(cypher_typecast);
node->expr = expr;
- node->typecast = typecast;
+ node->typname = (TypeName *) typname;
node->location = location;
return (Node *)node;
diff --git a/src/backend/parser/cypher_parser.c b/src/backend/parser/cypher_parser.c
index d2b64ff..ebf46b4 100644
--- a/src/backend/parser/cypher_parser.c
+++ b/src/backend/parser/cypher_parser.c
@@ -44,15 +44,9 @@
DOT_DOT,
TYPECAST,
PLUS_EQ,
- EQ_TILDE,
- LEFT_CONTAINS,
- RIGHT_CONTAINS,
- ACCESS_PATH,
- ANY_EXISTS,
- ALL_EXISTS,
- CONCAT,
CHAR,
- BQIDENT
+ BQIDENT,
+ OP
};
ag_token token;
@@ -68,6 +62,7 @@
break;
case AG_TOKEN_DECIMAL:
case AG_TOKEN_STRING:
+ case AG_TOKEN_OP:
lvalp->string = pstrdup(token.value.s);
break;
case AG_TOKEN_IDENTIFIER:
@@ -115,14 +110,6 @@
case AG_TOKEN_GT_EQ:
case AG_TOKEN_DOT_DOT:
case AG_TOKEN_PLUS_EQ:
- case AG_TOKEN_EQ_TILDE:
- case AG_TOKEN_ACCESS_PATH:
- case AG_TOKEN_ALL_EXISTS:
- case AG_TOKEN_ANY_EXISTS:
- case AG_TOKEN_LEFT_CONTAINS:
- case AG_TOKEN_RIGHT_CONTAINS:
- case AG_TOKEN_CONCAT:
- break;
case AG_TOKEN_TYPECAST:
break;
case AG_TOKEN_CHAR:
diff --git a/src/backend/utils/adt/agtype.c b/src/backend/utils/adt/agtype.c
index 86f41f2..d26929d 100644
--- a/src/backend/utils/adt/agtype.c
+++ b/src/backend/utils/adt/agtype.c
@@ -1116,7 +1116,10 @@
}
/*
- * common worker for above two functions
+ * Common worker for above two functions.
+ * If extend is set to true, the function will append
+ * ::vertex, ::edge or ::path based on the type of
+ * container.
*/
static char *agtype_to_cstring_worker(StringInfo out, agtype_container *in,
int estimated_len, bool indent,
@@ -3207,9 +3210,10 @@
/* check that we have a scalar value */
if (!AGT_ROOT_IS_SCALAR(arg_agt))
{
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
- errmsg("agtype argument must resolve to a scalar value")));
+ char *out;
+ out = agtype_to_cstring(NULL, &arg_agt->root, VARSIZE(arg_agt));
+ PG_FREE_IF_COPY(arg_agt, 0);
+ return CStringGetTextDatum(out);
}
/* get the arg parameter */
diff --git a/src/include/catalog/ag_graph.h b/src/include/catalog/ag_graph.h
index 4329545..f86efd8 100644
--- a/src/include/catalog/ag_graph.h
+++ b/src/include/catalog/ag_graph.h
@@ -39,6 +39,7 @@
uint32 get_graph_oid(const char *graph_name);
char *get_graph_namespace_name(const char *graph_name);
+bool graph_namespace_exists(Oid graph_oid);
List *get_graphnames(void);
void drop_graphs(List *graphnames);
diff --git a/src/include/nodes/cypher_nodes.h b/src/include/nodes/cypher_nodes.h
index f252701..db47eb3 100644
--- a/src/include/nodes/cypher_nodes.h
+++ b/src/include/nodes/cypher_nodes.h
@@ -484,7 +484,7 @@
{
ExtensibleNode extensible;
Node *expr;
- char *typecast;
+ TypeName *typname;
int location;
} cypher_typecast;
diff --git a/src/include/parser/ag_scanner.h b/src/include/parser/ag_scanner.h
index 3dd89ab..4cef632 100644
--- a/src/include/parser/ag_scanner.h
+++ b/src/include/parser/ag_scanner.h
@@ -45,15 +45,9 @@
AG_TOKEN_DOT_DOT,
AG_TOKEN_TYPECAST,
AG_TOKEN_PLUS_EQ,
- AG_TOKEN_EQ_TILDE,
- AG_TOKEN_LEFT_CONTAINS,
- AG_TOKEN_RIGHT_CONTAINS,
- AG_TOKEN_ACCESS_PATH,
- AG_TOKEN_ANY_EXISTS,
- AG_TOKEN_ALL_EXISTS,
- AG_TOKEN_CONCAT,
AG_TOKEN_CHAR,
- AG_TOKEN_BQIDENT
+ AG_TOKEN_BQIDENT,
+ AG_TOKEN_OP
} ag_token_type;
/*
diff --git a/src/include/parser/cypher_kwlist.h b/src/include/parser/cypher_kwlist.h
index ce48f28..e4c4437 100644
--- a/src/include/parser/cypher_kwlist.h
+++ b/src/include/parser/cypher_kwlist.h
@@ -29,6 +29,7 @@
PG_KEYWORD("merge", MERGE, RESERVED_KEYWORD)
PG_KEYWORD("not", NOT, RESERVED_KEYWORD)
PG_KEYWORD("null", NULL_P, RESERVED_KEYWORD)
+PG_KEYWORD("operator", OPERATOR, RESERVED_KEYWORD)
PG_KEYWORD("optional", OPTIONAL, RESERVED_KEYWORD)
PG_KEYWORD("or", OR, RESERVED_KEYWORD)
PG_KEYWORD("order", ORDER, RESERVED_KEYWORD)