From 3bef830846dc6d3a4874606763ee73430dfe7b46 Mon Sep 17 00:00:00 2001 From: Rafsun Masud Date: Mon, 16 Oct 2023 12:10:43 -0700 Subject: [PATCH] Extend EXPLAIN and add config param to switch transformation of property filter (#1262) * Add explain options Explain command in the following format is supported now: `EXPLAIN (VERBOSE, COSTS OFF, FORMAT XML) ...` Note that, this is basically Postgres' EXPLAIN command, and the purpose of this is to support debugging and regression tests. * Add config param to switch transformation method of property filter When the `age.enable_containment` parameter is on, the agtype containment operator is used to transform property filter. When off, access operator is used instead. The former case is preferable for GIN index, and the later for BTREE expression index. The idea of replacing containment with access operator in order to support BTREE index is taken from a patch by Josh Innis. A note on regression testing- although there are test cases for the `age.enable_containment` parameter, sometimes it may be useful to set this parameter before running any tests (not just the ones related to it). For example, when the logic related to property transformation changes. It can be set in the `age_regression.conf` file. --- Makefile | 5 +- regress/age_regression.conf | 1 + regress/expected/cypher_match.out | 175 +++++++++++++++++++++++++++++ regress/sql/cypher_match.sql | 56 +++++++++ src/backend/age.c | 2 + src/backend/parser/cypher_clause.c | 173 +++++++++++++++++++++++++++- src/backend/parser/cypher_gram.y | 80 +++++++++++++ src/backend/utils/ag_guc.c | 44 ++++++++ src/include/utils/ag_guc.h | 44 ++++++++ 9 files changed, 572 insertions(+), 8 deletions(-) create mode 100644 regress/age_regression.conf create mode 100644 src/backend/utils/ag_guc.c create mode 100644 src/include/utils/ag_guc.h diff --git a/Makefile b/Makefile index fbd3f3b8f..deb5ea2ba 100644 --- a/Makefile +++ b/Makefile @@ -69,7 +69,8 @@ OBJS = src/backend/age.o \ src/backend/utils/load/ag_load_edges.o \ src/backend/utils/load/age_load.o \ src/backend/utils/load/libcsv.o \ - src/backend/utils/name_validation.o + src/backend/utils/name_validation.o \ + src/backend/utils/ag_guc.o EXTENSION = age @@ -105,7 +106,7 @@ REGRESS = scan \ srcdir=`pwd` ag_regress_dir = $(srcdir)/regress -REGRESS_OPTS = --load-extension=age --inputdir=$(ag_regress_dir) --outputdir=$(ag_regress_dir) --temp-instance=$(ag_regress_dir)/instance --port=61958 --encoding=UTF-8 +REGRESS_OPTS = --load-extension=age --inputdir=$(ag_regress_dir) --outputdir=$(ag_regress_dir) --temp-instance=$(ag_regress_dir)/instance --port=61958 --encoding=UTF-8 --temp-config $(ag_regress_dir)/age_regression.conf ag_regress_out = instance/ log/ results/ regression.* EXTRA_CLEAN = $(addprefix $(ag_regress_dir)/, $(ag_regress_out)) src/backend/parser/cypher_gram.c src/include/parser/cypher_gram_def.h src/include/parser/cypher_kwlist_d.h diff --git a/regress/age_regression.conf b/regress/age_regression.conf new file mode 100644 index 000000000..96f5b0892 --- /dev/null +++ b/regress/age_regression.conf @@ -0,0 +1 @@ +#age.enable_containment = on diff --git a/regress/expected/cypher_match.out b/regress/expected/cypher_match.out index ae8905c92..42a991521 100644 --- a/regress/expected/cypher_match.out +++ b/regress/expected/cypher_match.out @@ -2419,6 +2419,170 @@ SELECT * FROM cypher('cypher_match', $$ MATCH p=(x:r)-[]->(x:R) RETURN p, x $$) ERROR: multiple labels for variable 'x' are not supported LINE 1: ...FROM cypher('cypher_match', $$ MATCH p=(x:r)-[]->(x:R) RETUR... ^ +-- +-- Test age.enable_containment configuration parameter +-- +-- Test queries are run before and after switching off this parameter. +-- When on, the containment operator should be used to filter properties. +-- When off, the access operator should be used. +-- +SELECT create_graph('test_enable_containment'); +NOTICE: graph "test_enable_containment" has been created + create_graph +-------------- + +(1 row) + +SELECT * FROM cypher('test_enable_containment', +$$ + CREATE (x:Customer { + name: 'Bob', + school: { + name: 'XYZ College', + program: { + major: 'Psyc', + degree: 'BSc' + } + }, + phone: [ 123456789, 987654321, 456987123 ], + addr: [ + {city: 'Vancouver', street: 30}, + {city: 'Toronto', street: 40} + ] + }) + RETURN x +$$) as (a agtype); + a +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + {"id": 844424930131969, "label": "Customer", "properties": {"addr": [{"city": "Vancouver", "street": 30}, {"city": "Toronto", "street": 40}], "name": "Bob", "phone": [123456789, 987654321, 456987123], "school": {"name": "XYZ College", "program": {"major": "Psyc", "degree": "BSc"}}}}::vertex +(1 row) + +-- With enable_containment on +SET age.enable_containment = on; +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {addr:[{city:'Toronto'}]}) RETURN x $$) as (a agtype); + count +------- + 1 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {addr:[{city:'Toronto'}, {city: 'Vancouver'}]}) RETURN x $$) as (a agtype); + count +------- + 1 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {addr:[{city:'Alberta'}]}) RETURN x $$) as (a agtype); + count +------- + 0 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {school:{program:{major:'Psyc'}}}) RETURN x $$) as (a agtype); + count +------- + 1 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {name:'Bob',school:{program:{degree:'BSc'}}}) RETURN x $$) as (a agtype); + count +------- + 1 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {school:{program:{major:'Cs'}}}) RETURN x $$) as (a agtype); + count +------- + 0 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {name:'Bob',school:{program:{degree:'PHd'}}}) RETURN x $$) as (a agtype); + count +------- + 0 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {phone:[987654321]}) RETURN x $$) as (a agtype); + count +------- + 1 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {phone:[654765876]}) RETURN x $$) as (a agtype); + count +------- + 0 +(1 row) + +SELECT * FROM cypher('test_enable_containment', $$ EXPLAIN (COSTS OFF) MATCH (x:Customer {school:{name:'XYZ',program:{degree:'BSc'}},phone:[987654321],parents:{}}) RETURN x $$) as (a agtype); + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------ + Seq Scan on "Customer" x + Filter: (properties @> '{"phone": [987654321], "school": {"name": "XYZ", "program": {"degree": "BSc"}}, "parents": {}}'::agtype) +(2 rows) + +-- Previous set of queries, with enable_containment off +SET age.enable_containment = off; +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {addr:[{city:'Toronto'}]}) RETURN x $$) as (a agtype); + count +------- + 1 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {addr:[{city:'Toronto'}, {city: 'Vancouver'}]}) RETURN x $$) as (a agtype); + count +------- + 1 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {addr:[{city:'Alberta'}]}) RETURN x $$) as (a agtype); + count +------- + 0 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {school:{program:{major:'Psyc'}}}) RETURN x $$) as (a agtype); + count +------- + 1 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {name:'Bob',school:{program:{degree:'BSc'}}}) RETURN x $$) as (a agtype); + count +------- + 1 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {school:{program:{major:'Cs'}}}) RETURN x $$) as (a agtype); + count +------- + 0 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {name:'Bob',school:{program:{degree:'PHd'}}}) RETURN x $$) as (a agtype); + count +------- + 0 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {phone:[987654321]}) RETURN x $$) as (a agtype); + count +------- + 1 +(1 row) + +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {phone:[654765876]}) RETURN x $$) as (a agtype); + count +------- + 0 +(1 row) + +SELECT * FROM cypher('test_enable_containment', $$ EXPLAIN (COSTS OFF) MATCH (x:Customer {school:{name:'XYZ',program:{degree:'BSc'}},phone:[987654321],parents:{}}) RETURN x $$) as (a agtype); + QUERY PLAN +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Seq Scan on "Customer" x + Filter: ((agtype_access_operator(VARIADIC ARRAY[properties, '"school"'::agtype, '"name"'::agtype]) = '"XYZ"'::agtype) AND (agtype_access_operator(VARIADIC ARRAY[properties, '"school"'::agtype, '"program"'::agtype, '"degree"'::agtype]) = '"BSc"'::agtype) AND (agtype_access_operator(VARIADIC ARRAY[properties, '"phone"'::agtype]) @> '[987654321]'::agtype) AND (agtype_access_operator(VARIADIC ARRAY[properties, '"parents"'::agtype]) @> '{}'::agtype)) +(2 rows) + -- -- Clean up -- @@ -2460,6 +2624,17 @@ NOTICE: graph "test_retrieve_var" has been dropped (1 row) +SELECT drop_graph('test_enable_containment', true); +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to table test_enable_containment._ag_label_vertex +drop cascades to table test_enable_containment._ag_label_edge +drop cascades to table test_enable_containment."Customer" +NOTICE: graph "test_enable_containment" has been dropped + drop_graph +------------ + +(1 row) + -- -- End -- diff --git a/regress/sql/cypher_match.sql b/regress/sql/cypher_match.sql index 739558d1f..e4db18ac1 100644 --- a/regress/sql/cypher_match.sql +++ b/regress/sql/cypher_match.sql @@ -1077,11 +1077,67 @@ SELECT * FROM cypher('cypher_match', $$ CREATE () WITH * MATCH (x{n0:x.n1}) RETU SELECT * FROM cypher('cypher_match', $$ MATCH p=(x)-[]->(x:R) RETURN p, x $$) AS (p agtype, x agtype); SELECT * FROM cypher('cypher_match', $$ MATCH p=(x:r)-[]->(x:R) RETURN p, x $$) AS (p agtype, x agtype); +-- +-- Test age.enable_containment configuration parameter +-- +-- Test queries are run before and after switching off this parameter. +-- When on, the containment operator should be used to filter properties. +-- When off, the access operator should be used. +-- + +SELECT create_graph('test_enable_containment'); +SELECT * FROM cypher('test_enable_containment', +$$ + CREATE (x:Customer { + name: 'Bob', + school: { + name: 'XYZ College', + program: { + major: 'Psyc', + degree: 'BSc' + } + }, + phone: [ 123456789, 987654321, 456987123 ], + addr: [ + {city: 'Vancouver', street: 30}, + {city: 'Toronto', street: 40} + ] + }) + RETURN x +$$) as (a agtype); + +-- With enable_containment on +SET age.enable_containment = on; +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {addr:[{city:'Toronto'}]}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {addr:[{city:'Toronto'}, {city: 'Vancouver'}]}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {addr:[{city:'Alberta'}]}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {school:{program:{major:'Psyc'}}}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {name:'Bob',school:{program:{degree:'BSc'}}}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {school:{program:{major:'Cs'}}}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {name:'Bob',school:{program:{degree:'PHd'}}}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {phone:[987654321]}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {phone:[654765876]}) RETURN x $$) as (a agtype); +SELECT * FROM cypher('test_enable_containment', $$ EXPLAIN (COSTS OFF) MATCH (x:Customer {school:{name:'XYZ',program:{degree:'BSc'}},phone:[987654321],parents:{}}) RETURN x $$) as (a agtype); + +-- Previous set of queries, with enable_containment off +SET age.enable_containment = off; +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {addr:[{city:'Toronto'}]}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {addr:[{city:'Toronto'}, {city: 'Vancouver'}]}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {addr:[{city:'Alberta'}]}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {school:{program:{major:'Psyc'}}}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {name:'Bob',school:{program:{degree:'BSc'}}}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {school:{program:{major:'Cs'}}}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {name:'Bob',school:{program:{degree:'PHd'}}}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {phone:[987654321]}) RETURN x $$) as (a agtype); +SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH (x:Customer {phone:[654765876]}) RETURN x $$) as (a agtype); +SELECT * FROM cypher('test_enable_containment', $$ EXPLAIN (COSTS OFF) MATCH (x:Customer {school:{name:'XYZ',program:{degree:'BSc'}},phone:[987654321],parents:{}}) RETURN x $$) as (a agtype); + -- -- Clean up -- SELECT drop_graph('cypher_match', true); SELECT drop_graph('test_retrieve_var', true); +SELECT drop_graph('test_enable_containment', true); -- -- End diff --git a/src/backend/age.c b/src/backend/age.c index e342146df..a512f1888 100644 --- a/src/backend/age.c +++ b/src/backend/age.c @@ -25,6 +25,7 @@ #include "nodes/ag_nodes.h" #include "optimizer/cypher_paths.h" #include "parser/cypher_analyze.h" +#include "utils/ag_guc.h" PG_MODULE_MAGIC; @@ -37,6 +38,7 @@ void _PG_init(void) object_access_hook_init(); process_utility_hook_init(); post_parse_analyze_init(); + define_config_params(); } void _PG_fini(void); diff --git a/src/backend/parser/cypher_clause.c b/src/backend/parser/cypher_clause.c index 1f7e48aa3..8467cbbab 100644 --- a/src/backend/parser/cypher_clause.c +++ b/src/backend/parser/cypher_clause.c @@ -65,6 +65,7 @@ #include "utils/ag_func.h" #include "utils/agtype.h" #include "utils/graphid.h" +#include "utils/ag_guc.h" /* * Variable string names for makeTargetEntry. As they are going to be variable @@ -172,6 +173,12 @@ static List *make_edge_quals(cypher_parsestate *cpstate, enum transform_entity_join_side side); static A_Expr *filter_vertices_on_label_id(cypher_parsestate *cpstate, Node *id_field, char *label); +static Node *transform_map_to_ind(cypher_parsestate *cpstate, + transform_entity *entity, cypher_map *map); +static List *transform_map_to_ind_recursive(cypher_parsestate *cpstate, + transform_entity *entity, + cypher_map *map, + List *parent_fields); static Node *create_property_constraints(cypher_parsestate *cpstate, transform_entity *entity, Node *property_constraints, @@ -3558,9 +3565,156 @@ static A_Expr *filter_vertices_on_label_id(cypher_parsestate *cpstate, } /* - * Creates the Contains operator to process property constraints for a vertex/ - * edge in a MATCH clause. Creates the agtype @> with the entity's properties - * on the right and the constraints in the MATCH clause on the left. + * Makes property constraint using indirection(s). This is an + * alternative to using the containment operator (@>). + * + * In case of array and empty map, containment is used instead of equality. + * + * For example, the following query + * + * MATCH (x:Label{ + * name: 'xyz', + * address: { + * city: 'abc', + * street: { + * name: 'pqr', + * number: 123 + * } + * }, + * phone: [9, 8, 7], + * parents: {} + * }) + * + * is transformed to- + * + * x.name = 'xyz' AND + * x.address.city = 'abc' AND + * x.address.street.name = 'pqr' AND + * x.address.street.number = 123 AND + * x.phone @> [6, 4, 3] AND + * x.parents @> {} + */ +static Node *transform_map_to_ind(cypher_parsestate *cpstate, + transform_entity *entity, cypher_map *map) +{ + List *quals; // list of equality and/or containment qual node + + quals = transform_map_to_ind_recursive(cpstate, entity, map, NIL); + Assert(quals != NIL); + + if (list_length(quals) > 1) + { + return (Node *)makeBoolExpr(AND_EXPR, quals, -1); + } + else + { + return (Node *)linitial(quals); + } +} + +/* + * Helper function of `transform_map_to_ind`. + * + * This function is called when a value of the `map` is a non-empty map. + * For example, the key `address.street` has a non-empty map. The + * `parent_fields` parameter will be set to the list of parents of the + * key `street` in order. In this case, only `address`. If no parent + * fields, set it to NIL. + */ +static List *transform_map_to_ind_recursive(cypher_parsestate *cpstate, + transform_entity *entity, + cypher_map *map, + List *parent_fields) +{ + int i; + ParseState *pstate; + Node *last_srf; + List *quals; + + pstate = (ParseState *)cpstate; + last_srf = pstate->p_last_srf; + quals = NIL; + + /* since this function recurses, it could be driven to stack overflow */ + check_stack_depth(); + + Assert(list_length(map->keyvals) != 0); + + for (i = 0; i < map->keyvals->length; i += 2) + { + Node *key; + Node *val; + char *keystr; + + key = (Node *)map->keyvals->elements[i].ptr_value; + val = (Node *)map->keyvals->elements[i + 1].ptr_value; + Assert(IsA(key, String)); + keystr = ((String *)key)->sval; + + if (is_ag_node(val, cypher_map) && + list_length(((cypher_map *)val)->keyvals) != 0) + { + List *new_parent_fields; + List *recursive_quals; + + new_parent_fields = lappend(list_copy(parent_fields), + makeString(keystr)); + + recursive_quals = transform_map_to_ind_recursive( + cpstate, entity, (cypher_map *)val, new_parent_fields); + + quals = list_concat(quals, recursive_quals); + + list_free(new_parent_fields); + list_free(recursive_quals); + } + else + { + Node *qual; + Node *lhs; + Node *rhs; + List *op; + A_Indirection *indir; + ColumnRef *variable; + + /* + * Lists and empty maps are transformed to containment. If a map + * makes it here, then it must be empty. Because non-empty maps + * are processed in the upper if-block. + */ + if (is_ag_node(val, cypher_list) || is_ag_node(val, cypher_map)) + { + op = list_make1(makeString("@>")); + } + else + { + op = list_make1(makeString("=")); + } + + variable = makeNode(ColumnRef); + variable->fields = + list_make1(makeString(entity->entity.node->name)); + variable->location = -1; + + indir = makeNode(A_Indirection); + indir->arg = (Node *)variable; + indir->indirection = lappend(list_copy(parent_fields), + makeString(keystr)); + + lhs = transform_cypher_expr(cpstate, (Node *)indir, + EXPR_KIND_WHERE); + rhs = transform_cypher_expr(cpstate, val, EXPR_KIND_WHERE); + + qual = (Node *)make_op(pstate, op, lhs, rhs, last_srf, -1); + quals = lappend(quals, qual); + } + } + + return quals; +} + +/* + * Creates the property constraints for a vertex/edge in a MATCH clause. */ static Node *create_property_constraints(cypher_parsestate *cpstate, transform_entity *entity, @@ -3603,11 +3757,18 @@ static Node *create_property_constraints(cypher_parsestate *cpstate, const_expr = transform_cypher_expr(cpstate, property_constraints, EXPR_KIND_WHERE); - return (Node *)make_op(pstate, list_make1(makeString("@>")), prop_expr, - const_expr, last_srf, -1); + if (age_enable_containment) + { + return (Node *)make_op(pstate, list_make1(makeString("@>")), prop_expr, + const_expr, last_srf, -1); + } + else + { + return (Node *)transform_map_to_ind( + cpstate, entity, (cypher_map *)property_constraints); + } } - /* * For the given path, transform each entity within the path, create * the path variable if needed, and construct the quals to enforce the diff --git a/src/backend/parser/cypher_gram.y b/src/backend/parser/cypher_gram.y index 94054324b..b722da117 100644 --- a/src/backend/parser/cypher_gram.y +++ b/src/backend/parser/cypher_gram.y @@ -33,6 +33,7 @@ #include "parser/ag_scanner.h" #include "parser/cypher_gram.h" #include "parser/cypher_parse_node.h" +#include "parser/scansup.h" // override the default action for locations #define YYLLOC_DEFAULT(current, rhs, n) \ @@ -184,6 +185,11 @@ /*set operations*/ %type all_or_distinct +/* utility options */ +%type utility_option_list +%type utility_option_elem utility_option_arg +%type utility_option_name + %{ // // internal alias check @@ -321,6 +327,20 @@ stmt: makeDefElem("verbose", NULL, @3));; extra->extra = (Node *)estmt; } + | EXPLAIN '(' utility_option_list ')' cypher_stmt semicolon_opt + { + ExplainStmt *estmt = NULL; + + if (yychar != YYEOF) + yyerror(&yylloc, scanner, extra, "syntax error"); + + extra->result = $5; + + estmt = makeNode(ExplainStmt); + estmt->query = NULL; + estmt->options = $3; + extra->extra = (Node *)estmt; + } ; cypher_stmt: @@ -1092,6 +1112,66 @@ where_opt: } ; +utility_option_list: + utility_option_elem + { + $$ = list_make1($1); + } + | utility_option_list ',' utility_option_elem + { + $$ = lappend($1, $3); + } + ; + +utility_option_elem: + utility_option_name utility_option_arg + { + $$ = (Node *)makeDefElem($1, $2, @1); + } + ; + +utility_option_name: + IDENTIFIER + { + char *modified_name = downcase_truncate_identifier($1, strlen($1), + true); + $$ = modified_name; + } + | safe_keywords + { + char *name = pstrdup($1); + char *modified_name = downcase_truncate_identifier(name, + strlen(name), + true); + $$ = modified_name; + } + ; + +utility_option_arg: + IDENTIFIER + { + char *modified_val = downcase_truncate_identifier($1, strlen($1), + true); + $$ = (Node *)makeString(modified_val); + } + | INTEGER + { + $$ = (Node *)makeInteger($1); + } + | TRUE_P + { + $$ = (Node *)makeString("true"); + } + | FALSE_P + { + $$ = (Node *)makeString("false"); + } + | /* EMPTY */ + { + $$ = NULL; + } + ; + /* * pattern */ diff --git a/src/backend/utils/ag_guc.c b/src/backend/utils/ag_guc.c new file mode 100644 index 000000000..efbc61678 --- /dev/null +++ b/src/backend/utils/ag_guc.c @@ -0,0 +1,44 @@ +/* + * 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 "utils/guc.h" +#include "utils/ag_guc.h" + +bool age_enable_containment = true; + +/* + * Defines AGE's custom configuration parameters. + * + * The name of the parameter must be `age.*`. This name is used for setting + * value to the parameter. For example, `SET age.enable_containment = on;`. + */ +void define_config_params(void) +{ + DefineCustomBoolVariable("age.enable_containment", + "Use @> operator to transform MATCH's filter. Otherwise, use -> operator.", + NULL, + &age_enable_containment, + true, + PGC_SUSET, + 0, + NULL, + NULL, + NULL); +} diff --git a/src/include/utils/ag_guc.h b/src/include/utils/ag_guc.h new file mode 100644 index 000000000..52fab2b85 --- /dev/null +++ b/src/include/utils/ag_guc.h @@ -0,0 +1,44 @@ +/* + * 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 AG_GUC_H +#define AG_GUC_H + +/* + * AGE configuration parameters. + * + * Ideally, these parameters should be documented in a .sgml file. + * + * To add a new parameter, add a global variable. Add its definition + * in the `define_config_params` function. Include this header file + * to use the global variable. The parameters can be set just like + * regular Postgres parameters. See guc.h for more details. + */ + +/* + * If set true, MATCH's property filter is transformed into the @> + * (containment) operator. Otherwise, the -> operator is used. The former case + * is useful when GIN index is desirable, the latter case is useful for Btree + * expression index. + */ +extern bool age_enable_containment; + +void define_config_params(void); + +#endif