diff --git a/README.adoc b/README.adoc index c1ca29ca10..6aef624a73 100644 --- a/README.adoc +++ b/README.adoc @@ -6,7 +6,7 @@ :artifactId: neo4j-cypher-dsl // This will be next version and also the one that will be put into the manual for the main branch -:neo4j-cypher-dsl-version: 2024.3.3-SNAPSHOT +:neo4j-cypher-dsl-version: 2024.4.0-SNAPSHOT // This is the latest released version, used only in the readme :neo4j-cypher-dsl-version-latest: 2024.3.2 diff --git a/neo4j-cypher-dsl-parser/src/main/java/org/neo4j/cypherdsl/parser/CypherDslASTFactory.java b/neo4j-cypher-dsl-parser/src/main/java/org/neo4j/cypherdsl/parser/CypherDslASTFactory.java index 8ee1049985..f6d0c6cb42 100644 --- a/neo4j-cypher-dsl-parser/src/main/java/org/neo4j/cypherdsl/parser/CypherDslASTFactory.java +++ b/neo4j-cypher-dsl-parser/src/main/java/org/neo4j/cypherdsl/parser/CypherDslASTFactory.java @@ -57,6 +57,7 @@ import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.ExposesRelationships; import org.neo4j.cypherdsl.core.Expression; +import org.neo4j.cypherdsl.core.Finish; import org.neo4j.cypherdsl.core.FunctionInvocation; import org.neo4j.cypherdsl.core.Hint; import org.neo4j.cypherdsl.core.KeyValueMapEntry; @@ -98,7 +99,7 @@ final class CypherDslASTFactory implements ASTFactory< Statement, Statement, Clause, - Clause, + Finish, Return, Expression, List, @@ -318,8 +319,8 @@ public Clause functionUseClause(InputPosition p, Expression function) { } @Override - public Clause newFinishClause(InputPosition p) { - throw new UnsupportedOperationException(); + public Finish newFinishClause(InputPosition p) { + return Finish.create(); } public List newReturnItems(InputPosition p, boolean returnAll, List returnItems) { diff --git a/neo4j-cypher-dsl-parser/src/test/java/org/neo4j/cypherdsl/parser/CypherDslASTFactoryTest.java b/neo4j-cypher-dsl-parser/src/test/java/org/neo4j/cypherdsl/parser/CypherDslASTFactoryTest.java index 18f7501290..1fe95369b3 100644 --- a/neo4j-cypher-dsl-parser/src/test/java/org/neo4j/cypherdsl/parser/CypherDslASTFactoryTest.java +++ b/neo4j-cypher-dsl-parser/src/test/java/org/neo4j/cypherdsl/parser/CypherDslASTFactoryTest.java @@ -119,7 +119,7 @@ class HandleNewMethods { "isNotNormalized", "normalizeExpression", "insertClause", "insertPathPattern", "subqueryInTransactionsBatchParameters", "subqueryInTransactionsConcurrencyParameters", "subqueryInTransactionsErrorParameters", - "grantRoles", "showRoles", "renameRole", "revokeRoles", "newFinishClause", + "grantRoles", "showRoles", "renameRole", "revokeRoles", "auth", "authId", "password", "passwordChangeRequired", "dynamicLabelLeaf", "databasePrivilegeScope" }) void newMethodsShouldNotBeSupportedOOTB(String methodName) { var factory = CypherDslASTFactory.getInstance(null); diff --git a/neo4j-cypher-dsl/src/main/java/org/neo4j/cypherdsl/core/DefaultStatementBuilder.java b/neo4j-cypher-dsl/src/main/java/org/neo4j/cypherdsl/core/DefaultStatementBuilder.java index c53a3ba771..5f2339f224 100644 --- a/neo4j-cypher-dsl/src/main/java/org/neo4j/cypherdsl/core/DefaultStatementBuilder.java +++ b/neo4j-cypher-dsl/src/main/java/org/neo4j/cypherdsl/core/DefaultStatementBuilder.java @@ -55,7 +55,7 @@ class DefaultStatementBuilder implements StatementBuilder, OngoingUpdate, OngoingMerge, OngoingReadingWithWhere, OngoingReadingWithoutWhere, OngoingMatchAndUpdate, BuildableMatchAndUpdate, - BuildableOngoingMergeAction, ExposesSubqueryCall.BuildableSubquery, StatementBuilder.VoidCall { + BuildableOngoingMergeAction, ExposesSubqueryCall.BuildableSubquery, StatementBuilder.VoidCall, StatementBuilder.Terminal { /** * Current list of reading or update clauses to be generated. @@ -407,10 +407,10 @@ public Statement build() { return buildImpl(null); } - protected final Statement buildImpl(Return returning) { + protected final Statement buildImpl(Clause returnOrFinish) { SinglePartQuery singlePartQuery = SinglePartQuery.create( - buildListOfVisitables(false), returning); + buildListOfVisitables(false), returnOrFinish); if (multiPartElements.isEmpty()) { return singlePartQuery; @@ -560,6 +560,11 @@ public final OngoingReadingWithoutWhere usingJoinOn(SymbolicName... names) { return new ForeachBuilder(variable); } + @Override + public @NotNull Terminal finish() { + return new DefaultStatementWithFinishBuilder(); + } + final class ForeachBuilder implements ForeachSourceStep, ForeachUpdateStep { private final SymbolicName variable; @@ -708,6 +713,14 @@ public ResultStatement build() { } } + protected final class DefaultStatementWithFinishBuilder implements Terminal { + + @Override + public @NotNull Statement build() { + return (ResultStatement) DefaultStatementBuilder.this.buildImpl(Finish.create()); + } + } + /** * Helper class aggregating a couple of interface, collecting conditions and returned objects. */ @@ -1077,6 +1090,11 @@ public LoadCSVStatementBuilder.OngoingLoadCSV loadCSV(URI from, boolean withHead .addWith(buildWith()) .foreach(variable); } + + @Override + public @NotNull StatementBuilder.Terminal finish() { + return DefaultStatementBuilder.this; + } } /** @@ -1480,6 +1498,13 @@ public Statement build() { DefaultStatementBuilder.this.addUpdatingClause(builder.build()); return DefaultStatementBuilder.this.foreach(variable); } + + @Override + public @NotNull StatementBuilder.Terminal finish() { + + DefaultStatementBuilder.this.addUpdatingClause(builder.build()); + return new DefaultStatementWithFinishBuilder(); + } } // Static builder and support classes @@ -1622,7 +1647,7 @@ public ProcedureCall build() { } static final class YieldingStandaloneCallBuilder extends AbstractCallBuilder - implements ExposesWhere, ExposesReturning, OngoingStandaloneCallWithReturnFields { + implements ExposesWhere, ExposesReturning, ExposesFinish, OngoingStandaloneCallWithReturnFields { private final YieldItems yieldItems; @@ -1730,6 +1755,11 @@ public ExposesAndThen andThen( this.delegate.currentSinglePartElements.add(statement); return this; } + + @Override + public @NotNull StatementBuilder.Terminal finish() { + return new DefaultStatementBuilder(this.buildCall()); + } } final class InQueryCallBuilder extends AbstractCallBuilder implements @@ -1865,6 +1895,11 @@ public VoidCall withoutResults() { DefaultStatementBuilder.this.currentSinglePartElements.add(this.buildCall()); return DefaultStatementBuilder.this.foreach(variable); } + + @Override + public @NotNull StatementBuilder.Terminal finish() { + return DefaultStatementBuilder.this; + } } static final class ConditionBuilder { diff --git a/neo4j-cypher-dsl/src/main/java/org/neo4j/cypherdsl/core/ExposesFinish.java b/neo4j-cypher-dsl/src/main/java/org/neo4j/cypherdsl/core/ExposesFinish.java new file mode 100644 index 0000000000..bb915f9d7a --- /dev/null +++ b/neo4j-cypher-dsl/src/main/java/org/neo4j/cypherdsl/core/ExposesFinish.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2019-2024 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.cypherdsl.core; + +import static org.apiguardian.api.API.Status.STABLE; + +import org.apiguardian.api.API; +import org.jetbrains.annotations.NotNull; +import org.neo4j.cypherdsl.core.annotations.CheckReturnValue; + +/** + * A step to finish the query without the need to return anything. + * + * @author Gerrit Meier + */ +@API(status = STABLE, since = "2024.4.0") +public interface ExposesFinish { + + @NotNull @CheckReturnValue + StatementBuilder.Terminal finish(); + +} diff --git a/neo4j-cypher-dsl/src/main/java/org/neo4j/cypherdsl/core/Finish.java b/neo4j-cypher-dsl/src/main/java/org/neo4j/cypherdsl/core/Finish.java new file mode 100644 index 0000000000..cf442e0070 --- /dev/null +++ b/neo4j-cypher-dsl/src/main/java/org/neo4j/cypherdsl/core/Finish.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2019-2024 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.cypherdsl.core; + +import static org.apiguardian.api.API.Status.STABLE; + +import org.apiguardian.api.API; +import org.neo4j.cypherdsl.core.ast.Visitor; + +/** + * @author Gerrit Meier + * @author Michael J. Simons + */ +@API(status = STABLE, since = "2024.4.0") +public final class Finish implements Clause { + + private static final Expression FINISH_EXPRESSION = new RawLiteral.RawElement("FINISH"); + + private Finish() { + + } + + public static Finish create() { + return new Finish(); + } + + @Override + public void accept(Visitor visitor) { + visitor.enter(this); + FINISH_EXPRESSION.accept(visitor); + visitor.leave(this); + } +} diff --git a/neo4j-cypher-dsl/src/main/java/org/neo4j/cypherdsl/core/SinglePartQuery.java b/neo4j-cypher-dsl/src/main/java/org/neo4j/cypherdsl/core/SinglePartQuery.java index 7b410fa4b2..bdedd49156 100644 --- a/neo4j-cypher-dsl/src/main/java/org/neo4j/cypherdsl/core/SinglePartQuery.java +++ b/neo4j-cypher-dsl/src/main/java/org/neo4j/cypherdsl/core/SinglePartQuery.java @@ -38,19 +38,19 @@ @API(status = INTERNAL, since = "1.0") class SinglePartQuery extends AbstractStatement implements SingleQuery { - static SinglePartQuery create(List precedingClauses, Return aReturn) { + static SinglePartQuery create(List precedingClauses, Clause returnOrFinish) { if (precedingClauses.isEmpty() || precedingClauses.get(precedingClauses.size() - 1) instanceof Match) { - Assertions.notNull(aReturn, "A return clause is required."); + Assertions.notNull(returnOrFinish, "A returning or finishing clause is required."); } - if (aReturn == null) { + if (returnOrFinish == null) { if (precedingClauses.get(precedingClauses.size() - 1) instanceof ResultStatement) { return new SinglePartQueryAsResultStatementWrapper(precedingClauses); } return new SinglePartQuery(precedingClauses); } else { - return new SinglePartQueryWithResult(precedingClauses, aReturn); + return new SinglePartQueryWithFinishingClause(precedingClauses, returnOrFinish); } } @@ -76,21 +76,21 @@ private SinglePartQueryAsResultStatementWrapper(List precedingClauses } } - static final class SinglePartQueryWithResult extends SinglePartQuery implements ResultStatement { + static final class SinglePartQueryWithFinishingClause extends SinglePartQuery implements ResultStatement { - private final Return aReturn; + private final Clause finishingClause; - private SinglePartQueryWithResult(List precedingClauses, Return aReturn) { + private SinglePartQueryWithFinishingClause(List precedingClauses, Clause finishingClause) { super(precedingClauses); - this.aReturn = aReturn; + this.finishingClause = finishingClause; } @Override public void accept(Visitor visitor) { visitor.enter(this); super.precedingClauses.forEach(c -> c.accept(visitor)); - aReturn.accept(visitor); + finishingClause.accept(visitor); visitor.leave(this); } diff --git a/neo4j-cypher-dsl/src/main/java/org/neo4j/cypherdsl/core/StatementBuilder.java b/neo4j-cypher-dsl/src/main/java/org/neo4j/cypherdsl/core/StatementBuilder.java index 437adafb10..3c0cc3db36 100644 --- a/neo4j-cypher-dsl/src/main/java/org/neo4j/cypherdsl/core/StatementBuilder.java +++ b/neo4j-cypher-dsl/src/main/java/org/neo4j/cypherdsl/core/StatementBuilder.java @@ -37,7 +37,7 @@ */ @API(status = STABLE, since = "1.0") public interface StatementBuilder - extends ExposesMatch, ExposesCreate, ExposesMerge, ExposesUnwind, ExposesReturning, ExposesSubqueryCall, ExposesWith { + extends ExposesMatch, ExposesCreate, ExposesMerge, ExposesUnwind, ExposesReturning, ExposesFinish, ExposesSubqueryCall, ExposesWith { /** * An ongoing update statement that can be used to chain more update statements or add a with or return clause. @@ -45,7 +45,7 @@ public interface StatementBuilder * @since 1.0 */ interface OngoingUpdate extends BuildableStatement, - ExposesCreate, ExposesMerge, ExposesDelete, ExposesReturning, ExposesWith, ExposesSet, ExposesForeach { + ExposesCreate, ExposesMerge, ExposesDelete, ExposesReturning, ExposesFinish, ExposesWith, ExposesSet, ExposesForeach { } /** @@ -103,7 +103,7 @@ interface OngoingReadingWithWhere extends OngoingReading, ExposesMatch, * @since 1.0 */ interface OngoingReading - extends ExposesReturning, ExposesWith, ExposesUpdatingClause, ExposesUnwind, ExposesCreate, ExposesMatch, + extends ExposesReturning, ExposesFinish, ExposesWith, ExposesUpdatingClause, ExposesUnwind, ExposesCreate, ExposesMatch, ExposesCall, ExposesSubqueryCall { } @@ -430,6 +430,16 @@ interface TerminalExposesLimit extends BuildableStatement { BuildableStatement limit(Expression expression); } + /** + * Terminal operation that only allows access to {@link BuildableStatement}. + * A marker interface to clarify the intention instead of just exposing the {@code BuildableStatement}. + * + * @since 2024.3.0 + */ + interface Terminal extends BuildableStatement { + + } + /** * See {@link TerminalExposesOrderBy}, but on a with clause. * @@ -837,7 +847,7 @@ interface ExposesSetAndRemove extends ExposesSet, ExposesSetLabel, ExposesMatch, ExposesWhere, - ExposesReturning, ExposesWith, ExposesSubqueryCall, + ExposesReturning, ExposesFinish, ExposesWith, ExposesSubqueryCall, ExposesAndThen permits DefaultStatementBuilder.YieldingStandaloneCallBuilder { } @@ -1062,6 +1072,6 @@ interface VoidCall extends OngoingReading { * An in-query call exposing where and return clauses. */ interface OngoingInQueryCallWithReturnFields extends - ExposesMatch, ExposesWhere, ExposesReturning, ExposesWith, ExposesSubqueryCall, ExposesForeach { + ExposesMatch, ExposesWhere, ExposesReturning, ExposesFinish, ExposesWith, ExposesSubqueryCall, ExposesForeach { } } diff --git a/neo4j-cypher-dsl/src/test/java/org/neo4j/cypherdsl/core/CypherIT.java b/neo4j-cypher-dsl/src/test/java/org/neo4j/cypherdsl/core/CypherIT.java index 8f5c07123e..fdc4c8937e 100644 --- a/neo4j-cypher-dsl/src/test/java/org/neo4j/cypherdsl/core/CypherIT.java +++ b/neo4j-cypher-dsl/src/test/java/org/neo4j/cypherdsl/core/CypherIT.java @@ -524,7 +524,72 @@ void distinct() { .isEqualTo( expected); } + + } + } + + @Nested + class Finish { + @Test + void finishAfterMatch() { + String expected = "MATCH (u:`User`) FINISH"; + + Statement statement = Cypher.match(userNode).finish().build(); + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + expected); + } + + @Test + void finishAfterMatchWithWhere() { + String expected = "MATCH (u:`User`) WHERE u:`User` FINISH"; + + Statement statement = Cypher.match(userNode).where(userNode.hasLabels("User")).finish().build(); + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + expected); + } + + @Test + void finishAfterSet() { + String expected = "MATCH (u:`User`) SET u.name = 'hans' FINISH"; + + Statement statement = Cypher.match(userNode).set(userNode.property("name").to(Cypher.literalOf("hans"))).finish().build(); + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + expected); + } + + @Test + void finishAfterDelete() { + String expected = "MATCH (u:`User`) DELETE u FINISH"; + + Statement statement = Cypher.match(userNode).delete(userNode).finish().build(); + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + expected); + } + + @Test + void finishAfterCreate() { + String expected = "CREATE (u:`User`) FINISH"; + + Statement statement = Cypher.create(userNode).finish().build(); + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + expected); + } + + @Test + void finishAfterMerge() { + String expected = "MERGE (u:`User`)-[:`KNOWS`]->(u) FINISH"; + + Statement statement = Cypher.merge(userNode.relationshipTo(userNode, "KNOWS")).finish().build(); + assertThat(cypherRenderer.render(statement)) + .isEqualTo( + expected); } + } @Nested diff --git a/neo4j-cypher-dsl/src/test/java/org/neo4j/cypherdsl/core/LoadCSVIT.java b/neo4j-cypher-dsl/src/test/java/org/neo4j/cypherdsl/core/LoadCSVIT.java index ff4e54e8e0..3b242ff171 100644 --- a/neo4j-cypher-dsl/src/test/java/org/neo4j/cypherdsl/core/LoadCSVIT.java +++ b/neo4j-cypher-dsl/src/test/java/org/neo4j/cypherdsl/core/LoadCSVIT.java @@ -271,4 +271,21 @@ void inQuery() { assertThat(statement.getCypher()) .isEqualTo("MATCH (u:`User` {name: 'Michael'}) WITH u ORDER BY u.name ASC LOAD CSV FROM 'file:///bikes.csv' AS row MERGE (u)-[:`OWNS`]->(:`Bike` {name: row[0]})"); } + + @Test + void finish() { + SymbolicName row = SymbolicName.of("row"); + Node userNode = Cypher.node("User").named("u").withProperties("name", Cypher.literalOf("Michael")); + + Statement statement = Cypher.match(userNode) + .with(userNode) + .orderBy(userNode.property("name")).ascending() + .loadCSV(URI.create("file:///bikes.csv")) + .as(row) + .finish() + .build(); + + assertThat(statement.getCypher()) + .isEqualTo("MATCH (u:`User` {name: 'Michael'}) WITH u ORDER BY u.name ASC LOAD CSV FROM 'file:///bikes.csv' AS row FINISH"); + } }