This is an automated email from the ASF dual-hosted git repository.

jgemignani pushed a commit to branch Dev_Multiple_Labels
in repository https://gitbox.apache.org/repos/asf/age.git


The following commit(s) were added to refs/heads/Dev_Multiple_Labels by this 
push:
     new 75ce99fc Add SET n:Label and REMOVE n:Label (#2281)
75ce99fc is described below

commit 75ce99fca916bfc35332f0ff11aac19577ed5084
Author: John Gemignani <[email protected]>
AuthorDate: Sat Dec 20 12:22:07 2025 -0800

    Add SET n:Label and REMOVE n:Label (#2281)
    
    NOTE: This PR was built with AI tools and verified by a human.
    
    Implements Cypher SET and REMOVE operations for vertex labels in the unified
    vertex table architecture. This allows dynamic label management on vertices.
    
    SET n:Label
    
    * Only works on vertices with no label.
    
    * Auto-creates the label if it doesn't exist.
    
    * Errors with hint if vertex already has a label:
    "Multiple labels are not supported. Use REMOVE to clear the label first."
    
    REMOVE n:Label
    
    * Removes a vertex's specified label.
    
    * Properties are preserved
    
    * No-op if vertex already has no label
    
    Added regression tests.
    
    modified:   regress/expected/cypher_remove.out
    modified:   regress/expected/unified_vertex_table.out
    modified:   regress/sql/unified_vertex_table.sql
    modified:   src/backend/executor/cypher_set.c
    modified:   src/backend/nodes/cypher_copyfuncs.c
    modified:   src/backend/nodes/cypher_outfuncs.c
    modified:   src/backend/nodes/cypher_readfuncs.c
    modified:   src/backend/parser/cypher_clause.c
    modified:   src/backend/parser/cypher_gram.y
    modified:   src/include/nodes/cypher_nodes.h
---
 regress/expected/cypher_remove.out        |   2 +-
 regress/expected/unified_vertex_table.out | 449 +++++++++++++++++++++++++++++-
 regress/sql/unified_vertex_table.sql      | 286 +++++++++++++++++++
 src/backend/executor/cypher_set.c         | 147 ++++++++++
 src/backend/nodes/cypher_copyfuncs.c      |   3 +
 src/backend/nodes/cypher_outfuncs.c       |   4 +
 src/backend/nodes/cypher_readfuncs.c      |   2 +
 src/backend/parser/cypher_clause.c        | 431 ++++++++++++++++------------
 src/backend/parser/cypher_gram.y          |  46 +++
 src/include/nodes/cypher_nodes.h          |   4 +
 10 files changed, 1200 insertions(+), 174 deletions(-)

diff --git a/regress/expected/cypher_remove.out 
b/regress/expected/cypher_remove.out
index 6aaeeea8..6b681605 100644
--- a/regress/expected/cypher_remove.out
+++ b/regress/expected/cypher_remove.out
@@ -463,7 +463,7 @@ ERROR:  REMOVE cannot be the first clause in a Cypher query
 LINE 1: SELECT * FROM cypher('cypher_remove', $$REMOVE n.i$$) AS (a ...
                                                 ^
 SELECT * FROM cypher('cypher_remove', $$MATCH (n) REMOVE n.i = NULL$$) AS (a 
agtype);
-ERROR:  REMOVE clause must be in the format: REMOVE variable.property_name
+ERROR:  REMOVE clause must be in the format: REMOVE variable.property_name or 
REMOVE variable:Label
 LINE 1: SELECT * FROM cypher('cypher_remove', $$MATCH (n) REMOVE n.i...
                                                 ^
 SELECT * FROM cypher('cypher_remove', $$MATCH (n) REMOVE wrong_var.i$$) AS (a 
agtype);
diff --git a/regress/expected/unified_vertex_table.out 
b/regress/expected/unified_vertex_table.out
index b6037b61..58a3f1e3 100644
--- a/regress/expected/unified_vertex_table.out
+++ b/regress/expected/unified_vertex_table.out
@@ -787,11 +787,443 @@ $$) AS (cnt agtype);
  0
 (1 row)
 
+--
+-- Test 18: SET label operation - error when vertex already has a label
+-- Multiple labels are not supported. SET only works on unlabeled vertices.
+--
+SELECT * FROM cypher('unified_test', $$
+    CREATE (:OldLabel {id: 1, name: 'vertex1'})
+$$) AS (v agtype);
+ v 
+---
+(0 rows)
+
+SELECT * FROM cypher('unified_test', $$
+    CREATE (:OldLabel {id: 2, name: 'vertex2'})
+$$) AS (v agtype);
+ v 
+---
+(0 rows)
+
+-- Verify initial label
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:OldLabel)
+    RETURN n.id, n.name, label(n) ORDER BY n.id
+$$) AS (id agtype, name agtype, lbl agtype);
+ id |   name    |    lbl     
+----+-----------+------------
+ 1  | "vertex1" | "OldLabel"
+ 2  | "vertex2" | "OldLabel"
+(2 rows)
+
+-- Try to change label on vertex1 - should FAIL because it already has a label
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:OldLabel {id: 1})
+    SET n:NewLabel
+    RETURN n.id, n.name, label(n)
+$$) AS (id agtype, name agtype, lbl agtype);
+ERROR:  SET label failed: vertex already has label "OldLabel"
+HINT:  Multiple labels are not supported. Use REMOVE to clear the label first.
+-- Verify vertex1 still has OldLabel (unchanged due to error)
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:OldLabel)
+    RETURN n.id, n.name, label(n) ORDER BY n.id
+$$) AS (id agtype, name agtype, lbl agtype);
+ id |   name    |    lbl     
+----+-----------+------------
+ 1  | "vertex1" | "OldLabel"
+ 2  | "vertex2" | "OldLabel"
+(2 rows)
+
+--
+-- Test 19: REMOVE label operation
+-- This tests removing a vertex's label using REMOVE n:Label syntax
+--
+SELECT * FROM cypher('unified_test', $$
+    CREATE (:RemoveTest {id: 1, data: 'test1'})
+$$) AS (v agtype);
+ v 
+---
+(0 rows)
+
+SELECT * FROM cypher('unified_test', $$
+    CREATE (:RemoveTest {id: 2, data: 'test2'})
+$$) AS (v agtype);
+ v 
+---
+(0 rows)
+
+-- Verify initial label
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:RemoveTest)
+    RETURN n.id, n.data, label(n) ORDER BY n.id
+$$) AS (id agtype, data agtype, lbl agtype);
+ id |  data   |     lbl      
+----+---------+--------------
+ 1  | "test1" | "RemoveTest"
+ 2  | "test2" | "RemoveTest"
+(2 rows)
+
+-- Remove label from vertex1
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:RemoveTest {id: 1})
+    REMOVE n:RemoveTest
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+ id |  data   | lbl 
+----+---------+-----
+ 1  | "test1" | ""
+(1 row)
+
+-- Verify vertex1 now has no label (empty string)
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n {data: 'test1'})
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+ id |  data   | lbl 
+----+---------+-----
+ 1  | "test1" | ""
+(1 row)
+
+-- Verify vertex2 still has RemoveTest label
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:RemoveTest)
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+ id |  data   |     lbl      
+----+---------+--------------
+ 2  | "test2" | "RemoveTest"
+(1 row)
+
+-- Verify properties are preserved after label removal
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n)
+    WHERE n.data = 'test1'
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+ id |  data   | lbl 
+----+---------+-----
+ 1  | "test1" | ""
+(1 row)
+
+--
+-- Test 20: SET label with property updates - error when vertex has label
+--
+SELECT * FROM cypher('unified_test', $$
+    CREATE (:CombinedTest {id: 1, val: 'original'})
+$$) AS (v agtype);
+ v 
+---
+(0 rows)
+
+-- Try to SET label and property - should FAIL because vertex has a label
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:CombinedTest {id: 1})
+    SET n:CombinedNew, n.val = 'updated'
+    RETURN n.id, n.val, label(n)
+$$) AS (id agtype, val agtype, lbl agtype);
+ERROR:  SET label failed: vertex already has label "CombinedTest"
+HINT:  Multiple labels are not supported. Use REMOVE to clear the label first.
+-- Verify vertex is unchanged
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:CombinedTest)
+    RETURN n.id, n.val, label(n) ORDER BY n.id
+$$) AS (id agtype, val agtype, lbl agtype);
+ id |    val     |      lbl       
+----+------------+----------------
+ 1  | "original" | "CombinedTest"
+(1 row)
+
+--
+-- Test 21: Proper workflow - REMOVE then SET label
+-- To change a label, first REMOVE the old one, then SET the new one
+--
+SELECT * FROM cypher('unified_test', $$
+    CREATE (:WorkflowTest {id: 50, val: 'workflow'})
+$$) AS (v agtype);
+ v 
+---
+(0 rows)
+
+-- First REMOVE the label
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:WorkflowTest {id: 50})
+    REMOVE n:WorkflowTest
+    RETURN n.id, n.val, label(n)
+$$) AS (id agtype, val agtype, lbl agtype);
+ id |    val     | lbl 
+----+------------+-----
+ 50 | "workflow" | ""
+(1 row)
+
+-- Now SET a new label (should work because vertex has no label)
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n {id: 50})
+    SET n:NewWorkflowLabel
+    RETURN n.id, n.val, label(n)
+$$) AS (id agtype, val agtype, lbl agtype);
+ id |    val     |        lbl         
+----+------------+--------------------
+ 50 | "workflow" | "NewWorkflowLabel"
+(1 row)
+
+-- Verify the new label
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:NewWorkflowLabel)
+    RETURN n.id, n.val, label(n) ORDER BY n.id
+$$) AS (id agtype, val agtype, lbl agtype);
+ id |    val     |        lbl         
+----+------------+--------------------
+ 50 | "workflow" | "NewWorkflowLabel"
+(1 row)
+
+--
+-- Test 22: SET label auto-creates label when vertex has no label
+--
+-- First create and remove label to get unlabeled vertex
+SELECT * FROM cypher('unified_test', $$
+    CREATE (:TempForAuto {id: 60, name: 'auto_create_test'})
+$$) AS (v agtype);
+ v 
+---
+(0 rows)
+
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:TempForAuto {id: 60})
+    REMOVE n:TempForAuto
+    RETURN n.id, n.name, label(n)
+$$) AS (id agtype, name agtype, lbl agtype);
+ id |        name        | lbl 
+----+--------------------+-----
+ 60 | "auto_create_test" | ""
+(1 row)
+
+-- Now SET a new label that doesn't exist yet (should auto-create)
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n {id: 60})
+    SET n:AutoCreatedLabel
+    RETURN n.id, n.name, label(n)
+$$) AS (id agtype, name agtype, lbl agtype);
+ id |        name        |        lbl         
+----+--------------------+--------------------
+ 60 | "auto_create_test" | "AutoCreatedLabel"
+(1 row)
+
+-- Verify the new label exists and the vertex is there
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:AutoCreatedLabel)
+    RETURN n.id, n.name, label(n)
+$$) AS (id agtype, name agtype, lbl agtype);
+ id |        name        |        lbl         
+----+--------------------+--------------------
+ 60 | "auto_create_test" | "AutoCreatedLabel"
+(1 row)
+
+--
+-- Test 23: SET label on vertex with NO label (blank -> labeled)
+--
+-- First create a vertex with a label, then remove it to get a blank label
+SELECT * FROM cypher('unified_test', $$
+    CREATE (:TempLabel {id: 100, data: 'unlabeled_test'})
+$$) AS (v agtype);
+ v 
+---
+(0 rows)
+
+-- Remove the label to make it blank
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:TempLabel {id: 100})
+    REMOVE n:TempLabel
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+ id  |       data       | lbl 
+-----+------------------+-----
+ 100 | "unlabeled_test" | ""
+(1 row)
+
+-- Verify it has no label (blank)
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n {id: 100})
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+ id  |       data       | lbl 
+-----+------------------+-----
+ 100 | "unlabeled_test" | ""
+(1 row)
+
+-- Now SET a label on the unlabeled vertex
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n {id: 100})
+    SET n:FromBlankLabel
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+ id  |       data       |       lbl        
+-----+------------------+------------------
+ 100 | "unlabeled_test" | "FromBlankLabel"
+(1 row)
+
+-- Verify the label was set
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:FromBlankLabel)
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+ id  |       data       |       lbl        
+-----+------------------+------------------
+ 100 | "unlabeled_test" | "FromBlankLabel"
+(1 row)
+
+--
+-- Test 24: REMOVE label on vertex that already has NO label (no-op)
+--
+-- Create another unlabeled vertex
+SELECT * FROM cypher('unified_test', $$
+    CREATE (:TempLabel2 {id: 101, data: 'already_blank'})
+$$) AS (v agtype);
+ v 
+---
+(0 rows)
+
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:TempLabel2 {id: 101})
+    REMOVE n:TempLabel2
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+ id  |      data       | lbl 
+-----+-----------------+-----
+ 101 | "already_blank" | ""
+(1 row)
+
+-- Now try to REMOVE a label from already-unlabeled vertex (should be no-op)
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n {id: 101})
+    REMOVE n:SomeLabel
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+ id  |      data       | lbl 
+-----+-----------------+-----
+ 101 | "already_blank" | ""
+(1 row)
+
+-- Verify still has no label
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n {id: 101})
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+ id  |      data       | lbl 
+-----+-----------------+-----
+ 101 | "already_blank" | ""
+(1 row)
+
+--
+-- Test 25: REMOVE with wrong label name (should be no-op)
+-- REMOVE should only remove the label if it matches the specified name
+--
+SELECT * FROM cypher('unified_test', $$
+    CREATE (:KeepThisLabel {id: 103, data: 'wrong_label_test'})
+$$) AS (v agtype);
+ v 
+---
+(0 rows)
+
+-- Try to REMOVE a different label than the vertex has - should be no-op
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:KeepThisLabel {id: 103})
+    REMOVE n:WrongLabel
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+ id  |        data        |       lbl       
+-----+--------------------+-----------------
+ 103 | "wrong_label_test" | "KeepThisLabel"
+(1 row)
+
+-- Verify label is still KeepThisLabel (unchanged)
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:KeepThisLabel {id: 103})
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+ id  |        data        |       lbl       
+-----+--------------------+-----------------
+ 103 | "wrong_label_test" | "KeepThisLabel"
+(1 row)
+
+-- Now REMOVE with the correct label - should work
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:KeepThisLabel {id: 103})
+    REMOVE n:KeepThisLabel
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+ id  |        data        | lbl 
+-----+--------------------+-----
+ 103 | "wrong_label_test" | ""
+(1 row)
+
+-- Verify label is now empty
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n {id: 103})
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+ id  |        data        | lbl 
+-----+--------------------+-----
+ 103 | "wrong_label_test" | ""
+(1 row)
+
+--
+-- Test 26: SET label to same label - error (vertex already has a label)
+--
+SELECT * FROM cypher('unified_test', $$
+    CREATE (:SameLabel {id: 102, data: 'same_label_test'})
+$$) AS (v agtype);
+ v 
+---
+(0 rows)
+
+-- SET to the same label it already has - should FAIL
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:SameLabel {id: 102})
+    SET n:SameLabel
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+ERROR:  SET label failed: vertex already has label "SameLabel"
+HINT:  Multiple labels are not supported. Use REMOVE to clear the label first.
+-- Verify label is unchanged
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:SameLabel {id: 102})
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+ id  |       data        |     lbl     
+-----+-------------------+-------------
+ 102 | "same_label_test" | "SameLabel"
+(1 row)
+
+--
+-- Test 27: Error case - SET/REMOVE label on edge (should error)
+--
+SELECT * FROM cypher('unified_test', $$
+    CREATE (:EdgeTest1 {id: 200})-[:CONNECTS]->(:EdgeTest2 {id: 201})
+$$) AS (v agtype);
+ v 
+---
+(0 rows)
+
+-- Try to SET label on an edge - should fail
+SELECT * FROM cypher('unified_test', $$
+    MATCH (:EdgeTest1)-[e:CONNECTS]->(:EdgeTest2)
+    SET e:NewEdgeLabel
+    RETURN e
+$$) AS (e agtype);
+ERROR:  SET/REMOVE label can only be used on vertices
+-- Try to REMOVE label on an edge - should fail
+SELECT * FROM cypher('unified_test', $$
+    MATCH (:EdgeTest1)-[e:CONNECTS]->(:EdgeTest2)
+    REMOVE e:CONNECTS
+    RETURN e
+$$) AS (e agtype);
+ERROR:  SET/REMOVE label can only be used on vertices
 --
 -- Cleanup
 --
 SELECT drop_graph('unified_test', true);
