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