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