-NOTICE:  drop cascades to 23 other objects
+NOTICE:  drop cascades to 38 other objects
 DETAIL:  drop cascades to table unified_test._ag_label_vertex
 drop cascades to table unified_test._ag_label_edge
 drop cascades to table unified_test."Person"
@@ -815,6 +1247,21 @@ drop cascades to table unified_test."DEL_EDGE"
 drop cascades to table unified_test."UpdateTest"
 drop cascades to table unified_test."StressTest"
 drop cascades to table unified_test."ST_EDGE"
+drop cascades to table unified_test."OldLabel"
+drop cascades to table unified_test."RemoveTest"
+drop cascades to table unified_test."CombinedTest"
+drop cascades to table unified_test."WorkflowTest"
+drop cascades to table unified_test."NewWorkflowLabel"
+drop cascades to table unified_test."TempForAuto"
+drop cascades to table unified_test."AutoCreatedLabel"
+drop cascades to table unified_test."TempLabel"
+drop cascades to table unified_test."FromBlankLabel"
+drop cascades to table unified_test."TempLabel2"
+drop cascades to table unified_test."KeepThisLabel"
+drop cascades to table unified_test."SameLabel"
+drop cascades to table unified_test."EdgeTest1"
+drop cascades to table unified_test."CONNECTS"
+drop cascades to table unified_test."EdgeTest2"
 NOTICE:  graph "unified_test" has been dropped
  drop_graph 
 ------------
