diff --git a/regress/expected/cypher_remove.out b/regress/expected/cypher_remove.out index 6aaeeea8f..6b6816051 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 b6037b610..58a3f1e36 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 f9f30f667..8eebf9c1d 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 c13cee04b..07ca0fcb9 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 db8408b75..4ba27afb8 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 a0fa6a4b0..0d352fc8f 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 15f5dac7d..8c24e7df3 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 089193292..eb7dccf01 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 5ba1e6354..7604b6a24 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 0a0987157..271aa49fc 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