From 80490a76cd4c921aa71c740f5549707f14fa5716 Mon Sep 17 00:00:00 2001 From: Andres Felder <81707831+andyfelder16@users.noreply.github.com> Date: Tue, 9 Jun 2026 11:41:11 -0300 Subject: [PATCH 1/2] add Neo4j Cypher query parser --- client-java/controller/pom.xml | 14 + .../neo4j/cypher25/Cypher25Lexer.g4 | 1402 ++++++++++++ .../neo4j/cypher25/Cypher25Parser.g4 | 2024 +++++++++++++++++ .../neo4j/conditions/AndCondition.java | 45 + .../neo4j/conditions/AnyLabelCondition.java | 37 + .../neo4j/conditions/ArithmeticOperand.java | 78 + .../neo4j/conditions/ArithmeticOperator.java | 15 + .../neo4j/conditions/ComparisonCondition.java | 59 + .../neo4j/conditions/ComparisonOperator.java | 38 + .../neo4j/conditions/CypherCondition.java | 14 + .../neo4j/conditions/LabelCondition.java | 47 + .../neo4j/conditions/ListOperand.java | 39 + .../neo4j/conditions/LiteralOperand.java | 39 + .../neo4j/conditions/NotCondition.java | 37 + .../controller/neo4j/conditions/Operand.java | 9 + .../neo4j/conditions/OrCondition.java | 45 + .../neo4j/conditions/PropertyCondition.java | 57 + .../neo4j/conditions/PropertyOperand.java | 47 + .../neo4j/conditions/RawCondition.java | 39 + .../neo4j/conditions/RawOperand.java | 37 + .../neo4j/conditions/TypeCondition.java | 47 + .../neo4j/conditions/XorCondition.java | 45 + .../operations/CypherQueryOperation.java | 7 + .../neo4j/operations/MatchOperation.java | 98 + .../neo4j/operations/MatchPattern.java | 76 + .../neo4j/operations/PatternEdge.java | 111 + .../neo4j/operations/PatternNode.java | 37 + .../operations/QuantifiedPathPattern.java | 74 + .../controller/neo4j/parser/CypherParser.java | 22 + .../neo4j/parser/CypherParserException.java | 20 + .../neo4j/parser/CypherParserFactory.java | 16 + .../parser/cypher25/Cypher25AntlrParser.java | 61 + .../parser/cypher25/Cypher25MatchVisitor.java | 834 +++++++ .../neo4j/parser/Cypher25AntlrParserTest.java | 808 +++++++ 34 files changed, 6378 insertions(+) create mode 100644 client-java/controller/src/main/antlr4/org/evomaster/client/java/controller/neo4j/cypher25/Cypher25Lexer.g4 create mode 100644 client-java/controller/src/main/antlr4/org/evomaster/client/java/controller/neo4j/cypher25/Cypher25Parser.g4 create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/AndCondition.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/AnyLabelCondition.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/ArithmeticOperand.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/ArithmeticOperator.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/ComparisonCondition.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/ComparisonOperator.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/CypherCondition.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/LabelCondition.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/ListOperand.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/LiteralOperand.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/NotCondition.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/Operand.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/OrCondition.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/PropertyCondition.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/PropertyOperand.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/RawCondition.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/RawOperand.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/TypeCondition.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/XorCondition.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/operations/CypherQueryOperation.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/operations/MatchOperation.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/operations/MatchPattern.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/operations/PatternEdge.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/operations/PatternNode.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/operations/QuantifiedPathPattern.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/parser/CypherParser.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/parser/CypherParserException.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/parser/CypherParserFactory.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/parser/cypher25/Cypher25AntlrParser.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/parser/cypher25/Cypher25MatchVisitor.java create mode 100644 client-java/controller/src/test/java/org/evomaster/client/java/controller/neo4j/parser/Cypher25AntlrParserTest.java diff --git a/client-java/controller/pom.xml b/client-java/controller/pom.xml index 27fa3c85a6..f11a021025 100644 --- a/client-java/controller/pom.xml +++ b/client-java/controller/pom.xml @@ -122,6 +122,10 @@ com.github.jsqlparser jsqlparser + + org.antlr + antlr4-runtime + org.junit.jupiter junit-jupiter-engine @@ -346,12 +350,22 @@ net.sf.jsqlparser shaded.net.sf.jsqlparser + + org.antlr + shaded.org.antlr + + + + org.antlr + antlr4-maven-plugin + + diff --git a/client-java/controller/src/main/antlr4/org/evomaster/client/java/controller/neo4j/cypher25/Cypher25Lexer.g4 b/client-java/controller/src/main/antlr4/org/evomaster/client/java/controller/neo4j/cypher25/Cypher25Lexer.g4 new file mode 100644 index 0000000000..57d1cd49d7 --- /dev/null +++ b/client-java/controller/src/main/antlr4/org/evomaster/client/java/controller/neo4j/cypher25/Cypher25Lexer.g4 @@ -0,0 +1,1402 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * 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 + * + * http://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. + */ +lexer grammar Cypher25Lexer; + +SPACE + : ( '\u0009' + | '\n' //can't parse this in unicode + | '\u000B' + | '\u000C' + | '\r' //can't parse this in unicode + | '\u001C' + | '\u001D' + | '\u001E' + | '\u001F' + | '\u0020' + | '\u0085' + | '\u00A0' + | '\u1680' + | '\u2000' + | '\u2001' + | '\u2002' + | '\u2003' + | '\u2004' + | '\u2005' + | '\u2006' + | '\u2007' + | '\u2008' + | '\u2009' + | '\u200A' + | '\u2028' + | '\u2029' + | '\u202F' + | '\u205F' + | '\u3000' + ) -> channel (HIDDEN) + ; + +SINGLE_LINE_COMMENT + : '//' ~[\r\n]* -> channel (HIDDEN) + ; + +MULTI_LINE_COMMENT + : '/*' .*? '*/' -> channel (HIDDEN) + ; + +DECIMAL_DOUBLE + : ([0-9] (INTEGER_PART)* '.' [0-9] (INTEGER_PART)* (DECIMAL_EXPONENT)? (IDENTIFIER)? | '.' [0-9] (INTEGER_PART)* (DECIMAL_EXPONENT)? (IDENTIFIER)? | [0-9] (INTEGER_PART)* DECIMAL_EXPONENT (IDENTIFIER)?) + ; + +UNSIGNED_DECIMAL_INTEGER + : ([1-9] (INTEGER_PART)* (PART_LETTER)* | '0') + ; + +fragment DECIMAL_EXPONENT + : [eE] ([+\-])? (INTEGER_PART)+ (PART_LETTER)* + ; + +fragment INTEGER_PART + : ('_')? [0-9] + ; + +UNSIGNED_HEX_INTEGER + : '0x' (PART_LETTER)* + ; + +UNSIGNED_OCTAL_INTEGER + : '0o' (PART_LETTER)* + ; + +STRING_LITERAL1 + : '\'' (~['\\] | EscapeSequence)* '\'' + ; + +STRING_LITERAL2 + : '"' (~["\\] | EscapeSequence)* '"' + ; + +// In Cypher it is allowed to have any character following a backslash. +// In the cases it is an actual escape code it is handled in the AST builder. +fragment EscapeSequence + : '\\' . + ; + +ESCAPED_SYMBOLIC_NAME + : '`' ( ~'`' | '``' )* '`' + ; + +ACCESS + : A C C E S S + ; + +ACTIVE + : A C T I V E + ; + +ADMIN + : A D M I N + ; + +ADMINISTRATOR + : A D M I N I S T R A T O R + ; + +ALIAS + : A L I A S + ; + +ALIASES + : A L I A S E S + ; + +ALL_SHORTEST_PATHS + : A L L S H O R T E S T P A T H S + ; + +ALL + : A L L + ; + +ALTER + : A L T E R + ; + +AND + : A N D + ; + +ANY + : A N Y + ; + +ARRAY + : A R R A Y + ; + +AS + : A S + ; + +ASC + : A S C + ; + +ASCENDING + : A S C E N D I N G + ; + +ASSIGN + : A S S I G N + ; + +AT + : A T + ; + +AUTH + : A U T H + ; + +BAR + : '|' + ; + +BINDINGS + : B I N D I N G S + ; + +BOOL + : B O O L + ; + +BOOLEAN + : B O O L E A N + ; + +BOOSTED + : B O O S T E D + ; + +BOTH + : B O T H + ; + +BREAK + : B R E A K + ; + +BUILT + : B U I L T + ; + +BY + : B Y + ; + +CALL + : C A L L + ; + +CASCADE + : C A S C A D E + ; + +CASE + : C A S E + ; + +CHANGE + : C H A N G E + ; + +CIDR + : C I D R + ; + +COLLECT + : C O L L E C T + ; + +COLON + : ':' + ; + +COLONCOLON + : '::' + ; + +COMMA + : ',' + ; + +COMMAND + : C O M M A N D + ; + +COMMANDS + : C O M M A N D S + ; + +COMPOSITE + : C O M P O S I T E + ; + +CONCURRENT + : C O N C U R R E N T + ; + +CONSTRAINT + : C O N S T R A I N T + ; + +CONSTRAINTS + : C O N S T R A I N T S + ; + +CONTAINS + : C O N T A I N S + ; + +COPY + : C O P Y + ; + +CONTINUE + : C O N T I N U E + ; + +COUNT + : C O U N T + ; + +CREATE + : C R E A T E + ; + +CSV + : C S V + ; + +CURRENT + : C U R R E N T + ; + +DATA + : D A T A + ; + +DATABASE + : D A T A B A S E + ; + +DATABASES + : D A T A B A S E S + ; + +DATE + : D A T E + ; + +DATETIME + : D A T E T I M E + ; + +DBMS + : D B M S + ; + +DEALLOCATE + : D E A L L O C A T E + ; + +DEFAULT + : D E F A U L T + ; + +DEFINED + : D E F I N E D + ; + +DELETE + : D E L E T E + ; + +DENY + : D E N Y + ; + +DESC + : D E S C + ; + +DESCENDING + : D E S C E N D I N G + ; + +DESTROY + : D E S T R O Y + ; + +DETACH + : D E T A C H + ; + +DIFFERENT + : D I F F E R E N T + ; + +DOLLAR + : '$' + ; + +DISTINCT + : D I S T I N C T + ; + +DIVIDE + : '/' + ; + +DOT + : '.' + ; + +DOTDOT + : '..' + ; + +DOUBLEBAR + : '||' + ; + +DRIVER + : D R I V E R + ; + +DROP + : D R O P + ; + +DRYRUN + : D R Y R U N + ; + +DUMP + : D U M P + ; + +DURATION + : D U R A T I O N + ; + +EACH + : E A C H + ; + +EDGE + : E D G E + ; + +ENABLE + : E N A B L E + ; + +ELEMENT + : E L E M E N T + ; + +ELEMENTS + : E L E M E N T S + ; + +ELSE + : E L S E + ; + +ENCRYPTED + : E N C R Y P T E D + ; + +END + : E N D + ; + +ENDS + : E N D S + ; + +EQ + : '=' + ; + +EXECUTABLE + : E X E C U T A B L E + ; + +EXECUTE + : E X E C U T E + ; + +EXIST + : E X I S T + ; + +EXISTENCE + : E X I S T E N C E + ; + +EXISTS + : E X I S T S + ; + +ERROR + : E R R O R + ; + +FAIL + : F A I L + ; + +FALSE + : F A L S E + ; + +FIELDTERMINATOR + : F I E L D T E R M I N A T O R + ; + +FINISH + : F I N I S H + ; + +FLOAT + : F L O A T + ; + +FOR + : F O R + ; + +FOREACH + : F O R E A C H + ; + +FROM + : F R O M + ; + +FULLTEXT + : F U L L T E X T + ; + +FUNCTION + : F U N C T I O N + ; + +FUNCTIONS + : F U N C T I O N S + ; + +GE + : '>=' + ; + +GRANT + : G R A N T + ; + +GRAPH + : G R A P H + ; + +GRAPHS + : G R A P H S + ; + +GROUP + : G R O U P + ; + +GROUPS + : G R O U P S + ; + +GT + : '>' + ; + +HEADERS + : H E A D E R S + ; + +HOME + : H O M E + ; + +ID + : I D + ; + +IF + : I F + ; + +IMPERSONATE + : I M P E R S O N A T E + ; + +IMMUTABLE + : I M M U T A B L E + ; + +IN + : I N + ; + +INDEX + : I N D E X + ; + +INDEXES + : I N D E X E S + ; + +INF + : I N F + ; + +INFINITY + : I N F I N I T Y + ; + +INSERT + : I N S E R T + ; + +INT + : I N T + ; + +INTEGER + : I N T E G E R + ; + +IS + : I S + ; + +JOIN + : J O I N + ; + +KEY + : K E Y + ; + +LABEL + : L A B E L + ; + +LABELS + : L A B E L S + ; + +AMPERSAND + : '&' + ; + +EXCLAMATION_MARK + : '!' + ; + +LBRACKET + : '[' + ; + +LCURLY + : '{' + ; + +LE + : '<=' + ; + +LEADING + : L E A D I N G + ; + +LIMITROWS + : L I M I T + ; + +LIST + : L I S T + ; + +LOAD + : L O A D + ; + +LOCAL + : L O C A L + ; + +LOOKUP + : L O O K U P + ; + +LPAREN + : '(' + ; + +LT + : '<' + ; + +MANAGEMENT + : M A N A G E M E N T + ; + +MAP + : M A P + ; + +MATCH + : M A T C H + ; + +MERGE + : M E R G E + ; + +MINUS + : '-' + ; + +PERCENT + : '%' + ; + +INVALID_NEQ + : '!=' + ; + +NEQ + : '<>' + ; + +NAME + : N A M E + ; + +NAMES + : N A M E S + ; + +NAN + : N A N + ; + +NFC + : N F C + ; + +NFD + : N F D + ; + +NFKC + : N F K C + ; + +NFKD + : N F K D + ; + +NEW + : N E W + ; + +NODE + : N O D E + ; + +NODETACH + : N O D E T A C H + ; + +NODES + : N O D E S + ; + +NONE + : N O N E + ; + +NORMALIZE + : N O R M A L I Z E + ; + +NORMALIZED + : N O R M A L I Z E D + ; + +NOT + : N O T + ; + +NOTHING + : N O T H I N G + ; + +NOWAIT + : N O W A I T + ; + +NULL + : N U L L + ; + +OF + : O F + ; + +OFFSET + : O F F S E T + ; + +ON + : O N + ; + +ONLY + : O N L Y + ; + +OPTIONAL + : O P T I O N A L + ; + +OPTIONS + : O P T I O N S + ; + +OPTION + : O P T I O N + ; + +OR + : O R + ; + +ORDER + : O R D E R + ; + +PASSWORD + : P A S S W O R D + ; + +PASSWORDS + : P A S S W O R D S + ; + +PATH + : P A T H + ; + +PATHS + : P A T H S + ; + +PLAINTEXT + : P L A I N T E X T + ; + +PLUS + : '+' + ; + +PLUSEQUAL + : '+=' + ; + +POINT + : P O I N T + ; + +POPULATED + : P O P U L A T E D + ; + +POW + : '^' + ; + +PRIMARY + : P R I M A R Y + ; + +PRIMARIES + : P R I M A R I E S + ; + +PRIVILEGE + : P R I V I L E G E + ; + +PRIVILEGES + : P R I V I L E G E S + ; + +PROCEDURE + : P R O C E D U R E + ; + +PROCEDURES + : P R O C E D U R E S + ; + +PROPERTIES + : P R O P E R T I E S + ; + +PROPERTY + : P R O P E R T Y + ; + +PROVIDER + : P R O V I D E R + ; + +PROVIDERS + : P R O V I D E R S + ; + +QUESTION + : '?' + ; + +RANGE + : R A N G E + ; + +RBRACKET + : ']' + ; + +RCURLY + : '}' + ; + +READ + : R E A D + ; + +REALLOCATE + : R E A L L O C A T E + ; + +REDUCE + : R E D U C E + ; + +RENAME + : R E N A M E + ; + +REGEQ + : '=~' + ; + +REL + : R E L + ; + +RELATIONSHIP + : R E L A T I O N S H I P + ; + +RELATIONSHIPS + : R E L A T I O N S H I P S + ; + +REMOVE + : R E M O V E + ; + +REPEATABLE + : R E P E A T A B L E + ; + +REPLACE + : R E P L A C E + ; + +REPORT + : R E P O R T + ; + +REQUIRE + : R E Q U I R E + ; + +REQUIRED + : R E Q U I R E D + ; + +RESTRICT + : R E S T R I C T + ; + +RETURN + : R E T U R N + ; + +REVOKE + : R E V O K E + ; + +ROLE + : R O L E + ; + +ROLES + : R O L E S + ; + +ROW + : R O W + ; + +ROWS + : R O W S + ; + +RPAREN + : ')' + ; + +SCAN + : S C A N + ; + +SEC + : S E C + ; + +SECOND + : S E C O N D + ; + +SECONDARY + : S E C O N D A R Y + ; + +SECONDARIES + : S E C O N D A R I E S + ; + +SECONDS + : S E C O N D S + ; + +SEEK + : S E E K + ; + +SEMICOLON + : ';' + ; + +SERVER + : S E R V E R + ; + +SERVERS + : S E R V E R S + ; + +SET + : S E T + ; + +SETTING + : S E T T I N G + ; + +SETTINGS + : S E T T I N G S + ; + +SHORTEST_PATH + : S H O R T E S T P A T H + ; + +SHORTEST + : S H O R T E S T + ; + +SHOW + : S H O W + ; + +SIGNED + : S I G N E D + ; + +SINGLE + : S I N G L E + ; + +SKIPROWS + : S K I P + ; + +START + : S T A R T + ; + +STARTS + : S T A R T S + ; + +STATUS + : S T A T U S + ; + +STOP + : S T O P + ; + +STRING + : S T R I N G + ; + +SUPPORTED + : S U P P O R T E D + ; + +SUSPENDED + : S U S P E N D E D + ; + +TARGET + : T A R G E T + ; + +TERMINATE + : T E R M I N A T E + ; + +TEXT + : T E X T + ; + +THEN + : T H E N + ; + +TIME + : T I M E + ; + +TIMES + : '*' + ; + +TIMESTAMP + : T I M E S T A M P + ; + +TIMEZONE + : T I M E Z O N E + ; + +TO + : T O + ; + +TOPOLOGY + : T O P O L O G Y + ; + +TRAILING + : T R A I L I N G + ; + +TRANSACTION + : T R A N S A C T I O N + ; + +TRANSACTIONS + : T R A N S A C T I O N S + ; + +TRAVERSE + : T R A V E R S E + ; + +TRIM + : T R I M + ; + +TRUE + : T R U E + ; + +TYPE + : T Y P E + ; + +TYPED + : T Y P E D + ; + +TYPES + : T Y P E S + ; + +UNION + : U N I O N + ; + +UNIQUE + : U N I Q U E + ; + +UNIQUENESS + : U N I Q U E N E S S + ; + +UNWIND + : U N W I N D + ; + +URL + : U R L + ; + +USE + : U S E + ; + +USER + : U S E R + ; + +USERS + : U S E R S + ; + +USING + : U S I N G + ; + +VALUE + : V A L U E + ; + +VARCHAR + : V A R C H A R + ; + +VECTOR + : V E C T O R + ; + +VERTEX + : V E R T E X + ; + +WAIT + : W A I T + ; + +WHEN + : W H E N + ; + +WHERE + : W H E R E + ; + +WITH + : W I T H + ; + +WITHOUT + : W I T H O U T + ; + +WRITE + : W R I T E + ; + +XOR + : X O R + ; + +YIELD + : Y I E L D + ; + +ZONE + : Z O N E + ; + +ZONED + : Z O N E D + ; + +IDENTIFIER + : LETTER (PART_LETTER)* + ; + +EXTENDED_IDENTIFIER + : PART_LETTER+ + ; + +ARROW_LINE + : [\-\u00AD‐‑‒–—―﹘﹣-] + ; + +ARROW_LEFT_HEAD + : [⟨〈﹤<] + ; + +ARROW_RIGHT_HEAD + : [⟩〉﹥>] + ; + +fragment LETTER + : [\u0041-\u005a\u005f\u0061-\u007a\u00aa\u00b5\u00ba\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376-\u0377\u037a-\u037d\u037f\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u052f\u0531-\u0556\u0559\u0560-\u0588\u05d0-\u05ea\u05ef-\u05f2\u0620-\u064a\u066e-\u066f\u0671-\u06d3\u06d5\u06e5-\u06e6\u06ee-\u06ef\u06fa-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07ca-\u07ea\u07f4-\u07f5\u07fa\u0800-\u0815\u081a\u0824\u0828\u0840-\u0858\u0860-\u086a\u08a0-\u08b4\u08b6-\u08c7\u0904-\u0939\u093d\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098c\u098f-\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc-\u09dd\u09df-\u09e1\u09f0-\u09f1\u09fc\u0a05-\u0a0a\u0a0f-\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32-\u0a33\u0a35-\u0a36\u0a38-\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2-\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0-\u0ae1\u0af9\u0b05-\u0b0c\u0b0f-\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32-\u0b33\u0b35-\u0b39\u0b3d\u0b5c-\u0b5d\u0b5f-\u0b61\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99-\u0b9a\u0b9c\u0b9e-\u0b9f\u0ba3-\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c39\u0c3d\u0c58-\u0c5a\u0c60-\u0c61\u0c80\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cde\u0ce0-\u0ce1\u0cf1-\u0cf2\u0d04-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d\u0d4e\u0d54-\u0d56\u0d5f-\u0d61\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32-\u0e33\u0e40-\u0e46\u0e81-\u0e82\u0e84\u0e86-\u0e8a\u0e8c-\u0ea3\u0ea5\u0ea7-\u0eb0\u0eb2-\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0edc-\u0edf\u0f00\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8c\u1000-\u102a\u103f\u1050-\u1055\u105a-\u105d\u1061\u1065-\u1066\u106e-\u1070\u1075-\u1081\u108e\u10a0-\u10c5\u10c7\u10cd\u10d0-\u10fa\u10fc-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f5\u13f8-\u13fd\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f8\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u1820-\u1878\u1880-\u1884\u1887-\u18a8\u18aa\u18b0-\u18f5\u1900-\u191e\u1950-\u196d\u1970-\u1974\u1980-\u19ab\u19b0-\u19c9\u1a00-\u1a16\u1a20-\u1a54\u1aa7\u1b05-\u1b33\u1b45-\u1b4b\u1b83-\u1ba0\u1bae-\u1baf\u1bba-\u1be5\u1c00-\u1c23\u1c4d-\u1c4f\u1c5a-\u1c7d\u1c80-\u1c88\u1c90-\u1cba\u1cbd-\u1cbf\u1ce9-\u1cec\u1cee-\u1cf3\u1cf5-\u1cf6\u1cfa\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u203f-\u2040\u2054\u2071\u207f\u2090-\u209c\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2ce4\u2ceb-\u2cee\u2cf2-\u2cf3\u2d00-\u2d25\u2d27\u2d2d\u2d30-\u2d67\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303c\u3041-\u3096\u309d-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312f\u3131-\u318e\u31a0-\u31bf\u31f0-\u31ff\u3400-\u4dbf\u4e00-\u9ffc\ua000-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua61f\ua62a-\ua62b\ua640-\ua66e\ua67f-\ua69d\ua6a0-\ua6ef\ua717-\ua71f\ua722-\ua788\ua78b-\ua7bf\ua7c2-\ua7ca\ua7f5-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8f2-\ua8f7\ua8fb\ua8fd-\ua8fe\ua90a-\ua925\ua930-\ua946\ua960-\ua97c\ua984-\ua9b2\ua9cf\ua9e0-\ua9e4\ua9e6-\ua9ef\ua9fa-\ua9fe\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa60-\uaa76\uaa7a\uaa7e-\uaaaf\uaab1\uaab5-\uaab6\uaab9-\uaabd\uaac0\uaac2\uaadb-\uaadd\uaae0-\uaaea\uaaf2-\uaaf4\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uab30-\uab5a\uab5c-\uab69\uab70-\uabe2\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\uf900-\ufa6d\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe33-\ufe34\ufe4d-\ufe4f\ufe70-\ufe74\ufe76-\ufefc\uff21-\uff3a\uff3f\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc] + ; + +fragment PART_LETTER + : LETTER + | [\u0030-\u0039\u0300-\u036f\u0483-\u0487\u058f\u0591-\u05bd\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05c7\u060b\u0610-\u061a\u064b-\u0669\u0670\u06d6-\u06dc\u06df-\u06e4\u06e7-\u06e8\u06ea-\u06ed\u06f0-\u06f9\u0711\u0730-\u074a\u07a6-\u07b0\u07c0-\u07c9\u07eb-\u07f3\u07fd-\u07ff\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0859-\u085b\u08d3-\u08e1\u08e3-\u0903\u093a-\u093c\u093e-\u094f\u0951-\u0957\u0962-\u0963\u0966-\u096f\u0981-\u0983\u09bc\u09be-\u09c4\u09c7-\u09c8\u09cb-\u09cd\u09d7\u09e2-\u09e3\u09e6-\u09ef\u09f2-\u09f3\u09fb\u09fe\u0a01-\u0a03\u0a3c\u0a3e-\u0a42\u0a47-\u0a48\u0a4b-\u0a4d\u0a51\u0a66-\u0a71\u0a75\u0a81-\u0a83\u0abc\u0abe-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0ae2-\u0ae3\u0ae6-\u0aef\u0af1\u0afa-\u0aff\u0b01-\u0b03\u0b3c\u0b3e-\u0b44\u0b47-\u0b48\u0b4b-\u0b4d\u0b55-\u0b57\u0b62-\u0b63\u0b66-\u0b6f\u0b82\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd7\u0be6-\u0bef\u0bf9\u0c00-\u0c04\u0c3e-\u0c44\u0c46-\u0c48\u0c4a-\u0c4d\u0c55-\u0c56\u0c62-\u0c63\u0c66-\u0c6f\u0c81-\u0c83\u0cbc\u0cbe-\u0cc4\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5-\u0cd6\u0ce2-\u0ce3\u0ce6-\u0cef\u0d00-\u0d03\u0d3b-\u0d3c\u0d3e-\u0d44\u0d46-\u0d48\u0d4a-\u0d4d\u0d57\u0d62-\u0d63\u0d66-\u0d6f\u0d81-\u0d83\u0dca\u0dcf-\u0dd4\u0dd6\u0dd8-\u0ddf\u0de6-\u0def\u0df2-\u0df3\u0e31\u0e34-\u0e3a\u0e3f\u0e47-\u0e4e\u0e50-\u0e59\u0eb1\u0eb4-\u0ebc\u0ec8-\u0ecd\u0ed0-\u0ed9\u0f18-\u0f19\u0f20-\u0f29\u0f35\u0f37\u0f39\u0f3e-\u0f3f\u0f71-\u0f84\u0f86-\u0f87\u0f8d-\u0f97\u0f99-\u0fbc\u0fc6\u102b-\u103e\u1040-\u1049\u1056-\u1059\u105e-\u1060\u1062-\u1064\u1067-\u106d\u1071-\u1074\u1082-\u108d\u108f-\u109d\u135d-\u135f\u1712-\u1714\u1732-\u1734\u1752-\u1753\u1772-\u1773\u17b4-\u17d3\u17db\u17dd\u17e0-\u17e9\u180b-\u180d\u1810-\u1819\u1885-\u1886\u18a9\u1920-\u192b\u1930-\u193b\u1946-\u194f\u19d0-\u19d9\u1a17-\u1a1b\u1a55-\u1a5e\u1a60-\u1a7c\u1a7f-\u1a89\u1a90-\u1a99\u1ab0-\u1abd\u1abf-\u1ac0\u1b00-\u1b04\u1b34-\u1b44\u1b50-\u1b59\u1b6b-\u1b73\u1b80-\u1b82\u1ba1-\u1bad\u1bb0-\u1bb9\u1be6-\u1bf3\u1c24-\u1c37\u1c40-\u1c49\u1c50-\u1c59\u1cd0-\u1cd2\u1cd4-\u1ce8\u1ced\u1cf4\u1cf7-\u1cf9\u1dc0-\u1df9\u1dfb-\u1dff\u20a0-\u20bf\u20d0-\u20dc\u20e1\u20e5-\u20f0\u2cef-\u2cf1\u2d7f\u2de0-\u2dff\u302a-\u302f\u3099-\u309a\ua620-\ua629\ua66f\ua674-\ua67d\ua69e-\ua69f\ua6f0-\ua6f1\ua802\ua806\ua80b\ua823-\ua827\ua82c\ua838\ua880-\ua881\ua8b4-\ua8c5\ua8d0-\ua8d9\ua8e0-\ua8f1\ua8ff-\ua909\ua926-\ua92d\ua947-\ua953\ua980-\ua983\ua9b3-\ua9c0\ua9d0-\ua9d9\ua9e5\ua9f0-\ua9f9\uaa29-\uaa36\uaa43\uaa4c-\uaa4d\uaa50-\uaa59\uaa7b-\uaa7d\uaab0\uaab2-\uaab4\uaab7-\uaab8\uaabe-\uaabf\uaac1\uaaeb-\uaaef\uaaf5-\uaaf6\uabe3-\uabea\uabec-\uabed\uabf0-\uabf9\ufb1e\ufdfc\ufe00-\ufe0f\ufe20-\ufe2f\ufe69\uff04\uff10-\uff19\uffe0-\uffe1\uffe5-\uffe6] + ; + +fragment A + : [aA] + ; + +fragment B + : [bB] + ; + +fragment C + : [cC] + ; + +fragment D + : [dD] + ; + +fragment E + : [eE] + ; + +fragment F + : [fF] + ; + +fragment G + : [gG] + ; + +fragment H + : [hH] + ; + +fragment I + : [iI] + ; + +fragment J + : [jJ] + ; + +fragment K + : [kK] + ; + +fragment L + : [lL] + ; + +fragment M + : [mM] + ; + +fragment N + : [nN] + ; + +fragment O + : [oO] + ; + +fragment P + : [pP] + ; + +fragment Q + : [qQ] + ; + +fragment R + : [rR] + ; + +fragment S + : [sS] + ; + +fragment T + : [tT] + ; + +fragment U + : [uU] + ; + +fragment V + : [vV] + ; + +fragment W + : [wW] + ; + +fragment X + : [xX] + ; + +fragment Y + : [yY] + ; + +fragment Z + : [zZ] + ; + +// Should always be last in the file before modes +ErrorChar + : . + ; diff --git a/client-java/controller/src/main/antlr4/org/evomaster/client/java/controller/neo4j/cypher25/Cypher25Parser.g4 b/client-java/controller/src/main/antlr4/org/evomaster/client/java/controller/neo4j/cypher25/Cypher25Parser.g4 new file mode 100644 index 0000000000..b97ac42ac1 --- /dev/null +++ b/client-java/controller/src/main/antlr4/org/evomaster/client/java/controller/neo4j/cypher25/Cypher25Parser.g4 @@ -0,0 +1,2024 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * 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 + * + * http://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. + */ +parser grammar Cypher25Parser; + + +options { tokenVocab = Cypher25Lexer; } +statements + : statement (SEMICOLON statement)* SEMICOLON? EOF + ; + +statement + : command | regularQuery + ; + +regularQuery + : singleQuery (UNION (ALL | DISTINCT)? singleQuery)* + ; + +singleQuery + : clause+ + ; + +clause + : useClause + | finishClause + | returnClause + | createClause + | insertClause + | deleteClause + | setClause + | removeClause + | matchClause + | mergeClause + | withClause + | unwindClause + | callClause + | subqueryClause + | loadCSVClause + | foreachClause + | orderBySkipLimitClause + ; + +useClause + : USE GRAPH? graphReference + ; + +graphReference + : LPAREN graphReference RPAREN + | functionInvocation + | symbolicAliasName + ; + +finishClause + : FINISH + ; + +returnClause + : RETURN returnBody + ; + +returnBody + : DISTINCT? returnItems orderBy? skip? limit? + ; + +returnItem + : expression (AS variable)? + ; + +returnItems + : (TIMES | returnItem) (COMMA returnItem)* + ; + +orderItem + : expression (ascToken | descToken)? + ; + +ascToken + : ASC | ASCENDING + ; + +descToken + : DESC | DESCENDING + ; + +orderBy + : ORDER BY orderItem (COMMA orderItem)* + ; + +skip + : (OFFSET | SKIPROWS) expression + ; + +limit + : LIMITROWS expression + ; + +whereClause + : WHERE expression + ; + +withClause + : WITH returnBody whereClause? + ; + +createClause + : CREATE patternList + ; + +insertClause + : INSERT insertPatternList + ; + +setClause + : SET setItem (COMMA setItem)* + ; + +setItem + : propertyExpression EQ expression # SetProp + | dynamicPropertyExpression EQ expression # SetDynamicProp + | variable EQ expression # SetProps + | variable PLUSEQUAL expression # AddProp + | variable nodeLabels # SetLabels + | variable nodeLabelsIs # SetLabelsIs + ; + +removeClause + : REMOVE removeItem (COMMA removeItem)* + ; + +removeItem + : propertyExpression # RemoveProp + | dynamicPropertyExpression # RemoveDynamicProp + | variable nodeLabels # RemoveLabels + | variable nodeLabelsIs # RemoveLabelsIs + ; + +deleteClause + : (DETACH | NODETACH)? DELETE expression (COMMA expression)* + ; + +matchClause + : OPTIONAL? MATCH matchMode? patternList hint* whereClause? + ; + +matchMode + : REPEATABLE (ELEMENT BINDINGS? | ELEMENTS) + | DIFFERENT (RELATIONSHIP BINDINGS? | RELATIONSHIPS) + ; + +hint + : USING ((( + INDEX + | TEXT INDEX + | RANGE INDEX + | POINT INDEX + ) SEEK? variable labelOrRelType LPAREN nonEmptyNameList RPAREN) + | JOIN ON nonEmptyNameList + | SCAN variable labelOrRelType + ) + ; + +mergeClause + : MERGE pattern mergeAction* + ; + +mergeAction + : ON (MATCH | CREATE) setClause + ; + +unwindClause + : UNWIND expression AS variable + ; + +callClause + : OPTIONAL? CALL procedureName (LPAREN (procedureArgument (COMMA procedureArgument)*)? RPAREN)? (YIELD (TIMES | procedureResultItem (COMMA procedureResultItem)* whereClause?))? + ; + +procedureName + : namespace symbolicNameString + ; + +procedureArgument + : expression + ; + +procedureResultItem + : symbolicNameString (AS variable)? + ; + +loadCSVClause + : LOAD CSV (WITH HEADERS)? FROM expression AS variable (FIELDTERMINATOR stringLiteral)? + ; + +foreachClause + : FOREACH LPAREN variable IN expression BAR clause+ RPAREN + ; + +subqueryClause + : OPTIONAL? CALL subqueryScope? LCURLY regularQuery RCURLY subqueryInTransactionsParameters? + ; + +subqueryScope + : LPAREN (TIMES | variable (COMMA variable)*)? RPAREN + ; + +subqueryInTransactionsParameters + : IN (expression? CONCURRENT)? TRANSACTIONS (subqueryInTransactionsBatchParameters | subqueryInTransactionsErrorParameters | subqueryInTransactionsReportParameters)* + ; + +subqueryInTransactionsBatchParameters + : OF expression (ROW | ROWS) + ; + +subqueryInTransactionsErrorParameters + : ON ERROR (CONTINUE | BREAK | FAIL) + ; + +subqueryInTransactionsReportParameters + : REPORT STATUS AS variable + ; + +orderBySkipLimitClause + : orderBy skip? limit? + | skip limit? + | limit + ; + +patternList + : pattern (COMMA pattern)* + ; + +insertPatternList + : insertPattern (COMMA insertPattern)* + ; + +pattern + : (variable EQ)? selector? anonymousPattern + ; + +insertPattern + : (symbolicNameString EQ)? insertNodePattern (insertRelationshipPattern insertNodePattern)* + ; + +quantifier + : LCURLY UNSIGNED_DECIMAL_INTEGER RCURLY + | LCURLY from = UNSIGNED_DECIMAL_INTEGER? COMMA to = UNSIGNED_DECIMAL_INTEGER? RCURLY + | PLUS + | TIMES + ; + +anonymousPattern + : shortestPathPattern + | patternElement + ; + +shortestPathPattern + : (SHORTEST_PATH | ALL_SHORTEST_PATHS) LPAREN patternElement RPAREN + ; + +patternElement + : (nodePattern (relationshipPattern quantifier? nodePattern)* | parenthesizedPath)+ + ; + +selector + : ANY SHORTEST pathToken? # AnyShortestPath + | ALL SHORTEST pathToken? # AllShortestPath + | ANY UNSIGNED_DECIMAL_INTEGER? pathToken? # AnyPath + | ALL pathToken? # AllPath + | SHORTEST UNSIGNED_DECIMAL_INTEGER? pathToken? groupToken # ShortestGroup + | SHORTEST UNSIGNED_DECIMAL_INTEGER pathToken? # AnyShortestPath + ; + +groupToken + : GROUP | GROUPS + ; + +pathToken + : PATH | PATHS + ; + +pathPatternNonEmpty + : nodePattern (relationshipPattern nodePattern)+ + ; + +nodePattern + : LPAREN variable? labelExpression? properties? (WHERE expression)? RPAREN + ; + +insertNodePattern + : LPAREN variable? insertNodeLabelExpression? map? RPAREN + ; + +parenthesizedPath + : LPAREN pattern (WHERE expression)? RPAREN quantifier? + ; + +nodeLabels + : (labelType | dynamicLabelType)+ + ; + +nodeLabelsIs + : IS (symbolicNameString | dynamicExpression) (labelType | dynamicLabelType)* + ; + +dynamicExpression + : DOLLAR LPAREN expression RPAREN + ; + +dynamicAnyAllExpression + : DOLLAR (ALL | ANY)? LPAREN expression RPAREN + ; + +dynamicLabelType + : COLON dynamicExpression + ; + +labelType + : COLON symbolicNameString + ; + +relType + : COLON symbolicNameString + ; + +labelOrRelType + : COLON symbolicNameString + ; + +properties + : map + | parameter["ANY"] + ; + +relationshipPattern + : leftArrow? arrowLine (LBRACKET variable? labelExpression? pathLength? properties? (WHERE expression)? RBRACKET)? arrowLine rightArrow? + ; + +insertRelationshipPattern + : leftArrow? arrowLine LBRACKET variable? insertRelationshipLabelExpression map? RBRACKET arrowLine rightArrow? + ; + +leftArrow + : LT + | ARROW_LEFT_HEAD + ; + +arrowLine + : ARROW_LINE + | MINUS + ; + +rightArrow + : GT + | ARROW_RIGHT_HEAD + ; + +pathLength + : TIMES (from = UNSIGNED_DECIMAL_INTEGER? DOTDOT to = UNSIGNED_DECIMAL_INTEGER? | single = UNSIGNED_DECIMAL_INTEGER)? + ; + +labelExpression + : COLON labelExpression4 + | IS labelExpression4Is + ; + +labelExpression4 + : labelExpression3 (BAR COLON? labelExpression3)* + ; + +labelExpression4Is + : labelExpression3Is (BAR COLON? labelExpression3Is)* + ; + +labelExpression3 + : labelExpression2 ((AMPERSAND | COLON) labelExpression2)* + ; + +labelExpression3Is + : labelExpression2Is ((AMPERSAND | COLON) labelExpression2Is)* + ; + +labelExpression2 + : EXCLAMATION_MARK* labelExpression1 + ; + +labelExpression2Is + : EXCLAMATION_MARK* labelExpression1Is + ; + +labelExpression1 + : LPAREN labelExpression4 RPAREN #ParenthesizedLabelExpression + | PERCENT #AnyLabel + | dynamicAnyAllExpression #DynamicLabel + | symbolicNameString #LabelName + ; + +labelExpression1Is + : LPAREN labelExpression4Is RPAREN #ParenthesizedLabelExpressionIs + | PERCENT #AnyLabelIs + | dynamicAnyAllExpression #DynamicLabelIs + | symbolicLabelNameString #LabelNameIs + ; + +insertNodeLabelExpression + : (COLON | IS) symbolicNameString ((AMPERSAND | COLON) symbolicNameString)* + ; + +insertRelationshipLabelExpression + : (COLON | IS) symbolicNameString + ; + +expression + : expression11 (OR expression11)* + ; + +expression11 + : expression10 (XOR expression10)* + ; + +expression10 + : expression9 (AND expression9)* + ; + +expression9 + : NOT* expression8 + ; + +// Making changes here? Consider looking at extendedWhen too. +expression8 + : expression7 (( + EQ + | INVALID_NEQ + | NEQ + | LE + | GE + | LT + | GT + ) expression7)* + ; + +expression7 + : expression6 comparisonExpression6? + ; + +// Making changes here? Consider looking at extendedWhen too. +comparisonExpression6 + : ( + REGEQ + | STARTS WITH + | ENDS WITH + | CONTAINS + | IN + ) expression6 # StringAndListComparison + | IS NOT? NULL # NullComparison + | (IS NOT? (TYPED | COLONCOLON) | COLONCOLON) type # TypeComparison + | IS NOT? normalForm? NORMALIZED # NormalFormComparison + ; + +normalForm + : NFC + | NFD + | NFKC + | NFKD + ; + +expression6 + : expression5 ((PLUS | MINUS | DOUBLEBAR) expression5)* + ; + +expression5 + : expression4 ((TIMES | DIVIDE | PERCENT) expression4)* + ; + +expression4 + : expression3 (POW expression3)* + ; + +expression3 + : expression2 + | (PLUS | MINUS) expression2 + ; + +expression2 + : expression1 postFix* + ; + +postFix + : property # PropertyPostfix + | labelExpression # LabelPostfix + | LBRACKET expression RBRACKET # IndexPostfix + | LBRACKET fromExp = expression? DOTDOT toExp = expression? RBRACKET # RangePostfix + ; + +property + : DOT propertyKeyName + ; + +dynamicProperty + : LBRACKET expression RBRACKET + ; + +propertyExpression + : expression1 property+ + ; + +dynamicPropertyExpression + : expression1 dynamicProperty + ; + +expression1 + : literal + | parameter["ANY"] + | caseExpression + | extendedCaseExpression + | countStar + | existsExpression + | countExpression + | collectExpression + | mapProjection + | listComprehension + | listLiteral + | patternComprehension + | reduceExpression + | listItemsPredicate + | normalizeFunction + | trimFunction + | patternExpression + | shortestPathExpression + | parenthesizedExpression + | functionInvocation + | variable + ; + +literal + : numberLiteral # NummericLiteral + | stringLiteral # StringsLiteral + | map # OtherLiteral + | TRUE # BooleanLiteral + | FALSE # BooleanLiteral + | INF # KeywordLiteral + | INFINITY # KeywordLiteral + | NAN # KeywordLiteral + | NULL # KeywordLiteral + ; + +caseExpression + : CASE caseAlternative+ (ELSE expression)? END + ; + +caseAlternative + : WHEN expression THEN expression + ; + +extendedCaseExpression + : CASE expression extendedCaseAlternative+ (ELSE elseExp = expression)? END + ; + +extendedCaseAlternative + : WHEN extendedWhen (COMMA extendedWhen)* THEN expression + ; + +// Making changes here? Consider looking at comparisonExpression6 and expression8 too. +extendedWhen + : (REGEQ | STARTS WITH | ENDS WITH) expression6 # WhenStringOrList + | IS NOT? NULL # WhenNull + | (IS NOT? TYPED | COLONCOLON) type # WhenType + | IS NOT? normalForm? NORMALIZED # WhenForm + | ( + EQ + | NEQ + | INVALID_NEQ + | LE + | GE + | LT + | GT + ) expression7 # WhenComparator + | expression # WhenEquals + ; + +// Observe that this is not possible to write as: +// (WHERE whereExp = expression)? (BAR barExp = expression)? RBRACKET +// Due to an ambigouity with cases such as [node IN nodes WHERE node:A|B] +// where |B will be interpreted as part of the whereExp, rather than as the expected barExp. +listComprehension + : LBRACKET variable IN expression ((WHERE whereExp = expression)? BAR barExp = expression | (WHERE whereExp = expression)?) RBRACKET + ; + +patternComprehension + : LBRACKET (variable EQ)? pathPatternNonEmpty (WHERE whereExp = expression)? BAR barExp = expression RBRACKET + ; + +reduceExpression + : REDUCE LPAREN variable EQ expression COMMA variable IN expression BAR expression RPAREN + ; + +listItemsPredicate + : ( + ALL + | ANY + | NONE + | SINGLE + ) LPAREN variable IN inExp = expression (WHERE whereExp = expression)? RPAREN + ; + +normalizeFunction + : NORMALIZE LPAREN expression (COMMA normalForm)? RPAREN + ; + +trimFunction + : TRIM LPAREN ((BOTH | LEADING | TRAILING)? (trimCharacterString = expression)? FROM)? trimSource = expression RPAREN + ; + +patternExpression + : pathPatternNonEmpty + ; + +shortestPathExpression + : shortestPathPattern + ; + +parenthesizedExpression + : LPAREN expression RPAREN + ; + +mapProjection + : variable LCURLY (mapProjectionElement (COMMA mapProjectionElement)* )? RCURLY + ; + +mapProjectionElement + : propertyKeyName COLON expression + | property + | variable + | DOT TIMES + ; + +countStar + : COUNT LPAREN TIMES RPAREN + ; + +existsExpression + : EXISTS LCURLY (regularQuery | matchMode? patternList whereClause?) RCURLY + ; + +countExpression + : COUNT LCURLY (regularQuery | matchMode? patternList whereClause?) RCURLY + ; + +collectExpression + : COLLECT LCURLY regularQuery RCURLY + ; + +numberLiteral + : MINUS? ( + DECIMAL_DOUBLE + | UNSIGNED_DECIMAL_INTEGER + | UNSIGNED_HEX_INTEGER + | UNSIGNED_OCTAL_INTEGER + ) + ; + +signedIntegerLiteral + : MINUS? UNSIGNED_DECIMAL_INTEGER + ; + +listLiteral + : LBRACKET (expression (COMMA expression)* )? RBRACKET + ; + +propertyKeyName + : symbolicNameString + ; + +parameter[String paramType] + : DOLLAR parameterName[paramType] + ; + +parameterName[String paramType] + : (symbolicNameString | UNSIGNED_DECIMAL_INTEGER | UNSIGNED_OCTAL_INTEGER | EXTENDED_IDENTIFIER) + ; + +functionInvocation + : functionName LPAREN (DISTINCT | ALL)? (functionArgument (COMMA functionArgument)* )? RPAREN + ; + +functionArgument + : expression + ; + +functionName + : namespace symbolicNameString + ; + +namespace + : (symbolicNameString DOT)* + ; + +variable + : symbolicNameString + ; + +// Returns non-list of propertyKeyNames +nonEmptyNameList + : symbolicNameString (COMMA symbolicNameString)* + ; + +type + : typePart (BAR typePart)* + ; + +typePart + : typeName typeNullability? typeListSuffix* + ; + +typeName + // Note! These are matched based on the first token. Take precaution in ExpressionBuilder.scala when modifying + : NOTHING + | NULL + | BOOL + | BOOLEAN + | VARCHAR + | STRING + | INT + | SIGNED? INTEGER + | FLOAT + | DATE + | LOCAL (TIME | DATETIME) + | ZONED (TIME | DATETIME) + | TIME (WITHOUT | WITH) (TIMEZONE | TIME ZONE) + | TIMESTAMP (WITHOUT | WITH) (TIMEZONE | TIME ZONE) + | DURATION + | POINT + | NODE + | VERTEX + | RELATIONSHIP + | EDGE + | MAP + | (LIST | ARRAY) LT type GT + | PATH + | PATHS + | PROPERTY VALUE + | ANY ( + NODE + | VERTEX + | RELATIONSHIP + | EDGE + | MAP + | PROPERTY VALUE + | VALUE? LT type GT + | VALUE + )? + ; + +typeNullability + : NOT NULL + | EXCLAMATION_MARK + ; + +typeListSuffix + : (LIST | ARRAY) typeNullability? + ; + +// Show, terminate, schema and admin commands + +command + : useClause? ( + createCommand + | dropCommand + | alterCommand + | renameCommand + | denyCommand + | revokeCommand + | grantCommand + | startDatabase + | stopDatabase + | enableServerCommand + | allocationCommand + | showCommand + | terminateCommand + ) + ; + +createCommand + : CREATE (OR REPLACE)? ( + createAlias + | createCompositeDatabase + | createConstraint + | createDatabase + | createIndex + | createRole + | createUser + ) + ; + +dropCommand + : DROP ( + dropAlias + | dropConstraint + | dropDatabase + | dropIndex + | dropRole + | dropServer + | dropUser + ) + ; + +showCommand + : SHOW ( + showAliases + | showConstraintCommand + | showCurrentUser + | showDatabase + | showFunctions + | showIndexCommand + | showPrivileges + | showProcedures + | showRolePrivileges + | showRoles + | showServers + | showSettings + | showSupportedPrivileges + | showTransactions + | showUserPrivileges + | showUsers + ) + ; + +showCommandYield + : yieldClause returnClause? + | whereClause + ; + +yieldItem + : variable (AS variable)? + ; + +yieldSkip + : (OFFSET | SKIPROWS) signedIntegerLiteral + ; + +yieldLimit + : LIMITROWS signedIntegerLiteral + ; + +yieldClause + : YIELD (TIMES | yieldItem (COMMA yieldItem)*) orderBy? yieldSkip? yieldLimit? whereClause? + ; + +commandOptions + : OPTIONS mapOrParameter + ; + +// Non-admin show and terminate commands + +terminateCommand + : TERMINATE terminateTransactions + ; + +composableCommandClauses + : terminateCommand + | composableShowCommandClauses + ; + +composableShowCommandClauses + : SHOW ( + showIndexCommand + | showConstraintCommand + | showFunctions + | showProcedures + | showSettings + | showTransactions + ) + ; + +showIndexCommand + : (showIndexType)? showIndexesEnd + ; + +showIndexType + : ALL + | FULLTEXT + | LOOKUP + | POINT + | RANGE + | TEXT + | VECTOR + ; + +showIndexesEnd + : indexToken showCommandYield? composableCommandClauses? + ; + +showConstraintCommand + : ALL? showConstraintsEnd # ShowConstraintAll + | (showConstraintEntity)? constraintExistType showConstraintsEnd # ShowConstraintExist + | (showConstraintEntity)? KEY showConstraintsEnd # ShowConstraintKey + | (showConstraintEntity)? PROPERTY TYPE showConstraintsEnd # ShowConstraintPropType + | (showConstraintEntity)? (PROPERTY)? (UNIQUE | UNIQUENESS) showConstraintsEnd # ShowConstraintUnique + ; + +showConstraintEntity + : NODE # nodeEntity + | (RELATIONSHIP | REL) # relEntity + ; + +constraintExistType + : EXISTENCE + | EXIST + | PROPERTY EXISTENCE + | PROPERTY EXIST + ; + +showConstraintsEnd + : constraintToken showCommandYield? composableCommandClauses? + ; + +showProcedures + : (PROCEDURE | PROCEDURES) executableBy? showCommandYield? composableCommandClauses? + ; + +showFunctions + : showFunctionsType? functionToken executableBy? showCommandYield? composableCommandClauses? + ; + +functionToken + : FUNCTION | FUNCTIONS + ; + +executableBy + : EXECUTABLE (BY (CURRENT USER | symbolicNameString))? + ; + +showFunctionsType + : ALL + | BUILT IN + | USER DEFINED + ; + +showTransactions + : transactionToken namesAndClauses + ; + +terminateTransactions + : transactionToken stringsOrExpression showCommandYield? composableCommandClauses? + ; + +showSettings + : settingToken namesAndClauses + ; + +settingToken + : SETTING | SETTINGS + ; + +namesAndClauses + : (showCommandYield? | stringsOrExpression showCommandYield?) composableCommandClauses? + ; + +stringsOrExpression + : stringList + | expression + ; + +// Schema commands + +commandNodePattern + : LPAREN variable labelType RPAREN + ; + +commandRelPattern + : LPAREN RPAREN leftArrow? arrowLine LBRACKET variable relType RBRACKET arrowLine rightArrow? LPAREN RPAREN + ; + +createConstraint + : CONSTRAINT symbolicNameOrStringParameter? (IF NOT EXISTS)? FOR (commandNodePattern | commandRelPattern) constraintType commandOptions? + ; + +constraintType + : REQUIRE propertyList (COLONCOLON | IS (TYPED | COLONCOLON)) type # ConstraintTyped + | REQUIRE propertyList IS (NODE | RELATIONSHIP | REL)? UNIQUE # ConstraintIsUnique + | REQUIRE propertyList IS (NODE | RELATIONSHIP | REL)? KEY # ConstraintKey + | REQUIRE propertyList IS NOT NULL # ConstraintIsNotNull + ; + +dropConstraint + : CONSTRAINT symbolicNameOrStringParameter (IF EXISTS)? + ; + +createIndex + : RANGE INDEX createIndex_ + | TEXT INDEX createIndex_ + | POINT INDEX createIndex_ + | VECTOR INDEX createIndex_ + | LOOKUP INDEX createLookupIndex + | FULLTEXT INDEX createFulltextIndex + | INDEX createIndex_ + ; + +createIndex_ + : symbolicNameOrStringParameter? (IF NOT EXISTS)? FOR (commandNodePattern | commandRelPattern) ON propertyList commandOptions? + ; + +createFulltextIndex + : symbolicNameOrStringParameter? (IF NOT EXISTS)? FOR (fulltextNodePattern | fulltextRelPattern) ON EACH LBRACKET enclosedPropertyList RBRACKET commandOptions? + ; + +fulltextNodePattern + : LPAREN variable COLON symbolicNameString (BAR symbolicNameString)* RPAREN + ; + +fulltextRelPattern + : LPAREN RPAREN leftArrow? arrowLine LBRACKET variable COLON symbolicNameString (BAR symbolicNameString)* RBRACKET arrowLine rightArrow? LPAREN RPAREN + ; + +createLookupIndex + : symbolicNameOrStringParameter? (IF NOT EXISTS)? FOR (lookupIndexNodePattern | lookupIndexRelPattern) symbolicNameString LPAREN variable RPAREN commandOptions? + ; + +lookupIndexNodePattern + : LPAREN variable RPAREN ON EACH + ; + +lookupIndexRelPattern + : LPAREN RPAREN leftArrow? arrowLine LBRACKET variable RBRACKET arrowLine rightArrow? LPAREN RPAREN ON EACH? + ; + +dropIndex + : INDEX symbolicNameOrStringParameter (IF EXISTS)? + ; + +propertyList + : variable property | LPAREN enclosedPropertyList RPAREN + ; + +enclosedPropertyList + : variable property (COMMA variable property)* + ; + +// Admin commands + +alterCommand + : ALTER ( + alterAlias + | alterCurrentUser + | alterDatabase + | alterUser + | alterServer + ) + ; + +renameCommand + : RENAME (renameRole | renameServer | renameUser) + ; + +grantCommand + : GRANT ( + IMMUTABLE? privilege TO roleNames + | roleToken grantRole + ) + ; + +denyCommand + : DENY IMMUTABLE? privilege TO roleNames + ; + +revokeCommand + : REVOKE ( + (DENY | GRANT)? IMMUTABLE? privilege FROM roleNames + | roleToken revokeRole + ) + ; + +userNames + : symbolicNameOrStringParameterList + ; + +roleNames + : symbolicNameOrStringParameterList + ; + +roleToken + : ROLES + | ROLE + ; + +// Server commands + +enableServerCommand + : ENABLE SERVER stringOrParameter commandOptions? + ; + +alterServer + : SERVER stringOrParameter SET commandOptions + ; + +renameServer + : SERVER stringOrParameter TO stringOrParameter + ; + +dropServer + : SERVER stringOrParameter + ; + +showServers + : (SERVER | SERVERS) showCommandYield? + ; + +allocationCommand + : DRYRUN? (deallocateDatabaseFromServers | reallocateDatabases) + ; + +deallocateDatabaseFromServers + : DEALLOCATE (DATABASE | DATABASES) FROM (SERVER | SERVERS) stringOrParameter (COMMA stringOrParameter)* + ; + +reallocateDatabases + : REALLOCATE (DATABASE | DATABASES) + ; + +// Role commands + +createRole + : IMMUTABLE? ROLE commandNameExpression (IF NOT EXISTS)? (AS COPY OF commandNameExpression)? + ; + +dropRole + : ROLE commandNameExpression (IF EXISTS)? + ; + +renameRole + : ROLE commandNameExpression (IF EXISTS)? TO commandNameExpression + ; + +showRoles + : (ALL | POPULATED)? roleToken (WITH (USER | USERS))? showCommandYield? + ; + +grantRole + : roleNames TO userNames + ; + +revokeRole + : roleNames FROM userNames + ; + +// User commands + +createUser + : USER commandNameExpression (IF NOT EXISTS)? (SET ( + password + | PASSWORD passwordChangeRequired + | userStatus + | homeDatabase + | setAuthClause + ))+; + +dropUser + : USER commandNameExpression (IF EXISTS)? + ; + +renameUser + : USER commandNameExpression (IF EXISTS)? TO commandNameExpression + ; + +alterCurrentUser + : CURRENT USER SET PASSWORD FROM passwordExpression TO passwordExpression + ; + +alterUser + : USER commandNameExpression (IF EXISTS)? (REMOVE ( + HOME DATABASE + | ALL AUTH (PROVIDER | PROVIDERS)? + | removeNamedProvider + ))* (SET ( + password + | PASSWORD passwordChangeRequired + | userStatus + | homeDatabase + | setAuthClause + ))* + ; + +removeNamedProvider + : AUTH (PROVIDER | PROVIDERS)? (stringLiteral | stringListLiteral | parameter["ANY"]) + ; + +password + : (PLAINTEXT | ENCRYPTED)? PASSWORD passwordExpression passwordChangeRequired? + ; + +passwordOnly + : (PLAINTEXT | ENCRYPTED)? PASSWORD passwordExpression + ; + +passwordExpression + : stringLiteral + | parameter["STRING"] + ; + +passwordChangeRequired + : CHANGE NOT? REQUIRED + ; + +userStatus + : STATUS (SUSPENDED | ACTIVE) + ; + +homeDatabase + : HOME DATABASE symbolicAliasNameOrParameter + ; + +setAuthClause + : AUTH PROVIDER? stringLiteral LCURLY (SET ( + userAuthAttribute + ))+ RCURLY + ; + +userAuthAttribute + : ID stringOrParameterExpression + | passwordOnly + | PASSWORD passwordChangeRequired + ; + +showUsers + : (USER | USERS) (WITH AUTH)? showCommandYield? + ; + +showCurrentUser + : CURRENT USER showCommandYield? + ; + +// Privilege commands + +showSupportedPrivileges + : SUPPORTED privilegeToken showCommandYield? + ; + +showPrivileges + : ALL? privilegeToken privilegeAsCommand? showCommandYield? + ; + +showRolePrivileges + : (ROLE | ROLES) roleNames privilegeToken privilegeAsCommand? showCommandYield? + ; + +showUserPrivileges + : (USER | USERS) userNames? privilegeToken privilegeAsCommand? showCommandYield? + ; + +privilegeAsCommand + : AS REVOKE? (COMMAND | COMMANDS) + ; + +privilegeToken + : PRIVILEGE + | PRIVILEGES + ; + +privilege + : allPrivilege + | createPrivilege + | databasePrivilege + | dbmsPrivilege + | dropPrivilege + | loadPrivilege + | qualifiedGraphPrivileges + | qualifiedGraphPrivilegesWithProperty + | removePrivilege + | setPrivilege + | showPrivilege + | writePrivilege + ; + +allPrivilege + : ALL allPrivilegeType? ON allPrivilegeTarget + ; + +allPrivilegeType + : (DATABASE | GRAPH | DBMS)? PRIVILEGES + ; + +allPrivilegeTarget + : HOME (DATABASE | GRAPH) # DefaultTarget + | (DATABASE | DATABASES) (TIMES | symbolicAliasNameList) # DatabaseVariableTarget + | (GRAPH | GRAPHS) (TIMES | symbolicAliasNameList) # GraphVariableTarget + | DBMS # DBMSTarget + ; + +createPrivilege + : CREATE ( + createPrivilegeForDatabase ON databaseScope + | actionForDBMS ON DBMS + | ON graphScope graphQualifier + ) + ; + +createPrivilegeForDatabase + : indexToken + | constraintToken + | createNodePrivilegeToken + | createRelPrivilegeToken + | createPropertyPrivilegeToken + ; + +createNodePrivilegeToken + : NEW NODE? (LABEL | LABELS) + ; + +createRelPrivilegeToken + : NEW RELATIONSHIP? (TYPE | TYPES) + ; + +createPropertyPrivilegeToken + : NEW PROPERTY? (NAME | NAMES) + ; + +actionForDBMS + : ALIAS + | COMPOSITE? DATABASE + | ROLE + | USER + ; + +dropPrivilege + : DROP ( + (indexToken | constraintToken) ON databaseScope + | actionForDBMS ON DBMS + ) + ; + +loadPrivilege + : LOAD ON ( + (URL | CIDR) stringOrParameter + | ALL DATA + ) + ; + +showPrivilege + : SHOW ( + (indexToken | constraintToken | transactionToken userQualifier?) ON databaseScope + | (ALIAS | PRIVILEGE | ROLE | SERVER | SERVERS | settingToken settingQualifier | USER) ON DBMS + ) + ; + +setPrivilege + : SET ( + (passwordToken | USER (STATUS | HOME DATABASE) | DATABASE ACCESS) ON DBMS + | LABEL labelsResource ON graphScope + | PROPERTY propertiesResource ON graphScope graphQualifier + | AUTH ON DBMS + ) + ; + +passwordToken + : PASSWORD + | PASSWORDS + ; + +removePrivilege + : REMOVE ( + (PRIVILEGE | ROLE) ON DBMS + | LABEL labelsResource ON graphScope + ) + ; + +writePrivilege + : WRITE ON graphScope + ; + +databasePrivilege + : ( + ACCESS + | START + | STOP + | (indexToken | constraintToken | NAME) MANAGEMENT? + | (TRANSACTION MANAGEMENT? | TERMINATE transactionToken) userQualifier? + ) + ON databaseScope + ; + +dbmsPrivilege + : ( + ALTER (ALIAS | DATABASE | USER) + | ASSIGN (PRIVILEGE | ROLE) + | (ALIAS | COMPOSITE? DATABASE | PRIVILEGE | ROLE | SERVER | USER) MANAGEMENT + | dbmsPrivilegeExecute + | RENAME (ROLE | USER) + | IMPERSONATE userQualifier? + ) + ON DBMS + ; + +dbmsPrivilegeExecute + : EXECUTE ( + adminToken PROCEDURES + | BOOSTED? ( + procedureToken executeProcedureQualifier + | (USER DEFINED?)? functionToken executeFunctionQualifier + ) + ) + ; + +adminToken + : ADMIN + | ADMINISTRATOR + ; + +procedureToken + : PROCEDURE + | PROCEDURES + ; + +indexToken + : INDEX + | INDEXES + ; + +constraintToken + : CONSTRAINT + | CONSTRAINTS + ; + +transactionToken + : TRANSACTION + | TRANSACTIONS + ; + +userQualifier + : LPAREN (TIMES | userNames) RPAREN + ; + +executeFunctionQualifier + : globs + ; + +executeProcedureQualifier + : globs + ; + +settingQualifier + : globs + ; + +globs + : glob (COMMA glob)* + ; + +glob + : escapedSymbolicNameString globRecursive? + | globRecursive + ; + +globRecursive + : globPart globRecursive? + ; + +globPart + : DOT escapedSymbolicNameString? + | QUESTION + | TIMES + | unescapedSymbolicNameString + ; + +qualifiedGraphPrivilegesWithProperty + : (TRAVERSE | (READ | MATCH) propertiesResource) ON graphScope graphQualifier (LPAREN TIMES RPAREN)? + ; + +qualifiedGraphPrivileges + : (DELETE | MERGE propertiesResource) ON graphScope graphQualifier + ; + +labelsResource + : TIMES + | nonEmptyStringList + ; + +propertiesResource + : LCURLY (TIMES | nonEmptyStringList) RCURLY + ; + +// Returns non-empty list of strings +nonEmptyStringList + : symbolicNameString (COMMA symbolicNameString)* + ; + +graphQualifier + : ( + graphQualifierToken (TIMES | nonEmptyStringList) + | FOR LPAREN variable? (COLON symbolicNameString (BAR symbolicNameString)*)? (RPAREN WHERE expression | (WHERE expression | map) RPAREN) + )? + ; + +graphQualifierToken + : relToken + | nodeToken + | elementToken + ; + +relToken + : RELATIONSHIP + | RELATIONSHIPS + ; + +elementToken + : ELEMENT + | ELEMENTS + ; + +nodeToken + : NODE + | NODES + ; + +databaseScope + : HOME DATABASE + | (DATABASE | DATABASES) (TIMES | symbolicAliasNameList) + ; + +graphScope + : HOME GRAPH + | (GRAPH | GRAPHS) (TIMES | symbolicAliasNameList) + ; + +// Database commands + +createCompositeDatabase + : COMPOSITE DATABASE symbolicAliasNameOrParameter (IF NOT EXISTS)? commandOptions? waitClause? + ; + +createDatabase + : DATABASE symbolicAliasNameOrParameter (IF NOT EXISTS)? (TOPOLOGY (primaryTopology | secondaryTopology)+)? commandOptions? waitClause? + ; + +primaryTopology + : uIntOrIntParameter primaryToken + ; + +primaryToken + : PRIMARY | PRIMARIES + ; + +secondaryTopology + : uIntOrIntParameter secondaryToken + ; + +secondaryToken + : SECONDARY | SECONDARIES + ; + +dropDatabase + : COMPOSITE? DATABASE symbolicAliasNameOrParameter (IF EXISTS)? aliasAction? ((DUMP | DESTROY) DATA)? waitClause? + ; + +aliasAction + : RESTRICT + | CASCADE (ALIAS | ALIASES) + ; + +alterDatabase + : DATABASE symbolicAliasNameOrParameter (IF EXISTS)? ( + (SET (alterDatabaseAccess | alterDatabaseTopology | alterDatabaseOption))+ + | (REMOVE OPTION symbolicNameString)+ + ) waitClause? + ; + +alterDatabaseAccess + : ACCESS READ (ONLY | WRITE) + ; + +alterDatabaseTopology + : TOPOLOGY (primaryTopology | secondaryTopology)+ + ; + +alterDatabaseOption + : OPTION symbolicNameString expression + ; + +startDatabase + : START DATABASE symbolicAliasNameOrParameter waitClause? + ; + +stopDatabase + : STOP DATABASE symbolicAliasNameOrParameter waitClause? + ; + +waitClause + : WAIT (UNSIGNED_DECIMAL_INTEGER secondsToken?)? + | NOWAIT + ; + +secondsToken + : SEC | SECOND | SECONDS; + +showDatabase + : (DEFAULT | HOME) DATABASE showCommandYield? + | (DATABASE | DATABASES) symbolicAliasNameOrParameter? showCommandYield? + ; + +aliasName + : symbolicAliasNameOrParameter + ; + +databaseName + : symbolicAliasNameOrParameter + ; + +// Alias commands + +createAlias + : ALIAS aliasName (IF NOT EXISTS)? FOR DATABASE databaseName (AT stringOrParameter USER commandNameExpression PASSWORD passwordExpression (DRIVER mapOrParameter)?)? (PROPERTIES mapOrParameter)? + ; + +dropAlias + : ALIAS aliasName (IF EXISTS)? FOR DATABASE + ; + +alterAlias + : ALIAS aliasName (IF EXISTS)? SET DATABASE ( + alterAliasTarget + | alterAliasUser + | alterAliasPassword + | alterAliasDriver + | alterAliasProperties + )+ + ; + +alterAliasTarget + : TARGET databaseName (AT stringOrParameter)? + ; + +alterAliasUser + : USER commandNameExpression + ; + +alterAliasPassword + : PASSWORD passwordExpression + ; + +alterAliasDriver + : DRIVER mapOrParameter + ; + +alterAliasProperties + : PROPERTIES mapOrParameter + ; + +showAliases + : (ALIAS | ALIASES) aliasName? FOR (DATABASE | DATABASES) showCommandYield? + ; + +// Various strings, symbolic names, lists and maps + +// Should return an Either[String, Parameter] +symbolicNameOrStringParameter + : symbolicNameString + | parameter["STRING"] + ; + +// Should return an Expression +commandNameExpression + : symbolicNameString + | parameter["STRING"] + ; + +symbolicNameOrStringParameterList + : commandNameExpression (COMMA commandNameExpression)* + ; + +symbolicAliasNameList + : symbolicAliasNameOrParameter (COMMA symbolicAliasNameOrParameter)* + ; + +symbolicAliasNameOrParameter + : symbolicAliasName + | parameter["STRING"] + ; + +symbolicAliasName + : symbolicNameString (DOT symbolicNameString)* + ; + +stringListLiteral + : LBRACKET (stringLiteral (COMMA stringLiteral)*)? RBRACKET + ; + +stringList + : stringLiteral (COMMA stringLiteral)+ + ; + +stringLiteral + : STRING_LITERAL1 + | STRING_LITERAL2 + ; + +// Should return an Expression +stringOrParameterExpression + : stringLiteral + | parameter["STRING"] + ; + +// Should return an Either[String, Parameter] +stringOrParameter + : stringLiteral + | parameter["STRING"] + ; + +// Should return an Either[Integer, Parameter] +// There is no unsigned integer Cypher Type so the parameter permits signed values. +uIntOrIntParameter + :UNSIGNED_DECIMAL_INTEGER + | parameter["INTEGER"] + ; + +mapOrParameter + : map + | parameter["MAP"] + ; + +map + : LCURLY (propertyKeyName COLON expression (COMMA propertyKeyName COLON expression)*)? RCURLY + ; + +symbolicNameString + : escapedSymbolicNameString + | unescapedSymbolicNameString + ; + +escapedSymbolicNameString + : ESCAPED_SYMBOLIC_NAME + ; + +unescapedSymbolicNameString + : unescapedLabelSymbolicNameString + | NOT + | NULL + | TYPED + | NORMALIZED + | NFC + | NFD + | NFKC + | NFKD + ; + +symbolicLabelNameString + : escapedSymbolicNameString + | unescapedLabelSymbolicNameString + ; + +// Do not remove this, it is needed for composing the grammar +// with other ones (e.g. language support ones) +unescapedLabelSymbolicNameString + : unescapedLabelSymbolicNameString_ + ; + +unescapedLabelSymbolicNameString_ + : IDENTIFIER + | ACCESS + | ACTIVE + | ADMIN + | ADMINISTRATOR + | ALIAS + | ALIASES + | ALL_SHORTEST_PATHS + | ALL + | ALTER + | AND + | ANY + | ARRAY + | AS + | ASC + | ASCENDING + | ASSIGN + | AT + | AUTH + | BINDINGS + | BOOL + | BOOLEAN + | BOOSTED + | BOTH + | BREAK + | BUILT + | BY + | CALL + | CASCADE + | CASE + | CHANGE + | CIDR + | COLLECT + | COMMAND + | COMMANDS + | COMPOSITE + | CONCURRENT + | CONSTRAINT + | CONSTRAINTS + | CONTAINS + | CONTINUE + | COPY + | COUNT + | CREATE + | CSV + | CURRENT + | DATA + | DATABASE + | DATABASES + | DATE + | DATETIME + | DBMS + | DEALLOCATE + | DEFAULT + | DEFINED + | DELETE + | DENY + | DESC + | DESCENDING + | DESTROY + | DETACH + | DIFFERENT + | DISTINCT + | DRIVER + | DROP + | DRYRUN + | DUMP + | DURATION + | EACH + | EDGE + | ELEMENT + | ELEMENTS + | ELSE + | ENABLE + | ENCRYPTED + | END + | ENDS + | ERROR + | EXECUTABLE + | EXECUTE + | EXIST + | EXISTENCE + | EXISTS + | FAIL + | FALSE + | FIELDTERMINATOR + | FINISH + | FLOAT + | FOREACH + | FOR + | FROM + | FULLTEXT + | FUNCTION + | FUNCTIONS + | GRANT + | GRAPH + | GRAPHS + | GROUP + | GROUPS + | HEADERS + | HOME + | ID + | IF + | IMMUTABLE + | IMPERSONATE + | IN + | INDEX + | INDEXES + | INF + | INFINITY + | INSERT + | INT + | INTEGER + | IS + | JOIN + | KEY + | LABEL + | LABELS + | LEADING + | LIMITROWS + | LIST + | LOAD + | LOCAL + | LOOKUP + | MATCH + | MANAGEMENT + | MAP + | MERGE + | NAME + | NAMES + | NAN + | NEW + | NODE + | NODETACH + | NODES + | NONE + | NORMALIZE + | NOTHING + | NOWAIT + | OF + | OFFSET + | ON + | ONLY + | OPTIONAL + | OPTIONS + | OPTION + | OR + | ORDER + | PASSWORD + | PASSWORDS + | PATH + | PATHS + | PLAINTEXT + | POINT + | POPULATED + | PRIMARY + | PRIMARIES + | PRIVILEGE + | PRIVILEGES + | PROCEDURE + | PROCEDURES + | PROPERTIES + | PROPERTY + | PROVIDER + | PROVIDERS + | RANGE + | READ + | REALLOCATE + | REDUCE + | REL + | RELATIONSHIP + | RELATIONSHIPS + | REMOVE + | RENAME + | REPEATABLE + | REPLACE + | REPORT + | REQUIRE + | REQUIRED + | RESTRICT + | RETURN + | REVOKE + | ROLE + | ROLES + | ROW + | ROWS + | SCAN + | SECONDARY + | SECONDARIES + | SEC + | SECOND + | SECONDS + | SEEK + | SERVER + | SERVERS + | SET + | SETTING + | SETTINGS + | SHORTEST + | SHORTEST_PATH + | SHOW + | SIGNED + | SINGLE + | SKIPROWS + | START + | STARTS + | STATUS + | STOP + | VARCHAR + | STRING + | SUPPORTED + | SUSPENDED + | TARGET + | TERMINATE + | TEXT + | THEN + | TIME + | TIMESTAMP + | TIMEZONE + | TO + | TOPOLOGY + | TRAILING + | TRANSACTION + | TRANSACTIONS + | TRAVERSE + | TRIM + | TRUE + | TYPE + | TYPES + | UNION + | UNIQUE + | UNIQUENESS + | UNWIND + | URL + | USE + | USER + | USERS + | USING + | VALUE + | VECTOR + | VERTEX + | WAIT + | WHEN + | WHERE + | WITH + | WITHOUT + | WRITE + | XOR + | YIELD + | ZONE + | ZONED + ; + +endOfFile + : EOF + ; diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/AndCondition.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/AndCondition.java new file mode 100644 index 0000000000..445d42b8ce --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/AndCondition.java @@ -0,0 +1,45 @@ +package org.evomaster.client.java.controller.neo4j.conditions; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Represents a logical AND of multiple conditions. + */ +public class AndCondition implements CypherCondition { + + private final List conditions; + + public AndCondition(List conditions) { + this.conditions = conditions != null ? new ArrayList<>(conditions) : new ArrayList<>(); + } + + public List getConditions() { + return Collections.unmodifiableList(conditions); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("("); + for (int i = 0; i < conditions.size(); i++) { + if (i > 0) sb.append(" AND "); + sb.append(conditions.get(i)); + } + sb.append(")"); + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AndCondition that = (AndCondition) o; + return conditions.equals(that.conditions); + } + + @Override + public int hashCode() { + return conditions.hashCode(); + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/AnyLabelCondition.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/AnyLabelCondition.java new file mode 100644 index 0000000000..f0081c5b44 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/AnyLabelCondition.java @@ -0,0 +1,37 @@ +package org.evomaster.client.java.controller.neo4j.conditions; + +import java.util.Objects; + +/** + * The any-label wildcard % in a label expression, e.g. (n:%). Satisfied when the element + * bound to the variable has a non-empty label set. + */ +public class AnyLabelCondition implements CypherCondition { + + private final String variableName; + + public AnyLabelCondition(String variableName) { + this.variableName = variableName; + } + + public String getVariableName() { + return variableName; + } + + @Override + public String toString() { + return variableName + ":%"; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return Objects.equals(variableName, ((AnyLabelCondition) o).variableName); + } + + @Override + public int hashCode() { + return variableName != null ? variableName.hashCode() : 0; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/ArithmeticOperand.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/ArithmeticOperand.java new file mode 100644 index 0000000000..db3f7d06d5 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/ArithmeticOperand.java @@ -0,0 +1,78 @@ +package org.evomaster.client.java.controller.neo4j.conditions; + +import java.util.Objects; + +/** + * An operand that is an arithmetic expression over other operands, e.g. {@code p.age + 5} or + * {@code -n.weight}. Keeps the structure (operator + sub-operands) instead of the original text, + * so the inner {@link PropertyOperand}s stay resolvable per mapping and the heuristics calculator + * can valuate {@code v(x op y) = apply(op, v(left), v(right))}. + * + *

