This is an automated email from the ASF dual-hosted git repository.

jgemignani pushed a commit to branch PG14
in repository https://gitbox.apache.org/repos/asf/age.git


The following commit(s) were added to refs/heads/PG14 by this push:
     new 6f316221 Add concat || operator to agtype (#1213)
6f316221 is described below

commit 6f316221b1fcc04cfa22936e7b371424206fb1d3
Author: Zainab Saad <105385638+zainab-s...@users.noreply.github.com>
AuthorDate: Fri Sep 8 03:42:23 2023 +0500

    Add concat || operator to agtype (#1213)
    
    - Implement the concatenation operator for agtype as operands, similar
       to the one in postgres where this operator works with jsonb operands
    
     - Allow using concat operator inside cypher queries
    
     - Add jsonb_operators.sql and jsonb_operators.out files
       for containing regression tests related to jsonb operators,
       i.e., (?,?|,?&,->,->>,#>,#>>,||)
---
 Makefile                             |   1 +
 age--1.4.0.sql                       |  14 +
 regress/expected/jsonb_operators.out | 577 +++++++++++++++++++++++++++++++++++
 regress/sql/jsonb_operators.sql      | 159 ++++++++++
 src/backend/parser/ag_scanner.l      |   9 +
 src/backend/parser/cypher_gram.y     |   8 +-
 src/backend/parser/cypher_parser.c   |   4 +-
 src/backend/utils/adt/agtype_ops.c   | 167 ++++++++--
 src/include/parser/ag_scanner.h      |   3 +-
 9 files changed, 912 insertions(+), 30 deletions(-)

diff --git a/Makefile b/Makefile
index ab98665e..fbd3f3b8 100644
--- a/Makefile
+++ b/Makefile
@@ -99,6 +99,7 @@ REGRESS = scan \
           analyze \
           graph_generation \
           name_validation \
+          jsonb_operators \
           drop
 
 srcdir=`pwd`
diff --git a/age--1.4.0.sql b/age--1.4.0.sql
index 3a911627..213b397f 100644
--- a/age--1.4.0.sql
+++ b/age--1.4.0.sql
@@ -1391,6 +1391,20 @@ CREATE OPERATOR ^ (
   RIGHTARG = agtype
 );
 
+CREATE FUNCTION ag_catalog.agtype_concat(agtype, agtype)
+RETURNS agtype
+LANGUAGE c
+STABLE
+RETURNS NULL ON NULL INPUT
+PARALLEL SAFE
+AS 'MODULE_PATHNAME';
+
+CREATE OPERATOR || (
+  FUNCTION = ag_catalog.agtype_concat,
+  LEFTARG = agtype,
+  RIGHTARG = agtype
+);
+
 CREATE FUNCTION ag_catalog.graphid_hash_cmp(graphid)
 RETURNS INTEGER
 LANGUAGE c
diff --git a/regress/expected/jsonb_operators.out 
b/regress/expected/jsonb_operators.out
new file mode 100644
index 00000000..71e4fe1a
--- /dev/null
+++ b/regress/expected/jsonb_operators.out
@@ -0,0 +1,577 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+LOAD 'age';
+SET search_path TO ag_catalog;
+--
+-- jsonb operators in AGE (?, ?&, ?|, ->, ->>, #>, #>>, ||)
+--
+--
+-- concat || operator
+--
+SELECT i, pg_typeof(i) FROM (SELECT '[0, 1]'::agtype || '[0, 1]'::agtype as i) 
a;
+      i       | pg_typeof 
+--------------+-----------
+ [0, 1, 0, 1] | agtype
+(1 row)
+
+SELECT i, pg_typeof(i) FROM (SELECT '2'::agtype || '[0, 1]'::agtype as i) a;
+     i     | pg_typeof 
+-----------+-----------
+ [2, 0, 1] | agtype
+(1 row)
+
+SELECT i, pg_typeof(i) FROM (SELECT '[0, 1]'::agtype || '2'::agtype as i) a;
+     i     | pg_typeof 
+-----------+-----------
+ [0, 1, 2] | agtype
+(1 row)
+
+SELECT i, pg_typeof(i) FROM (SELECT '{"a": 1}'::agtype || '[0, 1]'::agtype as 
i) a;
+        i         | pg_typeof 
+------------------+-----------
+ [{"a": 1}, 0, 1] | agtype
+(1 row)
+
+SELECT i, pg_typeof(i) FROM (SELECT '[0, 1]'::agtype || '{"a": 1}'::agtype as 
i) a;
+        i         | pg_typeof 
+------------------+-----------
+ [0, 1, {"a": 1}] | agtype
+(1 row)
+
+SELECT i, pg_typeof(i) FROM (SELECT '[]'::agtype || '[0, 1]'::agtype as i) a;
+   i    | pg_typeof 
+--------+-----------
+ [0, 1] | agtype
+(1 row)
+
+SELECT i, pg_typeof(i) FROM (SELECT '[0, 1]'::agtype || '[]'::agtype as i) a;
+   i    | pg_typeof 
+--------+-----------
+ [0, 1] | agtype
+(1 row)
+
+SELECT i, pg_typeof(i) FROM (SELECT 'null'::agtype || '[0, 1]'::agtype as i) a;
+      i       | pg_typeof 
+--------------+-----------
+ [null, 0, 1] | agtype
+(1 row)
+
+SELECT i, pg_typeof(i) FROM (SELECT '[0, 1]'::agtype || 'null'::agtype as i) a;
+      i       | pg_typeof 
+--------------+-----------
+ [0, 1, null] | agtype
+(1 row)
+
+SELECT i, pg_typeof(i) FROM (SELECT '[null]'::agtype || '[0, 1]'::agtype as i) 
a;
+      i       | pg_typeof 
+--------------+-----------
+ [null, 0, 1] | agtype
+(1 row)
+
+SELECT i, pg_typeof(i) FROM (SELECT '[0, 1]'::agtype || '[null]'::agtype as i) 
a;
+      i       | pg_typeof 
+--------------+-----------
+ [0, 1, null] | agtype
+(1 row)
+
+SELECT i, pg_typeof(i) FROM (SELECT NULL || '[0, 1]'::agtype as i) a;
+ i | pg_typeof 
+---+-----------
+   | agtype
+(1 row)
+
+SELECT i, pg_typeof(i) FROM (SELECT '[0, 1]'::agtype || NULL as i) a;
+ i | pg_typeof 
+---+-----------
+   | agtype
+(1 row)
+
+-- both operands are objects
+SELECT '{"aa":1 , "b":2, "cq":3}'::agtype || '{"cq":"l", "b":"g", "fg":false}';
+                  ?column?                   
+---------------------------------------------
+ {"b": "g", "aa": 1, "cq": "l", "fg": false}
+(1 row)
+
+SELECT '{"aa":1 , "b":2, "cq":3}'::agtype || '{"aq":"l"}';
+               ?column?                
+---------------------------------------
+ {"b": 2, "aa": 1, "aq": "l", "cq": 3}
+(1 row)
+
+SELECT '{"aa":1 , "b":2, "cq":3}'::agtype || '{"aa":"l"}';
+           ?column?           
+------------------------------
+ {"b": 2, "aa": "l", "cq": 3}
+(1 row)
+
+SELECT '{"aa":1 , "b":2, "cq":3}'::agtype || '{}';
+          ?column?          
+----------------------------
+ {"b": 2, "aa": 1, "cq": 3}
+(1 row)
+
+SELECT '{"aa":1 , "b":2, "cq":3, "cj": {"fg": true}}'::agtype || '{"cq":"l", 
"b":"g", "fg":false}';
+                            ?column?                             
+-----------------------------------------------------------------
+ {"b": "g", "aa": 1, "cj": {"fg": true}, "cq": "l", "fg": false}
+(1 row)
+
+SELECT '{"a": 13}'::agtype || '{"a": 13}'::agtype;
+ ?column?  
+-----------
+ {"a": 13}
+(1 row)
+
+SELECT '{}'::agtype || '{"a":"b"}'::agtype;
+  ?column?  
+------------
+ {"a": "b"}
+(1 row)
+
+SELECT '{}'::agtype || '{}'::agtype;
+ ?column? 
+----------
+ {}
+(1 row)
+
+-- both operands are arrays
+SELECT '["a", "b"]'::agtype || '["c"]';
+    ?column?     
+-----------------
+ ["a", "b", "c"]
+(1 row)
+
+SELECT '["a", "b"]'::agtype || '["c", "d"]';
+       ?column?       
+----------------------
+ ["a", "b", "c", "d"]
+(1 row)
+
+SELECT '["a", "b"]'::agtype || '["c", "d", "d"]';
+         ?column?          
+---------------------------
+ ["a", "b", "c", "d", "d"]
+(1 row)
+
+SELECT '["c"]' || '["a", "b"]'::agtype;
+    ?column?     
+-----------------
+ ["c", "a", "b"]
+(1 row)
+
+SELECT '[]'::agtype || '["a"]'::agtype;
+ ?column? 
+----------
+ ["a"]
+(1 row)
+
+SELECT '[]'::agtype || '[]'::agtype;
+ ?column? 
+----------
+ []
+(1 row)
+
+SELECT '["a", "b"]'::agtype || '"c"';
+    ?column?     
+-----------------
+ ["a", "b", "c"]
+(1 row)
+
+SELECT '"c"' || '["a", "b"]'::agtype;
+    ?column?     
+-----------------
+ ["c", "a", "b"]
+(1 row)
+
+SELECT '[]'::agtype || '"a"'::agtype;
+ ?column? 
+----------
+ ["a"]
+(1 row)
+
+SELECT '"b"'::agtype || '"a"'::agtype;
+  ?column?  
+------------
+ ["b", "a"]
+(1 row)
+
+SELECT '3'::agtype || '[]'::agtype;
+ ?column? 
+----------
+ [3]
+(1 row)
+
+SELECT '3'::agtype || '4'::agtype;
+ ?column? 
+----------
+ [3, 4]
+(1 row)
+
+SELECT '3'::agtype || '[4]';
+ ?column? 
+----------
+ [3, 4]
+(1 row)
+
+SELECT '3::numeric'::agtype || '[[]]'::agtype;
+     ?column?     
+------------------
+ [3::numeric, []]
+(1 row)
+
+SELECT null::agtype || null::agtype;
+ ?column? 
+----------
+ 
+(1 row)
+
+-- array and object as operands
+SELECT '{"aa":1 , "b":2, "cq":3}'::agtype || '[{"aa":"l"}]';
+                 ?column?                  
+-------------------------------------------
+ [{"b": 2, "aa": 1, "cq": 3}, {"aa": "l"}]
+(1 row)
+
+SELECT '{"aa":1 , "b":2, "cq":3}'::agtype || '[{"aa":"l", "aa": "k"}]';
+                 ?column?                  
+-------------------------------------------
+ [{"b": 2, "aa": 1, "cq": 3}, {"aa": "k"}]
+(1 row)
+
+SELECT '{"a": 13}'::agtype || '[{"a": 13}]'::agtype;
+        ?column?        
+------------------------
+ [{"a": 13}, {"a": 13}]
+(1 row)
+
+SELECT '[]'::agtype || '{"a":"b"}'::agtype;
+   ?column?   
+--------------
+ [{"a": "b"}]
+(1 row)
+
+SELECT '{"a":"b"}'::agtype || '[]'::agtype;
+   ?column?   
+--------------
+ [{"a": "b"}]
+(1 row)
+
+SELECT '[]'::agtype || '{}'::agtype;
+ ?column? 
+----------
+ [{}]
+(1 row)
+
+SELECT '[3]'::agtype || '{}'::agtype;
+ ?column? 
+----------
+ [3, {}]
+(1 row)
+
+SELECT '{}'::agtype || '[null]'::agtype;
+  ?column?  
+------------
+ [{}, null]
+(1 row)
+
+SELECT '[null]'::agtype || '{"a": null}'::agtype;
+      ?column?       
+---------------------
+ [null, {"a": null}]
+(1 row)
+
+SELECT '""'::agtype || '[]'::agtype;
+ ?column? 
+----------
+ [""]
+(1 row)
+
+-- vertex/edge/path as operand(s)
+SELECT '{"id": 1688849860263937, "label": "EDGE", "end_id": 1970324836974593, 
"start_id": 1407374883553281, "properties": {"a": "xyz", "b" : true, "c": 
-19.888, "e": {"f": "abcdef", "g": {}, "h": [[], {}]}, "i": {"j": 199, "k": 
{"l": "mnopq"}}}}::edge'::agtype || '"id"';
+                                                                               
                                           ?column?                             
                                                                                
             
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ [{"id": 1688849860263937, "label": "EDGE", "end_id": 1970324836974593, 
"start_id": 1407374883553281, "properties": {"a": "xyz", "b": true, "c": 
-19.888, "e": {"f": "abcdef", "g": {}, "h": [[], {}]}, "i": {"j": 199, "k": 
{"l": "mnopq"}}}}::edge, "id"]
+(1 row)
+
+SELECT '{"id": 1688849860263937, "label": "EDGE", "end_id": 1970324836974593, 
"start_id": 1407374883553281, "properties": {"a": "xyz", "b" : true, "c": 
-19.888, "e": {"f": "abcdef", "g": {}, "h": [[], {}]}, "i": {"j": 199, "k": 
{"l": "mnopq"}}}}::edge'::agtype || '"m"';
+                                                                               
                                          ?column?                              
                                                                                
            
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ [{"id": 1688849860263937, "label": "EDGE", "end_id": 1970324836974593, 
"start_id": 1407374883553281, "properties": {"a": "xyz", "b": true, "c": 
-19.888, "e": {"f": "abcdef", "g": {}, "h": [[], {}]}, "i": {"j": 199, "k": 
{"l": "mnopq"}}}}::edge, "m"]
+(1 row)
+
+SELECT '{"id": 1688849860263937, "label": "EDGE", "end_id": 1970324836974593, 
"start_id": 1407374883553281, "properties": {"a": "xyz", "b" : true, "c": 
-19.888, "e": {"f": "abcdef", "g": {}, "h": [[], {}]}, "i": {"j": 199, "k": 
{"l": "mnopq"}}}}::edge'::agtype || '{"m": []}';
+                                                                               
                                             ?column?                           
                                                                                
                  
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ [{"id": 1688849860263937, "label": "EDGE", "end_id": 1970324836974593, 
"start_id": 1407374883553281, "properties": {"a": "xyz", "b": true, "c": 
-19.888, "e": {"f": "abcdef", "g": {}, "h": [[], {}]}, "i": {"j": 199, "k": 
{"l": "mnopq"}}}}::edge, {"m": []}]
+(1 row)
+
+SELECT '{"id": 844424930131969, "label": "v", "properties": 
{}}::vertex'::agtype || '{"id": 844424930131971, "label": "v", "properties": 
{"key": "value"}}::vertex'::agtype;
+                                                                     ?column?  
                                                                   
