From 18833cfc35b5530f2a79673c73b851795c31514b Mon Sep 17 00:00:00 2001 From: Henson Choi Date: Fri, 26 Dec 2025 10:07:52 +0900 Subject: [PATCH 2/3] SQL/PGQ: Add PROPERTY_NAMES() graph element function Implement PROPERTY_NAMES() function that returns all property names of a graph element as text[]. This follows the SQL/PGQ standard for graph element functions. Implementation mirrors LABELS() by wrapping each element table in a subquery that adds a virtual __property_names__ column containing the element's property name array. This design enables the query planner to: - Prune Append branches when filtering by specific properties (e.g., WHERE 'name' = ANY(PROPERTY_NAMES(v)) scans only tables with 'name' property) - Constant-fold property name arrays at plan time - Eliminate scans entirely for non-matching property filters Property names are collected from all labels attached to an element, with duplicates removed. Elements without PROPERTIES clauses return an empty array. Key changes: - Add GraphPropertyNamesRef node type for PROPERTY_NAMES() in parse tree - Transform PROPERTY_NAMES(var) to Var referencing __property_names__ column - Add build_property_names_const() to collect properties from catalog - Extend create_element_subquery_rte() with property_names_attnum - Add optimizer pruning tests for property-based filtering --- src/backend/nodes/nodeFuncs.c | 4 + src/backend/parser/parse_collate.c | 1 + src/backend/parser/parse_graphtable.c | 43 +++++ src/backend/rewrite/rewriteGraphTable.c | 160 +++++++++++++++- src/backend/utils/adt/ruleutils.c | 8 + src/include/nodes/primnodes.h | 10 + src/test/regress/expected/graph_table.out | 218 ++++++++++++++++++++++ src/test/regress/sql/graph_table.sql | 104 +++++++++++ 8 files changed, 538 insertions(+), 10 deletions(-) diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 8c478a31f49..462e83aa232 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -288,6 +288,7 @@ exprType(const Node *expr) type = ((const GraphPropertyRef *) expr)->typeId; break; case T_GraphLabelsRef: + case T_GraphPropertyNamesRef: type = TEXTARRAYOID; break; default: @@ -545,6 +546,7 @@ exprTypmod(const Node *expr) case T_GraphPropertyRef: return ((const GraphPropertyRef *) expr)->typmod; case T_GraphLabelsRef: + case T_GraphPropertyNamesRef: return -1; default: break; @@ -1072,6 +1074,7 @@ exprCollation(const Node *expr) coll = ((const GraphPropertyRef *) expr)->collation; break; case T_GraphLabelsRef: + case T_GraphPropertyNamesRef: coll = DEFAULT_COLLATION_OID; break; default: @@ -2142,6 +2145,7 @@ expression_tree_walker_impl(Node *node, case T_CTESearchClause: case T_GraphPropertyRef: case T_GraphLabelsRef: + case T_GraphPropertyNamesRef: case T_MergeSupportFunc: /* primitive node types with no expression subnodes */ break; diff --git a/src/backend/parser/parse_collate.c b/src/backend/parser/parse_collate.c index 9aaf01bf58b..ab7a26830cf 100644 --- a/src/backend/parser/parse_collate.c +++ b/src/backend/parser/parse_collate.c @@ -548,6 +548,7 @@ assign_collations_walker(Node *node, assign_collations_context *context) case T_CurrentOfExpr: case T_GraphPropertyRef: case T_GraphLabelsRef: + case T_GraphPropertyNamesRef: /* * General case for childless expression nodes. These should diff --git a/src/backend/parser/parse_graphtable.c b/src/backend/parser/parse_graphtable.c index 5bd71eae536..2f8ec0cae30 100644 --- a/src/backend/parser/parse_graphtable.c +++ b/src/backend/parser/parse_graphtable.c @@ -143,6 +143,49 @@ transformGraphTableFuncCall(ParseState *pstate, FuncCall *fn) return (Node *) glr; } + if (pg_strcasecmp(funcname, "property_names") == 0) + { + Node *arg; + ColumnRef *cref; + char *elvarname; + GraphPropertyNamesRef *gpnr; + + if (list_length(fn->args) != 1) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("PROPERTY_NAMES() requires exactly one argument"), + parser_errposition(pstate, fn->location)); + + arg = linitial(fn->args); + + if (!IsA(arg, ColumnRef)) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("PROPERTY_NAMES() argument must be an element variable"), + parser_errposition(pstate, fn->location)); + + cref = (ColumnRef *) arg; + if (list_length(cref->fields) != 1) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("PROPERTY_NAMES() argument must be an element variable"), + parser_errposition(pstate, cref->location)); + + elvarname = strVal(linitial(cref->fields)); + + if (!list_member(gpstate->variables, linitial(cref->fields))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("element variable \"%s\" does not exist", elvarname), + parser_errposition(pstate, cref->location)); + + gpnr = makeNode(GraphPropertyNamesRef); + gpnr->elvarname = pstrdup(elvarname); + gpnr->location = fn->location; + + return (Node *) gpnr; + } + return NULL; } diff --git a/src/backend/rewrite/rewriteGraphTable.c b/src/backend/rewrite/rewriteGraphTable.c index 605ce35da97..44bdc8a1f20 100644 --- a/src/backend/rewrite/rewriteGraphTable.c +++ b/src/backend/rewrite/rewriteGraphTable.c @@ -91,6 +91,8 @@ struct path_element List *dest_quals; /* Attribute number of __labels__ column in subquery RTE */ AttrNumber labels_attnum; + /* Attribute number of __property_names__ column in subquery RTE */ + AttrNumber property_names_attnum; }; static Node *replace_property_refs(Oid propgraphid, Node *node, const List *mappings); @@ -98,7 +100,8 @@ static List *build_edge_vertex_link_quals(HeapTuple edgetup, int edgerti, int re static List *generate_queries_for_path_pattern(RangeTblEntry *rte, List *element_patterns); static Query *generate_query_for_graph_path(RangeTblEntry *rte, List *path); static Const *build_labels_const(Oid elemoid); -static RangeTblEntry *create_element_subquery_rte(struct path_element *pe, Oid reloid, AttrNumber *labels_attnum); +static Const *build_property_names_const(Oid elemoid); +static RangeTblEntry *create_element_subquery_rte(struct path_element *pe, Oid reloid, AttrNumber *labels_attnum, AttrNumber *property_names_attnum); static Node *generate_setop_from_pathqueries(List *pathqueries, List **rtable, List **targetlist); static List *generate_queries_for_path_pattern_recurse(RangeTblEntry *rte, List *pathqueries, List *cur_path, List *path_pattern_lists, int elempos); static Query *generate_query_for_empty_path_pattern(RangeTblEntry *rte); @@ -423,6 +426,7 @@ generate_query_for_graph_path(RangeTblEntry *rte, List *graph_path) RangeTblRef *rtr; RangeTblEntry *subrte; AttrNumber labels_attnum; + AttrNumber property_names_attnum; Assert(pf->kind == VERTEX_PATTERN || IS_EDGE_PATTERN(pf->kind)); @@ -500,8 +504,9 @@ generate_query_for_graph_path(RangeTblEntry *rte, List *graph_path) * make property graphs as a hole for unprivileged data access. This * is inline with the views being security_invoker by default. */ - subrte = create_element_subquery_rte(pe, pe->reloid, &labels_attnum); + subrte = create_element_subquery_rte(pe, pe->reloid, &labels_attnum, &property_names_attnum); pe->labels_attnum = labels_attnum; + pe->property_names_attnum = property_names_attnum; path_query->rtable = lappend(path_query->rtable, subrte); rtr = makeNode(RangeTblRef); @@ -1152,6 +1157,36 @@ replace_property_refs_mutator(Node *node, struct replace_property_refs_context * return (Node *) var; } + else if (IsA(node, GraphPropertyNamesRef)) + { + GraphPropertyNamesRef *gpnr = (GraphPropertyNamesRef *) node; + struct path_element *found_mapping = NULL; + Var *var; + + /* Find the element mapping for this variable */ + foreach_ptr(struct path_element, m, context->mappings) + { + if (m->path_factor->variable && + strcmp(gpnr->elvarname, m->path_factor->variable) == 0) + { + found_mapping = m; + break; + } + } + if (!found_mapping) + elog(ERROR, "undefined element variable \"%s\"", gpnr->elvarname); + + /* + * Return a Var referencing the __property_names__ column in the + * subquery RTE. This allows the optimizer to push down predicates + * involving PROPERTY_NAMES(). + */ + var = makeVar(found_mapping->path_factor->factorpos + 1, + found_mapping->property_names_attnum, + TEXTARRAYOID, -1, InvalidOid, 0); + + return (Node *) var; + } return expression_tree_mutator(node, replace_property_refs_mutator, context); } @@ -1533,14 +1568,109 @@ build_labels_const(Oid elemoid) } /* - * Create a subquery RTE that wraps the element table and adds a __labels__ - * column. The subquery structure is: + * Build a Const node containing an array of property names for the given element. + * Returns text[] constant like ARRAY['name', 'age', 'email']::text[]. + * + * Property names are collected from all labels associated with the element, + * with duplicates removed (since multiple labels can define the same property). + */ +static Const * +build_property_names_const(Oid elemoid) +{ + List *prop_names = NIL; + ArrayType *arr; + Datum *elems; + int nelems; + int i; + Relation elem_label_rel; + SysScanDesc elem_label_scan; + ScanKeyData elem_label_key[1]; + HeapTuple elem_label_tup; + + /* Get all element-label associations for this element */ + elem_label_rel = table_open(PropgraphElementLabelRelationId, AccessShareLock); + ScanKeyInit(&elem_label_key[0], + Anum_pg_propgraph_element_label_pgelelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(elemoid)); + elem_label_scan = systable_beginscan(elem_label_rel, + PropgraphElementLabelElementLabelIndexId, + true, NULL, 1, elem_label_key); + + while (HeapTupleIsValid(elem_label_tup = systable_getnext(elem_label_scan))) + { + Form_pg_propgraph_element_label elem_label_form = + (Form_pg_propgraph_element_label) GETSTRUCT(elem_label_tup); + Oid element_label_oid = elem_label_form->oid; + Relation label_prop_rel; + SysScanDesc label_prop_scan; + ScanKeyData label_prop_key[1]; + HeapTuple label_prop_tup; + + /* Get all properties for this element-label association */ + label_prop_rel = table_open(PropgraphLabelPropertyRelationId, AccessShareLock); + ScanKeyInit(&label_prop_key[0], + Anum_pg_propgraph_label_property_plpellabelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(element_label_oid)); + label_prop_scan = systable_beginscan(label_prop_rel, + PropgraphLabelPropertyLabelPropIndexId, + true, NULL, 1, label_prop_key); + + while (HeapTupleIsValid(label_prop_tup = systable_getnext(label_prop_scan))) + { + Form_pg_propgraph_label_property label_prop_form = + (Form_pg_propgraph_label_property) GETSTRUCT(label_prop_tup); + char *propname = get_propgraph_property_name(label_prop_form->plppropid); + bool found = false; + + /* Deduplicate: check if this property name is already in the list */ + foreach_ptr(String, existing, prop_names) + { + if (strcmp(strVal(existing), propname) == 0) + { + found = true; + break; + } + } + + if (!found) + prop_names = lappend(prop_names, makeString(propname)); + } + + systable_endscan(label_prop_scan); + table_close(label_prop_rel, AccessShareLock); + } + + systable_endscan(elem_label_scan); + table_close(elem_label_rel, AccessShareLock); + + /* Build ARRAY['prop1', 'prop2', ...]::text[] */ + nelems = list_length(prop_names); + elems = (Datum *) palloc(nelems * sizeof(Datum)); + i = 0; + foreach_ptr(String, s, prop_names) + { + elems[i++] = CStringGetTextDatum(strVal(s)); + } + + arr = construct_array(elems, nelems, TEXTOID, -1, false, TYPALIGN_INT); + + return makeConst(TEXTARRAYOID, -1, InvalidOid, + -1, PointerGetDatum(arr), false, false); +} + +/* + * Create a subquery RTE that wraps the element table and adds virtual columns + * for graph element functions. The subquery structure is: * - * SELECT *, ARRAY['Label1', 'Label2']::text[] AS __labels__ + * SELECT *, ARRAY['Label1', 'Label2']::text[] AS __labels__, + * ARRAY['prop1', 'prop2']::text[] AS __property_names__ * FROM element_table * - * This allows LABELS(v) to reference a Var instead of a constant, enabling - * the planner to push down predicates involving LABELS(). + * This allows LABELS(v) and PROPERTY_NAMES(v) to reference Vars instead of + * constants, enabling the planner to push down predicates and prune branches + * in UNION queries when filtering by specific labels or properties. * * The returned subquery RTE does NOT have relid/relkind/perminfoindex set, * matching how regular SQL subqueries work. Permission checking (including @@ -1548,7 +1678,8 @@ build_labels_const(Oid elemoid) * where the actual RTE_RELATION lives. */ static RangeTblEntry * -create_element_subquery_rte(struct path_element *pe, Oid reloid, AttrNumber *labels_attnum) +create_element_subquery_rte(struct path_element *pe, Oid reloid, + AttrNumber *labels_attnum, AttrNumber *property_names_attnum) { Query *subquery; RangeTblEntry *subrte; @@ -1559,7 +1690,9 @@ create_element_subquery_rte(struct path_element *pe, Oid reloid, AttrNumber *lab int attno; ParseNamespaceItem *pni; Const *labels_const; + Const *property_names_const; TargetEntry *labels_te; + TargetEntry *property_names_te; subquery = makeNode(Query); subquery->commandType = CMD_SELECT; @@ -1591,7 +1724,7 @@ create_element_subquery_rte(struct path_element *pe, Oid reloid, AttrNumber *lab rtr->rtindex = 1; subquery->jointree = makeFromExpr(list_make1(rtr), NULL); - /* Build targetlist: all columns from the table + __labels__ */ + /* Build targetlist: all columns from the table + virtual columns */ subquery->targetList = NIL; attno = 1; for (int i = 0; i < tupdesc->natts; i++) @@ -1615,7 +1748,14 @@ create_element_subquery_rte(struct path_element *pe, Oid reloid, AttrNumber *lab labels_te = makeTargetEntry((Expr *) labels_const, attno, pstrdup("__labels__"), false); subquery->targetList = lappend(subquery->targetList, labels_te); - *labels_attnum = attno; + *labels_attnum = attno++; + + /* Add __property_names__ column */ + property_names_const = build_property_names_const(pe->elemoid); + property_names_te = makeTargetEntry((Expr *) property_names_const, attno, + pstrdup("__property_names__"), false); + subquery->targetList = lappend(subquery->targetList, property_names_te); + *property_names_attnum = attno; /* Create the wrapper subquery RTE */ subrte = makeNode(RangeTblEntry); diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 9aff5b32527..72db4c87a79 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -11151,6 +11151,14 @@ get_rule_expr(Node *node, deparse_context *context, break; } + case T_GraphPropertyNamesRef: + { + GraphPropertyNamesRef *gpnr = (GraphPropertyNamesRef *) node; + + appendStringInfo(buf, "PROPERTY_NAMES(%s)", quote_identifier(gpnr->elvarname)); + break; + } + default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); break; diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index b658bb2be8c..980ddd5d093 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -2211,6 +2211,16 @@ typedef struct GraphLabelsRef ParseLoc location; } GraphLabelsRef; +/* + * GraphPropertyNamesRef - PROPERTY_NAMES(element_variable) inside GRAPH_TABLE clause + */ +typedef struct GraphPropertyNamesRef +{ + Expr xpr; + const char *elvarname; /* element variable name */ + ParseLoc location; +} GraphPropertyNamesRef; + /*-------------------- * TargetEntry - * a target entry (used in query target lists) diff --git a/src/test/regress/expected/graph_table.out b/src/test/regress/expected/graph_table.out index c82d7fd7291..a7377a6d768 100644 --- a/src/test/regress/expected/graph_table.out +++ b/src/test/regress/expected/graph_table.out @@ -1164,4 +1164,222 @@ EXPLAIN (VERBOSE, COSTS OFF) EXECUTE labels_test('orders'); (2 rows) DEALLOCATE labels_test; +-- PROPERTY_NAMES() function tests +-- basic PROPERTY_NAMES() in COLUMNS clause +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (c.name, PROPERTY_NAMES(c) AS props)) ORDER BY 1; + name | props +-----------+---------------------------- + customer1 | {name,customer_id,address} + customer2 | {name,customer_id,address} + customer3 | {name,customer_id,address} +(3 rows) + +-- PROPERTY_NAMES() for vertices and edges +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers)-[e IS customer_orders]->(o IS orders) COLUMNS (c.name, PROPERTY_NAMES(c) AS cprops, PROPERTY_NAMES(e) AS eprops, PROPERTY_NAMES(o) AS oprops)) ORDER BY 1; + name | cprops | eprops | oprops +-----------+----------------------------+---------------------------------------------------+------------------------------------------- + customer1 | {name,customer_id,address} | {customer_id,order_id,customer_orders_id,link_id} | {order_id,ordered_when,node_id,list_type} + customer2 | {name,customer_id,address} | {customer_id,order_id,customer_orders_id,link_id} | {order_id,ordered_when,node_id,list_type} +(2 rows) + +-- PROPERTY_NAMES() in WHERE clause +SELECT * FROM GRAPH_TABLE (myshop MATCH (c) WHERE 'name' = ANY(PROPERTY_NAMES(c)) COLUMNS (c.name)) ORDER BY 1; + name +----------- + customer1 + customer2 + customer3 + product1 + product2 + product3 +(6 rows) + +-- PROPERTY_NAMES() with array functions +SELECT * FROM GRAPH_TABLE (myshop MATCH (c) WHERE array_length(PROPERTY_NAMES(c), 1) >= 1 COLUMNS (c.name, PROPERTY_NAMES(c) AS props)) ORDER BY 1; + name | props +-----------+----------------------------------------------- + customer1 | {name,customer_id,address} + customer2 | {name,customer_id,address} + customer3 | {name,customer_id,address} + product1 | {product_no,name,price} + product2 | {product_no,name,price} + product3 | {product_no,name,price} + | {node_id,list_type,wishlist_id,wishlist_name} + | {node_id,list_type,wishlist_id,wishlist_name} + | {node_id,list_type,wishlist_id,wishlist_name} + | {order_id,ordered_when,node_id,list_type} + | {order_id,ordered_when,node_id,list_type} + | {order_id,ordered_when,node_id,list_type} +(12 rows) + +-- PROPERTY_NAMES() with element that has no properties (g3 has no PROPERTIES clauses) +SELECT * FROM GRAPH_TABLE (g3 MATCH (s IS pv) COLUMNS (s.val, PROPERTY_NAMES(s) AS props)) ORDER BY 1; + val | props +-----+---------- + 10 | {id,val} + 20 | {id,val} + 30 | {id,val} +(3 rows) + +SELECT * FROM GRAPH_TABLE (g3 MATCH (s)-[e]->(d) COLUMNS (e.val, PROPERTY_NAMES(e) AS edge_props)) ORDER BY 1; + val | edge_props +-----+------------------- + 100 | {id,val,src,dest} + 200 | {id,val,src,dest} + 300 | {id,val,src,dest} +(3 rows) + +-- PROPERTY_NAMES() EXPLAIN tests - check query plan generation +EXPLAIN (VERBOSE, COSTS OFF) +SELECT * FROM GRAPH_TABLE (myshop + MATCH (c IS customers) + COLUMNS (c.name, PROPERTY_NAMES(c) AS props) +); + QUERY PLAN +---------------------------------------------------------------- + Seq Scan on graph_table_tests.customers + Output: customers.name, '{name,customer_id,address}'::text[] +(2 rows) + +-- PROPERTY_NAMES() filtering in WHERE clause +EXPLAIN (VERBOSE, COSTS OFF) +SELECT * FROM GRAPH_TABLE (myshop + MATCH (c IS customers) + WHERE 'name' = ANY(PROPERTY_NAMES(c)) + COLUMNS (c.name, PROPERTY_NAMES(c) AS props) +); + QUERY PLAN +---------------------------------------------------------------- + Seq Scan on graph_table_tests.customers + Output: customers.name, '{name,customer_id,address}'::text[] +(2 rows) + +-- PROPERTY_NAMES() filtering outside GRAPH_TABLE (in outer WHERE clause) +EXPLAIN (VERBOSE, COSTS OFF) +SELECT * FROM GRAPH_TABLE (myshop + MATCH (c IS customers) + COLUMNS (c.name, PROPERTY_NAMES(c) AS props) +) WHERE 'address' = ANY(props); + QUERY PLAN +---------------------------------------------------------------- + Seq Scan on graph_table_tests.customers + Output: customers.name, '{name,customer_id,address}'::text[] +(2 rows) + +-- PROPERTY_NAMES() filtering with nonexistent property (should optimize away with One-Time Filter: false) +EXPLAIN (VERBOSE, COSTS OFF) +SELECT * FROM GRAPH_TABLE (myshop + MATCH (c IS customers) + WHERE 'nonexistent_prop' = ANY(PROPERTY_NAMES(c)) + COLUMNS (c.name, PROPERTY_NAMES(c) AS props) +); + QUERY PLAN +---------------------------------------------------------------- + Result + Output: customers.name, '{name,customer_id,address}'::text[] + Replaces: Scan on customers + One-Time Filter: false +(4 rows) + +-- PROPERTY_NAMES() filtering with existing property (should NOT optimize away) +EXPLAIN (VERBOSE, COSTS OFF) +SELECT * FROM GRAPH_TABLE (myshop + MATCH (c IS customers) + WHERE 'name' = ANY(PROPERTY_NAMES(c)) + COLUMNS (c.name) +); + QUERY PLAN +----------------------------------------- + Seq Scan on graph_table_tests.customers + Output: customers.name +(2 rows) + +-- PROPERTY_NAMES() with shared labels optimization pruning +-- The 'lists' label is shared by 'orders' and 'wishlists', both have same properties (node_id, list_type) +-- Filter by property that exists - both branches should remain +EXPLAIN (VERBOSE, COSTS OFF) +SELECT * FROM GRAPH_TABLE (myshop + MATCH (n IS lists) + WHERE 'node_id' = ANY(PROPERTY_NAMES(n)) + COLUMNS (PROPERTY_NAMES(n) AS props, n.node_id) +); + QUERY PLAN +------------------------------------------------------------------------------------------------ + Append + -> Seq Scan on graph_table_tests.orders + Output: '{order_id,ordered_when,node_id,list_type}'::text[], orders.order_id + -> Seq Scan on graph_table_tests.wishlists + Output: '{node_id,list_type,wishlist_id,wishlist_name}'::text[], wishlists.wishlist_id +(5 rows) + +-- Filter by property that doesn't exist - should show One-Time Filter: false for all branches +EXPLAIN (VERBOSE, COSTS OFF) +SELECT * FROM GRAPH_TABLE (myshop + MATCH (n IS lists) + WHERE 'nonexistent' = ANY(PROPERTY_NAMES(n)) + COLUMNS (PROPERTY_NAMES(n) AS props, n.node_id) +); + QUERY PLAN +------------------------------------------------------ + Result + Output: "graph_table".props, "graph_table".node_id + Replaces: Scan on graph_table + One-Time Filter: false +(4 rows) + +-- PROPERTY_NAMES() with no IS label - match all vertices, filter by property +-- Only tables with 'name' property (customers, products) remain in the plan +EXPLAIN (VERBOSE, COSTS OFF) +SELECT * FROM GRAPH_TABLE (myshop + MATCH (n) + WHERE 'name' = ANY(PROPERTY_NAMES(n)) + COLUMNS (PROPERTY_NAMES(n) AS props) +); + QUERY PLAN +------------------------------------------------------ + Append + -> Seq Scan on graph_table_tests.customers + Output: '{name,customer_id,address}'::text[] + -> Seq Scan on graph_table_tests.products + Output: '{product_no,name,price}'::text[] +(5 rows) + +-- PROPERTY_NAMES() with host variable (parameter) - optimizer cannot fold at plan time +PREPARE prop_test(text) AS +SELECT * FROM GRAPH_TABLE (myshop + MATCH (n) + WHERE $1 = ANY(PROPERTY_NAMES(n)) + COLUMNS (PROPERTY_NAMES(n) AS props) +); +EXPLAIN (VERBOSE, COSTS OFF) EXECUTE prop_test('name'); + QUERY PLAN +------------------------------------------------------ + Append + -> Seq Scan on graph_table_tests.customers + Output: '{name,customer_id,address}'::text[] + -> Seq Scan on graph_table_tests.products + Output: '{product_no,name,price}'::text[] +(5 rows) + +DEALLOCATE prop_test; +-- PROPERTY_NAMES() error: undefined variable +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (PROPERTY_NAMES(undefined_var))); +ERROR: element variable "undefined_var" does not exist +LINE 1: ...op MATCH (c IS customers) COLUMNS (PROPERTY_NAMES(undefined_... + ^ +-- PROPERTY_NAMES() error: no argument +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (PROPERTY_NAMES())); +ERROR: PROPERTY_NAMES() requires exactly one argument +LINE 1: ...APH_TABLE (myshop MATCH (c IS customers) COLUMNS (PROPERTY_N... + ^ +-- PROPERTY_NAMES() error: too many arguments +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (PROPERTY_NAMES(c, c))); +ERROR: PROPERTY_NAMES() requires exactly one argument +LINE 1: ...APH_TABLE (myshop MATCH (c IS customers) COLUMNS (PROPERTY_N... + ^ +-- PROPERTY_NAMES() error: non-variable argument +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (PROPERTY_NAMES(123))); +ERROR: PROPERTY_NAMES() argument must be an element variable +LINE 1: ...APH_TABLE (myshop MATCH (c IS customers) COLUMNS (PROPERTY_N... + ^ -- leave the objects behind for pg_upgrade/pg_dump tests diff --git a/src/test/regress/sql/graph_table.sql b/src/test/regress/sql/graph_table.sql index ba841dd8ab1..3e0b10e39d2 100644 --- a/src/test/regress/sql/graph_table.sql +++ b/src/test/regress/sql/graph_table.sql @@ -663,4 +663,108 @@ SELECT * FROM GRAPH_TABLE (myshop EXPLAIN (VERBOSE, COSTS OFF) EXECUTE labels_test('orders'); DEALLOCATE labels_test; +-- PROPERTY_NAMES() function tests +-- basic PROPERTY_NAMES() in COLUMNS clause +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (c.name, PROPERTY_NAMES(c) AS props)) ORDER BY 1; + +-- PROPERTY_NAMES() for vertices and edges +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers)-[e IS customer_orders]->(o IS orders) COLUMNS (c.name, PROPERTY_NAMES(c) AS cprops, PROPERTY_NAMES(e) AS eprops, PROPERTY_NAMES(o) AS oprops)) ORDER BY 1; + +-- PROPERTY_NAMES() in WHERE clause +SELECT * FROM GRAPH_TABLE (myshop MATCH (c) WHERE 'name' = ANY(PROPERTY_NAMES(c)) COLUMNS (c.name)) ORDER BY 1; + +-- PROPERTY_NAMES() with array functions +SELECT * FROM GRAPH_TABLE (myshop MATCH (c) WHERE array_length(PROPERTY_NAMES(c), 1) >= 1 COLUMNS (c.name, PROPERTY_NAMES(c) AS props)) ORDER BY 1; + +-- PROPERTY_NAMES() with element that has no properties (g3 has no PROPERTIES clauses) +SELECT * FROM GRAPH_TABLE (g3 MATCH (s IS pv) COLUMNS (s.val, PROPERTY_NAMES(s) AS props)) ORDER BY 1; +SELECT * FROM GRAPH_TABLE (g3 MATCH (s)-[e]->(d) COLUMNS (e.val, PROPERTY_NAMES(e) AS edge_props)) ORDER BY 1; + +-- PROPERTY_NAMES() EXPLAIN tests - check query plan generation +EXPLAIN (VERBOSE, COSTS OFF) +SELECT * FROM GRAPH_TABLE (myshop + MATCH (c IS customers) + COLUMNS (c.name, PROPERTY_NAMES(c) AS props) +); + +-- PROPERTY_NAMES() filtering in WHERE clause +EXPLAIN (VERBOSE, COSTS OFF) +SELECT * FROM GRAPH_TABLE (myshop + MATCH (c IS customers) + WHERE 'name' = ANY(PROPERTY_NAMES(c)) + COLUMNS (c.name, PROPERTY_NAMES(c) AS props) +); + +-- PROPERTY_NAMES() filtering outside GRAPH_TABLE (in outer WHERE clause) +EXPLAIN (VERBOSE, COSTS OFF) +SELECT * FROM GRAPH_TABLE (myshop + MATCH (c IS customers) + COLUMNS (c.name, PROPERTY_NAMES(c) AS props) +) WHERE 'address' = ANY(props); + +-- PROPERTY_NAMES() filtering with nonexistent property (should optimize away with One-Time Filter: false) +EXPLAIN (VERBOSE, COSTS OFF) +SELECT * FROM GRAPH_TABLE (myshop + MATCH (c IS customers) + WHERE 'nonexistent_prop' = ANY(PROPERTY_NAMES(c)) + COLUMNS (c.name, PROPERTY_NAMES(c) AS props) +); + +-- PROPERTY_NAMES() filtering with existing property (should NOT optimize away) +EXPLAIN (VERBOSE, COSTS OFF) +SELECT * FROM GRAPH_TABLE (myshop + MATCH (c IS customers) + WHERE 'name' = ANY(PROPERTY_NAMES(c)) + COLUMNS (c.name) +); + +-- PROPERTY_NAMES() with shared labels optimization pruning +-- The 'lists' label is shared by 'orders' and 'wishlists', both have same properties (node_id, list_type) +-- Filter by property that exists - both branches should remain +EXPLAIN (VERBOSE, COSTS OFF) +SELECT * FROM GRAPH_TABLE (myshop + MATCH (n IS lists) + WHERE 'node_id' = ANY(PROPERTY_NAMES(n)) + COLUMNS (PROPERTY_NAMES(n) AS props, n.node_id) +); + +-- Filter by property that doesn't exist - should show One-Time Filter: false for all branches +EXPLAIN (VERBOSE, COSTS OFF) +SELECT * FROM GRAPH_TABLE (myshop + MATCH (n IS lists) + WHERE 'nonexistent' = ANY(PROPERTY_NAMES(n)) + COLUMNS (PROPERTY_NAMES(n) AS props, n.node_id) +); + +-- PROPERTY_NAMES() with no IS label - match all vertices, filter by property +-- Only tables with 'name' property (customers, products) remain in the plan +EXPLAIN (VERBOSE, COSTS OFF) +SELECT * FROM GRAPH_TABLE (myshop + MATCH (n) + WHERE 'name' = ANY(PROPERTY_NAMES(n)) + COLUMNS (PROPERTY_NAMES(n) AS props) +); + +-- PROPERTY_NAMES() with host variable (parameter) - optimizer cannot fold at plan time +PREPARE prop_test(text) AS +SELECT * FROM GRAPH_TABLE (myshop + MATCH (n) + WHERE $1 = ANY(PROPERTY_NAMES(n)) + COLUMNS (PROPERTY_NAMES(n) AS props) +); +EXPLAIN (VERBOSE, COSTS OFF) EXECUTE prop_test('name'); +DEALLOCATE prop_test; + +-- PROPERTY_NAMES() error: undefined variable +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (PROPERTY_NAMES(undefined_var))); + +-- PROPERTY_NAMES() error: no argument +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (PROPERTY_NAMES())); + +-- PROPERTY_NAMES() error: too many arguments +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (PROPERTY_NAMES(c, c))); + +-- PROPERTY_NAMES() error: non-variable argument +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (PROPERTY_NAMES(123))); + -- leave the objects behind for pg_upgrade/pg_dump tests -- 2.50.1 (Apple Git-155)