diff --git a/regress/sql/unified_vertex_table.sql 
b/regress/sql/unified_vertex_table.sql
index f9f30f66..8eebf9c1 100644
--- a/regress/sql/unified_vertex_table.sql
+++ b/regress/sql/unified_vertex_table.sql
@@ -462,6 +462,292 @@ SELECT * FROM cypher('unified_test', $$
     RETURN count(n)
 $$) AS (cnt agtype);
 
+--
+-- Test 18: SET label operation - error when vertex already has a label
+-- Multiple labels are not supported. SET only works on unlabeled vertices.
+--
+SELECT * FROM cypher('unified_test', $$
+    CREATE (:OldLabel {id: 1, name: 'vertex1'})
+$$) AS (v agtype);
+
+SELECT * FROM cypher('unified_test', $$
+    CREATE (:OldLabel {id: 2, name: 'vertex2'})
+$$) AS (v agtype);
+
+-- Verify initial label
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:OldLabel)
+    RETURN n.id, n.name, label(n) ORDER BY n.id
+$$) AS (id agtype, name agtype, lbl agtype);
+
+-- Try to change label on vertex1 - should FAIL because it already has a label
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:OldLabel {id: 1})
+    SET n:NewLabel
+    RETURN n.id, n.name, label(n)
+$$) AS (id agtype, name agtype, lbl agtype);
+
+-- Verify vertex1 still has OldLabel (unchanged due to error)
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:OldLabel)
+    RETURN n.id, n.name, label(n) ORDER BY n.id
+$$) AS (id agtype, name agtype, lbl agtype);
+
+--
+-- Test 19: REMOVE label operation
+-- This tests removing a vertex's label using REMOVE n:Label syntax
+--
+SELECT * FROM cypher('unified_test', $$
+    CREATE (:RemoveTest {id: 1, data: 'test1'})
+$$) AS (v agtype);
+
+SELECT * FROM cypher('unified_test', $$
+    CREATE (:RemoveTest {id: 2, data: 'test2'})
+$$) AS (v agtype);
+
+-- Verify initial label
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:RemoveTest)
+    RETURN n.id, n.data, label(n) ORDER BY n.id
+$$) AS (id agtype, data agtype, lbl agtype);
+
+-- Remove label from vertex1
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:RemoveTest {id: 1})
+    REMOVE n:RemoveTest
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+
+-- Verify vertex1 now has no label (empty string)
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n {data: 'test1'})
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+
+-- Verify vertex2 still has RemoveTest label
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:RemoveTest)
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+
+-- Verify properties are preserved after label removal
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n)
+    WHERE n.data = 'test1'
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+
+--
+-- Test 20: SET label with property updates - error when vertex has label
+--
+SELECT * FROM cypher('unified_test', $$
+    CREATE (:CombinedTest {id: 1, val: 'original'})
+$$) AS (v agtype);
+
+-- Try to SET label and property - should FAIL because vertex has a label
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:CombinedTest {id: 1})
+    SET n:CombinedNew, n.val = 'updated'
+    RETURN n.id, n.val, label(n)
+$$) AS (id agtype, val agtype, lbl agtype);
+
+-- Verify vertex is unchanged
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:CombinedTest)
+    RETURN n.id, n.val, label(n) ORDER BY n.id
+$$) AS (id agtype, val agtype, lbl agtype);
+
+--
+-- Test 21: Proper workflow - REMOVE then SET label
+-- To change a label, first REMOVE the old one, then SET the new one
+--
+SELECT * FROM cypher('unified_test', $$
+    CREATE (:WorkflowTest {id: 50, val: 'workflow'})
+$$) AS (v agtype);
+
+-- First REMOVE the label
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:WorkflowTest {id: 50})
+    REMOVE n:WorkflowTest
+    RETURN n.id, n.val, label(n)
+$$) AS (id agtype, val agtype, lbl agtype);
+
+-- Now SET a new label (should work because vertex has no label)
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n {id: 50})
+    SET n:NewWorkflowLabel
+    RETURN n.id, n.val, label(n)
+$$) AS (id agtype, val agtype, lbl agtype);
+
+-- Verify the new label
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:NewWorkflowLabel)
+    RETURN n.id, n.val, label(n) ORDER BY n.id
+$$) AS (id agtype, val agtype, lbl agtype);
+
+--
+-- Test 22: SET label auto-creates label when vertex has no label
+--
+-- First create and remove label to get unlabeled vertex
+SELECT * FROM cypher('unified_test', $$
+    CREATE (:TempForAuto {id: 60, name: 'auto_create_test'})
+$$) AS (v agtype);
+
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:TempForAuto {id: 60})
+    REMOVE n:TempForAuto
+    RETURN n.id, n.name, label(n)
+$$) AS (id agtype, name agtype, lbl agtype);
+
+-- Now SET a new label that doesn't exist yet (should auto-create)
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n {id: 60})
+    SET n:AutoCreatedLabel
+    RETURN n.id, n.name, label(n)
+$$) AS (id agtype, name agtype, lbl agtype);
+
+-- Verify the new label exists and the vertex is there
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:AutoCreatedLabel)
+    RETURN n.id, n.name, label(n)
+$$) AS (id agtype, name agtype, lbl agtype);
+
+--
+-- Test 23: SET label on vertex with NO label (blank -> labeled)
+--
+-- First create a vertex with a label, then remove it to get a blank label
+SELECT * FROM cypher('unified_test', $$
+    CREATE (:TempLabel {id: 100, data: 'unlabeled_test'})
+$$) AS (v agtype);
+
+-- Remove the label to make it blank
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:TempLabel {id: 100})
+    REMOVE n:TempLabel
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+
+-- Verify it has no label (blank)
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n {id: 100})
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+
+-- Now SET a label on the unlabeled vertex
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n {id: 100})
+    SET n:FromBlankLabel
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+
+-- Verify the label was set
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:FromBlankLabel)
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+
+--
+-- Test 24: REMOVE label on vertex that already has NO label (no-op)
+--
+-- Create another unlabeled vertex
+SELECT * FROM cypher('unified_test', $$
+    CREATE (:TempLabel2 {id: 101, data: 'already_blank'})
+$$) AS (v agtype);
+
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:TempLabel2 {id: 101})
+    REMOVE n:TempLabel2
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+
+-- Now try to REMOVE a label from already-unlabeled vertex (should be no-op)
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n {id: 101})
+    REMOVE n:SomeLabel
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+
+-- Verify still has no label
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n {id: 101})
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+
+--
+-- Test 25: REMOVE with wrong label name (should be no-op)
+-- REMOVE should only remove the label if it matches the specified name
+--
+SELECT * FROM cypher('unified_test', $$
+    CREATE (:KeepThisLabel {id: 103, data: 'wrong_label_test'})
+$$) AS (v agtype);
+
+-- Try to REMOVE a different label than the vertex has - should be no-op
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:KeepThisLabel {id: 103})
+    REMOVE n:WrongLabel
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+
+-- Verify label is still KeepThisLabel (unchanged)
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:KeepThisLabel {id: 103})
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+
+-- Now REMOVE with the correct label - should work
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:KeepThisLabel {id: 103})
+    REMOVE n:KeepThisLabel
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+
+-- Verify label is now empty
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n {id: 103})
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+
+--
+-- Test 26: SET label to same label - error (vertex already has a label)
+--
+SELECT * FROM cypher('unified_test', $$
+    CREATE (:SameLabel {id: 102, data: 'same_label_test'})
+$$) AS (v agtype);
+
+-- SET to the same label it already has - should FAIL
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:SameLabel {id: 102})
+    SET n:SameLabel
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+
+-- Verify label is unchanged
+SELECT * FROM cypher('unified_test', $$
+    MATCH (n:SameLabel {id: 102})
+    RETURN n.id, n.data, label(n)
+$$) AS (id agtype, data agtype, lbl agtype);
+
+--
+-- Test 27: Error case - SET/REMOVE label on edge (should error)
+--
+SELECT * FROM cypher('unified_test', $$
+    CREATE (:EdgeTest1 {id: 200})-[:CONNECTS]->(:EdgeTest2 {id: 201})
+$$) AS (v agtype);
+
+-- Try to SET label on an edge - should fail
+SELECT * FROM cypher('unified_test', $$
+    MATCH (:EdgeTest1)-[e:CONNECTS]->(:EdgeTest2)
+    SET e:NewEdgeLabel
+    RETURN e
+$$) AS (e agtype);
+
+-- Try to REMOVE label on an edge - should fail
+SELECT * FROM cypher('unified_test', $$
+    MATCH (:EdgeTest1)-[e:CONNECTS]->(:EdgeTest2)
+    REMOVE e:CONNECTS
+    RETURN e
+$$) AS (e agtype);
+
 --
 -- Cleanup
 --