+--------------------------------------------------------------------------------------------------------------------------------------------------
+ [{"id": 844424930131969, "label": "v", "properties": {}}::vertex, {"id": 
844424930131971, "label": "v", "properties": {"key": "value"}}::vertex]
+(1 row)
+
+SELECT '{"id": 844424930131969, "label": "v", "properties": 
{}}::vertex'::agtype || '[]'::agtype;
+                             ?column?                              
+-------------------------------------------------------------------
+ [{"id": 844424930131969, "label": "v", "properties": {}}::vertex]
+(1 row)
+
+SELECT '{"id": 844424930131969, "label": "v", "properties": 
{}}::vertex'::agtype || '{}'::agtype;
+                               ?column?                                
+-----------------------------------------------------------------------
+ [{"id": 844424930131969, "label": "v", "properties": {}}::vertex, {}]
+(1 row)
+
+SELECT '{}'::agtype || '{"id": 844424930131969, "label": "v", "properties": 
{}}::vertex'::agtype;
+                               ?column?                                
+-----------------------------------------------------------------------
+ [{}, {"id": 844424930131969, "label": "v", "properties": {}}::vertex]
+(1 row)
+
+SELECT '"id"'::agtype || '{"id": 844424930131969, "label": "v", "properties": 
{}}::vertex'::agtype;
+                                ?column?                                 
+-------------------------------------------------------------------------
+ ["id", {"id": 844424930131969, "label": "v", "properties": {}}::vertex]
+(1 row)
+
+SELECT '{"id": 844424930131969, "label": "v", "properties": 
{}}::vertex'::agtype || '{"id": 1688849860263950, "label": "e_var", "end_id": 
281474976710662, "start_id": 281474976710661, "properties": {}}::edge'::agtype;
+                                                                               
            ?column?                                                            
                                
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ [{"id": 844424930131969, "label": "v", "properties": {}}::vertex, {"id": 
1688849860263950, "label": "e_var", "end_id": 281474976710662, "start_id": 
281474976710661, "properties": {}}::edge]
+(1 row)
+
+SELECT '[{"id": 281474976710672, "label": "", "properties": {}}::vertex, 
{"id": 1688849860263960, "label": "e_var", "end_id": 281474976710673, 
"start_id": 281474976710672, "properties": {}}::edge, {"id": 281474976710673, 
"label": "", "properties": {}}::vertex]::path'::agtype || '{"id": 
844424930131969, "label": "v", "properties": {}}::vertex'::agtype;
+                                                                               
                                                                                
