Repository: cassandra Updated Branches: refs/heads/trunk 1ecb70165 -> 6618bd89d
Accept dollar-quoted strings in CQL Patch by Robert Stupp; reviewed by Tyler Hobbs for CASSANDRA-7769 Project: http://git-wip-us.apache.org/repos/asf/cassandra/repo Commit: http://git-wip-us.apache.org/repos/asf/cassandra/commit/6618bd89 Tree: http://git-wip-us.apache.org/repos/asf/cassandra/tree/6618bd89 Diff: http://git-wip-us.apache.org/repos/asf/cassandra/diff/6618bd89 Branch: refs/heads/trunk Commit: 6618bd89d3d51b06a6c12ec947cce089807a4190 Parents: 1ecb701 Author: Robert Stupp <sn...@snazy.de> Authored: Tue Sep 23 15:19:13 2014 -0500 Committer: Tyler Hobbs <ty...@datastax.com> Committed: Tue Sep 23 15:19:13 2014 -0500 ---------------------------------------------------------------------- CHANGES.txt | 1 + pylib/cqlshlib/cql3handling.py | 5 +- pylib/cqlshlib/cqlhandling.py | 2 +- src/java/org/apache/cassandra/cql3/Cql.g | 24 +++++-- .../apache/cassandra/cql3/ErrorCollector.java | 6 +- .../org/apache/cassandra/cql3/CQLTester.java | 11 +-- .../org/apache/cassandra/cql3/PgStringTest.java | 76 ++++++++++++++++++++ test/unit/org/apache/cassandra/cql3/UFTest.java | 20 ++++++ 8 files changed, 128 insertions(+), 17 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cassandra/blob/6618bd89/CHANGES.txt ---------------------------------------------------------------------- diff --git a/CHANGES.txt b/CHANGES.txt index e36ef8f..267c4c2 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 3.0 + * Accept dollar quoted strings in CQL (CASSANDRA-7769) * Make assassinate a first class command (CASSANDRA-7935) * Support IN clause on any clustering column (CASSANDRA-4762) * Improve compaction logging (CASSANDRA-7818) http://git-wip-us.apache.org/repos/asf/cassandra/blob/6618bd89/pylib/cqlshlib/cql3handling.py ---------------------------------------------------------------------- diff --git a/pylib/cqlshlib/cql3handling.py b/pylib/cqlshlib/cql3handling.py index 3425ce2..69fc277 100644 --- a/pylib/cqlshlib/cql3handling.py +++ b/pylib/cqlshlib/cql3handling.py @@ -149,7 +149,10 @@ syntax_rules = r''' JUNK ::= /([ \t\r\f\v]+|(--|[/][/])[^\n\r]*([\n\r]|$)|[/][*].*?[*][/])/ ; -<stringLiteral> ::= /'([^']|'')*'/ ; +<stringLiteral> ::= <quotedStringLiteral> + | <pgStringLiteral> ; +<quotedStringLiteral> ::= /'([^']|'')*'/ ; +<pgStringLiteral> ::= /\$\$.*\$\$/; <quotedName> ::= /"([^"]|"")*"/ ; <float> ::= /-?[0-9]+\.[0-9]+/ ; <uuid> ::= /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/ ; http://git-wip-us.apache.org/repos/asf/cassandra/blob/6618bd89/pylib/cqlshlib/cqlhandling.py ---------------------------------------------------------------------- diff --git a/pylib/cqlshlib/cqlhandling.py b/pylib/cqlshlib/cqlhandling.py index fc3dc20..00ba736 100644 --- a/pylib/cqlshlib/cqlhandling.py +++ b/pylib/cqlshlib/cqlhandling.py @@ -302,7 +302,7 @@ class CqlParsingRuleSet(pylexotron.ParsingRuleSet): if tok[0] == 'unclosedName': # strip one quote return tok[1][1:].replace('""', '"') - if tok[0] == 'stringLiteral': + if tok[0] == 'quotedStringLiteral': # strip quotes return tok[1][1:-1].replace("''", "'") if tok[0] == 'unclosedString': http://git-wip-us.apache.org/repos/asf/cassandra/blob/6618bd89/src/java/org/apache/cassandra/cql3/Cql.g ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/Cql.g b/src/java/org/apache/cassandra/cql3/Cql.g index 8c40885..e4bfd32 100644 --- a/src/java/org/apache/cassandra/cql3/Cql.g +++ b/src/java/org/apache/cassandra/cql3/Cql.g @@ -517,7 +517,6 @@ createFunctionStatement returns [CreateFunctionStatement expr] ( body = STRING_LITERAL { bodyOrClassName = $body.text; } ) - /* TODO placeholder for pg-style function body */ ) ) ) @@ -1420,9 +1419,26 @@ fragment Y: ('y'|'Y'); fragment Z: ('z'|'Z'); STRING_LITERAL - @init{ StringBuilder b = new StringBuilder(); } - @after{ setText(b.toString()); } - : '\'' (c=~('\'') { b.appendCodePoint(c);} | '\'' '\'' { b.appendCodePoint('\''); })* '\'' + @init{ + StringBuilder txt = new StringBuilder(); // temporary to build pg-style-string + } + @after{ setText(txt.toString()); } + : + /* pg-style string literal */ + ( + '\$' '\$' + ( /* collect all input until '$$' is reached again */ + { (input.size() - input.index() > 1) + && !"$$".equals(input.substring(input.index(), input.index() + 1)) }? + => c=. { txt.appendCodePoint(c); } + )* + '\$' '\$' + ) + | + /* conventional quoted string literal */ + ( + '\'' (c=~('\'') { txt.appendCodePoint(c);} | '\'' '\'' { txt.appendCodePoint('\''); })* '\'' + ) ; QUOTED_NAME http://git-wip-us.apache.org/repos/asf/cassandra/blob/6618bd89/src/java/org/apache/cassandra/cql3/ErrorCollector.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/ErrorCollector.java b/src/java/org/apache/cassandra/cql3/ErrorCollector.java index ad0f703..f49cca4 100644 --- a/src/java/org/apache/cassandra/cql3/ErrorCollector.java +++ b/src/java/org/apache/cassandra/cql3/ErrorCollector.java @@ -146,7 +146,9 @@ public final class ErrorCollector implements ErrorListener if (!includeQueryStart) builder.append("..."); - lines[lineIndex(to)] = lines[lineIndex(to)].substring(0, getLastCharPositionInLine(to)); + String toLine = lines[lineIndex(to)]; + int toEnd = getLastCharPositionInLine(to); + lines[lineIndex(to)] = toEnd >= toLine.length() ? toLine : toLine.substring(0, toEnd); lines[lineIndex(offending)] = highlightToken(lines[lineIndex(offending)], offending); lines[lineIndex(from)] = lines[lineIndex(from)].substring(from.getCharPositionInLine()); @@ -162,7 +164,7 @@ public final class ErrorCollector implements ErrorListener /** * Checks if the specified tokens are valid. * - * @param token the tokens to check + * @param tokens the tokens to check * @return <code>true</code> if all the specified tokens are valid ones, * <code>false</code> otherwise. */ http://git-wip-us.apache.org/repos/asf/cassandra/blob/6618bd89/test/unit/org/apache/cassandra/cql3/CQLTester.java ---------------------------------------------------------------------- diff --git a/test/unit/org/apache/cassandra/cql3/CQLTester.java b/test/unit/org/apache/cassandra/cql3/CQLTester.java index b4a1b09..a182f43 100644 --- a/test/unit/org/apache/cassandra/cql3/CQLTester.java +++ b/test/unit/org/apache/cassandra/cql3/CQLTester.java @@ -37,7 +37,6 @@ import org.slf4j.LoggerFactory; import org.apache.cassandra.SchemaLoader; import org.apache.cassandra.config.CFMetaData; import org.apache.cassandra.config.Schema; -import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.Directories; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.marshal.*; @@ -45,7 +44,6 @@ import org.apache.cassandra.exceptions.*; import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.serializers.TypeSerializer; import org.apache.cassandra.service.StorageService; -import org.apache.cassandra.utils.ByteBufferUtil; /** * Base class for CQL tests. @@ -140,11 +138,7 @@ public abstract class CQLTester if (currentTable != null) Keyspace.open(KEYSPACE).getColumnFamilyStore(currentTable).forceFlush().get(); } - catch (InterruptedException e) - { - throw new RuntimeException(e); - } - catch (ExecutionException e) + catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } @@ -287,7 +281,6 @@ public abstract class CQLTester for (int j = 0; j < meta.size(); j++) { ColumnSpecification column = meta.get(j); - Object expectedValue = expected[j]; ByteBuffer expectedByteValue = makeByteBuffer(expected[j], (AbstractType)column.type); ByteBuffer actualValue = actual.getBytes(column.name.toString()); @@ -307,7 +300,7 @@ public abstract class CQLTester Assert.fail(String.format("Got less rows than expected. Expected %d but got %d.", rows.length, i)); } - Assert.assertTrue(String.format("Got more rows than expected. Expected %d but got %d", rows.length, i), i == rows.length); + Assert.assertTrue(String.format("Got %s rows than expected. Expected %d but got %d", rows.length>i ? "less" : "more", rows.length, i), i == rows.length); } protected void assertAllRows(Object[]... rows) throws Throwable http://git-wip-us.apache.org/repos/asf/cassandra/blob/6618bd89/test/unit/org/apache/cassandra/cql3/PgStringTest.java ---------------------------------------------------------------------- diff --git a/test/unit/org/apache/cassandra/cql3/PgStringTest.java b/test/unit/org/apache/cassandra/cql3/PgStringTest.java new file mode 100644 index 0000000..856a255 --- /dev/null +++ b/test/unit/org/apache/cassandra/cql3/PgStringTest.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.apache.cassandra.cql3; + +import org.junit.Test; + +import org.apache.cassandra.exceptions.SyntaxException; + +public class PgStringTest extends CQLTester +{ + @Test + public void testPgSyleFunction() throws Throwable + { + execute("create or replace function pg::pgfun1 ( input double ) returns text language java\n" + + "AS $$return \"foobar\";$$"); + } + + @Test + public void testPgSyleInsert() throws Throwable + { + createTable("CREATE TABLE %s (key ascii primary key, val text)"); + + // some non-terminated pg-strings + assertInvalidSyntax("INSERT INTO %s (key, val) VALUES ($ $key_empty$$, $$'' value for empty$$)"); + assertInvalidSyntax("INSERT INTO %s (key, val) VALUES ($$key_empty$$, $$'' value for empty$ $)"); + assertInvalidSyntax("INSERT INTO %s (key, val) VALUES ($$key_empty$ $, $$'' value for empty$$)"); + + // different pg-style markers for multiple strings + execute("INSERT INTO %s (key, val) VALUES ($$prim$ $ $key$$, $$some '' arbitrary value$$)"); + // same empty pg-style marker for multiple strings + execute("INSERT INTO %s (key, val) VALUES ($$key_empty$$, $$'' value for empty$$)"); + // stange but valid pg-style + execute("INSERT INTO %s (key, val) VALUES ($$$foo$_$foo$$, $$$'' value for empty$$)"); + // these are conventional quoted strings + execute("INSERT INTO %s (key, val) VALUES ('$txt$key$$$$txt$', '$txt$'' other value$txt$')"); + + assertRows(execute("SELECT key, val FROM %s WHERE key='prim$ $ $key'"), + row("prim$ $ $key", "some '' arbitrary value") + ); + assertRows(execute("SELECT key, val FROM %s WHERE key='key_empty'"), + row("key_empty", "'' value for empty") + ); + assertRows(execute("SELECT key, val FROM %s WHERE key='$foo$_$foo'"), + row("$foo$_$foo", "$'' value for empty") + ); + assertRows(execute("SELECT key, val FROM %s WHERE key='$txt$key$$$$txt$'"), + row("$txt$key$$$$txt$", "$txt$' other value$txt$") + ); + + // invalid syntax + assertInvalidSyntax("INSERT INTO %s (key, val) VALUES ($ascii$prim$$$key$invterm$, $txt$some '' arbitrary value$txt$)"); + } + + @Test(expected = SyntaxException.class) + public void testMarkerPgFail() throws Throwable + { + // must throw SyntaxException - not StringIndexOutOfBoundsException or similar + execute("create function foo::pgfun1 ( input double ) returns text language java\n" + + "AS $javasrc$return 0L;$javasrc$;"); + } +} http://git-wip-us.apache.org/repos/asf/cassandra/blob/6618bd89/test/unit/org/apache/cassandra/cql3/UFTest.java ---------------------------------------------------------------------- diff --git a/test/unit/org/apache/cassandra/cql3/UFTest.java b/test/unit/org/apache/cassandra/cql3/UFTest.java index b2e4b84..3a48500 100644 --- a/test/unit/org/apache/cassandra/cql3/UFTest.java +++ b/test/unit/org/apache/cassandra/cql3/UFTest.java @@ -410,4 +410,24 @@ public class UFTest extends CQLTester // function throws a RuntimeException which is wrapped by InvalidRequestException assertInvalid("SELECT key, val, foo::jrtef(val) FROM %s"); } + + @Test + public void testJavaDollarQuotedFunction() throws Throwable + { + String functionBody = "\n" + + " // parameter val is of type java.lang.Double\n" + + " /* return type is of type java.lang.Double */\n" + + " if (input == null) {\n" + + " return null;\n" + + " }\n" + + " double v = Math.sin( input.doubleValue() );\n" + + " return \"'\"+Double.valueOf(v)+'\\\'';\n"; + + execute("create function foo::pgfun1 ( input double ) returns text language java\n" + + "AS $$" + functionBody + "$$;"); + execute("CREATE FUNCTION foo::pgsin ( input double ) RETURNS double USING $$org.apache.cassandra.cql3.UFTest#sin$$"); + + assertRows(execute("SELECT language, body FROM system.schema_functions WHERE namespace='foo' AND name='pgfun1'"), + row("java", functionBody)); + } }