Skip to content
Browse files

CQL3 refactor to allow conversion function

patch by slebresne; reviewed by iamaleksey for CASSANDRA-5226
  • Loading branch information...
1 parent 1423fb1 commit 31e669ab425759b8a6e5c09a1d6635d013b838d9 @pcmanus pcmanus committed Jan 31, 2013
Showing with 3,560 additions and 2,516 deletions.
  1. +1 −0 CHANGES.txt
  2. +95 −54 doc/cql3/CQL.textile
  3. +82 −0 src/java/org/apache/cassandra/cql3/AbstractMarker.java
  4. +26 −0 src/java/org/apache/cassandra/cql3/AssignementTestable.java
  5. +23 −7 src/java/org/apache/cassandra/cql3/CFDefinition.java
  6. +2 −7 src/java/org/apache/cassandra/cql3/ColumnIdentifier.java
  7. +3 −4 src/java/org/apache/cassandra/cql3/ColumnNameBuilder.java
  8. +341 −0 src/java/org/apache/cassandra/cql3/Constants.java
  9. +162 −149 src/java/org/apache/cassandra/cql3/Cql.g
  10. +382 −0 src/java/org/apache/cassandra/cql3/Lists.java
  11. +271 −0 src/java/org/apache/cassandra/cql3/Maps.java
  12. +408 −0 src/java/org/apache/cassandra/cql3/Operation.java
  13. +4 −1 src/java/org/apache/cassandra/cql3/QueryProcessor.java
  14. +11 −27 src/java/org/apache/cassandra/cql3/Relation.java
  15. +6 −0 src/java/org/apache/cassandra/cql3/ResultSet.java
  16. +242 −0 src/java/org/apache/cassandra/cql3/Sets.java
  17. +72 −198 src/java/org/apache/cassandra/cql3/Term.java
  18. +59 −0 src/java/org/apache/cassandra/cql3/TypeCast.java
  19. +18 −1 src/java/org/apache/cassandra/cql3/UpdateParameters.java
  20. +68 −0 src/java/org/apache/cassandra/cql3/functions/AbstractFunction.java
  21. +70 −0 src/java/org/apache/cassandra/cql3/functions/BytesConversionFcts.java
  22. +42 −0 src/java/org/apache/cassandra/cql3/functions/Function.java
  23. +144 −0 src/java/org/apache/cassandra/cql3/functions/FunctionCall.java
  24. +185 −0 src/java/org/apache/cassandra/cql3/functions/Functions.java
  25. +75 −0 src/java/org/apache/cassandra/cql3/functions/TimeuuidFcts.java
  26. +71 −0 src/java/org/apache/cassandra/cql3/functions/TokenFct.java
  27. +0 −161 src/java/org/apache/cassandra/cql3/operations/ColumnOperation.java
  28. +0 −410 src/java/org/apache/cassandra/cql3/operations/ListOperation.java
  29. +0 −206 src/java/org/apache/cassandra/cql3/operations/MapOperation.java
  30. +0 −51 src/java/org/apache/cassandra/cql3/operations/Operation.java
  31. +0 −179 src/java/org/apache/cassandra/cql3/operations/PreparedOperation.java
  32. +0 −216 src/java/org/apache/cassandra/cql3/operations/SetOperation.java
  33. +26 −106 src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java
  34. +69 −0 src/java/org/apache/cassandra/cql3/statements/RawSelector.java
  35. +107 −249 src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
  36. +401 −0 src/java/org/apache/cassandra/cql3/statements/Selection.java
  37. +0 −181 src/java/org/apache/cassandra/cql3/statements/Selector.java
  38. +76 −101 src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java
  39. +2 −1 src/java/org/apache/cassandra/db/SystemTable.java
  40. +0 −7 src/java/org/apache/cassandra/db/marshal/AbstractType.java
  41. +0 −10 src/java/org/apache/cassandra/db/marshal/AsciiType.java
  42. +1 −10 src/java/org/apache/cassandra/db/marshal/BooleanType.java
  43. +0 −10 src/java/org/apache/cassandra/db/marshal/BytesType.java
  44. +8 −1 src/java/org/apache/cassandra/db/marshal/CollectionType.java
  45. +2 −10 src/java/org/apache/cassandra/db/marshal/CompositeType.java
  46. +1 −10 src/java/org/apache/cassandra/db/marshal/CounterColumnType.java
  47. +1 −10 src/java/org/apache/cassandra/db/marshal/DateType.java
  48. +0 −10 src/java/org/apache/cassandra/db/marshal/DecimalType.java
  49. +0 −10 src/java/org/apache/cassandra/db/marshal/DoubleType.java
  50. +0 −10 src/java/org/apache/cassandra/db/marshal/FloatType.java
  51. +0 −10 src/java/org/apache/cassandra/db/marshal/InetAddressType.java
  52. +0 −10 src/java/org/apache/cassandra/db/marshal/Int32Type.java
  53. +0 −10 src/java/org/apache/cassandra/db/marshal/IntegerType.java
  54. +0 −10 src/java/org/apache/cassandra/db/marshal/LexicalUUIDType.java
  55. +0 −10 src/java/org/apache/cassandra/db/marshal/LongType.java
  56. +1 −38 src/java/org/apache/cassandra/db/marshal/TimeUUIDType.java
  57. +1 −10 src/java/org/apache/cassandra/db/marshal/UTF8Type.java
  58. +0 −10 src/java/org/apache/cassandra/db/marshal/UUIDType.java
  59. +1 −1 src/java/org/apache/cassandra/dht/Murmur3Partitioner.java