?column?                                                                        
                                                                                
        
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ [[{"id": 281474976710672, "label": "", "properties": {}}::vertex, {"id": 
1688849860263960, "label": "e_var", "end_id": 281474976710673, "start_id": 
281474976710672, "properties": {}}::edge, {"id": 281474976710673, "label": "", 
"properties": {}}::vertex]::path, {"id": 844424930131969, "label": "v", 
"properties": {}}::vertex]
+(1 row)
+
+SELECT '[{"id": 281474976710672, "label": "", "properties": {}}::vertex, 
{"id": 1688849860263960, "label": "e_var", "end_id": 281474976710673, 
"start_id": 281474976710672, "properties": {}}::edge, {"id": 281474976710673, 
"label": "", "properties": {}}::vertex]::path'::agtype || '[{"id": 
281474976710672, "label": "", "properties": {}}::vertex, {"id": 
1688849860263960, "label": "e_var", "end_id": 281474976710673, "start_id": 
281474976710672, "properties": {}}::edge, {"id": 281474976710673, [...]
+                                                                               
                                                                                
                                                                                
                  ?column?                                                      
                                                                                
                                                                                
              [...]
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 [...]
+ [[{"id": 281474976710672, "label": "", "properties": {}}::vertex, {"id": 
1688849860263960, "label": "e_var", "end_id": 281474976710673, "start_id": 
281474976710672, "properties": {}}::edge, {"id": 281474976710673, "label": "", 
"properties": {}}::vertex]::path, [{"id": 281474976710672, "label": "", 
"properties": {}}::vertex, {"id": 1688849860263960, "label": "e_var", "end_id": 
281474976710673, "start_id": 281474976710672, "properties": {}}::edge, {"id": 
281474976710673, "label": "", "pro [...]
+(1 row)
+
+-- using concat more than once in a query
+SELECT '{}'::agtype || '{}'::agtype || '[{}]'::agtype;
+ ?column? 
+----------
+ [{}, {}]
+(1 row)
+
+SELECT '{"y": {}}'::agtype || '{"b": "5"}'::agtype || '{"a": {}}'::agtype || 
'{"z": []}'::agtype;
+               ?column?                
+---------------------------------------
+ {"a": {}, "b": "5", "y": {}, "z": []}
+(1 row)
+
+SELECT '{"y": {}}'::agtype || '{"b": "5"}'::agtype || '{"a": {}}'::agtype || 
'{"z": []}'::agtype || '[]'::agtype;
+                ?column?                 
+-----------------------------------------
+ [{"a": {}, "b": "5", "y": {}, "z": []}]
+(1 row)
+
+SELECT '{"y": {}}'::agtype || '{"b": "5"}'::agtype || '{"a": {}}'::agtype || 
'{"z": []}'::agtype || '[]'::agtype || '{}';
+                  ?column?                   
+---------------------------------------------
+ [{"a": {}, "b": "5", "y": {}, "z": []}, {}]
+(1 row)
+
+SELECT '"e"'::agtype || '1'::agtype || '{}'::agtype;
+   ?column?   
+--------------
+ ["e", 1, {}]
+(1 row)
+
+SELECT ('"e"'::agtype || '1'::agtype) || '{"[]": "p"}'::agtype;
+       ?column?        
+-----------------------
+ ["e", 1, {"[]": "p"}]
+(1 row)
+
+SELECT '{"{}": {"a": []}}'::agtype || '{"{}": {"[]": []}}'::agtype || '{"{}": 
{}}'::agtype;
+  ?column?  
+------------
+ {"{}": {}}
+(1 row)
+
+SELECT '{}'::agtype || '{}'::agtype || '[{}]'::agtype || '[{}]'::agtype || 
'{}'::agtype;
+     ?column?     
+------------------
+ [{}, {}, {}, {}]
+(1 row)
+
+-- should give an error
+SELECT '{"a": 13}'::agtype || 'null'::agtype;
+ERROR:  invalid right operand for agtype concatenation
+SELECT '"a"'::agtype || '{"a":1}';
+ERROR:  invalid left operand for agtype concatenation
+SELECT '3'::agtype || '{}'::agtype;
+ERROR:  invalid left operand for agtype concatenation
+SELECT '{"a":1}' || '"a"'::agtype;
+ERROR:  invalid right operand for agtype concatenation
+SELECT '{"b": [1, 2, {"[{}, {}]": "a"}, {"1": {}}]}'::agtype || true::agtype;
+ERROR:  invalid right operand for agtype concatenation
+SELECT '{"b": [1, 2, {"[{}, {}]": "a"}, {"1": {}}]}'::agtype || 'true'::agtype;
+ERROR:  invalid right operand for agtype concatenation
+SELECT '{"b": [1, 2, {"[{}, {}]": "a"}, {"1": {}}]}'::agtype || 
age_agtype_sum('1', '2');
+ERROR:  invalid right operand for agtype concatenation
+SELECT ('{"a": "5"}'::agtype || '{"a": {}}'::agtype) || '5'::agtype;
+ERROR:  invalid right operand for agtype concatenation
+SELECT ('{"a": "5"}'::agtype || '{"a": {}}'::agtype || '5') || '[5]'::agtype;
+ERROR:  invalid right operand for agtype concatenation
+-- both operands have to be of agtype
+SELECT '3'::agtype || 4;
+ERROR:  operator does not exist: agtype || integer
+LINE 1: SELECT '3'::agtype || 4;
+                           ^
+HINT:  No operator matches the given name and argument types. You might need 
to add explicit type casts.
+SELECT '3'::agtype || true;
+ERROR:  operator does not exist: agtype || boolean
+LINE 1: SELECT '3'::agtype || true;
+                           ^
+HINT:  No operator matches the given name and argument types. You might need 
to add explicit type casts.
+--
+-- jsonb operators inside cypher queries
+--
+SELECT create_graph('jsonb_operators');
+NOTICE:  graph "jsonb_operators" has been created
+ create_graph 
+--------------
+ 
+(1 row)
+
+SELECT * FROM cypher('jsonb_operators',$$CREATE ({list:['a', 'b', 'c'], 
json:{a:1, b:['a', 'b'], c:{d:'a'}}})$$) as (a agtype);
+ a 
+---
+(0 rows)
+
+--
+-- concat || operator
+--
+SELECT * FROM cypher('jsonb_operators', $$ RETURN [1,2] || 2 $$) AS (result 
agtype);
+  result   
+-----------
+ [1, 2, 2]
+(1 row)
+
+SELECT * FROM cypher('jsonb_operators', $$ RETURN true || false $$) AS (result 
agtype);
+    result     
+---------------
+ [true, false]
+(1 row)
+
+SELECT * FROM cypher('jsonb_operators', $$ RETURN true || false || {a: 
'string'} $$) AS (result agtype);
+             result             
+--------------------------------
+ [true, false, {"a": "string"}]
+(1 row)
+
+SELECT * FROM cypher('jsonb_operators', $$ RETURN true || false || {a: 
'string'} || true $$) AS (result agtype);
+                result                
+--------------------------------------
+ [true, false, {"a": "string"}, true]
+(1 row)
+
+SELECT * FROM cypher('jsonb_operators', $$ WITH [1,2,3] AS m WITH m, m || 
'string' AS n RETURN n $$) AS (result agtype);
+       result        
+---------------------
+ [1, 2, 3, "string"]
+(1 row)
+
+SELECT * FROM cypher('jsonb_operators', $$ WITH [1,2,3] AS m WITH m, m || {a: 
1::numeric} AS n RETURN n $$) AS (result agtype);
+            result            
+------------------------------
+ [1, 2, 3, {"a": 1::numeric}]
+(1 row)
+
+SELECT * FROM cypher('jsonb_operators', $$ WITH {a: [1,2,3]} AS m WITH m, m || 
{a: 1::numeric} AS n RETURN n $$) AS (result agtype);
+      result       
+-------------------
+ {"a": 1::numeric}
+(1 row)
+
+SELECT * FROM cypher('jsonb_operators', $$ WITH {b: [1,2,3]} AS m WITH m, m || 
{a: 1::numeric} AS n RETURN n $$) AS (result agtype);
+              result               
+-----------------------------------
+ {"a": 1::numeric, "b": [1, 2, 3]}
+(1 row)
+
+SELECT * FROM cypher('jsonb_operators', $$ MATCH(n) RETURN n || 1 || 'string' 
$$) AS (result agtype);
+                                                                          
result                                                                          
+----------------------------------------------------------------------------------------------------------------------------------------------------------
+ [{"id": 281474976710657, "label": "", "properties": {"json": {"a": 1, "b": 
["a", "b"], "c": {"d": "a"}}, "list": ["a", "b", "c"]}}::vertex, 1, "string"]
+(1 row)
+
+SELECT * FROM cypher('jsonb_operators', $$ MATCH(n) RETURN n || {list: [true, 
null]} $$) AS (result agtype);
+                                                                               
result                                                                          
      
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ [{"id": 281474976710657, "label": "", "properties": {"json": {"a": 1, "b": 
["a", "b"], "c": {"d": "a"}}, "list": ["a", "b", "c"]}}::vertex, {"list": 
[true, null]}]
+(1 row)
+
+SELECT * FROM cypher('jsonb_operators', $$ MATCH (n) MATCH(m) RETURN n || m 
$$) AS (result agtype);
+                                                                               
                                                          result                
                                                                                
                                         
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ [{"id": 281474976710657, "label": "", "properties": {"json": {"a": 1, "b": 
["a", "b"], "c": {"d": "a"}}, "list": ["a", "b", "c"]}}::vertex, {"id": 
281474976710657, "label": "", "properties": {"json": {"a": 1, "b": ["a", "b"], 
"c": {"d": "a"}}, "list": ["a", "b", "c"]}}::vertex]
+(1 row)
+
+SELECT * FROM cypher('jsonb_operators', $$ MATCH (n) RETURN n.list || [1, 2, 
3] $$) AS (result agtype);
+          result          
+--------------------------
+ ["a", "b", "c", 1, 2, 3]
+(1 row)
+
+SELECT * FROM cypher('jsonb_operators', $$ MATCH (n) RETURN n.json || [1, 2, 
3] $$) AS (result agtype);
+                        result                         
+-------------------------------------------------------
+ [{"a": 1, "b": ["a", "b"], "c": {"d": "a"}}, 1, 2, 3]
+(1 row)
+
+SELECT * FROM cypher('jsonb_operators', $$ MATCH (n) RETURN n.json || n.json 
$$) AS (result agtype);
+                   result                   
+--------------------------------------------
+ {"a": 1, "b": ["a", "b"], "c": {"d": "a"}}
+(1 row)
+
+SELECT * FROM cypher('jsonb_operators', $$ MATCH (n) RETURN n.json || n $$) AS 
(result agtype);
+                                                                               
          result                                                                
                          
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ [{"a": 1, "b": ["a", "b"], "c": {"d": "a"}}, {"id": 281474976710657, "label": 
"", "properties": {"json": {"a": 1, "b": ["a", "b"], "c": {"d": "a"}}, "list": 
["a", "b", "c"]}}::vertex]
+(1 row)
+
+-- should give an error
+SELECT * FROM cypher('jsonb_operators', $$ RETURN true || {a: 'string'} || 
true $$) AS (result agtype);
+ERROR:  invalid left operand for agtype concatenation
+SELECT * FROM cypher('jsonb_operators', $$ WITH 'b' AS m WITH m, m || {a: 1} 
AS n RETURN n $$) AS (result agtype);
+ERROR:  invalid left operand for agtype concatenation
+SELECT * FROM cypher('jsonb_operators', $$ MATCH (n) RETURN n.json || 1 $$) AS 
(result agtype);
+ERROR:  invalid right operand for agtype concatenation
+-- clean up
+SELECT drop_graph('jsonb_operators', true);
+NOTICE:  drop cascades to 2 other objects
+DETAIL:  drop cascades to table jsonb_operators._ag_label_vertex
+drop cascades to table jsonb_operators._ag_label_edge
+NOTICE:  graph "jsonb_operators" has been dropped
+ drop_graph 
+------------
+ 
+(1 row)
+
diff --git a/regress/sql/jsonb_operators.sql b/regress/sql/jsonb_operators.sql
new file mode 100644
index 00000000..6fbb872a
--- /dev/null
+++ b/regress/sql/jsonb_operators.sql
@@ -0,0 +1,159 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+LOAD 'age';
+SET search_path TO ag_catalog;
+
+--
+-- jsonb operators in AGE (?, ?&, ?|, ->, ->>, #>, #>>, ||)
+--
+
+--
+-- concat || operator
+--
+SELECT i, pg_typeof(i) FROM (SELECT '[0, 1]'::agtype || '[0, 1]'::agtype as i) 
a;
+
+SELECT i, pg_typeof(i) FROM (SELECT '2'::agtype || '[0, 1]'::agtype as i) a;
+SELECT i, pg_typeof(i) FROM (SELECT '[0, 1]'::agtype || '2'::agtype as i) a;
+
+SELECT i, pg_typeof(i) FROM (SELECT '{"a": 1}'::agtype || '[0, 1]'::agtype as 
i) a;
+SELECT i, pg_typeof(i) FROM (SELECT '[0, 1]'::agtype || '{"a": 1}'::agtype as 
i) a;
+
+SELECT i, pg_typeof(i) FROM (SELECT '[]'::agtype || '[0, 1]'::agtype as i) a;
+SELECT i, pg_typeof(i) FROM (SELECT '[0, 1]'::agtype || '[]'::agtype as i) a;
+SELECT i, pg_typeof(i) FROM (SELECT 'null'::agtype || '[0, 1]'::agtype as i) a;
+SELECT i, pg_typeof(i) FROM (SELECT '[0, 1]'::agtype || 'null'::agtype as i) a;
+SELECT i, pg_typeof(i) FROM (SELECT '[null]'::agtype || '[0, 1]'::agtype as i) 
a;
+SELECT i, pg_typeof(i) FROM (SELECT '[0, 1]'::agtype || '[null]'::agtype as i) 
a;
+
+SELECT i, pg_typeof(i) FROM (SELECT NULL || '[0, 1]'::agtype as i) a;
+SELECT i, pg_typeof(i) FROM (SELECT '[0, 1]'::agtype || NULL as i) a;
+
+-- both operands are objects
+SELECT '{"aa":1 , "b":2, "cq":3}'::agtype || '{"cq":"l", "b":"g", "fg":false}';
+SELECT '{"aa":1 , "b":2, "cq":3}'::agtype || '{"aq":"l"}';
+SELECT '{"aa":1 , "b":2, "cq":3}'::agtype || '{"aa":"l"}';
+SELECT '{"aa":1 , "b":2, "cq":3}'::agtype || '{}';
+SELECT '{"aa":1 , "b":2, "cq":3, "cj": {"fg": true}}'::agtype || '{"cq":"l", 
"b":"g", "fg":false}';
+SELECT '{"a": 13}'::agtype || '{"a": 13}'::agtype;
+SELECT '{}'::agtype || '{"a":"b"}'::agtype;
+SELECT '{}'::agtype || '{}'::agtype;
+
+-- both operands are arrays
+SELECT '["a", "b"]'::agtype || '["c"]';
+SELECT '["a", "b"]'::agtype || '["c", "d"]';
+SELECT '["a", "b"]'::agtype || '["c", "d", "d"]';
+SELECT '["c"]' || '["a", "b"]'::agtype;
+SELECT '[]'::agtype || '["a"]'::agtype;
+SELECT '[]'::agtype || '[]'::agtype;
+
+SELECT '["a", "b"]'::agtype || '"c"';
+SELECT '"c"' || '["a", "b"]'::agtype;
+SELECT '[]'::agtype || '"a"'::agtype;
+SELECT '"b"'::agtype || '"a"'::agtype;
+SELECT '3'::agtype || '[]'::agtype;
+SELECT '3'::agtype || '4'::agtype;
+SELECT '3'::agtype || '[4]';
+SELECT '3::numeric'::agtype || '[[]]'::agtype;
+SELECT null::agtype || null::agtype;
+
+-- array and object as operands
+SELECT '{"aa":1 , "b":2, "cq":3}'::agtype || '[{"aa":"l"}]';
+SELECT '{"aa":1 , "b":2, "cq":3}'::agtype || '[{"aa":"l", "aa": "k"}]';
+SELECT '{"a": 13}'::agtype || '[{"a": 13}]'::agtype;
+SELECT '[]'::agtype || '{"a":"b"}'::agtype;
+SELECT '{"a":"b"}'::agtype || '[]'::agtype;
+SELECT '[]'::agtype || '{}'::agtype;
+SELECT '[3]'::agtype || '{}'::agtype;
+SELECT '{}'::agtype || '[null]'::agtype;
+SELECT '[null]'::agtype || '{"a": null}'::agtype;
+SELECT '""'::agtype || '[]'::agtype;
+
+-- vertex/edge/path as operand(s)
+SELECT '{"id": 1688849860263937, "label": "EDGE", "end_id": 1970324836974593, 
"start_id": 1407374883553281, "properties": {"a": "xyz", "b" : true, "c": 
-19.888, "e": {"f": "abcdef", "g": {}, "h": [[], {}]}, "i": {"j": 199, "k": 
{"l": "mnopq"}}}}::edge'::agtype || '"id"';
+SELECT '{"id": 1688849860263937, "label": "EDGE", "end_id": 1970324836974593, 
"start_id": 1407374883553281, "properties": {"a": "xyz", "b" : true, "c": 
-19.888, "e": {"f": "abcdef", "g": {}, "h": [[], {}]}, "i": {"j": 199, "k": 
{"l": "mnopq"}}}}::edge'::agtype || '"m"';
+SELECT '{"id": 1688849860263937, "label": "EDGE", "end_id": 1970324836974593, 
"start_id": 1407374883553281, "properties": {"a": "xyz", "b" : true, "c": 
-19.888, "e": {"f": "abcdef", "g": {}, "h": [[], {}]}, "i": {"j": 199, "k": 
{"l": "mnopq"}}}}::edge'::agtype || '{"m": []}';
+SELECT '{"id": 844424930131969, "label": "v", "properties": 
{}}::vertex'::agtype || '{"id": 844424930131971, "label": "v", "properties": 
{"key": "value"}}::vertex'::agtype;
+SELECT '{"id": 844424930131969, "label": "v", "properties": 
{}}::vertex'::agtype || '[]'::agtype;
+SELECT '{"id": 844424930131969, "label": "v", "properties": 
{}}::vertex'::agtype || '{}'::agtype;
+SELECT '{}'::agtype || '{"id": 844424930131969, "label": "v", "properties": 
{}}::vertex'::agtype;
+SELECT '"id"'::agtype || '{"id": 844424930131969, "label": "v", "properties": 
{}}::vertex'::agtype;
+SELECT '{"id": 844424930131969, "label": "v", "properties": 
{}}::vertex'::agtype || '{"id": 1688849860263950, "label": "e_var", "end_id": 
281474976710662, "start_id": 281474976710661, "properties": {}}::edge'::agtype;
+SELECT '[{"id": 281474976710672, "label": "", "properties": {}}::vertex, 
{"id": 1688849860263960, "label": "e_var", "end_id": 281474976710673, 
"start_id": 281474976710672, "properties": {}}::edge, {"id": 281474976710673, 
"label": "", "properties": {}}::vertex]::path'::agtype || '{"id": 
844424930131969, "label": "v", "properties": {}}::vertex'::agtype;
+SELECT '[{"id": 281474976710672, "label": "", "properties": {}}::vertex, 
{"id": 1688849860263960, "label": "e_var", "end_id": 281474976710673, 
"start_id": 281474976710672, "properties": {}}::edge, {"id": 281474976710673, 
"label": "", "properties": {}}::vertex]::path'::agtype || '[{"id": 
281474976710672, "label": "", "properties": {}}::vertex, {"id": 
1688849860263960, "label": "e_var", "end_id": 281474976710673, "start_id": 
281474976710672, "properties": {}}::edge, {"id": 281474976710673, [...]
+
+-- using concat more than once in a query
+SELECT '{}'::agtype || '{}'::agtype || '[{}]'::agtype;
+SELECT '{"y": {}}'::agtype || '{"b": "5"}'::agtype || '{"a": {}}'::agtype || 
'{"z": []}'::agtype;
+SELECT '{"y": {}}'::agtype || '{"b": "5"}'::agtype || '{"a": {}}'::agtype || 
'{"z": []}'::agtype || '[]'::agtype;
+SELECT '{"y": {}}'::agtype || '{"b": "5"}'::agtype || '{"a": {}}'::agtype || 
'{"z": []}'::agtype || '[]'::agtype || '{}';
+SELECT '"e"'::agtype || '1'::agtype || '{}'::agtype;
+SELECT ('"e"'::agtype || '1'::agtype) || '{"[]": "p"}'::agtype;
+SELECT '{"{}": {"a": []}}'::agtype || '{"{}": {"[]": []}}'::agtype || '{"{}": 
{}}'::agtype;
+SELECT '{}'::agtype || '{}'::agtype || '[{}]'::agtype || '[{}]'::agtype || 
'{}'::agtype;
+
+-- should give an error
+SELECT '{"a": 13}'::agtype || 'null'::agtype;
+SELECT '"a"'::agtype || '{"a":1}';
+SELECT '3'::agtype || '{}'::agtype;
+SELECT '{"a":1}' || '"a"'::agtype;
+SELECT '{"b": [1, 2, {"[{}, {}]": "a"}, {"1": {}}]}'::agtype || true::agtype;
+SELECT '{"b": [1, 2, {"[{}, {}]": "a"}, {"1": {}}]}'::agtype || 'true'::agtype;
+SELECT '{"b": [1, 2, {"[{}, {}]": "a"}, {"1": {}}]}'::agtype || 
age_agtype_sum('1', '2');
+SELECT ('{"a": "5"}'::agtype || '{"a": {}}'::agtype) || '5'::agtype;
+SELECT ('{"a": "5"}'::agtype || '{"a": {}}'::agtype || '5') || '[5]'::agtype;
+-- both operands have to be of agtype
+SELECT '3'::agtype || 4;
+SELECT '3'::agtype || true;
+
+--
+-- jsonb operators inside cypher queries
+--
+SELECT create_graph('jsonb_operators');
+
+SELECT * FROM cypher('jsonb_operators',$$CREATE ({list:['a', 'b', 'c'], 
json:{a:1, b:['a', 'b'], c:{d:'a'}}})$$) as (a agtype);
+
+--
+-- concat || operator
+--
+SELECT * FROM cypher('jsonb_operators', $$ RETURN [1,2] || 2 $$) AS (result 
agtype);
+SELECT * FROM cypher('jsonb_operators', $$ RETURN true || false $$) AS (result 
agtype);
+SELECT * FROM cypher('jsonb_operators', $$ RETURN true || false || {a: 
'string'} $$) AS (result agtype);
+SELECT * FROM cypher('jsonb_operators', $$ RETURN true || false || {a: 
'string'} || true $$) AS (result agtype);
+
+SELECT * FROM cypher('jsonb_operators', $$ WITH [1,2,3] AS m WITH m, m || 
'string' AS n RETURN n $$) AS (result agtype);
+SELECT * FROM cypher('jsonb_operators', $$ WITH [1,2,3] AS m WITH m, m || {a: 
1::numeric} AS n RETURN n $$) AS (result agtype);
+SELECT * FROM cypher('jsonb_operators', $$ WITH {a: [1,2,3]} AS m WITH m, m || 
{a: 1::numeric} AS n RETURN n $$) AS (result agtype);
+SELECT * FROM cypher('jsonb_operators', $$ WITH {b: [1,2,3]} AS m WITH m, m || 
{a: 1::numeric} AS n RETURN n $$) AS (result agtype);
+
+SELECT * FROM cypher('jsonb_operators', $$ MATCH(n) RETURN n || 1 || 'string' 
$$) AS (result agtype);
+SELECT * FROM cypher('jsonb_operators', $$ MATCH(n) RETURN n || {list: [true, 
null]} $$) AS (result agtype);
+SELECT * FROM cypher('jsonb_operators', $$ MATCH (n) MATCH(m) RETURN n || m 
$$) AS (result agtype);
+SELECT * FROM cypher('jsonb_operators', $$ MATCH (n) RETURN n.list || [1, 2, 
3] $$) AS (result agtype);
+SELECT * FROM cypher('jsonb_operators', $$ MATCH (n) RETURN n.json || [1, 2, 
3] $$) AS (result agtype);
+SELECT * FROM cypher('jsonb_operators', $$ MATCH (n) RETURN n.json || n.json 
$$) AS (result agtype);
+SELECT * FROM cypher('jsonb_operators', $$ MATCH (n) RETURN n.json || n $$) AS 
(result agtype);
+
+-- should give an error
+SELECT * FROM cypher('jsonb_operators', $$ RETURN true || {a: 'string'} || 
true $$) AS (result agtype);
+SELECT * FROM cypher('jsonb_operators', $$ WITH 'b' AS m WITH m, m || {a: 1} 
AS n RETURN n $$) AS (result agtype);
+SELECT * FROM cypher('jsonb_operators', $$ MATCH (n) RETURN n.json || 1 $$) AS 
(result agtype);
+
+-- clean up
+SELECT drop_graph('jsonb_operators', true);
\ No newline at end of file
diff --git a/src/backend/parser/ag_scanner.l b/src/backend/parser/ag_scanner.l
index 18ce042c..ffc6673c 100644
--- a/src/backend/parser/ag_scanner.l
+++ b/src/backend/parser/ag_scanner.l
@@ -227,6 +227,7 @@ param \${id}
  * These are tokens that are used as operators and language constructs in
  * Cypher, and some of them are structural characters in JSON.
  */
+concat   "||"
 lt_gt    "<>"
 lt_eq    "<="
 gt_eq    ">="
@@ -642,6 +643,14 @@ ag_token token;
     return token;
 }
 