Fully-literal subtrees are folded to a {@link LiteralOperand} at parse time, so this class only + * ever holds an expression that still depends on the graph (at least one property reference). + * + *

For {@link ArithmeticOperator#NEGATE} (unary minus) the operand is {@link #getLeft()} and + * {@link #getRight()} is {@code null}. + */ +public final class ArithmeticOperand implements Operand { + + private final ArithmeticOperator operator; + private final Operand left; + private final Operand right; + + public ArithmeticOperand(ArithmeticOperator operator, Operand left, Operand right) { + this.operator = operator; + this.left = left; + this.right = right; + } + + public ArithmeticOperator getOperator() { + return operator; + } + + public Operand getLeft() { + return left; + } + + public Operand getRight() { + return right; + } + + @Override + public String toString() { + if (operator == ArithmeticOperator.NEGATE) { + return "-" + left; + } + return "(" + left + " " + symbol() + " " + right + ")"; + } + + private String symbol() { + switch (operator) { + case PLUS: return "+"; + case MINUS: return "-"; + case TIMES: return "*"; + case DIVIDE: return "/"; + case MODULO: return "%"; + case POWER: return "^"; + default: return "?"; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ArithmeticOperand that = (ArithmeticOperand) o; + return operator == that.operator + && Objects.equals(left, that.left) + && Objects.equals(right, that.right); + } + + @Override + public int hashCode() { + int result = operator != null ? operator.hashCode() : 0; + result = 31 * result + (left != null ? left.hashCode() : 0); + result = 31 * result + (right != null ? right.hashCode() : 0); + return result; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/ArithmeticOperator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/ArithmeticOperator.java new file mode 100644 index 0000000000..5062903856 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/ArithmeticOperator.java @@ -0,0 +1,15 @@ +package org.evomaster.client.java.controller.neo4j.conditions; + +/** + * The arithmetic connectives an {@link ArithmeticOperand} can carry. Binary operators map to the + * Cypher tokens {@code + - * / % ^}; {@link #NEGATE} is unary minus (its operand is the left side). + */ +public enum ArithmeticOperator { + PLUS, + MINUS, + TIMES, + DIVIDE, + MODULO, + POWER, + NEGATE +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/ComparisonCondition.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/ComparisonCondition.java new file mode 100644 index 0000000000..7073307024 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/ComparisonCondition.java @@ -0,0 +1,59 @@ +package org.evomaster.client.java.controller.neo4j.conditions; + +import java.util.Objects; + +/** + * A comparison from a WHERE clause: left OP right. + * Both sides are typed {@link Operand}s so each can be valuated against the graph + * independently. The right operand is null for the unary IS NULL / IS NOT NULL operators. + */ +public class ComparisonCondition implements CypherCondition { + + private final Operand left; + private final ComparisonOperator operator; + private final Operand right; + + public ComparisonCondition(Operand left, ComparisonOperator operator, Operand right) { + this.left = left; + this.operator = operator; + this.right = right; + } + + public Operand getLeft() { + return left; + } + + public ComparisonOperator getOperator() { + return operator; + } + + public Operand getRight() { + return right; + } + + @Override + public String toString() { + if (right == null) { + return left + " " + operator.getSymbol(); + } + return left + " " + operator.getSymbol() + " " + right; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ComparisonCondition that = (ComparisonCondition) o; + return operator == that.operator + && Objects.equals(left, that.left) + && Objects.equals(right, that.right); + } + + @Override + public int hashCode() { + int result = left != null ? left.hashCode() : 0; + result = 31 * result + (operator != null ? operator.hashCode() : 0); + result = 31 * result + (right != null ? right.hashCode() : 0); + return result; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/ComparisonOperator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/ComparisonOperator.java new file mode 100644 index 0000000000..1a28b290cc --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/ComparisonOperator.java @@ -0,0 +1,38 @@ +package org.evomaster.client.java.controller.neo4j.conditions; + +/** + * Comparison operators used in Cypher WHERE clauses. + */ +public enum ComparisonOperator { + EQUALS("="), + NOT_EQUALS("<>"), + LESS_THAN("<"), + LESS_THAN_OR_EQUALS("<="), + GREATER_THAN(">"), + GREATER_THAN_OR_EQUALS(">="), + STARTS_WITH("STARTS WITH"), + ENDS_WITH("ENDS WITH"), + CONTAINS("CONTAINS"), + IN("IN"), + IS_NULL("IS NULL"), + IS_NOT_NULL("IS NOT NULL"); + + private final String symbol; + + ComparisonOperator(String symbol) { + this.symbol = symbol; + } + + public String getSymbol() { + return symbol; + } + + public static ComparisonOperator fromSymbol(String symbol) { + for (ComparisonOperator op : values()) { + if (op.symbol.equalsIgnoreCase(symbol)) { + return op; + } + } + return null; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/CypherCondition.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/CypherCondition.java new file mode 100644 index 0000000000..7fea8e2348 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/CypherCondition.java @@ -0,0 +1,14 @@ +package org.evomaster.client.java.controller.neo4j.conditions; + +/** + * Represents a condition extracted from a Cypher query. + * Conditions can come from: + * - Node labels: (n:Person) → LabelCondition + * - Node properties: (n {name: "Alice"}) → PropertyCondition + * - Edge types: -[:KNOWS]-> → TypeCondition + * - Edge properties: -[:KNOWS {since: 2020}]-> → PropertyCondition + * - WHERE clause: WHERE n.age > 25 → ComparisonCondition + * - Logical operators: AND, OR, NOT + */ +public interface CypherCondition { +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/LabelCondition.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/LabelCondition.java new file mode 100644 index 0000000000..248580bdd4 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/LabelCondition.java @@ -0,0 +1,47 @@ +package org.evomaster.client.java.controller.neo4j.conditions; + +import java.util.Objects; + +/** + * Represents a label condition: n:Label + * Extracted from patterns like (n:Person) or (n:Person:Employee) + */ +public class LabelCondition implements CypherCondition { + + private final String variableName; + private final String label; + + public LabelCondition(String variableName, String label) { + this.variableName = variableName; + this.label = label; + } + + public String getVariableName() { + return variableName; + } + + public String getLabel() { + return label; + } + + @Override + public String toString() { + return variableName + ":" + label; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + LabelCondition that = (LabelCondition) o; + if (!Objects.equals(variableName, that.variableName)) return false; + return Objects.equals(label, that.label); + } + + @Override + public int hashCode() { + int result = variableName != null ? variableName.hashCode() : 0; + result = 31 * result + (label != null ? label.hashCode() : 0); + return result; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/ListOperand.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/ListOperand.java new file mode 100644 index 0000000000..33062cc348 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/ListOperand.java @@ -0,0 +1,39 @@ +package org.evomaster.client.java.controller.neo4j.conditions; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A list operand, e.g. the right-hand side of x IN [1, 2, 3]. Holds the element operands + * so a membership test can be valuated element by element. + */ +public final class ListOperand implements Operand { + + private final List elements; + + public ListOperand(List elements) { + this.elements = elements != null ? new ArrayList<>(elements) : new ArrayList<>(); + } + + public List getElements() { + return Collections.unmodifiableList(elements); + } + + @Override + public String toString() { + return elements.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return elements.equals(((ListOperand) o).elements); + } + + @Override + public int hashCode() { + return elements.hashCode(); + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/LiteralOperand.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/LiteralOperand.java new file mode 100644 index 0000000000..b3d300371d --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/LiteralOperand.java @@ -0,0 +1,39 @@ +package org.evomaster.client.java.controller.neo4j.conditions; + +import java.util.Objects; + +/** + * A constant operand: a string, number, boolean or null literal. + */ +public final class LiteralOperand implements Operand { + + private final Object value; + + public LiteralOperand(Object value) { + this.value = value; + } + + public Object getValue() { + return value; + } + + @Override + public String toString() { + if (value == null) { + return "null"; + } + return value instanceof String ? "\"" + value + "\"" : String.valueOf(value); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return Objects.equals(value, ((LiteralOperand) o).value); + } + + @Override + public int hashCode() { + return value != null ? value.hashCode() : 0; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/NotCondition.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/NotCondition.java new file mode 100644 index 0000000000..c4b5f64be0 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/NotCondition.java @@ -0,0 +1,37 @@ +package org.evomaster.client.java.controller.neo4j.conditions; + +import java.util.Objects; + +/** + * Represents a logical NOT of a condition. + */ +public class NotCondition implements CypherCondition { + + private final CypherCondition condition; + + public NotCondition(CypherCondition condition) { + this.condition = condition; + } + + public CypherCondition getCondition() { + return condition; + } + + @Override + public String toString() { + return "NOT " + condition; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NotCondition that = (NotCondition) o; + return Objects.equals(condition, that.condition); + } + + @Override + public int hashCode() { + return condition != null ? condition.hashCode() : 0; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/Operand.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/Operand.java new file mode 100644 index 0000000000..a0a44cd40c --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/Operand.java @@ -0,0 +1,9 @@ +package org.evomaster.client.java.controller.neo4j.conditions; + +/** + * One side of a comparison in a WHERE clause: a {@link PropertyOperand} resolved from the + * matched element, a {@link LiteralOperand} constant, or a {@link RawOperand} kept unchanged. + * Modelling each side explicitly lets {@code n.age > m.age} be told apart from {@code n.age > "m.age"}. + */ +public interface Operand { +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/OrCondition.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/OrCondition.java new file mode 100644 index 0000000000..2f0b47f873 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/OrCondition.java @@ -0,0 +1,45 @@ +package org.evomaster.client.java.controller.neo4j.conditions; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Represents a logical OR of multiple conditions. + */ +public class OrCondition implements CypherCondition { + + private final List conditions; + + public OrCondition(List conditions) { + this.conditions = conditions != null ? new ArrayList<>(conditions) : new ArrayList<>(); + } + + public List getConditions() { + return Collections.unmodifiableList(conditions); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("("); + for (int i = 0; i < conditions.size(); i++) { + if (i > 0) sb.append(" OR "); + sb.append(conditions.get(i)); + } + sb.append(")"); + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + OrCondition that = (OrCondition) o; + return conditions.equals(that.conditions); + } + + @Override + public int hashCode() { + return conditions.hashCode(); + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/PropertyCondition.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/PropertyCondition.java new file mode 100644 index 0000000000..ba80898fa6 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/PropertyCondition.java @@ -0,0 +1,57 @@ +package org.evomaster.client.java.controller.neo4j.conditions; + +import java.util.Objects; + +/** + * Represents a property equality condition {@code n.key = value} extracted from an inline property + * map like {@code {name: "Alice"}}. The value is a typed {@link Operand}, the same representation a + * WHERE comparison uses, so {@code {age: 25 + 5}} folds to a literal and {@code {at: time("11:11")}} + * keeps the expression as a {@link RawOperand} instead of guessing a value. + */ +public class PropertyCondition implements CypherCondition { + + private final String variableName; + private final String propertyKey; + private final Operand value; + + public PropertyCondition(String variableName, String propertyKey, Operand value) { + this.variableName = variableName; + this.propertyKey = propertyKey; + this.value = value; + } + + public String getVariableName() { + return variableName; + } + + public String getPropertyKey() { + return propertyKey; + } + + public Operand getValue() { + return value; + } + + @Override + public String toString() { + return variableName + "." + propertyKey + " = " + value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PropertyCondition that = (PropertyCondition) o; + if (!Objects.equals(variableName, that.variableName)) return false; + if (!Objects.equals(propertyKey, that.propertyKey)) return false; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + int result = variableName != null ? variableName.hashCode() : 0; + result = 31 * result + (propertyKey != null ? propertyKey.hashCode() : 0); + result = 31 * result + (value != null ? value.hashCode() : 0); + return result; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/PropertyOperand.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/PropertyOperand.java new file mode 100644 index 0000000000..dc596047dd --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/PropertyOperand.java @@ -0,0 +1,47 @@ +package org.evomaster.client.java.controller.neo4j.conditions; + +import java.util.Objects; + +/** + * A property reference operand: variable.key (e.g. n.age), resolved from the graph + * element bound to the variable under the current mapping. + */ +public final class PropertyOperand implements Operand { + + private final String variableName; + private final String propertyKey; + + public PropertyOperand(String variableName, String propertyKey) { + this.variableName = variableName; + this.propertyKey = propertyKey; + } + + public String getVariableName() { + return variableName; + } + + public String getPropertyKey() { + return propertyKey; + } + + @Override + public String toString() { + return variableName + "." + propertyKey; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PropertyOperand that = (PropertyOperand) o; + return Objects.equals(variableName, that.variableName) + && Objects.equals(propertyKey, that.propertyKey); + } + + @Override + public int hashCode() { + int result = variableName != null ? variableName.hashCode() : 0; + result = 31 * result + (propertyKey != null ? propertyKey.hashCode() : 0); + return result; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/RawCondition.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/RawCondition.java new file mode 100644 index 0000000000..64884725cc --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/RawCondition.java @@ -0,0 +1,39 @@ +package org.evomaster.client.java.controller.neo4j.conditions; + +import java.util.Objects; + +/** + * A WHERE predicate kept unchanged because the structural model does not decompose + * it (e.g. a boolean-returning function call or a bare boolean expression). It exists + * so that no part of the query is silently dropped: the boolean structure around it + * (AND/OR/XOR/NOT) is still preserved, and this leaf carries the original text. + */ +public class RawCondition implements CypherCondition { + + private final String expression; + + public RawCondition(String expression) { + this.expression = expression; + } + + public String getExpression() { + return expression; + } + + @Override + public String toString() { + return expression; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return Objects.equals(expression, ((RawCondition) o).expression); + } + + @Override + public int hashCode() { + return expression != null ? expression.hashCode() : 0; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/RawOperand.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/RawOperand.java new file mode 100644 index 0000000000..ddf94e9ad7 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/RawOperand.java @@ -0,0 +1,37 @@ +package org.evomaster.client.java.controller.neo4j.conditions; + +import java.util.Objects; + +/** + * An operand kept unchanged because the model does not decompose it: arithmetic such as + * 25 + 5, a function call, or a list. Carries the original text so nothing is dropped. + */ +public final class RawOperand implements Operand { + + private final String text; + + public RawOperand(String text) { + this.text = text; + } + + public String getText() { + return text; + } + + @Override + public String toString() { + return text; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return Objects.equals(text, ((RawOperand) o).text); + } + + @Override + public int hashCode() { + return text != null ? text.hashCode() : 0; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/TypeCondition.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/TypeCondition.java new file mode 100644 index 0000000000..7177078b81 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/TypeCondition.java @@ -0,0 +1,47 @@ +package org.evomaster.client.java.controller.neo4j.conditions; + +import java.util.Objects; + +/** + * Represents a relationship type condition: type(r) = T + * Extracted from patterns like -[:KNOWS]-> or -[r:WORKS_AT]-> + */ +public class TypeCondition implements CypherCondition { + + private final String variableName; + private final String type; + + public TypeCondition(String variableName, String type) { + this.variableName = variableName; + this.type = type; + } + + public String getVariableName() { + return variableName; + } + + public String getType() { + return type; + } + + @Override + public String toString() { + return "type(" + variableName + ") = " + type; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TypeCondition that = (TypeCondition) o; + if (!Objects.equals(variableName, that.variableName)) return false; + return Objects.equals(type, that.type); + } + + @Override + public int hashCode() { + int result = variableName != null ? variableName.hashCode() : 0; + result = 31 * result + (type != null ? type.hashCode() : 0); + return result; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/XorCondition.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/XorCondition.java new file mode 100644 index 0000000000..0eb3e60cf3 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/XorCondition.java @@ -0,0 +1,45 @@ +package org.evomaster.client.java.controller.neo4j.conditions; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Represents a logical XOR of multiple conditions. + */ +public class XorCondition implements CypherCondition { + + private final List conditions; + + public XorCondition(List conditions) { + this.conditions = conditions != null ? new ArrayList<>(conditions) : new ArrayList<>(); + } + + public List getConditions() { + return Collections.unmodifiableList(conditions); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("("); + for (int i = 0; i < conditions.size(); i++) { + if (i > 0) sb.append(" XOR "); + sb.append(conditions.get(i)); + } + sb.append(")"); + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + XorCondition that = (XorCondition) o; + return conditions.equals(that.conditions); + } + + @Override + public int hashCode() { + return conditions.hashCode(); + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/operations/CypherQueryOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/operations/CypherQueryOperation.java new file mode 100644 index 0000000000..3f166560bf --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/operations/CypherQueryOperation.java @@ -0,0 +1,7 @@ +package org.evomaster.client.java.controller.neo4j.operations; + +/** + * Represents a parsed Cypher query operation. + */ +public abstract class CypherQueryOperation { +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/operations/MatchOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/operations/MatchOperation.java new file mode 100644 index 0000000000..0070ee7088 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/operations/MatchOperation.java @@ -0,0 +1,98 @@ +package org.evomaster.client.java.controller.neo4j.operations; + +import org.evomaster.client.java.controller.neo4j.conditions.CypherCondition; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Represents a parsed MATCH query containing the structural pattern and extracted conditions. + *

+ * The structural pattern contains only the graph topology (nodes and edges). + * Conditions include labels, relationship types, property constraints, and WHERE clause predicates. + *

+ * A query may assign several path variables ({@code MATCH p = ..., q = ...}); all are kept in order. + * {@link #isOptional()} is true when the query involves an {@code OPTIONAL MATCH} (left-join semantics). + */ +public class MatchOperation extends CypherQueryOperation { + + private final MatchPattern pattern; + private final List conditions; + private final List pathVariables; + private final boolean optional; + + public MatchOperation(MatchPattern pattern, List conditions) { + this(pattern, conditions, (String) null); + } + + public MatchOperation(MatchPattern pattern, List conditions, String pathVariable) { + this(pattern, conditions, + pathVariable != null ? Collections.singletonList(pathVariable) : Collections.emptyList(), + false); + } + + public MatchOperation(MatchPattern pattern, List conditions, + List pathVariables, boolean optional) { + this.pattern = pattern; + this.conditions = conditions != null ? new ArrayList<>(conditions) : new ArrayList<>(); + this.pathVariables = pathVariables != null ? new ArrayList<>(pathVariables) : new ArrayList<>(); + this.optional = optional; + } + + /** + * Returns the structural pattern containing nodes and edges. + */ + public MatchPattern getPattern() { + return pattern; + } + + /** + * Returns all conditions extracted from both inline patterns and WHERE clause. + */ + public List getConditions() { + return Collections.unmodifiableList(conditions); + } + + /** + * Returns the first path variable assigned by the pattern (e.g. {@code path = ...}), or null if + * none. Kept for callers that expect a single assignment; {@link #getPathVariables()} returns all. + */ + public String getPathVariable() { + return pathVariables.isEmpty() ? null : pathVariables.get(0); + } + + /** + * Returns every path variable assigned by the query, in source order (empty if there are none). + */ + public List getPathVariables() { + return Collections.unmodifiableList(pathVariables); + } + + /** + * Returns true when the query involves an {@code OPTIONAL MATCH}. The match still succeeds with + * no binding when the pattern is absent, so a consumer should not penalize a missing optional part. + */ + public boolean isOptional() { + return optional; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("MatchOperation{"); + if (optional) { + sb.append("optional, "); + } + if (!pathVariables.isEmpty()) { + sb.append("pathVariables=").append(pathVariables).append(", "); + } + sb.append("pattern=").append(pattern); + sb.append(", conditions=["); + for (int i = 0; i < conditions.size(); i++) { + if (i > 0) sb.append(", "); + sb.append(conditions.get(i)); + } + sb.append("]}"); + return sb.toString(); + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/operations/MatchPattern.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/operations/MatchPattern.java new file mode 100644 index 0000000000..e1cc706cb8 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/operations/MatchPattern.java @@ -0,0 +1,76 @@ +package org.evomaster.client.java.controller.neo4j.operations; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Represents the structural MATCH pattern (P_s) after stripping labels and properties. + * Contains nodes, edges, and any quantified path patterns (repeated sub-patterns). + */ +public class MatchPattern { + + private final List nodes; + private final List edges; + private final List quantifiedPaths; + + public MatchPattern(List nodes, List edges) { + this(nodes, edges, null); + } + + public MatchPattern(List nodes, List edges, + List quantifiedPaths) { + this.nodes = nodes != null ? new ArrayList<>(nodes) : new ArrayList<>(); + this.edges = edges != null ? new ArrayList<>(edges) : new ArrayList<>(); + this.quantifiedPaths = quantifiedPaths != null ? new ArrayList<>(quantifiedPaths) : new ArrayList<>(); + } + + public List getNodes() { + return Collections.unmodifiableList(nodes); + } + + public List getEdges() { + return Collections.unmodifiableList(edges); + } + + public List getQuantifiedPaths() { + return Collections.unmodifiableList(quantifiedPaths); + } + + public int nodeCount() { + return nodes.size(); + } + + public int edgeCount() { + return edges.size(); + } + + public int quantifiedPathCount() { + return quantifiedPaths.size(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("MatchPattern{nodes=["); + for (int i = 0; i < nodes.size(); i++) { + if (i > 0) sb.append(", "); + sb.append(nodes.get(i)); + } + sb.append("], edges=["); + for (int i = 0; i < edges.size(); i++) { + if (i > 0) sb.append(", "); + sb.append(edges.get(i)); + } + sb.append("]"); + if (!quantifiedPaths.isEmpty()) { + sb.append(", quantifiedPaths=["); + for (int i = 0; i < quantifiedPaths.size(); i++) { + if (i > 0) sb.append(", "); + sb.append(quantifiedPaths.get(i)); + } + sb.append("]"); + } + sb.append("}"); + return sb.toString(); + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/operations/PatternEdge.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/operations/PatternEdge.java new file mode 100644 index 0000000000..a51080d370 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/operations/PatternEdge.java @@ -0,0 +1,111 @@ +package org.evomaster.client.java.controller.neo4j.operations; + +import java.util.Objects; + +/** + * Represents an edge in a structural MATCH pattern. + * Contains source/target variable names, direction, and variable-length path information. + * Type and properties are not part of the structural pattern - they become conditions. + */ +public class PatternEdge { + + private final String variableName; + private final String sourceVariable; + private final String targetVariable; + private final boolean directed; + private final boolean variableLength; + private final Integer minLength; + private final Integer maxLength; + + public PatternEdge(String variableName, String sourceVariable, String targetVariable, boolean directed) { + this(variableName, sourceVariable, targetVariable, directed, false, null, null); + } + + public PatternEdge(String variableName, String sourceVariable, String targetVariable, + boolean directed, boolean variableLength, Integer minLength, Integer maxLength) { + this.variableName = variableName; + this.sourceVariable = sourceVariable; + this.targetVariable = targetVariable; + this.directed = directed; + this.variableLength = variableLength; + this.minLength = minLength; + this.maxLength = maxLength; + } + + public String getVariableName() { + return variableName; + } + + public String getSourceVariable() { + return sourceVariable; + } + + public String getTargetVariable() { + return targetVariable; + } + + public boolean isDirected() { + return directed; + } + + public boolean isVariableLength() { + return variableLength; + } + + public Integer getMinLength() { + return minLength; + } + + public Integer getMaxLength() { + return maxLength; + } + + @Override + public String toString() { + StringBuilder relPart = new StringBuilder("["); + if (variableName != null) { + relPart.append(variableName); + } + if (variableLength) { + relPart.append("*"); + if (minLength != null || maxLength != null) { + if (minLength != null) relPart.append(minLength); + relPart.append(".."); + if (maxLength != null) relPart.append(maxLength); + } + } + relPart.append("]"); + + if (directed) { + return "(" + sourceVariable + ")-" + relPart + "->(" + targetVariable + ")"; + } else { + return "(" + sourceVariable + ")-" + relPart + "-(" + targetVariable + ")"; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PatternEdge that = (PatternEdge) o; + if (directed != that.directed) return false; + if (variableLength != that.variableLength) return false; + if (!Objects.equals(variableName, that.variableName)) return false; + if (!Objects.equals(sourceVariable, that.sourceVariable)) return false; + if (!Objects.equals(targetVariable, that.targetVariable)) return false; + if (!Objects.equals(minLength, that.minLength)) return false; + return Objects.equals(maxLength, that.maxLength); + } + + @Override + public int hashCode() { + int result = variableName != null ? variableName.hashCode() : 0; + result = 31 * result + (sourceVariable != null ? sourceVariable.hashCode() : 0); + result = 31 * result + (targetVariable != null ? targetVariable.hashCode() : 0); + result = 31 * result + (directed ? 1 : 0); + result = 31 * result + (variableLength ? 1 : 0); + result = 31 * result + (minLength != null ? minLength.hashCode() : 0); + result = 31 * result + (maxLength != null ? maxLength.hashCode() : 0); + return result; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/operations/PatternNode.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/operations/PatternNode.java new file mode 100644 index 0000000000..32f3164523 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/operations/PatternNode.java @@ -0,0 +1,37 @@ +package org.evomaster.client.java.controller.neo4j.operations; + +/** + * Represents a node in a structural MATCH pattern (P_s). + * Contains only the variable name, not labels or properties (those are conditions). + */ +public class PatternNode { + + private final String variableName; + + public PatternNode(String variableName) { + this.variableName = variableName; + } + + public String getVariableName() { + return variableName; + } + + @Override + public String toString() { + return "(" + (variableName != null ? variableName : "") + ")"; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PatternNode that = (PatternNode) o; + if (variableName == null) return that.variableName == null; + return variableName.equals(that.variableName); + } + + @Override + public int hashCode() { + return variableName != null ? variableName.hashCode() : 0; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/operations/QuantifiedPathPattern.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/operations/QuantifiedPathPattern.java new file mode 100644 index 0000000000..309d6aaf57 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/operations/QuantifiedPathPattern.java @@ -0,0 +1,74 @@ +package org.evomaster.client.java.controller.neo4j.operations; + +import org.evomaster.client.java.controller.neo4j.conditions.CypherCondition; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A quantified path pattern (QPP), i.e. a sub-pattern repeated a number of times, + * such as {@code ((a)-[:KNOWS]->(b)){1,3}}. + *
+ * Unlike a variable-length relationship ({@code *1..3}), which repeats a single + * edge, a QPP repeats an entire {@link MatchPattern} (multiple nodes and edges, + * and possibly nested QPPs). The repetition bounds are {@code [min, max]}, where a + * {@code null} max means unbounded ({@code +} or {@code *}). + *
+ * The labels, relationship types, properties and inline {@code WHERE} that constrain the + * sub-pattern are kept in {@link #getConditions()}, scoped to this QPP rather than mixed into + * the outer operation's list — so when the calculator expands the repetition it can clone the + * sub-pattern's conditions per hop. + */ +public class QuantifiedPathPattern { + + private final MatchPattern subPattern; + private final List conditions; + private final int min; + private final Integer max; + + public QuantifiedPathPattern(MatchPattern subPattern, int min, Integer max) { + this(subPattern, Collections.emptyList(), min, max); + } + + public QuantifiedPathPattern(MatchPattern subPattern, List conditions, int min, Integer max) { + this.subPattern = subPattern; + this.conditions = conditions != null ? new ArrayList<>(conditions) : new ArrayList<>(); + this.min = min; + this.max = max; + } + + public MatchPattern getSubPattern() { + return subPattern; + } + + /** + * Returns the conditions scoped to this sub-pattern (labels, relationship types, properties, + * inline WHERE), in source order. Empty when the sub-pattern is purely structural. + */ + public List getConditions() { + return Collections.unmodifiableList(conditions); + } + + public int getMin() { + return min; + } + + public Integer getMax() { + return max; + } + + public boolean isUnboundedMax() { + return max == null; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("QuantifiedPathPattern{").append(subPattern); + if (!conditions.isEmpty()) { + sb.append(", conditions=").append(conditions); + } + sb.append(" {").append(min).append(",").append(max != null ? max : "").append("}}"); + return sb.toString(); + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/parser/CypherParser.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/parser/CypherParser.java new file mode 100644 index 0000000000..fcbfe5e662 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/parser/CypherParser.java @@ -0,0 +1,22 @@ +package org.evomaster.client.java.controller.neo4j.parser; + +import org.evomaster.client.java.controller.neo4j.operations.MatchOperation; + +/** + * Parses the MATCH clause of a Cypher query into a structural {@link MatchOperation}. + *
+ * Implementations are built on top of the official Neo4j Cypher grammar (ANTLR4), + * so that any query accepted by this parser is guaranteed to be valid for Neo4j. + */ +public interface CypherParser { + + /** + * Parse a Cypher query, extracting its MATCH pattern and conditions. + * + * @param query the Cypher query string + * @return the structural pattern and conditions of the MATCH clause + * @throws CypherParserException if the query is not syntactically valid Cypher, + * or does not contain a MATCH clause + */ + MatchOperation parse(String query) throws CypherParserException; +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/parser/CypherParserException.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/parser/CypherParserException.java new file mode 100644 index 0000000000..8848565582 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/parser/CypherParserException.java @@ -0,0 +1,20 @@ +package org.evomaster.client.java.controller.neo4j.parser; + +/** + * Thrown when a Cypher query cannot be parsed: either it is not syntactically + * valid Cypher, or it does not contain the expected MATCH clause. + */ +public class CypherParserException extends Exception { + + public CypherParserException(String message) { + super(message); + } + + public CypherParserException(String message, Throwable cause) { + super(message, cause); + } + + public CypherParserException(Throwable cause) { + super(cause); + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/parser/CypherParserFactory.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/parser/CypherParserFactory.java new file mode 100644 index 0000000000..a487a4c8dc --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/parser/CypherParserFactory.java @@ -0,0 +1,16 @@ +package org.evomaster.client.java.controller.neo4j.parser; + +import org.evomaster.client.java.controller.neo4j.parser.cypher25.Cypher25AntlrParser; + +/** + * Builds the default {@link CypherParser} implementation. + */ +public class CypherParserFactory { + + private CypherParserFactory() { + } + + public static CypherParser buildParser() { + return new Cypher25AntlrParser(); + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/parser/cypher25/Cypher25AntlrParser.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/parser/cypher25/Cypher25AntlrParser.java new file mode 100644 index 0000000000..d1c0884d4f --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/parser/cypher25/Cypher25AntlrParser.java @@ -0,0 +1,61 @@ +package org.evomaster.client.java.controller.neo4j.parser.cypher25; + +import org.antlr.v4.runtime.*; +import org.antlr.v4.runtime.misc.ParseCancellationException; +import org.evomaster.client.java.controller.neo4j.cypher25.Cypher25Lexer; +import org.evomaster.client.java.controller.neo4j.cypher25.Cypher25Parser; +import org.evomaster.client.java.controller.neo4j.parser.CypherParser; +import org.evomaster.client.java.controller.neo4j.parser.CypherParserException; +import org.evomaster.client.java.controller.neo4j.operations.MatchOperation; + +/** + * {@link CypherParser} implementation built on the official Neo4j {@code Cypher25} + * ANTLR grammar. Because the grammar is the one Neo4j itself uses, any query this + * parser accepts is guaranteed to be valid Cypher. + *
+ * Syntax errors are not swallowed: the lexer and parser are wired with a listener + * that fails fast, and the error (with line/column) is surfaced as a + * {@link CypherParserException}. + */ +public class Cypher25AntlrParser implements CypherParser { + + private static final ThrowingErrorListener THROWING_LISTENER = new ThrowingErrorListener(); + + @Override + public MatchOperation parse(String query) throws CypherParserException { + if (query == null || query.trim().isEmpty()) { + throw new CypherParserException("Query is null or empty"); + } + + try { + Cypher25Lexer lexer = new Cypher25Lexer(CharStreams.fromString(query)); + lexer.removeErrorListeners(); + lexer.addErrorListener(THROWING_LISTENER); + + Cypher25Parser parser = new Cypher25Parser(new CommonTokenStream(lexer)); + parser.removeErrorListeners(); + parser.addErrorListener(THROWING_LISTENER); + + Cypher25Parser.StatementsContext tree = parser.statements(); + + Cypher25MatchVisitor visitor = new Cypher25MatchVisitor(); + visitor.visit(tree); + + if (!visitor.foundMatch()) { + throw new CypherParserException("Query does not contain a MATCH clause"); + } + return visitor.toOperation(); + + } catch (ParseCancellationException e) { + throw new CypherParserException(e.getMessage(), e); + } + } + + private static final class ThrowingErrorListener extends BaseErrorListener { + @Override + public void syntaxError(Recognizer recognizer, Object offendingSymbol, + int line, int charPositionInLine, String msg, RecognitionException e) { + throw new ParseCancellationException("line " + line + ":" + charPositionInLine + " " + msg); + } + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/parser/cypher25/Cypher25MatchVisitor.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/parser/cypher25/Cypher25MatchVisitor.java new file mode 100644 index 0000000000..562ef3cd3f --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/parser/cypher25/Cypher25MatchVisitor.java @@ -0,0 +1,834 @@ +package org.evomaster.client.java.controller.neo4j.parser.cypher25; + +import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.TerminalNode; +import org.evomaster.client.java.controller.neo4j.cypher25.Cypher25Parser; +import org.evomaster.client.java.controller.neo4j.cypher25.Cypher25ParserBaseVisitor; +import org.evomaster.client.java.controller.neo4j.conditions.*; +import org.evomaster.client.java.controller.neo4j.operations.*; + +import java.util.*; +import java.util.function.Function; + +/** + * Translates the parse tree produced by the Neo4j {@code Cypher25Parser} grammar + * into the internal {@link MatchOperation} model. + *
+ * Only the rules relevant to MATCH pattern matching are overridden; every other + * rule falls through to the default {@code visitChildren}, so the visitor stays + * compact while the underlying grammar remains the full Cypher language. + *
+ * Scope: node/relationship structure, label and relationship-type expressions, inline + * node/relationship properties, variable-length paths ({@code *1..3}), quantified path + * patterns ({@code ((a)-[]->(b)){1,3}}, possibly nested), path assignment, and the WHERE + * clause. Label/type expressions and the WHERE clause are both parsed into a faithful + * boolean tree ({@link AndCondition} / {@link OrCondition} / {@link XorCondition} / + * {@link NotCondition}, with parentheses) mirroring the grammar's precedence: {@code (n:A&B|C)} + * becomes {@code OrCondition[AndCondition[A,B], C]} and {@code [:A|B]} an OR of types, while + * comparison leaves carry typed {@link Operand}s so both sides of {@code n.age > m.age} stay + * property references. Predicates not decomposed + * structurally (functions, bare booleans) are kept unchanged as {@link RawCondition}. + */ +class Cypher25MatchVisitor extends Cypher25ParserBaseVisitor { + + private final PatternAcc root = new PatternAcc(); + private final List conditions = new ArrayList<>(); + /** Where conditions are collected right now; the root list by default, a QPP's own list inside one. */ + private List conditionSink = conditions; + + private final List pathVariables = new ArrayList<>(); + private boolean optional; + private boolean foundMatch; + private int anonNodeCounter; + private int anonRelCounter; + + boolean foundMatch() { + return foundMatch; + } + + MatchOperation toOperation() { + return new MatchOperation(root.toPattern(), conditions, pathVariables, optional); + } + + @Override + public Void visitMatchClause(Cypher25Parser.MatchClauseContext ctx) { + foundMatch = true; + // matchClause : OPTIONAL? MATCH ... — one OPTIONAL clause makes the whole (merged) operation optional. + if (ctx.OPTIONAL() != null) { + optional = true; + } + + for (Cypher25Parser.PatternContext pattern : ctx.patternList().pattern()) { + processPattern(pattern, root); + } + + if (ctx.whereClause() != null) { + processWhere(ctx.whereClause().expression()); + } + + // The clause is fully handled here; do not recurse into it again. + return null; + } + + private void processPattern(Cypher25Parser.PatternContext ctx, PatternAcc acc) { + // pattern : (variable EQ)? selector? anonymousPattern + if (ctx.variable() != null) { + pathVariables.add(name(ctx.variable())); + } + + Cypher25Parser.AnonymousPatternContext anon = ctx.anonymousPattern(); + if (anon.patternElement() != null) { + processPatternElement(anon.patternElement(), acc); + } else if (anon.shortestPathPattern() != null) { + processPatternElement(anon.shortestPathPattern().patternElement(), acc); + } + } + + private void processPatternElement(Cypher25Parser.PatternElementContext ctx, PatternAcc acc) { + // Walk children in source order so nodes and relationships pair up correctly: + // (n0) -rel0- (n1) -rel1- (n2) ... + String previousNode = null; + RelInfo pendingRel = null; + + for (ParseTree child : children(ctx)) { + if (child instanceof Cypher25Parser.NodePatternContext) { + String current = processNode((Cypher25Parser.NodePatternContext) child, acc); + if (pendingRel != null && previousNode != null) { + addEdge(pendingRel, previousNode, current, acc); + pendingRel = null; + } + previousNode = current; + } else if (child instanceof Cypher25Parser.RelationshipPatternContext) { + pendingRel = processRelationship((Cypher25Parser.RelationshipPatternContext) child); + } else if (child instanceof Cypher25Parser.QuantifierContext && pendingRel != null) { + applyQuantifier(pendingRel, (Cypher25Parser.QuantifierContext) child); + } else if (child instanceof Cypher25Parser.ParenthesizedPathContext) { + acc.quantifiedPaths.add(processParenthesizedPath((Cypher25Parser.ParenthesizedPathContext) child)); + } + } + } + + private QuantifiedPathPattern processParenthesizedPath(Cypher25Parser.ParenthesizedPathContext ctx) { + // parenthesizedPath : LPAREN pattern (WHERE expression)? RPAREN quantifier? + PatternAcc sub = new PatternAcc(); + // Scope the sub-pattern's labels/types/properties and inline WHERE to this QPP, not the outer + // list — restore the previous sink afterwards so a nested QPP nests its conditions correctly. + List subConditions = new ArrayList<>(); + List previousSink = conditionSink; + conditionSink = subConditions; + try { + processPattern(ctx.pattern(), sub); + if (ctx.expression() != null) { + processWhere(ctx.expression()); // inline WHERE filtering the sub-path + } + } finally { + conditionSink = previousSink; + } + + // A parenthesized path without a quantifier is only a grouping; treat as {1,1}. + Bounds bounds = ctx.quantifier() != null + ? quantifierBounds(ctx.quantifier()) + : new Bounds(1, 1); + + return new QuantifiedPathPattern(sub.toPattern(), subConditions, bounds.min, bounds.max); + } + + private String processNode(Cypher25Parser.NodePatternContext ctx, PatternAcc acc) { + // nodePattern : LPAREN variable? labelExpression? properties? (WHERE expression)? RPAREN + String variable = ctx.variable() != null + ? name(ctx.variable()) + : "_anon_node_" + (anonNodeCounter++); + + acc.nodes.putIfAbsent(variable, new PatternNode(variable)); + + if (ctx.labelExpression() != null) { + addLabelConditions(variable, ctx.labelExpression()); + } + if (ctx.properties() != null) { + for (Map.Entry p : properties(ctx.properties()).entrySet()) { + conditionSink.add(new PropertyCondition(variable, p.getKey(), p.getValue())); + } + } + if (ctx.expression() != null) { + processWhere(ctx.expression()); // inline predicate: (n WHERE ...) + } + return variable; + } + + private RelInfo processRelationship(Cypher25Parser.RelationshipPatternContext ctx) { + // relationshipPattern : leftArrow? arrowLine (LBRACKET variable? labelExpression? + // pathLength? properties? (WHERE expression)? RBRACKET)? arrowLine rightArrow? + RelInfo rel = new RelInfo(); + rel.leftArrow = ctx.leftArrow() != null; + rel.directed = rel.leftArrow || ctx.rightArrow() != null; + rel.variable = ctx.variable() != null + ? name(ctx.variable()) + : "_anon_rel_" + (anonRelCounter++); + + if (ctx.labelExpression() != null) { + // Relationship types use the same grammar rule as node labels, so the same + // faithful boolean tree applies: [:A|B] is an OR of types, [:!A] a negation. + String relVar = rel.variable; + rel.typeCondition = labelTree(ctx.labelExpression(), relVar, type -> new TypeCondition(relVar, type)); + } + if (ctx.pathLength() != null) { + applyPathLength(rel, ctx.pathLength()); + } + if (ctx.properties() != null) { + rel.properties.putAll(properties(ctx.properties())); + } + if (ctx.expression() != null) { + processWhere(ctx.expression()); // inline predicate: -[r WHERE ...]-> + } + return rel; + } + + private void addEdge(RelInfo rel, String left, String right, PatternAcc acc) { + // A left arrow (<-[]-) reverses the logical source/target. + String source = rel.leftArrow ? right : left; + String target = rel.leftArrow ? left : right; + + acc.edges.add(new PatternEdge(rel.variable, source, target, rel.directed, + rel.variableLength, rel.minLength, rel.maxLength)); + + if (rel.typeCondition != null) { + conditionSink.add(rel.typeCondition); + } + for (Map.Entry p : rel.properties.entrySet()) { + conditionSink.add(new PropertyCondition(rel.variable, p.getKey(), p.getValue())); + } + } + + private void addLabelConditions(String variable, Cypher25Parser.LabelExpressionContext ctx) { + CypherCondition tree = labelTree(ctx, variable, label -> new LabelCondition(variable, label)); + if (tree != null) { + conditionSink.add(tree); + } + } + + /** + * Builds the boolean tree of a label/type expression following the grammar's precedence: + * level 4 is OR, 3 is AND ({@code &} or {@code :}), 2 is NOT ({@code !}), 1 is an atom (a + * name, the {@code %} wildcard, or a parenthesised sub-expression). The two surface forms, + * {@code :A&B} (COLON) and {@code IS A&B}, have identical structure but distinct generated + * contexts, so each is walked by its own typed traversal; both share the {@link #or}, + * {@link #and} and {@link #negate} combinators and the {@code leaf} factory, which makes the + * leaf for a bare name ({@link LabelCondition} for nodes, {@link TypeCondition} for + * relationships). Returns null for dynamic-label forms. + */ + private CypherCondition labelTree(Cypher25Parser.LabelExpressionContext ctx, + String variable, Function leaf) { + if (ctx.labelExpression4() != null) { + return buildLabelOr(ctx.labelExpression4(), variable, leaf); + } + if (ctx.labelExpression4Is() != null) { + return buildLabelOrIs(ctx.labelExpression4Is(), variable, leaf); + } + return null; + } + + private CypherCondition or(List terms) { + return terms.size() == 1 ? terms.get(0) : new OrCondition(terms); + } + + private CypherCondition and(List terms) { + return terms.size() == 1 ? terms.get(0) : new AndCondition(terms); + } + + private CypherCondition negate(int notCount, CypherCondition inner) { + // An odd number of '!' negates; an even number cancels out. + return notCount % 2 == 1 ? new NotCondition(inner) : inner; + } + + private CypherCondition buildLabelOr(Cypher25Parser.LabelExpression4Context ctx, + String variable, Function leaf) { + List terms = new ArrayList<>(); + for (Cypher25Parser.LabelExpression3Context p : ctx.labelExpression3()) { + terms.add(buildLabelAnd(p, variable, leaf)); + } + return or(terms); + } + + private CypherCondition buildLabelAnd(Cypher25Parser.LabelExpression3Context ctx, + String variable, Function leaf) { + List terms = new ArrayList<>(); + for (Cypher25Parser.LabelExpression2Context p : ctx.labelExpression2()) { + terms.add(buildLabelNot(p, variable, leaf)); + } + return and(terms); + } + + private CypherCondition buildLabelNot(Cypher25Parser.LabelExpression2Context ctx, + String variable, Function leaf) { + return negate(ctx.EXCLAMATION_MARK().size(), buildLabelAtom(ctx.labelExpression1(), variable, leaf)); + } + + private CypherCondition buildLabelAtom(Cypher25Parser.LabelExpression1Context ctx, + String variable, Function leaf) { + if (ctx instanceof Cypher25Parser.ParenthesizedLabelExpressionContext) { + return buildLabelOr( + ((Cypher25Parser.ParenthesizedLabelExpressionContext) ctx).labelExpression4(), variable, leaf); + } + if (ctx instanceof Cypher25Parser.AnyLabelContext) { + return new AnyLabelCondition(variable); + } + if (ctx instanceof Cypher25Parser.LabelNameContext) { + return leaf.apply(name(((Cypher25Parser.LabelNameContext) ctx).symbolicNameString())); + } + return new RawCondition(ctx.getText()); + } + + private CypherCondition buildLabelOrIs(Cypher25Parser.LabelExpression4IsContext ctx, + String variable, Function leaf) { + List terms = new ArrayList<>(); + for (Cypher25Parser.LabelExpression3IsContext p : ctx.labelExpression3Is()) { + terms.add(buildLabelAndIs(p, variable, leaf)); + } + return or(terms); + } + + private CypherCondition buildLabelAndIs(Cypher25Parser.LabelExpression3IsContext ctx, + String variable, Function leaf) { + List terms = new ArrayList<>(); + for (Cypher25Parser.LabelExpression2IsContext p : ctx.labelExpression2Is()) { + terms.add(buildLabelNotIs(p, variable, leaf)); + } + return and(terms); + } + + private CypherCondition buildLabelNotIs(Cypher25Parser.LabelExpression2IsContext ctx, + String variable, Function leaf) { + return negate(ctx.EXCLAMATION_MARK().size(), buildLabelAtomIs(ctx.labelExpression1Is(), variable, leaf)); + } + + private CypherCondition buildLabelAtomIs(Cypher25Parser.LabelExpression1IsContext ctx, + String variable, Function leaf) { + if (ctx instanceof Cypher25Parser.ParenthesizedLabelExpressionIsContext) { + return buildLabelOrIs( + ((Cypher25Parser.ParenthesizedLabelExpressionIsContext) ctx).labelExpression4Is(), variable, leaf); + } + if (ctx instanceof Cypher25Parser.AnyLabelIsContext) { + return new AnyLabelCondition(variable); + } + if (ctx instanceof Cypher25Parser.LabelNameIsContext) { + return leaf.apply(name(((Cypher25Parser.LabelNameIsContext) ctx).symbolicLabelNameString())); + } + return new RawCondition(ctx.getText()); + } + + private void applyPathLength(RelInfo rel, Cypher25Parser.PathLengthContext ctx) { + rel.variableLength = true; + if (ctx.single != null) { + rel.minLength = parseInt(ctx.single.getText()); + rel.maxLength = rel.minLength; + } else { + if (ctx.from != null) rel.minLength = parseInt(ctx.from.getText()); + if (ctx.to != null) rel.maxLength = parseInt(ctx.to.getText()); + } + } + + private void applyQuantifier(RelInfo rel, Cypher25Parser.QuantifierContext ctx) { + Bounds bounds = quantifierBounds(ctx); + rel.variableLength = true; + rel.minLength = bounds.min; + rel.maxLength = bounds.max; + } + + private Bounds quantifierBounds(Cypher25Parser.QuantifierContext ctx) { + if (ctx.PLUS() != null) { + return new Bounds(1, null); + } + if (ctx.TIMES() != null) { + return new Bounds(0, null); + } + if (ctx.COMMA() != null) { + int min = ctx.from != null ? parseInt(ctx.from.getText()) : 0; + Integer max = ctx.to != null ? parseInt(ctx.to.getText()) : null; + return new Bounds(min, max); + } + int exact = parseInt(ctx.UNSIGNED_DECIMAL_INTEGER(0).getText()); + return new Bounds(exact, exact); + } + + /** + * Builds the full boolean tree of the WHERE expression and adds it as a single + * condition. The connective structure (OR / XOR / AND / NOT, with parentheses) + * is preserved faithfully; leaves are comparisons. Predicates that are not decomposed + * structurally (functions, bare booleans) are kept unchanged as {@link RawCondition} + * rather than dropped, so no part of the query is lost. + */ + private void processWhere(Cypher25Parser.ExpressionContext ctx) { + CypherCondition where = buildExpression(ctx); + if (where != null) { + conditionSink.add(where); + } + } + + // expression : expression11 (OR expression11)* + private CypherCondition buildExpression(Cypher25Parser.ExpressionContext ctx) { + List parts = ctx.expression11(); + if (parts.size() == 1) { + return buildXor(parts.get(0)); + } + List terms = new ArrayList<>(); + for (Cypher25Parser.Expression11Context p : parts) { + terms.add(buildXor(p)); + } + return new OrCondition(terms); + } + + // expression11 : expression10 (XOR expression10)* + private CypherCondition buildXor(Cypher25Parser.Expression11Context ctx) { + List parts = ctx.expression10(); + if (parts.size() == 1) { + return buildAnd(parts.get(0)); + } + List terms = new ArrayList<>(); + for (Cypher25Parser.Expression10Context p : parts) { + terms.add(buildAnd(p)); + } + return new XorCondition(terms); + } + + // expression10 : expression9 (AND expression9)* + private CypherCondition buildAnd(Cypher25Parser.Expression10Context ctx) { + List parts = ctx.expression9(); + if (parts.size() == 1) { + return buildNot(parts.get(0)); + } + List terms = new ArrayList<>(); + for (Cypher25Parser.Expression9Context p : parts) { + terms.add(buildNot(p)); + } + return new AndCondition(terms); + } + + // expression9 : NOT* expression8 + private CypherCondition buildNot(Cypher25Parser.Expression9Context ctx) { + CypherCondition inner = buildComparison(ctx.expression8()); + // An odd number of NOTs negates; an even number cancels out. + return ctx.NOT().size() % 2 == 1 ? new NotCondition(inner) : inner; + } + + // expression8 : expression7 ((EQ | NEQ | LE | GE | LT | GT) expression7)* + private CypherCondition buildComparison(Cypher25Parser.Expression8Context ctx) { + List operands = ctx.expression7(); + if (operands.size() == 1) { + return buildExpression7(operands.get(0)); + } + // A chain a op1 b op2 c expands to (a op1 b) AND (b op2 c), as Cypher chains comparisons. + // Operators are read in source order so a mixed chain (a < b <= c) keeps each one. + List operators = comparisonOperators(ctx); + List links = new ArrayList<>(); + for (int i = 0; i < operators.size(); i++) { + links.add(new ComparisonCondition( + operand(operands.get(i)), operators.get(i), operand(operands.get(i + 1)))); + } + return links.size() == 1 ? links.get(0) : new AndCondition(links); + } + + /** The comparison operators of an expression8 chain, in source order. */ + private List comparisonOperators(Cypher25Parser.Expression8Context ctx) { + List operators = new ArrayList<>(); + for (ParseTree child : children(ctx)) { + if (child instanceof TerminalNode) { + ComparisonOperator op = comparisonOperator(((TerminalNode) child).getSymbol().getType()); + if (op != null) { + operators.add(op); + } + } + } + return operators; + } + + // expression7 : expression6 comparisonExpression6? + private CypherCondition buildExpression7(Cypher25Parser.Expression7Context ctx) { + if (ctx.comparisonExpression6() != null) { + CypherCondition c = stringOrNullComparison(ctx); + if (c != null) { + return c; + } + } + // A parenthesized boolean group, e.g. (a OR b): recurse into the inner expression. + Cypher25Parser.ExpressionContext nested = enclosedExpression(ctx); + if (nested != null) { + return buildExpression(nested); + } + // A predicate that is not decomposed structurally (function, bare boolean). + return new RawCondition(ctx.getText()); + } + + private CypherCondition stringOrNullComparison(Cypher25Parser.Expression7Context ctx) { + Operand left = operand(ctx.expression6()); + Cypher25Parser.ComparisonExpression6Context cmp = ctx.comparisonExpression6(); + + if (cmp instanceof Cypher25Parser.StringAndListComparisonContext) { + Cypher25Parser.StringAndListComparisonContext s = + (Cypher25Parser.StringAndListComparisonContext) cmp; + ComparisonOperator op = null; + if (s.STARTS() != null) op = ComparisonOperator.STARTS_WITH; + else if (s.ENDS() != null) op = ComparisonOperator.ENDS_WITH; + else if (s.CONTAINS() != null) op = ComparisonOperator.CONTAINS; + else if (s.IN() != null) op = ComparisonOperator.IN; + if (op != null) { + return new ComparisonCondition(left, op, operand(s.expression6())); + } + } else if (cmp instanceof Cypher25Parser.NullComparisonContext) { + Cypher25Parser.NullComparisonContext n = (Cypher25Parser.NullComparisonContext) cmp; + ComparisonOperator op = n.NOT() != null + ? ComparisonOperator.IS_NOT_NULL + : ComparisonOperator.IS_NULL; + return new ComparisonCondition(left, op, (Operand) null); + } + return null; // TYPE / NORMALIZED comparison: not modelled + } + + /** + * Classifies one side of a comparison: a property reference {@code variable.key} as a + * {@link PropertyOperand}, a list {@code [..]} as a {@link ListOperand} of element operands, + * an operand that is exactly a literal as a {@link LiteralOperand}, an arithmetic expression + * ({@code + - * / % ^}, unary minus) as an {@link ArithmeticOperand} (folded to a literal when + * every leaf is a numeric literal), and anything else (a function call) as a {@link RawOperand}. + */ + private Operand operand(ParseTree expression) { + PropertyOperand property = propertyReference(expression); + if (property != null) { + return property; + } + Cypher25Parser.ListLiteralContext list = soleList(expression); + if (list != null) { + List elements = new ArrayList<>(); + for (Cypher25Parser.ExpressionContext element : list.expression()) { + elements.add(operand(element)); + } + return new ListOperand(elements); + } + Cypher25Parser.LiteralContext literal = soleLiteral(expression); + if (literal != null) { + return new LiteralOperand(convertLiteral(literal)); + } + Operand arithmetic = arithmetic(expression); + if (arithmetic != null) { + return arithmetic; + } + return new RawOperand(expression.getText()); + } + + /** + * Builds an {@link ArithmeticOperand} tree for an operand that is an arithmetic expression, + * or {@code null} when the operand is not arithmetic (so the caller keeps it as a + * {@link RawOperand}). Sub-operands go back through {@link #operand}, so a nested {@code p.age} + * stays a {@link PropertyOperand}; an all-numeric-literal subtree folds to a {@link LiteralOperand}. + */ + private Operand arithmetic(ParseTree expression) { + return buildArithmetic(descendSingleChild(expression)); + } + + private Operand buildArithmetic(ParseTree node) { + if (node instanceof Cypher25Parser.ParenthesizedExpressionContext) { + return operand(((Cypher25Parser.ParenthesizedExpressionContext) node).expression()); + } + if (node instanceof Cypher25Parser.Expression6Context // + - || (left-assoc) + || node instanceof Cypher25Parser.Expression5Context) { // * / % (left-assoc) + return buildLeftAssoc(node); + } + if (node instanceof Cypher25Parser.Expression4Context) { // ^ (right-assoc) + return buildPower((Cypher25Parser.Expression4Context) node); + } + if (node instanceof Cypher25Parser.Expression3Context) { // unary + / - + return buildUnary((Cypher25Parser.Expression3Context) node); + } + return null; + } + + /** Folds {@code operand (OP operand)*} left to right; null if any operator is non-arithmetic (e.g. {@code ||}). */ + private Operand buildLeftAssoc(ParseTree node) { + Operand acc = null; + ArithmeticOperator pending = null; + for (ParseTree child : children(node)) { + if (child instanceof TerminalNode) { + pending = arithmeticOperator(((TerminalNode) child).getSymbol().getType()); + if (pending == null) { + return null; + } + } else { + Operand current = operand(child); + acc = acc == null ? current : arithmeticNode(pending, acc, current); + } + } + return acc; + } + + /** {@code expression3 (POW expression3)*} folded right-associatively, as exponentiation associates. */ + private Operand buildPower(Cypher25Parser.Expression4Context node) { + List parts = node.expression3(); + Operand acc = operand(parts.get(parts.size() - 1)); + for (int i = parts.size() - 2; i >= 0; i--) { + acc = arithmeticNode(ArithmeticOperator.POWER, operand(parts.get(i)), acc); + } + return acc; + } + + /** {@code (PLUS | MINUS) expression2}: unary minus negates, unary plus is identity. */ + private Operand buildUnary(Cypher25Parser.Expression3Context node) { + Operand inner = operand(node.expression2()); + return node.MINUS() != null ? arithmeticNode(ArithmeticOperator.NEGATE, inner, null) : inner; + } + + /** A folded literal when the operands evaluate to one, else a structural {@link ArithmeticOperand}. */ + private Operand arithmeticNode(ArithmeticOperator op, Operand left, Operand right) { + Operand folded = fold(op, left, right); + return folded != null ? folded : new ArithmeticOperand(op, left, right); + } + + /** Evaluates an arithmetic node when every operand is a numeric literal; otherwise null. */ + private Operand fold(ArithmeticOperator op, Operand left, Operand right) { + Object l = numericLiteral(left); + if (l == null) { + return null; + } + if (op == ArithmeticOperator.NEGATE) { + return l instanceof Long + ? new LiteralOperand(-(Long) l) + : new LiteralOperand(-(Double) l); + } + Object r = numericLiteral(right); + if (r == null) { + return null; + } + return l instanceof Long && r instanceof Long + ? foldLong(op, (Long) l, (Long) r) + : foldDouble(op, ((Number) l).doubleValue(), ((Number) r).doubleValue()); + } + + /** Integer arithmetic; leaves the node unfolded (null) on overflow or division by zero. */ + private Operand foldLong(ArithmeticOperator op, long a, long b) { + try { + switch (op) { + case PLUS: return new LiteralOperand(Math.addExact(a, b)); + case MINUS: return new LiteralOperand(Math.subtractExact(a, b)); + case TIMES: return new LiteralOperand(Math.multiplyExact(a, b)); + case DIVIDE: return b == 0 ? null : new LiteralOperand(a / b); + case MODULO: return b == 0 ? null : new LiteralOperand(a % b); + case POWER: return new LiteralOperand(Math.pow(a, b)); // Neo4j's ^ yields a float + default: return null; + } + } catch (ArithmeticException overflow) { + return null; + } + } + + /** Floating-point arithmetic; leaves the node unfolded (null) on division by zero. */ + private Operand foldDouble(ArithmeticOperator op, double a, double b) { + switch (op) { + case PLUS: return new LiteralOperand(a + b); + case MINUS: return new LiteralOperand(a - b); + case TIMES: return new LiteralOperand(a * b); + case DIVIDE: return b == 0 ? null : new LiteralOperand(a / b); + case MODULO: return b == 0 ? null : new LiteralOperand(a % b); + case POWER: return new LiteralOperand(Math.pow(a, b)); + default: return null; + } + } + + private Object numericLiteral(Operand operand) { + if (operand instanceof LiteralOperand) { + Object value = ((LiteralOperand) operand).getValue(); + if (value instanceof Long || value instanceof Double) { + return value; + } + } + return null; + } + + private ArithmeticOperator arithmeticOperator(int tokenType) { + if (tokenType == Cypher25Parser.PLUS) return ArithmeticOperator.PLUS; + if (tokenType == Cypher25Parser.MINUS) return ArithmeticOperator.MINUS; + if (tokenType == Cypher25Parser.TIMES) return ArithmeticOperator.TIMES; + if (tokenType == Cypher25Parser.DIVIDE) return ArithmeticOperator.DIVIDE; + if (tokenType == Cypher25Parser.PERCENT) return ArithmeticOperator.MODULO; + if (tokenType == Cypher25Parser.POW) return ArithmeticOperator.POWER; + return null; // DOUBLEBAR (||) etc.: not numeric arithmetic + } + + /** The list literal an operand reduces to when it is nothing but {@code [..]}, else null. */ + private Cypher25Parser.ListLiteralContext soleList(ParseTree expression) { + ParseTree node = descendSingleChild(expression); + return node instanceof Cypher25Parser.ListLiteralContext + ? (Cypher25Parser.ListLiteralContext) node + : null; + } + + /** A property reference {@code variable.key}, or null if the operand is not exactly that. */ + private PropertyOperand propertyReference(ParseTree expression) { + ParseTree node = descendSingleChild(expression); + if (!(node instanceof Cypher25Parser.Expression2Context)) { + return null; + } + Cypher25Parser.Expression2Context e2 = (Cypher25Parser.Expression2Context) node; + List postfixes = e2.postFix(); + if (postfixes.size() != 1 || !(postfixes.get(0) instanceof Cypher25Parser.PropertyPostfixContext)) { + return null; // not a single `.key` access (e.g. indexing, or a longer chain) + } + Cypher25Parser.VariableContext base = e2.expression1().variable(); + if (base == null) { + return null; // the base is not a bare variable (e.g. a function result) + } + Cypher25Parser.PropertyPostfixContext postfix = + (Cypher25Parser.PropertyPostfixContext) postfixes.get(0); + String key = name(postfix.property().propertyKeyName().symbolicNameString()); + return new PropertyOperand(name(base), key); + } + + /** The literal an operand reduces to when it is nothing but a literal, else null. */ + private Cypher25Parser.LiteralContext soleLiteral(ParseTree tree) { + ParseTree t = tree; + while (t != null) { + if (t instanceof Cypher25Parser.LiteralContext) { + return (Cypher25Parser.LiteralContext) t; + } + if (t.getChildCount() != 1) { + return null; // a branch (operator, arguments): not a bare literal + } + t = t.getChild(0); + } + return null; + } + + /** Follows the single-child chain down to the first node that branches (or a leaf). */ + private ParseTree descendSingleChild(ParseTree tree) { + ParseTree t = tree; + while (t.getChildCount() == 1) { + t = t.getChild(0); + } + return t; + } + + /** Descends a single-child chain; returns the inner expression if it is a parenthesized group. */ + private Cypher25Parser.ExpressionContext enclosedExpression(ParseTree tree) { + if (tree instanceof Cypher25Parser.ParenthesizedExpressionContext) { + return ((Cypher25Parser.ParenthesizedExpressionContext) tree).expression(); + } + if (tree.getChildCount() == 1) { + return enclosedExpression(tree.getChild(0)); + } + return null; + } + + private ComparisonOperator comparisonOperator(int tokenType) { + if (tokenType == Cypher25Parser.EQ) return ComparisonOperator.EQUALS; + if (tokenType == Cypher25Parser.NEQ || tokenType == Cypher25Parser.INVALID_NEQ) return ComparisonOperator.NOT_EQUALS; + if (tokenType == Cypher25Parser.LE) return ComparisonOperator.LESS_THAN_OR_EQUALS; + if (tokenType == Cypher25Parser.GE) return ComparisonOperator.GREATER_THAN_OR_EQUALS; + if (tokenType == Cypher25Parser.LT) return ComparisonOperator.LESS_THAN; + if (tokenType == Cypher25Parser.GT) return ComparisonOperator.GREATER_THAN; + return null; + } + + private Map properties(Cypher25Parser.PropertiesContext ctx) { + // properties : map | parameter + Map result = new LinkedHashMap<>(); + Cypher25Parser.MapContext map = ctx.map(); + if (map == null) { + return result; // parameterised properties ($props) carry no literal values + } + List keys = map.propertyKeyName(); + List values = map.expression(); + for (int i = 0; i < keys.size(); i++) { + // A property value is classified exactly like a comparison operand, so it folds + // constants and keeps functions/parameters as a RawOperand instead of guessing. + result.put(name(keys.get(i).symbolicNameString()), operand(values.get(i))); + } + return result; + } + + private Object convertLiteral(Cypher25Parser.LiteralContext literal) { + if (literal instanceof Cypher25Parser.StringsLiteralContext) { + String raw = literal.getText(); + return raw.length() >= 2 ? raw.substring(1, raw.length() - 1) : raw; + } + if (literal instanceof Cypher25Parser.NummericLiteralContext) { + String number = literal.getText(); + try { + return number.contains(".") || number.toLowerCase().contains("e") + ? (Object) Double.parseDouble(number) + : (Object) Long.parseLong(number); + } catch (NumberFormatException e) { + return number; + } + } + if (literal instanceof Cypher25Parser.BooleanLiteralContext) { + return literal.getText().equalsIgnoreCase("true"); + } + if (literal instanceof Cypher25Parser.KeywordLiteralContext) { + return literal.getText().equalsIgnoreCase("null") ? null : literal.getText(); + } + return literal.getText(); + } + + private String name(Cypher25Parser.VariableContext ctx) { + return name(ctx.symbolicNameString()); + } + + private String name(Cypher25Parser.SymbolicNameStringContext ctx) { + return unquote(ctx.getText()); + } + + private String name(Cypher25Parser.SymbolicLabelNameStringContext ctx) { + return unquote(ctx.getText()); + } + + /** Strips the backtick quoting from an escaped identifier, leaving plain ones untouched. */ + private String unquote(String text) { + if (text.length() >= 2 && text.charAt(0) == '`' && text.charAt(text.length() - 1) == '`') { + return text.substring(1, text.length() - 1).replace("``", "`"); + } + return text; + } + + private List children(ParseTree parent) { + List result = new ArrayList<>(parent.getChildCount()); + for (int i = 0; i < parent.getChildCount(); i++) { + result.add(parent.getChild(i)); + } + return result; + } + + private Integer parseInt(String text) { + return Integer.valueOf(text); + } + + /** Accumulates the nodes, edges and quantified paths of one (sub-)pattern. */ + private static final class PatternAcc { + final Map nodes = new LinkedHashMap<>(); + final List edges = new ArrayList<>(); + final List quantifiedPaths = new ArrayList<>(); + + MatchPattern toPattern() { + return new MatchPattern(new ArrayList<>(nodes.values()), edges, quantifiedPaths); + } + } + + /** Repetition bounds [min, max]; a null max means unbounded. */ + private static final class Bounds { + final int min; + final Integer max; + + Bounds(int min, Integer max) { + this.min = min; + this.max = max; + } + } + + /** Mutable accumulator for a relationship while its endpoints are being resolved. */ + private static final class RelInfo { + String variable; + CypherCondition typeCondition; + final Map properties = new LinkedHashMap<>(); + boolean leftArrow; + boolean directed; + boolean variableLength; + Integer minLength; + Integer maxLength; + } +} diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/neo4j/parser/Cypher25AntlrParserTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/neo4j/parser/Cypher25AntlrParserTest.java new file mode 100644 index 0000000000..efd0872e14 --- /dev/null +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/neo4j/parser/Cypher25AntlrParserTest.java @@ -0,0 +1,808 @@ +package org.evomaster.client.java.controller.neo4j.parser; + +import org.evomaster.client.java.controller.neo4j.conditions.*; +import org.evomaster.client.java.controller.neo4j.operations.MatchOperation; +import org.evomaster.client.java.controller.neo4j.operations.PatternEdge; +import org.evomaster.client.java.controller.neo4j.operations.QuantifiedPathPattern; +import org.junit.jupiter.api.Test; +import org.evomaster.client.java.controller.neo4j.parser.cypher25.Cypher25AntlrParser; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for {@link Cypher25AntlrParser}, the parser built on the official Neo4j + * Cypher25 grammar. Invalid input fails with a {@link CypherParserException}, and label/type + * expressions are preserved as a faithful boolean tree ({@link OrCondition}/{@link AndCondition}/ + * {@link NotCondition} over {@link LabelCondition}/{@link TypeCondition} leaves), the same + * structure used for the WHERE clause. + */ +class Cypher25AntlrParserTest { + + private final CypherParser parser = CypherParserFactory.buildParser(); + + private MatchOperation parse(String query) { + try { + return parser.parse(query); + } catch (CypherParserException e) { + throw new AssertionError("Expected a successful parse for: " + query, e); + } + } + + private void assertFails(String query) { + assertThrows(CypherParserException.class, () -> parser.parse(query)); + } + + private long countOf(MatchOperation op, Class type) { + return op.getConditions().stream().filter(type::isInstance).count(); + } + + @Test + void testAllNodes() { + MatchOperation op = parse("MATCH (n) RETURN n"); + assertEquals(1, op.getPattern().nodeCount()); + assertEquals(0, op.getPattern().edgeCount()); + assertEquals(0, op.getConditions().size()); + assertEquals("n", op.getPattern().getNodes().get(0).getVariableName()); + } + + @Test + void testNodeWithLabel() { + MatchOperation op = parse("MATCH (movie:Movie) RETURN movie.title"); + assertEquals(1, op.getPattern().nodeCount()); + assertEquals(1, op.getConditions().size()); + LabelCondition label = (LabelCondition) op.getConditions().get(0); + assertEquals("movie", label.getVariableName()); + assertEquals("Movie", label.getLabel()); + } + + @Test + void testNodeWithMultipleLabelsAnd() { + MatchOperation op = parse("MATCH (e:Person:Employee) RETURN e"); + AndCondition and = (AndCondition) op.getConditions().get(0); + assertEquals(2, and.getConditions().size()); + assertTrue(and.getConditions().stream().allMatch(LabelCondition.class::isInstance)); + } + + @Test + void testNodeWithMultipleLabelsAmpersand() { + MatchOperation op = parse("MATCH (e:Person&Employee) RETURN e"); + AndCondition and = (AndCondition) op.getConditions().get(0); + assertEquals(2, and.getConditions().size()); + assertTrue(and.getConditions().stream().allMatch(LabelCondition.class::isInstance)); + } + + @Test + void testLabelExpressionOr() { + MatchOperation op = parse("MATCH (n:Movie|Person) RETURN n.name"); + OrCondition or = (OrCondition) op.getConditions().get(0); + assertEquals(2, or.getConditions().size()); + assertTrue(or.getConditions().stream().allMatch(LabelCondition.class::isInstance)); + assertEquals("n", ((LabelCondition) or.getConditions().get(0)).getVariableName()); + assertTrue(or.getConditions().stream() + .anyMatch(c -> "Movie".equals(((LabelCondition) c).getLabel()))); + assertTrue(or.getConditions().stream() + .anyMatch(c -> "Person".equals(((LabelCondition) c).getLabel()))); + } + + @Test + void testNodeLabelNegation() { + // (n:!Person): the node must NOT have the label. + MatchOperation op = parse("MATCH (n:!Person) RETURN n"); + NotCondition not = (NotCondition) op.getConditions().get(0); + assertEquals("Person", ((LabelCondition) not.getCondition()).getLabel()); + } + + @Test + void testNodeLabelMixedAndOr() { + // (n:A&B|C) parses as (A AND B) OR C; & binds tighter than |. + MatchOperation op = parse("MATCH (n:A&B|C) RETURN n"); + OrCondition or = (OrCondition) op.getConditions().get(0); + assertEquals(2, or.getConditions().size()); + AndCondition and = (AndCondition) or.getConditions().get(0); + assertEquals(2, and.getConditions().size()); + assertInstanceOf(LabelCondition.class, or.getConditions().get(1)); + } + + @Test + void testNodeLabelParenthesizedRegroup() { + // (n:A&(B|C)) regroups to A AND (B OR C). + MatchOperation op = parse("MATCH (n:A&(B|C)) RETURN n"); + AndCondition and = (AndCondition) op.getConditions().get(0); + assertEquals(2, and.getConditions().size()); + assertInstanceOf(LabelCondition.class, and.getConditions().get(0)); + assertEquals(2, ((OrCondition) and.getConditions().get(1)).getConditions().size()); + } + + @Test + void testNodeAnyLabelWildcard() { + MatchOperation op = parse("MATCH (n:%) RETURN n"); + AnyLabelCondition any = (AnyLabelCondition) op.getConditions().get(0); + assertEquals("n", any.getVariableName()); + } + + @Test + void testNodeIsLabel() { + // The GQL 'IS' form is equivalent to the colon form. + MatchOperation op = parse("MATCH (n IS Person) RETURN n"); + LabelCondition label = (LabelCondition) op.getConditions().get(0); + assertEquals("n", label.getVariableName()); + assertEquals("Person", label.getLabel()); + } + + @Test + void testNodeIsLabelExpression() { + // The 'IS' form supports the same &|! operators as the colon form. + MatchOperation op = parse("MATCH (n IS A&B|C) RETURN n"); + OrCondition or = (OrCondition) op.getConditions().get(0); + assertEquals(2, or.getConditions().size()); + assertEquals(2, ((AndCondition) or.getConditions().get(0)).getConditions().size()); + } + + @Test + void testRelationshipIsType() { + MatchOperation op = parse("MATCH (a)-[r IS KNOWS]->(b) RETURN a, b"); + TypeCondition type = (TypeCondition) op.getConditions().stream() + .filter(TypeCondition.class::isInstance).findFirst() + .orElseThrow(() -> new AssertionError("no TypeCondition for IS relationship type")); + assertEquals("KNOWS", type.getType()); + } + + @Test + void testNodeInlineWhere() { + MatchOperation op = parse("MATCH (n WHERE n.age > 18) RETURN n"); + ComparisonCondition cc = comparison(op); + assertEquals(ComparisonOperator.GREATER_THAN, cc.getOperator()); + assertEquals("age", ((PropertyOperand) cc.getLeft()).getPropertyKey()); + } + + @Test + void testRelationshipInlineWhere() { + MatchOperation op = parse("MATCH (a)-[r WHERE r.weight > 5]->(b) RETURN a"); + ComparisonCondition cc = comparison(op); + assertEquals("weight", ((PropertyOperand) cc.getLeft()).getPropertyKey()); + } + + @Test + void testAnonymousNode() { + MatchOperation op = parse("MATCH (:Person) RETURN count(*)"); + assertEquals(1, op.getPattern().nodeCount()); + assertTrue(op.getPattern().getNodes().get(0).getVariableName().startsWith("_anon_")); + } + + @Test + void testNodeWithProperty() { + MatchOperation op = parse("MATCH (p:Person {name: \"Alice\"}) RETURN p"); + assertEquals(1, countOf(op, PropertyCondition.class)); + PropertyCondition prop = (PropertyCondition) op.getConditions().stream() + .filter(PropertyCondition.class::isInstance).findFirst() + .orElseThrow(() -> new AssertionError("no PropertyCondition found")); + assertEquals("p", prop.getVariableName()); + assertEquals("name", prop.getPropertyKey()); + assertEquals(new LiteralOperand("Alice"), prop.getValue()); + } + + @Test + void testPropertyConstantArithmeticFolded() { + // A property value is an operand too, so a constant expression folds at parse time. + MatchOperation op = parse("MATCH (p {age: 25 + 5}) RETURN p"); + assertTrue(op.getConditions().stream().anyMatch(c -> + c instanceof PropertyCondition && new LiteralOperand(30L).equals(((PropertyCondition) c).getValue()))); + } + + @Test + void testPropertyFunctionValueKeptAsRawOperand() { + // A function-call value is kept whole as a RawOperand, not misread as an inner literal. + PropertyCondition prop = (PropertyCondition) parse("MATCH (p {at: time(\"11:11\")}) RETURN p") + .getConditions().stream().filter(PropertyCondition.class::isInstance).findFirst().orElseThrow(AssertionError::new); + assertInstanceOf(RawOperand.class, prop.getValue()); + assertEquals("time(\"11:11\")", ((RawOperand) prop.getValue()).getText()); + } + + @Test + void testPropertyParameterValueKeptAsRawOperand() { + PropertyCondition prop = (PropertyCondition) parse("MATCH (p {age: $minAge}) RETURN p") + .getConditions().stream().filter(PropertyCondition.class::isInstance).findFirst().orElseThrow(AssertionError::new); + assertInstanceOf(RawOperand.class, prop.getValue()); + } + + @Test + void testNodeWithMultipleProperties() { + MatchOperation op = parse("MATCH (p:Person {name: \"Alice\", age: 30}) RETURN p"); + assertEquals(2, countOf(op, PropertyCondition.class)); + } + + @Test + void testPropertyWithSingleQuotes() { + MatchOperation op = parse("MATCH (p:Person {name: 'Alice'}) RETURN p"); + assertTrue(op.getConditions().stream().anyMatch(c -> + c instanceof PropertyCondition && new LiteralOperand("Alice").equals(((PropertyCondition) c).getValue()))); + } + + @Test + void testNumericPropertyValues() { + MatchOperation op = parse("MATCH (p:Person {age: 30, salary: 50000.50}) RETURN p"); + assertTrue(op.getConditions().stream().anyMatch(c -> + c instanceof PropertyCondition && new LiteralOperand(30L).equals(((PropertyCondition) c).getValue()))); + assertTrue(op.getConditions().stream().anyMatch(c -> + c instanceof PropertyCondition && new LiteralOperand(50000.50).equals(((PropertyCondition) c).getValue()))); + } + + @Test + void testBooleanPropertyValues() { + MatchOperation op = parse("MATCH (p:Person {active: true, deleted: false}) RETURN p"); + assertTrue(op.getConditions().stream().anyMatch(c -> + c instanceof PropertyCondition && new LiteralOperand(true).equals(((PropertyCondition) c).getValue()))); + assertTrue(op.getConditions().stream().anyMatch(c -> + c instanceof PropertyCondition && new LiteralOperand(false).equals(((PropertyCondition) c).getValue()))); + } + + @Test + void testSimpleRelationshipWithBrackets() { + MatchOperation op = parse("MATCH (a)-[r]->(b) RETURN a, b"); + assertEquals(2, op.getPattern().nodeCount()); + assertEquals(1, op.getPattern().edgeCount()); + PatternEdge edge = op.getPattern().getEdges().get(0); + assertEquals("a", edge.getSourceVariable()); + assertEquals("b", edge.getTargetVariable()); + assertTrue(edge.isDirected()); + } + + @Test + void testEmptyRelationshipDirected() { + MatchOperation op = parse("MATCH (a)-->(b) RETURN a, b"); + assertEquals(1, op.getPattern().edgeCount()); + assertTrue(op.getPattern().getEdges().get(0).isDirected()); + } + + @Test + void testEmptyRelationshipUndirected() { + MatchOperation op = parse("MATCH (a)--(b) RETURN a, b"); + assertFalse(op.getPattern().getEdges().get(0).isDirected()); + } + + @Test + void testLeftArrowRelationship() { + MatchOperation op = parse("MATCH (a)<-[r]-(b) RETURN a, b"); + PatternEdge edge = op.getPattern().getEdges().get(0); + assertEquals("b", edge.getSourceVariable()); + assertEquals("a", edge.getTargetVariable()); + assertTrue(edge.isDirected()); + } + + @Test + void testRelationshipWithType() { + MatchOperation op = parse("MATCH (a)-[r:KNOWS]->(b) RETURN a, b"); + assertTrue(op.getConditions().stream().anyMatch(c -> + c instanceof TypeCondition && "KNOWS".equals(((TypeCondition) c).getType()))); + } + + @Test + void testMultipleRelationshipTypes() { + // [:ACTED_IN|DIRECTED] is an OR over the two types, not an (impossible) AND. + MatchOperation op = parse("MATCH (:Movie)<-[:ACTED_IN|DIRECTED]-(person:Person) RETURN person"); + assertEquals(2, op.getPattern().nodeCount()); + assertEquals(1, op.getPattern().edgeCount()); + OrCondition or = (OrCondition) op.getConditions().stream() + .filter(OrCondition.class::isInstance).findFirst() + .orElseThrow(() -> new AssertionError("no OrCondition for relationship types")); + assertEquals(2, or.getConditions().size()); + assertTrue(or.getConditions().stream().allMatch(TypeCondition.class::isInstance)); + } + + @Test + void testRelationshipTypeNegation() { + MatchOperation op = parse("MATCH (a)-[r:!KNOWS]->(b) RETURN a, b"); + NotCondition not = (NotCondition) op.getConditions().stream() + .filter(NotCondition.class::isInstance).findFirst() + .orElseThrow(() -> new AssertionError("no NotCondition for relationship type")); + assertEquals("KNOWS", ((TypeCondition) not.getCondition()).getType()); + } + + @Test + void testRelationshipWithProperties() { + MatchOperation op = parse("MATCH (a)-[r:ACTED_IN {role: 'Harry Potter'}]-(b) RETURN a, b"); + assertTrue(op.getConditions().stream().anyMatch(c -> + c instanceof PropertyCondition && new LiteralOperand("Harry Potter").equals(((PropertyCondition) c).getValue()))); + } + + @Test + void testVariableLengthAny() { + PatternEdge edge = parse("MATCH (a)-[*]->(b) RETURN a, b").getPattern().getEdges().get(0); + assertTrue(edge.isVariableLength()); + } + + @Test + void testVariableLengthRange() { + PatternEdge edge = parse("MATCH (a)-[*1..3]->(b) RETURN a, b").getPattern().getEdges().get(0); + assertTrue(edge.isVariableLength()); + assertEquals(1, edge.getMinLength().intValue()); + assertEquals(3, edge.getMaxLength().intValue()); + } + + @Test + void testVariableLengthMinOnly() { + PatternEdge edge = parse("MATCH (a)-[*2..]->(b) RETURN a, b").getPattern().getEdges().get(0); + assertEquals(2, edge.getMinLength().intValue()); + assertNull(edge.getMaxLength()); + } + + @Test + void testVariableLengthMaxOnly() { + PatternEdge edge = parse("MATCH (a)-[*..5]->(b) RETURN a, b").getPattern().getEdges().get(0); + assertNull(edge.getMinLength()); + assertEquals(5, edge.getMaxLength().intValue()); + } + + @Test + void testVariableLengthExact() { + PatternEdge edge = parse("MATCH (a)-[*3]->(b) RETURN a, b").getPattern().getEdges().get(0); + assertEquals(3, edge.getMinLength().intValue()); + assertEquals(3, edge.getMaxLength().intValue()); + } + + @Test + void testVariableLengthWithType() { + MatchOperation op = parse("MATCH (a)-[:KNOWS*1..3]->(b) RETURN a, b"); + PatternEdge edge = op.getPattern().getEdges().get(0); + assertEquals(1, edge.getMinLength().intValue()); + assertEquals(3, edge.getMaxLength().intValue()); + assertTrue(op.getConditions().stream().anyMatch(c -> + c instanceof TypeCondition && "KNOWS".equals(((TypeCondition) c).getType()))); + } + + @Test + void testChainedPattern() { + MatchOperation op = parse("MATCH (a)-[r1]->(b)-[r2]->(c) RETURN a, b, c"); + assertEquals(3, op.getPattern().nodeCount()); + assertEquals(2, op.getPattern().edgeCount()); + } + + @Test + void testMultiplePatternsWithComma() { + MatchOperation op = parse("MATCH (a:Person), (b:Movie) RETURN a, b"); + assertEquals(2, op.getPattern().nodeCount()); + assertEquals(0, op.getPattern().edgeCount()); + assertEquals(2, countOf(op, LabelCondition.class)); + } + + @Test + void testMultipleRelationshipPatterns() { + MatchOperation op = parse("MATCH (a)-[:KNOWS]->(b), (a)-[:WORKS_WITH]->(c) RETURN b, c"); + assertEquals(3, op.getPattern().nodeCount()); + assertEquals(2, op.getPattern().edgeCount()); + } + + @Test + void testPathAssignment() { + MatchOperation op = parse("MATCH path = (a)-[:KNOWS]->(b) RETURN path"); + assertEquals(2, op.getPattern().nodeCount()); + assertEquals(1, op.getPattern().edgeCount()); + assertEquals("path", op.getPathVariable()); + assertEquals(1, op.getPathVariables().size()); + assertEquals("path", op.getPathVariables().get(0)); + assertFalse(op.isOptional()); + } + + @Test + void testMultiplePathAssignmentsKept() { + // MATCH p = ..., q = ... keeps both path variables, in source order. + MatchOperation op = parse("MATCH p = (a)-[:KNOWS]->(b), q = (c)-[:LIKES]->(d) RETURN p, q"); + assertEquals(2, op.getPathVariables().size()); + assertEquals("p", op.getPathVariables().get(0)); + assertEquals("q", op.getPathVariables().get(1)); + assertEquals("p", op.getPathVariable()); + } + + @Test + void testOptionalMatchFlag() { + assertTrue(parse("OPTIONAL MATCH (p)-[:KNOWS]->(f) RETURN f").isOptional()); + assertFalse(parse("MATCH (p)-[:KNOWS]->(f) RETURN f").isOptional()); + } + + @Test + void testQuantifiedPathRange() { + MatchOperation op = parse("MATCH ((a)-[:KNOWS]->(b)){1,3} RETURN a"); + assertEquals(0, op.getPattern().nodeCount()); + assertEquals(0, op.getPattern().edgeCount()); + assertEquals(1, op.getPattern().quantifiedPathCount()); + + QuantifiedPathPattern qpp = op.getPattern().getQuantifiedPaths().get(0); + assertEquals(1, qpp.getMin()); + assertEquals(3, qpp.getMax().intValue()); + assertEquals(2, qpp.getSubPattern().nodeCount()); + assertEquals(1, qpp.getSubPattern().edgeCount()); + // The :KNOWS type is scoped to the QPP, not flattened into the outer operation's list. + assertEquals(0, countOf(op, TypeCondition.class)); + assertEquals(1, qpp.getConditions().stream().filter(TypeCondition.class::isInstance).count()); + } + + @Test + void testQuantifiedPathConditionsScopedToSubPattern() { + // Labels, types and the inline WHERE of a QPP belong to the QPP, not the global condition list. + MatchOperation op = parse("MATCH ((a:Person)-[:KNOWS]->(b) WHERE a.age > 30){1,3} RETURN a"); + assertEquals(0, op.getConditions().size()); + QuantifiedPathPattern qpp = op.getPattern().getQuantifiedPaths().get(0); + assertEquals(1, qpp.getConditions().stream().filter(LabelCondition.class::isInstance).count()); + assertEquals(1, qpp.getConditions().stream().filter(TypeCondition.class::isInstance).count()); + assertEquals(1, qpp.getConditions().stream().filter(ComparisonCondition.class::isInstance).count()); + } + + @Test + void testQuantifiedPathOneOrMore() { + QuantifiedPathPattern qpp = parse("MATCH ((a)-[:KNOWS]->(b))+ RETURN a") + .getPattern().getQuantifiedPaths().get(0); + assertEquals(1, qpp.getMin()); + assertNull(qpp.getMax()); + assertTrue(qpp.isUnboundedMax()); + } + + @Test + void testQuantifiedPathZeroOrMore() { + QuantifiedPathPattern qpp = parse("MATCH ((a)-[:KNOWS]->(b))* RETURN a") + .getPattern().getQuantifiedPaths().get(0); + assertEquals(0, qpp.getMin()); + assertNull(qpp.getMax()); + } + + @Test + void testQuantifiedPathExact() { + QuantifiedPathPattern qpp = parse("MATCH ((a)-[:KNOWS]->(b)){2} RETURN a") + .getPattern().getQuantifiedPaths().get(0); + assertEquals(2, qpp.getMin()); + assertEquals(2, qpp.getMax().intValue()); + } + + @Test + void testQuantifiedPathMultiEdgeSubPath() { + QuantifiedPathPattern qpp = parse("MATCH ((a)-[:R]->(b)-[:S]->(c)){1,2} RETURN a") + .getPattern().getQuantifiedPaths().get(0); + assertEquals(3, qpp.getSubPattern().nodeCount()); + assertEquals(2, qpp.getSubPattern().edgeCount()); + } + + @Test + void testQuantifiedPathMixedWithPlainElements() { + MatchOperation op = parse( + "MATCH (start)-[:R]->(a) ((a)-[:KNOWS]->(b)){1,3} (b)-[:S]->(end) RETURN start"); + assertEquals(4, op.getPattern().nodeCount()); // start, a, b, end + assertEquals(2, op.getPattern().edgeCount()); // the two plain edges + assertEquals(1, op.getPattern().quantifiedPathCount()); + } + + @Test + void testQuantifiedPathNested() { + MatchOperation op = parse("MATCH (((a)-[:R]->(b)){1,2}){1,3} RETURN a"); + assertEquals(1, op.getPattern().quantifiedPathCount()); + + QuantifiedPathPattern outer = op.getPattern().getQuantifiedPaths().get(0); + assertEquals(1, outer.getMin()); + assertEquals(3, outer.getMax().intValue()); + assertEquals(1, outer.getSubPattern().quantifiedPathCount()); + + QuantifiedPathPattern inner = outer.getSubPattern().getQuantifiedPaths().get(0); + assertEquals(1, inner.getMin()); + assertEquals(2, inner.getMax().intValue()); + assertEquals(2, inner.getSubPattern().nodeCount()); + assertEquals(1, inner.getSubPattern().edgeCount()); + } + + @Test + void testWhereEquality() { + MatchOperation op = parse("MATCH (p:Person) WHERE p.age = 25 RETURN p"); + ComparisonCondition cc = comparison(op); + PropertyOperand left = (PropertyOperand) cc.getLeft(); + assertEquals("p", left.getVariableName()); + assertEquals("age", left.getPropertyKey()); + assertEquals(ComparisonOperator.EQUALS, cc.getOperator()); + assertEquals(25L, ((LiteralOperand) cc.getRight()).getValue()); + } + + @Test + void testWhereGreaterThan() { + assertEquals(ComparisonOperator.GREATER_THAN, + comparison(parse("MATCH (p:Person) WHERE p.age > 25 RETURN p")).getOperator()); + } + + @Test + void testWhereLessThan() { + assertEquals(ComparisonOperator.LESS_THAN, + comparison(parse("MATCH (p:Person) WHERE p.age < 25 RETURN p")).getOperator()); + } + + @Test + void testWhereNotEquals() { + assertEquals(ComparisonOperator.NOT_EQUALS, + comparison(parse("MATCH (p:Person) WHERE p.age <> 25 RETURN p")).getOperator()); + } + + @Test + void testWhereGreaterThanOrEqual() { + assertEquals(ComparisonOperator.GREATER_THAN_OR_EQUALS, + comparison(parse("MATCH (p:Person) WHERE p.age >= 25 RETURN p")).getOperator()); + } + + @Test + void testWhereLessThanOrEqual() { + assertEquals(ComparisonOperator.LESS_THAN_OR_EQUALS, + comparison(parse("MATCH (p:Person) WHERE p.age <= 25 RETURN p")).getOperator()); + } + + @Test + void testWhereMultipleConditionsAnd() { + MatchOperation op = parse("MATCH (p) WHERE p.age > 18 AND p.age < 65 RETURN p"); + assertEquals(1, op.getConditions().size()); + AndCondition and = (AndCondition) op.getConditions().get(0); + assertEquals(2, and.getConditions().size()); + assertTrue(and.getConditions().stream().allMatch(ComparisonCondition.class::isInstance)); + } + + @Test + void testWhereOr() { + MatchOperation op = parse("MATCH (p) WHERE p.age < 18 OR p.age > 65 RETURN p"); + OrCondition or = (OrCondition) op.getConditions().get(0); + assertEquals(2, or.getConditions().size()); + assertTrue(or.getConditions().stream().allMatch(ComparisonCondition.class::isInstance)); + } + + @Test + void testWhereNot() { + MatchOperation op = parse("MATCH (p) WHERE NOT p.active = true RETURN p"); + NotCondition not = (NotCondition) op.getConditions().get(0); + assertInstanceOf(ComparisonCondition.class, not.getCondition()); + } + + @Test + void testWhereDoubleNegationCancels() { + MatchOperation op = parse("MATCH (p) WHERE NOT NOT p.x = 1 RETURN p"); + assertInstanceOf(ComparisonCondition.class, op.getConditions().get(0)); + } + + @Test + void testWhereXor() { + MatchOperation op = parse("MATCH (p) WHERE p.x = 1 XOR p.y = 2 RETURN p"); + XorCondition xor = (XorCondition) op.getConditions().get(0); + assertEquals(2, xor.getConditions().size()); + } + + @Test + void testWhereParenthesesGroup() { + // The parens force (b OR c) to be one branch of the AND. + MatchOperation op = parse("MATCH (p) WHERE p.a = 1 AND (p.b = 2 OR p.c = 3) RETURN p"); + AndCondition and = (AndCondition) op.getConditions().get(0); + assertEquals(2, and.getConditions().size()); + assertInstanceOf(ComparisonCondition.class, and.getConditions().get(0)); + OrCondition or = (OrCondition) and.getConditions().get(1); + assertEquals(2, or.getConditions().size()); + } + + @Test + void testWhereNotOfGroup() { + MatchOperation op = parse("MATCH (p) WHERE NOT (p.a = 1 OR p.b = 2) RETURN p"); + NotCondition not = (NotCondition) op.getConditions().get(0); + assertInstanceOf(OrCondition.class, not.getCondition()); + } + + @Test + void testWherePrecedenceOrOfAnds() { + // a AND b OR c == (a AND b) OR c + MatchOperation op = parse("MATCH (p) WHERE p.a = 1 AND p.b = 2 OR p.c = 3 RETURN p"); + OrCondition or = (OrCondition) op.getConditions().get(0); + assertEquals(2, or.getConditions().size()); + assertInstanceOf(AndCondition.class, or.getConditions().get(0)); + assertInstanceOf(ComparisonCondition.class, or.getConditions().get(1)); + } + + @Test + void testWhereStringComparison() { + ComparisonCondition cc = comparison(parse("MATCH (p:Person) WHERE p.name = \"Alice\" RETURN p")); + assertEquals("name", ((PropertyOperand) cc.getLeft()).getPropertyKey()); + assertEquals("Alice", ((LiteralOperand) cc.getRight()).getValue()); + } + + @Test + void testWhereStartsWith() { + assertEquals(ComparisonOperator.STARTS_WITH, + comparison(parse("MATCH (p:Person) WHERE p.name STARTS WITH 'A' RETURN p")).getOperator()); + } + + @Test + void testWhereEndsWith() { + assertEquals(ComparisonOperator.ENDS_WITH, + comparison(parse("MATCH (p:Person) WHERE p.name ENDS WITH 'son' RETURN p")).getOperator()); + } + + @Test + void testWhereContains() { + assertEquals(ComparisonOperator.CONTAINS, + comparison(parse("MATCH (p:Person) WHERE p.name CONTAINS 'li' RETURN p")).getOperator()); + } + + @Test + void testWhereIsNull() { + assertEquals(ComparisonOperator.IS_NULL, + comparison(parse("MATCH (p:Person) WHERE p.email IS NULL RETURN p")).getOperator()); + } + + @Test + void testWhereIsNotNull() { + assertEquals(ComparisonOperator.IS_NOT_NULL, + comparison(parse("MATCH (p:Person) WHERE p.email IS NOT NULL RETURN p")).getOperator()); + } + + @Test + void testComparisonPropertyVsLiteral() { + ComparisonCondition cc = comparison(parse("MATCH (p) WHERE p.age > 25 RETURN p")); + PropertyOperand left = (PropertyOperand) cc.getLeft(); + assertEquals("p", left.getVariableName()); + assertEquals("age", left.getPropertyKey()); + assertEquals(25L, ((LiteralOperand) cc.getRight()).getValue()); + } + + @Test + void testComparisonPropertyVsProperty() { + // Typed operands keep both sides of n.age > m.age as property references. + ComparisonCondition cc = comparison(parse("MATCH (n)-->(m) WHERE n.age > m.age RETURN n")); + PropertyOperand left = (PropertyOperand) cc.getLeft(); + PropertyOperand right = (PropertyOperand) cc.getRight(); + assertEquals("n", left.getVariableName()); + assertEquals("age", left.getPropertyKey()); + assertEquals("m", right.getVariableName()); + assertEquals("age", right.getPropertyKey()); + } + + @Test + void testComparisonInListDecomposed() { + ComparisonCondition cc = comparison(parse("MATCH (p) WHERE p.age IN [18, 21, 65] RETURN p")); + assertEquals(ComparisonOperator.IN, cc.getOperator()); + ListOperand list = (ListOperand) cc.getRight(); + assertEquals(3, list.getElements().size()); + assertEquals(18L, ((LiteralOperand) list.getElements().get(0)).getValue()); + assertEquals(65L, ((LiteralOperand) list.getElements().get(2)).getValue()); + } + + @Test + void testComparisonInListWithPropertyElement() { + // Elements are parsed recursively, so a property reference inside the list is preserved. + ComparisonCondition cc = comparison(parse("MATCH (p) WHERE p.age IN [p.min, 40] RETURN p")); + ListOperand list = (ListOperand) cc.getRight(); + assertEquals("min", ((PropertyOperand) list.getElements().get(0)).getPropertyKey()); + assertEquals(40L, ((LiteralOperand) list.getElements().get(1)).getValue()); + } + + @Test + void testComparisonConstantArithmeticFolded() { + // A fully-literal RHS is evaluated at parse time, so the calculator sees a single value. + ComparisonCondition cc = comparison(parse("MATCH (p) WHERE p.age > 25 + 5 RETURN p")); + assertInstanceOf(PropertyOperand.class, cc.getLeft()); + assertEquals(30L, ((LiteralOperand) cc.getRight()).getValue()); + } + + @Test + void testComparisonConstantArithmeticPrecedenceFolded() { + // Precedence and a double operand: 2 + 3 * 4 = 14, kept as a double once a double is involved. + ComparisonCondition cc = comparison(parse("MATCH (p) WHERE p.age > 2 + 3 * 4.0 RETURN p")); + assertEquals(14.0, ((LiteralOperand) cc.getRight()).getValue()); + } + + @Test + void testConstantArithmeticFoldsInLongDomainWithoutPrecisionLoss() { + // 2^53 + 1 is exact as a long but not as a double; folding in the long domain keeps it. + ComparisonCondition cc = comparison(parse("MATCH (p) WHERE p.age = 9007199254740992 + 1 RETURN p")); + assertEquals(9007199254740993L, ((LiteralOperand) cc.getRight()).getValue()); + } + + @Test + void testConstantArithmeticOverflowLeftUnfolded() { + // Long overflow would yield a wrong wrapped constant, so the node is kept structural instead. + ComparisonCondition cc = comparison( + parse("MATCH (p) WHERE p.age > 9223372036854775807 + 1 RETURN p")); + assertInstanceOf(ArithmeticOperand.class, cc.getRight()); + } + + @Test + void testComparisonNonConstantArithmeticKeepsPropertyRef() { + // A graph-dependent RHS stays a structured tree, with the inner property reference preserved. + ComparisonCondition cc = comparison(parse("MATCH (a)-[]->(p) WHERE a.age > p.age + 5 RETURN a")); + ArithmeticOperand rhs = (ArithmeticOperand) cc.getRight(); + assertEquals(ArithmeticOperator.PLUS, rhs.getOperator()); + assertEquals("age", ((PropertyOperand) rhs.getLeft()).getPropertyKey()); + assertEquals(5L, ((LiteralOperand) rhs.getRight()).getValue()); + } + + @Test + void testChainedComparisonExpandsToAnd() { + // a < b < c ≡ (a < b) AND (b < c) — Cypher chains comparisons, no operand is dropped. + AndCondition and = (AndCondition) parse("MATCH (p) WHERE 18 < p.age < 65 RETURN p") + .getConditions().get(0); + assertEquals(2, and.getConditions().size()); + ComparisonCondition first = (ComparisonCondition) and.getConditions().get(0); + ComparisonCondition second = (ComparisonCondition) and.getConditions().get(1); + assertEquals(ComparisonOperator.LESS_THAN, first.getOperator()); + assertEquals(18L, ((LiteralOperand) first.getLeft()).getValue()); + assertEquals("age", ((PropertyOperand) first.getRight()).getPropertyKey()); + // The middle operand is repeated as the left side of the second link. + assertEquals("age", ((PropertyOperand) second.getLeft()).getPropertyKey()); + assertEquals(65L, ((LiteralOperand) second.getRight()).getValue()); + } + + @Test + void testChainedComparisonMixedOperatorsKeptInOrder() { + // A mixed chain keeps each operator: a <= b < c. + AndCondition and = (AndCondition) parse("MATCH (p) WHERE 18 <= p.age < 65 RETURN p") + .getConditions().get(0); + assertEquals(ComparisonOperator.LESS_THAN_OR_EQUALS, + ((ComparisonCondition) and.getConditions().get(0)).getOperator()); + assertEquals(ComparisonOperator.LESS_THAN, + ((ComparisonCondition) and.getConditions().get(1)).getOperator()); + } + + @Test + void testComparisonNonNumericConcatKeptRaw() { + // String concatenation (||) is not numeric arithmetic, so it stays a RawOperand. + ComparisonCondition cc = comparison(parse("MATCH (p) WHERE p.name = \"a\" || \"b\" RETURN p")); + assertInstanceOf(RawOperand.class, cc.getRight()); + } + + @Test + void testComparisonStringLiteralOperand() { + ComparisonCondition cc = comparison(parse("MATCH (p) WHERE p.name = \"Alice\" RETURN p")); + assertEquals("Alice", ((LiteralOperand) cc.getRight()).getValue()); + } + + @Test + void testComplexExample() { + MatchOperation op = parse( + "MATCH (p:Person {name: \"Alice\"})-[:KNOWS]->(f:Person) WHERE f.age > 25 RETURN f"); + assertEquals(2, op.getPattern().nodeCount()); + assertEquals(1, op.getPattern().edgeCount()); + assertEquals(2, countOf(op, LabelCondition.class)); + assertEquals(1, countOf(op, PropertyCondition.class)); + assertEquals(1, countOf(op, TypeCondition.class)); + assertEquals(1, countOf(op, ComparisonCondition.class)); + } + + @Test + void testDocExampleMultipleRelChain() { + MatchOperation op = parse( + "MATCH (:Person)-[:ACTED_IN]->(movie:Movie)<-[:DIRECTED]-(director) RETURN movie, director"); + assertEquals(3, op.getPattern().nodeCount()); + assertEquals(2, op.getPattern().edgeCount()); + } + + @Test + void testMatchCaseInsensitive() { + assertEquals(1, parse("match (n:Person) return n").getPattern().nodeCount()); + } + + @Test + void testNullQueryFails() { + assertFails(null); + } + + @Test + void testEmptyQueryFails() { + assertFails(""); + } + + @Test + void testWhitespaceOnlyQueryFails() { + assertFails(" \t\n "); + } + + @Test + void testNonMatchQueryFails() { + assertFails("CREATE (n:Person) RETURN n"); + } + + @Test + void testSyntaxErrorFails() { + assertFails("MATCH (n:Person RETURN n"); // missing closing paren + } + + private ComparisonCondition comparison(MatchOperation op) { + return (ComparisonCondition) op.getConditions().stream() + .filter(ComparisonCondition.class::isInstance) + .findFirst() + .orElseThrow(() -> new AssertionError("no ComparisonCondition found")); + } +} From 2e7d1d8a7f17eb321da9833fdc76ab93deb7a605 Mon Sep 17 00:00:00 2001 From: Andres Felder <81707831+andyfelder16@users.noreply.github.com> Date: Tue, 9 Jun 2026 13:27:50 -0300 Subject: [PATCH 2/2] update operand javadocs, tidy test imports and helper placement --- .../java/controller/neo4j/conditions/Operand.java | 9 ++++++--- .../controller/neo4j/conditions/RawOperand.java | 4 ++-- .../neo4j/parser/Cypher25AntlrParserTest.java | 15 ++++++++------- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/Operand.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/Operand.java index a0a44cd40c..530619b5c1 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/Operand.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/Operand.java @@ -1,9 +1,12 @@ package org.evomaster.client.java.controller.neo4j.conditions; /** - * One side of a comparison in a WHERE clause: a {@link PropertyOperand} resolved from the - * matched element, a {@link LiteralOperand} constant, or a {@link RawOperand} kept unchanged. - * Modelling each side explicitly lets {@code n.age > m.age} be told apart from {@code n.age > "m.age"}. + * One operand of a condition: a side of a WHERE comparison or an inline property value. Concrete + * kinds are {@link PropertyOperand} (a {@code variable.key} resolved from the matched element), + * {@link LiteralOperand} (a constant), {@link ArithmeticOperand} (an arithmetic expression), + * {@link ListOperand} (an {@code IN} list), and {@link RawOperand} (kept verbatim when not + * decomposed). Modelling each explicitly lets {@code n.age > m.age} be told apart from + * {@code n.age > "m.age"}. */ public interface Operand { } diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/RawOperand.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/RawOperand.java index ddf94e9ad7..76cbef3426 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/RawOperand.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/neo4j/conditions/RawOperand.java @@ -3,8 +3,8 @@ import java.util.Objects; /** - * An operand kept unchanged because the model does not decompose it: arithmetic such as - * 25 + 5, a function call, or a list. Carries the original text so nothing is dropped. + * An operand kept unchanged because the model does not decompose it: a function call, a parameter, + * or string concatenation. Carries the original text so nothing is dropped. */ public final class RawOperand implements Operand { diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/neo4j/parser/Cypher25AntlrParserTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/neo4j/parser/Cypher25AntlrParserTest.java index efd0872e14..43eeb7c4c1 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/neo4j/parser/Cypher25AntlrParserTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/neo4j/parser/Cypher25AntlrParserTest.java @@ -4,8 +4,8 @@ import org.evomaster.client.java.controller.neo4j.operations.MatchOperation; import org.evomaster.client.java.controller.neo4j.operations.PatternEdge; import org.evomaster.client.java.controller.neo4j.operations.QuantifiedPathPattern; -import org.junit.jupiter.api.Test; import org.evomaster.client.java.controller.neo4j.parser.cypher25.Cypher25AntlrParser; +import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @@ -36,6 +36,13 @@ private long countOf(MatchOperation op, Class typ return op.getConditions().stream().filter(type::isInstance).count(); } + private ComparisonCondition comparison(MatchOperation op) { + return (ComparisonCondition) op.getConditions().stream() + .filter(ComparisonCondition.class::isInstance) + .findFirst() + .orElseThrow(() -> new AssertionError("no ComparisonCondition found")); + } + @Test void testAllNodes() { MatchOperation op = parse("MATCH (n) RETURN n"); @@ -799,10 +806,4 @@ void testSyntaxErrorFails() { assertFails("MATCH (n:Person RETURN n"); // missing closing paren } - private ComparisonCondition comparison(MatchOperation op) { - return (ComparisonCondition) op.getConditions().stream() - .filter(ComparisonCondition.class::isInstance) - .findFirst() - .orElseThrow(() -> new AssertionError("no ComparisonCondition found")); - } }