View
1 CHANGES.txt
@@ -13,6 +13,7 @@
* Expose secondary indicies to the rest of nodetool (CASSANDRA-4464)
* Binary protocol: avoid sending notification for 0.0.0.0 (CASSANDRA-5227)
* add UseCondCardMark XX jvm settings on jdk 1.7 (CASSANDRA-4366)
+ * CQL3 refactor to allow conversion function (CASSANDRA-5226)
1.2.1
View
149 doc/cql3/CQL.textile
@@ -84,37 +84,45 @@ bc(syntax)..
<number> ::= <integer> | <float>
<uuid> ::= a uuid constant
<boolean> ::= a boolean constant
-
- <final-term> ::= <string>
- | <number>
- | <uuid>
- | <boolean>
- <term> ::= <final-term>
- | '?'
- <int-term> ::= <integer>
- | '?'
+ <hex> ::= a blob constant
+
+ <constant> ::= <string>
+ | <number>
+ | <uuid>
+ | <boolean>
+ | <hex>
+ <variable> ::= '?'
+ <term> ::= <constant>
+ | <collection-literal>
+ | <variable>
+ | <function> '(' (<term> (',' <term>)*)? ')'
<collection-literal> ::= <map-literal>
| <set-literal>
| <list-literal>
- <map-literal> ::= '{' ( <final-term> ':' <final-term> ( ',' <final-term> ':' <final-term> )* )? '}'
- <set-literal> ::= '{' ( <final-term> ( ',' <final-term> )* )? '}'
- <list-literal> ::= '[' ( <final-term> ( ',' <final-term> )* )? ']'
+ <map-literal> ::= '{' ( <term> ':' <term> ( ',' <term> ':' <term> )* )? '}'
+ <set-literal> ::= '{' ( <term> ( ',' <term> )* )? '}'
+ <list-literal> ::= '[' ( <term> ( ',' <term> )* )? ']'
+
+ <function> ::= <ident>
<properties> ::= <property> (AND <property>)*
- <property> ::= <identifier> '=' ( <value> | <map-literal> )
- <value> ::= <identifier> | <string> | <number> | <boolean>
+ <property> ::= <identifier> '=' ( <identifier> | <constant> | <map-literal> )
p.
-The question mark (@?@) in the syntax above is a bind variables for "prepared statements":#preparedStatement.
+Please note that not every possible productions of the grammar above will be valid in practice. Most notably, @<variable>@ and nested @<collection-literal>@ are currently not allowed inside @<collection-literal>@.
-The @<properties>@ production is use by statement that create and alter keyspaces and tables. Each @<property>@ is either a _simple_ one, in which case it just has a value, or a _map_ one, in which case it's value is a map grouping sub-options. The following will refer to one or the other as the _kind_ (_simple_ or _map_) of the property.
+p. The question mark (@?@) of @<variable>@ is a bind variables for "prepared statements":#preparedStatement.
+
+p. The @<properties>@ production is use by statement that create and alter keyspaces and tables. Each @<property>@ is either a _simple_ one, in which case it just has a value, or a _map_ one, in which case it's value is a map grouping sub-options. The following will refer to one or the other as the _kind_ (_simple_ or _map_) of the property.
p. A @<tablename>@ will be used to identify a table. This is an identifier representing the table name that can be preceded by a keyspace name. The keyspace name, if provided, allow to identify a table in another keyspace than the currently active one (the currently active keyspace is set through the <a href="#useStmt"><tt>USE</tt></a> statement).
+p. For supported @<function>@, see the section on "functions":#functions.
+
h3(#preparedStatement). Prepared Statement
-CQL supports _prepared statements_. Prepared statement is an optimization that allows to parse a query only once but execute it multiple times with differente concrete values.
+CQL supports _prepared statements_. Prepared statement is an optimization that allows to parse a query only once but execute it multiple times with different concrete values.
In a statement, each time a column value is expected (in the data manipulation and query statements), a bind variable marker (denoted by a @?@ symbol) can be used instead. A statement with bind variables must then be _prepared_. Once it has been prepared, it can executed by providing concrete values for the bind variables (values for bind variables must be provided in the order the bind variables are defined in the query string). The exact procedure to prepare a statement and execute a prepared statement depends on the CQL driver used and is beyond the scope of this document.
@@ -569,15 +577,16 @@ bc(syntax)..
( LIMIT <integer> )?
( ALLOW FILTERING )?
-<select-clause> ::= <column-list>
+<select-clause> ::= <selection-list>
| COUNT '(' ( '*' | '1' ) ')'
-<column-list> ::= <selected_id> ( ',' <selected_id> )*
- | '*'
+<selection-list> ::= <selector> ( ',' <selector> )*
+ | '*'
-<selected_id> ::= <identifier>
- | WRITETIME '(' <identifier> ')'
- | TTL '(' <identifier> ')'
+<selector> ::= <identifier>
+ | WRITETIME '(' <identifier> ')'
+ | TTL '(' <identifier> ')'
+ | <function> '(' (<selector> (',' <selector>)*)? ')'
<where-clause> ::= <relation> ( "AND" <relation> )*
@@ -605,9 +614,9 @@ The @SELECT@ statements reads one or more columns for one or more rows in a tabl
h4(#selectSelection). @<select-clause>@
-The @<select-clause>@ determines which columns needs to be queried and returned in the result-set. It consists of either the comma-separated list of column names to query, or the wildcard character (@*@) to select all the columns defined for the table.
+The @<select-clause>@ determines which columns needs to be queried and returned in the result-set. It consists of either the comma-separated list of <selector> or the wildcard character (@*@) to select all the columns defined for the table.
-In addition to selecting columns, the @WRITETIME@ (resp. @TTL@) function allows to select the timestamp of when the column was inserted (resp. the time to live (in seconds) for the column (or null if the column has no expiration set)).
+A @<selector>@ is either a column name to retrieve, or a @<function>@ of one or multiple column names. The functions allows are the same that for @<term>@ and are describe in the "function section":#function. In addition to these generic functions, the @WRITETIME@ (resp. @TTL@) function allows to select the timestamp of when the column was inserted (resp. the time to live (in seconds) for the column (or null if the column has no expiration set)).
The @COUNT@ keyword can be used with parenthesis enclosing @*@. If so, the query will return a single result: the number of rows matching the query. Note that @COUNT(1)@ is supported as an alias.
@@ -739,7 +748,7 @@ p. The following table gives additional informations on the native data types, a
|@int@ | integers |32-bit signed int|
|@text@ | strings |UTF8 encoded string|
|@timestamp@| integers, strings |A timestamp. Strings constant are allow to input timestamps as dates, see "Working with dates":#usingdates below for more information.|
-|@timeuuid@ | uuids |Type 1 UUID. This is generally used as a "conflict-free" timestamp. See "Working with @timeuuid@":#usingtimeuuid below.|
+|@timeuuid@ | uuids |Type 1 UUID. This is generally used as a "conflict-free" timestamp. Also see the "functions on Timeuuid":#timeuuidFun|
|@uuid@ | uuids |Type 1 or type 4 UUID|
|@varchar@ | strings |UTF8 encoded string|
|@varint@ | integers |Arbitrary-precision integer|
@@ -775,30 +784,6 @@ The time of day may also be omitted, if the date is the only piece that matters:
In that case, the time of day will default to 00:00:00, in the specified or default time zone.
-h3(#usingtimeuuid). Working with @timeuuid@
-
-Values of the @timeuuid@ type are type 1 "UUID":http://en.wikipedia.org/wiki/Universally_unique_identifier, i.e. UUID that include the timestamp of their generation, and they sort accordingly to said timestamp. They thus serve as conflict-free timestamps.
-
-Valid @timeuuid@ values should be inputed using UUID constants described "here":#constants. However, a number of convenience method are provided to interact with @timeuuid@.
-
-First, the method @now@ generates a new unique timeuuid (at the time where the statement using it is executed). Note that this method is useful for insertion but is largely non-sensical in @WHERE@ clauses. For instance, a query of the form
-
-bc(sample).
-SELECT * FROM myTable WHERE t = now()
-
-will never return any result by design, since the value returned by @now()@ is guaranteed to be unique.
-
-For querying, the method @minTimeuuid@ (resp. @maxTimeuuid@) takes a date @d@ in argument and returns a _fake_ @timeuuid@ corresponding to the _smallest_ (resp. _biggest_) possible @timeuuid@ having for date @d@. So for instance:
-
-bc(sample).
-SELECT * FROM myTable WHERE t > maxTimeuuid('2013-01-01 00:05+0000') AND t < minTimeuuid('2013-02-02 10:00+0000')
-
-will select all rows where the @timeuuid@ column @t@ is strictly older than '2013-01-01 00:05+0000' but stricly younger than '2013-02-02 10:00+0000'. Please note that @t >= maxTimeuuid('2013-01-01 00:05+0000')@ would still _not_ select a @timeuuid@ generated exactly at '2013-01-01 00:05+0000' and is essentially equivalent to @t > maxTimeuuid('2013-01-01 00:05+0000')@.
-
-_Warning_: We called the values generated by @minTimeuuid@ and @maxTimeuuid@ _fake_ UUID because they do no respect the Time-Based UUID generation process specified by the "RFC 4122":http://www.ietf.org/rfc/rfc4122.txt. In particular, the value returned by these 2 methods will not be unique. This means you should only use those methods for querying (as in the example above). Inserting the result of those methods is almost certainly _a bad idea_.
-
-Lastly, the @dateOf@ and @unixTimestampOf@ methods can used in @SELECT@ clauses to extract the timestamp of a @timeuuid@ column in a resultset. The difference between the @dateOf@ and @unixTimestampOf@ is that the former return the extract timestamp as a date, while the latter returns it as a raw timestamp (i.e. a 64 bits integer).
-
h3(#counters). Counters
@@ -913,6 +898,61 @@ UPDATE plays SET scores = scores - [ 12, 21 ] WHERE id = '123-afde'; // removes
As with "maps":#map, TTLs if used only apply to the newly inserted/updated _values_.
+h2(#functions). Functions
+
+CQL3 supports a few functions (more to come). Currently, it only support functions on values (functions that transform one or more column values into a new value) and in particular aggregation functions are not supported. The functions supported are described below:
+
+h3(#tokenFun). Token
+
+The @token@ function allows to compute the token for a given partition key. The exact signature of the token function depends on the table concerned and of the partitioner used by the cluster.
+
+The type of the arguments of the @token@ depend on the type of the partition key columns. The return type depend on the partitioner in use:
+* For Murmur3Partitioner, the return type is @bigint@.
+* For RandomPartitioner, the return type is @varint@.
+* For ByteOrderedPartitioner, the return type is @blob@.
+
+For instance, in a cluster using the default Murmur3Partitioner, if a table is defined by
+
+bc(sample).
+CREATE TABLE users (
+ userid text PRIMARY KEY,
+ username text,
+ ...
+)
+
+then the @token@ function will take a single argument of type @text@ (in that case, the partition key is @userid@ (there is no clustering key so the partition key is the same than the primary key)), and the return type will be @bigint@.
+
+h3(#timeuuidFun). Timeuuid functions
+
+h4. @now@
+
+The @now@ function takes no arguments and generates a new unique timeuuid (at the time where the statement using it is executed). Note that this method is useful for insertion but is largely non-sensical in @WHERE@ clauses. For instance, a query of the form
+
+bc(sample).
+SELECT * FROM myTable WHERE t = now()
+
+will never return any result by design, since the value returned by @now()@ is guaranteed to be unique.
+
+h4. @minTimeuuid@ and @maxTimeuuid@
+
+The @minTimeuuid@ (resp. @maxTimeuuid@) function takes a @timestamp@ value @t@ (which can be "either a timestamp or a date string":#usingdates) and return a _fake_ @timeuuid@ corresponding to the _smallest_ (resp. _biggest_) possible @timeuuid@ having for timestamp @t@. So for instance:
+
+bc(sample).
+SELECT * FROM myTable WHERE t > maxTimeuuid('2013-01-01 00:05+0000') AND t < minTimeuuid('2013-02-02 10:00+0000')
+
+will select all rows where the @timeuuid@ column @t@ is strictly older than '2013-01-01 00:05+0000' but stricly younger than '2013-02-02 10:00+0000'. Please note that @t >= maxTimeuuid('2013-01-01 00:05+0000')@ would still _not_ select a @timeuuid@ generated exactly at '2013-01-01 00:05+0000' and is essentially equivalent to @t > maxTimeuuid('2013-01-01 00:05+0000')@.
+
+_Warning_: We called the values generated by @minTimeuuid@ and @maxTimeuuid@ _fake_ UUID because they do no respect the Time-Based UUID generation process specified by the "RFC 4122":http://www.ietf.org/rfc/rfc4122.txt. In particular, the value returned by these 2 methods will not be unique. This means you should only use those methods for querying (as in the example above). Inserting the result of those methods is almost certainly _a bad idea_.
+
+h4. @dateOf@ and @unixTimestampOf@
+
+The @dateOf@ and @unixTimestampOf@ functions take a @timeuuid@ argument and extract the embeded timestamp. However, while the @dateof@ function return it with the @timestamp@ type (that most client, including cqlsh, interpret as a date), the @unixTimestampOf@ function returns it as a @bigint@ raw value.
+
+h3(#blobFun). Blob conversion functions
+
+A number of functions are provided to "convert" the native types into binary data (@blob@). For every @<native-type>@ @type@ supported by CQL3 (a notable exceptions is @blob@, for obvious reasons), the function @typeAsBlob@ takes a argument of type @type@ and return it as a @blob@. Conversely, the function @blobAsType@ takes a 64-bit @blob@ argument and convert it to a @bigint@ value. And so for instance, @bigintAsBlob(3)@ is @0x0000000000000003@ and @blobAsBigint(0x0000000000000003)@ is @3@.
+
+
h2(#appendixA). Appendix A: CQL Keywords
CQL distinguishes between _reserved_ and _non-reserved_ keywords. Reserved keywords cannot be used as identifier, they are truly reserved for the language (but one can enclose a reserved keyword by double-quotes to use it as an identifier). Non-reserved keywords however only have a specific meaning in certain context but can used as identifer otherwise. The only _raison d'être_ of these non-reserved keywords is convenience: some keyword are non-reserved when it was always easy for the parser to decide whether they were used as keywords or not.
@@ -1007,13 +1047,14 @@ The following describes the addition/changes brought for each version of CQL.
h3. 3.0.2
-- Type validation for the "constants":#constants has been fixed. For instance, the implementation used to allow @'2'@ as a valid value for an @int@ column (interpreting it has the equivalent of @2@), or @42@ as a valid @blob@ value (in which case @42@ was interpreted as an hexadecimal representation of the blob). This is no longer the case, type validation of constants is now more strict. See the "data types":#dataTypes section for details on which constant is allowed for which type.
-- The type validation fixed of the previous point has lead to the introduction of "blobs constants":#constants to allow inputing blobs. Do note that while inputing blobs as strings constant is still supported by this version (to allow smoother transition to blob constant), it is now deprecated (in particular the "data types":#dataTypes section does not list strings constants as valid blobs) and will be removed by a future version. If you were using strings as blobs, you should thus update your client code asap to switch blob constants.
+* Type validation for the "constants":#constants has been fixed. For instance, the implementation used to allow @'2'@ as a valid value for an @int@ column (interpreting it has the equivalent of @2@), or @42@ as a valid @blob@ value (in which case @42@ was interpreted as an hexadecimal representation of the blob). This is no longer the case, type validation of constants is now more strict. See the "data types":#types section for details on which constant is allowed for which type.
+* The type validation fixed of the previous point has lead to the introduction of "blobs constants":#constants to allow inputing blobs. Do note that while inputing blobs as strings constant is still supported by this version (to allow smoother transition to blob constant), it is now deprecated (in particular the "data types":#types section does not list strings constants as valid blobs) and will be removed by a future version. If you were using strings as blobs, you should thus update your client code asap to switch blob constants.
+* A number of functions to convert native types to blobs have also been introduced. Furthermore the token function is now also allowed in select clauses. See the "section on functions":#functions for details.
h3. 3.0.1
-- "Date strings":#usingdates (and timestamps) are no longer accepted as valid @timeuuid@ values. Doing so was a bug in the sense that date string are not valid @timeuuid@, and it was thus resulting in "confusing behaviors":https://issues.apache.org/jira/browse/CASSANDRA-4936. However, the following new methods have been added to help working with @timeuuid@: @now@, @minTimeuuid@, @maxTimeuuid@ , @dateOf@ and @unixTimestampOf@. See the "section dedicated to these methods":#usingtimeuuid for more detail.
-- "Float constants"#constants now support the exponent notation. In other words, @4.2E10@ is now a valid floating point value.
+* "Date strings":#usingdates (and timestamps) are no longer accepted as valid @timeuuid@ values. Doing so was a bug in the sense that date string are not valid @timeuuid@, and it was thus resulting in "confusing behaviors":https://issues.apache.org/jira/browse/CASSANDRA-4936. However, the following new methods have been added to help working with @timeuuid@: @now@, @minTimeuuid@, @maxTimeuuid@ , @dateOf@ and @unixTimestampOf@. See the "section dedicated to these methods":#usingtimeuuid for more detail.
+* "Float constants"#constants now support the exponent notation. In other words, @4.2E10@ is now a valid floating point value.
h2. Versioning
View
82 src/java/org/apache/cassandra/cql3/AbstractMarker.java
@@ -0,0 +1,82 @@
+/*
+ * 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 java.nio.ByteBuffer;
+
+import org.apache.cassandra.db.marshal.CollectionType;
+import org.apache.cassandra.exceptions.InvalidRequestException;
+
+
+/**
+ * A single bind marker.
+ */
+public abstract class AbstractMarker extends Term.NonTerminal
+{
+ protected final int bindIndex;
+ protected final ColumnSpecification receiver;
+
+ protected AbstractMarker(int bindIndex, ColumnSpecification receiver)
+ {
+ this.bindIndex = bindIndex;
+ this.receiver = receiver;
+ }
+
+ public void collectMarkerSpecification(ColumnSpecification[] boundNames)
+ {
+ boundNames[bindIndex] = receiver;
+ }
+
+ /**
+ * A parsed, but non prepared, bind marker.
+ */
+ public static class Raw implements Term.Raw
+ {
+ protected final int bindIndex;
+
+ public Raw(int bindIndex)
+ {
+ this.bindIndex = bindIndex;
+ }
+
+ public AbstractMarker prepare(ColumnSpecification receiver) throws InvalidRequestException
+ {
+ if (!(receiver.type instanceof CollectionType))
+ return new Constants.Marker(bindIndex, receiver);
+
+ switch (((CollectionType)receiver.type).kind)
+ {
+ case LIST: return new Lists.Marker(bindIndex, receiver);
+ case SET: return new Sets.Marker(bindIndex, receiver);
+ case MAP: return new Maps.Marker(bindIndex, receiver);
+ }
+ throw new AssertionError();
+ }
+
+ public boolean isAssignableTo(ColumnSpecification receiver)
+ {
+ return true;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "?";
+ }
+ }
+}
View
26 src/java/org/apache/cassandra/cql3/AssignementTestable.java
@@ -0,0 +1,26 @@
+/*
+ * 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;
+
+public interface AssignementTestable
+{
+ /**
+ * @return whether this object can be assigned to the provided receiver
+ */
+ public boolean isAssignableTo(ColumnSpecification receiver);
+}
View
30 src/java/org/apache/cassandra/cql3/CFDefinition.java
@@ -21,6 +21,7 @@
import java.util.*;
import com.google.common.base.Joiner;
+import com.google.common.base.Objects;
import com.google.common.collect.AbstractIterator;
import org.apache.cassandra.config.CFMetaData;
@@ -293,6 +294,26 @@ private Name(String ksName, String cfName, ColumnIdentifier name, Kind kind, int
public final Kind kind;
public final int position; // only make sense for KEY_ALIAS and COLUMN_ALIAS
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if(!(o instanceof Name))
+ return false;
+ Name that = (Name)o;
+ return Objects.equal(ksName, that.ksName)
+ && Objects.equal(cfName, that.cfName)
+ && Objects.equal(name, that.name)
+ && Objects.equal(type, that.type)
+ && kind == that.kind
+ && position == that.position;
+ }
+
+ @Override
+ public final int hashCode()
+ {
+ return Objects.hashCode(ksName, cfName, name, type, kind, position);
+ }
}
@Override
@@ -329,14 +350,9 @@ public NonCompositeBuilder add(ByteBuffer bb)
return this;
}
- public NonCompositeBuilder add(Term t, Relation.Type op, List<ByteBuffer> variables) throws InvalidRequestException
+ public NonCompositeBuilder add(ByteBuffer bb, Relation.Type op)
{
- if (columnName != null)
- throw new IllegalStateException("Column name is already constructed");
-
- // We don't support the relation type yet, i.e., there is no distinction between x > 3 and x >= 3.
- columnName = t.getByteBuffer(type, variables);
- return this;
+ return add(bb);
}
public int componentCount()
View
9 src/java/org/apache/cassandra/cql3/ColumnIdentifier.java
@@ -23,12 +23,12 @@
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.utils.ByteBufferUtil;
-import org.apache.cassandra.cql3.statements.Selector;
+import org.apache.cassandra.cql3.statements.RawSelector;
/**
* Represents an identifer for a CQL column definition.
*/
-public class ColumnIdentifier extends Selector implements Comparable<ColumnIdentifier>
+public class ColumnIdentifier implements RawSelector, Comparable<ColumnIdentifier>
{
public final ByteBuffer key;
private final String text;
@@ -70,9 +70,4 @@ public int compareTo(ColumnIdentifier other)
{
return key.compareTo(other.key);
}
-
- public ColumnIdentifier id()
- {
- return this;
- }
}
View
7 src/java/org/apache/cassandra/cql3/ColumnNameBuilder.java
@@ -36,14 +36,13 @@
public ColumnNameBuilder add(ByteBuffer bb);
/**
- * Add a new Term as the next component for this name.
- * @param t the Term to add
+ * Add a new ByteBuffer as the next component for this name.
+ * @param bb the ByteBuffer to add
* @param op the relationship this component should respect.
- * @param variables the variables corresponding to prepared markers
* @throws IllegalStateException if the builder if full, i.e. if enough component has been added.
* @return this builder
*/
- public ColumnNameBuilder add(Term t, Relation.Type op, List<ByteBuffer> variables) throws InvalidRequestException;
+ public ColumnNameBuilder add(ByteBuffer t, Relation.Type op);
/**
* Returns the number of component already added to this builder.
View
341 src/java/org/apache/cassandra/cql3/Constants.java
@@ -0,0 +1,341 @@
+/*
+ * 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 java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import com.google.common.base.Objects;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.db.ColumnFamily;
+import org.apache.cassandra.db.filter.QueryPath;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.BytesType;
+import org.apache.cassandra.db.marshal.CollectionType;
+import org.apache.cassandra.db.marshal.CounterColumnType;
+import org.apache.cassandra.db.marshal.LongType;
+import org.apache.cassandra.db.marshal.MarshalException;
+import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.utils.ByteBufferUtil;
+
+/**
+ * Static helper methods and classes for constants.
+ */
+public abstract class Constants
+{
+ private static final Logger logger = LoggerFactory.getLogger(Constants.class);
+
+ public enum Type
+ {
+ STRING, INTEGER, UUID, FLOAT, BOOLEAN, HEX;
+ }
+
+ public static class Literal implements Term.Raw
+ {
+ private final Type type;
+ private final String text;
+
+ // For transition post-5198, see below
+ private static volatile boolean stringAsBlobWarningLogged = false;
+
+ private Literal(Type type, String text)
+ {
+ assert type != null && text != null;
+ this.type = type;
+ this.text = text;
+ }
+
+ public static Literal string(String text)
+ {
+ return new Literal(Type.STRING, text);
+ }
+
+ public static Literal integer(String text)
+ {
+ return new Literal(Type.INTEGER, text);
+ }
+
+ public static Literal floatingPoint(String text)
+ {
+ return new Literal(Type.FLOAT, text);
+ }
+
+ public static Literal uuid(String text)
+ {
+ return new Literal(Type.UUID, text);
+ }
+
+ public static Literal bool(String text)
+ {
+ return new Literal(Type.BOOLEAN, text);
+ }
+
+ public static Literal hex(String text)
+ {
+ return new Literal(Type.HEX, text);
+ }
+
+ public Value prepare(ColumnSpecification receiver) throws InvalidRequestException
+ {
+ if (!isAssignableTo(receiver))
+ throw new InvalidRequestException(String.format("Invalid %s constant (%s) for %s of type %s", type, text, receiver, receiver.type.asCQL3Type()));
+
+ return new Value(parsedValue(receiver.type));
+ }
+
+ private ByteBuffer parsedValue(AbstractType<?> validator) throws InvalidRequestException
+ {
+ try
+ {
+ // BytesType doesn't want it's input prefixed by '0x'.
+ if (type == Type.HEX && validator instanceof BytesType)
+ return validator.fromString(text.substring(2));
+ if (validator instanceof CounterColumnType)
+ return LongType.instance.fromString(text);
+ return validator.fromString(text);
+ }
+ catch (MarshalException e)
+ {
+ throw new InvalidRequestException(e.getMessage());
+ }
+ }
+
+ public String getRawText()
+ {
+ return text;
+ }
+
+ public boolean isAssignableTo(ColumnSpecification receiver)
+ {
+ CQL3Type receiverType = receiver.type.asCQL3Type();
+ if (receiverType.isCollection())
+ return false;
+
+ if (!(receiverType instanceof CQL3Type.Native))
+ // Skip type validation for custom types. May or may not be a good idea
+ return true;
+
+ CQL3Type.Native nt = (CQL3Type.Native)receiverType;
+ switch (type)
+ {
+ case STRING:
+ switch (nt)
+ {
+ case ASCII:
+ case TEXT:
+ case INET:
+ case VARCHAR:
+ case TIMESTAMP:
+ return true;
+ case BLOB:
+ // Blobs should now be inputed as hexadecimal constants. However, to allow people to upgrade, we still allow
+ // blob-as-strings, even though it is deprecated (see #5198).
+ if (!stringAsBlobWarningLogged)
+ {
+ stringAsBlobWarningLogged = true;
+ logger.warn("Inputing CLQ3 blobs as strings (like {} = '{}') is now deprecated and will be removed in a future version. "
+ + "You should convert client code to use a blob constant ({} = {}) instead (see http://cassandra.apache.org/doc/cql3/CQL.html "
+ + "changelog section for more info).",
+ new Object[]{receiver, text, receiver, "0x" + text});
+ }
+ return true;
+ }
+ return false;
+ case INTEGER:
+ switch (nt)
+ {
+ case BIGINT:
+ case COUNTER:
+ case DECIMAL:
+ case DOUBLE:
+ case FLOAT:
+ case INT:
+ case TIMESTAMP:
+ case VARINT:
+ return true;
+ }
+ return false;
+ case UUID:
+ switch (nt)
+ {
+ case UUID:
+ case TIMEUUID:
+ return true;
+ }
+ return false;
+ case FLOAT:
+ switch (nt)
+ {
+ case DECIMAL:
+ case DOUBLE:
+ case FLOAT:
+ return true;
+ }
+ return false;
+ case BOOLEAN:
+ switch (nt)
+ {
+ case BOOLEAN:
+ return true;
+ }
+ return false;
+ case HEX:
+ switch (nt)
+ {
+ case BLOB:
+ return true;
+ }
+ return false;
+ }
+ return false;
+ }
+
+ @Override
+ public String toString()
+ {
+ return type == Type.STRING ? String.format("'%s'", text) : text;
+ }
+ }
+
+ /**
+ * A constant value, i.e. a ByteBuffer.
+ */
+ public static class Value extends Term.Terminal
+ {
+ public final ByteBuffer bytes;
+
+ public Value(ByteBuffer bytes)
+ {
+ this.bytes = bytes;
+ }
+
+ public ByteBuffer get()
+ {
+ return bytes;
+ }
+
+ @Override
+ public ByteBuffer bindAndGet(List<ByteBuffer> values)
+ {
+ return bytes;
+ }
+ }
+
+ public static class Marker extends AbstractMarker
+ {
+ protected Marker(int bindIndex, ColumnSpecification receiver)
+ {
+ super(bindIndex, receiver);
+ assert !(receiver.type instanceof CollectionType);
+ }
+
+ @Override
+ public ByteBuffer bindAndGet(List<ByteBuffer> values) throws InvalidRequestException
+ {
+ try
+ {
+ ByteBuffer value = values.get(bindIndex);
+ receiver.type.validate(value);
+ return value;
+ }
+ catch (MarshalException e)
+ {
+ throw new InvalidRequestException(e.getMessage());
+ }
+ }
+
+ public Value bind(List<ByteBuffer> values) throws InvalidRequestException
+ {
+ return new Constants.Value(bindAndGet(values));
+ }
+ }
+
+ public static class Setter extends Operation
+ {
+ public Setter(ColumnIdentifier column, Term t)
+ {
+ super(column, t);
+ }
+
+ public void execute(ByteBuffer rowKey, ColumnFamily cf, ColumnNameBuilder prefix, UpdateParameters params) throws InvalidRequestException
+ {
+ ByteBuffer cname = columnName == null ? prefix.build() : prefix.add(columnName.key).build();
+ cf.addColumn(params.makeColumn(cname, t.bindAndGet(params.variables)));
+ }
+ }
+
+ public static class Adder extends Operation
+ {
+ public Adder(ColumnIdentifier column, Term t)
+ {
+ super(column, t);
+ }
+
+ public void execute(ByteBuffer rowKey, ColumnFamily cf, ColumnNameBuilder prefix, UpdateParameters params) throws InvalidRequestException
+ {
+ long increment = ByteBufferUtil.toLong(t.bindAndGet(params.variables));
+ ByteBuffer cname = columnName == null ? prefix.build() : prefix.add(columnName.key).build();
+ cf.addCounter(new QueryPath(cf.metadata().cfName, null, cname), increment);
+ }
+ }
+
+ public static class Substracter extends Operation
+ {
+ public Substracter(ColumnIdentifier column, Term t)
+ {
+ super(column, t);
+ }
+
+ public void execute(ByteBuffer rowKey, ColumnFamily cf, ColumnNameBuilder prefix, UpdateParameters params) throws InvalidRequestException
+ {
+ long increment = ByteBufferUtil.toLong(t.bindAndGet(params.variables));
+ if (increment == Long.MIN_VALUE)
+ throw new InvalidRequestException("The negation of " + increment + " overflows supported counter precision (signed 8 bytes integer)");
+
+ ByteBuffer cname = columnName == null ? prefix.build() : prefix.add(columnName.key).build();
+ cf.addCounter(new QueryPath(cf.metadata().cfName, null, cname), -increment);
+ }
+ }
+
+ // This happens to also handle collection because it doesn't felt worth
+ // duplicating this further
+ public static class Deleter extends Operation
+ {
+ private final boolean isCollection;
+
+ public Deleter(ColumnIdentifier column, boolean isCollection)
+ {
+ super(column, null);
+ this.isCollection = isCollection;
+ }
+
+ public void execute(ByteBuffer rowKey, ColumnFamily cf, ColumnNameBuilder prefix, UpdateParameters params) throws InvalidRequestException
+ {
+ ColumnNameBuilder column = prefix.add(columnName.key);
+
+ if (isCollection)
+ cf.addAtom(params.makeRangeTombstone(column.build(), column.buildAsEndOfRange()));
+ else
+ cf.addColumn(params.makeTombstone(column.build()));
+ }
+ };
+}
View
311 src/java/org/apache/cassandra/cql3/Cql.g
@@ -39,8 +39,9 @@ options {
import org.apache.cassandra.auth.Permission;
import org.apache.cassandra.auth.DataResource;
import org.apache.cassandra.auth.IResource;
- import org.apache.cassandra.cql3.operations.*;
+ import org.apache.cassandra.cql3.*;
import org.apache.cassandra.cql3.statements.*;
+ import org.apache.cassandra.cql3.functions.FunctionCall;
import org.apache.cassandra.db.marshal.CollectionType;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.exceptions.InvalidRequestException;
@@ -75,32 +76,47 @@ options {
throw new SyntaxException(recognitionErrors.get((recognitionErrors.size()-1)));
}
- // used by UPDATE of the counter columns to validate if '-' was supplied by user
- public void validateMinusSupplied(Object op, final Term value, IntStream stream) throws MissingTokenException
+ public Map<String, String> convertPropertyMap(Maps.Literal map)
{
- if (op == null && Long.parseLong(value.getText()) > 0)
- throw new MissingTokenException(102, stream, value);
- }
-
- public Map<String, String> convertMap(Map<Term, Term> terms)
- {
- if (terms == null || terms.isEmpty())
+ if (map == null || map.entries == null || map.entries.isEmpty())
return Collections.<String, String>emptyMap();
- Map<String, String> res = new HashMap<String, String>(terms.size());
+ Map<String, String> res = new HashMap<String, String>(map.entries.size());
- for (Map.Entry<Term, Term> entry : terms.entrySet())
+ for (Pair<Term.Raw, Term.Raw> entry : map.entries)
{
// Because the parser tries to be smart and recover on error (to
// allow displaying more than one error I suppose), we have null
// entries in there. Just skip those, a proper error will be thrown in the end.
- if (entry.getKey() == null || entry.getValue() == null)
+ if (entry.left == null || entry.right == null)
+ break;
+
+ if (!(entry.left instanceof Constants.Literal))
+ {
+ addRecognitionError("Invalid property name: " + entry.left);
+ break;
+ }
+ if (!(entry.right instanceof Constants.Literal))
+ {
+ addRecognitionError("Invalid property value: " + entry.right);
break;
- res.put(entry.getKey().getText(), entry.getValue().getText());
+ }
+
+ res.put(((Constants.Literal)entry.left).getRawText(), ((Constants.Literal)entry.right).getRawText());
}
return res;
}
+
+ public void addRawUpdate(List<Pair<ColumnIdentifier, Operation.RawUpdate>> operations, ColumnIdentifier key, Operation.RawUpdate update)
+ {
+ for (Pair<ColumnIdentifier, Operation.RawUpdate> p : operations)
+ {
+ if (p.left.equals(key) && !p.right.isCompatibleWith(update))
+ addRecognitionError("Multiple incompatible setting of column " + key);
+ }
+ operations.add(Pair.create(key, update));
+ }
}
@lexer::header {
@@ -214,22 +230,28 @@ selectStatement returns [SelectStatement.RawStatement expr]
}
;
-selectClause returns [List<Selector> expr]
- : t1=selector { $expr = new ArrayList<Selector>(); $expr.add(t1); } (',' tN=selector { $expr.add(tN); })*
- | '\*' { $expr = Collections.<Selector>emptyList();}
+selectClause returns [List<RawSelector> expr]
+ : t1=selector { $expr = new ArrayList<RawSelector>(); $expr.add(t1); } (',' tN=selector { $expr.add(tN); })*
+ | '\*' { $expr = Collections.<RawSelector>emptyList();}
+ ;
+
+selectionFunctionArgs returns [List<RawSelector> a]
+ : '(' ')' { $a = Collections.emptyList(); }
+ | '(' s1=selector { List<RawSelector> args = new ArrayList<RawSelector>(); args.add(s1); }
+ ( ',' sn=selector { args.add(sn); } )*
+ ')' { $a = args; }
;
-selector returns [Selector s]
- : c=cident { $s = c; }
- | K_WRITETIME '(' c=cident ')' { $s = new Selector.WithFunction(c, Selector.Function.WRITE_TIME); }
- | K_TTL '(' c=cident ')' { $s = new Selector.WithFunction(c, Selector.Function.TTL); }
- | K_DATE_OF '(' c=cident ')' { $s = new Selector.WithFunction(c, Selector.Function.DATE_OF); }
- | K_UNIXTIMESTAMP_OF '(' c=cident ')' { $s = new Selector.WithFunction(c, Selector.Function.UNIXTIMESTAMP_OF); }
+selector returns [RawSelector s]
+ : c=cident { $s = c; }
+ | K_WRITETIME '(' c=cident ')' { $s = new RawSelector.WritetimeOrTTL(c, true); }
+ | K_TTL '(' c=cident ')' { $s = new RawSelector.WritetimeOrTTL(c, false); }
+ | f=functionName args=selectionFunctionArgs { $s = new RawSelector.WithFunction(f, args); }
;
-selectCountClause returns [List<Selector> expr]
- : '\*' { $expr = Collections.<Selector>emptyList();}
- | i=INTEGER { if (!i.getText().equals("1")) addRecognitionError("Only COUNT(1) is supported, got COUNT(" + i.getText() + ")"); $expr = Collections.<Selector>emptyList();}
+selectCountClause returns [List<RawSelector> expr]
+ : '\*' { $expr = Collections.<RawSelector>emptyList();}
+ | i=INTEGER { if (!i.getText().equals("1")) addRecognitionError("Only COUNT(1) is supported, got COUNT(" + i.getText() + ")"); $expr = Collections.<RawSelector>emptyList();}
;
whereClause returns [List<Relation> clause]
@@ -255,15 +277,15 @@ insertStatement returns [UpdateStatement expr]
@init {
Attributes attrs = new Attributes();
List<ColumnIdentifier> columnNames = new ArrayList<ColumnIdentifier>();
- List<Operation> columnOperations = new ArrayList<Operation>();
+ List<Term.Raw> values = new ArrayList<Term.Raw>();
}
: K_INSERT K_INTO cf=columnFamilyName
'(' c1=cident { columnNames.add(c1); } ( ',' cn=cident { columnNames.add(cn); } )* ')'
K_VALUES
- '(' v1=set_operation { columnOperations.add(v1); } ( ',' vn=set_operation { columnOperations.add(vn); } )* ')'
+ '(' v1=term { values.add(v1); } ( ',' vn=term { values.add(vn); } )* ')'
( usingClause[attrs] )?
{
- $expr = new UpdateStatement(cf, attrs, columnNames, columnOperations);
+ $expr = new UpdateStatement(cf, attrs, columnNames, values);
}
;
@@ -293,14 +315,14 @@ usingClauseObjective[Attributes attrs]
updateStatement returns [UpdateStatement expr]
@init {
Attributes attrs = new Attributes();
- List<Pair<ColumnIdentifier, Operation>> columns = new ArrayList<Pair<ColumnIdentifier, Operation>>();
+ List<Pair<ColumnIdentifier, Operation.RawUpdate>> operations = new ArrayList<Pair<ColumnIdentifier, Operation.RawUpdate>>();
}
: K_UPDATE cf=columnFamilyName
( usingClause[attrs] )?
- K_SET termPairWithOperation[columns] (',' termPairWithOperation[columns])*
+ K_SET columnOperation[operations] (',' columnOperation[operations])*
K_WHERE wclause=whereClause
{
- return new UpdateStatement(cf, columns, wclause, attrs);
+ return new UpdateStatement(cf, operations, wclause, attrs);
}
;
@@ -313,24 +335,26 @@ updateStatement returns [UpdateStatement expr]
deleteStatement returns [DeleteStatement expr]
@init {
Attributes attrs = new Attributes();
- List<Selector> columnsList = Collections.emptyList();
+ List<Operation.RawDeletion> columnDeletions = Collections.emptyList();
}
- : K_DELETE ( ids=deleteSelection { columnsList = ids; } )?
+ : K_DELETE ( dels=deleteSelection { columnDeletions = dels; } )?
K_FROM cf=columnFamilyName
( usingClauseDelete[attrs] )?
K_WHERE wclause=whereClause
{
- return new DeleteStatement(cf, columnsList, wclause, attrs);
+ return new DeleteStatement(cf, columnDeletions, wclause, attrs);
}
;
-deleteSelection returns [List<Selector> expr]
- : t1=deleteSelector { $expr = new ArrayList<Selector>(); $expr.add(t1); } (',' tN=deleteSelector { $expr.add(tN); })*
+deleteSelection returns [List<Operation.RawDeletion> operations]
+ : { $operations = new ArrayList<Operation.RawDeletion>(); }
+ t1=deleteOp { $operations.add(t1); }
+ (',' tN=deleteOp { $operations.add(tN); })*
;
-deleteSelector returns [Selector s]
- : c=cident { $s = c; }
- | c=cident '[' t=term ']' { $s = new Selector.WithKey(c, t); }
+deleteOp returns [Operation.RawDeletion op]
+ : c=cident { $op = new Operation.ColumnDeletion(c); }
+ | c=cident '[' t=term ']' { $op = new Operation.ElementDeletion(c, t); }
;
/**
@@ -638,97 +662,102 @@ cfOrKsName[CFName name, boolean isKs]
| k=unreserved_keyword { if (isKs) $name.setKeyspace(k, false); else $name.setColumnFamily(k, false); }
;
-set_operation returns [Operation op]
- : t=finalTerm { $op = ColumnOperation.Set(t); }
- | mk=QMARK { $op = new PreparedOperation(new Term($mk.text, $mk.type, ++currentBindMarkerIdx), PreparedOperation.Kind.SET); }
- | m=map_literal { $op = MapOperation.Set(m); }
- | l=list_literal { $op = ListOperation.Set(l); }
- | s=set_literal { $op = SetOperation.Set(s); }
+constant returns [Constants.Literal constant]
+ : t=STRING_LITERAL { $constant = Constants.Literal.string($t.text); }
+ | t=INTEGER { $constant = Constants.Literal.integer($t.text); }
+ | t=FLOAT { $constant = Constants.Literal.floatingPoint($t.text); }
+ | t=BOOLEAN { $constant = Constants.Literal.bool($t.text); }
+ | t=UUID { $constant = Constants.Literal.uuid($t.text); }
+ | t=HEXNUMBER { $constant = Constants.Literal.hex($t.text); }
;
-list_literal returns [List<Term> value]
- : '[' { List<Term> l = new ArrayList<Term>(); } ( t1=finalTerm { l.add(t1); } ( ',' tn=finalTerm { l.add(tn); } )* )? ']' { $value = l; }
+set_tail[List<Term.Raw> s]
+ : '}'
+ | ',' t=term { s.add(t); } set_tail[s]
;
-set_literal returns [List<Term> value]
- : '{' { List<Term> s = new ArrayList<Term>(); } ( t1=finalTerm { s.add(t1); } ( ',' tn=finalTerm { s.add(tn); } )* )? '}' { $value = s; }
+map_tail[List<Pair<Term.Raw, Term.Raw>> m]
+ : '}'
+ | ',' k=term ':' v=term { m.add(Pair.create(k, v)); } map_tail[m]
;
-map_literal returns [Map<Term, Term> value]
- // Note that we have an ambiguity between maps and set for "{}". So we force it to a set, and deal with it later based on the type of the column
- : '{' { Map<Term, Term> m = new HashMap<Term, Term>(); }
- k1=finalTerm ':' v1=finalTerm { m.put(k1, v1); } ( ',' kn=finalTerm ':' vn=finalTerm { m.put(kn, vn); } )* '}'
- { $value = m; }
+map_literal returns [Maps.Literal map]
+ : '{' '}' { $map = new Maps.Literal(Collections.<Pair<Term.Raw, Term.Raw>>emptyList()); }
+ | '{' { List<Pair<Term.Raw, Term.Raw>> m = new ArrayList<Pair<Term.Raw, Term.Raw>>(); }
+ k1=term ':' v1=term { m.add(Pair.create(k1, v1)); } map_tail[m]
+ { $map = new Maps.Literal(m); }
;
-finalTerm returns [Term term]
- : t=(STRING_LITERAL | UUID | INTEGER | FLOAT | BOOLEAN | HEXNUMBER ) { $term = new Term($t.text, $t.type); }
- | f=(K_MIN_TIMEUUID | K_MAX_TIMEUUID | K_NOW) '(' (v=(STRING_LITERAL | INTEGER))? ')' { $term = new Term($f.text + "(" + ($v == null ? "" : $v.text) + ")", Term.Type.UUID, true); }
+set_or_map[Term.Raw t] returns [Term.Raw value]
+ : ':' v=term { List<Pair<Term.Raw, Term.Raw>> m = new ArrayList<Pair<Term.Raw, Term.Raw>>(); m.add(Pair.create(t, v)); } map_tail[m] { $value = new Maps.Literal(m); }
+ | { List<Term.Raw> s = new ArrayList<Term.Raw>(); s.add(t); } set_tail[s] { $value = new Sets.Literal(s); }
;
-term returns [Term term]
- : ft=finalTerm { $term = ft; }
- | t=QMARK { $term = new Term($t.text, $t.type, ++currentBindMarkerIdx); }
+// This is a bit convoluted but that's because I haven't found a much better to have antl disambiguate between sets and maps
+collection_literal returns [Term.Raw value]
+ : '[' { List<Term.Raw> l = new ArrayList<Term.Raw>(); } ( t1=term { l.add(t1); } ( ',' tn=term { l.add(tn); } )* )? ']' { $value = new Lists.Literal(l); }
+ | '{' t=term v=set_or_map[t] { $value = v; }
+ // Note that we have an ambiguity between maps and set for "{}". So we force it to a set literal, and deal with it later based on the type of the column (SetLiteral.java).
+ | '{' '}' { $value = new Sets.Literal(Collections.<Term.Raw>emptyList()); }
;
-termPairWithOperation[List<Pair<ColumnIdentifier, Operation>> columns]
- : key=cident '='
- ( set_op=set_operation { columns.add(Pair.<ColumnIdentifier, Operation>create(key, set_op)); }
- | c=cident op=operation
- {
- if (!key.equals(c))
- addRecognitionError("Only expressions like X = X <op> <value> are supported.");
- columns.add(Pair.<ColumnIdentifier, Operation>create(key, op));
- }
- | ll=list_literal '+' c=cident
+value returns [Term.Raw value]
+ : c=constant { $value = c; }
+ | l=collection_literal { $value = l; }
+ | QMARK { $value = new AbstractMarker.Raw(++currentBindMarkerIdx); }
+ ;
+
+functionName returns [String s]
+ : f=IDENT { $s = $f.text; }
+ | u=unreserved_function_keyword { $s = u; }
+ | K_TOKEN { $s = "token"; }
+ ;
+
+functionArgs returns [List<Term.Raw> a]
+ : '(' ')' { $a = Collections.emptyList(); }
+ | '(' t1=term { List<Term.Raw> args = new ArrayList<Term.Raw>(); args.add(t1); }
+ ( ',' tn=term { args.add(tn); } )*
+ ')' { $a = args; }
+ ;
+
+term returns [Term.Raw term]
+ : v=value { $term = v; }
+ | f=functionName args=functionArgs { $term = new FunctionCall.Raw(f, args); }
+ | '(' c=comparatorType ')' t=term { $term = new TypeCast(c, t); }
+ ;
+
+columnOperation[List<Pair<ColumnIdentifier, Operation.RawUpdate>> operations]
+ : key=cident '=' t=term ('+' c=cident )?
+ {
+ if (c == null)
{
- if (!key.equals(c))
- addRecognitionError("Only expressions like X = <value> + X are supported.");
- columns.add(Pair.<ColumnIdentifier, Operation>create(key, ListOperation.Prepend(ll)));
+ addRawUpdate(operations, key, new Operation.SetValue(t));
}
- | mk=QMARK '+' c=cident
+ else
{
if (!key.equals(c))
- addRecognitionError("Only expressions like X = <value> + X are supported.");
- PreparedOperation pop = new PreparedOperation(new Term($mk.text, $mk.type, ++currentBindMarkerIdx), PreparedOperation.Kind.PREPARED_PLUS);
- columns.add(Pair.<ColumnIdentifier, Operation>create(key, pop));
+ addRecognitionError("Only expressions of the form X = <value> + X are supported.");
+ addRawUpdate(operations, key, new Operation.Prepend(t));
}
- )
- | key=cident '[' t=term ']' '=' vv=term
+ }
+ | key=cident '=' c=cident sig=('+' | '-') t=term
{
- // This is ambiguous, this can either set a list by index, or be a map put.
- // So we always return a list setIndex and we'll check later and
- // backtrack to a map operation if need be.
- columns.add(Pair.<ColumnIdentifier, Operation>create(key, ListOperation.SetIndex(Arrays.asList(t, vv))));
+ if (!key.equals(c))
+ addRecognitionError("Only expressions of the form X = X " + $sig.text + "<value> are supported.");
+ addRawUpdate(operations, key, $sig.text.equals("+") ? new Operation.Addition(t) : new Operation.Substraction(t));
}
- ;
-
-intTerm returns [Term integer]
- : t=INTEGER { $integer = new Term($t.text, $t.type); }
- | t=QMARK { $integer = new Term($t.text, $t.type, ++currentBindMarkerIdx); }
- ;
-
-
-operation returns [Operation op]
- : '+' i=INTEGER { $op = ColumnOperation.CounterInc(new Term($i.text, $i.type)); }
- | sign='-'? i=INTEGER
+ | key=cident '=' c=cident i=INTEGER
{
- Term t = new Term($i.text, $i.type);
- validateMinusSupplied(sign, t, input);
- if (sign == null)
- t = new Term(-(Long.valueOf(t.getText())), t.getType());
- $op = ColumnOperation.CounterDec(t);
+ // Note that this production *is* necessary because X = X - 3 will in fact be lexed as [ X, '=', X, INTEGER].
+ if (!key.equals(c))
+ // We don't yet allow a '+' in front of an integer, but we could in the future really, so let's be future-proof in our error message
+ addRecognitionError("Only expressions of the form X = X " + ($i.text.charAt(0) == '-' ? '-' : '+') + " <value> are supported.");
+ addRawUpdate(operations, key, new Operation.Addition(Constants.Literal.integer($i.text)));
+ }
+ | key=cident '[' k=term ']' '=' t=term
+ {
+ addRawUpdate(operations, key, new Operation.SetElement(k, t));
}
- | '+' mk=QMARK { $op = new PreparedOperation(new Term($mk.text, $mk.type, ++currentBindMarkerIdx), PreparedOperation.Kind.PLUS_PREPARED); }
- | '-' mk=QMARK { $op = new PreparedOperation(new Term($mk.text, $mk.type, ++currentBindMarkerIdx), PreparedOperation.Kind.MINUS_PREPARED); }
-
- | '+' ll=list_literal { $op = ListOperation.Append(ll); }
- | '-' ll=list_literal { $op = ListOperation.Discard(ll); }
-
- | '+' sl=set_literal { $op = SetOperation.Add(sl); }
- | '-' sl=set_literal { $op = SetOperation.Discard(sl); }
-
- | '+' ml=map_literal { $op = MapOperation.Put(ml); }
;
properties[PropertyDefinitions props]
@@ -737,41 +766,32 @@ properties[PropertyDefinitions props]
property[PropertyDefinitions props]
: k=cident '=' (simple=propertyValue { try { $props.addProperty(k.toString(), simple); } catch (SyntaxException e) { addRecognitionError(e.getMessage()); } }
- | map=map_literal { try { $props.addProperty(k.toString(), convertMap(map)); } catch (SyntaxException e) { addRecognitionError(e.getMessage()); } })
+ | map=map_literal { try { $props.addProperty(k.toString(), convertPropertyMap(map)); } catch (SyntaxException e) { addRecognitionError(e.getMessage()); } })
;
propertyValue returns [String str]
- : v=(STRING_LITERAL | IDENT | INTEGER | FLOAT | BOOLEAN | HEXNUMBER) { $str = $v.text; }
- | u=unreserved_keyword { $str = u; }
+ : c=constant { $str = c.getRawText(); }
+ | u=unreserved_keyword { $str = u; }
;
-// Either a term or a list of terms
-tokenDefinition returns [Pair<Term, List<Term>> tkdef]
- : K_TOKEN { List<Term> l = new ArrayList<Term>(); }
- '(' t1=term { l.add(t1); } ( ',' tn=term { l.add(tn); } )* ')' { $tkdef = Pair.<Term, List<Term>>create(null, l); }
- | t=term { $tkdef = Pair.<Term, List<Term>>create(t, null); }
+relationType returns [Relation.Type op]
+ : '=' { $op = Relation.Type.EQ; }
+ | '<' { $op = Relation.Type.LT; }
+ | '<=' { $op = Relation.Type.LTE; }
+ | '>' { $op = Relation.Type.GT; }
+ | '>=' { $op = Relation.Type.GTE; }
;
relation[List<Relation> clauses]
- : name=cident type=('=' | '<' | '<=' | '>=' | '>') t=term { $clauses.add(new Relation($name.id, $type.text, $t.term)); }
- | K_TOKEN { List<ColumnIdentifier> l = new ArrayList<ColumnIdentifier>(); }
- '(' name1=cident { l.add(name1); } ( ',' namen=cident { l.add(namen); })* ')'
- type=('=' |'<' | '<=' | '>=' | '>') tkd=tokenDefinition
- {
- if (tkd.right != null && tkd.right.size() != l.size())
- {
- addRecognitionError("The number of arguments to the token() function don't match");
- }
- else
- {
- Term tokenLitteral = tkd.left;
- for (int i = 0; i < l.size(); i++)
- {
- Term tt = tokenLitteral == null ? Term.tokenOf(tkd.right.get(i)) : tokenLitteral;
- $clauses.add(new Relation(l.get(i), $type.text, tt, true));
- }
- }
- }
+ : name=cident type=relationType t=term { $clauses.add(new Relation(name, type, t)); }
+ | K_TOKEN
+ { List<ColumnIdentifier> l = new ArrayList<ColumnIdentifier>(); }
+ '(' name1=cident { l.add(name1); } ( ',' namen=cident { l.add(namen); })* ')'
+ type=relationType t=term
+ {
+ for (ColumnIdentifier id : l)
+ $clauses.add(new Relation(id, type, t, true));
+ }
| name=cident K_IN { Relation rel = Relation.createInRelation($name.id); }
'(' f1=term { rel.addInValue(f1); } (',' fN=term { rel.addInValue(fN); } )* ')' { $clauses.add(rel); }
;
@@ -829,15 +849,17 @@ username
;
unreserved_keyword returns [String str]
+ : u=unreserved_function_keyword { $str = u; }
+ | k=(K_TTL | K_COUNT | K_WRITETIME) { $str = $k.text; }
+ ;
+
+unreserved_function_keyword returns [String str]
: k=( K_KEY
| K_CLUSTERING
- | K_COUNT
- | K_TTL
| K_COMPACT
| K_STORAGE
| K_TYPE
| K_VALUES
- | K_WRITETIME
| K_MAP
| K_LIST
| K_FILTERING
@@ -850,9 +872,6 @@ unreserved_keyword returns [String str]
| K_SUPERUSER
| K_NOSUPERUSER
| K_PASSWORD
- | K_MIN_TIMEUUID
- | K_MAX_TIMEUUID
- | K_NOW
) { $str = $k.text; }
| t=native_type { $str = t.toString(); }
;
@@ -945,12 +964,6 @@ K_WRITETIME: W R I T E T I M E;
K_MAP: M A P;
K_LIST: L I S T;
-K_MIN_TIMEUUID: M I N T I M E U U I D;
-K_MAX_TIMEUUID: M A X T I M E U U I D;
-K_NOW: N O W;
-K_DATE_OF: D A T E O F;
-K_UNIXTIMESTAMP_OF: U N I X T I M E S T A M P O F;
-
// Case-insensitive alpha characters
fragment A: ('a'|'A');
fragment B: ('b'|'B');
View
382 src/java/org/apache/cassandra/cql3/Lists.java
@@ -0,0 +1,382 @@
+/*
+ * 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 java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.cassandra.db.ColumnFamily;
+import org.apache.cassandra.db.IColumn;
+import org.apache.cassandra.db.marshal.CollectionType;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.marshal.ListType;
+import org.apache.cassandra.db.marshal.MarshalException;
+import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.Pair;
+import org.apache.cassandra.utils.UUIDGen;
+
+/**
+ * Static helper methods and classes for lists.
+ */
+public abstract class Lists
+{
+ private Lists() {}
+
+ public static ColumnSpecification indexSpecOf(ColumnSpecification column)
+ {
+ return new ColumnSpecification(column.ksName, column.cfName, new ColumnIdentifier("idx(" + column.name + ")", true), Int32Type.instance);
+ }
+
+ public static ColumnSpecification valueSpecOf(ColumnSpecification column)
+ {
+ return new ColumnSpecification(column.ksName, column.cfName, new ColumnIdentifier("value(" + column.name + ")", true), ((ListType)column.type).elements);
+ }
+
+ public static class Literal implements Term.Raw
+ {
+ private final List<Term.Raw> elements;
+
+ public Literal(List<Term.Raw> elements)
+ {
+ this.elements = elements;
+ }
+
+ public Value prepare(ColumnSpecification receiver) throws InvalidRequestException
+ {
+ validateAssignableTo(receiver);
+
+ ColumnSpecification valueSpec = Lists.valueSpecOf(receiver);
+ List<ByteBuffer> values = new ArrayList<ByteBuffer>(elements.size());
+ for (Term.Raw rt : elements)
+ {
+ Term t = rt.prepare(valueSpec);
+
+ if (!(t instanceof Constants.Value))
+ {
+ if (t instanceof Term.NonTerminal)
+ throw new InvalidRequestException(String.format("Invalid list literal for %s: bind variables are not supported inside collection literals", receiver));
+ else
+ throw new InvalidRequestException(String.format("Invalid list literal for %s: nested collections are not supported", receiver));
+ }
+
+ // We don't allow prepared marker in collections, nor nested collections
+ assert t instanceof Constants.Value;
+ values.add(((Constants.Value)t).bytes);
+ }
+ return new Value(values);
+ }
+
+ private void validateAssignableTo(ColumnSpecification receiver) throws InvalidRequestException
+ {
+ if (!(receiver.type instanceof ListType))
+ throw new InvalidRequestException(String.format("Invalid list literal for %s of type %s", receiver, receiver.type.asCQL3Type()));
+
+ ColumnSpecification valueSpec = Lists.valueSpecOf(receiver);
+ for (Term.Raw rt : elements)
+ {
+ if (!rt.isAssignableTo(valueSpec))
+ throw new InvalidRequestException(String.format("Invalid list literal for %s: value %s is not of type %s", receiver, rt, valueSpec.type.asCQL3Type()));
+ }
+ }
+
+ public boolean isAssignableTo(ColumnSpecification receiver)
+ {
+ try
+ {
+ validateAssignableTo(receiver);
+ return true;
+ }
+ catch (InvalidRequestException e)
+ {
+ return false;
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return elements.toString();
+ }
+ }
+
+ public static class Value extends Term.Terminal
+ {
+ public final List<ByteBuffer> elements;
+
+ public Value(List<ByteBuffer> elements)
+ {
+ this.elements = elements;
+ }
+
+ public static Value fromSerialized(ByteBuffer value, ListType type) throws InvalidRequestException
+ {
+ try
+ {
+ // Collections have this small hack that validate cannot be called on a serialized object,
+ // but compose does the validation (so we're fine).
+ List<?> l = type.compose(value);
+ List<ByteBuffer> elements = new ArrayList<ByteBuffer>(l.size());
+ for (Object element : l)
+ elements.add(type.elements.decompose(element));
+ return new Value(elements);
+ }
+ catch (MarshalException e)
+ {
+ throw new InvalidRequestException(e.getMessage());
+ }
+ }
+
+ public ByteBuffer get()
+ {
+ return CollectionType.pack(elements, elements.size());
+ }
+ }
+
+ public static class Marker extends AbstractMarker
+ {
+ protected Marker(int bindIndex, ColumnSpecification receiver)
+ {
+ super(bindIndex, receiver);
+ assert receiver.type instanceof ListType;
+ }
+
+ public Value bind(List<ByteBuffer> values) throws InvalidRequestException
+ {
+ ByteBuffer value = values.get(bindIndex);
+ return Value.fromSerialized(value, (ListType)receiver.type);
+
+ }
+ }
+
+ /*
+ * For prepend, we need to be able to generate unique but decreasing time
+ * UUID, which is a bit challenging. To do that, given a time in milliseconds,
+ * we adds a number representing the 100-nanoseconds precision and make sure
+ * that within the same millisecond, that number is always decreasing. We
+ * do rely on the fact that the user will only provide decreasing
+ * milliseconds timestamp for that purpose.
+ */
+ private static class PrecisionTime
+ {
+ // Our reference time (1 jan 2010, 00:00:00) in milliseconds.
+ private static final long REFERENCE_TIME = 1262304000000L;
+ private static final AtomicReference<PrecisionTime> last = new AtomicReference<PrecisionTime>(new PrecisionTime(Long.MAX_VALUE, 0));
+
+ public final long millis;
+ public final int nanos;
+
+ PrecisionTime(long millis, int nanos)
+ {
+ this.millis = millis;
+ this.nanos = nanos;
+ }
+
+ static PrecisionTime getNext(long millis)
+ {
+ while (true)
+ {
+ PrecisionTime current = last.get();
+
+ assert millis <= current.millis;
+ PrecisionTime next = millis < current.millis
+ ? new PrecisionTime(millis, 9999)
+ : new PrecisionTime(millis, Math.max(0, current.nanos - 1));
+
+ if (last.compareAndSet(current, next))
+ return next;
+ }
+ }
+ }
+
+ public static class Setter extends Operation
+ {
+ public Setter(ColumnIdentifier column, Term t)
+ {
+ super(column, t);
+ }
+
+ public void execute(ByteBuffer rowKey, ColumnFamily cf, ColumnNameBuilder prefix, UpdateParameters params) throws InvalidRequestException
+ {
+ // delete + append
+ ColumnNameBuilder column = prefix.add(columnName.key);
+ cf.addAtom(params.makeTombstoneForOverwrite(column.build(), column.buildAsEndOfRange()));
+ Appender.doAppend(t, cf, column, params);
+ }
+ }
+
+ public static class SetterByIndex extends Operation
+ {
+ private final Term idx;
+
+ public SetterByIndex(ColumnIdentifier column, Term idx, Term t)
+ {
+ super(column, t);
+ this.idx = idx;
+ }
+
+ @Override
+ public boolean requiresRead()
+ {
+ return true;
+ }
+
+ @Override
+ public void collectMarkerSpecification(ColumnSpecification[] boundNames)
+ {
+ super.collectMarkerSpecification(boundNames);
+ idx.collectMarkerSpecification(boundNames);
+ }
+
+ public void execute(ByteBuffer rowKey, ColumnFamily cf, ColumnNameBuilder prefix, UpdateParameters params) throws InvalidRequestException
+ {
+ Term.Terminal index = idx.bind(params.variables);
+ Term.Terminal value = t.bind(params.variables);
+ assert index instanceof Constants.Value && value instanceof Constants.Value;
+
+ List<Pair<ByteBuffer, IColumn>> existingList = params.getPrefetchedList(rowKey, columnName.key);
+ int idx = ByteBufferUtil.toInt(((Constants.Value)index).bytes);
+ if (idx < 0 || idx >= existingList.size())
+ throw new InvalidRequestException(String.format("List index %d out of bound, list has size %d", idx, existingList.size()));
+
+ ByteBuffer elementName = existingList.get(idx).right.name();
+ cf.addColumn(params.makeColumn(elementName, ((Constants.Value)value).bytes));
+ }
+ }
+
+ public static class Appender extends Operation
+ {
+ public Appender(ColumnIdentifier column, Term t)
+ {
+ super(column, t);
+ }
+
+ public void execute(ByteBuffer rowKey, ColumnFamily cf, ColumnNameBuilder prefix, UpdateParameters params) throws InvalidRequestException
+ {
+ doAppend(t, cf, prefix.add(columnName.key), params);
+ }
+
+ static void doAppend(Term t, ColumnFamily cf, ColumnNameBuilder columnName, UpdateParameters params) throws InvalidRequestException
+ {
+ Term.Terminal value = t.bind(params.variables);
+ assert value instanceof Lists.Value;
+
+ List<ByteBuffer> toAdd = ((Lists.Value)value).elements;
+ for (int i = 0; i < toAdd.size(); i++)
+ {
+ ColumnNameBuilder b = i == toAdd.size() - 1 ? columnName : columnName.copy();
+ ByteBuffer uuid = ByteBuffer.wrap(UUIDGen.getTimeUUIDBytes());
+ ByteBuffer cellName = b.add(uuid).build();
+ cf.addColumn(params.makeColumn(cellName, toAdd.get(i)));
+ }
+ }
+ }
+
+ public static class Prepender extends Operation
+ {
+ public Prepender(ColumnIdentifier column, Term t)
+ {
+ super(column, t);
+ }
+
+ public void execute(ByteBuffer rowKey, ColumnFamily cf, ColumnNameBuilder prefix, UpdateParameters params) throws InvalidRequestException
+ {
+ Term.Terminal value = t.bind(params.variables);
+ assert value instanceof Lists.Value;
+
+ long time = PrecisionTime.REFERENCE_TIME - (System.currentTimeMillis() - PrecisionTime.REFERENCE_TIME);
+
+ List<ByteBuffer> toAdd = ((Lists.Value)value).elements;
+ ColumnNameBuilder column = prefix.add(columnName.key);
+ for (int i = 0; i < toAdd.size(); i++)
+ {
+ ColumnNameBuilder b = i == toAdd.size() - 1 ? column : column.copy();
+ PrecisionTime pt = PrecisionTime.getNext(time);
+ ByteBuffer uuid = ByteBuffer.wrap(UUIDGen.getTimeUUIDBytes(pt.millis, pt.nanos));
+ ByteBuffer cellName = b.add(uuid).build();
+ cf.addColumn(params.makeColumn(cellName, toAdd.get(i)));
+ }
+ }
+ }
+
+ public static class Discarder extends Operation
+ {
+ public Discarder(ColumnIdentifier column, Term t)
+ {
+ super(column, t);
+ }
+
+ @Override
+ public boolean requiresRead()
+ {
+ return true;
+ }
+
+ public void execute(ByteBuffer rowKey, ColumnFamily cf, ColumnNameBuilder prefix, UpdateParameters params) throws InvalidRequestException
+ {
+ List<Pair<ByteBuffer, IColumn>> existingList = params.getPrefetchedList(rowKey, columnName.key);
+ if (existingList.isEmpty())
+ return;
+
+ Term.Terminal value = t.bind(params.variables);
+ assert value instanceof Lists.Value;
+
+ // Note: below, we will call 'contains' on this toDiscard list for each element of existingList.
+ // Meaning that if toDiscard is big, converting it to a HashSet might be more efficient. However,
+ // the read-before-write this operation requires limits its usefulness on big lists, so in practice
+ // toDiscard will be small and keeping a list will be more efficient.
+ List<ByteBuffer> toDiscard = ((Lists.Value)value).elements;
+ for (Pair<ByteBuffer, IColumn> p : existingList)
+ {
+ IColumn element = p.right;
+ if (toDiscard.contains(element.value()))
+ cf.addColumn(params.makeTombstone(element.name()));
+ }
+ }
+ }
+
+ public static class DiscarderByIndex extends Operation
+ {
+ public DiscarderByIndex(ColumnIdentifier column, Term idx)
+ {
+ super(column, idx);
+ }
+
+ @Override
+ public boolean requiresRead()
+ {
+ return true;
+ }
+
+ public void execute(ByteBuffer rowKey, ColumnFamily cf, ColumnNameBuilder prefix, UpdateParameters params) throws InvalidRequestException
+ {
+ Term.Terminal index = t.bind(params.variables);
+ assert index instanceof Constants.Value;
+
+ List<Pair<ByteBuffer, IColumn>> existingList = params.getPrefetchedList(rowKey, columnName.key);
+ int idx = ByteBufferUtil.toInt(((Constants.Value)index).bytes);
+ if (idx < 0 || idx >= existingList.size())
+ throw new InvalidRequestException(String.format("List index %d out of bound, list has size %d", idx, existingList.size()));
+
+ ByteBuffer elementName = existingList.get(idx).right.name();
+ cf.addColumn(params.makeTombstone(elementName));
+ }
+ }
+}
View
271 src/java/org/apache/cassandra/cql3/Maps.java
@@ -0,0 +1,271 @@
+/*
+ * 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 java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.apache.cassandra.db.ColumnFamily;
+import org.apache.cassandra.db.marshal.CollectionType;
+import org.apache.cassandra.db.marshal.MapType;
+import org.apache.cassandra.db.marshal.MarshalException;
+import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.utils.Pair;
+
+/**
+ * Static helper methods and classes for maps.
+ */
+public abstract class Maps
+{
+ private Maps() {}
+
+ public static ColumnSpecification keySpecOf(ColumnSpecification column)
+ {
+ return new ColumnSpecification(column.ksName, column.cfName, new ColumnIdentifier("key(" + column.name + ")", true), ((MapType)column.type).keys);
+ }
+
+ public static ColumnSpecification valueSpecOf(ColumnSpecification column)
+ {
+ return new ColumnSpecification(column.ksName, column.cfName, new ColumnIdentifier("value(" + column.name + ")", true), ((MapType)column.type).values);
+ }
+
+ public static class Literal implements Term.Raw
+ {
+ public final List<Pair<Term.Raw, Term.Raw>> entries;
+
+ public Literal(List<Pair<Term.Raw, Term.Raw>> entries)
+ {
+ this.entries = entries;
+ }
+
+ public Value prepare(ColumnSpecification receiver) throws InvalidRequestException
+ {
+ validateAssignableTo(receiver);
+
+ ColumnSpecification keySpec = Maps.keySpecOf(receiver);
+ ColumnSpecification valueSpec = Maps.valueSpecOf(receiver);
+ Map<ByteBuffer, ByteBuffer> values = new TreeMap<ByteBuffer, ByteBuffer>(((MapType)receiver.type).keys);
+ for (Pair<Term.Raw, Term.Raw> entry : entries)
+ {
+ Term k = entry.left.prepare(keySpec);
+ Term v = entry.right.prepare(valueSpec);
+
+ if (!(k instanceof Constants.Value && v instanceof Constants.Value))
+ {
+ if (k instanceof Term.NonTerminal || v instanceof Term.NonTerminal)
+ throw new InvalidRequestException(String.format("Invalid map literal for %s: bind variables are not supported inside collection literals", receiver));
+ else
+ throw new InvalidRequestException(String.format("Invalid map literal for %s: nested collections are not supported", receiver));
+ }
+
+ if (values.put(((Constants.Value)k).bytes, ((Constants.Value)v).bytes) != null)
+ throw new InvalidRequestException(String.format("Invalid map literal: duplicate entry for key %s", entry.left));
+ }
+ return new Value(values);
+ }
+
+ private void validateAssignableTo(ColumnSpecification receiver) throws InvalidRequestException
+ {
+ if (!(receiver.type instanceof MapType))
+ throw new InvalidRequestException(String.format("Invalid map literal for %s of type %s", receiver, receiver.type.asCQL3Type()));
+
+ ColumnSpecification keySpec = Maps.keySpecOf(receiver);
+ ColumnSpecification valueSpec = Maps.valueSpecOf(receiver);
+ for (Pair<Term.Raw, Term.Raw> entry : entries)
+ {
+ if (!entry.left.isAssignableTo(keySpec))
+ throw new InvalidRequestException(String.format("Invalid map literal for %s: key %s is not of type %s", receiver, entry.left, keySpec.type.asCQL3Type()));
+ if (!entry.right.isAssignableTo(valueSpec))
+ throw new InvalidRequestException(String.format("Invalid map literal for %s: value %s is not of type %s", receiver, entry.right, valueSpec.type.asCQL3Type()));
+ }
+ }
+
+ public boolean isAssignableTo(ColumnSpecification receiver)
+ {
+ try
+ {
+ validateAssignableTo(receiver);
+ return true;
+ }
+ catch (InvalidRequestException e)
+ {
+ return false;
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{");
+ for (int i = 0; i < entries.size(); i++)
+ {
+ if (i > 0) sb.append(", ");
+ sb.append(entries.get(i).left).append(":").append(entries.get(i).right);
+ }
+ sb.append("}");
+ return sb.toString();
+ }
+ }
+
+ public static class Value extends Term.Terminal
+ {
+ public final Map<ByteBuffer, ByteBuffer> map;
+
+ public Value(Map<ByteBuffer, ByteBuffer> map)
+ {
+ this.map = map;
+ }
+
+ public static Value fromSerialized(ByteBuffer value, MapType type) throws InvalidRequestException
+ {
+ try
+ {
+ // Collections have this small hack that validate cannot be called on a serialized object,
+ // but compose does the validation (so we're fine).
+ Map<?, ?> m = type.compose(value);
+ Map<ByteBuffer, ByteBuffer> map = new LinkedHashMap<ByteBuffer, ByteBuffer>(m.size());
+ for (Map.Entry<?, ?> entry : m.entrySet())
+ map.put(type.keys.decompose(entry.getKey()), type.values.decompose(entry.getValue()));
+ return new Value(map);
+ }
+ catch (MarshalException e)
+ {
+ throw new InvalidRequestException(e.getMessage());
+ }
+ }
+
+ public ByteBuffer get()
+ {
+ List<ByteBuffer> buffers = new ArrayList<ByteBuffer>(2 * map.size());
+ for (Map.Entry<ByteBuffer, ByteBuffer> entry : map.entrySet())
+ {
+ buffers.add(entry.getKey());
+ buffers.add(entry.getValue());
+ }
+ return CollectionType.pack(buffers, map.size());
+ }
+ }
+
+ public static class Marker extends AbstractMarker
+ {
+ protected Marker(int bindIndex, ColumnSpecification receiver)
+ {
+ super(bindIndex, receiver);
+ assert receiver.type instanceof MapType;
+ }
+
+ public Value bind(List<ByteBuffer> values) throws InvalidRequestException
+ {
+ ByteBuffer value = values.get(bindIndex);
+ return Value.fromSerialized(value, (MapType)receiver.type);
+ }
+ }
+
+ public static class Setter extends Operation
+ {
+ public Setter(ColumnIdentifier column, Term t)
+ {
+ super(column, t);
+ }
+
+ public void execute(ByteBuffer rowKey, ColumnFamily cf, ColumnNameBuilder prefix, UpdateParameters params) throws InvalidRequestException
+ {
+ // delete + put
+ ColumnNameBuilder column = prefix.add(columnName.key);
+ cf.addAtom(params.makeTombstoneForOverwrite(column.build(), column.buildAsEndOfRange()));
+ Putter.doPut(t, cf, column, params);
+ }
+ }
+
+ public static class SetterByKey extends Operation
+ {
+ private final Term k;
+
+ public SetterByKey(ColumnIdentifier column, Term k, Term t)
+ {
+ super(column, t);
+ this.k = k;
+ }
+
+ @Override
+ public void collectMarkerSpecification(ColumnSpecification[] boundNames)
+ {
+ super.collectMarkerSpecification(boundNames);
+ k.collectMarkerSpecification(boundNames);
+ }
+
+ public void execute(ByteBuffer rowKey, ColumnFamily cf, ColumnNameBuilder prefix, UpdateParameters params) throws InvalidRequestException
+ {
+ Term.Terminal key = k.bind(params.variables);
+ Term.Terminal value = t.bind(params.variables);
+ assert key instanceof Constants.Value && value instanceof Constants.Value;
+
+ ByteBuffer cellName = prefix.add(columnName.key).add(((Constants.Value)key).bytes).build();
+ cf.addColumn(params.makeColumn(cellName, ((Constants.Value)value).bytes));
+ }
+ }
+
+ public static class Putter extends Operation
+ {
+ public Putter(ColumnIdentifier column, Term t)
+ {
+ super(column, t);
+ }
+
+ public void execute(ByteBuffer rowKey, ColumnFamily cf, ColumnNameBuilder prefix, UpdateParameters params) throws InvalidRequestException
+ {
+ doPut(t, cf, prefix.add(columnName.key), params);
+ }
+
+ static void doPut(Term t, ColumnFamily cf, ColumnNameBuilder columnName, UpdateParameters params) throws InvalidRequestException
+ {
+ Term.Terminal value = t.bind(params.variables);
+ assert value instanceof Maps.Value;
+
+ Map<ByteBuffer, ByteBuffer> toAdd = ((Maps.Value)value).map;
+ for (Map.Entry<ByteBuffer, ByteBuffer> entry : toAdd.entrySet())
+ {
+ ByteBuffer cellName = columnName.copy().add(entry.getKey()).build();
+ cf.addColumn(params.makeColumn(cellName, entry.getValue()));
+ }
+ }
+ }
+
+ public static class DiscarderByKey extends Operation
+ {
+ public DiscarderByKey(ColumnIdentifier column, Term k)
+ {
+ super(column, k);
+ }
+
+ public void execute(ByteBuffer rowKey, ColumnFamily cf, ColumnNameBuilder prefix, UpdateParameters params) throws InvalidRequestException
+ {
+ Term.Terminal key = t.bind(params.variables);
+ assert key instanceof Constants.Value;
+
+ ByteBuffer cellName = prefix.add(columnName.key).add(((Constants.Value)key).bytes).build();
+ cf.addColumn(params.makeTombstone(cellName));
+ }
+ }
+}
View
408 src/java/org/apache/cassandra/cql3/Operation.java
@@ -0,0 +1,408 @@
+/*
+ * 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 java.nio.ByteBuffer;
+
+import org.apache.cassandra.db.ColumnFamily;
+import org.apache.cassandra.db.marshal.CollectionType;
+import org.apache.cassandra.db.marshal.CounterColumnType;
+import org.apache.cassandra.db.marshal.ListType;
+import org.apache.cassandra.exceptions.InvalidRequestException;
+
+/**
+ * An UPDATE or DELETE operation.
+ *
+ * For UPDATE this includes:
+ * - setting a constant
+ * - counter operations
+ * - collections operations
+ * and for DELETE:
+ * - deleting a column
+ * - deleting an element of collection column
+ *
+ * Fine grained operation are obtained from their raw counterpart (Operation.Raw, which
+ * correspond to a parsed, non-checked operation) by provided the receiver for the operation.
+ */
+public abstract class Operation
+{
+ // Name of the column the operation applies to
+ public final ColumnIdentifier columnName;
+
+ // Term involved in the operation. In theory this should not be here since some operation
+ // may require none of more than one term, but most need 1 so it simplify things a bit.
+ protected final Term t;
+
+ protected Operation(ColumnIdentifier columnName, Term t)
+ {
+ this.columnName = columnName;
+ this.t = t;
+ }
+
+ /**
+ * @return whether the operation requires a read of the previous value to be executed
+ * (only lists setterByIdx, discard and discardByIdx requires that).
+ */
+ public boolean requiresRead()
+ {
+ return false;
+ }
+
+ /**
+ * Collects the column specification for the bind variables of this operation.
+ *
+ * @param boundNames the list of column specification where to collect the
+ * bind variables of this term in.
+ */
+ public void collectMarkerSpecification(ColumnSpecification[] boundNames)
+ {
+ if (t != null)
+ t.collectMarkerSpecification(boundNames);
+ }
+
+ /**
+ * Execute the operation.
+ *
+ * @param rowKey row key for the update.
+ * @param cf the column family to which to add the updates generated by this operation.
+ * @param namePrefix the prefix that identify the CQL3 row this operation applies to (callers should not reuse
+ * the ColumnNameBuilder they pass here).
+ * @param params parameters of the update.
+ */
+ public abstract void execute(ByteBuffer rowKey, ColumnFamily cf, ColumnNameBuilder namePrefix, UpdateParameters params) throws InvalidRequestException;
+
+ /**
+ * A parsed raw UPDATE operation.
+ *
+ * This can be one of:
+ * - Setting a value: c = v
+ * - Setting an element of a collection: c[x] = v
+ * - An addition/substraction to a variable: c = c +/- v (where v can be a collection literal)
+ * - An prepend operation: c = v + c
+ */
+ public interface RawUpdate
+ {
+ /**
+ * This method validates the operation (i.e. validate it is well typed)
+ * based on the specification of the receiver of the operation.
+ *
+ * It returns an Operation which can be though as post-preparation well-typed
+ * Operation.
+ *
+ * @param receiver the "column" this operation applies to. Note that
+ * contrarly to the method of same name in Term.Raw, the receiver should always
+ * be a true column.
+ * @return the prepared update operation.
+ */
+ public Operation prepare(CFDefinition.Name receiver) throws InvalidRequestException;
+
+ /**
+ * @return whether this operation can be applied alongside the {@code
+ * other} update (in the same UPDATE statement for the same column).
+ */
+ public boolean isCompatibleWith(RawUpdate other);
+ }
+
+ /**
+ * A parsed raw DELETE operation.
+ *
+ * This can be one of:
+ * - Deleting a column
+ * - Deleting an element of a collection
+ */
+ public interface RawDeletion
+ {
+ /**
+ * The name of the column affected by this delete operation.
+ */
+ public ColumnIdentifier affectedColumn();
+
+ /**
+ * This method validates the operation (i.e. validate it is well typed)
+ * based on the specification of the column affected by the operation (i.e the
+ * one returned by affectedColumn()).
+ *
+ * It returns an Operation which can be though as post-preparation well-typed
+ * Operation.
+ *
+ * @param receiver the "column" this operation applies to.
+ * @return the prepared delete operation.
+ */
+ public Operation prepare(ColumnSpecification receiver) throws InvalidRequestException;
+ }
+
+ public static class SetValue implements RawUpdate
+ {
+ private final Term.Raw value;
+
+ public SetValue(Term.Raw value)
+ {
+ this.value = value;
+ }
+
+ public Operation prepare(CFDefinition.Name receiver) throws InvalidRequestException
+ {
+ Term v = value.prepare(receiver);
+
+ if (!(receiver.type instanceof CollectionType))
+ return new Constants.Setter(receiver.kind == CFDefinition.Name.Kind.VALUE_ALIAS ? null : receiver.name, v);
+
+ switch (((CollectionType)receiver.type).kind)
+ {
+ case LIST:
+ return new Lists.Setter(receiver.name, v);
+ case SET:
+ return new Sets.Setter(receiver.name, v);
+ case MAP:
+ return new Maps.Setter(receiver.name, v);
+ }
+ throw new AssertionError();
+ }
+
+ protected String toString(ColumnSpecification column)
+ {