+{concat} {
+    update_location();
+    token.type = AG_TOKEN_CONCAT;
+    token.value.s = yytext;
+    token.location = get_location();
+    return token;
+}
+
 {lt_gt} {
     update_location();
     token.type = AG_TOKEN_LT_GT;
diff --git a/src/backend/parser/cypher_gram.y b/src/backend/parser/cypher_gram.y
index 26908122..5d671624 100644
--- a/src/backend/parser/cypher_gram.y
+++ b/src/backend/parser/cypher_gram.y
@@ -75,7 +75,7 @@
 %token <string> PARAMETER
 
 /* operators that have more than 1 character */
-%token NOT_EQ LT_EQ GT_EQ DOT_DOT TYPECAST PLUS_EQ EQ_TILDE
+%token NOT_EQ LT_EQ GT_EQ DOT_DOT TYPECAST PLUS_EQ EQ_TILDE CONCAT
 
 /* keywords in alphabetical order */
 %token <keyword> ALL ANALYZE AND AS ASC ASCENDING
@@ -169,7 +169,7 @@
 %left XOR
 %right NOT
 %left '=' NOT_EQ '<' LT_EQ '>' GT_EQ
-%left '+' '-'
+%left '+' '-' CONCAT
 %left '*' '/' '%'
 %left '^'
 %nonassoc IN IS
@@ -1324,6 +1324,10 @@ expr:
         {
             $$ = build_comparison_expression($1, $3, ">=", @2);
         }
+    | expr CONCAT expr
+        {
+            $$ = (Node *)makeSimpleA_Expr(AEXPR_OP, "||", $1, $3, @2);
+        }
     | expr '+' expr
         {
             $$ = (Node *)makeSimpleA_Expr(AEXPR_OP, "+", $1, $3, @2);
diff --git a/src/backend/parser/cypher_parser.c 
b/src/backend/parser/cypher_parser.c
index 487f5607..407d6c07 100644
--- a/src/backend/parser/cypher_parser.c
+++ b/src/backend/parser/cypher_parser.c
@@ -46,7 +46,8 @@ int cypher_yylex(YYSTYPE *lvalp, YYLTYPE *llocp, ag_scanner_t 
scanner)
         DOT_DOT,
         TYPECAST,
         PLUS_EQ,
-        EQ_TILDE
+        EQ_TILDE,
+        CONCAT
     };
 
     ag_token token;
@@ -98,6 +99,7 @@ int cypher_yylex(YYSTYPE *lvalp, YYLTYPE *llocp, ag_scanner_t 
scanner)
     case AG_TOKEN_DOT_DOT:
     case AG_TOKEN_PLUS_EQ:
     case AG_TOKEN_EQ_TILDE:
+    case AG_TOKEN_CONCAT:
         break;
     case AG_TOKEN_TYPECAST:
         break;
diff --git a/src/backend/utils/adt/agtype_ops.c 
b/src/backend/utils/adt/agtype_ops.c
index 9ee2180b..829483a4 100644
--- a/src/backend/utils/adt/agtype_ops.c
+++ b/src/backend/utils/adt/agtype_ops.c
@@ -33,7 +33,7 @@
 #include "utils/agtype.h"
 
 static void ereport_op_str(const char *op, agtype *lhs, agtype *rhs);
-static agtype *agtype_concat(agtype *agt1, agtype *agt2);
+static agtype *agtype_concat_impl(agtype *agt1, agtype *agt2);
 static agtype_value *iterator_concat(agtype_iterator **it1,
                                      agtype_iterator **it2,
                                      agtype_parse_state **state);
@@ -162,7 +162,7 @@ Datum agtype_add(PG_FUNCTION_ARGS)
             (AGT_ROOT_IS_OBJECT(lhs) && AGT_ROOT_IS_OBJECT(rhs)))
             ereport_op_str("+", lhs, rhs);
 