diff --git a/src/backend/executor/cypher_set.c 
b/src/backend/executor/cypher_set.c
index c13cee04..07ca0fcb 100644
--- a/src/backend/executor/cypher_set.c
+++ b/src/backend/executor/cypher_set.c
@@ -419,6 +419,8 @@ static void process_update_list(CustomScanState *node)
          */
         if (scanTupleSlot->tts_isnull[update_item->entity_position - 1])
         {
+            /* increment the loop index before continuing */
+            lidx++;
             continue;
         }
 
@@ -447,6 +449,151 @@ static void process_update_list(CustomScanState *node)
         label = GET_AGTYPE_VALUE_OBJECT_VALUE(original_entity_value, "label");
 
         label_name = pnstrdup(label->val.string.val, label->val.string.len);
+
+        /*
+         * Handle label SET/REMOVE operations
+         */
+        if (update_item->is_label_op)
+        {
+            Oid graph_namespace_oid;
+            Oid new_label_table_oid;
+            char *new_label_name;
+
+            /* Label operations only apply to vertices */
+            if (original_entity_value->type != AGTV_VERTEX)
+            {
+                ereport(ERROR,
+                        (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                         errmsg("SET/REMOVE label can only be used on 
vertices")));
+            }
+
+            /* Get the original properties - we keep them unchanged */
+            original_properties = 
GET_AGTYPE_VALUE_OBJECT_VALUE(original_entity_value,
+                                                                "properties");
+
+            /* Get the namespace OID for the graph */
+            graph_namespace_oid = get_namespace_oid(css->set_list->graph_name, 
false);
+
+            /*
+             * Determine the new label. For REMOVE, set to default (empty) only
+             * if the vertex has the specified label. For SET, only allow if 
the
+             * vertex has no label (multiple labels are not supported).
+             */
+            if (update_item->remove_item)
+            {
+                /*
+                 * REMOVE label: only remove if the vertex has the specified 
label.
+                 * If the vertex has a different label (or no label), do 
nothing.
+                 */
+                if (label->val.string.len == 0 ||
+                    strcmp(label_name, update_item->label_name) != 0)
+                {
+                    /* Label doesn't match - skip this update, continue to 
next */
+                    lidx++;
+                    continue;
+                }
+
+                /* Label matches - set to default (no label) */
+                new_label_name = "";
+                new_label_table_oid = 
get_relname_relid(AG_DEFAULT_LABEL_VERTEX,
+                                                        graph_namespace_oid);
+            }
+            else
+            {
+                /*
+                 * SET label: only allow if the vertex currently has no label.
+                 * Multiple labels are not supported.
+                 */
+                if (label->val.string.len > 0)
+                {
+                    ereport(ERROR,
+                            (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                             errmsg("SET label failed: vertex already has 
label \"%s\"",
+                                    label_name),
+                             errhint("Multiple labels are not supported. Use 
REMOVE to clear the label first.")));
+                }
+
+                /* SET label: use the new label name */
+                new_label_name = update_item->label_name;
+
+                /* Check if the label exists, create if not */
+                new_label_table_oid = get_relname_relid(new_label_name,
+                                                        graph_namespace_oid);
+                if (!OidIsValid(new_label_table_oid))
+                {
+                    /*
+                     * Per Cypher specification, if the label doesn't exist,
+                     * we create it automatically.
+                     */
+                    create_label(css->set_list->graph_name, new_label_name,
+                                 LABEL_TYPE_VERTEX, NIL);
+
+                    /* Get the OID of the newly created label table */
+                    new_label_table_oid = get_relname_relid(new_label_name,
+                                                            
graph_namespace_oid);
+                }
+            }
+
+            /* Use the unified vertex table for the update */
+            resultRelInfo = create_entity_result_rel_info(
+                estate, css->set_list->graph_name, AG_DEFAULT_LABEL_VERTEX);
+
+            slot = ExecInitExtraTupleSlot(
+                estate, RelationGetDescr(resultRelInfo->ri_RelationDesc),
+                &TTSOpsHeapTuple);
+
+            /* Create the new vertex with updated label */
+            new_entity = make_vertex(GRAPHID_GET_DATUM(id->val.int_value),
+                                     CStringGetDatum(new_label_name),
+                                     
AGTYPE_P_GET_DATUM(agtype_value_to_agtype(original_properties)));
+
+            /* Populate the tuple table slot */
+            slot = populate_vertex_tts(slot, id, original_properties);
+
+            /* Set the labels column to the new label table OID */
+            slot->tts_values[vertex_tuple_labels] = 
ObjectIdGetDatum(new_label_table_oid);
+            slot->tts_isnull[vertex_tuple_labels] = false;
+
+            /* Update in-memory tuple */
+            scanTupleSlot->tts_values[update_item->entity_position - 1] = 
new_entity;
+
+            /* Update any paths containing this entity */
+            update_all_paths(node, id->val.int_value, 
DATUM_GET_AGTYPE_P(new_entity));
+
+            /* Perform the on-disk update */
+            cid = estate->es_snapshot->curcid;
+            estate->es_snapshot->curcid = GetCurrentCommandId(false);
+
+            if (luindex[update_item->entity_position - 1] == lidx)
+            {
+                ScanKeyInit(&scan_keys[0], 1, BTEqualStrategyNumber, F_INT8EQ,
+                            Int64GetDatum(id->val.int_value));
+
+                (void) RelationGetIndexList(resultRelInfo->ri_RelationDesc);
+                pk_index_oid = resultRelInfo->ri_RelationDesc->rd_pkindex;
+
+                scan_desc = systable_beginscan(resultRelInfo->ri_RelationDesc,
+                                               pk_index_oid, true,
+                                               estate->es_snapshot, 1, 
scan_keys);
+                heap_tuple = systable_getnext(scan_desc);
+
+                if (HeapTupleIsValid(heap_tuple))
+                {
+                    heap_tuple = update_entity_tuple(resultRelInfo, slot, 
estate,
+                                                     heap_tuple);
+                }
+                systable_endscan(scan_desc);
+            }
+
+            estate->es_snapshot->curcid = cid;
+            ExecCloseIndices(resultRelInfo);
+            table_close(resultRelInfo->ri_RelationDesc, RowExclusiveLock);
+
+            /* increment loop index and continue to next item */
+            lidx++;
+            continue;
+        }
+
         /* get the properties we need to update */
         original_properties = 
GET_AGTYPE_VALUE_OBJECT_VALUE(original_entity_value,
                                                             "properties");
diff --git a/src/backend/nodes/cypher_copyfuncs.c 
b/src/backend/nodes/cypher_copyfuncs.c
index db8408b7..4ba27afb 100644
--- a/src/backend/nodes/cypher_copyfuncs.c
+++ b/src/backend/nodes/cypher_copyfuncs.c
@@ -125,6 +125,7 @@ void copy_cypher_update_information(ExtensibleNode 
*newnode, const ExtensibleNod
     COPY_STRING_FIELD(clause_name);
 }
 
+/* copy function for cypher_update_item */
 /* copy function for cypher_update_item */
 void copy_cypher_update_item(ExtensibleNode *newnode, const ExtensibleNode 
*from)
 {
@@ -137,6 +138,8 @@ void copy_cypher_update_item(ExtensibleNode *newnode, const 
ExtensibleNode *from
     COPY_NODE_FIELD(qualified_name);
     COPY_SCALAR_FIELD(remove_item);
     COPY_SCALAR_FIELD(is_add);
+    COPY_SCALAR_FIELD(is_label_op);
+    COPY_STRING_FIELD(label_name);
 }
 
 /* copy function for cypher_delete_information */
diff --git a/src/backend/nodes/cypher_outfuncs.c 
b/src/backend/nodes/cypher_outfuncs.c
index a0fa6a4b..0d352fc8 100644
--- a/src/backend/nodes/cypher_outfuncs.c
+++ b/src/backend/nodes/cypher_outfuncs.c
@@ -159,6 +159,8 @@ void out_cypher_set_item(StringInfo str, const 
ExtensibleNode *node)
     WRITE_NODE_FIELD(prop);
     WRITE_NODE_FIELD(expr);
     WRITE_BOOL_FIELD(is_add);
+    WRITE_BOOL_FIELD(is_label_op);
+    WRITE_STRING_FIELD(label_name);
 }
 
 /* serialization function for the cypher_delete ExtensibleNode. */
@@ -428,6 +430,8 @@ void out_cypher_update_item(StringInfo str, const 
ExtensibleNode *node)
     WRITE_NODE_FIELD(qualified_name);
     WRITE_BOOL_FIELD(remove_item);
     WRITE_BOOL_FIELD(is_add);
+    WRITE_BOOL_FIELD(is_label_op);
+    WRITE_STRING_FIELD(label_name);
 }
 
 /* serialization function for the cypher_delete_information ExtensibleNode. */
diff --git a/src/backend/nodes/cypher_readfuncs.c 
b/src/backend/nodes/cypher_readfuncs.c
index 15f5dac7..8c24e7df 100644
--- a/src/backend/nodes/cypher_readfuncs.c
+++ b/src/backend/nodes/cypher_readfuncs.c
@@ -270,6 +270,8 @@ void read_cypher_update_item(struct ExtensibleNode *node)
     READ_NODE_FIELD(qualified_name);
     READ_BOOL_FIELD(remove_item);
     READ_BOOL_FIELD(is_add);
+    READ_BOOL_FIELD(is_label_op);
+    READ_STRING_FIELD(label_name);
 }
 
 /*
diff --git a/src/backend/parser/cypher_clause.c 
b/src/backend/parser/cypher_clause.c
index 08919329..eb7dccf0 100644
--- a/src/backend/parser/cypher_clause.c
+++ b/src/backend/parser/cypher_clause.c
@@ -1663,9 +1663,8 @@ cypher_update_information 
*transform_cypher_remove_item_list(
         cypher_set_item *set_item = lfirst(li);
         cypher_update_item *item;
         ColumnRef *ref;
-        A_Indirection *ind;
-        char *variable_name, *property_name;
-        String *property_node, *variable_node;
+        char *variable_name;
+        String *variable_node;
 
         item = make_ag_node(cypher_update_item);
 
@@ -1685,69 +1684,115 @@ cypher_update_information 
*transform_cypher_remove_item_list(
         }
         set_item->is_add = false;
 
-        item->remove_item = true;
+        /* Check if this is a label removal operation */
+        if (set_item->is_label_op)
+        {
+            /* Label removal: REMOVE n:Label */
+            item->is_label_op = true;
+            item->label_name = set_item->label_name;
+            item->remove_item = true;
+            item->prop_name = NULL;
 
+            /* Extract variable name from ColumnRef */
+            if (!IsA(set_item->prop, ColumnRef))
+            {
+                ereport(ERROR,
+                        (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                         errmsg("REMOVE label must be in the format: REMOVE 
variable:Label"),
+                         parser_errposition(pstate, set_item->location)));
+            }
 
-        if (!IsA(set_item->prop, A_Indirection))
-        {
-            ereport(ERROR,
-                    (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-                     errmsg("REMOVE clause must be in the format: REMOVE 
variable.property_name"),
-                     parser_errposition(pstate, set_item->location)));
-        }
+            ref = (ColumnRef *)set_item->prop;
+            variable_node = linitial(ref->fields);
+            variable_name = variable_node->sval;
+            item->var_name = variable_name;
 
-        ind = (A_Indirection *)set_item->prop;
+            item->entity_position = get_target_entry_resno(query->targetList,
+                                                           variable_name);
+            if (item->entity_position == -1)
+            {
+                ereport(ERROR,
+                        (errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+                         errmsg("undefined reference to variable %s in REMOVE 
clause",
+                                variable_name),
+                         parser_errposition(pstate, set_item->location)));
+            }
 
-        /* extract variable name */
-        if (!IsA(ind->arg, ColumnRef))
-        {
-            ereport(ERROR,
-                    (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-                     errmsg("REMOVE clause must be in the format: REMOVE 
variable.property_name"),
-                     parser_errposition(pstate, set_item->location)));
+            add_volatile_wrapper_to_target_entry(query->targetList,
+                                                 item->entity_position);
         }
+        else
+        {
+            /* Property removal: REMOVE n.property */
+            A_Indirection *ind;
+            char *property_name;
+            String *property_node;
 
-        ref = (ColumnRef *)ind->arg;
+            item->is_label_op = false;
+            item->label_name = NULL;
+            item->remove_item = true;
 
-        variable_node = linitial(ref->fields);
+            if (!IsA(set_item->prop, A_Indirection))
+            {
+                ereport(ERROR,
+                        (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                         errmsg("REMOVE clause must be in the format: REMOVE 
variable.property_name or REMOVE variable:Label"),
+                         parser_errposition(pstate, set_item->location)));
+            }
 
-        variable_name = variable_node->sval;
-        item->var_name = variable_name;
+            ind = (A_Indirection *)set_item->prop;
 
-        item->entity_position = get_target_entry_resno(query->targetList,
-                                                       variable_name);
-        if (item->entity_position == -1)
-        {
-            ereport(ERROR,
-                    (errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
-                     errmsg("undefined reference to variable %s in REMOVE 
clause",
-                            variable_name),
-                     parser_errposition(pstate, set_item->location)));
-        }
+            /* extract variable name */
+            if (!IsA(ind->arg, ColumnRef))
+            {
+                ereport(ERROR,
+                        (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                         errmsg("REMOVE clause must be in the format: REMOVE 
variable.property_name"),
+                         parser_errposition(pstate, set_item->location)));
+            }
 
-        add_volatile_wrapper_to_target_entry(query->targetList,
-                                             item->entity_position);
+            ref = (ColumnRef *)ind->arg;
 
-        /* extract property name */
-        if (list_length(ind->indirection) != 1)
-        {
-            ereport(ERROR,
-                    (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-                     errmsg("REMOVE clause must be in the format: REMOVE 
variable.property_name"),
-                     parser_errposition(pstate, set_item->location)));
-        }
+            variable_node = linitial(ref->fields);
 
-        property_node = linitial(ind->indirection);
+            variable_name = variable_node->sval;
+            item->var_name = variable_name;
 
-        if (!IsA(property_node, String))
-        {
-            ereport(ERROR,
-                    (errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
-                     errmsg("REMOVE clause expects a property name"),
-                     parser_errposition(pstate, set_item->location)));
+            item->entity_position = get_target_entry_resno(query->targetList,
+                                                           variable_name);
+            if (item->entity_position == -1)
+            {
+                ereport(ERROR,
+                        (errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+                         errmsg("undefined reference to variable %s in REMOVE 
clause",
+                                variable_name),
+                         parser_errposition(pstate, set_item->location)));
+            }
+
+            add_volatile_wrapper_to_target_entry(query->targetList,
+                                                 item->entity_position);
+
+            /* extract property name */
+            if (list_length(ind->indirection) != 1)
+            {
+                ereport(ERROR,
+                        (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                         errmsg("REMOVE clause must be in the format: REMOVE 
variable.property_name"),
+                         parser_errposition(pstate, set_item->location)));
+            }
+
+            property_node = linitial(ind->indirection);
+
+            if (!IsA(property_node, String))
+            {
+                ereport(ERROR,
+                        (errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+                         errmsg("REMOVE clause expects a property name"),
+                         parser_errposition(pstate, set_item->location)));
+            }
+            property_name = property_node->sval;
+            item->prop_name = property_name;
         }
-        property_name = property_node->sval;
-        item->prop_name = property_name;
 
         info->set_items = lappend(info->set_items, item);
     }
@@ -1769,165 +1814,207 @@ cypher_update_information 
*transform_cypher_set_item_list(
     foreach (li, set_item_list)
     {
         cypher_set_item *set_item = lfirst(li);
-        TargetEntry *target_item;
         cypher_update_item *item;
-        ColumnRef *ref;
-        A_Indirection *ind;
-        char *variable_name, *property_name;
-        String *property_node, *variable_node;
-        int is_entire_prop_update = 0; /* true if a map is assigned to 
variable */
 
-        /* LHS of set_item must be a variable or an indirection. */
-        if (IsA(set_item->prop, ColumnRef))
+        if (!is_ag_node(lfirst(li), cypher_set_item))
         {
-            /*
-             * A variable can only be assigned a map, a function call that
-             * evaluates to a map, or a variable.
-             *
-             * In case of a function call, whether it actually evaluates to
-             * map is checked in the execution stage.
-             */
-            if (!is_ag_node(set_item->expr, cypher_map) &&
-                !IsA(set_item->expr, FuncCall) &&
-                !IsA(set_item->expr, ColumnRef))
+            ereport(ERROR,
+                    (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                     errmsg("unexpected node in cypher update list")));
+        }
+
+        item = make_ag_node(cypher_update_item);
+        item->remove_item = false;
+
+        /* Check if this is a label SET operation */
+        if (set_item->is_label_op)
+        {
+            /* Label SET: SET n:Label */
+            ColumnRef *ref;
+            String *variable_node;
+            char *variable_name;
+
+            item->is_label_op = true;
+            item->label_name = set_item->label_name;
+            item->prop_name = NULL;
+            item->is_add = false;
+
+            /* Extract variable name from ColumnRef */
+            if (!IsA(set_item->prop, ColumnRef))
             {
                 ereport(ERROR,
                         (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-                         errmsg("SET clause expects a map"),
+                         errmsg("SET label must be in the format: SET 
variable:Label"),
                          parser_errposition(pstate, set_item->location)));
             }
 
-            is_entire_prop_update = 1;
+            ref = (ColumnRef *)set_item->prop;
+            variable_node = linitial(ref->fields);
+            variable_name = variable_node->sval;
+            item->var_name = variable_name;
 
-            /*
-             * In case of a variable, it is wrapped as an argument to
-             * the 'properties' function.
-             */
-            if (IsA(set_item->expr, ColumnRef))
+            item->entity_position = get_target_entry_resno(query->targetList,
+                                                           variable_name);
+            if (item->entity_position == -1)
             {
-                List *qualified_name, *args;
-
-                qualified_name = list_make2(makeString("ag_catalog"),
-                                            makeString("age_properties"));
-                args = list_make1(set_item->expr);
-                set_item->expr = (Node *)makeFuncCall(qualified_name, args,
-                                                      COERCE_SQL_SYNTAX, -1);
+                ereport(ERROR,
+                        (errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+                         errmsg("undefined reference to variable %s in SET 
clause",
+                                variable_name),
+                         parser_errposition(pstate, set_item->location)));
             }
-        }
-        else if (!IsA(set_item->prop, A_Indirection))
-        {
-            ereport(ERROR, (errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
-                            errmsg("SET clause expects a variable name"),
-                            parser_errposition(pstate, set_item->location)));
-        }
 
-        item = make_ag_node(cypher_update_item);
+            add_volatile_wrapper_to_target_entry(query->targetList,
+                                                 item->entity_position);
 
-        if (!is_ag_node(lfirst(li), cypher_set_item))
-        {
-            ereport(ERROR,
-                    (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-                     errmsg("unexpected node in cypher update list")));
-        }
+            /* No prop_position needed for label operations */
+            item->prop_position = -1;
 
-        item->remove_item = false;
-
-        /* set variable, is_add and extract property name */
-        if (is_entire_prop_update)
-        {
-            ref = (ColumnRef *)set_item->prop;
-            item->is_add = set_item->is_add;
-            item->prop_name = NULL;
+            info->set_items = lappend(info->set_items, item);
         }
         else
         {
-            ind = (A_Indirection *)set_item->prop;
-            ref = (ColumnRef *)ind->arg;
+            /* Property SET: original logic */
+            TargetEntry *target_item;
+            ColumnRef *ref;
+            A_Indirection *ind;
+            char *variable_name, *property_name;
+            String *property_node, *variable_node;
+            int is_entire_prop_update = 0;
+
+            item->is_label_op = false;
+            item->label_name = NULL;
 
-            if (set_item->is_add)
+            /* LHS of set_item must be a variable or an indirection. */
+            if (IsA(set_item->prop, ColumnRef))
             {
-                ereport(
-                    ERROR,
-                    (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-                     errmsg(
-                         "SET clause does not yet support incrementing a 
specific property"),
-                     parser_errposition(pstate, set_item->location)));
+                if (!is_ag_node(set_item->expr, cypher_map) &&
+                    !IsA(set_item->expr, FuncCall) &&
+                    !IsA(set_item->expr, ColumnRef))
+                {
+                    ereport(ERROR,
+                            (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                             errmsg("SET clause expects a map"),
+                             parser_errposition(pstate, set_item->location)));
+                }
+
+                is_entire_prop_update = 1;
+
+                if (IsA(set_item->expr, ColumnRef))
+                {
+                    List *qualified_name, *args;
+
+                    qualified_name = list_make2(makeString("ag_catalog"),
+                                                makeString("age_properties"));
+                    args = list_make1(set_item->expr);
+                    set_item->expr = (Node *)makeFuncCall(qualified_name, args,
+                                                          COERCE_SQL_SYNTAX, 
-1);
+                }
+            }
+            else if (!IsA(set_item->prop, A_Indirection))
+            {
+                ereport(ERROR, (errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+                                errmsg("SET clause expects a variable name"),
+                                parser_errposition(pstate, 
set_item->location)));
             }
-            set_item->is_add = false;
 
-            /* extract property name */
-            if (list_length(ind->indirection) != 1)
+            /* set variable, is_add and extract property name */
+            if (is_entire_prop_update)
             {
-                ereport(
-                    ERROR,
-                    (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-                     errmsg("SET clause doesn't not support updating maps or 
lists in a property"),
-                     parser_errposition(pstate, set_item->location)));
+                ref = (ColumnRef *)set_item->prop;
+                item->is_add = set_item->is_add;
+                item->prop_name = NULL;
             }
+            else
+            {
+                ind = (A_Indirection *)set_item->prop;
+                ref = (ColumnRef *)ind->arg;
 
-            property_node = linitial(ind->indirection);
-            if (!IsA(property_node, String))
+                if (set_item->is_add)
+                {
+                    ereport(
+                        ERROR,
+                        (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                         errmsg(
+                             "SET clause does not yet support incrementing a 
specific property"),
+                         parser_errposition(pstate, set_item->location)));
+                }
+                set_item->is_add = false;
+
+                /* extract property name */
+                if (list_length(ind->indirection) != 1)
+                {
+                    ereport(
+                        ERROR,
+                        (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                         errmsg("SET clause doesn't not support updating maps 
or lists in a property"),
+                         parser_errposition(pstate, set_item->location)));
+                }
+
+                property_node = linitial(ind->indirection);
+                if (!IsA(property_node, String))
+                {
+                    ereport(ERROR,
+                            (errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+                             errmsg("SET clause expects a property name"),
+                             parser_errposition(pstate, set_item->location)));
+                }
+
+                property_name = property_node->sval;
+                item->prop_name = property_name;
+            }
+
+            /* extract variable name */
+            variable_node = linitial(ref->fields);
+            if (!IsA(variable_node, String))
             {
                 ereport(ERROR,
                         (errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
-                         errmsg("SET clause expects a property name"),
+                         errmsg("SET clause expects a variable name"),
                          parser_errposition(pstate, set_item->location)));
             }
 
-            property_name = property_node->sval;
-            item->prop_name = property_name;
-        }
+            variable_name = variable_node->sval;
+            item->var_name = variable_name;
 
-        /* extract variable name */
-        variable_node = linitial(ref->fields);
-        if (!IsA(variable_node, String))
-        {
-            ereport(ERROR,
-                    (errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
-                     errmsg("SET clause expects a variable name"),
-                     parser_errposition(pstate, set_item->location)));
-        }
+            item->entity_position = get_target_entry_resno(query->targetList,
+                                                           variable_name);
+            if (item->entity_position == -1)
+            {
+                ereport(ERROR,
+                        (errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+                         errmsg("undefined reference to variable %s in SET 
clause",
+                                variable_name),
+                                parser_errposition(pstate, 
set_item->location)));
+            }
 
-        variable_name = variable_node->sval;
-        item->var_name = variable_name;
+            add_volatile_wrapper_to_target_entry(query->targetList,
+                                                 item->entity_position);
 
-        item->entity_position = get_target_entry_resno(query->targetList,
-                                                       variable_name);
-        if (item->entity_position == -1)
-        {
-            ereport(ERROR,
-                    (errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
-                     errmsg("undefined reference to variable %s in SET clause",
-                            variable_name),
-                            parser_errposition(pstate, set_item->location)));
-        }
+            /* set keep_null property */
+            if (is_ag_node(set_item->expr, cypher_map))
+            {
+                ((cypher_map*)set_item->expr)->keep_null = set_item->is_add;
+            }
 
-        add_volatile_wrapper_to_target_entry(query->targetList,
-                                             item->entity_position);
+            /* create target entry for the new property value */
+            item->prop_position = (AttrNumber)pstate->p_next_resno;
+            target_item = transform_cypher_item(cpstate, set_item->expr, NULL,
+                                                EXPR_KIND_SELECT_TARGET, NULL,
+                                                false);
 
-        /* set keep_null property */
-        if (is_ag_node(set_item->expr, cypher_map))
-        {
-            ((cypher_map*)set_item->expr)->keep_null = set_item->is_add;
-        }
+            if (nodeTag(target_item->expr) == T_Aggref)
+            {
+                ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                        errmsg("Invalid use of aggregation in this context"),
+                        parser_errposition(pstate, set_item->location)));
+            }
 
-        /* create target entry for the new property value */
-        item->prop_position = (AttrNumber)pstate->p_next_resno;
-        target_item = transform_cypher_item(cpstate, set_item->expr, NULL,
-                                            EXPR_KIND_SELECT_TARGET, NULL,
-                                            false);
+            target_item->expr = add_volatile_wrapper(target_item->expr);
 
-        if (nodeTag(target_item->expr) == T_Aggref)
-        {
-            ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-                    errmsg("Invalid use of aggregation in this context"),
-                    parser_errposition(pstate, set_item->location)));
+            query->targetList = lappend(query->targetList, target_item);
+            info->set_items = lappend(info->set_items, item);
         }
-
-        target_item->expr = add_volatile_wrapper(target_item->expr);
-
-        query->targetList = lappend(query->targetList, target_item);
-        info->set_items = lappend(info->set_items, item);
     }
 
     return info;
diff --git a/src/backend/parser/cypher_gram.y b/src/backend/parser/cypher_gram.y
index 5ba1e635..7604b6a2 100644
--- a/src/backend/parser/cypher_gram.y
+++ b/src/backend/parser/cypher_gram.y
@@ -1041,6 +1041,8 @@ set_item:
             n->prop = $1;
             n->expr = $3;
             n->is_add = false;
+            n->is_label_op = false;
+            n->label_name = NULL;
             n->location = @1;
 
             $$ = (Node *)n;
@@ -1053,6 +1055,28 @@ set_item:
             n->prop = $1;
             n->expr = $3;
             n->is_add = true;
+            n->is_label_op = false;
+            n->label_name = NULL;
+            n->location = @1;
+
+            $$ = (Node *)n;
+        }
+    | var_name ':' label_name
+        {
+            cypher_set_item *n;
+            ColumnRef *cref;
+
+            /* Create a ColumnRef for the variable */
+            cref = makeNode(ColumnRef);
+            cref->fields = list_make1(makeString($1));
+            cref->location = @1;
+
+            n = make_ag_node(cypher_set_item);
+            n->prop = (Node *)cref;
+            n->expr = NULL;
+            n->is_add = false;
+            n->is_label_op = true;
+            n->label_name = $3;
             n->location = @1;
 
             $$ = (Node *)n;
@@ -1093,6 +1117,28 @@ remove_item:
             n->prop = $1;
             n->expr = make_null_const(-1);
             n->is_add = false;
+            n->is_label_op = false;
+            n->label_name = NULL;
+
+            $$ = (Node *)n;
+        }
+    | var_name ':' label_name
+        {
+            cypher_set_item *n;
+            ColumnRef *cref;
+
+            /* Create a ColumnRef for the variable */
+            cref = makeNode(ColumnRef);
+            cref->fields = list_make1(makeString($1));
+            cref->location = @1;
+
+            n = make_ag_node(cypher_set_item);
+            n->prop = (Node *)cref;
+            n->expr = NULL;
+            n->is_add = false;
+            n->is_label_op = true;
+            n->label_name = $3;
+            n->location = @1;
 
             $$ = (Node *)n;
         }
diff --git a/src/include/nodes/cypher_nodes.h b/src/include/nodes/cypher_nodes.h
index 0a098715..271aa49f 100644
--- a/src/include/nodes/cypher_nodes.h
+++ b/src/include/nodes/cypher_nodes.h
@@ -103,6 +103,8 @@ typedef struct cypher_set_item
     Node *prop; /* LHS */
     Node *expr; /* RHS */
     bool is_add; /* true if this is += */
+    bool is_label_op; /* true if this is a label SET/REMOVE operation */
+    char *label_name; /* label name for SET/REMOVE label operations */
     int location;
 } cypher_set_item;
 
@@ -453,6 +455,8 @@ typedef struct cypher_update_item
     List *qualified_name;
     bool remove_item;
     bool is_add;
+    bool is_label_op; /* true if this is a label SET/REMOVE operation */
+    char *label_name; /* label name for SET/REMOVE label operations */
 } cypher_update_item;
 
 typedef struct cypher_delete_information

Reply via email to