diff --git a/Makefile b/Makefile index 394951ca0..65e5d39c5 100644 --- a/Makefile +++ b/Makefile @@ -111,6 +111,7 @@ REGRESS = scan \ name_validation \ jsonb_operators \ list_comprehension \ + predicate_functions \ map_projection \ direct_field_access \ security diff --git a/regress/expected/predicate_functions.out b/regress/expected/predicate_functions.out new file mode 100644 index 000000000..47226453d --- /dev/null +++ b/regress/expected/predicate_functions.out @@ -0,0 +1,409 @@ +/* + * 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('predicate_functions'); +NOTICE: graph "predicate_functions" has been created + create_graph +-------------- + +(1 row) + +-- +-- all() predicate function +-- +-- all elements satisfy predicate -> true +SELECT * FROM cypher('predicate_functions', $$ + RETURN all(x IN [1, 2, 3] WHERE x > 0) +$$) AS (result agtype); + result +-------- + true +(1 row) + +-- not all elements satisfy predicate -> false +SELECT * FROM cypher('predicate_functions', $$ + RETURN all(x IN [1, 2, 3] WHERE x > 1) +$$) AS (result agtype); + result +-------- + false +(1 row) + +-- empty list -> true (vacuous truth) +SELECT * FROM cypher('predicate_functions', $$ + RETURN all(x IN [] WHERE x > 0) +$$) AS (result agtype); + result +-------- + true +(1 row) + +-- +-- any() predicate function +-- +-- at least one element satisfies -> true +SELECT * FROM cypher('predicate_functions', $$ + RETURN any(x IN [1, 2, 3] WHERE x > 2) +$$) AS (result agtype); + result +-------- + true +(1 row) + +-- no element satisfies -> false +SELECT * FROM cypher('predicate_functions', $$ + RETURN any(x IN [1, 2, 3] WHERE x > 5) +$$) AS (result agtype); + result +-------- + false +(1 row) + +-- empty list -> false +SELECT * FROM cypher('predicate_functions', $$ + RETURN any(x IN [] WHERE x > 0) +$$) AS (result agtype); + result +-------- + false +(1 row) + +-- +-- none() predicate function +-- +-- no element satisfies predicate -> true +SELECT * FROM cypher('predicate_functions', $$ + RETURN none(x IN [1, 2, 3] WHERE x > 5) +$$) AS (result agtype); + result +-------- + true +(1 row) + +-- at least one satisfies -> false +SELECT * FROM cypher('predicate_functions', $$ + RETURN none(x IN [1, 2, 3] WHERE x > 2) +$$) AS (result agtype); + result +-------- + false +(1 row) + +-- empty list -> true +SELECT * FROM cypher('predicate_functions', $$ + RETURN none(x IN [] WHERE x > 0) +$$) AS (result agtype); + result +-------- + true +(1 row) + +-- +-- single() predicate function +-- +-- exactly one element satisfies -> true +SELECT * FROM cypher('predicate_functions', $$ + RETURN single(x IN [1, 2, 3] WHERE x > 2) +$$) AS (result agtype); + result +-------- + true +(1 row) + +-- more than one satisfies -> false +SELECT * FROM cypher('predicate_functions', $$ + RETURN single(x IN [1, 2, 3] WHERE x > 1) +$$) AS (result agtype); + result +-------- + false +(1 row) + +-- none satisfies -> false +SELECT * FROM cypher('predicate_functions', $$ + RETURN single(x IN [1, 2, 3] WHERE x > 5) +$$) AS (result agtype); + result +-------- + false +(1 row) + +-- empty list -> false +SELECT * FROM cypher('predicate_functions', $$ + RETURN single(x IN [] WHERE x > 0) +$$) AS (result agtype); + result +-------- + false +(1 row) + +-- +-- NULL list input: all four return null +-- (NULL-list guard in the grammar produces CASE WHEN expr IS NULL +-- THEN NULL ELSE END) +-- +SELECT * FROM cypher('predicate_functions', $$ + RETURN all(x IN null WHERE x > 0) +$$) AS (result agtype); + result +-------- + +(1 row) + +SELECT * FROM cypher('predicate_functions', $$ + RETURN any(x IN null WHERE x > 0) +$$) AS (result agtype); + result +-------- + +(1 row) + +SELECT * FROM cypher('predicate_functions', $$ + RETURN none(x IN null WHERE x > 0) +$$) AS (result agtype); + result +-------- + +(1 row) + +SELECT * FROM cypher('predicate_functions', $$ + RETURN single(x IN null WHERE x > 0) +$$) AS (result agtype); + result +-------- + +(1 row) + +-- +-- NULL predicate results: three-valued logic +-- +-- Note: In AGE's agtype, null is a first-class value. The comparison +-- agtype_null > agtype_integer evaluates to true (not SQL NULL). +-- Three-valued logic only applies when the predicate itself is a +-- literal null constant, which becomes SQL NULL after coercion. +-- agtype null in list: null > 0 = true in AGE, so any() = true +SELECT * FROM cypher('predicate_functions', $$ + RETURN any(x IN [null] WHERE x > 0) +$$) AS (result agtype); + result +-------- + true +(1 row) + +-- agtype null + real values: all comparisons are true +SELECT * FROM cypher('predicate_functions', $$ + RETURN any(x IN [null, 1, 2] WHERE x > 0) +$$) AS (result agtype); + result +-------- + true +(1 row) + +-- literal null predicate: pred = SQL NULL -> three-valued logic +-- all([1] WHERE null) = null (unknown) +SELECT * FROM cypher('predicate_functions', $$ + RETURN all(x IN [1] WHERE null) +$$) AS (result agtype); + result +-------- + +(1 row) + +-- agtype null in list: null > 0 = true in AGE, so all() = true +SELECT * FROM cypher('predicate_functions', $$ + RETURN all(x IN [1, null, 2] WHERE x > 0) +$$) AS (result agtype); + result +-------- + true +(1 row) + +-- -1 > 0 = false, so all() = false +SELECT * FROM cypher('predicate_functions', $$ + RETURN all(x IN [1, null, -1] WHERE x > 0) +$$) AS (result agtype); + result +-------- + false +(1 row) + +-- agtype null > 0 = true in AGE, so none() = false +SELECT * FROM cypher('predicate_functions', $$ + RETURN none(x IN [null] WHERE x > 0) +$$) AS (result agtype); + result +-------- + false +(1 row) + +-- 5 > 0 = true, so none() = false +SELECT * FROM cypher('predicate_functions', $$ + RETURN none(x IN [null, 5] WHERE x > 0) +$$) AS (result agtype); + result +-------- + false +(1 row) + +-- agtype null > 0 = true AND 5 > 0 = true: 2 matches, single = false +SELECT * FROM cypher('predicate_functions', $$ + RETURN single(x IN [null, 5] WHERE x > 0) +$$) AS (result agtype); + result +-------- + false +(1 row) + +-- single() with null list: NULL (same as other predicate functions) +SELECT * FROM cypher('predicate_functions', $$ + RETURN single(x IN null WHERE x > 0) +$$) AS (result agtype); + result +-------- + +(1 row) + +-- +-- Integration with graph data +-- +SELECT * FROM cypher('predicate_functions', $$ + CREATE ({name: 'even', vals: [2, 4, 6, 8]}) +$$) AS (result agtype); + result +-------- +(0 rows) + +SELECT * FROM cypher('predicate_functions', $$ + CREATE ({name: 'mixed', vals: [1, 2, 3, 4]}) +$$) AS (result agtype); + result +-------- +(0 rows) + +SELECT * FROM cypher('predicate_functions', $$ + CREATE ({name: 'odd', vals: [1, 3, 5, 7]}) +$$) AS (result agtype); + result +-------- +(0 rows) + +-- all() with graph properties +SELECT * FROM cypher('predicate_functions', $$ + MATCH (u) WHERE all(x IN u.vals WHERE x % 2 = 0) + RETURN u.name + ORDER BY u.name +$$) AS (result agtype); + result +-------- + "even" +(1 row) + +-- any() with graph properties +SELECT * FROM cypher('predicate_functions', $$ + MATCH (u) WHERE any(x IN u.vals WHERE x > 6) + RETURN u.name + ORDER BY u.name +$$) AS (result agtype); + result +-------- + "even" + "odd" +(2 rows) + +-- none() with graph properties +SELECT * FROM cypher('predicate_functions', $$ + MATCH (u) WHERE none(x IN u.vals WHERE x < 0) + RETURN u.name + ORDER BY u.name +$$) AS (result agtype); + result +--------- + "even" + "mixed" + "odd" +(3 rows) + +-- single() with graph properties +SELECT * FROM cypher('predicate_functions', $$ + MATCH (u) WHERE single(x IN u.vals WHERE x = 8) + RETURN u.name + ORDER BY u.name +$$) AS (result agtype); + result +-------- + "even" +(1 row) + +-- +-- Predicate functions in boolean expressions +-- +SELECT * FROM cypher('predicate_functions', $$ + RETURN any(x IN [1, 2, 3] WHERE x > 2) + AND all(y IN [4, 5, 6] WHERE y > 0) +$$) AS (result agtype); + result +-------- + true +(1 row) + +SELECT * FROM cypher('predicate_functions', $$ + RETURN none(x IN [1, 2, 3] WHERE x > 5) + OR single(y IN [1, 2, 3] WHERE y = 2) +$$) AS (result agtype); + result +-------- + true +(1 row) + +-- +-- Nested predicate functions +-- +SELECT * FROM cypher('predicate_functions', $$ + RETURN any(x IN [1, 2, 3] WHERE all(y IN [1, 2] WHERE y < x)) +$$) AS (result agtype); + result +-------- + true +(1 row) + +-- +-- Keywords as property key names (safe_keywords backward compatibility) +-- +SELECT * FROM cypher('predicate_functions', $$ + RETURN {any: 1, none: 2, single: 3} +$$) AS (result agtype); + result +------------------------------------ + {"any": 1, "none": 2, "single": 3} +(1 row) + +-- +-- Cleanup +-- +SELECT * FROM drop_graph('predicate_functions', true); +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to table predicate_functions._ag_label_vertex +drop cascades to table predicate_functions._ag_label_edge +NOTICE: graph "predicate_functions" has been dropped + drop_graph +------------ + +(1 row) + diff --git a/regress/sql/predicate_functions.sql b/regress/sql/predicate_functions.sql new file mode 100644 index 000000000..7466cc2a4 --- /dev/null +++ b/regress/sql/predicate_functions.sql @@ -0,0 +1,250 @@ +/* + * 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('predicate_functions'); + +-- +-- all() predicate function +-- +-- all elements satisfy predicate -> true +SELECT * FROM cypher('predicate_functions', $$ + RETURN all(x IN [1, 2, 3] WHERE x > 0) +$$) AS (result agtype); + +-- not all elements satisfy predicate -> false +SELECT * FROM cypher('predicate_functions', $$ + RETURN all(x IN [1, 2, 3] WHERE x > 1) +$$) AS (result agtype); + +-- empty list -> true (vacuous truth) +SELECT * FROM cypher('predicate_functions', $$ + RETURN all(x IN [] WHERE x > 0) +$$) AS (result agtype); + +-- +-- any() predicate function +-- +-- at least one element satisfies -> true +SELECT * FROM cypher('predicate_functions', $$ + RETURN any(x IN [1, 2, 3] WHERE x > 2) +$$) AS (result agtype); + +-- no element satisfies -> false +SELECT * FROM cypher('predicate_functions', $$ + RETURN any(x IN [1, 2, 3] WHERE x > 5) +$$) AS (result agtype); + +-- empty list -> false +SELECT * FROM cypher('predicate_functions', $$ + RETURN any(x IN [] WHERE x > 0) +$$) AS (result agtype); + +-- +-- none() predicate function +-- +-- no element satisfies predicate -> true +SELECT * FROM cypher('predicate_functions', $$ + RETURN none(x IN [1, 2, 3] WHERE x > 5) +$$) AS (result agtype); + +-- at least one satisfies -> false +SELECT * FROM cypher('predicate_functions', $$ + RETURN none(x IN [1, 2, 3] WHERE x > 2) +$$) AS (result agtype); + +-- empty list -> true +SELECT * FROM cypher('predicate_functions', $$ + RETURN none(x IN [] WHERE x > 0) +$$) AS (result agtype); + +-- +-- single() predicate function +-- +-- exactly one element satisfies -> true +SELECT * FROM cypher('predicate_functions', $$ + RETURN single(x IN [1, 2, 3] WHERE x > 2) +$$) AS (result agtype); + +-- more than one satisfies -> false +SELECT * FROM cypher('predicate_functions', $$ + RETURN single(x IN [1, 2, 3] WHERE x > 1) +$$) AS (result agtype); + +-- none satisfies -> false +SELECT * FROM cypher('predicate_functions', $$ + RETURN single(x IN [1, 2, 3] WHERE x > 5) +$$) AS (result agtype); + +-- empty list -> false +SELECT * FROM cypher('predicate_functions', $$ + RETURN single(x IN [] WHERE x > 0) +$$) AS (result agtype); + +-- +-- NULL list input: all four return null +-- (NULL-list guard in the grammar produces CASE WHEN expr IS NULL +-- THEN NULL ELSE END) +-- +SELECT * FROM cypher('predicate_functions', $$ + RETURN all(x IN null WHERE x > 0) +$$) AS (result agtype); + +SELECT * FROM cypher('predicate_functions', $$ + RETURN any(x IN null WHERE x > 0) +$$) AS (result agtype); + +SELECT * FROM cypher('predicate_functions', $$ + RETURN none(x IN null WHERE x > 0) +$$) AS (result agtype); + +SELECT * FROM cypher('predicate_functions', $$ + RETURN single(x IN null WHERE x > 0) +$$) AS (result agtype); + +-- +-- NULL predicate results: three-valued logic +-- +-- Note: In AGE's agtype, null is a first-class value. The comparison +-- agtype_null > agtype_integer evaluates to true (not SQL NULL). +-- Three-valued logic only applies when the predicate itself is a +-- literal null constant, which becomes SQL NULL after coercion. + +-- agtype null in list: null > 0 = true in AGE, so any() = true +SELECT * FROM cypher('predicate_functions', $$ + RETURN any(x IN [null] WHERE x > 0) +$$) AS (result agtype); + +-- agtype null + real values: all comparisons are true +SELECT * FROM cypher('predicate_functions', $$ + RETURN any(x IN [null, 1, 2] WHERE x > 0) +$$) AS (result agtype); + +-- literal null predicate: pred = SQL NULL -> three-valued logic +-- all([1] WHERE null) = null (unknown) +SELECT * FROM cypher('predicate_functions', $$ + RETURN all(x IN [1] WHERE null) +$$) AS (result agtype); + +-- agtype null in list: null > 0 = true in AGE, so all() = true +SELECT * FROM cypher('predicate_functions', $$ + RETURN all(x IN [1, null, 2] WHERE x > 0) +$$) AS (result agtype); + +-- -1 > 0 = false, so all() = false +SELECT * FROM cypher('predicate_functions', $$ + RETURN all(x IN [1, null, -1] WHERE x > 0) +$$) AS (result agtype); + +-- agtype null > 0 = true in AGE, so none() = false +SELECT * FROM cypher('predicate_functions', $$ + RETURN none(x IN [null] WHERE x > 0) +$$) AS (result agtype); + +-- 5 > 0 = true, so none() = false +SELECT * FROM cypher('predicate_functions', $$ + RETURN none(x IN [null, 5] WHERE x > 0) +$$) AS (result agtype); + +-- agtype null > 0 = true AND 5 > 0 = true: 2 matches, single = false +SELECT * FROM cypher('predicate_functions', $$ + RETURN single(x IN [null, 5] WHERE x > 0) +$$) AS (result agtype); + +-- single() with null list: NULL (same as other predicate functions) +SELECT * FROM cypher('predicate_functions', $$ + RETURN single(x IN null WHERE x > 0) +$$) AS (result agtype); + +-- +-- Integration with graph data +-- +SELECT * FROM cypher('predicate_functions', $$ + CREATE ({name: 'even', vals: [2, 4, 6, 8]}) +$$) AS (result agtype); + +SELECT * FROM cypher('predicate_functions', $$ + CREATE ({name: 'mixed', vals: [1, 2, 3, 4]}) +$$) AS (result agtype); + +SELECT * FROM cypher('predicate_functions', $$ + CREATE ({name: 'odd', vals: [1, 3, 5, 7]}) +$$) AS (result agtype); + +-- all() with graph properties +SELECT * FROM cypher('predicate_functions', $$ + MATCH (u) WHERE all(x IN u.vals WHERE x % 2 = 0) + RETURN u.name + ORDER BY u.name +$$) AS (result agtype); + +-- any() with graph properties +SELECT * FROM cypher('predicate_functions', $$ + MATCH (u) WHERE any(x IN u.vals WHERE x > 6) + RETURN u.name + ORDER BY u.name +$$) AS (result agtype); + +-- none() with graph properties +SELECT * FROM cypher('predicate_functions', $$ + MATCH (u) WHERE none(x IN u.vals WHERE x < 0) + RETURN u.name + ORDER BY u.name +$$) AS (result agtype); + +-- single() with graph properties +SELECT * FROM cypher('predicate_functions', $$ + MATCH (u) WHERE single(x IN u.vals WHERE x = 8) + RETURN u.name + ORDER BY u.name +$$) AS (result agtype); + +-- +-- Predicate functions in boolean expressions +-- +SELECT * FROM cypher('predicate_functions', $$ + RETURN any(x IN [1, 2, 3] WHERE x > 2) + AND all(y IN [4, 5, 6] WHERE y > 0) +$$) AS (result agtype); + +SELECT * FROM cypher('predicate_functions', $$ + RETURN none(x IN [1, 2, 3] WHERE x > 5) + OR single(y IN [1, 2, 3] WHERE y = 2) +$$) AS (result agtype); + +-- +-- Nested predicate functions +-- +SELECT * FROM cypher('predicate_functions', $$ + RETURN any(x IN [1, 2, 3] WHERE all(y IN [1, 2] WHERE y < x)) +$$) AS (result agtype); + +-- +-- Keywords as property key names (safe_keywords backward compatibility) +-- +SELECT * FROM cypher('predicate_functions', $$ + RETURN {any: 1, none: 2, single: 3} +$$) AS (result agtype); + +-- +-- Cleanup +-- +SELECT * FROM drop_graph('predicate_functions', true); diff --git a/src/backend/nodes/ag_nodes.c b/src/backend/nodes/ag_nodes.c index 7aaaecaa5..bd78549ca 100644 --- a/src/backend/nodes/ag_nodes.c +++ b/src/backend/nodes/ag_nodes.c @@ -64,7 +64,8 @@ const char *node_names[] = { "cypher_update_item", "cypher_delete_information", "cypher_delete_item", - "cypher_merge_information" + "cypher_merge_information", + "cypher_predicate_function" }; /* @@ -132,7 +133,8 @@ const ExtensibleNodeMethods node_methods[] = { DEFINE_NODE_METHODS_EXTENDED(cypher_update_item), DEFINE_NODE_METHODS_EXTENDED(cypher_delete_information), DEFINE_NODE_METHODS_EXTENDED(cypher_delete_item), - DEFINE_NODE_METHODS_EXTENDED(cypher_merge_information) + DEFINE_NODE_METHODS_EXTENDED(cypher_merge_information), + DEFINE_NODE_METHODS_EXTENDED(cypher_predicate_function) }; static bool equal_ag_node(const ExtensibleNode *a, const ExtensibleNode *b) diff --git a/src/backend/nodes/cypher_copyfuncs.c b/src/backend/nodes/cypher_copyfuncs.c index 56895ee06..420ab1d22 100644 --- a/src/backend/nodes/cypher_copyfuncs.c +++ b/src/backend/nodes/cypher_copyfuncs.c @@ -169,3 +169,15 @@ void copy_cypher_merge_information(ExtensibleNode *newnode, const ExtensibleNode COPY_SCALAR_FIELD(merge_function_attr); COPY_NODE_FIELD(path); } + +/* copy function for cypher_predicate_function */ +void copy_cypher_predicate_function(ExtensibleNode *newnode, + const ExtensibleNode *from) +{ + COPY_LOCALS(cypher_predicate_function); + + COPY_SCALAR_FIELD(kind); + COPY_STRING_FIELD(varname); + COPY_NODE_FIELD(expr); + COPY_NODE_FIELD(where); +} diff --git a/src/backend/nodes/cypher_outfuncs.c b/src/backend/nodes/cypher_outfuncs.c index 4772621c9..cf8a400fc 100644 --- a/src/backend/nodes/cypher_outfuncs.c +++ b/src/backend/nodes/cypher_outfuncs.c @@ -189,6 +189,17 @@ void out_cypher_list_comprehension(StringInfo str, const ExtensibleNode *node) } +/* serialization function for the cypher_predicate_function ExtensibleNode. */ +void out_cypher_predicate_function(StringInfo str, const ExtensibleNode *node) +{ + DEFINE_AG_NODE(cypher_predicate_function); + + WRITE_ENUM_FIELD(kind, cypher_predicate_function_kind); + WRITE_STRING_FIELD(varname); + WRITE_NODE_FIELD(expr); + WRITE_NODE_FIELD(where); +} + /* serialization function for the cypher_delete ExtensibleNode. */ void out_cypher_merge(StringInfo str, const ExtensibleNode *node) { diff --git a/src/backend/nodes/cypher_readfuncs.c b/src/backend/nodes/cypher_readfuncs.c index a58b90cbc..14b553dbb 100644 --- a/src/backend/nodes/cypher_readfuncs.c +++ b/src/backend/nodes/cypher_readfuncs.c @@ -311,3 +311,17 @@ void read_cypher_merge_information(struct ExtensibleNode *node) READ_INT_FIELD(merge_function_attr); READ_NODE_FIELD(path); } + +/* + * Deserialize a string representing the cypher_predicate_function + * data structure. + */ +void read_cypher_predicate_function(struct ExtensibleNode *node) +{ + READ_LOCALS(cypher_predicate_function); + + READ_ENUM_FIELD(kind, cypher_predicate_function_kind); + READ_STRING_FIELD(varname); + READ_NODE_FIELD(expr); + READ_NODE_FIELD(where); +} diff --git a/src/backend/parser/cypher_analyze.c b/src/backend/parser/cypher_analyze.c index a408eea6e..7844af2f0 100644 --- a/src/backend/parser/cypher_analyze.c +++ b/src/backend/parser/cypher_analyze.c @@ -843,6 +843,22 @@ bool cypher_raw_expr_tree_walker_impl(Node *node, return true; } } + else if (is_ag_node(node, cypher_predicate_function)) + { + cypher_predicate_function *pf; + + pf = (cypher_predicate_function *)node; + + if (WALK(pf->expr)) + { + return true; + } + + if (WALK(pf->where)) + { + return true; + } + } /* Add more node types here as needed */ else { diff --git a/src/backend/parser/cypher_clause.c b/src/backend/parser/cypher_clause.c index 446e97b3f..7541c5899 100644 --- a/src/backend/parser/cypher_clause.c +++ b/src/backend/parser/cypher_clause.c @@ -25,6 +25,7 @@ #include "postgres.h" #include "access/heapam.h" +#include "catalog/pg_aggregate.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "optimizer/optimizer.h" @@ -254,6 +255,10 @@ static Query *transform_cypher_unwind(cypher_parsestate *cpstate, static Query *transform_cypher_list_comprehension(cypher_parsestate *cpstate, cypher_clause *clause); +/* predicate functions */ +static Query *transform_cypher_predicate_function(cypher_parsestate *cpstate, + cypher_clause *clause); + /* merge */ static Query *transform_cypher_merge(cypher_parsestate *cpstate, cypher_clause *clause); @@ -512,6 +517,10 @@ Query *transform_cypher_clause(cypher_parsestate *cpstate, { result = transform_cypher_list_comprehension(cpstate, clause); } + else if (is_ag_node(self, cypher_predicate_function)) + { + result = transform_cypher_predicate_function(cpstate, clause); + } else { ereport(ERROR, (errmsg_internal("unexpected Node for cypher_clause"))); @@ -1602,6 +1611,331 @@ static Query *transform_cypher_list_comprehension(cypher_parsestate *cpstate, return query; } +/* + * Helper: build a BooleanTest node (pred IS TRUE, pred IS FALSE, etc.) + */ +static Node *make_boolean_test(Node *arg, BoolTestType testtype) +{ + BooleanTest *bt = makeNode(BooleanTest); + + bt->arg = (Expr *) arg; + bt->booltesttype = testtype; + bt->location = -1; + + return (Node *) bt; +} + +/* + * Helper: build a fully-transformed bool_or(expr) Aggref node. + * + * The argument must already be a transformed boolean expression. + * We construct the Aggref manually to avoid going through FuncCall + * + transformExpr, which expects raw parse tree nodes. + */ +static Node *make_bool_or_agg(ParseState *pstate, Node *arg) +{ + Aggref *agg; + TargetEntry *te; + Oid bool_or_oid; + Oid argtypes[1] = { BOOLOID }; + + /* Look up bool_or(boolean) */ + bool_or_oid = LookupFuncName(list_make1(makeString("bool_or")), + 1, argtypes, false); + + /* Build the TargetEntry for the aggregate argument */ + te = makeTargetEntry((Expr *) arg, 1, NULL, false); + + /* Construct the Aggref */ + agg = makeNode(Aggref); + agg->aggfnoid = bool_or_oid; + agg->aggtype = BOOLOID; + agg->aggcollid = InvalidOid; + agg->inputcollid = InvalidOid; + agg->aggtranstype = InvalidOid; /* filled by planner */ + agg->aggargtypes = list_make1_oid(BOOLOID); + agg->aggdirectargs = NIL; + agg->args = list_make1(te); + agg->aggorder = NIL; + agg->aggdistinct = NIL; + agg->aggfilter = NULL; + agg->aggstar = false; + agg->aggvariadic = false; + agg->aggkind = AGGKIND_NORMAL; + agg->aggpresorted = false; + agg->agglevelsup = 0; + agg->aggsplit = AGGSPLIT_SIMPLE; + agg->aggno = -1; + agg->aggtransno = -1; + agg->location = -1; + + /* Register the aggregate with the parse state */ + pstate->p_hasAggs = true; + + return (Node *) agg; +} + +/* + * Helper: build a transformed CASE expression implementing three-valued + * predicate logic for all(), any(), and none(). + * + * any(): CASE WHEN bool_or(pred IS TRUE) THEN true + * WHEN bool_or(pred IS NULL) THEN NULL + * ELSE false END + * + * all(): CASE WHEN bool_or(pred IS FALSE) THEN false + * WHEN bool_or(pred IS NULL) THEN NULL + * ELSE true END + * + * none(): CASE WHEN bool_or(pred IS TRUE) THEN false + * WHEN bool_or(pred IS NULL) THEN NULL + * ELSE true END + * + * Empty list: both bool_or calls return NULL (no rows), so the CASE + * falls through to the default: false for any(), true for all()/none(). + * This matches Cypher's vacuous truth semantics. + */ +static Node *make_predicate_case_expr(ParseState *pstate, Node *pred, + cypher_predicate_function_kind kind) +{ + CaseExpr *cexpr; + CaseWhen *when1, *when2; + Node *bool_or_first, *bool_or_null; + Node *true_const, *false_const, *null_const; + + /* boolean constants */ + true_const = makeBoolConst(true, false); + false_const = makeBoolConst(false, false); + null_const = makeBoolConst(false, true); /* isnull = true */ + + /* Second branch is common to all: bool_or(pred IS NULL) -> NULL */ + bool_or_null = make_bool_or_agg(pstate, + make_boolean_test(pred, IS_UNKNOWN)); + + when2 = makeNode(CaseWhen); + when2->expr = (Expr *) bool_or_null; + when2->result = (Expr *) null_const; + when2->location = -1; + + if (kind == CPFK_ALL) + { + /* bool_or(pred IS FALSE) -> false */ + bool_or_first = make_bool_or_agg(pstate, + make_boolean_test(pred, IS_FALSE)); + when1 = makeNode(CaseWhen); + when1->expr = (Expr *) bool_or_first; + when1->result = (Expr *) false_const; + when1->location = -1; + + cexpr = makeNode(CaseExpr); + cexpr->casetype = BOOLOID; + cexpr->arg = NULL; + cexpr->args = list_make2(when1, when2); + cexpr->defresult = (Expr *) true_const; + cexpr->location = -1; + } + else if (kind == CPFK_ANY) + { + /* bool_or(pred IS TRUE) -> true */ + bool_or_first = make_bool_or_agg(pstate, + make_boolean_test(pred, IS_TRUE)); + when1 = makeNode(CaseWhen); + when1->expr = (Expr *) bool_or_first; + when1->result = (Expr *) true_const; + when1->location = -1; + + cexpr = makeNode(CaseExpr); + cexpr->casetype = BOOLOID; + cexpr->arg = NULL; + cexpr->args = list_make2(when1, when2); + cexpr->defresult = (Expr *) false_const; + cexpr->location = -1; + } + else /* CPFK_NONE */ + { + /* bool_or(pred IS TRUE) -> false */ + bool_or_first = make_bool_or_agg(pstate, + make_boolean_test(pred, IS_TRUE)); + when1 = makeNode(CaseWhen); + when1->expr = (Expr *) bool_or_first; + when1->result = (Expr *) false_const; + when1->location = -1; + + cexpr = makeNode(CaseExpr); + cexpr->casetype = BOOLOID; + cexpr->arg = NULL; + cexpr->args = list_make2(when1, when2); + cexpr->defresult = (Expr *) true_const; + cexpr->location = -1; + } + + return (Node *) cexpr; +} + +/* + * Transform a cypher_predicate_function node into a query tree. + * + * Generates aggregate-based queries that preserve Cypher's three-valued + * NULL semantics. The grammar layer wraps the SubLink with a + * CASE WHEN list IS NULL THEN NULL ELSE (subquery) END guard so all + * four functions return NULL when the input list is NULL. + * + * For all()/any()/none(): + * SELECT CASE WHEN bool_or(pred IS TRUE/FALSE) THEN ... + * WHEN bool_or(pred IS NULL) THEN NULL + * ELSE ... END + * FROM unnest(list) AS x + * + * For single(): + * SELECT count(*) + * FROM unnest(list) AS x + * WHERE pred IS TRUE + * + * All four use EXPR_SUBLINK so the subquery returns a scalar value. + */ +static Query *transform_cypher_predicate_function(cypher_parsestate *cpstate, + cypher_clause *clause) +{ + Query *query; + RangeFunction *rf; + cypher_predicate_function *pred_func; + FuncCall *func_call; + Node *pred, *n; + RangeTblEntry *rte = NULL; + int rtindex; + List *namespace = NULL; + TargetEntry *te; + cypher_parsestate *child_cpstate = make_cypher_parsestate(cpstate); + ParseState *child_pstate = (ParseState *) child_cpstate; + + pred_func = (cypher_predicate_function *) clause->self; + + query = makeNode(Query); + query->commandType = CMD_SELECT; + + /* FROM unnest(expr) AS varname */ + func_call = makeFuncCall(list_make1(makeString("unnest")), + list_make1(pred_func->expr), + COERCE_SQL_SYNTAX, -1); + + rf = makeNode(RangeFunction); + rf->lateral = false; + rf->ordinality = false; + rf->is_rowsfrom = false; + rf->functions = list_make1(list_make2((Node *) func_call, NIL)); + rf->alias = makeAlias(pred_func->varname, NIL); + rf->coldeflist = NIL; + + n = transform_from_clause_item(child_cpstate, (Node *) rf, + &rte, &rtindex, &namespace); + checkNameSpaceConflicts(child_pstate, child_pstate->p_namespace, namespace); + child_pstate->p_joinlist = lappend(child_pstate->p_joinlist, n); + child_pstate->p_namespace = list_concat(child_pstate->p_namespace, + namespace); + + /* make all namespace items unconditionally visible */ + setNamespaceLateralState(child_pstate->p_namespace, false, true); + + /* Transform the predicate expression */ + pred = transform_cypher_expr(child_cpstate, pred_func->where, + EXPR_KIND_WHERE); + if (pred) + { + pred = coerce_to_boolean(child_pstate, pred, "WHERE"); + } + + if (pred_func->kind == CPFK_SINGLE) + { + /* + * single(): SELECT count(*) FROM unnest(list) AS x + * WHERE pred IS TRUE + * + * Using IS TRUE ensures NULL predicates are not counted as + * matches, preserving correct semantics. The grammar layer + * compares the result = 1. + * + * Note: a LIMIT 2 optimization (to short-circuit after two + * matches) would require a nested subquery that breaks + * correlated variable references. Deferred to a future + * optimization pass. + */ + FuncCall *count_call; + Node *count_expr; + Node *is_true_qual; + + /* WHERE pred IS TRUE -- NULLs are not counted */ + is_true_qual = make_boolean_test(pred, IS_TRUE); + + count_call = makeFuncCall(list_make1(makeString("count")), + NIL, COERCE_SQL_SYNTAX, -1); + count_call->agg_star = true; + + count_expr = transformExpr(child_pstate, (Node *) count_call, + EXPR_KIND_SELECT_TARGET); + + te = makeTargetEntry((Expr *) count_expr, + (AttrNumber) child_pstate->p_next_resno++, + "count", false); + + query->targetList = lappend(query->targetList, te); + query->jointree = makeFromExpr(child_pstate->p_joinlist, + is_true_qual); + query->rtable = child_pstate->p_rtable; + query->rteperminfos = child_pstate->p_rteperminfos; + query->hasAggs = child_pstate->p_hasAggs; + query->hasSubLinks = child_pstate->p_hasSubLinks; + query->hasTargetSRFs = child_pstate->p_hasTargetSRFs; + + assign_query_collations(child_pstate, query); + + if (child_pstate->p_hasAggs || + query->groupClause || query->groupingSets || query->havingQual) + { + parse_check_aggregates(child_pstate, query); + } + + free_cypher_parsestate(child_cpstate); + + return query; + } + else + { + /* + * all()/any()/none(): Build a CASE expression with bool_or() + * aggregates that preserves three-valued NULL semantics. + * No WHERE clause -- the logic is entirely in the SELECT list. + */ + Node *case_expr; + + case_expr = make_predicate_case_expr(child_pstate, pred, + pred_func->kind); + + te = makeTargetEntry((Expr *) case_expr, + (AttrNumber) child_pstate->p_next_resno++, + "result", false); + + query->targetList = lappend(query->targetList, te); + query->jointree = makeFromExpr(child_pstate->p_joinlist, NULL); + query->rtable = child_pstate->p_rtable; + query->rteperminfos = child_pstate->p_rteperminfos; + query->hasAggs = child_pstate->p_hasAggs; + query->hasSubLinks = child_pstate->p_hasSubLinks; + query->hasTargetSRFs = child_pstate->p_hasTargetSRFs; + + assign_query_collations(child_pstate, query); + + if (child_pstate->p_hasAggs || + query->groupClause || query->groupingSets || query->havingQual) + { + parse_check_aggregates(child_pstate, query); + } + + free_cypher_parsestate(child_cpstate); + + return query; + } +} + /* * Iterate through the list of items to delete and extract the variable name. * Then find the resno that the variable name belongs to. diff --git a/src/backend/parser/cypher_gram.y b/src/backend/parser/cypher_gram.y index 5ba1e6354..0d9978859 100644 --- a/src/backend/parser/cypher_gram.y +++ b/src/backend/parser/cypher_gram.y @@ -79,7 +79,7 @@ %token NOT_EQ LT_EQ GT_EQ DOT_DOT TYPECAST PLUS_EQ /* keywords in alphabetical order */ -%token ALL ANALYZE AND AS ASC ASCENDING +%token ALL ANALYZE AND ANY_P AS ASC ASCENDING BY CALL CASE COALESCE CONTAINS COUNT CREATE DELETE DESC DESCENDING DETACH DISTINCT @@ -88,10 +88,10 @@ IN IS LIMIT MATCH MERGE - NOT NULL_P + NONE NOT NULL_P OPERATOR OPTIONAL OR ORDER REMOVE RETURN - SET SKIP STARTS + SET SINGLE SKIP STARTS THEN TRUE_P UNION UNWIND VERBOSE @@ -274,11 +274,19 @@ static Node *build_comparison_expression(Node *left_grammar_node, Node *right_grammar_node, char *opr_name, int location); +/* shared helper for list iteration constructs */ +static char *extract_iter_variable_name(Node *var); + /* list_comprehension */ static Node *build_list_comprehension_node(Node *var, Node *expr, Node *where, Node *mapping_expr, int location); +/* predicate functions: all(), any(), none(), single() */ +static Node *build_predicate_function_node(cypher_predicate_function_kind kind, + Node *var, Node *expr, + Node *where, int location); + /* helper functions */ static ExplainStmt *make_explain_stmt(List *options); static void validate_return_item_aliases(List *items, ag_scanner_t scanner); @@ -1853,6 +1861,22 @@ expr_func_subexpr: list_make1(makeString("count")), NIL, @1); $$ = (Node *)n; } + | ALL '(' expr IN expr WHERE expr ')' + { + $$ = build_predicate_function_node(CPFK_ALL, $3, $5, $7, @1); + } + | ANY_P '(' expr IN expr WHERE expr ')' + { + $$ = build_predicate_function_node(CPFK_ANY, $3, $5, $7, @1); + } + | NONE '(' expr IN expr WHERE expr ')' + { + $$ = build_predicate_function_node(CPFK_NONE, $3, $5, $7, @1); + } + | SINGLE '(' expr IN expr WHERE expr ')' + { + $$ = build_predicate_function_node(CPFK_SINGLE, $3, $5, $7, @1); + } ; expr_subquery: @@ -2372,6 +2396,7 @@ safe_keywords: ALL { $$ = KEYWORD_STRDUP($1); } | ANALYZE { $$ = KEYWORD_STRDUP($1); } | AND { $$ = KEYWORD_STRDUP($1); } + | ANY_P { $$ = KEYWORD_STRDUP($1); } | AS { $$ = KEYWORD_STRDUP($1); } | ASC { $$ = KEYWORD_STRDUP($1); } | ASCENDING { $$ = KEYWORD_STRDUP($1); } @@ -2396,6 +2421,7 @@ safe_keywords: | LIMIT { $$ = KEYWORD_STRDUP($1); } | MATCH { $$ = KEYWORD_STRDUP($1); } | MERGE { $$ = KEYWORD_STRDUP($1); } + | NONE { $$ = KEYWORD_STRDUP($1); } | NOT { $$ = KEYWORD_STRDUP($1); } | OPERATOR { $$ = KEYWORD_STRDUP($1); } | OPTIONAL { $$ = KEYWORD_STRDUP($1); } @@ -2404,6 +2430,7 @@ safe_keywords: | REMOVE { $$ = KEYWORD_STRDUP($1); } | RETURN { $$ = KEYWORD_STRDUP($1); } | SET { $$ = KEYWORD_STRDUP($1); } + | SINGLE { $$ = KEYWORD_STRDUP($1); } | SKIP { $$ = KEYWORD_STRDUP($1); } | STARTS { $$ = KEYWORD_STRDUP($1); } | THEN { $$ = KEYWORD_STRDUP($1); } @@ -3268,6 +3295,44 @@ static cypher_relationship *build_VLE_relation(List *left_arg, return cr; } +/* + * Extract and validate the iterator variable name from a ColumnRef node. + * Used by predicate functions (all/any/none/single) which share the + * "variable IN list" syntax with list comprehensions. + */ +static char *extract_iter_variable_name(Node *var) +{ + ColumnRef *cref; + String *val; + + if (!IsA(var, ColumnRef)) + { + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("syntax error at or near IN"))); + } + + cref = (ColumnRef *)var; + + /* The iterator must be a simple unqualified name (single field) */ + if (list_length(cref->fields) != 1) + { + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("qualified name not allowed as iterator variable"))); + } + + val = linitial(cref->fields); + if (!IsA(val, String)) + { + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid iterator variable name"))); + } + + return val->sval; +} + /* helper function to build a list_comprehension grammar node */ static Node *build_list_comprehension_node(Node *var, Node *expr, Node *where, Node *mapping_expr, @@ -3317,6 +3382,119 @@ static Node *build_list_comprehension_node(Node *var, Node *expr, return (Node *) node_to_agtype((Node *)sub, "agtype[]", location); } +/* + * Helper function to build a predicate function grammar node. + * + * Predicate functions follow the openCypher syntax: + * all(x IN list WHERE predicate) + * any(x IN list WHERE predicate) + * none(x IN list WHERE predicate) + * single(x IN list WHERE predicate) + * + * All four use EXPR_SUBLINK (scalar subquery). The transform layer + * generates aggregate-based queries (using bool_or + CASE) for + * all/any/none to preserve three-valued NULL semantics, and + * count(*) with IS TRUE filtering for single(). + * + * For single(), the subquery result is compared = 1. + */ +static Node *build_predicate_function_node(cypher_predicate_function_kind kind, + Node *var, Node *expr, + Node *where, int location) +{ + SubLink *sub; + cypher_predicate_function *pred_func = NULL; + Node *result; + + /* build the predicate function node */ + pred_func = make_ag_node(cypher_predicate_function); + pred_func->kind = kind; + pred_func->varname = extract_iter_variable_name(var); + pred_func->expr = expr; + pred_func->where = where; + + /* + * Wrap the predicate function in a SubLink. PostgreSQL's SubLink is + * reused here as the carrier for our custom subquery node -- the + * predicate function node is stored as the subselect and will be + * transformed into a real Query by transform_cypher_predicate_function() + * in cypher_clause.c. + * + * All predicate functions now use EXPR_SUBLINK: the transform layer + * generates aggregate-based queries that return a scalar boolean + * (for all/any/none) or integer (for single). + * + * The transform layer also wraps the result with a NULL-list guard + * (CASE WHEN list IS NULL THEN NULL ELSE END) to ensure + * all four functions return NULL when the input list is NULL. + */ + sub = makeNode(SubLink); + sub->subLinkId = 0; + sub->testexpr = NULL; + sub->operName = NIL; + sub->subselect = (Node *) pred_func; + sub->location = location; + sub->subLinkType = EXPR_SUBLINK; + + if (kind == CPFK_SINGLE) + { + /* + * single() -> (subquery) = 1 + * The subquery returns count(*) with IS TRUE filtering. + */ + Node *eq_expr; + + eq_expr = (Node *) makeSimpleA_Expr(AEXPR_OP, "=", + (Node *) sub, + make_int_const(1, location), + location); + result = (Node *) node_to_agtype(eq_expr, "boolean", location); + } + else + { + /* + * all()/any()/none(): the subquery returns a boolean directly + * from the CASE+bool_or() aggregate expression. + */ + result = (Node *) node_to_agtype((Node *) sub, "boolean", location); + } + + /* + * NULL-list guard: CASE WHEN expr IS NULL THEN NULL ELSE result END + * + * Without this, unnest(NULL) produces zero rows, causing all/any/none + * to accidentally return NULL (via bool_or over empty input) and + * single to return false (count(*) = 0). Cypher semantics require + * all four to return NULL when the input list is NULL. + * + * The expr pointer is shared with pred_func->expr. This is safe + * because AGE's expression transformer (transform_cypher_expr_recurse) + * creates new nodes rather than modifying the parse tree in-place, + * so the two references are transformed independently. + */ + { + NullTest *null_test = makeNode(NullTest); + CaseWhen *case_when = makeNode(CaseWhen); + CaseExpr *guard = makeNode(CaseExpr); + + null_test->arg = (Expr *) expr; + null_test->nulltesttype = IS_NULL; + null_test->argisrow = false; + null_test->location = location; + + case_when->expr = (Expr *) null_test; + case_when->result = (Expr *) make_null_const(location); + case_when->location = location; + + guard->arg = NULL; + guard->args = list_make1(case_when); + guard->defresult = (Expr *) result; + guard->location = location; + + return (Node *) guard; + } +} + /* Helper function to create an ExplainStmt node */ static ExplainStmt *make_explain_stmt(List *options) { diff --git a/src/include/nodes/ag_nodes.h b/src/include/nodes/ag_nodes.h index 121832c01..47c55041b 100644 --- a/src/include/nodes/ag_nodes.h +++ b/src/include/nodes/ag_nodes.h @@ -75,7 +75,9 @@ typedef enum ag_node_tag /* delete data structures */ cypher_delete_information_t, cypher_delete_item_t, - cypher_merge_information_t + cypher_merge_information_t, + /* predicate functions */ + cypher_predicate_function_t } ag_node_tag; extern const char *node_names[]; diff --git a/src/include/nodes/cypher_copyfuncs.h b/src/include/nodes/cypher_copyfuncs.h index d7ed7eff7..e770cebe2 100644 --- a/src/include/nodes/cypher_copyfuncs.h +++ b/src/include/nodes/cypher_copyfuncs.h @@ -52,4 +52,8 @@ void copy_cypher_delete_item(ExtensibleNode *newnode, /* merge data structure */ void copy_cypher_merge_information(ExtensibleNode *newnode, const ExtensibleNode *from); + +/* predicate function data structure */ +void copy_cypher_predicate_function(ExtensibleNode *newnode, + const ExtensibleNode *from); #endif diff --git a/src/include/nodes/cypher_nodes.h b/src/include/nodes/cypher_nodes.h index db47eb313..93bbe01de 100644 --- a/src/include/nodes/cypher_nodes.h +++ b/src/include/nodes/cypher_nodes.h @@ -224,6 +224,27 @@ typedef struct cypher_map_projection Node *mapping_expr; } cypher_list_comprehension; +/* + * Predicate function kinds for all(), any(), none(), single(). + * These take the form: func(variable IN list WHERE predicate) + */ +typedef enum cypher_predicate_function_kind +{ + CPFK_ALL = 0, + CPFK_ANY, + CPFK_NONE, + CPFK_SINGLE +} cypher_predicate_function_kind; + +typedef struct cypher_predicate_function +{ + ExtensibleNode extensible; + cypher_predicate_function_kind kind; + char *varname; + Node *expr; /* the list to iterate over */ + Node *where; /* the predicate to test */ +} cypher_predicate_function; + typedef enum cypher_map_projection_element_type { PROPERTY_SELECTOR = 0, /* map_var { .key } */ diff --git a/src/include/nodes/cypher_outfuncs.h b/src/include/nodes/cypher_outfuncs.h index 418d35f4e..55285bdba 100644 --- a/src/include/nodes/cypher_outfuncs.h +++ b/src/include/nodes/cypher_outfuncs.h @@ -49,6 +49,7 @@ void out_cypher_map(StringInfo str, const ExtensibleNode *node); void out_cypher_map_projection(StringInfo str, const ExtensibleNode *node); void out_cypher_list(StringInfo str, const ExtensibleNode *node); void out_cypher_list_comprehension(StringInfo str, const ExtensibleNode *node); +void out_cypher_predicate_function(StringInfo str, const ExtensibleNode *node); /* comparison expression */ void out_cypher_comparison_aexpr(StringInfo str, const ExtensibleNode *node); diff --git a/src/include/nodes/cypher_readfuncs.h b/src/include/nodes/cypher_readfuncs.h index 5792332ac..9202ba511 100644 --- a/src/include/nodes/cypher_readfuncs.h +++ b/src/include/nodes/cypher_readfuncs.h @@ -51,4 +51,7 @@ void read_cypher_delete_item(struct ExtensibleNode *node); void read_cypher_merge_information(struct ExtensibleNode *node); +/* predicate function data structure */ +void read_cypher_predicate_function(struct ExtensibleNode *node); + #endif diff --git a/src/include/parser/cypher_kwlist.h b/src/include/parser/cypher_kwlist.h index e4c4437ba..0de294979 100644 --- a/src/include/parser/cypher_kwlist.h +++ b/src/include/parser/cypher_kwlist.h @@ -1,6 +1,7 @@ PG_KEYWORD("all", ALL, RESERVED_KEYWORD) PG_KEYWORD("analyze", ANALYZE, RESERVED_KEYWORD) PG_KEYWORD("and", AND, RESERVED_KEYWORD) +PG_KEYWORD("any", ANY_P, RESERVED_KEYWORD) PG_KEYWORD("as", AS, RESERVED_KEYWORD) PG_KEYWORD("asc", ASC, RESERVED_KEYWORD) PG_KEYWORD("ascending", ASCENDING, RESERVED_KEYWORD) @@ -27,6 +28,7 @@ PG_KEYWORD("is", IS, RESERVED_KEYWORD) PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD) PG_KEYWORD("match", MATCH, RESERVED_KEYWORD) PG_KEYWORD("merge", MERGE, RESERVED_KEYWORD) +PG_KEYWORD("none", NONE, RESERVED_KEYWORD) PG_KEYWORD("not", NOT, RESERVED_KEYWORD) PG_KEYWORD("null", NULL_P, RESERVED_KEYWORD) PG_KEYWORD("operator", OPERATOR, RESERVED_KEYWORD) @@ -36,6 +38,7 @@ PG_KEYWORD("order", ORDER, RESERVED_KEYWORD) PG_KEYWORD("remove", REMOVE, RESERVED_KEYWORD) PG_KEYWORD("return", RETURN, RESERVED_KEYWORD) PG_KEYWORD("set", SET, RESERVED_KEYWORD) +PG_KEYWORD("single", SINGLE, RESERVED_KEYWORD) PG_KEYWORD("skip", SKIP, RESERVED_KEYWORD) PG_KEYWORD("starts", STARTS, RESERVED_KEYWORD) PG_KEYWORD("then", THEN, RESERVED_KEYWORD)