Hi All, This was earlier discussed in [1], but starting a new thread to discuss only this issue.
We create dependencies between a view and the labels specified in the label expressions in the view definition. An empty label expression resolves to a disjunction between all the labels. But since there is no explicit label mentioned we do not create dependency between any label and the view. Further adding and dropping labels in the property graph changes the element tables that are scanned by the query. That feels awkward - a user who created the view may not be expecting this change. Well, one can have the same effect if an existing label is associated with a new element table - but at least the user is aware of the effect when they add or remove a label to or from the element table. The awkwardness is arguable. When we support all property references things need to change. An all property reference [1] is resolved to all the properties associated with all the labels specified in the element pattern. Since the shape of the view result depends upon the properties references in the COLUMNs clause, an all property reference can not be stored in the view definition as an all property reference. Instead the expanded form of all property reference needs to be stored in the catalog. This is in line with how we store a * in the target list in the view definition. This requires resolving an all property reference during transformation phase itself. This means that we have to resolve an empty label expression during the transformation phase itself. This, in turn, means an empty label expression in the view definition will be stored as disjunction of all labels in the catalogs. As explained earlier, this is different from what we do today and may result in a backward in-compatibility. So I think we should discuss whether we need to fix the empty label expression resolution now and avoid backward compatibility or tackle the problem when we will support all properties reference. Just backporting the changes to query transformation are not enough to fix the backward incompatibility since already created views won't be fixed that way - they will need to be recreated. PFA the patch which fixes the transformation of empty label expression. The patch might need some work. The patch may be considered invasive at this late stage in the PG 19. For that reason I am fine to defer this to PG 20 or whenever we support all properties reference. [1] https://www.postgresql.org/message-id/CAExHW5twGP5Zuk4Zch4kz8XDrSpckWQipMs=ysaj8gmqna2...@mail.gmail.com [2] https://www.postgresql.org/message-id/caexhw5tyce9qycvvrakueeskw5rtr+mrzsg3u64qsps-rpj...@mail.gmail.com -- Best Wishes, Ashutosh Bapat
From f0db156b611125b5080deb3936eb81405aa47089 Mon Sep 17 00:00:00 2001 From: Ashutosh Bapat <[email protected]> Date: Thu, 28 May 2026 02:45:58 +0530 Subject: [PATCH v20260807] Empty label expression in view definition A view definition depends on all the graph labels that are explicitly mentioned in the graph patterns in it so as to avoid it being rendered invalid when any of the labels is dropped. But when a view definition contains an empty label expression, we do not create any dependency between the view and the labels that the empty label expression resolves to. Resolve an empty label expression during transformation phase so that we can create dependency between those labels and the view. This will further help to avoid invalidation of a view containing all-properties references when we support it. An empty label expression may resolve to an empty set of labels if there are not labels associated with the elements matching the kind of element pattern containing the label expression. We do not have a syntax level support for a label expression containing no labels. Hence we can not dump a view containing such an empty label expression. Throw an error when a query contains such an empty label expression. There are possibly no real usecases which use such queries. Author: Ashutosh Bapat <[email protected]> Discussion: https://www.postgresql.org/message-id/CAExHW5twGP5Zuk4Zch4kz8XDrSpckWQipMs=ysaj8gmqna2...@mail.gmail.com --- src/backend/parser/parse_graphtable.c | 110 +++++++++++++++++- src/backend/rewrite/rewriteGraphTable.c | 78 ++++--------- src/include/nodes/parsenodes.h | 8 ++ .../expected/create_property_graph.out | 1 + src/test/regress/expected/graph_table.out | 28 ++++- .../regress/sql/create_property_graph.sql | 1 + src/test/regress/sql/graph_table.sql | 15 ++- 7 files changed, 178 insertions(+), 63 deletions(-) diff --git a/src/backend/parser/parse_graphtable.c b/src/backend/parser/parse_graphtable.c index 73fbfb541f7..5c9da75c8cf 100644 --- a/src/backend/parser/parse_graphtable.c +++ b/src/backend/parser/parse_graphtable.c @@ -18,6 +18,8 @@ #include "access/genam.h" #include "access/htup_details.h" #include "access/table.h" +#include "catalog/pg_propgraph_element.h" +#include "catalog/pg_propgraph_element_label.h" #include "catalog/pg_propgraph_label.h" #include "catalog/pg_propgraph_property.h" #include "miscadmin.h" @@ -151,6 +153,51 @@ transformGraphTablePropertyRef(ParseState *pstate, ColumnRef *cref) return NULL; } +/* + * Given the OID of a label and the kind of graph element pattern, return true if + * there exists at least one element matching the given kind associated with the + * label. Otherwise return false. + */ +static bool +label_has_elements_of_kind(Oid labelid, GraphElementPatternKind gepkind) +{ + Relation rel; + SysScanDesc scan; + ScanKeyData key[1]; + HeapTuple tup; + bool result = false; + + rel = table_open(PropgraphElementLabelRelationId, AccessShareLock); + ScanKeyInit(&key[0], + Anum_pg_propgraph_element_label_pgellabelid, + BTEqualStrategyNumber, + F_OIDEQ, ObjectIdGetDatum(labelid)); + scan = systable_beginscan(rel, PropgraphElementLabelLabelIndexId, + true, NULL, 1, key); + while (!result && HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_propgraph_element_label element_label = (Form_pg_propgraph_element_label) GETSTRUCT(tup); + Oid element_oid = element_label->pgelelid; + HeapTuple element_tup = SearchSysCache1(PROPGRAPHELOID, ObjectIdGetDatum(element_oid)); + Form_pg_propgraph_element element_form; + + if (!HeapTupleIsValid(element_tup)) + elog(ERROR, "cache lookup failed for property graph element %u", element_oid); + + element_form = (Form_pg_propgraph_element) GETSTRUCT(element_tup); + + if ((element_form->pgekind == PGEKIND_VERTEX && gepkind == VERTEX_PATTERN) || + (element_form->pgekind == PGEKIND_EDGE && IS_EDGE_PATTERN(gepkind))) + result = true; + + ReleaseSysCache(element_tup); + } + + systable_endscan(scan); + table_close(rel, AccessShareLock); + return result; +} + /* * Transform a label expression. * @@ -161,14 +208,66 @@ transformGraphTablePropertyRef(ParseState *pstate, ColumnRef *cref) * GraphLabelRef nodes corresponding to the names of the labels appearing in the * expression. If any label name cannot be resolved to a label in the property * graph, an error is raised. + * + * An empty label expression is treated as a special case. According to section + * 9.2 "Contextual inference of a set of labels" subclause 2.a.ii of SQL/PGQ + * standard, element pattern which does not have a label expression is + * considered to have label expression equivalent to '%|!%' which is set of all + * labels which have at least one element of the given element kind associated with it. */ static Node * -transformLabelExpr(GraphTableParseState *gpstate, Node *labelexpr) +transformLabelExpr(GraphTableParseState *gpstate, Node *labelexpr, GraphElementPatternKind gepkind) { Node *result; - if (labelexpr == NULL) - return NULL; + if (!labelexpr) + { + Relation rel; + SysScanDesc scan; + ScanKeyData key[1]; + HeapTuple tup; + List *args = NIL; + + rel = table_open(PropgraphLabelRelationId, AccessShareLock); + ScanKeyInit(&key[0], + Anum_pg_propgraph_label_pglpgid, + BTEqualStrategyNumber, + F_OIDEQ, ObjectIdGetDatum(gpstate->graphid)); + scan = systable_beginscan(rel, PropgraphLabelGraphNameIndexId, + true, NULL, 1, key); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_propgraph_label label = (Form_pg_propgraph_label) GETSTRUCT(tup); + GraphLabelRef *lref; + + if (!label_has_elements_of_kind(label->oid, gepkind)) + continue; + + lref = makeNode(GraphLabelRef); + lref->labelid = label->oid; + lref->location = -1; + args = lappend(args, lref); + } + systable_endscan(scan); + table_close(rel, AccessShareLock); + + /* + * If there are no labels with elements of the given kind, the set of + * labels that this label expression resolves to is empty. There is no + * way to represent an empty set of labels as a label expression since + * we do not support label conjunction as well as negation. So we can + * not dump a view containing such a label expression. Hence prohibit + * it for now. + */ + if (!args) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("empty label expression does not resolve to any label")); + + result = (Node *) makeBoolExpr(OR_EXPR, args, -1); + return result; + + } check_stack_depth(); @@ -208,7 +307,7 @@ transformLabelExpr(GraphTableParseState *gpstate, Node *labelexpr) { Node *arg = (Node *) lfirst(lc); - arg = transformLabelExpr(gpstate, arg); + arg = transformLabelExpr(gpstate, arg, gepkind); args = lappend(args, arg); } @@ -249,7 +348,8 @@ transformGraphElementPattern(ParseState *pstate, GraphElementPattern *gep) gpstate->cur_gep = gep; - gep->labelexpr = transformLabelExpr(gpstate, gep->labelexpr); + gep->has_empty_labelexpr = !gep->labelexpr; + gep->labelexpr = transformLabelExpr(gpstate, gep->labelexpr, gep->kind); gep->whereClause = transformExpr(pstate, gep->whereClause, EXPR_KIND_WHERE); diff --git a/src/backend/rewrite/rewriteGraphTable.c b/src/backend/rewrite/rewriteGraphTable.c index cdb1f4c0dca..a5b1be77a0a 100644 --- a/src/backend/rewrite/rewriteGraphTable.c +++ b/src/backend/rewrite/rewriteGraphTable.c @@ -58,6 +58,8 @@ struct path_factor { GraphElementPatternKind kind; const char *variable; + bool has_empty_labelexpr; /* Copied from the corresponding + * GraphElementPattern */ Node *labelexpr; Node *whereClause; int factorpos; /* Position of this path factor in the list of @@ -221,9 +223,12 @@ generate_queries_for_path_pattern(RangeTblEntry *rte, List *path_pattern) * expression itself. Hence if only one of the two element * patterns has a label expression use that expression. */ - if (!other->labelexpr) + if (other->has_empty_labelexpr) + { other->labelexpr = gep->labelexpr; - else if (gep->labelexpr && !equal(other->labelexpr, gep->labelexpr)) + other->has_empty_labelexpr = gep->has_empty_labelexpr; + } + else if (!gep->has_empty_labelexpr && !equal(other->labelexpr, gep->labelexpr)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("element patterns with same variable name \"%s\" but different label expressions are not supported", @@ -250,6 +255,7 @@ generate_queries_for_path_pattern(RangeTblEntry *rte, List *path_pattern) pf = palloc0_object(struct path_factor); pf->factorpos = factorpos++; pf->kind = gep->kind; + pf->has_empty_labelexpr = gep->has_empty_labelexpr; pf->labelexpr = gep->labelexpr; pf->variable = gep->variable; pf->whereClause = gep->whereClause; @@ -830,37 +836,9 @@ get_labels_for_expr(Oid propgraphid, Node *labelexpr) { List *label_oids; - if (!labelexpr) - { - Relation rel; - SysScanDesc scan; - ScanKeyData key[1]; - HeapTuple tup; - - /* - * According to section 9.2 "Contextual inference of a set of labels" - * subclause 2.a.ii of SQL/PGQ standard, element pattern which does - * not have a label expression is considered to have label expression - * equivalent to '%|!%' which is set of all labels. - */ - label_oids = NIL; - rel = table_open(PropgraphLabelRelationId, AccessShareLock); - ScanKeyInit(&key[0], - Anum_pg_propgraph_label_pglpgid, - BTEqualStrategyNumber, - F_OIDEQ, ObjectIdGetDatum(propgraphid)); - scan = systable_beginscan(rel, PropgraphLabelGraphNameIndexId, - true, NULL, 1, key); - while (HeapTupleIsValid(tup = systable_getnext(scan))) - { - Form_pg_propgraph_label label = (Form_pg_propgraph_label) GETSTRUCT(tup); + Assert(labelexpr); - label_oids = lappend_oid(label_oids, label->oid); - } - systable_endscan(scan); - table_close(rel, AccessShareLock); - } - else if (IsA(labelexpr, GraphLabelRef)) + if (IsA(labelexpr, GraphLabelRef)) { GraphLabelRef *glr = castNode(GraphLabelRef, labelexpr); @@ -910,7 +888,6 @@ get_path_elements_for_path_factor(Oid propgraphid, struct path_factor *pf) List *elem_oids_seen = NIL; List *pf_elem_oids = NIL; List *path_elements = NIL; - List *unresolved_labels = NIL; Relation rel; SysScanDesc scan; ScanKeyData key[1]; @@ -977,33 +954,26 @@ get_path_elements_for_path_factor(Oid propgraphid, struct path_factor *pf) { /* * We did not find any qualified element associated with this - * label. The label or its properties can not be associated with - * the given element pattern. Throw an error if the label was - * explicitly specified in the element pattern. Otherwise remember - * it for later use. + * label. Throw an error. + * + * An empty label expression is replaced by all labels that are + * associated with at least one element of the required kind. We + * should not reach here in that case. */ - if (!pf->labelexpr) - unresolved_labels = lappend_oid(unresolved_labels, labeloid); - else - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("no property graph element of type \"%s\" has label \"%s\" associated with it in property graph \"%s\"", - pf->kind == VERTEX_PATTERN ? "vertex" : "edge", - get_propgraph_label_name(labeloid), - get_rel_name(propgraphid)))); + Assert(!pf->has_empty_labelexpr); + + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("no property graph element of type \"%s\" has label \"%s\" associated with it in property graph \"%s\"", + pf->kind == VERTEX_PATTERN ? "vertex" : "edge", + get_propgraph_label_name(labeloid), + get_rel_name(propgraphid)))); } systable_endscan(scan); } table_close(rel, AccessShareLock); - - /* - * Remove the labels which were not explicitly mentioned in the label - * expression but do not have any qualified elements associated with them. - * Properties associated with such labels may not be referenced. See - * replace_property_refs_mutator() for more details. - */ - pf->labeloids = list_difference_oid(label_oids, unresolved_labels); + pf->labeloids = label_oids; return path_elements; } diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 5c8f9a07b62..b98a0758a7b 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1050,7 +1050,15 @@ typedef struct GraphElementPattern NodeTag type; GraphElementPatternKind kind; const char *variable; + + /* + * If no label expression is specified, we will replace it with a non-NULL + * expression in transformLabelExpr(). This flag indicates whether the + * label expression was originally empty. + */ + bool has_empty_labelexpr; Node *labelexpr; + List *subexpr; Node *whereClause; List *quantifier; diff --git a/src/test/regress/expected/create_property_graph.out b/src/test/regress/expected/create_property_graph.out index 646e5fed5e2..68c4accf2ef 100644 --- a/src/test/regress/expected/create_property_graph.out +++ b/src/test/regress/expected/create_property_graph.out @@ -977,6 +977,7 @@ DETAIL: Table "v2tmp" is a temporary table. DROP TABLE g2; -- error: wrong object type ERROR: "g2" is not a table HINT: Use DROP PROPERTY GRAPH to remove a property graph. +ALTER PROPERTY GRAPH g1 ADD VERTEX TABLES (t1 KEY (a)); -- to make a valid graph query CREATE VIEW vg1 AS SELECT * FROM GRAPH_TABLE(g1 MATCH () COLUMNS (1 AS one)); DROP PROPERTY GRAPH g1; -- error ERROR: cannot drop property graph g1 because other objects depend on it diff --git a/src/test/regress/expected/graph_table.out b/src/test/regress/expected/graph_table.out index cde3114ebf4..51b2b1e9d6d 100644 --- a/src/test/regress/expected/graph_table.out +++ b/src/test/regress/expected/graph_table.out @@ -872,6 +872,8 @@ EXECUTE loopstmt; (2 rows) -- inheritance and partitioning +-- +-- Also test temporary property graphs, keywords NODE and RELATIONSHIP CREATE TABLE pv (id int, val int); CREATE TABLE cv1 () INHERITS (pv); CREATE TABLE cv2 () INHERITS (pv); @@ -941,8 +943,12 @@ CREATE TABLE ptne1 PARTITION OF ptne FOR VALUES IN (1, 2); CREATE TABLE ptne2 PARTITION OF ptne FOR VALUES IN (3); INSERT INTO ptne VALUES (1, 1, 2, 100), (2, 2, 3, 200), (3, 3, 1, 300); CREATE PROPERTY GRAPH g4 - VERTEX TABLES (ptnv) - EDGE TABLES ( + VERTEX TABLES (ptnv); +-- empty label expression which resolves to no labels +SELECT * FROM GRAPH_TABLE (g4 MATCH (s is ptnv)-[e]-(d is ptnv) COLUMNS (s.val, e.val, d.val)) ORDER BY 1, 2, 3; -- error +ERROR: empty label expression does not resolve to any label +ALTER PROPERTY GRAPH g4 + ADD EDGE TABLES ( ptne SOURCE KEY (src) REFERENCES ptnv(id) DESTINATION KEY (dest) REFERENCES ptnv(id) @@ -999,6 +1005,17 @@ ALTER PROPERTY GRAPH myshop ALTER VERTEX TABLE products ERROR: cannot drop property price of property graph myshop because other objects depend on it DETAIL: view customers_us depends on property price of property graph myshop HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- Empty label expression creates a dependency between the view and the set of +-- labels it resolves to +CREATE VIEW v_empty_label AS SELECT * FROM GRAPH_TABLE (g1 MATCH (v WHERE v.vprop1 = 10) COLUMNS (v.elname)); +ALTER PROPERTY GRAPH g1 ALTER VERTEX TABLE v1 DROP LABEL vl1; -- error +ERROR: cannot drop label vl1 of property graph g1 because other objects depend on it +DETAIL: view v_empty_label depends on label vl1 of property graph g1 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +ALTER PROPERTY GRAPH g1 ALTER VERTEX TABLE v2 DROP LABEL vl2; -- error +ERROR: cannot drop label vl2 of property graph g1 because other objects depend on it +DETAIL: view v_empty_label depends on label vl2 of property graph g1 +HINT: Use DROP ... CASCADE to drop the dependent objects too. -- ruleutils reverse parsing SELECT pg_get_viewdef('customers_us'::regclass); pg_get_viewdef @@ -1012,6 +1029,13 @@ SELECT pg_get_viewdef('customers_us'::regclass); ORDER BY g.customer_name, g.product_name; (1 row) +SELECT pg_get_viewdef('v_empty_label'::regclass); + pg_get_viewdef +---------------------------------------------------------------------------------------------------------- + SELECT elname + + FROM GRAPH_TABLE (g1 MATCH (v IS l1|vl1|vl2|vl3 WHERE (v.vprop1 = 10)) COLUMNS (v.elname AS elname)); +(1 row) + -- test view/graph nesting CREATE VIEW customers_view AS SELECT customer_id, 'redacted' || customer_id AS name_redacted, address FROM customers; SELECT * FROM customers; diff --git a/src/test/regress/sql/create_property_graph.sql b/src/test/regress/sql/create_property_graph.sql index b1a8d12a040..9f689b49200 100644 --- a/src/test/regress/sql/create_property_graph.sql +++ b/src/test/regress/sql/create_property_graph.sql @@ -388,6 +388,7 @@ ALTER PROPERTY GRAPH g1 -- DROP, ALTER SET SCHEMA, ALTER PROPERTY GRAPH RENAME TO DROP TABLE g2; -- error: wrong object type +ALTER PROPERTY GRAPH g1 ADD VERTEX TABLES (t1 KEY (a)); -- to make a valid graph query CREATE VIEW vg1 AS SELECT * FROM GRAPH_TABLE(g1 MATCH () COLUMNS (1 AS one)); DROP PROPERTY GRAPH g1; -- error ALTER PROPERTY GRAPH g1 SET SCHEMA create_property_graph_tests_2; diff --git a/src/test/regress/sql/graph_table.sql b/src/test/regress/sql/graph_table.sql index 7a4189833d8..59c6663a923 100644 --- a/src/test/regress/sql/graph_table.sql +++ b/src/test/regress/sql/graph_table.sql @@ -483,6 +483,8 @@ ALTER PROPERTY GRAPH g1 ALTER EDGE TABLE e3_3 ALTER LABEL l2 ADD PROPERTIES ((en EXECUTE loopstmt; -- inheritance and partitioning +-- +-- Also test temporary property graphs, keywords NODE and RELATIONSHIP CREATE TABLE pv (id int, val int); CREATE TABLE cv1 () INHERITS (pv); CREATE TABLE cv2 () INHERITS (pv); @@ -529,8 +531,11 @@ CREATE TABLE ptne1 PARTITION OF ptne FOR VALUES IN (1, 2); CREATE TABLE ptne2 PARTITION OF ptne FOR VALUES IN (3); INSERT INTO ptne VALUES (1, 1, 2, 100), (2, 2, 3, 200), (3, 3, 1, 300); CREATE PROPERTY GRAPH g4 - VERTEX TABLES (ptnv) - EDGE TABLES ( + VERTEX TABLES (ptnv); +-- empty label expression which resolves to no labels +SELECT * FROM GRAPH_TABLE (g4 MATCH (s is ptnv)-[e]-(d is ptnv) COLUMNS (s.val, e.val, d.val)) ORDER BY 1, 2, 3; -- error +ALTER PROPERTY GRAPH g4 + ADD EDGE TABLES ( ptne SOURCE KEY (src) REFERENCES ptnv(id) DESTINATION KEY (dest) REFERENCES ptnv(id) @@ -561,8 +566,14 @@ ALTER PROPERTY GRAPH myshop ALTER VERTEX TABLE customers ALTER LABEL customers DROP PROPERTIES (address); -- error ALTER PROPERTY GRAPH myshop ALTER VERTEX TABLE products ALTER LABEL products DROP PROPERTIES (price); -- error +-- Empty label expression creates a dependency between the view and the set of +-- labels it resolves to +CREATE VIEW v_empty_label AS SELECT * FROM GRAPH_TABLE (g1 MATCH (v WHERE v.vprop1 = 10) COLUMNS (v.elname)); +ALTER PROPERTY GRAPH g1 ALTER VERTEX TABLE v1 DROP LABEL vl1; -- error +ALTER PROPERTY GRAPH g1 ALTER VERTEX TABLE v2 DROP LABEL vl2; -- error -- ruleutils reverse parsing SELECT pg_get_viewdef('customers_us'::regclass); +SELECT pg_get_viewdef('v_empty_label'::regclass); -- test view/graph nesting base-commit: 0ec3f048bfc15c8eb9933e8228b847593389da1b -- 2.34.1
