From aef5e1a6e382f1ab0203f067e4efbda566ba447a Mon Sep 17 00:00:00 2001
From: John Gemignani
Date: Wed, 10 Aug 2022 12:14:51 -0700
Subject: [PATCH 001/191] Add upgrade script for 1.0.0 to 1.1.0
Added upgrade script for 1.0.0 to 1.1.0
---
age--1.0.0--1.1.0.sql | 247 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 247 insertions(+)
create mode 100644 age--1.0.0--1.1.0.sql
diff --git a/age--1.0.0--1.1.0.sql b/age--1.0.0--1.1.0.sql
new file mode 100644
index 000000000..13b205a47
--- /dev/null
+++ b/age--1.0.0--1.1.0.sql
@@ -0,0 +1,247 @@
+/*
+ * 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.
+ */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "ALTER EXTENSION age UPDATE TO '1.1.0'" to load this file. \quit
+
+--
+-- agtype - access operators ( ->, ->> )
+--
+
+CREATE FUNCTION ag_catalog.agtype_object_field(agtype, text)
+RETURNS agtype
+LANGUAGE c
+IMMUTABLE
+RETURNS NULL ON NULL INPUT
+PARALLEL SAFE
+AS 'MODULE_PATHNAME';
+
+-- get agtype object field
+CREATE OPERATOR -> (
+ LEFTARG = agtype,
+ RIGHTARG = text,
+ FUNCTION = ag_catalog.agtype_object_field
+);
+
+CREATE FUNCTION ag_catalog.agtype_object_field_text(agtype, text)
+RETURNS text
+LANGUAGE c
+IMMUTABLE
+RETURNS NULL ON NULL INPUT
+PARALLEL SAFE
+AS 'MODULE_PATHNAME';
+
+-- get agtype object field as text
+CREATE OPERATOR ->> (
+ LEFTARG = agtype,
+ RIGHTARG = text,
+ FUNCTION = ag_catalog.agtype_object_field_text
+);
+
+CREATE FUNCTION ag_catalog.agtype_array_element(agtype, int4)
+RETURNS agtype
+LANGUAGE c
+IMMUTABLE
+RETURNS NULL ON NULL INPUT
+PARALLEL SAFE
+AS 'MODULE_PATHNAME';
+
+-- get agtype array element
+CREATE OPERATOR -> (
+ LEFTARG = agtype,
+ RIGHTARG = int4,
+ FUNCTION = ag_catalog.agtype_array_element
+);
+
+CREATE FUNCTION ag_catalog.agtype_array_element_text(agtype, int4)
+RETURNS text
+LANGUAGE c
+IMMUTABLE
+RETURNS NULL ON NULL INPUT
+PARALLEL SAFE
+AS 'MODULE_PATHNAME';
+
+-- get agtype array element as text
+CREATE OPERATOR ->> (
+ LEFTARG = agtype,
+ RIGHTARG = int4,
+ FUNCTION = ag_catalog.agtype_array_element_text
+);
+
+--
+-- Contains operators @> <@
+--
+CREATE FUNCTION ag_catalog.agtype_contains(agtype, agtype)
+RETURNS boolean
+LANGUAGE c
+STABLE
+RETURNS NULL ON NULL INPUT
+PARALLEL SAFE
+AS 'MODULE_PATHNAME';
+
+CREATE OPERATOR @> (
+ LEFTARG = agtype,
+ RIGHTARG = agtype,
+ FUNCTION = ag_catalog.agtype_contains,
+ COMMUTATOR = '<@',
+ RESTRICT = contsel,
+ JOIN = contjoinsel
+);
+
+CREATE FUNCTION ag_catalog.agtype_contained_by(agtype, agtype)
+RETURNS boolean
+LANGUAGE c
+STABLE
+RETURNS NULL ON NULL INPUT
+PARALLEL SAFE
+AS 'MODULE_PATHNAME';
+
+CREATE OPERATOR <@ (
+ LEFTARG = agtype,
+ RIGHTARG = agtype,
+ FUNCTION = ag_catalog.agtype_contained_by,
+ COMMUTATOR = '@>',
+ RESTRICT = contsel,
+ JOIN = contjoinsel
+);
+
+--
+-- Key Existence Operators ? ?| ?&
+--
+CREATE FUNCTION ag_catalog.agtype_exists(agtype, text)
+RETURNS boolean
+LANGUAGE c
+IMMUTABLE
+RETURNS NULL ON NULL INPUT
+PARALLEL SAFE
+AS 'MODULE_PATHNAME';
+
+CREATE OPERATOR ? (
+ LEFTARG = agtype,
+ RIGHTARG = text,
+ FUNCTION = ag_catalog.agtype_exists,
+ COMMUTATOR = '?',
+ RESTRICT = contsel,
+ JOIN = contjoinsel
+);
+
+CREATE FUNCTION ag_catalog.agtype_exists_any(agtype, text[])
+RETURNS boolean
+LANGUAGE c
+IMMUTABLE
+RETURNS NULL ON NULL INPUT
+PARALLEL SAFE
+AS 'MODULE_PATHNAME';
+
+CREATE OPERATOR ?| (
+ LEFTARG = agtype,
+ RIGHTARG = text[],
+ FUNCTION = ag_catalog.agtype_exists_any,
+ RESTRICT = contsel,
+ JOIN = contjoinsel
+);
+
+CREATE FUNCTION ag_catalog.agtype_exists_all(agtype, text[])
+RETURNS boolean
+LANGUAGE c
+IMMUTABLE
+RETURNS NULL ON NULL INPUT
+PARALLEL SAFE
+AS 'MODULE_PATHNAME';
+
+CREATE OPERATOR ?& (
+ LEFTARG = agtype,
+ RIGHTARG = text[],
+ FUNCTION = ag_catalog.agtype_exists_all,
+ RESTRICT = contsel,
+ JOIN = contjoinsel
+);
+
+--
+-- agtype GIN support
+--
+CREATE FUNCTION ag_catalog.gin_compare_agtype(text, text)
+RETURNS int
+AS 'MODULE_PATHNAME'
+LANGUAGE C
+IMMUTABLE
+STRICT
+PARALLEL SAFE;
+
+CREATE FUNCTION gin_extract_agtype(agtype, internal)
+RETURNS internal
+AS 'MODULE_PATHNAME'
+LANGUAGE C
+IMMUTABLE
+STRICT
+PARALLEL SAFE;
+
+CREATE FUNCTION ag_catalog.gin_extract_agtype_query(agtype, internal, int2,
+ internal, internal)
+RETURNS internal
+AS 'MODULE_PATHNAME'
+LANGUAGE C
+IMMUTABLE
+STRICT
+PARALLEL SAFE;
+
+CREATE FUNCTION ag_catalog.gin_consistent_agtype(internal, int2, agtype, int4,
+ internal, internal)
+RETURNS bool
+AS 'MODULE_PATHNAME'
+LANGUAGE C
+IMMUTABLE
+STRICT
+PARALLEL SAFE;
+
+CREATE FUNCTION ag_catalog.gin_triconsistent_agtype(internal, int2, agtype, int4,
+ internal, internal, internal)
+RETURNS bool
+AS 'MODULE_PATHNAME'
+LANGUAGE C
+IMMUTABLE
+STRICT
+PARALLEL SAFE;
+
+CREATE OPERATOR CLASS ag_catalog.gin_agtype_ops
+DEFAULT FOR TYPE agtype USING gin AS
+ OPERATOR 7 @>,
+ OPERATOR 9 ?(agtype, text),
+ OPERATOR 10 ?|(agtype, text[]),
+ OPERATOR 11 ?&(agtype, text[]),
+ FUNCTION 1 ag_catalog.gin_compare_agtype(text,text),
+ FUNCTION 2 ag_catalog.gin_extract_agtype(agtype, internal),
+ FUNCTION 3 ag_catalog.gin_extract_agtype_query(agtype, internal, int2,
+ internal, internal),
+ FUNCTION 4 ag_catalog.gin_consistent_agtype(internal, int2, agtype, int4,
+ internal, internal),
+ FUNCTION 6 ag_catalog.gin_triconsistent_agtype(internal, int2, agtype, int4,
+ internal, internal, internal),
+STORAGE text;
+
+--
+-- graph id conversion function
+--
+ALTER FUNCTION ag_catalog.agtype_access_operator(VARIADIC agtype[]) IMMUTABLE;
+
+DROP FUNCTION IF EXISTS ag_catalog._property_constraint_check(agtype, agtype);
+
+--
+-- end
+--
From 9b67e00864e72b7e4d2824544c05f58d1ec92777 Mon Sep 17 00:00:00 2001
From: Dehowe Feng
Date: Wed, 10 Aug 2022 12:55:46 -0700
Subject: [PATCH 002/191] Upgrade AGE version from 1.0.0-->1.1.0
Upgrade AGE version to 1.1.0
---
Makefile | 2 +-
README.md | 2 +-
RELEASE | 40 ++++++++++++++++++--------------
age--1.0.0.sql => age--1.1.0.sql | 0
age.control | 2 +-
5 files changed, 26 insertions(+), 20 deletions(-)
rename age--1.0.0.sql => age--1.1.0.sql (100%)
diff --git a/Makefile b/Makefile
index deb3b4d90..b8ed4f023 100644
--- a/Makefile
+++ b/Makefile
@@ -70,7 +70,7 @@ OBJS = src/backend/age.o \
EXTENSION = age
-DATA = age--1.0.0.sql
+DATA = age--1.1.0.sql
# sorted in dependency order
REGRESS = scan \
diff --git a/README.md b/README.md
index 7484e4d76..285496329 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,7 @@ Intelligent -- AGE allows you to perform graph queries that are the basis for ma
## Latest happenings
-- Latest Apache AGE release, [Apache AGE 1.0.0 (https://github.com/apache/age/releases/tag/v1.0.0-rc1).
+- Latest Apache AGE release, [Apache AGE 1.0.0 (https://github.com/apache/age/releases/tag/v1.1.0-rc1).
- The latest Apache AGE documentation is now available at [here](https://age.apache.org/docs/master/index.html).
- The roadmap has been updated, please check out the [Apache AGE website](http://age.apache.org/).
- Send all your comments and inquiries to the user mailing list, users@age.apache.org.
diff --git a/RELEASE b/RELEASE
index 3e706219f..a8369970f 100644
--- a/RELEASE
+++ b/RELEASE
@@ -15,22 +15,28 @@
# specific language governing permissions and limitations
# under the License.
-Release Notes for Apache AGE release v1.0.0
+Release Notes for Apache AGE release v1.1.0
-Apache AGE 1.0.0 - Release Notes
+Apache AGE 1.1.0 - Release Notes
+
+ Support for Agtype containment ops and GIN Indices.
+ Add CALL [YIELD] grammar rules for the implementation of CALL procedures.
+ VLE path variable integration performance patch.
+ Improve WHERE clause performance and support index scans.
+ Allow global graph contexts to see currentCommandIds.
+ Cache Agtype and GRAPHID OIDs.
+ Allow lists and maps to be used in the SET clause.
+ Fix bug in aggregate function collect().
+ Fix Bug in WHERE clause and property constraints.
+ Fix VLE local cache bug (crash).
+ Fix bug where integers were not being serialized correctly when stored in GIN indices.
+ Fix the VLE peek_stack_head routine to return a NULL if the stack is NULL.
+ Fix MERGE visibility in chained commands, SET specifically.
+ Fix github issue #212 - Add access operator (`->`, `->>`) to Agtype.
+ Fix github issue #220 - fix local cached contexts for static procedures.
+ Fix github issue #224 - fix regression tests to fix issues on mac with trigonometric functions.
+ Fix github issue #235 - when MERGE and SET were used together.
+ Fix github issue #240 - negative array bounds.
+ Fix github issue #240 - negative array bounds - addendum.
+ Updated README.
- Add an upgrading SQL script file from 0.5.0 to 0.6.0
- Add upgrading file age--0.6.0--0.7.0.sql
- Refactor function get_agtype_value_object_value
- Age load issue (#188)
- Refactor agtype_access_operator
- Bugfix - Remove INLINE from function declaration
- Rebase VLE code
- Implement Merge Clause
- Bugfix: chained union logic
- Allow a path of one vertex
- Created functions for load graph from CSV files
- Add UNION into EXPLAIN grammar rule
- Implement `UNWIND` clause(#173)
- Bugfix:(nodejs) Corrects parsing for independence value(#177)
- Feat: Implement `OPTIONAL MATCH` (#175)
diff --git a/age--1.0.0.sql b/age--1.1.0.sql
similarity index 100%
rename from age--1.0.0.sql
rename to age--1.1.0.sql
diff --git a/age.control b/age.control
index 4e320d024..b0fb1401d 100644
--- a/age.control
+++ b/age.control
@@ -15,7 +15,7 @@
# specific language governing permissions and limitations
# under the License.
-default_version = '1.0.0'
+default_version = '1.1.0'
comment = 'AGE database extension'
module_pathname = '$libdir/age'
From 0c1f4e7f52a278d159a4ef0284f84544d58a61c3 Mon Sep 17 00:00:00 2001
From: John Gemignani
Date: Wed, 10 Aug 2022 15:55:02 -0700
Subject: [PATCH 003/191] Update NOTICE file
Updated NOTICE file to remove 'incubating'
---
NOTICE | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/NOTICE b/NOTICE
index 282a6fc1a..40ce5ef8a 100644
--- a/NOTICE
+++ b/NOTICE
@@ -1,4 +1,4 @@
-Apache AGE (incubating)
+Apache AGE
Copyright 2022 The Apache Software Foundation.
This product includes software developed at
The Apache Software Foundation (http://www.apache.org/).
From 3bd002cf3fc560686d35018382eb979e192cdce6 Mon Sep 17 00:00:00 2001
From: Dehowe Feng
Date: Tue, 19 Jul 2022 15:28:33 +0900
Subject: [PATCH 004/191] Implement CALL ...[YIELD] for cypher functions.
Queries can now call functions using the form CALL ... YIELD.
CALL ... YIELD can be used in some of the following forms:
Individual:
CALL
CALL YIELD
CALL YIELD WHERE UPDATE/RETURN
Subqueries:
READING_CLAUSE CALL YIELD UPDATE/RETURN
CALL YIELD READING_CLAUSE UPDATE/RETURN
In the future, CALL YIELD support for record returning functions and
multiple variable output functions can be added.
Known Issue with WHERE clause where a WHERE in a MATCH + CALL subquery
does not filter results is known.
---
Makefile | 1 +
regress/expected/cypher_call.out | 203 ++++++++++++++++++++++
regress/sql/cypher_call.sql | 99 +++++++++++
src/backend/nodes/ag_nodes.c | 2 +
src/backend/nodes/cypher_outfuncs.c | 11 ++
src/backend/parser/cypher_clause.c | 257 +++++++++++++++++++++++++++-
src/backend/parser/cypher_gram.y | 120 ++++++++++++-
src/include/nodes/ag_nodes.h | 2 +
src/include/nodes/cypher_nodes.h | 14 ++
src/include/nodes/cypher_outfuncs.h | 4 +
10 files changed, 701 insertions(+), 12 deletions(-)
create mode 100644 regress/expected/cypher_call.out
create mode 100644 regress/sql/cypher_call.sql
diff --git a/Makefile b/Makefile
index b8ed4f023..da0553b45 100644
--- a/Makefile
+++ b/Makefile
@@ -88,6 +88,7 @@ REGRESS = scan \
cypher_with \
cypher_vle \
cypher_union \
+ cypher_call \
cypher_merge \
age_load \
index \
diff --git a/regress/expected/cypher_call.out b/regress/expected/cypher_call.out
new file mode 100644
index 000000000..e9f9e346e
--- /dev/null
+++ b/regress/expected/cypher_call.out
@@ -0,0 +1,203 @@
+/*
+ * 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;
+SELECT create_graph('cypher_call');
+NOTICE: graph "cypher_call" has been created
+ create_graph
+--------------
+
+(1 row)
+
+SELECT * FROM cypher('cypher_call', $$CREATE ({n: 'a'})$$) as (a agtype);
+ a
+---
+(0 rows)
+
+SELECT * FROM cypher('cypher_call', $$CREATE ({n: 'b'})$$) as (a agtype);
+ a
+---
+(0 rows)
+
+CREATE SCHEMA call_stmt_test;
+CREATE FUNCTION call_stmt_test.add_agtype(agtype, agtype) RETURNS agtype
+ AS 'select $1 + $2;'
+ LANGUAGE SQL
+ IMMUTABLE
+ RETURNS NULL ON NULL INPUT;
+/*
+ * CALL (solo)
+ */
+SELECT * FROM cypher('cypher_call', $$CALL sqrt(64)$$) as (sqrt agtype);
+ sqrt
+------
+ 8.0
+(1 row)
+
+/* CALL RETURN, should fail */
+SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) RETURN sqrt$$) as (sqrt agtype);
+ERROR: Procedure call inside a query does not support naming results implicitly
+LINE 2: SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) RETURN s...
+ ^
+HINT: Name explicitly using `YIELD` instead
+/* CALL YIELD */
+SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) YIELD sqrt$$) as (sqrt agtype);
+ sqrt
+------
+ 8.0
+(1 row)
+
+/* incorrect variable should fail */
+SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) YIELD squirt$$) as (sqrt agtype);
+ERROR: Unknown CALL output
+LINE 2: ... FROM cypher('cypher_call', $$CALL sqrt(64) YIELD squirt$$) ...
+ ^
+/* qualified name */
+SELECT * FROM cypher('cypher_call', $$CALL call_stmt_test.add_agtype(1,2)$$) as (sqrt agtype);
+ sqrt
+------
+ 3
+(1 row)
+
+/* non-existent schema should fail */
+SELECT * FROM cypher('cypher_call', $$CALL ag_catalog.add_agtype(1,2)$$) as (sqrt agtype);
+ERROR: function ag_catalog.add_agtype(agtype, agtype) does not exist
+LINE 2: ...cypher('cypher_call', $$CALL ag_catalog.add_agtype(1,2)$$) a...
+ ^
+HINT: No function matches the given name and argument types. You might need to add explicit type casts.
+/* CALL YIELD WHERE, should fail */
+SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) YIELD sqrt WHERE sqrt > 1$$) as (sqrt agtype);
+ERROR: Cannot use standalone CALL with WHERE
+LINE 2: ...r('cypher_call', $$CALL sqrt(64) YIELD sqrt WHERE sqrt > 1$$...
+ ^
+HINT: Instead use `CALL ... WITH * WHERE ... RETURN *`
+/*
+ * subquery
+ */
+ /* CALL YIELD UPDATE/RETURN */
+SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt RETURN sqrt $$) as (sqrt agtype);
+ sqrt
+------
+ 8.0
+(1 row)
+
+/* Unrecognized YIELD, correct RETURN, should fail*/
+SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD squirt RETURN sqrt $$) as (sqrt agtype);
+ERROR: Unknown CALL output
+LINE 2: ...FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD squirt RET...
+ ^
+/* CALL YIELD WHERE RETURN */
+SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) YIELD sqrt WHERE sqrt > 1 RETURN sqrt $$) as (a agtype);
+ a
+-----
+ 8.0
+(1 row)
+
+/* MATCH CALL RETURN, should fail */
+SELECT * FROM cypher('cypher_call', $$ MATCH (a) CALL sqrt(64) RETURN sqrt $$) as (sqrt agtype);
+ERROR: Procedure call inside a query does not support naming results implicitly
+LINE 2: SELECT * FROM cypher('cypher_call', $$ MATCH (a) CALL sqrt(6...
+ ^
+HINT: Name explicitly using `YIELD` instead
+/* MATCH CALL YIELD RETURN */
+SELECT * FROM cypher('cypher_call', $$ MATCH (a) CALL sqrt(64) YIELD sqrt RETURN a, sqrt $$) as (a agtype, sqrt agtype);
+ a | sqrt
+------------------------------------------------------------------------+------
+ {"id": 281474976710657, "label": "", "properties": {"n": "a"}}::vertex | 8.0
+ {"id": 281474976710658, "label": "", "properties": {"n": "b"}}::vertex | 8.0
+(2 rows)
+
+/* MATCH CALL YIELD WHERE UPDATE/RETURN */
+SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt WHERE sqrt > 1 CREATE ({n:'c'}) $$) as (a agtype);
+ a
+---
+(0 rows)
+
+SELECT * FROM cypher('cypher_call', $$ MATCH (a) CALL sqrt(64) YIELD sqrt WHERE a.n = 'c' DELETE (a) $$) as (a agtype);
+ a
+---
+(0 rows)
+
+SELECT * FROM cypher('cypher_call', $$ MATCH (a) CALL sqrt(64) YIELD sqrt WHERE a.n = 'c' RETURN a $$) as (a agtype);
+ a
+---
+(0 rows)
+
+/* CALL MATCH YIELD WHERE UPDATE/RETURN */
+SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt WHERE sqrt > 1 CREATE ({n:'c'}) $$) as (a agtype);
+ a
+---
+(0 rows)
+
+SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt MATCH (a) WHERE a.n = 'c' DELETE (a) $$) as (a agtype);
+ a
+---
+(0 rows)
+
+SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt MATCH (a) WHERE a.n = 'c' RETURN a $$) as (a agtype);
+ a
+---
+(0 rows)
+
+/* Multiple Calls: CALL YIELD CALL YIELD... RETURN */
+SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt CALL agtype_sum(2,2) YIELD agtype_sum RETURN sqrt, agtype_sum $$) as (sqrt agtype, agtype_sum agtype);
+ sqrt | agtype_sum
+------+------------
+ 8.0 | 4
+(1 row)
+
+/* should fail */
+SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt CALL sqrt(81) YIELD sqrt RETURN sqrt, sqrt $$) as (a agtype, b agtype);
+ERROR: duplicate variable "sqrt"
+LINE 2: ..., $$ CALL sqrt(64) YIELD sqrt CALL sqrt(81) YIELD sqrt RETUR...
+ ^
+/* Aliasing: CALL YIELD AS CALL YIELD AS ... UPDATE/RETURN */
+SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt CALL sqrt(81) YIELD sqrt AS sqrt1 RETURN sqrt, sqrt1 $$) as (sqrt agtype, sqrt1 agtype);
+ sqrt | sqrt1
+------+-------
+ 8.0 | 9.0
+(1 row)
+
+SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt AS sqrt1 CALL sqrt(81) YIELD sqrt RETURN sqrt, sqrt1 $$) as (sqrt agtype, sqrt1 agtype);
+ sqrt | sqrt1
+------+-------
+ 9.0 | 8.0
+(1 row)
+
+/* duplicated alias should fail */
+SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt AS sqrt1 CALL sqrt(81) YIELD sqrt AS sqrt1 RETURN sqrt1, sqrt1 $$) as (a agtype, b agtype);
+ERROR: duplicate variable "sqrt1"
+LINE 2: ... sqrt(64) YIELD sqrt AS sqrt1 CALL sqrt(81) YIELD sqrt AS sq...
+ ^
+SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt CALL agtype_sum(2,2) YIELD agtype_sum AS sqrt RETURN sqrt, sqrt $$) as (a agtype, b agtype);
+ERROR: duplicate variable "sqrt"
+LINE 1: ...LL sqrt(64) YIELD sqrt CALL agtype_sum(2,2) YIELD agtype_sum...
+ ^
+DROP SCHEMA call_stmt_test CASCADE;
+NOTICE: drop cascades to function call_stmt_test.add_agtype(agtype,agtype)
+SELECT drop_graph('cypher_call', true);
+NOTICE: drop cascades to 2 other objects
+DETAIL: drop cascades to table cypher_call._ag_label_vertex
+drop cascades to table cypher_call._ag_label_edge
+NOTICE: graph "cypher_call" has been dropped
+ drop_graph
+------------
+
+(1 row)
+
diff --git a/regress/sql/cypher_call.sql b/regress/sql/cypher_call.sql
new file mode 100644
index 000000000..403049a10
--- /dev/null
+++ b/regress/sql/cypher_call.sql
@@ -0,0 +1,99 @@
+/*
+ * 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;
+
+
+
+SELECT create_graph('cypher_call');
+
+SELECT * FROM cypher('cypher_call', $$CREATE ({n: 'a'})$$) as (a agtype);
+SELECT * FROM cypher('cypher_call', $$CREATE ({n: 'b'})$$) as (a agtype);
+
+CREATE SCHEMA call_stmt_test;
+
+CREATE FUNCTION call_stmt_test.add_agtype(agtype, agtype) RETURNS agtype
+ AS 'select $1 + $2;'
+ LANGUAGE SQL
+ IMMUTABLE
+ RETURNS NULL ON NULL INPUT;
+
+/*
+ * CALL (solo)
+ */
+
+SELECT * FROM cypher('cypher_call', $$CALL sqrt(64)$$) as (sqrt agtype);
+/* CALL RETURN, should fail */
+SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) RETURN sqrt$$) as (sqrt agtype);
+/* CALL YIELD */
+SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) YIELD sqrt$$) as (sqrt agtype);
+/* incorrect variable should fail */
+SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) YIELD squirt$$) as (sqrt agtype);
+
+/* qualified name */
+SELECT * FROM cypher('cypher_call', $$CALL call_stmt_test.add_agtype(1,2)$$) as (sqrt agtype);
+/* non-existent schema should fail */
+SELECT * FROM cypher('cypher_call', $$CALL ag_catalog.add_agtype(1,2)$$) as (sqrt agtype);
+
+/* CALL YIELD WHERE, should fail */
+SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) YIELD sqrt WHERE sqrt > 1$$) as (sqrt agtype);
+
+/*
+ * subquery
+ */
+
+ /* CALL YIELD UPDATE/RETURN */
+SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt RETURN sqrt $$) as (sqrt agtype);
+/* Unrecognized YIELD, correct RETURN, should fail*/
+SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD squirt RETURN sqrt $$) as (sqrt agtype);
+
+/* CALL YIELD WHERE RETURN */
+SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) YIELD sqrt WHERE sqrt > 1 RETURN sqrt $$) as (a agtype);
+
+/* MATCH CALL RETURN, should fail */
+SELECT * FROM cypher('cypher_call', $$ MATCH (a) CALL sqrt(64) RETURN sqrt $$) as (sqrt agtype);
+
+/* MATCH CALL YIELD RETURN */
+SELECT * FROM cypher('cypher_call', $$ MATCH (a) CALL sqrt(64) YIELD sqrt RETURN a, sqrt $$) as (a agtype, sqrt agtype);
+
+/* MATCH CALL YIELD WHERE UPDATE/RETURN */
+SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt WHERE sqrt > 1 CREATE ({n:'c'}) $$) as (a agtype);
+SELECT * FROM cypher('cypher_call', $$ MATCH (a) CALL sqrt(64) YIELD sqrt WHERE a.n = 'c' DELETE (a) $$) as (a agtype);
+SELECT * FROM cypher('cypher_call', $$ MATCH (a) CALL sqrt(64) YIELD sqrt WHERE a.n = 'c' RETURN a $$) as (a agtype);
+
+/* CALL MATCH YIELD WHERE UPDATE/RETURN */
+SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt WHERE sqrt > 1 CREATE ({n:'c'}) $$) as (a agtype);
+SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt MATCH (a) WHERE a.n = 'c' DELETE (a) $$) as (a agtype);
+SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt MATCH (a) WHERE a.n = 'c' RETURN a $$) as (a agtype);
+
+/* Multiple Calls: CALL YIELD CALL YIELD... RETURN */
+SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt CALL agtype_sum(2,2) YIELD agtype_sum RETURN sqrt, agtype_sum $$) as (sqrt agtype, agtype_sum agtype);
+/* should fail */
+SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt CALL sqrt(81) YIELD sqrt RETURN sqrt, sqrt $$) as (a agtype, b agtype);
+
+/* Aliasing: CALL YIELD AS CALL YIELD AS ... UPDATE/RETURN */
+SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt CALL sqrt(81) YIELD sqrt AS sqrt1 RETURN sqrt, sqrt1 $$) as (sqrt agtype, sqrt1 agtype);
+SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt AS sqrt1 CALL sqrt(81) YIELD sqrt RETURN sqrt, sqrt1 $$) as (sqrt agtype, sqrt1 agtype);
+/* duplicated alias should fail */
+SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt AS sqrt1 CALL sqrt(81) YIELD sqrt AS sqrt1 RETURN sqrt1, sqrt1 $$) as (a agtype, b agtype);
+SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt CALL agtype_sum(2,2) YIELD agtype_sum AS sqrt RETURN sqrt, sqrt $$) as (a agtype, b agtype);
+
+DROP SCHEMA call_stmt_test CASCADE;
+SELECT drop_graph('cypher_call', true);
\ No newline at end of file
diff --git a/src/backend/nodes/ag_nodes.c b/src/backend/nodes/ag_nodes.c
index d65bb5038..14715bd2e 100644
--- a/src/backend/nodes/ag_nodes.c
+++ b/src/backend/nodes/ag_nodes.c
@@ -52,6 +52,7 @@ const char *node_names[] = {
"cypher_typecast",
"cypher_integer_const",
"cypher_sub_pattern",
+ "cypher_call",
"cypher_create_target_nodes",
"cypher_create_path",
"cypher_target_node",
@@ -114,6 +115,7 @@ const ExtensibleNodeMethods node_methods[] = {
DEFINE_NODE_METHODS(cypher_typecast),
DEFINE_NODE_METHODS(cypher_integer_const),
DEFINE_NODE_METHODS(cypher_sub_pattern),
+ DEFINE_NODE_METHODS(cypher_call),
DEFINE_NODE_METHODS_EXTENDED(cypher_create_target_nodes),
DEFINE_NODE_METHODS_EXTENDED(cypher_create_path),
DEFINE_NODE_METHODS_EXTENDED(cypher_target_node),
diff --git a/src/backend/nodes/cypher_outfuncs.c b/src/backend/nodes/cypher_outfuncs.c
index 05c0f7f50..0c48fb5ae 100644
--- a/src/backend/nodes/cypher_outfuncs.c
+++ b/src/backend/nodes/cypher_outfuncs.c
@@ -296,6 +296,17 @@ void out_cypher_sub_pattern(StringInfo str, const ExtensibleNode *node)
WRITE_NODE_FIELD(pattern);
}
+// serialization function for the cypher_call ExtensibleNode.
+void out_cypher_call(StringInfo str, const ExtensibleNode *node)
+{
+ DEFINE_AG_NODE(cypher_call);
+
+ WRITE_NODE_FIELD(funccall);
+ WRITE_NODE_FIELD(funcexpr);
+ WRITE_NODE_FIELD(where);
+ WRITE_NODE_FIELD(yield_items);
+}
+
// serialization function for the cypher_create_target_nodes ExtensibleNode.
void out_cypher_create_target_nodes(StringInfo str, const ExtensibleNode *node)
{
diff --git a/src/backend/parser/cypher_clause.c b/src/backend/parser/cypher_clause.c
index 2269d3f3d..2c94fbad9 100644
--- a/src/backend/parser/cypher_clause.c
+++ b/src/backend/parser/cypher_clause.c
@@ -198,6 +198,9 @@ static void handle_prev_clause(cypher_parsestate *cpstate, Query *query,
cypher_clause *clause, bool first_rte);
static TargetEntry *placeholder_target_entry(cypher_parsestate *cpstate,
char *name);
+
+static List *makeTargetListFromRTE(ParseState *pstate, RangeTblEntry *rte);
+
static Query *transform_cypher_sub_pattern(cypher_parsestate *cpstate,
cypher_clause *clause);
// set and remove clause
@@ -261,6 +264,12 @@ static void
transform_cypher_merge_mark_tuple_position(List *target_list,
cypher_create_path *path);
+//call...[yield]
+static Query *transform_cypher_call_stmt(cypher_parsestate *cpstate,
+ cypher_clause *clause);
+static Query *transform_cypher_call_subquery(cypher_parsestate *cpstate,
+ cypher_clause *clause);
+
// transform
#define PREV_CYPHER_CLAUSE_ALIAS "_"
#define CYPHER_OPT_RIGHT_ALIAS "_R"
@@ -372,6 +381,10 @@ Query *transform_cypher_clause(cypher_parsestate *cpstate,
{
result = transform_cypher_unwind(cpstate, clause);
}
+ else if (is_ag_node(self, cypher_call))
+ {
+ result = transform_cypher_call_stmt(cpstate, clause);
+ }
else
{
ereport(ERROR, (errmsg_internal("unexpected Node for cypher_clause")));
@@ -1001,6 +1014,211 @@ transform_cypher_union_tree(cypher_parsestate *cpstate, cypher_clause *clause,
}//end else (is not leaf)
}
+/*
+ * Function that takes a cypher call and returns the yielded result
+ * This function also catches some cases that should fail that could not
+ * be picked up by the grammar. transform_cypher_call_subquery handles the
+ * call transformation itself.
+ */
+static Query * transform_cypher_call_stmt(cypher_parsestate *cpstate,
+ cypher_clause *clause)
+{
+ ParseState *pstate = (ParseState *)cpstate;
+ cypher_call *self = (cypher_call *)clause->self;
+
+ if (!clause->prev && !clause->next) /* CALL [YIELD] -- the most simple call */
+ {
+ if (self->where) /* Error check for WHERE clause after YIELD without RETURN */
+ {
+ Assert(self->yield_items);
+
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("Cannot use standalone CALL with WHERE"),
+ errhint("Instead use `CALL ... WITH * WHERE ... RETURN *`"),
+ parser_errposition(pstate,
+ exprLocation((Node *) self->where))));
+ }
+
+ return transform_cypher_call_subquery(cpstate, clause);
+ }
+ else /* subqueries */
+ {
+ if (!self->yield_items)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("Procedure call inside a query does not support naming results implicitly"),
+ errhint("Name explicitly using `YIELD` instead"),
+ parser_errposition(pstate,
+ exprLocation((Node *) self))));
+ }
+ Assert(self->yield_items);
+
+ if (!clause->next)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("Query cannot conclude with CALL"),
+ errhint("Must be RETURN or an update clause"),
+ parser_errposition(pstate,
+ exprLocation((Node *) self))));
+ }
+
+ return transform_cypher_clause_with_where(cpstate,
+ transform_cypher_call_subquery,
+ clause);
+ }
+
+ return NULL;
+}
+
+/*
+ * Helper routine for transform_cypher_call_stmt. This routine transforms the
+ * call statement and handles the YIELD clause.
+ */
+static Query *transform_cypher_call_subquery(cypher_parsestate *cpstate,
+ cypher_clause *clause)
+{
+ ParseState *pstate = (ParseState *)cpstate;
+ ParseState *p_child_parse_state = make_parsestate(NULL);
+ cypher_call *self = (cypher_call *)clause->self;
+ Query *query;
+ char *colName;
+ FuncExpr *node = NULL;
+ TargetEntry *tle;
+
+ Expr *where_qual = NULL;
+
+ query = makeNode(Query);
+ query->commandType = CMD_SELECT;
+
+ if (clause->prev)
+ {
+ /* we want to retain all previous range table entries */
+ handle_prev_clause(cpstate, query, clause->prev, false);
+ }
+
+ /* transform the funccall and store it in a funcexpr node */
+ node = castNode( FuncExpr, transform_cypher_expr(cpstate, (Node *) self->funccall,
+ EXPR_KIND_FROM_FUNCTION));
+
+ /* retrieve the column name from funccall */
+ colName = strVal(linitial(self->funccall->funcname));
+
+ /* make a targetentry from the funcexpr node */
+ tle = makeTargetEntry((Expr *) node,
+ (AttrNumber) p_child_parse_state->p_next_resno++,
+ colName,
+ false);
+
+ if (self->yield_items) /* if there are yield items, we need to check them */
+ {
+ List *yield_targetList;
+ ListCell *lc;
+
+ yield_targetList = list_make1(tle);
+
+ foreach (lc, self->yield_items)
+ {
+ ResTarget *target = NULL;
+ ColumnRef *var = NULL;
+ TargetEntry *yielded_tle = NULL;
+
+ target = (ResTarget *) lfirst(lc);
+
+ if (!IsA(target->val, ColumnRef))
+ {
+ ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("YIELD item must be ColumnRef"),
+ parser_errposition(&cpstate->pstate, 0)));
+ }
+
+ var = (ColumnRef *) target->val;
+
+ /* check if the restarget variable exists in the yield_targetList*/
+ if (findTarget(yield_targetList, strVal(linitial(var->fields))) != NULL)
+ {
+ /* check if an alias exists. if one does, we check if it is
+ already declared in the targetlist */
+ if (target->name)
+ {
+ if (findTarget(query->targetList, target->name) != NULL)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_ALIAS),
+ errmsg("duplicate variable \"%s\"", target->name),
+ parser_errposition((ParseState *) cpstate, exprLocation((Node *) target))));
+ }
+ else
+ {
+ yielded_tle = makeTargetEntry((Expr *) node,
+ (AttrNumber) pstate->p_next_resno++,
+ target->name,
+ false);
+ query->targetList = lappend(query->targetList, yielded_tle);
+ }
+ }
+ else/* if there is no alias, we check if the variable is already declared */
+ {
+ if (findTarget(query->targetList, strVal(linitial(var->fields))) != NULL)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_ALIAS),
+ errmsg("duplicate variable \"%s\"", colName),
+ parser_errposition((ParseState *) cpstate, exprLocation((Node *) target))));
+ }
+ else
+ {
+ yielded_tle = makeTargetEntry((Expr *) node,
+ (AttrNumber) pstate->p_next_resno++,
+ colName,
+ false);
+ query->targetList = lappend(query->targetList, yielded_tle);
+ }
+ }
+ }
+ else
+ {
+ /* if the yield_item is not found and we return an error */
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+ errmsg("Unknown CALL output"),
+ parser_errposition(pstate, exprLocation((Node *) target))));
+ }
+ }
+ }
+ else /* if there are no yield items this must be a solo call */
+ {
+ tle = makeTargetEntry((Expr *) node,
+ (AttrNumber) pstate->p_next_resno++,
+ colName,
+ false);
+ query->targetList = list_make1(tle);
+ }
+
+
+
+ markTargetListOrigins(pstate, query->targetList);
+
+ query->rtable = cpstate->pstate.p_rtable;
+ query->jointree = makeFromExpr(cpstate->pstate.p_joinlist, (Node *)where_qual);
+ query->hasAggs = pstate->p_hasAggs;
+
+ assign_query_collations(pstate, query);
+
+ /* this must be done after collations, for reliable comparison of exprs */
+ if (pstate->p_hasAggs ||
+ query->groupClause || query->groupingSets || query->havingQual)
+ {
+ parse_check_aggregates(pstate, query);
+ }
+
+ free_parsestate(p_child_parse_state);
+
+ return query;
+}
+
/*
* Transform the Delete clause. Creates a _cypher_delete_clause
* and passes the necessary information that is needed in the
@@ -1927,8 +2145,22 @@ static Query *transform_cypher_clause_with_where(cypher_parsestate *cpstate,
{
ParseState *pstate = (ParseState *)cpstate;
Query *query;
- cypher_match *self = (cypher_match *)clause->self;
- Node *where = self->where;
+ Node *self = clause->self;
+ cypher_match *match_self;
+ cypher_call *call_self;
+ Node *where;
+
+
+ if (is_ag_node(self, cypher_call))
+ {
+ call_self = (cypher_call*) clause->self;
+ where = call_self->where;
+ }
+ else
+ {
+ match_self = (cypher_match*) clause->self;
+ where = match_self->where;
+ }
if (where)
{
@@ -1948,8 +2180,27 @@ static Query *transform_cypher_clause_with_where(cypher_parsestate *cpstate,
markTargetListOrigins(pstate, query->targetList);
query->rtable = pstate->p_rtable;
- query->jointree = makeFromExpr(pstate->p_joinlist, NULL);
+ if (is_ag_node(clause, cypher_call))
+ {
+ cypher_call *call = (cypher_call *)clause->self;
+
+ if (call->where != NULL)
+ {
+ Expr *where_qual = NULL;
+
+ where_qual = (Expr *)transform_cypher_expr(cpstate, call->where,
+ EXPR_KIND_WHERE);
+
+ where_qual = (Expr *)coerce_to_boolean(pstate, (Node *)where_qual,
+ "WHERE");
+ query->jointree = makeFromExpr(pstate->p_joinlist, (Node *)where_qual);
+ }
+ }
+ else
+ {
+ query->jointree = makeFromExpr(pstate->p_joinlist, NULL);
+ }
assign_query_collations(pstate, query);
}
else
diff --git a/src/backend/parser/cypher_gram.y b/src/backend/parser/cypher_gram.y
index b751cdbe6..84eb957a1 100644
--- a/src/backend/parser/cypher_gram.y
+++ b/src/backend/parser/cypher_gram.y
@@ -331,17 +331,98 @@ cypher_stmt:
call_stmt:
CALL expr_func_norm
{
- ereport(ERROR,
- (errcode(ERRCODE_SYNTAX_ERROR),
- errmsg("CALL not supported yet"),
- ag_scanner_errposition(@1, scanner)));
+ cypher_call *n = make_ag_node(cypher_call);
+ n->funccall = castNode (FuncCall, $2);
+
+ $$ = (Node *)n;
+ }
+ | CALL expr '.' expr
+ {
+ cypher_call *n = make_ag_node(cypher_call);
+
+ if (IsA($4, FuncCall) && IsA($2, ColumnRef))
+ {
+ FuncCall *fc = (FuncCall*)$4;
+ ColumnRef *cr = (ColumnRef*)$2;
+ List *fields = cr->fields;
+ Value *string = linitial(fields);
+
+ /*
+ * A function can only be qualified with a single schema. So, we
+ * check to see that the function isn't already qualified. There
+ * may be unforeseen cases where we might need to remove this in
+ * the future.
+ */
+ if (list_length(fc->funcname) == 1)
+ {
+ fc->funcname = lcons(string, fc->funcname);
+ $$ = (Node*)fc;
+ }
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("function already qualified"),
+ ag_scanner_errposition(@1, scanner)));
+
+ n->funccall = fc;
+ $$ = (Node *)n;
+ }
+ else
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("CALL statement must be a qualified function"),
+ ag_scanner_errposition(@1, scanner)));
+ }
}
| CALL expr_func_norm YIELD yield_item_list where_opt
{
- ereport(ERROR,
- (errcode(ERRCODE_SYNTAX_ERROR),
- errmsg("CALL... [YIELD] not supported yet"),
- ag_scanner_errposition(@1, scanner)));
+ cypher_call *n = make_ag_node(cypher_call);
+ n->funccall = castNode (FuncCall, $2);
+ n->yield_items = $4;
+ n->where = $5;
+ $$ = (Node *)n;
+ }
+ | CALL expr '.' expr YIELD yield_item_list where_opt
+ {
+ cypher_call *n = make_ag_node(cypher_call);
+
+ if (IsA($4, FuncCall) && IsA($2, ColumnRef))
+ {
+ FuncCall *fc = (FuncCall*)$4;
+ ColumnRef *cr = (ColumnRef*)$2;
+ List *fields = cr->fields;
+ Value *string = linitial(fields);
+
+ /*
+ * A function can only be qualified with a single schema. So, we
+ * check to see that the function isn't already qualified. There
+ * may be unforeseen cases where we might need to remove this in
+ * the future.
+ */
+ if (list_length(fc->funcname) == 1)
+ {
+ fc->funcname = lcons(string, fc->funcname);
+ $$ = (Node*)fc;
+ }
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("function already qualified"),
+ ag_scanner_errposition(@1, scanner)));
+
+ n->funccall = fc;
+ n->yield_items = $6;
+ n->where = $7;
+ $$ = (Node *)n;
+ }
+ else
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("CALL statement must be a qualified function"),
+ ag_scanner_errposition(@1, scanner)));
+ }
}
;
@@ -359,14 +440,31 @@ yield_item_list:
yield_item:
expr AS var_name
{
+ ResTarget *n;
+ n = makeNode(ResTarget);
+ n->name = $3;
+ n->indirection = NIL;
+ n->val = $1;
+ n->location = @1;
+
+ $$ = (Node *)n;
}
| expr
{
+ ResTarget *n;
+ n = makeNode(ResTarget);
+ n->name = NULL;
+ n->indirection = NIL;
+ n->val = $1;
+ n->location = @1;
+
+ $$ = (Node *)n;
}
;
+
semicolon_opt:
/* empty */
| ';'
@@ -419,6 +517,10 @@ query_part_last:
{
$$ = lappend(list_concat($1, $2), $3);
}
+ | reading_clause_list call_stmt
+ {
+ $$ = list_concat($1, list_make1($2));
+ }
;
reading_clause_list:
@@ -435,6 +537,7 @@ reading_clause_list:
reading_clause:
match
| unwind
+ | call_stmt
;
updating_clause_list_0:
@@ -462,7 +565,6 @@ updating_clause:
| remove
| delete
| merge
- | call_stmt
;
cypher_varlen_opt:
diff --git a/src/include/nodes/ag_nodes.h b/src/include/nodes/ag_nodes.h
index 15ff409f1..e51a8de43 100644
--- a/src/include/nodes/ag_nodes.h
+++ b/src/include/nodes/ag_nodes.h
@@ -59,6 +59,8 @@ typedef enum ag_node_tag
cypher_integer_const_t,
// sub patterns
cypher_sub_pattern_t,
+ // procedure calls
+ cypher_call_t,
// create data structures
cypher_create_target_nodes_t,
cypher_create_path_t,
diff --git a/src/include/nodes/cypher_nodes.h b/src/include/nodes/cypher_nodes.h
index fe9f6bd70..792edde35 100644
--- a/src/include/nodes/cypher_nodes.h
+++ b/src/include/nodes/cypher_nodes.h
@@ -236,6 +236,20 @@ typedef struct cypher_create_path
char *var_name;
} cypher_create_path;
+/*
+ * procedure call
+ */
+
+typedef struct cypher_call
+{
+ ExtensibleNode extensible;
+ FuncCall *funccall; /*from the parser */
+ FuncExpr *funcexpr; /*transformed */
+
+ Node *where;
+ List *yield_items; // optional yield subclause
+} cypher_call;
+
#define CYPHER_CLAUSE_FLAG_NONE 0x0000
#define CYPHER_CLAUSE_FLAG_TERMINAL 0x0001
#define CYPHER_CLAUSE_FLAG_PREVIOUS_CLAUSE 0x0002
diff --git a/src/include/nodes/cypher_outfuncs.h b/src/include/nodes/cypher_outfuncs.h
index 20ef73fa9..836765bc9 100644
--- a/src/include/nodes/cypher_outfuncs.h
+++ b/src/include/nodes/cypher_outfuncs.h
@@ -65,6 +65,10 @@ void out_cypher_integer_const(StringInfo str, const ExtensibleNode *node);
// sub pattern
void out_cypher_sub_pattern(StringInfo str, const ExtensibleNode *node);
+// procedure call
+
+void out_cypher_call(StringInfo str, const ExtensibleNode *node);
+
// create private data structures
void out_cypher_create_target_nodes(StringInfo str, const ExtensibleNode *node);
void out_cypher_create_path(StringInfo str, const ExtensibleNode *node);
From dbc4584786655f848cf698a04713f47bb73f8ca9 Mon Sep 17 00:00:00 2001
From: Shoaib
Date: Mon, 22 Aug 2022 17:38:01 +0200
Subject: [PATCH 005/191] Graph names with empty string '' are no more allowed.
(#251)
---
regress/expected/catalog.out | 6 ++++--
regress/sql/catalog.sql | 2 +-
src/backend/commands/graph_commands.c | 11 +++++++++--
3 files changed, 14 insertions(+), 5 deletions(-)
diff --git a/regress/expected/catalog.out b/regress/expected/catalog.out
index 8a83f45a6..4fca3e6ab 100644
--- a/regress/expected/catalog.out
+++ b/regress/expected/catalog.out
@@ -97,9 +97,11 @@ SELECT count(*) FROM pg_namespace WHERE nspname = 'g';
-- invalid cases
SELECT create_graph(NULL);
-ERROR: graph name must not be NULL
+ERROR: graph name can not be NULL
SELECT drop_graph(NULL);
-ERROR: graph name must not be NULL
+ERROR: graph name can not be NULL
+SELECT create_graph('');
+ERROR: graph name can not be empty
--
-- alter_graph() RENAME function tests
--
diff --git a/regress/sql/catalog.sql b/regress/sql/catalog.sql
index 6bc19814c..59a720f06 100644
--- a/regress/sql/catalog.sql
+++ b/regress/sql/catalog.sql
@@ -52,7 +52,7 @@ SELECT count(*) FROM pg_namespace WHERE nspname = 'g';
-- invalid cases
SELECT create_graph(NULL);
SELECT drop_graph(NULL);
-
+SELECT create_graph('');
--
-- alter_graph() RENAME function tests
--
diff --git a/src/backend/commands/graph_commands.c b/src/backend/commands/graph_commands.c
index c298324c8..bb21f8a9c 100644
--- a/src/backend/commands/graph_commands.c
+++ b/src/backend/commands/graph_commands.c
@@ -67,11 +67,18 @@ Datum create_graph(PG_FUNCTION_ARGS)
if (PG_ARGISNULL(0))
{
ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
- errmsg("graph name must not be NULL")));
+ errmsg("graph name can not be NULL")));
}
graph_name = PG_GETARG_NAME(0);
graph_name_str = NameStr(*graph_name);
+
+ if (strlen(graph_name_str) == 0)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("graph name can not be empty")));
+ }
if (graph_exists(graph_name_str))
{
ereport(ERROR,
@@ -158,7 +165,7 @@ Datum drop_graph(PG_FUNCTION_ARGS)
if (PG_ARGISNULL(0))
{
ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
- errmsg("graph name must not be NULL")));
+ errmsg("graph name can not be NULL")));
}
graph_name = PG_GETARG_NAME(0);
cascade = PG_GETARG_BOOL(1);
From e426e947a250d0dfa9e4ca75fed29cc4e290caae Mon Sep 17 00:00:00 2001
From: Andrew Ko <76510292+aked21@users.noreply.github.com>
Date: Tue, 6 Sep 2022 13:21:09 +0900
Subject: [PATCH 006/191] Update README.md
Updating the latest release
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 285496329..6df5917d5 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,7 @@ Intelligent -- AGE allows you to perform graph queries that are the basis for ma
## Latest happenings
-- Latest Apache AGE release, [Apache AGE 1.0.0 (https://github.com/apache/age/releases/tag/v1.1.0-rc1).
+- Latest Apache AGE release, [Apache AGE 1.1.0 (https://github.com/apache/age/releases/tag/v1.1.0-rc0).
- The latest Apache AGE documentation is now available at [here](https://age.apache.org/docs/master/index.html).
- The roadmap has been updated, please check out the [Apache AGE website](http://age.apache.org/).
- Send all your comments and inquiries to the user mailing list, users@age.apache.org.
From 4547d2d9b5178e5835e96738e2a2b17ec9e1fc76 Mon Sep 17 00:00:00 2001
From: Tomasz Durda
Date: Sun, 4 Sep 2022 13:10:19 +0200
Subject: [PATCH 007/191] Updated drivers, minor docs fixes
---
README.md | 21 ++++++++++++++++-----
1 file changed, 16 insertions(+), 5 deletions(-)
diff --git a/README.md b/README.md
index 6df5917d5..a01a1c504 100644
--- a/README.md
+++ b/README.md
@@ -27,11 +27,11 @@ Intelligent -- AGE allows you to perform graph queries that are the basis for ma
## Latest happenings
-- Latest Apache AGE release, [Apache AGE 1.1.0 (https://github.com/apache/age/releases/tag/v1.1.0-rc0).
+- Latest Apache AGE release, [Apache AGE 1.1.0](https://github.com/apache/age/releases/tag/v1.1.0-rc1).
- The latest Apache AGE documentation is now available at [here](https://age.apache.org/docs/master/index.html).
-- The roadmap has been updated, please check out the [Apache AGE website](http://age.apache.org/).
+- The roadmap - [Apache AGE website](http://age.apache.org/).
- Send all your comments and inquiries to the user mailing list, users@age.apache.org.
-- To focus more on implementing the openCypher specification, the support for PostgreSQL 12 will be added in the Q1 2022.
+- To focus more on implementing the openCypher specification, the support for PostgreSQL 12 will be added in the Q4 2022.
## Installation
@@ -40,7 +40,7 @@ Intelligent -- AGE allows you to perform graph queries that are the basis for ma
## Graph visualization tool for AGE
-Apache AGE Viewer is a subproject of the Apache AGE project: https://github.com/apache/age-viewer
+Apache AGE Viewer is a subproject of the Apache AGE project:
- This is a visualization tool.
After AGE Extension Installation
@@ -54,7 +54,18 @@ Here is the link to the latest [Apache AGE documentation](https://age.apache.org
You can learn about how to install Apache AGE, its features and built-in functions and how to use various Cypher queries.
## Language Specific Drivers
-[Apache AGE Python Driver](https://github.com/rhizome-ai/apache-age-python)
+
+### Built-in
+
+- [Go driver](./drivers/golang)
+- [Java driver](./drivers/jdbc)
+- [NodeJs driver](./drivers/nodejs)
+- [Python driver](./drivers/python)
+
+### Community
+
+- [Apache AGE Python Driver](https://github.com/rhizome-ai/apache-age-python)
+- [Apache AGE Rust Driver](https://github.com/Dzordzu/rust-apache-age.git)
## Contribution
From 6283dd4f6f0429e9541ee0a115e775c3dd5caead Mon Sep 17 00:00:00 2001
From: gorgonzola <76480303+gorgonzola-oh@users.noreply.github.com>
Date: Wed, 7 Sep 2022 16:37:31 +0900
Subject: [PATCH 008/191] Update README.md
fixes
- documentation link
- age viewer description
---
README.md | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index a01a1c504..d21bbe1ff 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,10 @@ Intelligent -- AGE allows you to perform graph queries that are the basis for ma
## Graph visualization tool for AGE
-Apache AGE Viewer is a subproject of the Apache AGE project:
+Apache AGE Viewer is a user interface for Apache AGE to provide visualization and exploration of data.
+It provides a simple web visualization tool for users to enter complex graph queries and explore the results in graph and table forms.
+Apache AGE Viewer is enhanced to proceed with large graph data and discover the insights through various graph algorithms.
+Apache AGE Viewer will become a graph data administration and development platform for Apache AGE to support multiple relational databases: .
- This is a visualization tool.
After AGE Extension Installation
@@ -50,7 +53,7 @@ Under Connect to Database , select database type as "Apache AGE"
## Documentation
-Here is the link to the latest [Apache AGE documentation](https://age.apache.org/docs/master/index.html).
+Here is the link to the latest [Apache AGE documentation](https://age.apache.org/age-manual/master/index.html).
You can learn about how to install Apache AGE, its features and built-in functions and how to use various Cypher queries.
## Language Specific Drivers
From 9407ece8c6c8c3d10936893b9f23ce0cd03de590 Mon Sep 17 00:00:00 2001
From: Pieterjan De Potter
Date: Thu, 8 Sep 2022 12:07:05 +0200
Subject: [PATCH 009/191] Update documentation link in README.md
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index d21bbe1ff..e84ceb706 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
[](https://github.com/apache/age/network/members)
[](https://github.com/apache/age/stargazers)
-
+
Apache AGE is a PostgreSQL Extension that provides graph database functionality. AGE is an acronym for A Graph Extension, and is inspired by Bitnine's fork of PostgreSQL 10, AgensGraph, which is a multi-model database. The goal of the project is to create single storage that can handle both relational and graph model data so that users can use standard ANSI SQL along with openCypher, the Graph query language.
@@ -28,7 +28,7 @@ Intelligent -- AGE allows you to perform graph queries that are the basis for ma
## Latest happenings
- Latest Apache AGE release, [Apache AGE 1.1.0](https://github.com/apache/age/releases/tag/v1.1.0-rc1).
-- The latest Apache AGE documentation is now available at [here](https://age.apache.org/docs/master/index.html).
+- The latest Apache AGE documentation is now available at [here](https://age.apache.org/age-manual/master/index.html).
- The roadmap - [Apache AGE website](http://age.apache.org/).
- Send all your comments and inquiries to the user mailing list, users@age.apache.org.
- To focus more on implementing the openCypher specification, the support for PostgreSQL 12 will be added in the Q4 2022.
From bae4fac865e496b028f7dcaf5e1bbf1b7553e6b6 Mon Sep 17 00:00:00 2001
From: Jasper Blues
Date: Fri, 16 Sep 2022 12:02:58 +1000
Subject: [PATCH 010/191] Update README.md
Add another important use-case.
---
README.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/README.md b/README.md
index e84ceb706..f5371e823 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,8 @@ Powerful -- AGE adds graph database support to the already popular PostgreSQL da
Flexible -- AGE allows you to perform openCypher queries, which make complex queries much easier to write.
Intelligent -- AGE allows you to perform graph queries that are the basis for many next level web services such as fraud & intrusion detection, master data management, product recommendations, identity and relationship management, experience personalization, knowledge management and more.
+Also, while the technology can be integrated with against many data layers, a graph database is also the perfect companion for a [GraphQL](https://graphql.org/) API! Since the information is already in a native format, it simplifies many factors and even allows many of the default operations to be generated automatically. GraphQL is rapidly superceeding REST as the standard for cloud applications.
+
## Overview
- **Apache AGE is currently being developed for the PostgreSQL 11 release** and will support PostgreSQL 12, 13 and all the future releases of PostgreSQL.
From aad430ce4103a18632afc7ef7d6916961271f359 Mon Sep 17 00:00:00 2001
From: Jasper Blues
Date: Sat, 17 Sep 2022 12:20:05 +1000
Subject: [PATCH 011/191] Update README.md
fix typo.
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index f5371e823..40e3a47e2 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ Powerful -- AGE adds graph database support to the already popular PostgreSQL da
Flexible -- AGE allows you to perform openCypher queries, which make complex queries much easier to write.
Intelligent -- AGE allows you to perform graph queries that are the basis for many next level web services such as fraud & intrusion detection, master data management, product recommendations, identity and relationship management, experience personalization, knowledge management and more.
-Also, while the technology can be integrated with against many data layers, a graph database is also the perfect companion for a [GraphQL](https://graphql.org/) API! Since the information is already in a native format, it simplifies many factors and even allows many of the default operations to be generated automatically. GraphQL is rapidly superceeding REST as the standard for cloud applications.
+Also, while the technology can be integrated against many data layers, a graph database is also the perfect companion for a [GraphQL](https://graphql.org/) API! Since the information is already in a native format, it simplifies many factors and even allows many of the default operations to be generated automatically. GraphQL is rapidly superceeding REST as the standard for cloud applications.
## Overview
From c7cc0bf227fa01263cdb7421a6ae71e23638bc92 Mon Sep 17 00:00:00 2001
From: Jasper Blues
Date: Sat, 17 Sep 2022 12:21:05 +1000
Subject: [PATCH 012/191] Update README.md
Improve wording.
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 40e3a47e2..ffe110de7 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ Powerful -- AGE adds graph database support to the already popular PostgreSQL da
Flexible -- AGE allows you to perform openCypher queries, which make complex queries much easier to write.
Intelligent -- AGE allows you to perform graph queries that are the basis for many next level web services such as fraud & intrusion detection, master data management, product recommendations, identity and relationship management, experience personalization, knowledge management and more.
-Also, while the technology can be integrated against many data layers, a graph database is also the perfect companion for a [GraphQL](https://graphql.org/) API! Since the information is already in a native format, it simplifies many factors and even allows many of the default operations to be generated automatically. GraphQL is rapidly superceeding REST as the standard for cloud applications.
+Also, while the technology can be integrated against many data layers, a graph database is also the perfect companion for a [GraphQL](https://graphql.org/) API! Since the information is already in a native format, it simplifies many factors and even allows many operations to be generated automatically. GraphQL is rapidly superceeding REST as the standard for cloud applications.
## Overview
From 015434ea586ae298a9bebe24dadd3212efc86451 Mon Sep 17 00:00:00 2001
From: Dehowe Feng
Date: Thu, 29 Sep 2022 14:34:37 -0700
Subject: [PATCH 013/191] Fix Bug with CALL... YIELD clause ignores WHERE
Typo fix where an if statement check was causing a logic error that
caused the where statement to be ignored.
Regression tests added to address this and thoroughly check WHERE
clauses as well.
Co-authored by: John Gemignani
---
regress/expected/cypher_call.out | 51 ++++++++++++++++++++++++++++++
regress/sql/cypher_call.sql | 10 ++++++
src/backend/parser/cypher_clause.c | 2 +-
3 files changed, 62 insertions(+), 1 deletion(-)
diff --git a/regress/expected/cypher_call.out b/regress/expected/cypher_call.out
index e9f9e346e..a7862b5d0 100644
--- a/regress/expected/cypher_call.out
+++ b/regress/expected/cypher_call.out
@@ -109,6 +109,22 @@ SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) YIELD sqrt WHERE sqrt > 1 RE
8.0
(1 row)
+SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) YIELD sqrt WHERE sqrt = 1 RETURN sqrt $$) as (a agtype);
+ a
+---
+(0 rows)
+
+SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) YIELD sqrt WHERE sqrt = 8 RETURN sqrt $$) as (a agtype);
+ a
+-----
+ 8.0
+(1 row)
+
+/* should fail */
+SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) YIELD sqrt WHERE a = 8 RETURN sqrt $$) as (a agtype);
+ERROR: could not find rte for a
+LINE 2: ...r('cypher_call', $$CALL sqrt(64) YIELD sqrt WHERE a = 8 RETU...
+ ^
/* MATCH CALL RETURN, should fail */
SELECT * FROM cypher('cypher_call', $$ MATCH (a) CALL sqrt(64) RETURN sqrt $$) as (sqrt agtype);
ERROR: Procedure call inside a query does not support naming results implicitly
@@ -139,6 +155,22 @@ SELECT * FROM cypher('cypher_call', $$ MATCH (a) CALL sqrt(64) YIELD sqrt WHERE
---
(0 rows)
+SELECT * FROM cypher('cypher_call', $$ MATCH (a) CALL sqrt(64) YIELD sqrt WHERE sqrt = 1 RETURN a, sqrt $$) as (a agtype, sqrt agtype);
+ a | sqrt
+---+------
+(0 rows)
+
+SELECT * FROM cypher('cypher_call', $$ MATCH (a) CALL sqrt(64) YIELD sqrt WHERE sqrt = 8 RETURN a, sqrt $$) as (a agtype, sqrt agtype);
+ a | sqrt
+------------------------------------------------------------------------+------
+ {"id": 281474976710657, "label": "", "properties": {"n": "a"}}::vertex | 8.0
+ {"id": 281474976710658, "label": "", "properties": {"n": "b"}}::vertex | 8.0
+(2 rows)
+
+SELECT * FROM cypher('cypher_call', $$ MATCH (a) CALL sqrt(64) YIELD sqrt WHERE b = 8 RETURN a, sqrt $$) as (a agtype, sqrt agtype);
+ERROR: could not find rte for b
+LINE 1: ...all', $$ MATCH (a) CALL sqrt(64) YIELD sqrt WHERE b = 8 RETU...
+ ^
/* CALL MATCH YIELD WHERE UPDATE/RETURN */
SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt WHERE sqrt > 1 CREATE ({n:'c'}) $$) as (a agtype);
a
@@ -155,6 +187,25 @@ SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt MATCH (a) WHERE
---
(0 rows)
+SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt MATCH (a) WHERE sqrt = 1 RETURN a, sqrt $$) as (a agtype, sqrt agtype);
+ a | sqrt
+---+------
+(0 rows)
+
+SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt MATCH (a) WHERE sqrt = 8 RETURN a, sqrt $$) as (a agtype, sqrt agtype);
+ a | sqrt
+------------------------------------------------------------------------+------
+ {"id": 281474976710657, "label": "", "properties": {"n": "a"}}::vertex | 8.0
+ {"id": 281474976710658, "label": "", "properties": {"n": "b"}}::vertex | 8.0
+(2 rows)
+
+SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt WHERE sqrt = 8 MATCH (a) RETURN a, sqrt $$) as (a agtype, sqrt agtype);
+ a | sqrt
+------------------------------------------------------------------------+------
+ {"id": 281474976710657, "label": "", "properties": {"n": "a"}}::vertex | 8.0
+ {"id": 281474976710658, "label": "", "properties": {"n": "b"}}::vertex | 8.0
+(2 rows)
+
/* Multiple Calls: CALL YIELD CALL YIELD... RETURN */
SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt CALL agtype_sum(2,2) YIELD agtype_sum RETURN sqrt, agtype_sum $$) as (sqrt agtype, agtype_sum agtype);
sqrt | agtype_sum
diff --git a/regress/sql/cypher_call.sql b/regress/sql/cypher_call.sql
index 403049a10..9f600a96f 100644
--- a/regress/sql/cypher_call.sql
+++ b/regress/sql/cypher_call.sql
@@ -66,6 +66,10 @@ SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD squirt RETURN sqrt $$
/* CALL YIELD WHERE RETURN */
SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) YIELD sqrt WHERE sqrt > 1 RETURN sqrt $$) as (a agtype);
+SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) YIELD sqrt WHERE sqrt = 1 RETURN sqrt $$) as (a agtype);
+SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) YIELD sqrt WHERE sqrt = 8 RETURN sqrt $$) as (a agtype);
+/* should fail */
+SELECT * FROM cypher('cypher_call', $$CALL sqrt(64) YIELD sqrt WHERE a = 8 RETURN sqrt $$) as (a agtype);
/* MATCH CALL RETURN, should fail */
SELECT * FROM cypher('cypher_call', $$ MATCH (a) CALL sqrt(64) RETURN sqrt $$) as (sqrt agtype);
@@ -77,11 +81,17 @@ SELECT * FROM cypher('cypher_call', $$ MATCH (a) CALL sqrt(64) YIELD sqrt RETURN
SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt WHERE sqrt > 1 CREATE ({n:'c'}) $$) as (a agtype);
SELECT * FROM cypher('cypher_call', $$ MATCH (a) CALL sqrt(64) YIELD sqrt WHERE a.n = 'c' DELETE (a) $$) as (a agtype);
SELECT * FROM cypher('cypher_call', $$ MATCH (a) CALL sqrt(64) YIELD sqrt WHERE a.n = 'c' RETURN a $$) as (a agtype);
+SELECT * FROM cypher('cypher_call', $$ MATCH (a) CALL sqrt(64) YIELD sqrt WHERE sqrt = 1 RETURN a, sqrt $$) as (a agtype, sqrt agtype);
+SELECT * FROM cypher('cypher_call', $$ MATCH (a) CALL sqrt(64) YIELD sqrt WHERE sqrt = 8 RETURN a, sqrt $$) as (a agtype, sqrt agtype);
+SELECT * FROM cypher('cypher_call', $$ MATCH (a) CALL sqrt(64) YIELD sqrt WHERE b = 8 RETURN a, sqrt $$) as (a agtype, sqrt agtype);
/* CALL MATCH YIELD WHERE UPDATE/RETURN */
SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt WHERE sqrt > 1 CREATE ({n:'c'}) $$) as (a agtype);
SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt MATCH (a) WHERE a.n = 'c' DELETE (a) $$) as (a agtype);
SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt MATCH (a) WHERE a.n = 'c' RETURN a $$) as (a agtype);
+SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt MATCH (a) WHERE sqrt = 1 RETURN a, sqrt $$) as (a agtype, sqrt agtype);
+SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt MATCH (a) WHERE sqrt = 8 RETURN a, sqrt $$) as (a agtype, sqrt agtype);
+SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt WHERE sqrt = 8 MATCH (a) RETURN a, sqrt $$) as (a agtype, sqrt agtype);
/* Multiple Calls: CALL YIELD CALL YIELD... RETURN */
SELECT * FROM cypher('cypher_call', $$ CALL sqrt(64) YIELD sqrt CALL agtype_sum(2,2) YIELD agtype_sum RETURN sqrt, agtype_sum $$) as (sqrt agtype, agtype_sum agtype);
diff --git a/src/backend/parser/cypher_clause.c b/src/backend/parser/cypher_clause.c
index 2c94fbad9..70afb5fa4 100644
--- a/src/backend/parser/cypher_clause.c
+++ b/src/backend/parser/cypher_clause.c
@@ -2181,7 +2181,7 @@ static Query *transform_cypher_clause_with_where(cypher_parsestate *cpstate,
query->rtable = pstate->p_rtable;
- if (is_ag_node(clause, cypher_call))
+ if (is_ag_node(clause->self, cypher_call))
{
cypher_call *call = (cypher_call *)clause->self;
From c9c07f71123d2f11d6209188633dff1ad341d23a Mon Sep 17 00:00:00 2001
From: John Gemignani
Date: Tue, 4 Oct 2022 12:28:35 -0700
Subject: [PATCH 014/191] Fix EXPLAIN to allow for nested cypher commands
Fixed the EXPLAIN command to allow for nested cypher commands within
more complex SQL queries.
---
src/backend/parser/cypher_analyze.c | 117 ++++++++++++++++++----------
1 file changed, 77 insertions(+), 40 deletions(-)
diff --git a/src/backend/parser/cypher_analyze.c b/src/backend/parser/cypher_analyze.c
index 15222ffa2..1a8ea23e5 100644
--- a/src/backend/parser/cypher_analyze.c
+++ b/src/backend/parser/cypher_analyze.c
@@ -44,7 +44,17 @@
#include "utils/ag_func.h"
#include "utils/agtype.h"
+/*
+ * extra_node is a global variable to this source to store, at the moment, the
+ * explain stmt node passed up by the parser. The return value from the parser
+ * contains an 'extra' value, hence the name.
+ */
static Node *extra_node = NULL;
+/*
+ * Takes a query node and builds an explain stmt query node. It then replaces
+ * the passed query node with the new explain stmt query node.
+ */
+static void build_explain_query(Query *query, Node *explain_node);
static post_parse_analyze_hook_type prev_post_parse_analyze_hook;
@@ -79,9 +89,34 @@ void post_parse_analyze_fini(void)
static void post_parse_analyze(ParseState *pstate, Query *query)
{
if (prev_post_parse_analyze_hook)
+ {
prev_post_parse_analyze_hook(pstate, query);
+ }
+
+ /*
+ * extra_node is set in the parsing stage to keep track of EXPLAIN.
+ * So it needs to be set to NULL prior to any cypher parsing.
+ */
+ extra_node = NULL;
convert_cypher_walker((Node *)query, pstate);
+
+ /*
+ * If there is an extra_node returned, we need to check to see if
+ * it is an EXPLAIN.
+ */
+ if (extra_node != NULL)
+ {
+ /* process the EXPLAIN node */
+ if (nodeTag(extra_node) == T_ExplainStmt)
+ {
+ build_explain_query(query, extra_node);
+ }
+
+ /* reset extra_node */
+ pfree(extra_node);
+ extra_node = NULL;
+ }
}
// find cypher() calls in FROM clauses and convert them to SELECT subqueries
@@ -178,57 +213,55 @@ static bool convert_cypher_walker(Node *node, ParseState *pstate)
flags = QTW_EXAMINE_RTES | QTW_IGNORE_RT_SUBQUERIES |
QTW_IGNORE_JOINALIASES;
- /* clear the global variable extra_node */
- extra_node = NULL;
-
/* recurse on query */
result = query_tree_walker(query, convert_cypher_walker, pstate, flags);
- /* check for EXPLAIN */
- if (extra_node != NULL && nodeTag(extra_node) == T_ExplainStmt)
- {
- ExplainStmt *estmt = NULL;
- Query *query_copy = NULL;
- Query *query_node = NULL;
+ return result;
+ }
- /*
- * Create a copy of the query node. This is purposely a shallow copy
- * because we are only moving the contents to another pointer.
- */
- query_copy = (Query *) palloc(sizeof(Query));
- memcpy(query_copy, query, sizeof(Query));
+ return expression_tree_walker(node, convert_cypher_walker, pstate);
+}
- /* build our Explain node and store the query node copy in it */
- estmt = makeNode(ExplainStmt);
- estmt->query = (Node *)query_copy;
- estmt->options = ((ExplainStmt *)extra_node)->options;
+/*
+ * Takes a query node and builds an explain stmt query node. It then replaces
+ * the passed query node with the new explain stmt query node.
+ */
+static void build_explain_query(Query *query, Node *explain_node)
+{
+ ExplainStmt *estmt = NULL;
+ Query *query_copy = NULL;
+ Query *query_node = NULL;
- /* build our replacement query node */
- query_node = makeNode(Query);
- query_node->commandType = CMD_UTILITY;
- query_node->utilityStmt = (Node *)estmt;
- query_node->canSetTag = true;
+ /*
+ * Create a copy of the query node. This is purposely a shallow copy
+ * because we are only moving the contents to another pointer.
+ */
+ query_copy = (Query *) palloc(sizeof(Query));
+ memcpy(query_copy, query, sizeof(Query));
- /* now replace the top query node with our replacement query node */
- memcpy(query, query_node, sizeof(Query));
+ /* build our Explain node and store the query node copy in it */
+ estmt = makeNode(ExplainStmt);
+ estmt->query = (Node *)query_copy;
+ estmt->options = ((ExplainStmt *)explain_node)->options;
- /*
- * We need to free and clear the global variable when done. But, not
- * the ExplainStmt options. Those will get freed by PG when the
- * query is deleted.
- */
- ((ExplainStmt *)extra_node)->options = NULL;
- pfree(extra_node);
- extra_node = NULL;
+ /* build our replacement query node */
+ query_node = makeNode(Query);
+ query_node->commandType = CMD_UTILITY;
+ query_node->utilityStmt = (Node *)estmt;
+ query_node->canSetTag = true;
- /* we need to free query_node as it is no longer needed */
- pfree(query_node);
- }
+ /* now replace the top query node with our replacement query node */
+ memcpy(query, query_node, sizeof(Query));
- return result;
- }
+ /*
+ * We need to free and clear the global variable when done. But, not
+ * the ExplainStmt options. Those will get freed by PG when the
+ * query is deleted.
+ */
+ ((ExplainStmt *)explain_node)->options = NULL;
- return expression_tree_walker(node, convert_cypher_walker, pstate);
+ /* we need to free query_node as it is no longer needed */
+ pfree(query_node);
}
static bool is_rte_cypher(RangeTblEntry *rte)
@@ -386,6 +419,10 @@ static void convert_cypher_to_subquery(RangeTblEntry *rte, ParseState *pstate)
{
Node *temp = llast(stmt);
+ ereport(WARNING,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("too many extra_nodes passed from parser")));
+
list_delete_ptr(stmt, temp);
}
From 07d9133b337cbf776f8c71e95424f0ce2f2ad264 Mon Sep 17 00:00:00 2001
From: Nataliapark <112913392+Nataliapark@users.noreply.github.com>
Date: Tue, 11 Oct 2022 09:41:07 +0900
Subject: [PATCH 015/191] Update README.md (#322)
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index ffe110de7..1df66d8fd 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,7 @@ Also, while the technology can be integrated against many data layers, a graph d
## Latest happenings
-- Latest Apache AGE release, [Apache AGE 1.1.0](https://github.com/apache/age/releases/tag/v1.1.0-rc1).
+- The latest Apache AGE release, [Apache AGE 1.1.0](https://github.com/apache/age/releases/tag/v1.1.0-rc1).
- The latest Apache AGE documentation is now available at [here](https://age.apache.org/age-manual/master/index.html).
- The roadmap - [Apache AGE website](http://age.apache.org/).
- Send all your comments and inquiries to the user mailing list, users@age.apache.org.
From 623ed005ce677a8df2917d90b497ac9204d36327 Mon Sep 17 00:00:00 2001
From: jbiz805 <49049733+jbiz805@users.noreply.github.com>
Date: Tue, 11 Oct 2022 11:20:19 +0900
Subject: [PATCH 016/191] Update README.md (#323)
Updated latest version link in :
Latest happenings
The latest Apache AGE release, Apache AGE 1.1.0.
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 1df66d8fd..5cafbb9c6 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,7 @@ Also, while the technology can be integrated against many data layers, a graph d
## Latest happenings
-- The latest Apache AGE release, [Apache AGE 1.1.0](https://github.com/apache/age/releases/tag/v1.1.0-rc1).
+- The latest Apache AGE release, [Apache AGE 1.1.0](https://github.com/apache/age/releases/tag/v1.1.0-rc0).
- The latest Apache AGE documentation is now available at [here](https://age.apache.org/age-manual/master/index.html).
- The roadmap - [Apache AGE website](http://age.apache.org/).
- Send all your comments and inquiries to the user mailing list, users@age.apache.org.
From ff82d3da646c5987df374c1ee1eed00b2fbc0f1c Mon Sep 17 00:00:00 2001
From: jbiz805 <49049733+jbiz805@users.noreply.github.com>
Date: Tue, 11 Oct 2022 11:20:34 +0900
Subject: [PATCH 017/191] Update README.md (#324)
updated release badge
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 5cafbb9c6..9928cf743 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# [Apache AGE](https://age.apache.org/#)
[](https://github.com/apache/age/blob/master/LICENSE)
-[](https://github.com/apache/age/releases)
+[](https://github.com/apache/age/releases)
[](https://github.com/apache/age/issues)
[](https://github.com/apache/age/network/members)
[](https://github.com/apache/age/stargazers)
From a5d19baf32b81159c31da4c68de4a258113741f7 Mon Sep 17 00:00:00 2001
From: jbiz805 <49049733+jbiz805@users.noreply.github.com>
Date: Tue, 11 Oct 2022 17:41:52 +0900
Subject: [PATCH 018/191] Update README.md (#325)
Changed overall stuff like grammar, word choices, and format.
---
README.md | 21 ++++++++++-----------
1 file changed, 10 insertions(+), 11 deletions(-)
diff --git a/README.md b/README.md
index 9928cf743..01b323b4e 100644
--- a/README.md
+++ b/README.md
@@ -8,15 +8,15 @@
-Apache AGE is a PostgreSQL Extension that provides graph database functionality. AGE is an acronym for A Graph Extension, and is inspired by Bitnine's fork of PostgreSQL 10, AgensGraph, which is a multi-model database. The goal of the project is to create single storage that can handle both relational and graph model data so that users can use standard ANSI SQL along with openCypher, the Graph query language.
+Apache AGE is a PostgreSQL Extension that provides graph database functionality. AGE is an acronym for A Graph Extension, and is inspired by Bitnine's AgensGraph, a multimodel database fork of PostgreSQL. The goal of the project is to enable users of Postgres to use graph query modeling in unison with Postgres' existing relational model.
A graph consists of a set of vertices (also called nodes) and edges, where each individual vertex and edge possesses a map of properties. A vertex is the basic object of a graph, that can exist independently of everything else in the graph. An edge creates a directed connection between two vertices. A graph database is simply composed of vertices and edges. This type of database is useful when the meaning is in the relationships between the data. Relational databases can easily handle direct relationships, but indirect relationships are more difficult to deal with in relational databases. A graph database stores relationship information as a first-class entity. Apache AGE gives you the best of both worlds, simultaneously.
Apache AGE is:
-Powerful -- AGE adds graph database support to the already popular PostgreSQL database: PostgreSQL is used by organizations including Apple, Spotify, and NASA.
-Flexible -- AGE allows you to perform openCypher queries, which make complex queries much easier to write.
-Intelligent -- AGE allows you to perform graph queries that are the basis for many next level web services such as fraud & intrusion detection, master data management, product recommendations, identity and relationship management, experience personalization, knowledge management and more.
+- **Powerful**: adds graph database support to the already popular PostgreSQL database: PostgreSQL is used by organizations including Apple, Spotify, and NASA.
+- **Flexible**: allows you to perform openCypher queries, which make complex queries much easier to write.
+- **Intelligent**: allows you to perform graph queries that are the basis for many next level web services such as fraud detection, master data management, product recommendations, identity and relationship management, experience personalization, knowledge management and more.
Also, while the technology can be integrated against many data layers, a graph database is also the perfect companion for a [GraphQL](https://graphql.org/) API! Since the information is already in a native format, it simplifies many factors and even allows many operations to be generated automatically. GraphQL is rapidly superceeding REST as the standard for cloud applications.
@@ -27,7 +27,7 @@ Also, while the technology can be integrated against many data layers, a graph d
- Apache AGE enables querying multiple graphs at the same time.
- Apache AGE will be enhanced with an aim to support all of the key features of AgensGraph (PostgreSQL fork extended with graph DB functionality).
-## Latest happenings
+## Latest Events
- The latest Apache AGE release, [Apache AGE 1.1.0](https://github.com/apache/age/releases/tag/v1.1.0-rc0).
- The latest Apache AGE documentation is now available at [here](https://age.apache.org/age-manual/master/index.html).
@@ -42,21 +42,20 @@ Also, while the technology can be integrated against many data layers, a graph d
## Graph visualization tool for AGE
-Apache AGE Viewer is a user interface for Apache AGE to provide visualization and exploration of data.
-It provides a simple web visualization tool for users to enter complex graph queries and explore the results in graph and table forms.
+Apache AGE Viewer is a user interface for Apache AGE that provides visualization and exploration of data.
+Through this simple web visualization tool, users can enter complex graph queries and explore the results in graph and table forms.
Apache AGE Viewer is enhanced to proceed with large graph data and discover the insights through various graph algorithms.
Apache AGE Viewer will become a graph data administration and development platform for Apache AGE to support multiple relational databases: .
- This is a visualization tool.
-After AGE Extension Installation
-You can use this tool to use the visualization features.
+After installing AGE Extension, you may use this tool to get access to the visualization features.
- Follow the instructions on the link to run it.
-Under Connect to Database , select database type as "Apache AGE"
+Under "Connect to Database", select database type as "Apache AGE"
## Documentation
Here is the link to the latest [Apache AGE documentation](https://age.apache.org/age-manual/master/index.html).
-You can learn about how to install Apache AGE, its features and built-in functions and how to use various Cypher queries.
+Learn how to install Apache AGE, its features and built-in functions, and how to use various Cypher queries.
## Language Specific Drivers
From 649f32f3717292a4def1f5194a2c1eeee0da11b0 Mon Sep 17 00:00:00 2001
From: jbiz805 <49049733+jbiz805@users.noreply.github.com>
Date: Wed, 12 Oct 2022 13:09:11 +0900
Subject: [PATCH 019/191] Update README.md (#327)
- Deleted outdated sentence in 'Graph Visualization tool for AGE' to avoid misunderstanding
- Renamed Community to Community-driven Driver
- Deleted Apache AGE Python driver in Community-driven Driver since it has already merged into Python driver available in 'Built-in' category.
- Rearranged some header images and badges alignment
---
README.md | 53 ++++++++++++++++++++++++++++++++++++-----------------
1 file changed, 36 insertions(+), 17 deletions(-)
diff --git a/README.md b/README.md
index 01b323b4e..a23f1154a 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,37 @@
-# [Apache AGE](https://age.apache.org/#)
-
-[](https://github.com/apache/age/blob/master/LICENSE)
-[](https://github.com/apache/age/releases)
-[](https://github.com/apache/age/issues)
-[](https://github.com/apache/age/network/members)
-[](https://github.com/apache/age/stargazers)
-
-
-
-Apache AGE is a PostgreSQL Extension that provides graph database functionality. AGE is an acronym for A Graph Extension, and is inspired by Bitnine's AgensGraph, a multimodel database fork of PostgreSQL. The goal of the project is to enable users of Postgres to use graph query modeling in unison with Postgres' existing relational model.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+[Apache AGE](https://age.apache.org/#) is a PostgreSQL Extension that provides graph database functionality. AGE is an acronym for A Graph Extension, and is inspired by Bitnine's AgensGraph, a multimodel database fork of PostgreSQL. The goal of the project is to enable users of Postgres to use graph query modeling in unison with Postgres' existing relational model.
A graph consists of a set of vertices (also called nodes) and edges, where each individual vertex and edge possesses a map of properties. A vertex is the basic object of a graph, that can exist independently of everything else in the graph. An edge creates a directed connection between two vertices. A graph database is simply composed of vertices and edges. This type of database is useful when the meaning is in the relationships between the data. Relational databases can easily handle direct relationships, but indirect relationships are more difficult to deal with in relational databases. A graph database stores relationship information as a first-class entity. Apache AGE gives you the best of both worlds, simultaneously.
@@ -40,7 +63,7 @@ Also, while the technology can be integrated against many data layers, a graph d
- [Use a docker image - official ver.](https://hub.docker.com/r/apache/age)
- [Installing from source](https://age.apache.org/#)
-## Graph visualization tool for AGE
+## Graph Visualization Tool for AGE
Apache AGE Viewer is a user interface for Apache AGE that provides visualization and exploration of data.
Through this simple web visualization tool, users can enter complex graph queries and explore the results in graph and table forms.
@@ -49,8 +72,6 @@ Apache AGE Viewer will become a graph data administration and development platfo
- This is a visualization tool.
After installing AGE Extension, you may use this tool to get access to the visualization features.
-- Follow the instructions on the link to run it.
-Under "Connect to Database", select database type as "Apache AGE"
## Documentation
@@ -66,9 +87,7 @@ Learn how to install Apache AGE, its features and built-in functions, and how to
- [NodeJs driver](./drivers/nodejs)
- [Python driver](./drivers/python)
-### Community
-
-- [Apache AGE Python Driver](https://github.com/rhizome-ai/apache-age-python)
+### Community-driven Driver
- [Apache AGE Rust Driver](https://github.com/Dzordzu/rust-apache-age.git)
## Contribution
From 4510facd7782fc8371c6a27c624571f214875a32 Mon Sep 17 00:00:00 2001
From: jbiz805 <49049733+jbiz805@users.noreply.github.com>
Date: Thu, 13 Oct 2022 17:30:10 +0900
Subject: [PATCH 020/191] Update README.md (#331)
Changed the category orders to : Documentation -> Installation(as is) -> Language Specifc Drivers(as is) -> Graph Visualization Tool for AGE
Changes in Overview
- Apache AGE is currently being developed for the PostgreSQL 12 release, not 11.
- Replaced the old message (adding all features of AgensGraph) to (make it compatible with all RDBs)
Changes in Latest Events:
- Removed section about Documentation, since there's a category regarding documentation below (repetitive)
- Changed statement about Apache AGE roadmap to "Renewed Apache AGE Homepage" since the roadmap isn't available on the renewed homepage anymore.
- Rephrased sentence regarding support for postgresql
Changes in Documentation:
- Rephrased sentence
Changes in Installation:
- Added age.apache.org/download link to Installing from source
- Added Apache AGE setup guide and its link to setup documentation
- Replaced and edited Use a docker image - official ver to Installing via docker image and added documentation link to installing via docker image.
Change to Contribution
- Replaced link of Apache AGE Official Site - Developer Guidelines to https://age.apache.org/contribution/guide
---
README.md | 47 +++++++++++++++++++++++------------------------
1 file changed, 23 insertions(+), 24 deletions(-)
diff --git a/README.md b/README.md
index a23f1154a..ea4d04a37 100644
--- a/README.md
+++ b/README.md
@@ -41,42 +41,31 @@ Apache AGE is:
- **Flexible**: allows you to perform openCypher queries, which make complex queries much easier to write.
- **Intelligent**: allows you to perform graph queries that are the basis for many next level web services such as fraud detection, master data management, product recommendations, identity and relationship management, experience personalization, knowledge management and more.
-Also, while the technology can be integrated against many data layers, a graph database is also the perfect companion for a [GraphQL](https://graphql.org/) API! Since the information is already in a native format, it simplifies many factors and even allows many operations to be generated automatically. GraphQL is rapidly superceeding REST as the standard for cloud applications.
+Also, while the technology can be integrated against many data layers, a graph database is also the perfect companion for a [GraphQL](https://graphql.org/) API. Since the information is already in a native format, it simplifies many factors and even allows many operations to be generated automatically. GraphQL is rapidly superceeding REST as the standard for cloud applications.
## Overview
-- **Apache AGE is currently being developed for the PostgreSQL 11 release** and will support PostgreSQL 12, 13 and all the future releases of PostgreSQL.
+- **Apache AGE is currently being developed for the PostgreSQL 12 release** and will support PostgreSQL 13 and all the future releases of PostgreSQL.
- Apache AGE supports the openCypher graph query language.
- Apache AGE enables querying multiple graphs at the same time.
-- Apache AGE will be enhanced with an aim to support all of the key features of AgensGraph (PostgreSQL fork extended with graph DB functionality).
+- The goal of Apache AGE is to make it compatible with all relational databases in the future.
## Latest Events
-- The latest Apache AGE release, [Apache AGE 1.1.0](https://github.com/apache/age/releases/tag/v1.1.0-rc0).
-- The latest Apache AGE documentation is now available at [here](https://age.apache.org/age-manual/master/index.html).
-- The roadmap - [Apache AGE website](http://age.apache.org/).
+- Latest Apache AGE release, [Apache AGE 1.1.0](https://github.com/apache/age/releases/tag/v1.1.0-rc0).
+- Renewed Apache AGE homepage - [Apache AGE website](http://age.apache.org/).
- Send all your comments and inquiries to the user mailing list, users@age.apache.org.
-- To focus more on implementing the openCypher specification, the support for PostgreSQL 12 will be added in the Q4 2022.
+- Support for PostgreSQL will be added in the Q4 2022 to focus more on implementing the openCypher specification.
-## Installation
-
-- [Use a docker image - official ver.](https://hub.docker.com/r/apache/age)
-- [Installing from source](https://age.apache.org/#)
-
-## Graph Visualization Tool for AGE
-
-Apache AGE Viewer is a user interface for Apache AGE that provides visualization and exploration of data.
-Through this simple web visualization tool, users can enter complex graph queries and explore the results in graph and table forms.
-Apache AGE Viewer is enhanced to proceed with large graph data and discover the insights through various graph algorithms.
-Apache AGE Viewer will become a graph data administration and development platform for Apache AGE to support multiple relational databases: .
+## Documentation
-- This is a visualization tool.
-After installing AGE Extension, you may use this tool to get access to the visualization features.
+Refer to our latest [Apache AGE documentation](https://age.apache.org/age-manual/master/index.html) to learn about installation, features and built-in functions, and Cypher queries.
-## Documentation
+## Installation
-Here is the link to the latest [Apache AGE documentation](https://age.apache.org/age-manual/master/index.html).
-Learn how to install Apache AGE, its features and built-in functions, and how to use various Cypher queries.
+- [Installing from source](https://age.apache.org/download)
+- [Apache AGE setup guide](https://age.apache.org/age-manual/master/intro/setup.html#)
+- [Installing via docker image](https://age.apache.org/age-manual/master/intro/setup.html#installing-via-docker-image)
## Language Specific Drivers
@@ -90,7 +79,17 @@ Learn how to install Apache AGE, its features and built-in functions, and how to
### Community-driven Driver
- [Apache AGE Rust Driver](https://github.com/Dzordzu/rust-apache-age.git)
+## Graph Visualization Tool for AGE
+
+Apache AGE Viewer is a user interface for Apache AGE that provides visualization and exploration of data.
+Through this simple web visualization tool, users can enter complex graph queries and explore the results in graph and table forms.
+Apache AGE Viewer is enhanced to proceed with large graph data and discover the insights through various graph algorithms.
+Apache AGE Viewer will become a graph data administration and development platform for Apache AGE to support multiple relational databases: .
+
+- This is a visualization tool.
+After installing AGE Extension, you may use this tool to get access to the visualization features.
+
## Contribution
You can improve ongoing efforts or initiate new ones by sending pull requests to [this repository](https://github.com/apache/age).
-Also, you can learn from the code review process, how to merge pull requests, and from code style compliance to documentation, by visiting the [Apache AGE official site - Developer Guidelines](https://age.apache.org/#codereview).
+Also, you can learn from the code review process, how to merge pull requests, and from code style compliance to documentation, by visiting the [Apache AGE official site - Developer Guidelines](https://age.apache.org/contribution/guide).
From 0aba332cd783ee0bcd580186af77b19ab6dc2acf Mon Sep 17 00:00:00 2001
From: John Gemignani
Date: Mon, 17 Oct 2022 15:51:34 -0700
Subject: [PATCH 021/191] Fix delete_global_graphs
Fixed the delete_global_graphs function. It was not keeping track of
the previous graph global context pointer.
This was causing a memory leak for multiple graph contexts if
individual graphs were deleted.
---
src/backend/utils/adt/age_global_graph.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/backend/utils/adt/age_global_graph.c b/src/backend/utils/adt/age_global_graph.c
index 3da11b44c..c18f9c935 100644
--- a/src/backend/utils/adt/age_global_graph.c
+++ b/src/backend/utils/adt/age_global_graph.c
@@ -858,7 +858,8 @@ static bool delete_specific_GRAPH_global_contexts(char *graph_name)
return true;
}
- /* advance to the next one */
+ /* save the current as previous and advance to the next one */
+ prev_ggctx = curr_ggctx;
curr_ggctx = next_ggctx;
}
From 492e7788d0497dabf24af76fb0197ff9c351a400 Mon Sep 17 00:00:00 2001
From: Eya Badal
Date: Thu, 20 Oct 2022 11:43:57 -0700
Subject: [PATCH 022/191] Update CONTRIBUTING.md
Removed "Incubator"
---
CONTRIBUTING.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 63687ae74..50a2c89e2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,4 +1,4 @@
-# Contributing to Apache Incubator AGE
+# Contributing to Apache AGE
First off, thank you for considering contributing.
@@ -12,4 +12,4 @@ Thank you for raising issues.
## Pull Requests
-Pull requests are a great way to get your ideas into this repository. Check out our list of good [first issues](https://github.com/apache/incubator-age/labels/good%20first%20issue)
+Pull requests are a great way to get your ideas into this repository. Check out our list of good [first issues](https://github.com/apache/age/labels/good%20first%20issue)
From 24a7be5c75f67459531a7c32f2442c4667c32a33 Mon Sep 17 00:00:00 2001
From: Eya Badal
Date: Thu, 20 Oct 2022 11:56:20 -0700
Subject: [PATCH 023/191] Delete DISCLAIMER
Removed DISCLAIMER file
---
DISCLAIMER | 10 ----------
1 file changed, 10 deletions(-)
delete mode 100644 DISCLAIMER
diff --git a/DISCLAIMER b/DISCLAIMER
deleted file mode 100644
index 8be5086c5..000000000
--- a/DISCLAIMER
+++ /dev/null
@@ -1,10 +0,0 @@
-Apache AGE is an effort undergoing incubation at
-The Apache Software Foundation (ASF), sponsored by Apache Incubator PMC.
-
-Incubation is required of all newly accepted projects until a further review
-indicates that the infrastructure, communications, and decision making process
-have stabilized in a manner consistent with other successful ASF projects.
-
-While incubation status is not necessarily a reflection of the completeness
-or stability of the code, it does indicate that the project has yet to be fully
-endorsed by the ASF.
From 0375ecbceccbca269ed482a2d1a24749a0284a91 Mon Sep 17 00:00:00 2001
From: Eya Badal
Date: Thu, 20 Oct 2022 12:40:06 -0700
Subject: [PATCH 024/191] Update README.md
Removed "incubator"
---
drivers/golang/README.md | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/drivers/golang/README.md b/drivers/golang/README.md
index bad9d064d..1d58fa6cf 100644
--- a/drivers/golang/README.md
+++ b/drivers/golang/README.md
@@ -1,4 +1,4 @@
-# incubator-age AGType parser and driver support for Golang
+# age AGType parser and driver support for Golang
AGType parser and driver support for [Apache AGE](https://age.apache.org/), graph extention for PostgreSQL.
@@ -13,20 +13,20 @@ AGType parser and driver support for [Apache AGE](https://age.apache.org/), grap
### Go get
```
-go get github.com/apache/incubator-age/drivers/golang
+go get github.com/apache/age/drivers/golang
```
### gomod
```
-require github.com/apache/incubator-age/drivers/golang {version}
+require github.com/apache/age/drivers/golang {version}
```
-Check [latest version](https://github.com/apache/incubator-age/releases)
+Check [latest version](https://github.com/apache/age/releases)
### For more information about [Apache AGE](https://age.apache.org/)
-* Apache Incubator Age : https://age.apache.org/
-* Github : https://github.com/apache/incubator-age
-* Document : https://age.incubator.apache.org/docs/
+* Apache Age : https://age.apache.org/
+* Github : https://github.com/apache/age
+* Document : https://age.apache.org/docs/
### Check AGE loaded on your PostgreSQL
Connect to your containerized Postgres instance and then run the following commands:
@@ -38,9 +38,9 @@ SET search_path = ag_catalog, "$user", public;
```
### Test
-Check out and rewrite DSN in incubator-age/drivers/golang/age/age_test.go
+Check out and rewrite DSN in age/drivers/golang/age/age_test.go
```
-cd incubator-age/drivers/golang/age
+cd age/drivers/golang/age
go test . -v
```
@@ -56,4 +56,4 @@ go test . -v
### License
-Apache-2.0 License
\ No newline at end of file
+Apache-2.0 License
From 327014a2b522536514de134c2a4a3479fe5f0044 Mon Sep 17 00:00:00 2001
From: John Gemignani
Date: Thu, 20 Oct 2022 15:10:13 -0700
Subject: [PATCH 025/191] Remove incubating from GO driver
Removed incubating from GO driver.
---
drivers/golang/age/builder.go | 2 +-
drivers/golang/age/mapper.go | 2 +-
drivers/golang/go.mod | 2 +-
drivers/golang/samples/age_wrapper_sample.go | 2 +-
drivers/golang/samples/sql_api_sample.go | 2 +-
5 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/drivers/golang/age/builder.go b/drivers/golang/age/builder.go
index 7fa6d3b3e..be1c4bbb1 100644
--- a/drivers/golang/age/builder.go
+++ b/drivers/golang/age/builder.go
@@ -26,7 +26,7 @@ import (
"strings"
"github.com/antlr/antlr4/runtime/Go/antlr"
- "github.com/apache/incubator-age/drivers/golang/parser"
+ "github.com/apache/age/drivers/golang/parser"
)
const MaxUint = ^uint(0)
diff --git a/drivers/golang/age/mapper.go b/drivers/golang/age/mapper.go
index 099407951..5857b1331 100644
--- a/drivers/golang/age/mapper.go
+++ b/drivers/golang/age/mapper.go
@@ -24,7 +24,7 @@ import (
"strings"
"github.com/antlr/antlr4/runtime/Go/antlr"
- "github.com/apache/incubator-age/drivers/golang/parser"
+ "github.com/apache/age/drivers/golang/parser"
)
type AGMapper struct {
diff --git a/drivers/golang/go.mod b/drivers/golang/go.mod
index c7082b370..81525d66f 100644
--- a/drivers/golang/go.mod
+++ b/drivers/golang/go.mod
@@ -17,7 +17,7 @@
// * under the License.
// */
-module github.com/apache/incubator-age/drivers/golang
+module github.com/apache/age/drivers/golang
go 1.16
diff --git a/drivers/golang/samples/age_wrapper_sample.go b/drivers/golang/samples/age_wrapper_sample.go
index ac611fd76..6040ac525 100644
--- a/drivers/golang/samples/age_wrapper_sample.go
+++ b/drivers/golang/samples/age_wrapper_sample.go
@@ -21,7 +21,7 @@ package main
import (
"fmt"
- "github.com/apache/incubator-age/drivers/golang/age"
+ "github.com/apache/age/drivers/golang/age"
)
// Do cypher query to AGE with Age API
diff --git a/drivers/golang/samples/sql_api_sample.go b/drivers/golang/samples/sql_api_sample.go
index e0a17fc1a..491532f38 100644
--- a/drivers/golang/samples/sql_api_sample.go
+++ b/drivers/golang/samples/sql_api_sample.go
@@ -22,7 +22,7 @@ import (
"database/sql"
"fmt"
- "github.com/apache/incubator-age/drivers/golang/age"
+ "github.com/apache/age/drivers/golang/age"
)
// Do cypher query to AGE with database/sql Tx API transaction conrol
From cd4c6b49a427b0465b4eeb7e1ead81ad1fb36170 Mon Sep 17 00:00:00 2001
From: John Gemignani
Date: Thu, 20 Oct 2022 15:28:16 -0700
Subject: [PATCH 026/191] Fix GO driver module version
Fixed the GO driver module version.
---
drivers/golang/go.mod | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/drivers/golang/go.mod b/drivers/golang/go.mod
index 81525d66f..bd27c5c16 100644
--- a/drivers/golang/go.mod
+++ b/drivers/golang/go.mod
@@ -22,7 +22,7 @@ module github.com/apache/age/drivers/golang
go 1.16
require (
- github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210521184019-c5ad59b459ec
+ github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20221020221120-327014a2b522
github.com/lib/pq v1.10.2
github.com/stretchr/testify v1.7.0
)
From 01e64f1959e49c3b9ea07cb4545ff71cfacb3cec Mon Sep 17 00:00:00 2001
From: John Gemignani
Date: Thu, 20 Oct 2022 15:46:46 -0700
Subject: [PATCH 027/191] Remove incubating from Python driver
Removed incubating from the Python driver.
---
drivers/python/README.md | 18 +++++++++---------
drivers/python/setup.py | 6 +++---
2 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/drivers/python/README.md b/drivers/python/README.md
index 7de16f534..d01b88593 100644
--- a/drivers/python/README.md
+++ b/drivers/python/README.md
@@ -1,4 +1,4 @@
-# incubator-age AGType parser and driver support for Python
+# AGE AGType parser and driver support for Python
AGType parser and driver support for [Apache AGE](https://age.apache.org/), graph extention for PostgreSQL.
### Features
@@ -22,9 +22,9 @@ python -m unittest -v test_agtypes.py
```
### Build from source
-```
-git clone https://github.com/apache/incubator-age.git
-cd incubator-age/dirivers/python
+```
+git clone https://github.com/apache/age.git
+cd age/drivers/python
python setup.py install
@@ -32,15 +32,15 @@ python setup.py install
### Install from PyPi
-```
+```
pip install apache-age-python
```
### For more information about [Apache AGE](https://age.apache.org/)
-* Apache Incubator Age : https://age.apache.org/
-* Github : https://github.com/apache/incubator-age
-* Document : https://age.incubator.apache.org/docs/
+* Apache Age : https://age.apache.org/
+* Github : https://github.com/apache/age
+* Document : https://age.apache.org/age-manual/master/index.html
* apache-age-python GitHub : https://github.com/rhizome-ai/apache-age-python
### Check AGE loaded on your PostgreSQL
@@ -58,4 +58,4 @@ SET search_path = ag_catalog, "$user", public;
* Agtype converting samples: [Agtype Sample](samples/apache-age-agtypes.ipynb) in Samples.
### License
-Apache-2.0 License
\ No newline at end of file
+Apache-2.0 License
diff --git a/drivers/python/setup.py b/drivers/python/setup.py
index e8cf410e5..7d8981fab 100644
--- a/drivers/python/setup.py
+++ b/drivers/python/setup.py
@@ -20,14 +20,14 @@
long_description = fh.read()
setup(
- name = 'incubator-age',
+ name = 'age',
version = VERSION.VERSION,
description = 'Python driver support for Apache AGE',
long_description=long_description,
long_description_content_type="text/markdown",
author = 'rhizome',
author_email = 'rhizome.ai@gmail.com',
- url = 'https://github.com/apache/incubator-age',
+ url = 'https://github.com/apache/age',
license = 'Apache2.0',
install_requires = [ 'psycopg2', 'antlr4-python3-runtime' ],
packages = ['age', 'age.gen'],
@@ -38,4 +38,4 @@
classifiers = [
'Programming Language :: Python :: 3.9'
]
-)
\ No newline at end of file
+)
From 567d2b0aada47c05dbd3845124209ed24960379f Mon Sep 17 00:00:00 2001
From: John Gemignani
Date: Thu, 20 Oct 2022 16:26:05 -0700
Subject: [PATCH 028/191] Remove incubating from ag_load
Removed incubation from ag_load.
---
src/include/utils/ag_load.h | 6 +++---
src/include/utils/load/age_load.h | 6 +++---
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/include/utils/ag_load.h b/src/include/utils/ag_load.h
index 5e461ac52..2c51dc2d9 100644
--- a/src/include/utils/ag_load.h
+++ b/src/include/utils/ag_load.h
@@ -17,8 +17,8 @@
* under the License.
*/
-#ifndef INCUBATOR_AGE_AG_LOAD_H
-#define INCUBATOR_AGE_AG_LOAD_H
+#ifndef AGE_AG_LOAD_H
+#define AGE_AG_LOAD_H
#include "postgres.h"
@@ -57,4 +57,4 @@
#include "utils/graphid.h"
-#endif //INCUBATOR_AGE_AG_LOAD_H
+#endif //AGE_AG_LOAD_H
diff --git a/src/include/utils/load/age_load.h b/src/include/utils/load/age_load.h
index 9eeca3614..8424fb4c6 100644
--- a/src/include/utils/load/age_load.h
+++ b/src/include/utils/load/age_load.h
@@ -54,8 +54,8 @@
#include "utils/agtype.h"
#include "utils/graphid.h"
-#ifndef INCUBATOR_AGE_ENTITY_CREATOR_H
-#define INCUBATOR_AGE_ENTITY_CREATOR_H
+#ifndef AGE_ENTITY_CREATOR_H
+#define AGE_ENTITY_CREATOR_H
agtype* create_agtype_from_list(char **header, char **fields,
size_t fields_len, int64 vertex_id);
@@ -67,4 +67,4 @@ void insert_edge_simple(Oid graph_id, char* label_name, graphid edge_id,
graphid start_id, graphid end_id,
agtype* end_properties);
-#endif //INCUBATOR_AGE_ENTITY_CREATOR_H
+#endif //AGE_ENTITY_CREATOR_H
From 04fd1481adbab6e9bff181bb3f135d8e07a53482 Mon Sep 17 00:00:00 2001
From: Rafsun Masud
Date: Tue, 25 Oct 2022 15:02:38 -0400
Subject: [PATCH 029/191] add regression tests for delete_global_graphs (#336)
---
Makefile | 1 +
regress/expected/age_global_graph.out | 196 ++++++++++++++++++++++++++
regress/sql/age_global_graph.sql | 68 +++++++++
3 files changed, 265 insertions(+)
create mode 100644 regress/expected/age_global_graph.out
create mode 100644 regress/sql/age_global_graph.sql
diff --git a/Makefile b/Makefile
index da0553b45..8c685a33d 100644
--- a/Makefile
+++ b/Makefile
@@ -90,6 +90,7 @@ REGRESS = scan \
cypher_union \
cypher_call \
cypher_merge \
+ age_global_graph \
age_load \
index \
drop
diff --git a/regress/expected/age_global_graph.out b/regress/expected/age_global_graph.out
new file mode 100644
index 000000000..171ec1bd5
--- /dev/null
+++ b/regress/expected/age_global_graph.out
@@ -0,0 +1,196 @@
+LOAD 'age';
+SET search_path TO ag_catalog;
+--
+-- test delete_specific_GRAPH_global_contexts function
+--
+-- create 3 graphs
+SELECT * FROM create_graph('age_global_graph_1');
+NOTICE: graph "age_global_graph_1" has been created
+ create_graph
+--------------
+
+(1 row)
+
+SELECT * FROM cypher('age_global_graph_1', $$ CREATE (v:vertex_from_graph_1) RETURN v $$) AS (v agtype);
+ v
+-----------------------------------------------------------------------------------
+ {"id": 844424930131969, "label": "vertex_from_graph_1", "properties": {}}::vertex
+(1 row)
+
+SELECT * FROM create_graph('age_global_graph_2');
+NOTICE: graph "age_global_graph_2" has been created
+ create_graph
+--------------
+
+(1 row)
+
+SELECT * FROM cypher('age_global_graph_2', $$ CREATE (v:vertex_from_graph_2) RETURN v $$) AS (v agtype);
+ v
+-----------------------------------------------------------------------------------
+ {"id": 844424930131969, "label": "vertex_from_graph_2", "properties": {}}::vertex
+(1 row)
+
+SELECT * FROM create_graph('age_global_graph_3');
+NOTICE: graph "age_global_graph_3" has been created
+ create_graph
+--------------
+
+(1 row)
+
+SELECT * FROM cypher('age_global_graph_3', $$ CREATE (v:vertex_from_graph_3) RETURN v $$) AS (v agtype);
+ v
+-----------------------------------------------------------------------------------
+ {"id": 844424930131969, "label": "vertex_from_graph_3", "properties": {}}::vertex
+(1 row)
+
+-- load contexts using the vertex_stats command
+SELECT * FROM cypher('age_global_graph_3', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype);
+ result
+-----------------------------------------------------------------------------------------------------------
+ {"id": 844424930131969, "label": "vertex_from_graph_3", "in_degree": 0, "out_degree": 0, "self_loops": 0}
+(1 row)
+
+SELECT * FROM cypher('age_global_graph_2', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype);
+ result
+-----------------------------------------------------------------------------------------------------------
+ {"id": 844424930131969, "label": "vertex_from_graph_2", "in_degree": 0, "out_degree": 0, "self_loops": 0}
+(1 row)
+
+SELECT * FROM cypher('age_global_graph_1', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype);
+ result
+-----------------------------------------------------------------------------------------------------------
+ {"id": 844424930131969, "label": "vertex_from_graph_1", "in_degree": 0, "out_degree": 0, "self_loops": 0}
+(1 row)
+
+-- delete age_global_graph_2's context
+-- should return true (succeeded)
+SELECT * FROM cypher('age_global_graph_2', $$ RETURN delete_global_graphs('age_global_graph_2') $$) AS (result agtype);
+ result
+--------
+ true
+(1 row)
+
+-- delete age_global_graph_1's context
+-- should return true (succeed) because previous command should not delete the 1st graph's context
+SELECT * FROM cypher('age_global_graph_1', $$ RETURN delete_global_graphs('age_global_graph_1') $$) AS (result agtype);
+ result
+--------
+ true
+(1 row)
+
+-- delete age_global_graph_3's context
+-- should return true (succeed) because previous commands should not delete the 3rd graph's context
+SELECT * FROM cypher('age_global_graph_3', $$ RETURN delete_global_graphs('age_global_graph_3') $$) AS (result agtype);
+ result
+--------
+ true
+(1 row)
+
+-- delete all graphs' context again
+-- should return false (did not succeed) for all of them because already removed
+SELECT * FROM cypher('age_global_graph_2', $$ RETURN delete_global_graphs('age_global_graph_2') $$) AS (result agtype);
+ result
+--------
+ false
+(1 row)
+
+SELECT * FROM cypher('age_global_graph_1', $$ RETURN delete_global_graphs('age_global_graph_1') $$) AS (result agtype);
+ result
+--------
+ false
+(1 row)
+
+SELECT * FROM cypher('age_global_graph_3', $$ RETURN delete_global_graphs('age_global_graph_3') $$) AS (result agtype);
+ result
+--------
+ false
+(1 row)
+
+
+--
+-- test delete_GRAPH_global_contexts function
+--
+-- load contexts again
+SELECT * FROM cypher('age_global_graph_3', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype);
+ result
+-----------------------------------------------------------------------------------------------------------
+ {"id": 844424930131969, "label": "vertex_from_graph_3", "in_degree": 0, "out_degree": 0, "self_loops": 0}
+(1 row)
+
+SELECT * FROM cypher('age_global_graph_2', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype);
+ result
+-----------------------------------------------------------------------------------------------------------
+ {"id": 844424930131969, "label": "vertex_from_graph_2", "in_degree": 0, "out_degree": 0, "self_loops": 0}
+(1 row)
+
+SELECT * FROM cypher('age_global_graph_1', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype);
+ result
+-----------------------------------------------------------------------------------------------------------
+ {"id": 844424930131969, "label": "vertex_from_graph_1", "in_degree": 0, "out_degree": 0, "self_loops": 0}
+(1 row)
+
+-- delete all graph contexts
+-- should return true
+SELECT * FROM cypher('age_global_graph_1', $$ RETURN delete_global_graphs(NULL) $$) AS (result agtype);
+ result
+--------
+ true
+(1 row)
+
+-- delete all graphs' context individually
+-- should return false for all of them because already removed
+SELECT * FROM cypher('age_global_graph_1', $$ RETURN delete_global_graphs('age_global_graph_1') $$) AS (result agtype);
+ result
+--------
+ false
+(1 row)
+
+SELECT * FROM cypher('age_global_graph_2', $$ RETURN delete_global_graphs('age_global_graph_2') $$) AS (result agtype);
+ result
+--------
+ false
+(1 row)
+
+SELECT * FROM cypher('age_global_graph_3', $$ RETURN delete_global_graphs('age_global_graph_3') $$) AS (result agtype);
+ result
+--------
+ false
+(1 row)
+
+-- drop graphs
+SELECT * FROM drop_graph('age_global_graph_1', true);
+NOTICE: drop cascades to 3 other objects
+DETAIL: drop cascades to table age_global_graph_1._ag_label_vertex
+drop cascades to table age_global_graph_1._ag_label_edge
+drop cascades to table age_global_graph_1.vertex_from_graph_1
+NOTICE: graph "age_global_graph_1" has been dropped
+ drop_graph
+------------
+
+(1 row)
+
+SELECT * FROM drop_graph('age_global_graph_2', true);
+NOTICE: drop cascades to 3 other objects
+DETAIL: drop cascades to table age_global_graph_2._ag_label_vertex
+drop cascades to table age_global_graph_2._ag_label_edge
+drop cascades to table age_global_graph_2.vertex_from_graph_2
+NOTICE: graph "age_global_graph_2" has been dropped
+ drop_graph
+------------
+
+(1 row)
+
+SELECT * FROM drop_graph('age_global_graph_3', true);
+NOTICE: drop cascades to 3 other objects
+DETAIL: drop cascades to table age_global_graph_3._ag_label_vertex
+drop cascades to table age_global_graph_3._ag_label_edge
+drop cascades to table age_global_graph_3.vertex_from_graph_3
+NOTICE: graph "age_global_graph_3" has been dropped
+ drop_graph
+------------
+
+(1 row)
+
+--
+-- End of tests
+--
diff --git a/regress/sql/age_global_graph.sql b/regress/sql/age_global_graph.sql
new file mode 100644
index 000000000..a62d3358a
--- /dev/null
+++ b/regress/sql/age_global_graph.sql
@@ -0,0 +1,68 @@
+LOAD 'age';
+SET search_path TO ag_catalog;
+
+--
+-- test delete_specific_GRAPH_global_contexts function
+--
+
+-- create 3 graphs
+SELECT * FROM create_graph('age_global_graph_1');
+SELECT * FROM cypher('age_global_graph_1', $$ CREATE (v:vertex_from_graph_1) RETURN v $$) AS (v agtype);
+
+SELECT * FROM create_graph('age_global_graph_2');
+SELECT * FROM cypher('age_global_graph_2', $$ CREATE (v:vertex_from_graph_2) RETURN v $$) AS (v agtype);
+
+SELECT * FROM create_graph('age_global_graph_3');
+SELECT * FROM cypher('age_global_graph_3', $$ CREATE (v:vertex_from_graph_3) RETURN v $$) AS (v agtype);
+
+-- load contexts using the vertex_stats command
+SELECT * FROM cypher('age_global_graph_3', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype);
+SELECT * FROM cypher('age_global_graph_2', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype);
+SELECT * FROM cypher('age_global_graph_1', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype);
+
+-- delete age_global_graph_2's context
+-- should return true (succeeded)
+SELECT * FROM cypher('age_global_graph_2', $$ RETURN delete_global_graphs('age_global_graph_2') $$) AS (result agtype);
+
+-- delete age_global_graph_1's context
+-- should return true (succeed) because previous command should not delete the 1st graph's context
+SELECT * FROM cypher('age_global_graph_1', $$ RETURN delete_global_graphs('age_global_graph_1') $$) AS (result agtype);
+
+-- delete age_global_graph_3's context
+-- should return true (succeed) because previous commands should not delete the 3rd graph's context
+SELECT * FROM cypher('age_global_graph_3', $$ RETURN delete_global_graphs('age_global_graph_3') $$) AS (result agtype);
+
+-- delete all graphs' context again
+-- should return false (did not succeed) for all of them because already removed
+SELECT * FROM cypher('age_global_graph_2', $$ RETURN delete_global_graphs('age_global_graph_2') $$) AS (result agtype);
+SELECT * FROM cypher('age_global_graph_1', $$ RETURN delete_global_graphs('age_global_graph_1') $$) AS (result agtype);
+SELECT * FROM cypher('age_global_graph_3', $$ RETURN delete_global_graphs('age_global_graph_3') $$) AS (result agtype);
+
+
+--
+-- test delete_GRAPH_global_contexts function
+--
+
+-- load contexts again
+SELECT * FROM cypher('age_global_graph_3', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype);
+SELECT * FROM cypher('age_global_graph_2', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype);
+SELECT * FROM cypher('age_global_graph_1', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype);
+
+-- delete all graph contexts
+-- should return true
+SELECT * FROM cypher('age_global_graph_1', $$ RETURN delete_global_graphs(NULL) $$) AS (result agtype);
+
+-- delete all graphs' context individually
+-- should return false for all of them because already removed
+SELECT * FROM cypher('age_global_graph_1', $$ RETURN delete_global_graphs('age_global_graph_1') $$) AS (result agtype);
+SELECT * FROM cypher('age_global_graph_2', $$ RETURN delete_global_graphs('age_global_graph_2') $$) AS (result agtype);
+SELECT * FROM cypher('age_global_graph_3', $$ RETURN delete_global_graphs('age_global_graph_3') $$) AS (result agtype);
+
+-- drop graphs
+SELECT * FROM drop_graph('age_global_graph_1', true);
+SELECT * FROM drop_graph('age_global_graph_2', true);
+SELECT * FROM drop_graph('age_global_graph_3', true);
+
+--
+-- End of tests
+--
From 17714bcccf3a6a3473f5597d40582fdaaa51a50d Mon Sep 17 00:00:00 2001
From: jbiz805 <49049733+jbiz805@users.noreply.github.com>
Date: Wed, 26 Oct 2022 16:14:37 +0900
Subject: [PATCH 030/191] Update README.md (#340)
Added Discord Badge
---
README.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/README.md b/README.md
index ea4d04a37..4223e4ce8 100644
--- a/README.md
+++ b/README.md
@@ -27,6 +27,8 @@
+
+
From 2fabc17aa6be7846b21e0e574e9e2037d293eb2c Mon Sep 17 00:00:00 2001
From: Dehowe Feng
Date: Fri, 21 Oct 2022 17:07:33 -0700
Subject: [PATCH 031/191] Invalid labels now return NULL
Updated the behavior of invalid labels to return NULL rather than
throw an error.
Added additional regression tests as well.
---
regress/expected/cypher_match.out | 48 ++++--
regress/sql/cypher_match.sql | 8 +
src/backend/parser/cypher_clause.c | 228 +++++++++++++++++++++++------
3 files changed, 228 insertions(+), 56 deletions(-)
diff --git a/regress/expected/cypher_match.out b/regress/expected/cypher_match.out
index 6327f1069..475739255 100644
--- a/regress/expected/cypher_match.out
+++ b/regress/expected/cypher_match.out
@@ -523,21 +523,45 @@ LINE 2: MATCH (a:v1)-[]-()-[]-(a {id:'will_fail'}) RETURN a
^
--Incorrect Labels
SELECT * FROM cypher('cypher_match', $$MATCH (n)-[:v]-() RETURN n$$) AS (n agtype);
-ERROR: label v is for vertices, not edges
-LINE 1: SELECT * FROM cypher('cypher_match', $$MATCH (n)-[:v]-() RET...
- ^
+ n
+---
+(0 rows)
+
SELECT * FROM cypher('cypher_match', $$MATCH (n)-[:emissing]-() RETURN n$$) AS (n agtype);
-ERROR: label emissing does not exists
-LINE 1: SELECT * FROM cypher('cypher_match', $$MATCH (n)-[:emissing]...
- ^
+ n
+---
+(0 rows)
+
SELECT * FROM cypher('cypher_match', $$MATCH (n:e1)-[]-() RETURN n$$) AS (n agtype);
-ERROR: label e1 is for edges, not vertices
-LINE 1: SELECT * FROM cypher('cypher_match', $$MATCH (n:e1)-[]-() RE...
- ^
+ n
+---
+(0 rows)
+
SELECT * FROM cypher('cypher_match', $$MATCH (n:vmissing)-[]-() RETURN n$$) AS (n agtype);
-ERROR: label vmissing does not exists
-LINE 1: SELECT * FROM cypher('cypher_match', $$MATCH (n:vmissing)-[]...
- ^
+ n
+---
+(0 rows)
+
+SELECT * FROM cypher('cypher_match', $$MATCH (:e1)-[r]-() RETURN r$$) AS (r agtype);
+ r
+---
+(0 rows)
+
+SELECT * FROM cypher('cypher_match', $$MATCH (:vmissing)-[r]-() RETURN r$$) AS (r agtype);
+ r
+---
+(0 rows)
+
+SELECT * FROM cypher('cypher_match', $$MATCH (n),(:e1) RETURN n$$) AS (n agtype);
+ n
+---
+(0 rows)
+
+SELECT * FROM cypher('cypher_match', $$MATCH (n),()-[:v]-() RETURN n$$) AS (n agtype);
+ n
+---
+(0 rows)
+
--
-- Path of one vertex. This should select 14
--
diff --git a/regress/sql/cypher_match.sql b/regress/sql/cypher_match.sql
index 5d42d2ebd..11282961c 100644
--- a/regress/sql/cypher_match.sql
+++ b/regress/sql/cypher_match.sql
@@ -293,6 +293,14 @@ SELECT * FROM cypher('cypher_match', $$MATCH (n:e1)-[]-() RETURN n$$) AS (n agty
SELECT * FROM cypher('cypher_match', $$MATCH (n:vmissing)-[]-() RETURN n$$) AS (n agtype);
+SELECT * FROM cypher('cypher_match', $$MATCH (:e1)-[r]-() RETURN r$$) AS (r agtype);
+
+SELECT * FROM cypher('cypher_match', $$MATCH (:vmissing)-[r]-() RETURN r$$) AS (r agtype);
+
+SELECT * FROM cypher('cypher_match', $$MATCH (n),(:e1) RETURN n$$) AS (n agtype);
+
+SELECT * FROM cypher('cypher_match', $$MATCH (n),()-[:v]-() RETURN n$$) AS (n agtype);
+
--
-- Path of one vertex. This should select 14
--
diff --git a/src/backend/parser/cypher_clause.c b/src/backend/parser/cypher_clause.c
index 70afb5fa4..825628777 100644
--- a/src/backend/parser/cypher_clause.c
+++ b/src/backend/parser/cypher_clause.c
@@ -128,10 +128,10 @@ static List *transform_match_path(cypher_parsestate *cpstate, Query *query,
cypher_path *path);
static Expr *transform_cypher_edge(cypher_parsestate *cpstate,
cypher_relationship *rel,
- List **target_list);
+ List **target_list, bool valid_label);
static Expr *transform_cypher_node(cypher_parsestate *cpstate,
cypher_node *node, List **target_list,
- bool output_node);
+ bool output_node, bool valid_label);
static Node *make_vertex_expr(cypher_parsestate *cpstate, RangeTblEntry *rte,
char *label);
static Node *make_edge_expr(cypher_parsestate *cpstate, RangeTblEntry *rte,
@@ -2139,6 +2139,64 @@ static Query *transform_cypher_with(cypher_parsestate *cpstate,
wrapper);
}
+static bool match_check_valid_label(cypher_match *match,
+ cypher_parsestate *cpstate)
+{
+ ListCell *cell1;
+ ListCell *cell2;
+ cypher_path *path;
+
+ foreach(cell1, match->pattern)
+ {
+ int i = 0;
+ path = (cypher_path*) lfirst(cell1);
+
+ foreach(cell2, path->path)
+ {
+ if (i % 2 == 0)
+ {
+ cypher_node *node = NULL;
+
+ node = lfirst(cell2);
+
+ if (node->label)
+ {
+ label_cache_data *lcd =
+ search_label_name_graph_cache(node->label,
+ cpstate->graph_oid);
+
+ if (lcd == NULL ||
+ lcd->kind != LABEL_KIND_VERTEX)
+ {
+ return false;
+ }
+ }
+ }
+ else
+ {
+ cypher_relationship *rel = NULL;
+
+ rel = lfirst(cell2);
+
+ if (rel->label)
+ {
+ label_cache_data *lcd =
+ search_label_name_graph_cache(rel->label,
+ cpstate->graph_oid);
+
+ if (lcd == NULL || lcd->kind != LABEL_KIND_EDGE)
+ {
+ return false;
+ }
+ }
+ }
+ i++;
+ }
+ }
+
+ return true;
+}
+
static Query *transform_cypher_clause_with_where(cypher_parsestate *cpstate,
transform_method transform,
cypher_clause *clause)
@@ -2150,7 +2208,6 @@ static Query *transform_cypher_clause_with_where(cypher_parsestate *cpstate,
cypher_call *call_self;
Node *where;
-
if (is_ag_node(self, cypher_call))
{
call_self = (cypher_call*) clause->self;
@@ -2218,6 +2275,24 @@ static Query *transform_cypher_clause_with_where(cypher_parsestate *cpstate,
static Query *transform_cypher_match(cypher_parsestate *cpstate,
cypher_clause *clause)
{
+ cypher_match *match_self = (cypher_match*) clause->self;
+
+ if(!match_check_valid_label(match_self, cpstate))
+ {
+ cypher_bool_const *l = make_ag_node(cypher_bool_const);
+ cypher_bool_const *r = make_ag_node(cypher_bool_const);
+
+ l->boolean = true;
+ l->location = -1;
+ r->boolean = false;
+ r->location = -1;
+
+ /*if the label is invalid, create a paradoxical where to get null*/
+ match_self->where = (Node *)makeSimpleA_Expr(AEXPR_OP, "=",
+ (Node *)l,
+ (Node *)r, -1);
+ }
+
return transform_cypher_clause_with_where(
cpstate, transform_cypher_match_pattern, clause);
}
@@ -3556,6 +3631,56 @@ static bool isa_special_VLE_case(cypher_path *path)
return false;
}
+static bool path_check_valid_label(cypher_path *path,
+ cypher_parsestate *cpstate)
+{
+ ListCell *lc = NULL;
+ int i = 0;
+
+ foreach (lc, path->path)
+ {
+ if (i % 2 == 0)
+ {
+ cypher_node *node = NULL;
+
+ node = lfirst(lc);
+
+ if (node->label)
+ {
+ label_cache_data *lcd =
+ search_label_name_graph_cache(node->label,
+ cpstate->graph_oid);
+
+ if (lcd == NULL || lcd->kind != LABEL_KIND_VERTEX)
+ {
+ return false;
+ }
+ }
+ }
+ else
+ {
+ cypher_relationship *rel = NULL;
+
+ rel = lfirst(lc);
+
+ if (rel->label)
+ {
+ label_cache_data *lcd =
+ search_label_name_graph_cache(rel->label,
+ cpstate->graph_oid);
+
+ if (lcd == NULL || lcd->kind != LABEL_KIND_EDGE)
+ {
+ return false;
+ }
+ }
+ }
+ i++;
+ }
+
+ return true;
+}
+
/*
* Iterate through the path and construct all edges and necessary vertices
*/
@@ -3569,8 +3694,10 @@ static List *transform_match_entities(cypher_parsestate *cpstate, Query *query,
bool node_declared_in_prev_clause = false;
transform_entity *prev_entity = NULL;
bool special_VLE_case = false;
+ bool valid_label = true;
special_VLE_case = isa_special_VLE_case(path);
+ valid_label = path_check_valid_label(path, cpstate);
/*
* Iterate through every node in the path, construct the expr node
@@ -3618,7 +3745,7 @@ static List *transform_match_entities(cypher_parsestate *cpstate, Query *query,
/* transform vertex */
expr = transform_cypher_node(cpstate, node, &query->targetList,
- output_node);
+ output_node, valid_label);
entity = make_transform_entity(cpstate, ENT_VERTEX, (Node *)node,
expr);
@@ -3677,7 +3804,8 @@ static List *transform_match_entities(cypher_parsestate *cpstate, Query *query,
}
}
- expr = transform_cypher_edge(cpstate, rel, &query->targetList);
+ expr = transform_cypher_edge(cpstate, rel, &query->targetList,
+ valid_label);
entity = make_transform_entity(cpstate, ENT_EDGE, (Node *)rel,
expr);
@@ -3964,7 +4092,8 @@ static Node *make_qual(cypher_parsestate *cpstate,
static Expr *transform_cypher_edge(cypher_parsestate *cpstate,
cypher_relationship *rel,
- List **target_list)
+ List **target_list,
+ bool valid_label)
{
ParseState *pstate = (ParseState *)cpstate;
char *schema_name;
@@ -3980,31 +4109,20 @@ static Expr *transform_cypher_edge(cypher_parsestate *cpstate,
{
rel->label = AG_DEFAULT_LABEL_EDGE;
}
- else
+ else if(!valid_label)
{
/*
* XXX: Need to determine proper rules, for when label does not exist
* or is for an edge. Maybe labels and edges should share names, like
* in openCypher. But these are stand in errors, to prevent
* segmentation faults, and other errors.
+ *
+ * Update: Nonexistent and mismatched labels now return a NULL value to
+ * prevent segmentation faults, and other errors. We can also consider
+ * if an all-purpose label would be useful.
*/
- label_cache_data *lcd =
- search_label_name_graph_cache(rel->label, cpstate->graph_oid);
-
- if (lcd == NULL)
- {
- ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("label %s does not exists", rel->label),
- parser_errposition(pstate, rel->location)));
- }
+ rel->label = NULL;
- if (lcd->kind != LABEL_KIND_EDGE)
- {
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("label %s is for vertices, not edges", rel->label),
- parser_errposition(pstate, rel->location)));
- }
}
if (rel->name != NULL)
@@ -4077,7 +4195,16 @@ static Expr *transform_cypher_edge(cypher_parsestate *cpstate,
}
schema_name = get_graph_namespace_name(cpstate->graph_name);
- rel_name = get_label_relation_name(rel->label, cpstate->graph_oid);
+
+ if (valid_label)
+ {
+ rel_name = get_label_relation_name(rel->label, cpstate->graph_oid);
+ }
+ else
+ {
+ rel_name = AG_DEFAULT_LABEL_EDGE;
+ }
+
label_range_var = makeRangeVar(schema_name, rel_name, -1);
alias = makeAlias(rel->name, NIL);
@@ -4091,7 +4218,14 @@ static Expr *transform_cypher_edge(cypher_parsestate *cpstate,
resno = pstate->p_next_resno++;
- expr = (Expr *)make_edge_expr(cpstate, rte, rel->label);
+ if (valid_label)
+ {
+ expr = (Expr *)make_edge_expr(cpstate, rte, rel->label);
+ }
+ else
+ {
+ expr = (Expr*)makeNullConst(AGTYPEOID, -1, InvalidOid);
+ }
if (rel->name)
{
@@ -4104,7 +4238,7 @@ static Expr *transform_cypher_edge(cypher_parsestate *cpstate,
static Expr *transform_cypher_node(cypher_parsestate *cpstate,
cypher_node *node, List **target_list,
- bool output_node)
+ bool output_node, bool valid_label)
{
ParseState *pstate = (ParseState *)cpstate;
char *schema_name;
@@ -4120,30 +4254,20 @@ static Expr *transform_cypher_node(cypher_parsestate *cpstate,
{
node->label = AG_DEFAULT_LABEL_VERTEX;
}
- else
+ else if (!valid_label)
{
/*
* XXX: Need to determine proper rules, for when label does not exist
* or is for an edge. Maybe labels and edges should share names, like
* in openCypher. But these are stand in errors, to prevent
* segmentation faults, and other errors.
+ *
+ * Update: Nonexistent and mismatched labels now return a NULL value to
+ * prevent segmentation faults, and other errors. We can also consider
+ * if an all-purpose label would be useful.
*/
- label_cache_data *lcd =
- search_label_name_graph_cache(node->label, cpstate->graph_oid);
+ node->label = NULL;
- if (lcd == NULL)
- {
- ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("label %s does not exists", node->label),
- parser_errposition(pstate, node->location)));
- }
- if (lcd->kind != LABEL_KIND_VERTEX)
- {
- ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("label %s is for edges, not vertices",
- node->label),
- parser_errposition(pstate, node->location)));
- }
}
if (!output_node)
@@ -4222,7 +4346,16 @@ static Expr *transform_cypher_node(cypher_parsestate *cpstate,
}
schema_name = get_graph_namespace_name(cpstate->graph_name);
- rel_name = get_label_relation_name(node->label, cpstate->graph_oid);
+
+ if (valid_label)
+ {
+ rel_name = get_label_relation_name(node->label, cpstate->graph_oid);
+ }
+ else
+ {
+ rel_name = AG_DEFAULT_LABEL_VERTEX;
+ }
+
label_range_var = makeRangeVar(schema_name, rel_name, -1);
alias = makeAlias(node->name, NIL);
@@ -4236,7 +4369,14 @@ static Expr *transform_cypher_node(cypher_parsestate *cpstate,
resno = pstate->p_next_resno++;
- expr = (Expr *)make_vertex_expr(cpstate, rte, node->label);
+ if (valid_label)
+ {
+ expr = (Expr *)make_vertex_expr(cpstate, rte, node->label);
+ }
+ else
+ {
+ expr = (Expr*)makeNullConst(AGTYPEOID, -1, InvalidOid);
+ }
/* make target entry and add it */
te = makeTargetEntry(expr, resno, node->name, false);
From 8e5497cd7ba99fbf7c4cafcc9901629383e0d6ff Mon Sep 17 00:00:00 2001
From: jbiz805 <49049733+jbiz805@users.noreply.github.com>
Date: Wed, 16 Nov 2022 03:06:57 +0900
Subject: [PATCH 032/191] Update CONTRIBUTING.md (#348)
Edited the details available in https://age.apache.org/contribution/how
---
CONTRIBUTING.md | 32 ++++++++++++++++++++++++++------
1 file changed, 26 insertions(+), 6 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 50a2c89e2..d13839179 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,15 +1,35 @@
# Contributing to Apache AGE
-First off, thank you for considering contributing.
+Welcome, future Contributor!
-## Issues
+First off, thank you for considering contributing to Apache AGE. Team AGE welcomes anyone who is willing to help us mature AGE to become a fully-featured graph database extension for PostgreSQL.
-Issues are very valuable to this project. Issues can be questions, ideas, suggestions or bug reports.
+There are multiple ways you can contribute to the Apache AGE and [Apache AGE Viewer](https://github.com/apache/age-viewer) projects. We hope that adding features, fixing bugs, and changing documentations can be fun and educational for anyone and everyone.
-Questions are encouraged. Your questions let us know what is unclear. If you spent longer than expected finding an answer let us know that too.
+## Code of Conduct
-Thank you for raising issues.
+The community members of Apache AGE are expected to follow the 'Apache Way'. Please read the [Code of Conduct](https://www.apache.org/foundation/policies/conduct) provided by Apache Software Foundation.
+
+## How to Start
+
+A great way to get involved in the project is to ask questions on the mailing lists, Apache AGE Discord, or the Apache Reddit forum (r/apacheage). Reviewing the list of projects in the Apache AGE and AGE Viewer GitHubs may help you understand the overall roadmap.
+
+Once you understand the ins and outs of Apache AGE, share your knowledge by helping the newcomers as well. Spending a few minutes to answer questions are a valuable open source community service, which also demonstrates your expertise.
+
+We strongly recommend you to subscribe the mailing lists, join the Apache AGE Discord and Apache AGE Reddit community (r/apacheage) to keep up to date on what's happening in AGE. Visit [joinus](https://age.apache.org/joinus) for pathways you can follow to help you get started.
## Pull Requests
-Pull requests are a great way to get your ideas into this repository. Check out our list of good [first issues](https://github.com/apache/age/labels/good%20first%20issue)
+Changes to AGE source code are proposed, reviewed, and committed via Github pull requests (described in Code Convention). Pull requests are a great way to get your ideas into this repository. Anyone can view and comment on active changes here. Reviewing others' changes are a good way to learn how the change process works and gain exposure to activity in various parts of the code. You can help by reviewing the changes, asking questions, or pointing out issues as simple as typos.
+
+## Documentation Changes
+
+You can propose changes to Apache AGE documentation, edit the Markdown source files for the Apache AGE website pages.
+
+## Bug Reports
+
+Ideally, bug reports are accompanied by a proposed code change to fix the bug. This isn't always possible, as those who discover a bug may not have the experience to fix it. A bug may be reported by creating a GitHub issue, but without creating a pull request.
+
+Bug reports are only useful, however, if they include enough information to understand, isolate and ideally reproduce the bug. Simply encountering an error does not mean a bug should be reported; search GitHub and inquire on the Apache AGE's dev mailing list first. Unreproducible bugs or simple error reports without context shall be closed.
+
+The more context about a bug, the better, such as: how the bug was introduced, by which commit, etc. It assists the committers in the decision process on how far the bug fix should be backported, when the pull request is merged. The pull request to fix the bug should narrow down the problem to the root cause. Data correctness/data loss bugs are very serious. Make sure the corresponding bug report GitHub issue is labeled as correctness or data-loss. Please send an email to dev@age.apache.org after submitting the bug report, to quickly draw attention to the issue. Performance issues are classified as bugs. The pull request to fix a performance bug must provide a benchmark to prove the problem is indeed fixed.
From 4e9110d0b88812b08c316ef7df2ff07f98e3a066 Mon Sep 17 00:00:00 2001
From: Jamie Gaskins
Date: Sun, 20 Nov 2022 20:50:51 -0500
Subject: [PATCH 033/191] Use Debian Buster base image (#243)
postgres:11 is currently based on Debian Bullseye. Presumable it used to
be based on Buster.
See: https://packages.debian.org/search?suite=default§ion=all&arch=any&searchon=names&keywords=postgresql-server-dev
---
Dockerfile | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index d0b449aed..11d882b2e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -17,7 +17,7 @@
#
-FROM postgres:11
+FROM postgres:11-buster
RUN apt-get update
RUN apt-get install --assume-yes --no-install-recommends --no-install-suggests \
@@ -31,4 +31,4 @@ RUN cd /age && make install
COPY docker-entrypoint-initdb.d/00-create-extension-age.sql /docker-entrypoint-initdb.d/00-create-extension-age.sql
-CMD ["postgres", "-c", "shared_preload_libraries=age"]
\ No newline at end of file
+CMD ["postgres", "-c", "shared_preload_libraries=age"]
From f49c322eee85e47e4cfe4a2b1cacdbe74c9f4496 Mon Sep 17 00:00:00 2001
From: John Gemignani
Date: Mon, 21 Nov 2022 09:33:58 -0800
Subject: [PATCH 034/191] Add the ability to pass parameters to the cypher
function
Added the ability to pass parameters to the cypher() function via
a function called age_prepare_cypher().
This extra function is necessary because the cypher() function itself
isn't actually executed - it is instead transformed and replaced. This
means that it needs to have its input parameters resolved prior to
that transform. However, parameters aren't resolved until the
execution phase. So, another command to resolve them needs to run
prior to the cypher() function call.
This mainly impacts the drivers, which will need to be updated.
Additionally, modified the golang driver as an example of this new
usage.
Added regression tests.
---
Makefile | 2 +
age--1.1.0.sql | 12 +-
drivers/golang/age/age.go | 52 ++-----
drivers/golang/go.mod | 2 +-
drivers/golang/go.sum | 14 ++
drivers/golang/parser/Age.g4 | 3 +-
regress/expected/analyze.out | 135 +++++++++++++++++
regress/sql/analyze.sql | 60 ++++++++
src/backend/parser/cypher_analyze.c | 178 +++++++++++++++++------
src/backend/utils/adt/age_session_info.c | 170 ++++++++++++++++++++++
src/include/utils/age_session_info.h | 29 ++++
11 files changed, 571 insertions(+), 86 deletions(-)
create mode 100644 drivers/golang/go.sum
create mode 100644 regress/expected/analyze.out
create mode 100644 regress/sql/analyze.sql
create mode 100644 src/backend/utils/adt/age_session_info.c
create mode 100644 src/include/utils/age_session_info.h
diff --git a/Makefile b/Makefile
index 8c685a33d..da84c31ce 100644
--- a/Makefile
+++ b/Makefile
@@ -57,6 +57,7 @@ OBJS = src/backend/age.o \
src/backend/utils/adt/agtype_parser.o \
src/backend/utils/adt/agtype_util.o \
src/backend/utils/adt/age_global_graph.o \
+ src/backend/utils/adt/age_session_info.o \
src/backend/utils/adt/age_vle.o \
src/backend/utils/adt/cypher_funcs.o \
src/backend/utils/adt/ag_float8_supp.o \
@@ -93,6 +94,7 @@ REGRESS = scan \
age_global_graph \
age_load \
index \
+ analyze \
drop
srcdir=`pwd`
diff --git a/age--1.1.0.sql b/age--1.1.0.sql
index e5c42192a..6fe30add8 100644
--- a/age--1.1.0.sql
+++ b/age--1.1.0.sql
@@ -3385,8 +3385,9 @@ AS 'MODULE_PATHNAME';
--
-- query functions
--
-CREATE FUNCTION ag_catalog.cypher(graph_name name, query_string cstring,
- params agtype = NULL)
+CREATE FUNCTION ag_catalog.cypher(graph_name name = NULL,
+ query_string cstring = NULL,
+ params agtype = NULL)
RETURNS SETOF record
LANGUAGE c
AS 'MODULE_PATHNAME';
@@ -4156,6 +4157,13 @@ STABLE
PARALLEL SAFE
AS 'MODULE_PATHNAME';
+CREATE FUNCTION ag_catalog.age_prepare_cypher(cstring, cstring)
+RETURNS boolean
+LANGUAGE c
+STABLE
+PARALLEL SAFE
+AS 'MODULE_PATHNAME';
+
--
-- End
--
diff --git a/drivers/golang/age/age.go b/drivers/golang/age/age.go
index 38a629f21..3f8392058 100644
--- a/drivers/golang/age/age.go
+++ b/drivers/golang/age/age.go
@@ -76,29 +76,34 @@ func execCypher(cursorProvider CursorProvider, tx *sql.Tx, graphName string, col
cypherStmt := fmt.Sprintf(cypher, args...)
- buf.WriteString("SELECT * from cypher('")
- buf.WriteString(graphName)
- buf.WriteString("', $$ ")
- buf.WriteString(cypherStmt)
- buf.WriteString(" $$)")
- buf.WriteString(" as (")
- buf.WriteString("v0 agtype")
+ buf.WriteString("SELECT * from cypher(NULL,NULL) as (v0 agtype")
+
for i := 1; i < columnCount; i++ {
buf.WriteString(fmt.Sprintf(", v%d agtype", i))
}
- buf.WriteString(")")
+ buf.WriteString(");")
stmt := buf.String()
+ // Pass in the graph name and cypher statement via parameters to prepare
+ // the cypher function call for session info.
+
+ prepare_stmt := "SELECT * FROM age_prepare_cypher($1, $2);"
+ _, perr := tx.Exec(prepare_stmt, graphName, cypherStmt)
+ if perr != nil {
+ fmt.Println(prepare_stmt + " " + graphName + " " + cypher)
+ return nil, perr
+ }
+
if columnCount == 0 {
- _, err := tx.Exec(stmt)
+ _, err := tx.Exec(stmt)
if err != nil {
fmt.Println(stmt)
return nil, err
}
return nil, nil
} else {
- rows, err := tx.Query(stmt)
+ rows, err := tx.Query(stmt)
if err != nil {
fmt.Println(stmt)
return nil, err
@@ -133,33 +138,6 @@ func ExecCypherMap(tx *sql.Tx, graphName string, columnCount int, cypher string,
return cypherMapCursor, err
}
-// // ExecCypher execute without return
-// // CREATE , DROP .... */
-// func ExecCypher2(tx *sql.Tx, graphName string, cypher string, args ...interface{}) error {
-// cypherStmt := fmt.Sprintf(cypher, args...)
-// stmt := fmt.Sprintf("SELECT * from cypher('%s', $$ %s $$) as (v agtype);",
-// graphName, cypherStmt)
-
-// _, err := tx.Exec(stmt)
-// return err
-// }
-
-// // QueryCypher execute with return
-// // MATCH .... RETURN ....
-// // CREATE , DROP .... RETURN ...
-// func QueryCypher(tx *sql.Tx, graphName string, cypher string, args ...interface{}) (*CypherCursor, error) {
-// cypherStmt := fmt.Sprintf(cypher, args...)
-// stmt := fmt.Sprintf("SELECT * from cypher('%s', $$ %s $$) as (v agtype);",
-// graphName, cypherStmt)
-
-// rows, err := tx.Query(stmt)
-// if err != nil {
-// return nil, err
-// } else {
-// return NewCypherCursor(1, rows), nil
-// }
-// }
-
type Age struct {
db *sql.DB
graphName string
diff --git a/drivers/golang/go.mod b/drivers/golang/go.mod
index bd27c5c16..81525d66f 100644
--- a/drivers/golang/go.mod
+++ b/drivers/golang/go.mod
@@ -22,7 +22,7 @@ module github.com/apache/age/drivers/golang
go 1.16
require (
- github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20221020221120-327014a2b522
+ github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210521184019-c5ad59b459ec
github.com/lib/pq v1.10.2
github.com/stretchr/testify v1.7.0
)
diff --git a/drivers/golang/go.sum b/drivers/golang/go.sum
new file mode 100644
index 000000000..13a472eb9
--- /dev/null
+++ b/drivers/golang/go.sum
@@ -0,0 +1,14 @@
+github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210521184019-c5ad59b459ec h1:EEyRvzmpEUZ+I8WmD5cw/vY8EqhambkOqy5iFr0908A=
+github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210521184019-c5ad59b459ec/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
+github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/drivers/golang/parser/Age.g4 b/drivers/golang/parser/Age.g4
index 5ba157ace..c5516a90a 100644
--- a/drivers/golang/parser/Age.g4
+++ b/drivers/golang/parser/Age.g4
@@ -79,7 +79,7 @@ BOOL
;
NULL
- : 'null'
+ : 'null'
;
@@ -125,4 +125,3 @@ fragment EXP
WS
: [ \t\n\r] + -> skip
;
-
\ No newline at end of file
diff --git a/regress/expected/analyze.out b/regress/expected/analyze.out
new file mode 100644
index 000000000..5b248385a
--- /dev/null
+++ b/regress/expected/analyze.out
@@ -0,0 +1,135 @@
+/*
+ * 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;
+--
+-- Start of tests
+--
+SELECT * FROM create_graph('analyze');
+NOTICE: graph "analyze" has been created
+ create_graph
+--------------
+
+(1 row)
+
+SELECT * FROM cypher('analyze', $$ CREATE (u) RETURN u $$) AS (result agtype);
+ result
+----------------------------------------------------------------
+ {"id": 281474976710657, "label": "", "properties": {}}::vertex
+(1 row)
+
+-- should error due to invalid input to cypher function
+SELECT * FROM cypher(NULL, NULL) AS (result agtype);
+ERROR: a name constant is expected
+LINE 1: SELECT * FROM cypher(NULL, NULL) AS (result agtype);
+ ^
+SELECT * FROM cypher('analyze', NULL) AS (result agtype);
+ERROR: a dollar-quoted string constant is expected
+LINE 1: SELECT * FROM cypher('analyze', NULL) AS (result agtype);
+ ^
+SELECT * FROM cypher(NULL, '') AS (result agtype);
+ERROR: a name constant is expected
+LINE 1: SELECT * FROM cypher(NULL, '') AS (result agtype);
+ ^
+SELECT * FROM cypher('', '') AS (result agtype);
+ERROR: a dollar-quoted string constant is expected
+LINE 1: SELECT * FROM cypher('', '') AS (result agtype);
+ ^
+SELECT * FROM cypher('analyze', '') AS (result agtype);
+ERROR: a dollar-quoted string constant is expected
+LINE 1: SELECT * FROM cypher('analyze', '') AS (result agtype);
+ ^
+-- should error due to bad cypher statement
+SELECT * FROM cypher('analyze', $$ $$) AS (result agtype);
+ERROR: syntax error at end of input
+LINE 1: SELECT * FROM cypher('analyze', $$ $$) AS (result agtype);
+ ^
+-- should return false due to invalid input to age_prepare_function
+SELECT * FROM age_prepare_cypher(NULL, NULL);
+ age_prepare_cypher
+--------------------
+ f
+(1 row)
+
+SELECT * FROM age_prepare_cypher('analyze', NULL);
+ age_prepare_cypher
+--------------------
+ f
+(1 row)
+
+SELECT * FROM age_prepare_cypher(NULL, '');
+ age_prepare_cypher
+--------------------
+ f
+(1 row)
+
+-- should return true but cypher should fail
+SELECT * FROM age_prepare_cypher('analyze', '');
+ age_prepare_cypher
+--------------------
+ t
+(1 row)
+
+SELECT * FROM cypher(NULL, NULL) AS (result agtype);
+ERROR: syntax error at end of input
+LINE 1: SELECT * FROM cypher(NULL, NULL) AS (result agtype);
+ ^
+-- should return true and execute cypher command
+SELECT * FROM age_prepare_cypher('analyze', 'MATCH (u) RETURN (u)');
+ age_prepare_cypher
+--------------------
+ t
+(1 row)
+
+SELECT * FROM cypher(NULL, NULL) AS (result agtype);
+ result
+----------------------------------------------------------------
+ {"id": 281474976710657, "label": "", "properties": {}}::vertex
+(1 row)
+
+-- should error due to invalid input to cypher function
+SELECT * FROM cypher(NULL, NULL) AS (result agtype);
+ERROR: a name constant is expected
+LINE 1: SELECT * FROM cypher(NULL, NULL) AS (result agtype);
+ ^
+-- should return true but cypher should fail
+SELECT * FROM age_prepare_cypher('analyze', '$$ $$');
+ age_prepare_cypher
+--------------------
+ t
+(1 row)
+
+SELECT * FROM cypher(NULL, NULL) AS (result agtype);
+ERROR: unexpected character at or near "$"
+LINE 1: SELECT * FROM cypher(NULL, NULL) AS (result agtype);
+ ^
+-- drop graphs
+SELECT * FROM drop_graph('analyze', true);
+NOTICE: drop cascades to 2 other objects
+DETAIL: drop cascades to table "analyze"._ag_label_vertex
+drop cascades to table "analyze"._ag_label_edge
+NOTICE: graph "analyze" has been dropped
+ drop_graph
+------------
+
+(1 row)
+
+--
+-- End of tests
+--
diff --git a/regress/sql/analyze.sql b/regress/sql/analyze.sql
new file mode 100644
index 000000000..881ef3ffd
--- /dev/null
+++ b/regress/sql/analyze.sql
@@ -0,0 +1,60 @@
+/*
+ * 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;
+
+--
+-- Start of tests
+--
+
+SELECT * FROM create_graph('analyze');
+SELECT * FROM cypher('analyze', $$ CREATE (u) RETURN u $$) AS (result agtype);
+
+-- should error due to invalid input to cypher function
+SELECT * FROM cypher(NULL, NULL) AS (result agtype);
+SELECT * FROM cypher('analyze', NULL) AS (result agtype);
+SELECT * FROM cypher(NULL, '') AS (result agtype);
+SELECT * FROM cypher('', '') AS (result agtype);
+SELECT * FROM cypher('analyze', '') AS (result agtype);
+-- should error due to bad cypher statement
+SELECT * FROM cypher('analyze', $$ $$) AS (result agtype);
+
+-- should return false due to invalid input to age_prepare_function
+SELECT * FROM age_prepare_cypher(NULL, NULL);
+SELECT * FROM age_prepare_cypher('analyze', NULL);
+SELECT * FROM age_prepare_cypher(NULL, '');
+-- should return true but cypher should fail
+SELECT * FROM age_prepare_cypher('analyze', '');
+SELECT * FROM cypher(NULL, NULL) AS (result agtype);
+-- should return true and execute cypher command
+SELECT * FROM age_prepare_cypher('analyze', 'MATCH (u) RETURN (u)');
+SELECT * FROM cypher(NULL, NULL) AS (result agtype);
+-- should error due to invalid input to cypher function
+SELECT * FROM cypher(NULL, NULL) AS (result agtype);
+-- should return true but cypher should fail
+SELECT * FROM age_prepare_cypher('analyze', '$$ $$');
+SELECT * FROM cypher(NULL, NULL) AS (result agtype);
+
+-- drop graphs
+SELECT * FROM drop_graph('analyze', true);
+
+--
+-- End of tests
+--
diff --git a/src/backend/parser/cypher_analyze.c b/src/backend/parser/cypher_analyze.c
index 1a8ea23e5..685c7cb0a 100644
--- a/src/backend/parser/cypher_analyze.c
+++ b/src/backend/parser/cypher_analyze.c
@@ -42,6 +42,7 @@
#include "parser/cypher_parse_node.h"
#include "parser/cypher_parser.h"
#include "utils/ag_func.h"
+#include "utils/age_session_info.h"
#include "utils/agtype.h"
/*
@@ -75,6 +76,7 @@ static Query *analyze_cypher_and_coerce(List *stmt, RangeTblFunction *rtfunc,
char *graph_name, Oid graph_oid,
Param *params);
+
void post_parse_analyze_init(void)
{
prev_post_parse_analyze_hook = post_parse_analyze_hook;
@@ -302,15 +304,18 @@ static void convert_cypher_to_subquery(RangeTblEntry *rte, ParseState *pstate)
{
RangeTblFunction *rtfunc = linitial(rte->functions);
FuncExpr *funcexpr = (FuncExpr *)rtfunc->funcexpr;
- Node *arg;
- Name graph_name;
- Oid graph_oid;
- const char *query_str;
- int query_loc;
- Param *params;
- errpos_ecb_state ecb_state;
- List *stmt;
- Query *query;
+ Node *arg1 = NULL;
+ Node *arg2 = NULL;
+ Node *arg3 = NULL;
+ Name graph_name = NULL;
+ char *graph_name_str = NULL;
+ Oid graph_oid = InvalidOid;
+ const char *query_str = NULL;
+ int query_loc = -1;
+ Param *params = NULL;
+ errpos_ecb_state ecb_state = {{0}};
+ List *stmt = NULL;
+ Query *query = NULL;
/*
* We cannot apply this feature directly to SELECT subquery because the
@@ -326,28 +331,14 @@ static void convert_cypher_to_subquery(RangeTblEntry *rte, ParseState *pstate)
parser_errposition(pstate, exprLocation((Node *)funcexpr))));
}
- arg = linitial(funcexpr->args);
- Assert(exprType(arg) == NAMEOID);
+ /* get our first 2 arguments */
+ arg1 = linitial(funcexpr->args);
+ arg2 = lsecond(funcexpr->args);
- graph_name = expr_get_const_name(arg);
- if (!graph_name)
- {
- ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR),
- errmsg("a name constant is expected"),
- parser_errposition(pstate, exprLocation(arg))));
- }
+ Assert(exprType(arg1) == NAMEOID);
+ Assert(exprType(arg2) == CSTRINGOID);
- graph_oid = get_graph_oid(NameStr(*graph_name));
- if (!OidIsValid(graph_oid))
- {
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_SCHEMA),
- errmsg("graph \"%s\" does not exist", NameStr(*graph_name)),
- parser_errposition(pstate, exprLocation(arg))));
- }
-
- arg = lsecond(funcexpr->args);
- Assert(exprType(arg) == CSTRINGOID);
+ graph_name = expr_get_const_name(arg1);
/*
* Since cypher() function is nothing but an interface to get a Cypher
@@ -361,32 +352,131 @@ static void convert_cypher_to_subquery(RangeTblEntry *rte, ParseState *pstate)
* may differ from what they are shown. This will confuse users.
* * In the case above, the error position may not be accurate.
*/
- query_str = expr_get_const_cstring(arg, pstate->p_sourcetext);
- if (!query_str)
+ query_str = expr_get_const_cstring(arg2, pstate->p_sourcetext);
+
+ /*
+ * Validate appropriate cypher function usage -
+ *
+ * Session info OVERRIDES ANY INPUT PASSED and if any is passed, it will
+ * cause the cypher function to error out.
+ *
+ * If this is using session info, both of the first 2 input parameters need
+ * to be NULL, in addition to the session info being set up. Furthermore,
+ * the input parameters passed in by session info need to both be non-NULL.
+ *
+ * If this is not using session info, both input parameters need to be
+ * non-NULL.
+ *
+ */
+ if (is_session_info_prepared())
{
- ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR),
- errmsg("a dollar-quoted string constant is expected"),
- parser_errposition(pstate, exprLocation(arg))));
+ /* check to see if either input parameter is non-NULL*/
+ if (graph_name != NULL || query_str != NULL)
+ {
+ Node *arg = (graph_name == NULL) ? arg1 : arg2;
+
+ /*
+ * Make sure to clean up session info because the ereport will
+ * cause the function to exit.
+ */
+ reset_session_info();
+
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("session info requires cypher(NULL, NULL) to be passed"),
+ parser_errposition(pstate, exprLocation(arg))));
+ }
+ /* get our input parameters from session info */
+ else
+ {
+ graph_name_str = get_session_info_graph_name();
+ query_str = get_session_info_cypher_statement();
+
+ /* check to see if either are NULL */
+ if (graph_name_str == NULL || query_str == NULL)
+ {
+ /*
+ * Make sure to clean up session info because the ereport will
+ * cause the function to exit.
+ */
+ reset_session_info();
+
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("both session info parameters need to be non-NULL"),
+ parser_errposition(pstate, -1)));
+ }
+ }
+ }
+ /* otherwise, we get the parameters from the passed function input */
+ else
+ {
+ /* get the graph name string from the passed parameters */
+ if (!graph_name)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("a name constant is expected"),
+ parser_errposition(pstate, exprLocation(arg1))));
+ }
+ else
+ {
+ graph_name_str = NameStr(*graph_name);
+ }
+ /* get the query string from the passed parameters */
+ if (!query_str)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("a dollar-quoted string constant is expected"),
+ parser_errposition(pstate, exprLocation(arg2))));
+ }
}
- query_loc = get_query_location(((Const *)arg)->location,
- pstate->p_sourcetext);
/*
- * Check to see if the cypher function had any parameters passed to it,
+ * The session info is only valid for one cypher call. Now that we are done
+ * with it, if it was used, we need to reset it to free the memory used.
+ * Additionally, the query location is dependent on how we got the query
+ * string, so set the location accordingly.
+ */
+ if (is_session_info_prepared())
+ {
+ reset_session_info();
+ query_loc = 0;
+ }
+ else
+ {
+ /* this call will crash if we use session info */
+ query_loc = get_query_location(((Const *)arg2)->location,
+ pstate->p_sourcetext);
+ }
+
+ /* validate the graph exists */
+ graph_oid = get_graph_oid(graph_name_str);
+ if (!OidIsValid(graph_oid))
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_SCHEMA),
+ errmsg("graph \"%s\" does not exist", graph_name_str),
+ parser_errposition(pstate, exprLocation(arg1))));
+ }
+
+ /*
+ * Check to see if the cypher function had a third parameter passed to it,
* if so make sure Postgres parsed the second argument to a Param node.
*/
if (list_length(funcexpr->args) == 3)
{
- arg = lthird(funcexpr->args);
- if (!IsA(arg, Param))
+ arg3 = lthird(funcexpr->args);
+ if (!IsA(arg3, Param))
{
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("third argument of cypher function must be a parameter"),
- parser_errposition(pstate, exprLocation(arg))));
+ parser_errposition(pstate, exprLocation(arg3))));
}
- params = (Param *)arg;
+ params = (Param *)arg3;
}
else
{
@@ -454,13 +544,13 @@ static void convert_cypher_to_subquery(RangeTblEntry *rte, ParseState *pstate)
}
query = analyze_cypher(stmt, pstate, query_str, query_loc,
- NameStr(*graph_name), graph_oid, params);
+ graph_name_str, graph_oid, params);
}
else
{
query = analyze_cypher_and_coerce(stmt, rtfunc, pstate, query_str,
- query_loc, NameStr(*graph_name),
- graph_oid, params);
+ query_loc, graph_name_str, graph_oid,
+ params);
}
pstate->p_lateral_active = false;
diff --git a/src/backend/utils/adt/age_session_info.c b/src/backend/utils/adt/age_session_info.c
new file mode 100644
index 000000000..193aaa104
--- /dev/null
+++ b/src/backend/utils/adt/age_session_info.c
@@ -0,0 +1,170 @@
+/*
+ * 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.
+ */
+
+#include "postgres.h"
+
+#include "funcapi.h"
+
+#include
+#include "utils/age_session_info.h"
+
+/*
+ * static/global session info variables for use with the driver interface.
+ */
+static int session_info_pid = -1;
+static char *session_info_graph_name = NULL;
+static char *session_info_cypher_statement = NULL;
+static bool session_info_prepared = false;
+
+static void set_session_info(char *graph_name, char *cypher_statement);
+
+/* function to set the session info. it will clean it, if necessary. */
+static void set_session_info(char *graph_name, char *cypher_statement)
+{
+ MemoryContext oldctx = NULL;
+
+ if (is_session_info_prepared())
+ {
+ reset_session_info();
+ }
+
+ /* we need to use a higher memory context for the data pointed to. */
+ oldctx = MemoryContextSwitchTo(TopMemoryContext);
+
+ if (graph_name != NULL)
+ {
+ session_info_graph_name = pstrdup(graph_name);
+ }
+ else
+ {
+ session_info_graph_name = NULL;
+ }
+
+ if (cypher_statement != NULL)
+ {
+ session_info_cypher_statement = pstrdup(cypher_statement);
+ }
+ else
+ {
+ session_info_cypher_statement = NULL;
+ }
+
+ /* switch back to the original context */
+ MemoryContextSwitchTo(oldctx);
+
+ session_info_pid = getpid();
+ session_info_prepared = true;
+}
+
+/*
+ * Helper function to return the value of session_info_cypher_statement or NULL
+ * if the value isn't set. The value returned is a copy, so please free it when
+ * done.
+ */
+char *get_session_info_graph_name(void)
+{
+ if (is_session_info_prepared() &&
+ session_info_graph_name != NULL)
+ {
+ return pstrdup(session_info_graph_name);
+ }
+
+ return NULL;
+}
+
+/*
+ * Helper function to return the value of session_info_cypher_statement or NULL
+ * if the value isn't set. The value returned is a copy, so please free it when
+ * done.
+ */
+char *get_session_info_cypher_statement(void)
+{
+ if (is_session_info_prepared() &&
+ session_info_cypher_statement != NULL)
+ {
+ return pstrdup(session_info_cypher_statement);
+ }
+
+ return NULL;
+}
+
+/* function to return the state of the session info data */
+bool is_session_info_prepared(void)
+{
+ /* is the session infor prepared AND is the pid the same pid */
+ if (session_info_prepared == true &&
+ session_info_pid == getpid())
+ {
+ return true;
+ }
+
+ return false;
+}
+
+/* function to clean and reset the session info back to default values */
+void reset_session_info(void)
+{
+ /* if the session info is prepared, free the strings */
+ if (session_info_prepared == true)
+ {
+ if (session_info_graph_name != NULL)
+ {
+ pfree(session_info_graph_name);
+ }
+
+ if (session_info_cypher_statement != NULL)
+ {
+ pfree(session_info_cypher_statement);
+ }
+ }
+
+ /* reset the session info back to default unused values */
+ session_info_graph_name = NULL;
+ session_info_cypher_statement = NULL;
+ session_info_prepared = false;
+ session_info_pid = -1;
+}
+
+/* AGE SQL function to prepare session info */
+PG_FUNCTION_INFO_V1(age_prepare_cypher);
+
+Datum age_prepare_cypher(PG_FUNCTION_ARGS)
+{
+ char *graph_name_str = NULL;
+ char *cypher_statement_str = NULL;
+
+ /* both arguments must be non-NULL */
+ if (PG_ARGISNULL(0) || PG_ARGISNULL(1))
+ {
+ PG_RETURN_BOOL(false);
+ }
+
+ graph_name_str = PG_GETARG_CSTRING(0);
+ cypher_statement_str = PG_GETARG_CSTRING(1);
+
+ /* both strings must be non-NULL */
+ if (graph_name_str == NULL || cypher_statement_str == NULL)
+ {
+ PG_RETURN_BOOL(false);
+ }
+
+ set_session_info(graph_name_str, cypher_statement_str);
+
+ PG_RETURN_BOOL(true);
+}
diff --git a/src/include/utils/age_session_info.h b/src/include/utils/age_session_info.h
new file mode 100644
index 000000000..ebf0035ab
--- /dev/null
+++ b/src/include/utils/age_session_info.h
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+#ifndef AGE_SESSION_INFO_H
+#define AGE_SESSION_INFO_H
+
+bool is_session_info_prepared(void);
+char *get_session_info_graph_name(void);
+char *get_session_info_cypher_statement(void);
+void reset_session_info(void);
+
+#endif
+
From c17d8330a7b323b4a5071cab47a8272b9ef43abd Mon Sep 17 00:00:00 2001
From: John Gemignani
Date: Tue, 22 Nov 2022 16:43:52 -0800
Subject: [PATCH 035/191] Add license header
Added license header to the following 3 files -
regress/sql/age_global_graph.sql
regress/sql/age_load.sql
regress/sql/index.sql
Adjusted regression tests for these 3.
---
regress/expected/age_global_graph.out | 29 ++++++++++++++++++++------
regress/expected/age_load.out | 18 ++++++++++++++++
regress/expected/index.out | 18 ++++++++++++++++
regress/sql/age_global_graph.sql | 30 +++++++++++++++++++++------
regress/sql/age_load.sql | 19 +++++++++++++++++
regress/sql/index.sql | 19 +++++++++++++++++
6 files changed, 121 insertions(+), 12 deletions(-)
diff --git a/regress/expected/age_global_graph.out b/regress/expected/age_global_graph.out
index 171ec1bd5..9512a7a7d 100644
--- a/regress/expected/age_global_graph.out
+++ b/regress/expected/age_global_graph.out
@@ -1,9 +1,27 @@
+/*
+ * 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;
--
--- test delete_specific_GRAPH_global_contexts function
+-- test delete_specific_GRAPH_global_contexts function
--
--- create 3 graphs
+-- create 3 graphs
SELECT * FROM create_graph('age_global_graph_1');
NOTICE: graph "age_global_graph_1" has been created
create_graph
@@ -43,7 +61,7 @@ SELECT * FROM cypher('age_global_graph_3', $$ CREATE (v:vertex_from_graph_3) RET
{"id": 844424930131969, "label": "vertex_from_graph_3", "properties": {}}::vertex
(1 row)
--- load contexts using the vertex_stats command
+-- load contexts using the vertex_stats command
SELECT * FROM cypher('age_global_graph_3', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype);
result
-----------------------------------------------------------------------------------------------------------
@@ -106,11 +124,10 @@ SELECT * FROM cypher('age_global_graph_3', $$ RETURN delete_global_graphs('age_g
false
(1 row)
-
--
--- test delete_GRAPH_global_contexts function
+-- test delete_GRAPH_global_contexts function
--
--- load contexts again
+-- load contexts again
SELECT * FROM cypher('age_global_graph_3', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype);
result
-----------------------------------------------------------------------------------------------------------
diff --git a/regress/expected/age_load.out b/regress/expected/age_load.out
index bae5924b3..5e74eaae6 100644
--- a/regress/expected/age_load.out
+++ b/regress/expected/age_load.out
@@ -1,3 +1,21 @@
+/*
+ * 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.
+ */
\! cp -r regress/age_load/data regress/instance/data/age_load
LOAD 'age';
SET search_path TO ag_catalog;
diff --git a/regress/expected/index.out b/regress/expected/index.out
index a27ca5be0..c4f9012b3 100644
--- a/regress/expected/index.out
+++ b/regress/expected/index.out
@@ -1,3 +1,21 @@
+/*
+ * 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.
+ */
\! cp -r regress/age_load/data regress/instance/data/age_load
LOAD 'age';
SET search_path TO ag_catalog;
diff --git a/regress/sql/age_global_graph.sql b/regress/sql/age_global_graph.sql
index a62d3358a..fa2dba9a7 100644
--- a/regress/sql/age_global_graph.sql
+++ b/regress/sql/age_global_graph.sql
@@ -1,11 +1,30 @@
+/*
+ * 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;
--
--- test delete_specific_GRAPH_global_contexts function
+-- test delete_specific_GRAPH_global_contexts function
--
--- create 3 graphs
+-- create 3 graphs
SELECT * FROM create_graph('age_global_graph_1');
SELECT * FROM cypher('age_global_graph_1', $$ CREATE (v:vertex_from_graph_1) RETURN v $$) AS (v agtype);
@@ -15,7 +34,7 @@ SELECT * FROM cypher('age_global_graph_2', $$ CREATE (v:vertex_from_graph_2) RET
SELECT * FROM create_graph('age_global_graph_3');
SELECT * FROM cypher('age_global_graph_3', $$ CREATE (v:vertex_from_graph_3) RETURN v $$) AS (v agtype);
--- load contexts using the vertex_stats command
+-- load contexts using the vertex_stats command
SELECT * FROM cypher('age_global_graph_3', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype);
SELECT * FROM cypher('age_global_graph_2', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype);
SELECT * FROM cypher('age_global_graph_1', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype);
@@ -38,12 +57,11 @@ SELECT * FROM cypher('age_global_graph_2', $$ RETURN delete_global_graphs('age_g
SELECT * FROM cypher('age_global_graph_1', $$ RETURN delete_global_graphs('age_global_graph_1') $$) AS (result agtype);
SELECT * FROM cypher('age_global_graph_3', $$ RETURN delete_global_graphs('age_global_graph_3') $$) AS (result agtype);
-
--
--- test delete_GRAPH_global_contexts function
+-- test delete_GRAPH_global_contexts function
--
--- load contexts again
+-- load contexts again
SELECT * FROM cypher('age_global_graph_3', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype);
SELECT * FROM cypher('age_global_graph_2', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype);
SELECT * FROM cypher('age_global_graph_1', $$ MATCH (u) RETURN vertex_stats(u) $$) AS (result agtype);
diff --git a/regress/sql/age_load.sql b/regress/sql/age_load.sql
index 3516a170b..105c2fa60 100644
--- a/regress/sql/age_load.sql
+++ b/regress/sql/age_load.sql
@@ -1,3 +1,22 @@
+/*
+ * 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.
+ */
+
\! cp -r regress/age_load/data regress/instance/data/age_load
LOAD 'age';
diff --git a/regress/sql/index.sql b/regress/sql/index.sql
index 9e1cd0434..95322082d 100644
--- a/regress/sql/index.sql
+++ b/regress/sql/index.sql
@@ -1,3 +1,22 @@
+/*
+ * 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.
+ */
+
\! cp -r regress/age_load/data regress/instance/data/age_load
LOAD 'age';
From ed44f91cb2607605ec2e225248e9ca46e146c215 Mon Sep 17 00:00:00 2001
From: Dehowe Feng
Date: Wed, 23 Nov 2022 15:33:44 -0800
Subject: [PATCH 036/191] Modify the python driver's parameterization
Modified the python driver to pass parameters to the cypher() function
via age_prepare_cypher()
---
drivers/python/age/age.py | 53 +++++++++++++++++++++++---------
drivers/python/age/exceptions.py | 2 +-
2 files changed, 40 insertions(+), 15 deletions(-)
diff --git a/drivers/python/age/age.py b/drivers/python/age/age.py
index 2c59610a6..7ba4a27f7 100644
--- a/drivers/python/age/age.py
+++ b/drivers/python/age/age.py
@@ -17,6 +17,7 @@
import psycopg2
from psycopg2 import errors
from psycopg2 import extensions as ext
+from psycopg2 import sql
from .exceptions import *
from .builder import ResultHandler , parseAgeValue, newResultHandler
@@ -47,15 +48,15 @@ def setUpAge(conn:ext.connection, graphName:str):
# Create the graph, if it does not exist
def checkGraphCreated(conn:ext.connection, graphName:str):
with conn.cursor() as cursor:
- cursor.execute("SELECT count(*) FROM ag_graph WHERE name=%s", (graphName,))
+ cursor.execute(sql.SQL("SELECT count(*) FROM ag_graph WHERE name={graphName}").format(graphName=sql.Literal(graphName)))
if cursor.fetchone()[0] == 0:
- cursor.execute("SELECT create_graph(%s);", (graphName,))
+ cursor.execute(sql.SQL("SELECT create_graph({graphName});").format(graphName=sql.Literal(graphName)))
conn.commit()
def deleteGraph(conn:ext.connection, graphName:str):
with conn.cursor() as cursor:
- cursor.execute("SELECT drop_graph(%s, true);", (graphName,))
+ cursor.execute(sql.SQL("SELECT drop_graph({graphName}, true);").format(graphName=sql.Literal(graphName)))
conn.commit()
@@ -76,11 +77,7 @@ def buildCypher(graphName:str, cypherStmt:str, columns:list) ->str:
columnExp.append('v agtype')
stmtArr = []
- stmtArr.append("SELECT * from cypher('")
- stmtArr.append(graphName)
- stmtArr.append("', $$ ")
- stmtArr.append(cypherStmt)
- stmtArr.append(" $$) as (")
+ stmtArr.append("SELECT * from cypher(NULL,NULL) as (")
stmtArr.append(','.join(columnExp))
stmtArr.append(");")
return "".join(stmtArr)
@@ -101,7 +98,7 @@ def execSql(conn:ext.connection, stmt:str, commit:bool=False, params:tuple=None)
raise cause
except Exception as cause:
conn.rollback()
- raise SqlExcutionError("Excution ERR[" + str(cause) +"](" + stmt +")", cause)
+ raise SqlExecutionError("Execution ERR[" + str(cause) +"](" + stmt +")", cause)
def querySql(conn:ext.connection, stmt:str, params:tuple=None) -> ext.cursor :
@@ -115,23 +112,51 @@ def execCypher(conn:ext.connection, graphName:str, cypherStmt:str, cols:list=Non
if conn == None or conn.closed:
raise _EXCEPTION_NoConnection
- stmt = buildCypher(graphName, cypherStmt, cols)
+ cursor = conn.cursor()
+ #clean up the string for mogrificiation
+ cypherStmt = cypherStmt.replace("\n", "")
+ cypherStmt = cypherStmt.replace("\t", "")
+ cypher = str(cursor.mogrify(cypherStmt, params))
+ cypher = cypher[2:len(cypher)-1]
+
+ preparedStmt = "SELECT * FROM age_prepare_cypher({graphName},{cypherStmt})"
cursor = conn.cursor()
try:
- cursor.execute(stmt, params)
+ cursor.execute(sql.SQL(preparedStmt).format(graphName=sql.Literal(graphName),cypherStmt=sql.Literal(cypher)))
+ except SyntaxError as cause:
+ conn.rollback()
+ raise cause
+ except Exception as cause:
+ conn.rollback()
+ raise SqlExecutionError("Execution ERR[" + str(cause) +"](" + preparedStmt +")", cause)
+
+ stmt = buildCypher(graphName, cypher, cols)
+
+ cursor = conn.cursor()
+ try:
+ cursor.execute(stmt)
return cursor
except SyntaxError as cause:
conn.rollback()
raise cause
except Exception as cause:
conn.rollback()
- raise SqlExcutionError("Excution ERR[" + str(cause) +"](" + stmt +")", cause)
+ raise SqlExecutionError("Execution ERR[" + str(cause) +"](" + stmt +")", cause)
def cypher(cursor:ext.cursor, graphName:str, cypherStmt:str, cols:list=None, params:tuple=None) -> ext.cursor :
- stmt = buildCypher(graphName, cypherStmt, cols)
- cursor.execute(stmt, params)
+ #clean up the string for mogrificiation
+ cypherStmt = cypherStmt.replace("\n", "")
+ cypherStmt = cypherStmt.replace("\t", "")
+ cypher = str(cursor.mogrify(cypherStmt, params))
+ cypher = cypher[2:len(cypher)-1]
+
+ preparedStmt = "SELECT * FROM age_prepare_cypher({graphName},{cypherStmt})"
+ cursor.execute(sql.SQL(preparedStmt).format(graphName=sql.Literal(graphName),cypherStmt=sql.Literal(cypher)))
+
+ stmt = buildCypher(graphName, cypher, cols)
+ cursor.execute(stmt)
# def execCypherWithReturn(conn:ext.connection, graphName:str, cypherStmt:str, columns:list=None , params:tuple=None) -> ext.cursor :
diff --git a/drivers/python/age/exceptions.py b/drivers/python/age/exceptions.py
index c023b5371..7bbb5b4be 100644
--- a/drivers/python/age/exceptions.py
+++ b/drivers/python/age/exceptions.py
@@ -52,7 +52,7 @@ class NoCursor(Exception):
def __repr__(self) :
return 'No Cursor'
-class SqlExcutionError(Exception):
+class SqlExecutionError(Exception):
def __init__(self, msg, cause):
self.msg = msg
self.cause = cause
From 69f9a91f3e547ac17b034fbc11c463e1b4fa6ac3 Mon Sep 17 00:00:00 2001
From: Luke Hinds <7058938+lukehinds@users.noreply.github.com>
Date: Mon, 28 Nov 2022 18:51:52 +0000
Subject: [PATCH 037/191] Implement CI testing for Golang Driver (#372)
This change will implement running of the go driver unit tests
upon every pull commit made to the `drivers/golang` code.
It uses the `paths` directive to ensure the github action workflow
only runs when the go driver code is changed.
The docker-compose file is required to instaniate a postgres
instance, needed for unit testing.
---
.github/workflows/go-driver.yml | 33 +++++++++++++++++++++++++++++++
drivers/golang/docker-compose.yml | 10 ++++++++++
2 files changed, 43 insertions(+)
create mode 100644 .github/workflows/go-driver.yml
create mode 100644 drivers/golang/docker-compose.yml
diff --git a/.github/workflows/go-driver.yml b/.github/workflows/go-driver.yml
new file mode 100644
index 000000000..52febad64
--- /dev/null
+++ b/.github/workflows/go-driver.yml
@@ -0,0 +1,33 @@
+name: Go Driver Tests
+
+on:
+ push:
+ branches: [ "master" ]
+ paths:
+ - 'drivers/golang'
+ pull_request:
+ branches: [ "master" ]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: drivers/golang/age/
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Run apache/age docker image
+ run: docker-compose up -d
+
+ - name: Set up Go
+ uses: actions/setup-go@v3
+ with:
+ go-version: 1.16
+
+ - name: Build
+ run: go build -v ./...
+
+ - name: Test
+ run: go test . -v
diff --git a/drivers/golang/docker-compose.yml b/drivers/golang/docker-compose.yml
new file mode 100644
index 000000000..668eb19d3
--- /dev/null
+++ b/drivers/golang/docker-compose.yml
@@ -0,0 +1,10 @@
+version: "3.3"
+services:
+ db:
+ image: apache/age
+ environment:
+ - POSTGRES_USER=postgres
+ - POSTGRES_PASSWORD=agens
+ - POSTGRES_DB=postgres
+ ports:
+ - 5432:5432
\ No newline at end of file
From 4d4173edfad73d4bc8eef545b87eeac535e9739f Mon Sep 17 00:00:00 2001
From: Eya Badal
Date: Mon, 28 Nov 2022 11:07:10 -0800
Subject: [PATCH 038/191] Update README.md
Added AGE logo and more AGE description.
---
README.md | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/README.md b/README.md
index 4223e4ce8..50c91f505 100644
--- a/README.md
+++ b/README.md
@@ -3,9 +3,27 @@
+
+
+
+
+
+
+
+
+ is the leading multi-model graph database
+
+
+
+Graph Processing & Analytics for Relational Databases
+
+
+
+
+
From bce56eda96e90486ef07cb48709aa9ce202faf55 Mon Sep 17 00:00:00 2001
From: eyab
Date: Mon, 28 Nov 2022 16:09:26 -0800
Subject: [PATCH 039/191] added new images for README
---
img/AGE.png | Bin 0 -> 21332 bytes
img/agce.gif | Bin 0 -> 1692177 bytes
img/age-01.png | Bin 0 -> 599716 bytes
img/age-02.png | Bin 0 -> 633093 bytes
img/age-03.png | Bin 0 -> 465950 bytes
img/apple.svg | 4 ++++
img/community.svg | 14 ++++++++++++++
img/contents.svg | 16 ++++++++++++++++
img/contributing.svg | 12 ++++++++++++
img/docker.svg | 1 +
img/documentation.svg | 7 +++++++
img/features.svg | 12 ++++++++++++
img/gettingstarted.svg | 9 +++++++++
img/installation.svg | 9 +++++++++
img/pg.svg | 1 +
img/tick.svg | 16 ++++++++++++++++
img/tux.svg | 1 +
img/visualization.svg | 8 ++++++++
18 files changed, 110 insertions(+)
create mode 100644 img/AGE.png
create mode 100644 img/agce.gif
create mode 100644 img/age-01.png
create mode 100644 img/age-02.png
create mode 100644 img/age-03.png
create mode 100644 img/apple.svg
create mode 100644 img/community.svg
create mode 100644 img/contents.svg
create mode 100644 img/contributing.svg
create mode 100644 img/docker.svg
create mode 100644 img/documentation.svg
create mode 100644 img/features.svg
create mode 100644 img/gettingstarted.svg
create mode 100644 img/installation.svg
create mode 100644 img/pg.svg
create mode 100644 img/tick.svg
create mode 100644 img/tux.svg
create mode 100644 img/visualization.svg
diff --git a/img/AGE.png b/img/AGE.png
new file mode 100644
index 0000000000000000000000000000000000000000..278d7baf607edaa72f0fd30827c94ead14a74ad8
GIT binary patch
literal 21332
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
literal 0
HcmV?d00001
diff --git a/img/agce.gif b/img/agce.gif
new file mode 100644
index 0000000000000000000000000000000000000000..85361758b84ab57df639219c47c940b86cb14f80
GIT binary patch
literal 1692177
zcmeEshf`C}7jH-fgn$J^x_}y_7<%Y|&;<;FhzLlRA{crRX#s*rvw~8jgGdQQIuZ!I
z_l|~M1izspEzjS~`!C)*`NF?hqgWpaJNvID*-1
z7}yP&_*~)FpTnE0Sub8;y>7`W@QhW!jrFGAMSjam{1%r4-LKqmzjD)qOZ^SE_9?H%
zBCpmOZ+okNO0bZ`Q(?(_!jH1W)Lq3r+wMq6-%)UstSXhYJeLm{S5VPXd^x1#X{{XS
zsN!I*8r+N44^p#tRP(XNc-dnDoiGs-8rpX?JS#Qu?%L*Wv?Fmk!Oc36INb=GZj^_f
zP3T<@hr2#bcV9j;Ff=xBNHcovZxZGEAi&+s+|tbM!=unrbKgMoqy?+^;3v+BHhzJ2
zpXcn8q8u{f937mTzQj3u;9NhPJjufPHsU_-jAw+!sf5U-me3vg}tP2E*almW`Ap#D=I85DlRP|mX*}?m9(vtHH=ia
z=2rw1e*a!o)jCmA+gQ`nR{OcKc3{2s=UJUUu`aB|Fe5^xVN{bcXGXVX||7Y(!aGjSlcs19vs>_8y*=Q
z{qb{jc4h3JEU%Ae^-WC9PEJlvO;1hzTAZmFotd4R-TyQ9bMEKj%CBGZzbLQ
zqod>FzvpMi$Hyn9CnslTr#pwIC#R=p|8VvXXNP-d$A@PpM`!<(|5N|d{2M(xJO79C
z^S$%))1&j#e^3AG(P*^mvwtfr(m5<~|80IvT9Lm2d}-JpJX{7M;XQGJ#c4FIA^3tL-(1l*?>qS$0PPKPHA-w>+oo-IcSi
z7G34JJxNHr?o{21yuK8a*T!sDMgG7SG@g-1ud-k$Tj$+v%kIjrBl$`*x@mgf3&*~h
zRk+M`fB!a7YE6#exm#5`uE|T{2hizq>KlQ(gM2DHL>pSHFfh
zPmaDUV%1wyw%CyZ`V9YP^2`?5nT-?-vPwfzPm^W_$6+R6uS=gZ%D`!_6uDd1>AL@5u_+U;T~shyPK?
zuW5Yuel+~uT^srKWZ*~R$su*I=gYmOAOHN)Zcmc$%B}h3k2#z8mZLiR%2f!C?Ql_mtL;uC5jqX+330+tQ(3
z1b`VwU49qWtQ7t(X*k&UT}pzAY!X}9T{p`*Z+_b>@0I<%Suv<3vQ;^1>bmuP(y^xq
z1VLaozV7HuY}AwtJKsu&JAM08w^{Z3Pd&9=%=@GF9*TV*
zGkH?%Gj3tGwLkvEkNIHIHi_q8%0Amoc0eG1^I!(oA$B+`Jaa6ju;b)oM>^}Ix!{09
zHvu(37UUorL@yMN21QBrqZeZN`bmqg6;czuX@8V^KaR!8Dy7^`E#aP$Qpzb*KN+VfA056){ZnvQ0|6~2P(@$bfg
z^sGSb_u-cjN;Ztf{tSli7b20dnM_Z0$eh#EC{%AI+^>_&drT#unX*`E1f5m^&h2O&
z{j5t#ovk+&wqMX$Lh>rq_TvU`ni1PPTC58j95Zq
zK|IH=vbk4vIuxe2Sg%_SRU%(Mbf=^5VK#rnpJ_CTdn-39aJF7a|
zVOK)|AUc0Kpea?IRot3Tq)q}_(xVk%x}oqZg52YjcGEG~JYqH-{<#5@1)+=lsK-DF>hZ*|YypTNU<3UjG{$K9VhpAQ?>
z3`zqhdVU@h9#&GR1T`@(wdSr^I;+G8HhO?XKkp@;tpagVAp5uQ+}HLIUM>%81|tQ)
zYJd|MA>Iwml<=9P}}utGvi*!W?dX7<*RsfFR<4iwIR=68wTig
zLck>G9&5CaAuAyH?7jP~%h$kgrX_)itwgPt#@~RxaMmiZ@Yy;5R}m}Y1>E-wzW!*_
z;z7DQC~JVPQ(o_&yn028V5``E6snN!f}wO4?OMudN7IV_s>00Qe*gZ8^uyj&m6N{%
zq43HKn8BJl*YRMqVP)3U;x*1GyYdf>qF``nWQe~Y?>VJ|oj0@ue{G5`)OADf;lcVn
zzk`bVQ;y|-dfTTz$U=yhhpKz%`JXlw%HF~YZO6JB{dBi65
zZ8|EROqtJH2d#>%1FSRosFNM4EAuQ?n`i)-qr9vp62LgnoieE|nNlt}H!}RSEBj!*
zr{;#~Tc2;)vm0v=3mozd^kELfI$tqP@tK!~9rsVHN9bG+HA?Yxo;9JS^_Jmw%;VDq
z4nI_oU*O>JO>4UMe>zk*_+cUMhxmeNgHF{MQ{Ub~(eP7#0yDLB$9b$SbK9xIIbYHG
z&Osk(MfB0&&|ibs4Ax|Uy9$r)9rs`CG3NS-C~L_y*e`s1wm}Id722@OlP?)~k4e7=)B(6Az>YSz^~@yX(9=mhP06u)jQ*GhpQD<|6J
zN$o7l9zt8`V?g|;JAk=Sg&ZZx_KDDWymiRsR4U;P3V7woQTx5~ZNk9NyFBk=!t>u2
z^7qa*9@CB|2Ywt>51jq^{h*xtzppfE{rP_Q8U4!qnzjHG5JtHia^%v4SLtoe4tc-0
zwH@4aemD>^@5lb8;omO4Dp7btlm{aE(ZuxW2UA12XAHND&8~)BLWN!Fun)?6
z8{8I3CkY~WJ(b*8xl<$=IJu8jXHtgIGc&Jh0Sm8a#yu*F?YJbE5;?=6a{(JZlJ#OY6mR(^Dq_zV
zjI~iq1OWK~NB~IDnocPyoYMYe>59MV1IG)9Sh87cH!2bQDDiS~1a#lyI+$*JoF(ai
zr4ImRVs$rb6?^AM7lV!otCnP<0D=c0VB+iIRZ)zOgoD4I>5zNDz9#z6&cFeN1kf=6
zz@}7_TL4f24FF=HK&+?$c54U{=U%;lZ;_2;
zuIx0assgD60Qddr*eL*ZqBWB0QSJNaqDcyXAwqj9g|jY`TjAOP34W5uVu{1PH%V1X
z!ro*E?FnFWa$=cY()ryPuhx>dIBLXm!&J&8yLQLJ%ZzDZ&^uuAejgiiP))Q}V2;k`
z9KE_6{plRT;~d`=KAbdj5;AMA?NQK#SSgC9Sm3p)-wVuytQ$w#4XM21hlV#Ib?1=G
z(}~Q})I1!cPwXnoIyT=oFyH@keqddG&~$$AaegRgLAXLegn2w_S;2sgY@4?45pmfW{lq+7rK~c
z;+y}9)JMBcy2vRq;*xWiS_hdrGME+=N;(Fa8qrMiNal^YlFjLoKgT6h&e9!)(mnIi
zgTT_G&!xw8rKi)S=f|Z0E+SBo2!2e2z97=45E;3MR2)Mcma!G>Hs1MYWbccJ3!7Yv
ziw{uUW7l3S&l_P3V9uc9!Is~AQI1F{zg=H0I#VuwQjX-RkW{RYeq13-dr^T(sZgk|
zP@1VwIjKN%RjMmiYCNvgN+}nbp%a<`2x!`8A6Fg+xM=wZicPyHcH-;9U4Lffigk!x
z|NGsVtIAff%KmYcp{&tAl2$gHNiR
zPxQ@Bih`4}#v`6)Tw_5h)CkGT*>}B?vU`)DSo`*I?VA_1i7B=3U)QF5tNr-9Hua=7
zO|dTPab4z%x{Q>%oY!^VzSZUZt}8mJD^{#8e_UVoqMn#iU-`Pe{#$+3@A`(5`bNcu
zmd6dvFB(WG4Xs?Y>Pg-auHoj#U$}*eAE*__o-|CRG)~ty&dxOcJZT)H+4`l|vQ6dG
zxc`l!r2N>Z|FJpqmgfSUwVB7rSP&>#|h
zDv7aybRvA4<8lU_!0S_MDEd+Ydr&h+YBOg;GxuyW?`bnXcZ+~hi;zXj&7c-UYRm10
z7SY)j@zWM0H(64NENwxS4I-mb$w(%;?nIVdEX&o$Y?dZ0`&5=OVqXEe
z`AlYR7uq@#(M(Nfrd2(rpxKV#(~eN?&Tys92#e0Bpw8&j&KO!lXY6ce{Ap(*ch_5`
zuJ;yQAA`D*Q@cJlbfwOArJr_XaCg;(cGL|L3WslFPFY5%7nZFF|6R!^a`%)g^;BB)
zR0Z|ar1sP`^fb)&{5b6)ard?;^|o5{wg>fgruKF>^!Cp7_Mi3+a`$x>`HzKOcuC(3
zC}*Rgnd^}7X=E&gyMIBcf61bMC8&QbwSS|be{;6~&uKrEdtgUtV9#RUAZXwyb>O&R
z;BgNnC1rIXM?OfLzk3?u2>GSzZ~L78{%vn;+`Ag
zJsaZZ85U3;7P1_^`EnSMHhjBrShRT9)wJ6?uMfP{M@O?9k$pLWN*hsV98sDZQ8^ny
z^Ngx1kIrv0gNvU%@)#V;xWG6!YEN#rZam-?C=*}F0CNQFZ
z)~8`P?)Y-tDQ(=Pas1ibxck}o!Lw0_8+)PBIPT?yf7(P~<3!NhMDW>!EVGZc+aN7`
z&|q#XI&CthaWZypGX88bk!R}d+2|9V5p$!7W%PJx<5cS0RQlOe2G4Ys<$!0Jmbby+
zC*|qF#_6KD>5{W)V%&&l+^AjH)MuWVnzWg^#+in>nH1%jr)7P<%Fmwb*;Xsx%K}HV
zF-LS2&pI#9d}Gefp+}QtXc6N$(Hiu)L%~zh?HMw^~7T$j5J)H>u
zSu|Cra1S^A&%Mn(SAcH;er8$1*F)hQ4$RpX^MMAjYi_ex0*g@I&uN?Bqd7CZ+P3MMva7m&}BN(S)?}y@d>>F(ka3%Jehz-$_Py
zEfJoY!L*EJS+Hh4^kzcHz&GRINrOx@BC|p;%W?)&5|YBzKSds7BqEs)SC^LgITr^l
z4q1H`Nld+=_w()HCWXJovCPMA@Jb59TE_g{9}`mmreYEkGI#}EvS@4t&x+6+M=q}o
zG96gclPHu$Eb{`EWhQjRxql{Ll6jk!xEAwc?bT1_0RMpn%Z1t0=^cA!8V>T1%Hl9c
z{};>H$j@0BdO?4XafQOrYYjicF+8+h>c=v6urljY>4_xfjX|bVES#7DUO}%#!Y7tV
z;D=P`D2}O)2=$?|6o+0QW8r-ih7ly3Mua`L4rv@*sr|E*Ey0vN$P$^jK05e2SQ*|!
z1wG`4SGSjHRBKSzzs3jk)K
zHN!`K#*a8gh6HLBj&X31`2zF0cBA_vkB3^l-BTI&XRyp0Na%Gmqc@J;hXmH6(kGz*
zC;%>41EBZO7ic(!`!p&`T?Y7J1}F#zQRRnNTVGIOfu{3AmJ^x9eA2Z-_H@4N={4=?
z|JpO8?P2-$??2zu)cvN9V^~Q1E%%GwCj)%*7o$=Lq#Nn!%DSrxfGQ;J+|K~}3^J0=
zp#&_HMh!B4a_Ad!=>O#~Fyyf1SrEHWU)rWKHY=qlUOP;{#na-Mgn_NK@s<0e=|1DsnF*XmiAys5^lXUgkF_$
zLLvPI((aA&7|u48fP;|G4i)Hg5|~1~pihMAg`UMBJ)h%F$L>HP_?QPtV10f_5Eb+s
zdjL?2n?Z*|n0YmlvGeLt%seU)97YQoQHxA2T-BSfcM?hO6OD&&^SzUNcrIg`wPbKj
z_#tPQUCV!rX?&6Ii|CA35;bF#U!H?GP1dmZpV6F~?C?)_Kx|A(k(}S1MeXNfd6!q*
zK9_`SH3UPrOp7K$9``s4>hyiNVwi-Y8rgn(bMa=J|5?HA;+ywce|d$Cv{$?3g}rk|
zAGL#H-!=y2#A04Q*s<^a5N4}19($|VLl3q5SgJksl7hZ@K<`QPt6L%7q@{P|t5XRW
zRhnMFebME?UI`l59Lj5PMD=LNv*bY;P9;40t0R0ZAo}WM1--o$Calavi~hUWpXXVEGkMSfi0A(+v_a5_5C4zWSzy6d#J
z+QA%N*9=Q)!r#j&bv=!h=_a+6N&4Enf_qhdEo)cQzVY3}_};z*m$^OGs{E3dSrWU6t!lU{Oz(13EB
zms1v92e*rg*y`tJ{eNXSUz9r&EnnYpXUR6t+P-P&&KbMO>Do(o>vPC~zgue1*UTs$
zu8v$N_w(UbdEDOMKkRmCekDv|Zcb_sB_HO7$*3Z3UW4(P?H2Lms62CX=Y?l6tQgp9f&8v5>|F2!~dE?>A9jtTEIZDmys-g;k~}}h^{RPPy$aO&enGYVdC#2=C9giY)zo$mmrq)$b+u)AT3$-d
z8a%F*neENpBKK0v(>}?sHtPnlqSK+`_}_UibyvY&!k&b89&{;DW89emt6%II{hETq%jHx<
z&bWq;YVfTrpWhA-DSz=82wrRXb$+FWbVBDQiBZ%v{<9dStKR~;r>prO+3(8N2Lm*G
zn*uvX6V*b=xM#&O6eVM-tY5oECI`+DTgvv-CM!8EA6rRYn^uOp2>di92l9^%bsEJpC@Roj{N^sD)@^d%Aq!uNYeRne&pT6w8&UaCcM#xi-G(}N!zicJCVb6%
zOc?)G?*U`<2L=yTneHK$farMl#Wii!N~1V8p`4F9YufVCx9U8sy}64o7u>nZ$wF{{
z&vm>BTc_w=-23_;f7EO3YQ5>|VDDb8nNFzE!9^DTz0rJChzmAS!6Xallb`#oK=xPy
z{=C;a^$8wfZjm&Ux8nZQ$Zl2WqDyTSlFjQ*pQVvxhe=udyQ0p8+d(V3SE`2coyt60
zUV3J#2|)`opfYzwrVbM9PU+wwkj*DktY#=;*yWYv4(U20qwr_hoX1R&G;VrDvli3w
z^2oE_eDT{XURD}hH2$b3kQa}JKbpd0=sQ#jnkPSGvEp|09>x`3Ou#0GmFE_VI>1B-
zwj?s=d?@U8!ytG;b{^Dr{5Z&*T2~3my-#dvb|qcMGVdo}VtcIX
zP!inAs78=cE^>3jEiL_2s{0mg>n&)bDz2T_LkGw4FS7Zr0mV*f9zAjfc6@C}JdeH+
z)61Z{odd_)3A%X>Vz5WtZCG%?Z@NM&EoL5HBwjF(DgO4m-ad<9uhjWeW|LDjrpyeO
zX5)d0%F{pJmnk=vdTlNom4JFYwJJX}LEwQXAbf`iMSa{3W6&WDC$)R$$uv3}`UQ^s
z%Q-219!k(Ag>f&?OQt?Zb22UE*?rUs&XE3=E+^4u|3hueLtz9`4OJy4(d1wE{A-e$@ZPhp>jSBn=a
z#M@=e%>6R0UTYotq$He8-rbP4;fcaEjT8IY@bW9Ij#}fAJRF`LbE2w55IxuPmo3a;Izm7
z*sz3<{^PSvj+B4Pm|$ytoohb{c2x2C5U=XTLGneQ{j60(SYLaS2;N9oX$3wQneTE0
zLt!I=(S-eXs?>_lV0}W|3FFZ&V^AU?@vc=)g*e
zFkh4+@CtgvneYOq;Oj*2N2uCTp;fQzZ`ijOrYQO+M|q=TLnvslH0gy7iLbXsM2O_u
zsTRWDOw4ErEFc6GK;;227bn=yV*)LJ+AK2L!ZO=(WQVrxQWM+*-OnKOFQhif%LduW
zEh_%-`WAWZBH<}(s~WhmKtfIvtTN37Lp#f9gO%qmr@0kC^td(5=+jcVHMI4A*w=IG
z2Nk-~6Fj0}A0!B_HP8>WP=7SaBP(t76)%fP!{a?z@mFZcHC1nhBqJm~WDW6xNGMr@
z`toaeTf=;CgcN6(J5DLY3@QO2T*-*9P%-!cJkXYFfTnEeMlj
zr6NoMo&2mIl|84kEmeL~<7%#RB7)zHnaBn+50
z{j)(H0duM8vZWFN5M5t{IwL9x9#?eQPPB@f{~6hlG9SeiO9-amjgpnxH^`nf)z!^R
zi;BvVOyM3@~m2=UJ=2QSJh7l{?V{t-6n4wVb551Gqu|n
z4f7%)aIAP+ojTT7K6_TsoxiZhiFmhH>QZLV5Hw*!0|sH@fXtHqi%<>TZu!q!cW;ys
zgzGw5M|ra19Xbgi=!ntUT+XkZun#G(xl=hm6$Cqh^_+Vuvh`f{Y67UJ-BZ}M&_LCD
zkdP2eJ-O>fs-oU*Wvy$t-n&o+RJhONRhu4yZz9YKp_M*Ea+HSIpBj1-c|R=O%b%?Y
zL4r(l>U<~bRgXX&Jp?Bt$fp1rD-y+})EP+6!8(TCUqoNGRNcq0N+Bc5-r=!Wf(KQB
zrd$MdGb^esx=UoxP|t}p^;cGmlT}ofYw~8**u1M02|+WDswvAU%czung8m4SRVs#z
zT8xfJjp{@~EGLbvLu1hm#!u}`G$AJc;wKzCy~=E(+I=JK+z*^iOeTy>41B8eMaTMF
zA(L-4o921wb9H9-NG}Er(nd6`yz?Bx%0sX)FAj(q7AxIQZyO4d?u}~d)=lzKFt+Y@
z?j&gW_xqbQxb4-%V+cp`U@s0uMJg!1LCS{$mcoLAkMIf&Q`&NvzRblft=eIf_u_RnIb6$$%2)_+EK41E6mghO(YQIRWpcC^BNj8Iu8@
zKLFBm>mAL=sTziE=m;zV0^CQv$W^!MR0Q($V*s#-eV9>aO)vr~Z;cV_>;iuUc@;$Y
z7J!QP6f}=e+8mMEB*ok@&X@bBdq_}lsHC(gR16FAr4p)52>*TRQLVhI-xjGr1v@y&
zTJlG^RuIjR7QYsa8DBJ-^FwV>a^x{c8w6?{I_sJZ0V*n+qkA=!8?7=%$;*)nH4&Bo
zXy1*f(Ol)OIg7CyPzCx(uiue((^`k#Sy-SftSMkKDwsw&h1eo2Y>yz?%kSbnMk%?F
zzBY(C{m*|G0zv0*SUnX)>TN|n8Ju-S`4mIw@>KY9*qJn;_LAVg%fvot46qqnNQfeRM;1glJs}M&vm-kQiN(5O@
zVLpgRRdht*Gf3fkkRbrZWf5gWi3o`nb?AhNqN9qoREZcscu9PhIivWEYAK*D1kh5J0;_fr%Xv9+H5@laRpZ1-U;!6C_AF(#D5MP~eF2
zi0(>~DPhR~EPMu_{f8DW*{^s`shtBftcDnL@C+o7o-5E~60qcHl$u-bj3AV4p&VEr
zO1_MIgnew3h_^X{85Q6k`bS>=H}_OL21^J*fi-dRC{}ARG!%ua#Nr}qK73~=(Zdx$
zeJNtWX3&Q>VJcMoOA=a?Ncb0)F)oODgq-+iqysv_Lk4eDKu|mf(dBg<4b8}I*(#JZ
z%-_AFzYGyw3ztJj*f~WcEJHAf_`6h)gg@BA=>b;0)$}D;KKZ>RA|iG%B2*SsBNJg0
z`dEb+IZ_ejuG331-3L3mL!>w!i&62|BQt9RL@E)ll?;(bg3S0Ki9ci$#lxo}Bdu5o
z_1ul22N8E!K`J<~7y_t$r0{kn!lrZ8T+6^-kY_X%qO!K4qJY%V*#pqMh!A
zf)t36kNiRM1!^jZcnk?LI{$S3b*l?X&Vmx5;2&YcidUF~h#i3{S3ozc@Y>0cr^a6j
zK3y)8W$Oscy|$gzH1(q`{N3#hmH!s|EE^>JY6(~>C}XE`Adq2Nt__4@(Ai%6$V4Hrm^kgEc!o4JW1VTweMRaLk~v@QG$CK8JGB5jES
zCTQDCwQ=#VfWh^nG7!Jc)~IR3m^e^T2P~RcE*|P8OTP^*vmdzhd?QRNS$mMO$xwum}LC9*Uo2hs7pIiiXP19)MpGpNaD8NK8iBhDstXQ#KA@FG3%F
z-C2;RQxLnfh)Xsbk0!(dd4`x?Y5PjVqW>DLmNdWU(^vnn=*;W1<^c{p}wJ@oyz;d8S-L8kb1P6Dg~y>
zIx*>pSN%85=y5YMJ73D$`FU&zGQvoQFf6|+ue-_#+Zz9Ta5l5{{8MU47FY@gI1g?z=DWV3jZBL?TggJI8_st`Z>SvYw#wD}$7DTSUC)Q4BijQfvM1msjA(4&52d@OAB*{a#dg7X531;9IB3
z6_>ghuRE^cH6_}duD=Q~y@HPv{_o5lv>Bs<|tk8Y%ZOc;|=xxRrTLN1M
zkq*W^fA)q4WqSpa;Cp`2sa<5n$)jI&YLw1UW?s6DJ5h~MUg(V`XMoUKUCDd{`9^;B
zC23lv9LtB@rECbQGuh*bgym$WrUZ@4<32z8dGcZccdI`pUht9*Qth&HXCl)D59y&+
zScvp|Tbh5j0A9pE9~}mcm=<136ZfZ!xpWJ#9md30v!llDkrTECyWB7ZR_ejpksq
zC0Ja-Vv~fYx3AyUWW5i5q-pQfB9eJ@{rA^B(tO9qbL+CZEe|5oiwy5HGhJ(ivWKwtvdXb&
z3dQnSyM!4%a%c{V6g2m?iDagQkc`0GmZ=uExp|TohVK|oy)I=BiLgy%b_|kFzUJ95
zT*@O$x~Q+QEBo+*XkA83+Y?Q;%m)&B5%&xE@^rRicpSaMAl$c3jUpr~QU@dNd&VtZ
z5i7oexi5kIFJ=3Oa<&uRhR7}0U$4=!WkAt(!NWyWd$U?^4k3VDEwg;Kk}fl`YCL*UL&N|dq;xeuT3po=2oCZhqtL#gko3(;;D8Cf`1=?
z99?6IP;m4bQVu$L1f
z^MqLG3=PQKJ_s$<?)
z?t1kuz>=)h1%s{WZ#Jkg#?`d2@owWWrx|cne=@Hu`4yk&V!ry8+4_CC)E9~0iyRv}
zR11e*drH&VxGIOVWG2Z!JU6p>Gu2upN~&2xP6c`PDZf?K=NY8V$eIiOY4Z2?6MpQ-
z@VquyR9(9GIi%8&clEEf(xfqS2sNAc`{N>}U86J%zq;}WiNMEe!*Vwr#*@0Q?IFH5
z87x{QlXudg(r-f!$%x`Rk0z*gHh{@_s$lXTNkh9CSjqE
z=jxbi4ze%CuW2g{OYrz4+f~2Tgt-a!u>AU%#C-xN;f^TFlZ8k`M=;uDs!(srUEDCx
zyJGTE7)d8HY0F-RfJj^_1DuT?Mt}0WoJuijpZtsAJ?rcUoH$_#A=)*b6E_3Q3V!Y@
zd@QByQDFkU^ETf0$x&HHZYr}$EH23yxRa=%;xmziwgpo%X?ZKkm~2CxyGV@oA4275
zFb0}nxJF-k%Ht&Bs>LR(E(lNFiKdgWaFTZ+CPT!tpdZ3z9JqU9qs22WIa)e>Vz#A({)pd&33Sr>Xm6@^Hhzj&T*>L{=tLp1A6fq
zhj*TosQ)}e`78V0-SEd2tW+3XN)g8%Zt!qoY%L1r}k
z_5RF=N1cxH-R>o*!pzqpso&=gSzU4GYRuyd4BRUP(W>np2|=BLGHXPF@?aR;uLeU|
zJ0rmVQei=67_MNXEQNgaav+K+;x_Flc?=VeFIPMo8|t4K2o)^iSqO&
z&r<-fupRAg-&mzc;Z0O?p9F5ZPLStMh8tFMELW-#rw@!GerilV
zh>+viaDu5I6!(0Y8Yv3Z9ja7`gy4MVAN$IR)A&y7le{?tW*x992^*zuU$*r)0(ou3
zh~Pl-T^hbvI?$++R&qg!vO?4zfCRJ5WG((a7?f8$;`~`T-poCw^Q?ll@}{LDAKM_j
zS?3Sp+~eA=ipRB65xHM~A-kr+&paIRl^w3zwZHsReCPa5brxF%y~<$|
z%lR)p8glQLLP-v;F-59fQ*~-%`@F#*{2s^@XBKs7Fzkv&KZi~;$7r+aDM0-F%)*aRalN`J>fM=2R2pLmHril1KmBOi%Q#U4Lerp+@5a{QY{|09Dw
zn4gI^Rg6_fe-J9u3A$|^CS?(=MB-=tk-_vMBl3)Vqj$h}sQtGX?fPYL-FN&^Yfg9u
z9-wb5h6!pz-<<*PYE~(GHO=s5u6S-PcW&AN4K8Ytey@IA0}wox#G#d(-^&tUbDak`
z50g!&&%(&R$1Hxu$dbb3iwJT17@qM=-VF=(&zQG=o8QV7_OB*quC#3=tzAO;(-6%Vgl?5CSsq@t?4mtynUzBS`VI
zb6B~xPM7jr-_iU`UEUsJSIH)4&5z;-#zfFyev@iW@s56^FkveBnb1|YXBgQdyqGtP
zD>2V0{H9xfo?G`#ZvSRMe}2y!6SgK8rBHlN4!vKZqeuUW2k$M<;hQnk7&$Y%(Qvz)
z{w*KVd>^Y-J9~`$Y0EF638({J#k<38*wHiosS`X(cn~8Sfl+F0zBrc_-1ammd_JiE
zR?yF<@%cR7YBE%MrCx)7UlM6p5IMa<
zU;lSQn_;yNM~_eE}tx{6-GAg_=stz4_!MJiQXE3zQp+3^xit`*@J
z*;W=Ae-Mi3LOkCnEu^_LKwTOyxrl^f>WnZl(RkG9MxO8OZ^yUutJE`iLGR>02?;oe
zi4{T2K{6Uet%jm)de1t{GGtAP+D(f-r{_NO(QkJz>Wu&Q*DIc^a;xiY(Yr_daydmk
z@tfU}u=$3~*3X;8#{%?^GGTui1nHP%kfw!Jy(i&3&J96Hx46vhJKJrjT4b
zJzhNB4pP9nDaDeL)xZ8a7o*(I_dO4nM0~X~!Ms%)kIYHB7k+i@zV75O-r*}~tm$EfIQD-U8+DIk?r&|C?t
zO5U4=sN$df?Ejq$C7g|qxooMRKQ3+*|K}(+_g3uZwAhEy2l5(j6m@8HjMKhj>4E#5
zL*LRv&r*B+d=wK#`2CZ=RV{z>O8=JJIW8|fuDWyjqxAG6-!c0iw?W~NW(*R=tBNr65`!bG?nEgpJS
zj(xlzZJJ8Yos4A6)?xfYWUQdRYQS9}6PfCfF&5$SCeGIJV6Gf0(;<@i1j*8Yj6ndE
zq#p?d(10;S;-ymLF@WA${(c?kO_FkI=&%Gxa(x4B@59^9&&JR55bJ;vk?{Kn6UcXv?oo}c1
zCDQi|>%1989)V}eP?r`ZOI`s^;B<(B}>1l6xQ#k)E|&G
zn5Z-$d%s2Jt$W*Tpx70@Nn>H(u@i+pnwDM~42)f%W-%FKx$nlRKJDMh_hG!v5SVcY
z_xRkPV3X`^9hpZ1@h10t6zMG$
zTQRCiz+0(!LAcD@_m$RHt8DnH-nRb2lzjUnd+;g!>n9hx%L
z7NV^}f}fsZBB&U*e8T}|?E&4{&cCyRxtQ@{)))TjI`z`F9Y3FXRrx-b(-V=s9Pc9E
zIsQyc-0en`&xH?$Pkc9)g#D_kUwo5NdRl$u81teNL^05wm*G|1I|#VQ9eh{2gE?Iu
z{qfe3+Ost7gtIq`diZM@7>ltm$=-OS$yE7S(7;Pm1$yp2XYRgNNgdmVw#%)yTa&h*
z!nu=QIkm}tE=czGs9AlPN-D#zEB{>Aef3`~YLT-~m4s1I3Ky)zSU>l>pZ24UhOS-!xrMpv7KmkGN5|D0~Z=UB_Ywfl6
zyY~0){eIv3FaN-g`@FB~yw5m}3r{K}YT}&u^M^8A
z^RbbKB3$&sR#eJS2(@Vu<1WbM;F74}>MkQ0I%h0O8EZIXcIVHQ0N}b<+xJfk
zc*=hpXnxlx+J;TL-%BXLiRmsB_FeaD{{YMm?UY;BK2YI3pN;cvFW;U!KT{7}LdHI#
zJL9{o=nU{lAi8^-l4;n5v8pcYLAo2==O}V?1_RRaH(z|kl_Ujw-iXko!f4(p(NO<}
zR%5qTiwFp(*N|GGL*#mKaijo(t+lXLK!y~5XuvNX1)3N~(Vw#S$HCJr`xic9O(CsK
zF=B}}W-G>H9hI(8ZJm!b6RXr|rK8}{{jwdjvauGj_rp@%Xe#BFTcvhHL4&^*a7ws!-MdrKEr}oeN}A
zcf5Y+kggULbSXPBNGx1B02|vQz3A5)u@E9=g@o#P4RB;!G4{z|z6wqVaE^
z#xIPVX$gbY8mt$J?oQTre!@h3?(YyL3fW19x1;c@-YQg~uz#|9=Sc)3j9(YW8PTiw
z6AX*Kz5K3AfK+1_SPl59HqEj`3i$o{v_V#mz8wQ0>&L2C$P#OV3iBU)czPWf?v7g%xkPme5wDIn9vhXNJcoA!
zcuO9Wv)cg^lY#j3kx#bhE*-4#Xw(vTJM$_drjEA0b!Nkq656AU!szyjc8W>VkB=0i
zSyVV!$hnVedI_FZ#}?o`4`(8*?iqC(tskbfngnT&9u5*H0;X|6u!T5u65lUC#b}$a
zWhTjubw~|GL7C;0Vtl$&L;x`tJ))OfTaA4n7^cz9@`*SCqRd@&zFQRP*0vjx>97kZ
zrp5HV#vV3)A}3GaQGU1<^O%uaAwq7HeKSVp)~GjFBb0z(kQ@|qK%~WPI(k^`jo+HD>B#PI}r*nlNV+!UPqA1kq_d<`nnvpf&SU;XyHFD*)T
zXgE?h?8fC?JnSV=H#q92v0XeGWR5X79_Fz1N_X(n#5(M*FU87EwTurS^SKNg%ywBn
z3IY4BGG}J{USAD@eK$*YvfbWZU}sv~&9h}|Fk0+_y)G-m$P|@-D$)u}mMUaI6Ob!x
znbTXT=h;hDRj$r+#qws()1BE#r@nGm`9(Q$NY-5mb&*G46f#E66a3mnoD4J
z17C(rFJclrpE(of>kqh`*T5Jraet8x&F33T)Xmo9X0)rJ%b5Hr(u6j#0Ss0d!j>!2
z>(6`XvzP!Fl#`hZXQv2la2rU&aELm!L!wrJHDkQ|
z@}JE%DRy0I$~!HxGQ21slxQ;tNyo=AylPWCyRDJu_OOq#doZUmZWOgn%tj?qXW19BPDB4+Z#>~r=f
zGao@S;sS8YNtY0+odkbtQ!HCJBC?#@(vcQD;bu2aw3oeKp5sL$1Tc3Lhtysygp#
zXJ6w%VmZNH%YE57X*v}$v*0#9SxMtnYxE
z@2yzrH@1Tjtc$DUwR3%V!Zj>#X`;vnQ=M|w@BJb&-afn$)Ed7kKT!#lIg;`l$>Rhh
zk}czUUT(f9(z1fudHl@$VK!_kEZ_^UBJQ>hU_99#Og}=zc*l;9+1iANolk+hPE8nH
zduwH{?YC}^^2co-ok}c{Y)f1OOrE5KK+2E0Ueq^_yW0$A+#LCY<6s|DC?0EZ2fk}1=;5@dAsUZ@Z0ho9?R8ezp$`l
zoiqa*D;xcKbw3)0lplatYGJnMd?Cpw+G=w0^AWCN*)D62Nlf}83p2>C&oI_{xt4a{
z<)U?k!Pb2u{m1V|PYA{IY`uTK`Wd7gDl#`_ix|9-7*Lq8AIY=}a(pmHm-ZO^)REBD{V8-ctm$+7_RhbS!}aR3k&n>c>XS{O#;dUa
ziq{`W4ps`L2PY7Wr`gD&7W?${A-#v2w~sS+RFPH6BP&j&Tp9k|S=X}>OLlkqvPoaf
zF9eH@Y$~^~*CF!I;fE29^;JuoTw7KP^Ng;|zOJbHXXee)k@{mUK?D^6}oTk
zPu5=QZTUX};FJ~t~!_KJu;
zI&axQSb{ylx|?j$ug9DY!I97Jjp)mW>dMyg@^UHYORfR2_vFuRH(3`)c<CL+i4Zs7vQU~@wEMEcdF5{Kul61j|egLRaP*Z#e^2%jAw_K6}00A``r4yX$
zyo>Tu3h%2F&9)!zWf!Qbi)_S?W<(12haZiW2AvcxvW3Ah(GNy0@oqmWmyC7aV|wl4naWc*3_7K(~-fU%ql}$;TkY#gO%s9}|Oh
z0Rd?4W}tSYKSUrOXw!`%AW#9UW+VE%#}AmaSl*cly_g1HzGHb)#cAp&Y*8z0eIne-
zk(Ps72RN*KJOZdX&U4XDq9tYxR}PE)K*yxW@v2r7{e+H009xn>EvrD6exm$34T0At+2Cs9@@(U{R-FJ;O`0`XNaxX2sFeM&u1^kg61aabu=P`S`axs*J=Pns$rH?6$%k)5SSQ!Ztfph~}!%5a^^
z*s02tpz5rX>U^E*(y8i-pxTC$+D@I?{;ArLp!%7U`c<9!-KjcINCVSZ1J_yI0zrKY
zEG{vk?XY%X+|^_j(t7Bu#Zj-tbEd^Fq%GvEEn2TFai%RT^jzNg`P2I6C^mwokdBVC
z&MZf<7AE-13Ms9T;_qBh>oZ+@Aw6ejJ@VwXAn}-`w!N{rr8mu+;{Ig08OB*tyk|u=T8q_54@(aD&kYVVeyXo1F%m{d1cm
zVcRnoTcm`G#ng!{P{a)eg7Oj_|^cM8uxL)t;u&p5eltS;XO?s{==)1J8v6zlfud
ztD|V6qr`S68!S
zBlinegos;^t6Tl~dF7c~oQQj(t9xprd*+3Eu82pWt4C?0$H&BHSHhn4uAa?}p6wT&
z-6CH7u3p29USk(tQzG88uHG8~ZlM?6DHLc$B(!b_XND=x!pAQAO$5weuv>#c}xNMyfT~
zT*YNKg*|kS=V*?1ri$km{UGH2L1aE&;_8F6Xo9?Z!qesi)vE-mqz^joi7%QHi<^Rs
zM3YS2lPsE(tgn*nMU$PIlf0Xfy|0oHqA5Y{DPheiQCBHo>cm9%)YRtG6^c}3u4r1J
zds=C8TE$gbjeCfpdwO$odi#}Et!PHSd&Y2c#@JQH6islad**y|=F(N>ifGn`dlvtk
zcji^rk!bdrXoy-!*4#$7b$GF<1XyXQXi$m3WDKVr`0
zhvo};%RIdv(J;H8M3tqGo8eSI~L5obU)3sZQtgnlFU5lJO
zirt~+_SeM-Xo>nap`ezMsOysUhT=qz($p5)ii^@*Xj!30S!qjI#dTQ?^kZR*u50th
z_Un(`(DHtd^5K^9!R!1fXvM5Y#e7S}Y)k11v~t6va;K$oA8K3=twNr8R9&@H-Cb7!
z#j26lmAI|dp4WLx&>9NQ8k*J`hSuOKVCh58PaHSz2yZ^|i`5Ew){3^KV_wxti`B_{
z);(>lQ@yFv6sy<*;1OG?tIfyBi35)+4@oRL)lGhw^&=hXWOu+No#G}
zlvsP|TvkYH`_fH&r*q+~XUEQ~(EOW@BeBji&(5pXPGD;XP`nG%s|&ZSkR-j8M7*2A
ztDA=2f90l|S-j_=R}Y7H5B_Zr|E(vTSFdPWuf%OH2c%cts}Fh7(4%_WrzzgA5srUsbQ;sn>u-+ko}$Kos?Wv)7<|+o1RDAVPd7XtB?}Z76E-t+e=XqStV0TTT1T
zP_Foh(0N;7+en4D@x13yz1L^Q>yh@`&)wpq{kKMAEu&+%qf_EzvtDEKZApeVV=Ln0
z8(!l(ZR7j5<458XI~kv^+9vGvCV&!?nBJ4P?URIelO#)%gI-g;i=7O2Q_K=y9(sS_
z@YcJ#{lahPOyfN*+CDAe-Ip#tBkw))v^@iMH)Gp2rsF;PqJ7rzZq_KXT)_LQg}1fl
z-B+`=8E5Zr?(N^CX}=*P=7PNEcA!&HcXM$P^NHT`sqOQ~jf~hZiG@P%h0@Ht+P1GX
z65s0^7Mk0?x8Hs5mRRifUL3xgF}Yiul31GcUYc)T!qxk}BJpFx`^Sz%!p`S!M-o5J
zynj+OE#75TUr8)u`Yhuz!jN~%B$6u>uf40ze{v99W4A}hI}lVxanyKZ>DVI;Y!=d)>%Wngl@dDpz@?6c+GvE?n<
zVK2G8(!Ao^v90q1yeP4g=(CgBv6Cs8gfF>U=(Fo-xLt9-TO+v`<@3v;W25%|SGVMz
zXy$fv#~%2{uYSq>S)cuw#rdWC{T0cB4WEOZ`^owH1LTq9@287mgM2R?#V&l2myLSo
ziN05q2B6yi#NVV
z4kJdk@!(t>wJI?mLxB3#nI&QB{e|_(afpy;B
zp$N=KBDfzKNC}R>Vhn}*<2@dN2Y@7TkVd{$s!B#dl&?dLf@y7rj6xWFaEwD)qLqxp
z9In<3!#OL5j3XX*;Fv@{8Q(Vc5dINr5)C~VGKrDA$9Wqo1A6*4PQh5-BwqRP@Y@e+
zlDMV`>WoiK6LnvQnI`GJ4vY(X?SuO+#W*^ZIhjmi5VhT1IgHxw@4z+7a2S7Tmg({%
z%q+{}VAw3%=N{KQ#~-9@o*T>VjuWe48-@=xQ+?<*R9gukzBIT(3g)p1`|rm>UatSxad6JYge
zdVA%wT|n9&$gZS_xN^QXhbS@-B{pAyB<8=QM+CcjKIE+R9(fspYlzl{Q#})
zsC{q#v9Og`?)iJQ;YZn#4kMgZqYj@RcM>>`KABK)925Q-={OGkJ?c0ii6n5Elp#`e
znoCZa2lTAS1
z*AmcO7ab;4U6)*bM!Ej*_&w(O(+5fDw(L)&=C%?Hj&@rO=N)%ji-v*R*W=Y6yM9lq
z8s(kd&mf%jk#!%b8gYz~_1G!!3_!N*tmTY*{Hp8(dG39hQ1jex_!;ea(DHlS^LGal
zb`g9KVy9FuYXVYBJYtzT>v~gKMd|1U^AlSsOGW9v_NWyAa-JZ
zwTVC3t=?iEqT9w?BVc|=K?I7^LmX}w_<%bXS3R_cyv#0$+bS3T4Nfohh+VK?c`l)C
zXfOS)U5NNzE|D)z9~0hDAl)7oRt!6+=_%;$B*GvA
zi>ec6fG^xW@-25h%|z&cAhOIp%E~IA?kCP5WW+w&sXU+Icj%z_E^6d$FCUCl8nnQ1
zi1okC69T!*bHz{46`Ig}r%&Pi<&Fu?nf^9BqS=D+=s0$UXkaN_{Sk+xy7EFE+py0DyAH{1dxgBdD3lDIV@l5H1FG5*C5$J>*6f}+
z#aUYM9vmOFlOn%E`c|T>&9&)fOR$&~uOjWj9W&Osi$y2G#+}L>Gxw~D#eU*WxQ#ew
zot76%{0^J&+I7sn-7AJ6aVP!ooN_R{gV`nKRjtjZ=~kHx7e`lA+?ancp0PaD{t_Jh
z!Sqx3!gc|Ztmyc=P$y&ot7-6)H2W_Jc}|6#)*R2??0!k