diff --git a/cypher/CHANGES.txt b/cypher/CHANGES.txt index a73def85d..b0fdef60d 100644 --- a/cypher/CHANGES.txt +++ b/cypher/CHANGES.txt @@ -1,3 +1,7 @@ +1.9.M01 +-------------------- +o Added all the escape characters that Java uses + 1.8.M06 (2012-07-06) -------------------- o Fixes problem when graph elements are deleted multiple times in the same query diff --git a/cypher/src/docs/dev/expressions.txt b/cypher/src/docs/dev/expressions.txt index def07df31..9e6ee3a20 100644 --- a/cypher/src/docs/dev/expressions.txt +++ b/cypher/src/docs/dev/expressions.txt @@ -16,3 +16,21 @@ An expression in Cypher can be: * An aggregate function -- `avg(x.prop)`, `count(*)` * Relationship types -- `:REL_TYPE`, +:\`REL TYPE`+, `:REL1|REL2` * A path-pattern: `a-->()<--b` + += Note on string literals = +Strings can contain escape sequences such as the ones defined for Java: + +[options="header", cols=">,<", width="50%"] +|=================== +|Escape seq|Character +|`\t`|Tab +|`\b`|Backspace +|`\n`|Newline +|`\r`|Carriage return +|`\f`|Form feed +|`\'`|Single quote +|`\"`|Double quote +|`\\`|Backslash +|=================== + + diff --git a/cypher/src/main/scala/org/neo4j/cypher/CypherParser.scala b/cypher/src/main/scala/org/neo4j/cypher/CypherParser.scala index 8dc18419f..db89a704c 100644 --- a/cypher/src/main/scala/org/neo4j/cypher/CypherParser.scala +++ b/cypher/src/main/scala/org/neo4j/cypher/CypherParser.scala @@ -41,7 +41,7 @@ class CypherParser(version: String) { v match { case "1.7" => v17.parse(q) case "1.8" => v18.parse(q) - case "1.9" => v18.parse(q) + case "1.9" => v19.parse(q) case _ => throw new SyntaxException("Versions supported are 1.7, 1.8 and 1.9") } } diff --git a/cypher/src/main/scala/org/neo4j/cypher/internal/commands/ReturnItem.scala b/cypher/src/main/scala/org/neo4j/cypher/internal/commands/ReturnItem.scala index e468e6aa8..72442b668 100644 --- a/cypher/src/main/scala/org/neo4j/cypher/internal/commands/ReturnItem.scala +++ b/cypher/src/main/scala/org/neo4j/cypher/internal/commands/ReturnItem.scala @@ -41,7 +41,7 @@ case class ReturnItem(expression: Expression, name: String, renamed: Boolean = f extends ReturnColumn { def expressions(symbols: SymbolTable) = Map(name -> expression) - override def toString = name + override def toString = expression + " as `"+ name + "`" def rename(newName: String) = ReturnItem(expression, newName, renamed = true) } \ No newline at end of file diff --git a/cypher/src/main/scala/org/neo4j/cypher/internal/commands/expressions/Literal.scala b/cypher/src/main/scala/org/neo4j/cypher/internal/commands/expressions/Literal.scala index fe259d2aa..907f674fc 100644 --- a/cypher/src/main/scala/org/neo4j/cypher/internal/commands/expressions/Literal.scala +++ b/cypher/src/main/scala/org/neo4j/cypher/internal/commands/expressions/Literal.scala @@ -37,4 +37,6 @@ case class Literal(v: Any) extends Expression { def calculateType(symbols: SymbolTable): CypherType = CypherType.fromJava(v) def symbolTableDependencies = Set() + + override def toString() = "Literal(" + v.toString + ")" } \ No newline at end of file diff --git a/cypher/src/main/scala/org/neo4j/cypher/internal/parser/v1_9/Expressions.scala b/cypher/src/main/scala/org/neo4j/cypher/internal/parser/v1_9/Expressions.scala index 1c1ca7579..da8f91178 100644 --- a/cypher/src/main/scala/org/neo4j/cypher/internal/parser/v1_9/Expressions.scala +++ b/cypher/src/main/scala/org/neo4j/cypher/internal/parser/v1_9/Expressions.scala @@ -83,21 +83,35 @@ trait Expressions extends Base with ParserPattern with Predicates { var result: Option[ParseResult[Expression]] = None while (!ls.isEmpty && result.isEmpty) { - val (pref, suf) = ls span { c => c != '\\' && c != startChar } + val (pref, suf) = ls span { + c => c != '\\' && c != startChar + } idx += pref.length sb ++= pref - if (suf.isEmpty) + if (suf.isEmpty) { result = Some(Failure("end of string missing", in)) + } else { - val first: Char = suf(0) - first match { - case c if c == startChar => + val first: Char = suf(0) + if (first == startChar) { result = Some(Success(Literal(sb.result()), in.drop(idx - in.offset + 2))) - case '\\' if suf(1) == '\''||suf(1)=='\"' => - sb.append(suf(1)) - idx += 2 - ls = suf.drop(2) + } else { + val (escChars, afterEscape) = suf.splitAt(2) + + if (escChars.size == 1) { + result = Some(Failure("invalid escape sequence", in)) + } else { + + ls = afterEscape + idx += 2 + + parseEscapeChars(escChars.tail, in) match { + case Left(c) => sb.append(c) + case Right(failure) => result = Some(failure) + } + } + } } } @@ -108,6 +122,22 @@ trait Expressions extends Base with ParserPattern with Predicates { } } + case class EscapeProduct(result: Option[ParseResult[Expression]]) + + private def parseEscapeChars(suf: List[Char], in:Input): Either[Char, Failure] = suf match { + case '\\' :: tail => Left('\\') + case 'a' :: tail => Left('\u0007') + case 'b' :: tail => Left('\b') + case 'f' :: tail => Left('\f') + case 'n' :: tail => Left('\n') + case 'r' :: tail => Left('\r') + case 't' :: tail => Left('\t') + case 'v' :: tail => Left('\u000b') + case '\'' :: tail => Left('\'') + case '"' :: tail => Left('"') + case _ => Right(Failure("invalid escape sequence", in)) + } + def numberLiteral: Parser[Expression] = number ^^ (x => { val value: Any = if (x.contains(".")) x.toDouble diff --git a/cypher/src/test/scala/org/neo4j/cypher/CypherParserTest.scala b/cypher/src/test/scala/org/neo4j/cypher/CypherParserTest.scala index aff4f8b60..961c4f0f8 100644 --- a/cypher/src/test/scala/org/neo4j/cypher/CypherParserTest.scala +++ b/cypher/src/test/scala/org/neo4j/cypher/CypherParserTest.scala @@ -45,13 +45,20 @@ class CypherParserTest extends JUnitSuite with Assertions { returns(ReturnItem(Literal("apa"), "\"apa\""))) } - @Test def should_return_string_literal_with_escaped_quote_in() { + @Test def should_return_string_literal_with_escaped_quote_in1_8() { testFrom_1_8("start s = node(1) return \"ap\\\"a\"", Query. start(NodeById("s", 1)). returns(ReturnItem(Literal("ap\"a"), "\"ap\\\"a\""))) } + @Test def should_return_string_literal_with_escaped_quote_in() { + testFrom_1_9("start s = node(1) return \"a\\tp\\\"a\"", + Query. + start(NodeById("s", 1)). + returns(ReturnItem(Literal("a\tp\"a"), "\"a\\tp\\\"a\""))) + } + @Test def allTheNodes() { testAll("start s = NODE(*) return s", Query. @@ -1794,6 +1801,10 @@ foreach(x in [1,2,3] : test_1_9(query, expectedQuery) } + def testFrom_1_9(query: String, expectedQuery: Query) { + test_1_9(query, expectedQuery) + } + def testAll(query: String, expectedQuery: Query) { test_1_7(query, expectedQuery) test_1_8(query, expectedQuery) @@ -1804,6 +1815,14 @@ foreach(x in [1,2,3] : test_1_7(queryText, queryAst) } + @Test + def special_should_not_exists() { + val parser = new CypherParser() + val v18 = parser.parse("cypher 1.8 start s = node(1) return \"ap\\\"a\"") + val v19 = parser.parse("cypher 1.9 start s = node(1) return \"ap\\\"a\"") + assertEquals(v18, v19) + } + def testQuery(version: Option[String], query: String, expectedQuery: Query) { val parser = new CypherParser()