@@ -1525,6 +1525,290 @@ func (e *StorageExecutor) executeCompoundCreateWithDelete(ctx context.Context, c
15251525 return result , nil
15261526}
15271527
1528+ // executeCreateSet handles CREATE ... SET queries (Neo4j compatibility).
1529+ // Neo4j allows SET immediately after CREATE to set additional properties
1530+ // on newly created nodes/relationships.
1531+ // Example: CREATE (n:Node {id: 'test'}) SET n.content = 'value' RETURN n
1532+ func (e * StorageExecutor ) executeCreateSet (ctx context.Context , cypher string ) (* ExecuteResult , error ) {
1533+ // Substitute parameters AFTER routing to avoid keyword detection issues
1534+ if params := getParamsFromContext (ctx ); params != nil {
1535+ cypher = e .substituteParams (cypher , params )
1536+ }
1537+
1538+ result := & ExecuteResult {
1539+ Columns : []string {},
1540+ Rows : [][]interface {}{},
1541+ Stats : & QueryStats {},
1542+ }
1543+
1544+ // Normalize whitespace for index finding (newlines/tabs become spaces)
1545+ normalized := strings .ReplaceAll (strings .ReplaceAll (cypher , "\n " , " " ), "\t " , " " )
1546+
1547+ // Find clause boundaries
1548+ setIdx := findKeywordIndex (normalized , "SET" )
1549+ returnIdx := findKeywordIndex (normalized , "RETURN" )
1550+
1551+ if setIdx < 0 {
1552+ return nil , fmt .Errorf ("SET clause not found in CREATE...SET query" )
1553+ }
1554+
1555+ // Extract CREATE part (everything before SET)
1556+ createPart := strings .TrimSpace (normalized [:setIdx ])
1557+
1558+ // Extract SET part (between SET and RETURN, or end)
1559+ var setPart string
1560+ if returnIdx > 0 {
1561+ setPart = strings .TrimSpace (normalized [setIdx + 4 : returnIdx ])
1562+ } else {
1563+ setPart = strings .TrimSpace (normalized [setIdx + 4 :])
1564+ }
1565+
1566+ // Pre-validate SET assignments BEFORE executing CREATE
1567+ // This ensures we fail fast and don't create nodes that would be orphaned
1568+ if ! strings .Contains (setPart , "+=" ) {
1569+ assignments := e .splitSetAssignmentsRespectingBrackets (setPart )
1570+ if err := e .validateSetAssignments (assignments ); err != nil {
1571+ return nil , err
1572+ }
1573+ }
1574+
1575+ // Execute CREATE first and get references to created entities
1576+ createResult , createdNodes , createdEdges , err := e .executeCreateWithRefs (ctx , createPart )
1577+ if err != nil {
1578+ return nil , fmt .Errorf ("CREATE failed in CREATE...SET: %w" , err )
1579+ }
1580+ result .Stats .NodesCreated = createResult .Stats .NodesCreated
1581+ result .Stats .RelationshipsCreated = createResult .Stats .RelationshipsCreated
1582+
1583+ // Check for property merge operator: n += $properties
1584+ if strings .Contains (setPart , "+=" ) {
1585+ // Handle property merge on created entities
1586+ err := e .applySetMergeToCreated (setPart , createdNodes , createdEdges , result )
1587+ if err != nil {
1588+ return nil , err
1589+ }
1590+ } else {
1591+ // Handle regular SET assignments
1592+ // Split SET clause into individual assignments
1593+ assignments := e .splitSetAssignmentsRespectingBrackets (setPart )
1594+
1595+ for _ , assignment := range assignments {
1596+ assignment = strings .TrimSpace (assignment )
1597+ if assignment == "" {
1598+ continue
1599+ }
1600+
1601+ // Parse assignment: var.property = value
1602+ eqIdx := strings .Index (assignment , "=" )
1603+ if eqIdx == - 1 {
1604+ // Could be a label assignment like "n:Label"
1605+ colonIdx := strings .Index (assignment , ":" )
1606+ if colonIdx > 0 {
1607+ varName := strings .TrimSpace (assignment [:colonIdx ])
1608+ newLabel := strings .TrimSpace (assignment [colonIdx + 1 :])
1609+ if node , exists := createdNodes [varName ]; exists {
1610+ // Add label to existing node
1611+ if ! containsString (node .Labels , newLabel ) {
1612+ node .Labels = append (node .Labels , newLabel )
1613+ if err := e .storage .UpdateNode (node ); err != nil {
1614+ return nil , fmt .Errorf ("failed to add label: %w" , err )
1615+ }
1616+ result .Stats .LabelsAdded ++
1617+ }
1618+ }
1619+ }
1620+ continue
1621+ }
1622+
1623+ leftSide := strings .TrimSpace (assignment [:eqIdx ])
1624+ rightSide := strings .TrimSpace (assignment [eqIdx + 1 :])
1625+
1626+ // Parse variable.property
1627+ dotIdx := strings .Index (leftSide , "." )
1628+ if dotIdx == - 1 {
1629+ return nil , fmt .Errorf ("invalid SET assignment (expected var.property): %s" , assignment )
1630+ }
1631+
1632+ varName := strings .TrimSpace (leftSide [:dotIdx ])
1633+ propName := strings .TrimSpace (leftSide [dotIdx + 1 :])
1634+
1635+ // Note: Function validation is already done in validateSetAssignments()
1636+ // which runs before CREATE to ensure rollback safety
1637+
1638+ // Parse the value
1639+ value := e .parseValue (rightSide )
1640+
1641+ // Apply to created node or edge
1642+ if node , exists := createdNodes [varName ]; exists {
1643+ node .Properties [propName ] = value
1644+ if err := e .storage .UpdateNode (node ); err != nil {
1645+ return nil , fmt .Errorf ("failed to update node property: %w" , err )
1646+ }
1647+ result .Stats .PropertiesSet ++
1648+ } else if edge , exists := createdEdges [varName ]; exists {
1649+ edge .Properties [propName ] = value
1650+ if err := e .storage .UpdateEdge (edge ); err != nil {
1651+ return nil , fmt .Errorf ("failed to update edge property: %w" , err )
1652+ }
1653+ result .Stats .PropertiesSet ++
1654+ } else {
1655+ return nil , fmt .Errorf ("unknown variable in SET clause: %s" , varName )
1656+ }
1657+ }
1658+ }
1659+
1660+ // Handle RETURN clause
1661+ if returnIdx > 0 {
1662+ returnPart := strings .TrimSpace (normalized [returnIdx + 6 :])
1663+
1664+ // Parse return items
1665+ returnItems := splitReturnExpressions (returnPart )
1666+ for _ , item := range returnItems {
1667+ item = strings .TrimSpace (item )
1668+ alias := item
1669+
1670+ // Check for alias
1671+ upperItem := strings .ToUpper (item )
1672+ if asIdx := strings .Index (upperItem , " AS " ); asIdx > 0 {
1673+ alias = strings .TrimSpace (item [asIdx + 4 :])
1674+ item = strings .TrimSpace (item [:asIdx ])
1675+ }
1676+
1677+ result .Columns = append (result .Columns , alias )
1678+
1679+ // Resolve the value
1680+ if node , exists := createdNodes [item ]; exists {
1681+ if len (result .Rows ) == 0 {
1682+ result .Rows = append (result .Rows , []interface {}{})
1683+ }
1684+ result .Rows [0 ] = append (result .Rows [0 ], node )
1685+ } else if edge , exists := createdEdges [item ]; exists {
1686+ if len (result .Rows ) == 0 {
1687+ result .Rows = append (result .Rows , []interface {}{})
1688+ }
1689+ result .Rows [0 ] = append (result .Rows [0 ], edge )
1690+ } else {
1691+ // Could be an expression or property access
1692+ if len (result .Rows ) == 0 {
1693+ result .Rows = append (result .Rows , []interface {}{})
1694+ }
1695+ result .Rows [0 ] = append (result .Rows [0 ], nil )
1696+ }
1697+ }
1698+ } else {
1699+ // No RETURN clause - return created entities by default
1700+ for _ , node := range createdNodes {
1701+ if len (result .Columns ) == 0 {
1702+ result .Columns = append (result .Columns , "node" )
1703+ }
1704+ if len (result .Rows ) == 0 {
1705+ result .Rows = append (result .Rows , []interface {}{})
1706+ }
1707+ result .Rows [0 ] = append (result .Rows [0 ], node )
1708+ }
1709+ }
1710+
1711+ return result , nil
1712+ }
1713+
1714+ // applySetMergeToCreated applies SET += property merge to created entities.
1715+ func (e * StorageExecutor ) applySetMergeToCreated (setPart string , createdNodes map [string ]* storage.Node , createdEdges map [string ]* storage.Edge , result * ExecuteResult ) error {
1716+ // Parse: n += {prop: value, ...}
1717+ parts := strings .SplitN (setPart , "+=" , 2 )
1718+ if len (parts ) != 2 {
1719+ return fmt .Errorf ("invalid SET += syntax" )
1720+ }
1721+
1722+ varName := strings .TrimSpace (parts [0 ])
1723+ propsStr := strings .TrimSpace (parts [1 ])
1724+
1725+ // Parse the properties map
1726+ props := e .parseMapLiteral (propsStr )
1727+ if props == nil {
1728+ return fmt .Errorf ("failed to parse properties in SET +=" )
1729+ }
1730+
1731+ // Apply to node or edge
1732+ if node , exists := createdNodes [varName ]; exists {
1733+ for k , v := range props {
1734+ node .Properties [k ] = v
1735+ result .Stats .PropertiesSet ++
1736+ }
1737+ if err := e .storage .UpdateNode (node ); err != nil {
1738+ return fmt .Errorf ("failed to update node: %w" , err )
1739+ }
1740+ } else if edge , exists := createdEdges [varName ]; exists {
1741+ for k , v := range props {
1742+ edge .Properties [k ] = v
1743+ result .Stats .PropertiesSet ++
1744+ }
1745+ if err := e .storage .UpdateEdge (edge ); err != nil {
1746+ return fmt .Errorf ("failed to update edge: %w" , err )
1747+ }
1748+ } else {
1749+ return fmt .Errorf ("unknown variable in SET +=: %s" , varName )
1750+ }
1751+
1752+ return nil
1753+ }
1754+
1755+ // containsString checks if a slice contains a string.
1756+ func containsString (slice []string , s string ) bool {
1757+ for _ , item := range slice {
1758+ if item == s {
1759+ return true
1760+ }
1761+ }
1762+ return false
1763+ }
1764+
1765+ // validateSetAssignments pre-validates SET clause assignments before executing CREATE
1766+ // This ensures we fail fast on invalid function calls, preventing orphaned nodes
1767+ func (e * StorageExecutor ) validateSetAssignments (assignments []string ) error {
1768+ // Known Cypher functions
1769+ knownFunctions := map [string ]bool {
1770+ "COALESCE" : true , "TOSTRING" : true , "TOINT" : true , "TOFLOAT" : true ,
1771+ "TOBOOLEAN" : true , "TOLOWER" : true , "TOUPPER" : true , "TRIM" : true ,
1772+ "SIZE" : true , "LENGTH" : true , "ABS" : true , "CEIL" : true , "FLOOR" : true ,
1773+ "ROUND" : true , "RAND" : true , "SQRT" : true , "SIGN" : true , "LOG" : true ,
1774+ "LOG10" : true , "EXP" : true , "SIN" : true , "COS" : true , "TAN" : true ,
1775+ "DATE" : true , "DATETIME" : true , "TIME" : true , "TIMESTAMP" : true ,
1776+ "DURATION" : true , "LOCALDATETIME" : true , "LOCALTIME" : true ,
1777+ "HEAD" : true , "LAST" : true , "TAIL" : true , "KEYS" : true , "LABELS" : true ,
1778+ "TYPE" : true , "ID" : true , "ELEMENTID" : true , "PROPERTIES" : true ,
1779+ "POINT" : true , "DISTANCE" : true , "REPLACE" : true , "SUBSTRING" : true ,
1780+ "LEFT" : true , "RIGHT" : true , "SPLIT" : true , "REVERSE" : true ,
1781+ "LTRIM" : true , "RTRIM" : true , "COLLECT" : true , "RANGE" : true ,
1782+ }
1783+
1784+ for _ , assignment := range assignments {
1785+ assignment = strings .TrimSpace (assignment )
1786+ if assignment == "" {
1787+ continue
1788+ }
1789+
1790+ // Parse assignment: var.property = value or var:Label
1791+ eqIdx := strings .Index (assignment , "=" )
1792+ if eqIdx == - 1 {
1793+ // Could be a label assignment like "n:Label" - these are valid
1794+ continue
1795+ }
1796+
1797+ rightSide := strings .TrimSpace (assignment [eqIdx + 1 :])
1798+
1799+ // Check if right side looks like a function call
1800+ if strings .Contains (rightSide , "(" ) && strings .HasSuffix (strings .TrimSpace (rightSide ), ")" ) {
1801+ // Extract function name (before first parenthesis)
1802+ parenIdx := strings .Index (rightSide , "(" )
1803+ funcName := strings .ToUpper (strings .TrimSpace (rightSide [:parenIdx ]))
1804+ if ! knownFunctions [funcName ] {
1805+ return fmt .Errorf ("unknown function: %s" , funcName )
1806+ }
1807+ }
1808+ }
1809+ return nil
1810+ }
1811+
15281812// executeMerge handles MERGE queries with ON CREATE SET / ON MATCH SET support.
15291813// This implements Neo4j-compatible MERGE semantics:
15301814// 1. Try to find an existing node matching the pattern
0 commit comments