diff --git a/engine/src/main/java/com/arcadedb/query/opencypher/executor/steps/MergeStep.java b/engine/src/main/java/com/arcadedb/query/opencypher/executor/steps/MergeStep.java index 7866bc95a6..a23cc72c0a 100644 --- a/engine/src/main/java/com/arcadedb/query/opencypher/executor/steps/MergeStep.java +++ b/engine/src/main/java/com/arcadedb/query/opencypher/executor/steps/MergeStep.java @@ -306,24 +306,32 @@ private boolean mergePath(final PathPattern pathPattern, final ResultInternal re * @return matching vertex or null */ private Vertex findNode(final NodePattern nodePattern, final Result result) { - if (!nodePattern.hasLabels() || !nodePattern.hasProperties()) { - // Can't match without label and properties + if (!nodePattern.hasLabels()) { + // Can't match without a label return null; } final String label = nodePattern.getFirstLabel(); + + // Check if the type exists in the schema before iterating + if (!context.getDatabase().getSchema().existsType(label)) { + return null; + } + @SuppressWarnings("unchecked") final Iterator iterator = (Iterator) (Object) context.getDatabase().iterateType(label, true); - // Evaluate property expressions against current result context - final Map evaluatedProperties = evaluateProperties(nodePattern.getProperties(), result); + // Evaluate property expressions against current result context (may be empty) + final Map evaluatedProperties = nodePattern.hasProperties() + ? evaluateProperties(nodePattern.getProperties(), result) + : null; - // Find first vertex matching all properties + // Find first vertex matching all properties (or any vertex if no properties specified) while (iterator.hasNext()) { final Identifiable identifiable = iterator.next(); if (identifiable instanceof Vertex) { final Vertex vertex = (Vertex) identifiable; - if (matchesProperties(vertex, evaluatedProperties)) { + if (evaluatedProperties == null || matchesProperties(vertex, evaluatedProperties)) { return vertex; } } diff --git a/engine/src/test/java/com/arcadedb/query/opencypher/OpenCypherMergeTest.java b/engine/src/test/java/com/arcadedb/query/opencypher/OpenCypherMergeTest.java index e7c905c158..faa89b1022 100644 --- a/engine/src/test/java/com/arcadedb/query/opencypher/OpenCypherMergeTest.java +++ b/engine/src/test/java/com/arcadedb/query/opencypher/OpenCypherMergeTest.java @@ -161,6 +161,66 @@ void testMergeRelationship() { assertThat(count).isEqualTo(1); } + /** + * Test that MERGE with label only (no properties) finds existing node instead of creating duplicates. + * This is the pattern: MERGE (n:PIPELINE_CONFIG) ON CREATE SET n.pipelines = ["miaou"] ON MATCH SET n.pipelines = ["miaou"] + */ + @Test + void testMergeLabelOnlyFindsExistingNode() { + database.getSchema().createVertexType("PIPELINE_CONFIG"); + + // First MERGE should create the node + database.transaction(() -> { + final ResultSet result = database.command("opencypher", + "MERGE (n:PIPELINE_CONFIG) ON CREATE SET n.pipelines = ['miaou'] ON MATCH SET n.pipelines = ['miaou'] RETURN n.pipelines as pipelines"); + assertThat(result.hasNext()).isTrue(); + result.next(); + }); + + // Second MERGE should find the existing node, not create a duplicate + database.transaction(() -> { + final ResultSet result = database.command("opencypher", + "MERGE (n:PIPELINE_CONFIG) ON CREATE SET n.pipelines = ['miaou'] ON MATCH SET n.pipelines = ['miaou'] RETURN n.pipelines as pipelines"); + assertThat(result.hasNext()).isTrue(); + result.next(); + }); + + // Verify only one node exists + final ResultSet verify = database.query("opencypher", + "MATCH (n:PIPELINE_CONFIG) RETURN n"); + int count = 0; + while (verify.hasNext()) { + verify.next(); + count++; + } + assertThat(count).isEqualTo(1); + } + + /** + * Test that MERGE with label only (no properties) correctly triggers ON CREATE SET on first call + * and ON MATCH SET on subsequent calls. + */ + @Test + void testMergeLabelOnlyWithOnCreateAndOnMatchSet() { + database.getSchema().createVertexType("SINGLETON"); + + // First MERGE should create and apply ON CREATE SET + ResultSet result = database.command("opencypher", + "MERGE (n:SINGLETON) ON CREATE SET n.status = 'created', n.count = 1 ON MATCH SET n.status = 'matched', n.count = 2 RETURN n"); + assertThat(result.hasNext()).isTrue(); + Vertex v = (Vertex) result.next().getProperty("n"); + assertThat(v.get("status")).isEqualTo("created"); + assertThat(((Number) v.get("count")).intValue()).isEqualTo(1); + + // Second MERGE should match and apply ON MATCH SET + result = database.command("opencypher", + "MERGE (n:SINGLETON) ON CREATE SET n.status = 'created', n.count = 1 ON MATCH SET n.status = 'matched', n.count = 2 RETURN n"); + assertThat(result.hasNext()).isTrue(); + v = (Vertex) result.next().getProperty("n"); + assertThat(v.get("status")).isEqualTo("matched"); + assertThat(((Number) v.get("count")).intValue()).isEqualTo(2); + } + /** * Test for issue #3217: Backticks in relationship types should be treated as escape characters, * not included in the relationship type name.