v20260512-0001-Prevent-dropping-the-last-label-from-a-pro.patch
text/x-patch
Filename: v20260512-0001-Prevent-dropping-the-last-label-from-a-pro.patch
Type: text/x-patch
Part: 0
From dafd688d335d417c7c1e05418646000fb635d87f Mon Sep 17 00:00:00 2001
From: satyanarayana narlapuram <satyanarlapuram@gmail.com>
Date: Tue, 21 Apr 2026 07:52:56 +0000
Subject: [PATCH v20260512 1/4] Prevent dropping the last label from a property
graph element
Per SQL/PGQ standard, every graph element must have at least one label. When
dropping a label from a graph element, ensure that there exists at least one
other label on the element. If the label being dropped is the only label on the
element, raise an error.
Add a test to make sure that dropping labels concurrently from the same
element has the same effect.
Reported By: Satyanarayana Narlapuram <satyanarlapuram@gmail.com>
Author: Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>
Author: Satyanarayana Narlapuram <satyanarlapuram@gmail.com>
Reviewed by: Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>
Discussion: https://www.postgresql.org/message-id/CAHg+QDeP=mTHTV48R23zKMy1SBmCKZ_L7-z5zKnYyw+K0x-gCg@mail.gmail.com
---
doc/src/sgml/ref/alter_property_graph.sgml | 4 +-
src/backend/commands/propgraphcmds.c | 54 +++++++++++++++++--
.../drop-propgraph-label-concurrently.out | 20 +++++++
src/test/isolation/isolation_schedule | 1 +
.../drop-propgraph-label-concurrently.spec | 36 +++++++++++++
.../expected/create_property_graph.out | 6 +++
.../regress/sql/create_property_graph.sql | 4 ++
7 files changed, 119 insertions(+), 6 deletions(-)
create mode 100644 src/test/isolation/expected/drop-propgraph-label-concurrently.out
create mode 100644 src/test/isolation/specs/drop-propgraph-label-concurrently.spec
diff --git a/doc/src/sgml/ref/alter_property_graph.sgml b/doc/src/sgml/ref/alter_property_graph.sgml
index f517f2b2d7a..2fd0c138e12 100644
--- a/doc/src/sgml/ref/alter_property_graph.sgml
+++ b/doc/src/sgml/ref/alter_property_graph.sgml
@@ -99,7 +99,9 @@ ALTER PROPERTY GRAPH [ IF EXISTS ] <replaceable class="parameter">name</replacea
<term><literal>ALTER {VERTEX|NODE|EDGE|RELATIONSHIP} TABLE ... DROP LABEL</literal></term>
<listitem>
<para>
- This form removes a label from an existing vertex or edge table.
+ This form removes a label from an existing vertex or edge table. The
+ last label on an element table cannot be dropped; every vertex or edge
+ table must have at least one label.
</para>
</listitem>
</varlistentry>
diff --git a/src/backend/commands/propgraphcmds.c b/src/backend/commands/propgraphcmds.c
index cc516e27020..59b2f76ca4f 100644
--- a/src/backend/commands/propgraphcmds.c
+++ b/src/backend/commands/propgraphcmds.c
@@ -1491,8 +1491,14 @@ AlterPropGraph(ParseState *pstate, const AlterPropGraphStmt *stmt)
{
Oid peoid;
Oid labeloid;
- Oid ellabeloid;
+ Oid ellabeloid = InvalidOid;
ObjectAddress obj;
+ Relation ellabelrel;
+ SysScanDesc ellabelscan;
+ ScanKeyData ellabelkey[1];
+ int nlabels = 0;
+ HeapTuple tuple;
+
if (stmt->element_kind == PROPGRAPH_ELEMENT_KIND_VERTEX)
peoid = get_vertex_oid(pstate, pgrelid, stmt->element_alias, -1);
@@ -1510,11 +1516,38 @@ AlterPropGraph(ParseState *pstate, const AlterPropGraphStmt *stmt)
get_rel_name(pgrelid), stmt->element_alias, stmt->drop_label),
parser_errposition(pstate, -1));
- ellabeloid = GetSysCacheOid2(PROPGRAPHELEMENTLABELELEMENTLABEL,
- Anum_pg_propgraph_element_label_oid,
- ObjectIdGetDatum(peoid),
- ObjectIdGetDatum(labeloid));
+ /*
+ * Is the given label associated with the element? Is this the only
+ * label associated with the element?
+ */
+ ellabelrel = table_open(PropgraphElementLabelRelationId, AccessShareLock);
+ ScanKeyInit(&ellabelkey[0],
+ Anum_pg_propgraph_element_label_pgelelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(peoid));
+ ellabelscan = systable_beginscan(ellabelrel, PropgraphElementLabelElementLabelIndexId,
+ true, NULL, 1, ellabelkey);
+ while (HeapTupleIsValid(tuple = systable_getnext(ellabelscan)))
+ {
+ Form_pg_propgraph_element_label ellabelform = (Form_pg_propgraph_element_label) GETSTRUCT(tuple);
+
+ nlabels++;
+ if (ellabelform->pgellabelid == labeloid)
+ ellabeloid = ellabelform->oid;
+
+ /*
+ * If we find more than one label associated with the given
+ * element and also found the label we are going to drop, we are
+ * done.
+ */
+ if (nlabels > 1 && ellabeloid)
+ break;
+ }
+ systable_endscan(ellabelscan);
+ table_close(ellabelrel, AccessShareLock);
+
+ /* Given label is not associated with the element. */
if (!ellabeloid)
ereport(ERROR,
errcode(ERRCODE_UNDEFINED_OBJECT),
@@ -1522,6 +1555,17 @@ AlterPropGraph(ParseState *pstate, const AlterPropGraphStmt *stmt)
get_rel_name(pgrelid), stmt->element_alias, stmt->drop_label),
parser_errposition(pstate, -1));
+ /*
+ * Prevent dropping the last label from an element. Every element must
+ * have at least one label.
+ */
+ if (nlabels == 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("cannot drop the last label from element \"%s\"",
+ stmt->element_alias),
+ errhint("Every element must have at least one label.")));
+
ObjectAddressSet(obj, PropgraphElementLabelRelationId, ellabeloid);
performDeletion(&obj, stmt->drop_behavior, 0);
diff --git a/src/test/isolation/expected/drop-propgraph-label-concurrently.out b/src/test/isolation/expected/drop-propgraph-label-concurrently.out
new file mode 100644
index 00000000000..a3fc89fbac1
--- /dev/null
+++ b/src/test/isolation/expected/drop-propgraph-label-concurrently.out
@@ -0,0 +1,20 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1b s1drop1 s2b s2drop2 s1c s2c
+step s1b: BEGIN;
+step s1drop1: ALTER PROPERTY GRAPH pgg1 ALTER VERTEX TABLE pgt1 DROP LABEL pgl1;
+step s2b: BEGIN;
+step s2drop2: ALTER PROPERTY GRAPH pgg1 ALTER VERTEX TABLE pgt1 DROP LABEL pgl2; <waiting ...>
+step s1c: COMMIT;
+step s2drop2: <... completed>
+ERROR: cannot drop the last label from element "pgt1"
+step s2c: COMMIT;
+
+starting permutation: s1b s1drop1 s2b s2drop2 s1r s2c
+step s1b: BEGIN;
+step s1drop1: ALTER PROPERTY GRAPH pgg1 ALTER VERTEX TABLE pgt1 DROP LABEL pgl1;
+step s2b: BEGIN;
+step s2drop2: ALTER PROPERTY GRAPH pgg1 ALTER VERTEX TABLE pgt1 DROP LABEL pgl2; <waiting ...>
+step s1r: ROLLBACK;
+step s2drop2: <... completed>
+step s2c: COMMIT;
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 1578ba191c8..9c64ee43641 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -126,3 +126,4 @@ test: serializable-parallel-3
test: matview-write-skew
test: lock-nowait
test: for-portion-of
+test: drop-propgraph-label-concurrently
diff --git a/src/test/isolation/specs/drop-propgraph-label-concurrently.spec b/src/test/isolation/specs/drop-propgraph-label-concurrently.spec
new file mode 100644
index 00000000000..ce8f4351364
--- /dev/null
+++ b/src/test/isolation/specs/drop-propgraph-label-concurrently.spec
@@ -0,0 +1,36 @@
+# ALTER PROPERTY GRAPH ... DROP LABEL - concurrent
+#
+# Verify that two concurrent transactions cannot both drop a label from
+# the same element such that the element ends up with no labels.
+
+setup
+{
+ CREATE TABLE pgt1 (a int, b int);
+ CREATE PROPERTY GRAPH pgg1
+ VERTEX TABLES (pgt1 KEY (a)
+ LABEL pgl1 PROPERTIES (b)
+ LABEL pgl2 PROPERTIES (a, b));
+}
+
+teardown
+{
+ DROP PROPERTY GRAPH IF EXISTS pgg1;
+ DROP TABLE pgt1;
+}
+
+session s1
+step s1b { BEGIN; }
+step s1drop1 { ALTER PROPERTY GRAPH pgg1 ALTER VERTEX TABLE pgt1 DROP LABEL pgl1; }
+step s1c { COMMIT; }
+step s1r { ROLLBACK; }
+
+session s2
+step s2b { BEGIN; }
+step s2drop2 { ALTER PROPERTY GRAPH pgg1 ALTER VERTEX TABLE pgt1 DROP LABEL pgl2; }
+step s2c { COMMIT; }
+
+# s2drop2 fails since by then pgl2 is the only remaining label on pgt1
+permutation s1b s1drop1 s2b s2drop2 s1c s2c
+
+# s2drop2 succeeds since rollback leaves pgl1 behind
+permutation s1b s1drop1 s2b s2drop2 s1r s2c
diff --git a/src/test/regress/expected/create_property_graph.out b/src/test/regress/expected/create_property_graph.out
index f625524abce..dd6c67167e5 100644
--- a/src/test/regress/expected/create_property_graph.out
+++ b/src/test/regress/expected/create_property_graph.out
@@ -57,6 +57,12 @@ ALTER PROPERTY GRAPH g3
ALTER PROPERTY GRAPH g3 ALTER VERTEX TABLE t3 DROP LABEL t3l3x; -- error
ERROR: property graph "g3" element "t3" has no label "t3l3x"
ALTER PROPERTY GRAPH g3 ALTER VERTEX TABLE t3 DROP LABEL t3l3;
+-- Test that the last label on an element cannot be dropped
+ALTER PROPERTY GRAPH g3 ALTER VERTEX TABLE t3 DROP LABEL t3l2;
+ALTER PROPERTY GRAPH g3 ALTER VERTEX TABLE t3 DROP LABEL t3l1; -- error: last label
+ERROR: cannot drop the last label from element "t3"
+HINT: Every element must have at least one label.
+ALTER PROPERTY GRAPH g3 ALTER VERTEX TABLE t3 ADD LABEL t3l2 PROPERTIES ALL COLUMNS;
ALTER PROPERTY GRAPH g3 DROP VERTEX TABLES (t2); -- fail
ERROR: cannot drop vertex t2 of property graph g3 because other objects depend on it
DETAIL: edge e1 of property graph g3 depends on vertex t2 of property graph g3
diff --git a/src/test/regress/sql/create_property_graph.sql b/src/test/regress/sql/create_property_graph.sql
index 241f93df302..4cf771596a8 100644
--- a/src/test/regress/sql/create_property_graph.sql
+++ b/src/test/regress/sql/create_property_graph.sql
@@ -52,6 +52,10 @@ ALTER PROPERTY GRAPH g3
ADD LABEL t3l3 PROPERTIES ALL COLUMNS;
ALTER PROPERTY GRAPH g3 ALTER VERTEX TABLE t3 DROP LABEL t3l3x; -- error
ALTER PROPERTY GRAPH g3 ALTER VERTEX TABLE t3 DROP LABEL t3l3;
+-- Test that the last label on an element cannot be dropped
+ALTER PROPERTY GRAPH g3 ALTER VERTEX TABLE t3 DROP LABEL t3l2;
+ALTER PROPERTY GRAPH g3 ALTER VERTEX TABLE t3 DROP LABEL t3l1; -- error: last label
+ALTER PROPERTY GRAPH g3 ALTER VERTEX TABLE t3 ADD LABEL t3l2 PROPERTIES ALL COLUMNS;
ALTER PROPERTY GRAPH g3 DROP VERTEX TABLES (t2); -- fail
ALTER PROPERTY GRAPH g3 DROP VERTEX TABLES (t2) CASCADE;
ALTER PROPERTY GRAPH g3 DROP EDGE TABLES (e2);
base-commit: 8268e41aca23ae3414360b0a1dc6ae99ea7b43f4
--
2.34.1