-        agt = AGTYPE_P_GET_DATUM(agtype_concat(lhs, rhs));
+        agt = AGTYPE_P_GET_DATUM(agtype_concat_impl(lhs, rhs));
 
         PG_RETURN_DATUM(agt);
     }
@@ -1156,7 +1156,26 @@ Datum agtype_exists_all(PG_FUNCTION_ARGS)
     PG_RETURN_BOOL(true);
 }
 
-static agtype *agtype_concat(agtype *agt1, agtype *agt2)
+PG_FUNCTION_INFO_V1(agtype_concat);
+
+Datum agtype_concat(PG_FUNCTION_ARGS)
+{
+    agtype *agt_lhs = AG_GET_ARG_AGTYPE_P(0);
+    agtype *agt_rhs = AG_GET_ARG_AGTYPE_P(1);
+
+    /*
+     * Jsonb returns NULL for PG Null, but not for jsonb's NULL value,
+     * so we do the same.
+     */
+    if (PG_ARGISNULL(0) || PG_ARGISNULL(1))
+    {
+        PG_RETURN_NULL();
+    }
+
+    AG_RETURN_AGTYPE_P(agtype_concat_impl(agt_lhs, agt_rhs));
+}
+
+static agtype *agtype_concat_impl(agtype *agt1, agtype *agt2)
 {
     agtype_parse_state *state = NULL;
     agtype_value *res;
@@ -1172,9 +1191,13 @@ static agtype *agtype_concat(agtype *agt1, agtype *agt2)
     if (AGT_ROOT_IS_OBJECT(agt1) == AGT_ROOT_IS_OBJECT(agt2))
     {
         if (AGT_ROOT_COUNT(agt1) == 0 && !AGT_ROOT_IS_SCALAR(agt2))
+        {
             return agt2;
+        }
         else if (AGT_ROOT_COUNT(agt2) == 0 && !AGT_ROOT_IS_SCALAR(agt1))
+        {
             return agt1;
+        }
     }
 
     it1 = agtype_iterator_init(&agt1->root);
@@ -1210,22 +1233,31 @@ static agtype_value *iterator_concat(agtype_iterator 
**it1,
     if (rk1 == WAGT_BEGIN_OBJECT && rk2 == WAGT_BEGIN_OBJECT)
     {
         /*
-         * Append the all tokens from v1 to res, except last WAGT_END_OBJECT
+         * Append all tokens from v1 to res, except last WAGT_END_OBJECT
          * (because res will not be finished yet).
          */
         push_agtype_value(state, r1, NULL);
+
         while ((r1 = agtype_iterator_next(it1, &v1, true)) != WAGT_END_OBJECT)
+        {
+            Assert(r1 == WAGT_KEY || r1 == WAGT_VALUE);
             push_agtype_value(state, r1, &v1);
+        }
 
         /*
-         * Append the all tokens from v2 to res, include last WAGT_END_OBJECT
-         * (the concatenation will be completed).
+         * Append all tokens from v2 to res, except last WAGT_END_OBJECT
          */
-        while ((r2 = agtype_iterator_next(it2, &v2, true)) != 0)
-            res = push_agtype_value(state, r2,
-                                    r2 != WAGT_END_OBJECT ? &v2 : NULL);
-    }
+        while ((r2 = agtype_iterator_next(it2, &v2, true)) != WAGT_END_OBJECT)
+        {
+            Assert(r2 == WAGT_KEY || r2 == WAGT_VALUE);
+            push_agtype_value(state, r2, &v2);
+        }
 
+        /*
+         * Append the last token WAGT_END_OBJECT to complete res
+         */
+        res = push_agtype_value(state, WAGT_END_OBJECT, NULL);
+    }
     /*
      * Both elements are arrays (either can be scalar).
      */
@@ -1242,11 +1274,10 @@ static agtype_value *iterator_concat(agtype_iterator 
**it1,
         while ((r2 = agtype_iterator_next(it2, &v2, true)) != WAGT_END_ARRAY)
         {
             Assert(r2 == WAGT_ELEM);
-            push_agtype_value(state, WAGT_ELEM, &v2);
+            push_agtype_value(state, r2, &v2);
         }
 
-        res = push_agtype_value(state, WAGT_END_ARRAY,
-                                NULL /* signal to sort */);
+        res = push_agtype_value(state, WAGT_END_ARRAY, NULL);
     }
     /* have we got array || object or object || array? */
     else if (((rk1 == WAGT_BEGIN_ARRAY && !(*it1)->is_scalar) &&
@@ -1264,36 +1295,120 @@ static agtype_value *iterator_concat(agtype_iterator 
**it1,
         if (prepend)
         {
             push_agtype_value(state, WAGT_BEGIN_OBJECT, NULL);
-            while ((r1 = agtype_iterator_next(it_object, &v1, true)) != 0)
-                push_agtype_value(state, r1,
-                                  r1 != WAGT_END_OBJECT ? &v1 : NULL);
 
-            while ((r2 = agtype_iterator_next(it_array, &v2, true)) != 0)
-                res = push_agtype_value(state, r2,
-                                        r2 != WAGT_END_ARRAY ? &v2 : NULL);
+            while ((r1 = agtype_iterator_next(it_object, &v1, true)) !=
+                    WAGT_END_OBJECT)
+            {
+                Assert(r1 == WAGT_KEY || r1 == WAGT_VALUE);
+                push_agtype_value(state, r1, &v1);
+            }
+
+            push_agtype_value(state, WAGT_END_OBJECT, NULL);
+
+            while ((r2 = agtype_iterator_next(it_array, &v2, true)) !=
+                    WAGT_END_ARRAY)
+            {
+                Assert(r2 == WAGT_ELEM);
+                push_agtype_value(state, r2, &v2);
+            }
+
+            res = push_agtype_value(state, WAGT_END_ARRAY, NULL);
         }
         else
         {
             while ((r1 = agtype_iterator_next(it_array, &v1, true)) !=
                    WAGT_END_ARRAY)
+            {
+                Assert(r1 == WAGT_ELEM);
                 push_agtype_value(state, r1, &v1);
+            }
 
             push_agtype_value(state, WAGT_BEGIN_OBJECT, NULL);
-            while ((r2 = agtype_iterator_next(it_object, &v2, true)) != 0)
-                push_agtype_value(state, r2,
-                                  r2 != WAGT_END_OBJECT ? &v2 : NULL);
+
+            while ((r2 = agtype_iterator_next(it_object, &v2, true)) !=
+                    WAGT_END_OBJECT)
+            {
+                Assert(r2 == WAGT_KEY || r2 == WAGT_VALUE);
+                push_agtype_value(state, r2,&v2);
+            }
+
+            push_agtype_value(state, WAGT_END_OBJECT, NULL);
 
             res = push_agtype_value(state, WAGT_END_ARRAY, NULL);
         }
     }
+    else if (rk1 == WAGT_BEGIN_OBJECT)
+    {
+        /*
+         * We have object || array.
+         */
+        Assert(rk1 == WAGT_BEGIN_OBJECT);
+        Assert(rk2 == WAGT_BEGIN_ARRAY);
+
+        push_agtype_value(state, WAGT_BEGIN_ARRAY, NULL);
+        push_agtype_value(state, WAGT_BEGIN_OBJECT, NULL);
+
+        while ((r1 = agtype_iterator_next(it1, &v1, true)) != WAGT_END_OBJECT)
+        {
+            Assert(r1 == WAGT_KEY || r1 == WAGT_VALUE);
+            push_agtype_value(state, r1, &v1);
+        }
+
+        push_agtype_value(state, WAGT_END_OBJECT, NULL);
+
+        while ((r2 = agtype_iterator_next(it2, &v2, true)) != WAGT_END_ARRAY)
+        {
+            if (v2.type < AGTV_VERTEX || v2.type > AGTV_PATH)
+            {
+                ereport(ERROR,
+                        (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                         errmsg("invalid right operand for agtype "
+                                "concatenation")));
+            }
+
+            Assert(r2 == WAGT_ELEM);
+
+            push_agtype_value(state, r2, &v2);
+        }
+
+        res = push_agtype_value(state, WAGT_END_ARRAY, NULL);
+    }
     else
     {
         /*
-         * This must be scalar || object or object || scalar, as that's all
-         * that's left. Both of these make no sense, so error out.
+         * We have array || object.
          */
-        ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-                        errmsg("invalid concatenation of agtype objects")));
+        Assert(rk1 == WAGT_BEGIN_ARRAY);
+        Assert(rk2 == WAGT_BEGIN_OBJECT);
+
+        push_agtype_value(state, WAGT_BEGIN_ARRAY, NULL);
+
+        while ((r1 = agtype_iterator_next(it1, &v1, true)) != WAGT_END_ARRAY)
+        {
+            if (v1.type < AGTV_VERTEX || v1.type > AGTV_PATH)
+            {
+                ereport(ERROR,
+                        (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                        errmsg("invalid left operand for agtype "
+                               "concatenation")));
+            }
+
+            Assert(r1 == WAGT_ELEM);
+
+            push_agtype_value(state, r1, &v1);
+        }
+
+        push_agtype_value(state, WAGT_BEGIN_OBJECT, NULL);
+
+        while ((r2 = agtype_iterator_next(it2, &v2, true)) != WAGT_END_OBJECT)
+        {
+            Assert(r2 == WAGT_KEY || r2 == WAGT_VALUE);
+            push_agtype_value(state, r2, &v2);
+        }
+
+        push_agtype_value(state, WAGT_END_OBJECT, NULL);
+
+        res = push_agtype_value(state, WAGT_END_ARRAY, NULL);
     }
 
     return res;
diff --git a/src/include/parser/ag_scanner.h b/src/include/parser/ag_scanner.h
index d5ce9e9f..01087dc1 100644
--- a/src/include/parser/ag_scanner.h
+++ b/src/include/parser/ag_scanner.h
@@ -46,7 +46,8 @@ typedef enum ag_token_type
     AG_TOKEN_TYPECAST,
     AG_TOKEN_PLUS_EQ,
     AG_TOKEN_EQ_TILDE,
-    AG_TOKEN_CHAR
+    AG_TOKEN_CONCAT,
+    AG_TOKEN_CHAR,
 } ag_token_type;
 
 /*

Reply via email to