Repository: cassandra Updated Branches: refs/heads/trunk e379f978b -> b7be1980b
Fix string encoding of JSON map keys Patch by Tyler Hobbs; reviewed by Jonathan Ellis for CASSANDRA-9190 Project: http://git-wip-us.apache.org/repos/asf/cassandra/repo Commit: http://git-wip-us.apache.org/repos/asf/cassandra/commit/b7be1980 Tree: http://git-wip-us.apache.org/repos/asf/cassandra/tree/b7be1980 Diff: http://git-wip-us.apache.org/repos/asf/cassandra/diff/b7be1980 Branch: refs/heads/trunk Commit: b7be1980b1b41731f27433cd80743ec76b4beab1 Parents: e379f97 Author: Tyler Hobbs <tylerlho...@gmail.com> Authored: Wed May 13 12:38:26 2015 -0500 Committer: Tyler Hobbs <tylerlho...@gmail.com> Committed: Wed May 13 12:38:26 2015 -0500 ---------------------------------------------------------------------- src/java/org/apache/cassandra/cql3/Json.java | 12 +++ .../apache/cassandra/db/marshal/ListType.java | 4 + .../apache/cassandra/db/marshal/MapType.java | 12 ++- .../apache/cassandra/db/marshal/SetType.java | 4 + .../apache/cassandra/db/marshal/TupleType.java | 8 +- .../apache/cassandra/db/marshal/UserType.java | 3 + .../org/apache/cassandra/cql3/JsonTest.java | 87 ++++++++++++++++++++ 7 files changed, 125 insertions(+), 5 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cassandra/blob/b7be1980/src/java/org/apache/cassandra/cql3/Json.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/Json.java b/src/java/org/apache/cassandra/cql3/Json.java index 905f6e0..e4bce29 100644 --- a/src/java/org/apache/cassandra/cql3/Json.java +++ b/src/java/org/apache/cassandra/cql3/Json.java @@ -39,6 +39,18 @@ public class Json public static final ColumnIdentifier JSON_COLUMN_ID = new ColumnIdentifier("[json]", true); + public static Object decodeJson(String json) + { + try + { + return JSON_OBJECT_MAPPER.readValue(json, Object.class); + } + catch (IOException exc) + { + throw new MarshalException("Error decoding JSON string: " + exc.getMessage()); + } + } + public interface Raw { public Prepared prepareAndCollectMarkers(CFMetaData metadata, Collection<ColumnDefinition> receivers, VariableSpecifications boundNames); http://git-wip-us.apache.org/repos/asf/cassandra/blob/b7be1980/src/java/org/apache/cassandra/db/marshal/ListType.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/db/marshal/ListType.java b/src/java/org/apache/cassandra/db/marshal/ListType.java index bae8043..03f39d7 100644 --- a/src/java/org/apache/cassandra/db/marshal/ListType.java +++ b/src/java/org/apache/cassandra/db/marshal/ListType.java @@ -20,6 +20,7 @@ package org.apache.cassandra.db.marshal; import java.nio.ByteBuffer; import java.util.*; +import org.apache.cassandra.cql3.Json; import org.apache.cassandra.cql3.Lists; import org.apache.cassandra.cql3.Term; import org.apache.cassandra.db.Cell; @@ -180,6 +181,9 @@ public class ListType<T> extends CollectionType<List<T>> @Override public Term fromJSONObject(Object parsed) throws MarshalException { + if (parsed instanceof String) + parsed = Json.decodeJson((String) parsed); + if (!(parsed instanceof List)) throw new MarshalException(String.format( "Expected a list, but got a %s: %s", parsed.getClass().getSimpleName(), parsed)); http://git-wip-us.apache.org/repos/asf/cassandra/blob/b7be1980/src/java/org/apache/cassandra/db/marshal/MapType.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/db/marshal/MapType.java b/src/java/org/apache/cassandra/db/marshal/MapType.java index 3ed3dd1..983710b 100644 --- a/src/java/org/apache/cassandra/db/marshal/MapType.java +++ b/src/java/org/apache/cassandra/db/marshal/MapType.java @@ -20,6 +20,7 @@ package org.apache.cassandra.db.marshal; import java.nio.ByteBuffer; import java.util.*; +import org.apache.cassandra.cql3.Json; import org.apache.cassandra.cql3.Maps; import org.apache.cassandra.cql3.Term; import org.apache.cassandra.db.Cell; @@ -200,6 +201,9 @@ public class MapType<K, V> extends CollectionType<Map<K, V>> @Override public Term fromJSONObject(Object parsed) throws MarshalException { + if (parsed instanceof String) + parsed = Json.decodeJson((String) parsed); + if (!(parsed instanceof Map)) throw new MarshalException(String.format( "Expected a map, but got a %s: %s", parsed.getClass().getSimpleName(), parsed)); @@ -229,7 +233,13 @@ public class MapType<K, V> extends CollectionType<Map<K, V>> if (i > 0) sb.append(", "); - sb.append(keys.toJSONString(CollectionSerializer.readValue(buffer, protocolVersion), protocolVersion)); + // map keys must be JSON strings, so convert non-string keys to strings + String key = keys.toJSONString(CollectionSerializer.readValue(buffer, protocolVersion), protocolVersion); + if (key.startsWith("\"")) + sb.append(key); + else + sb.append('"').append(Json.JSON_STRING_ENCODER.quoteAsString(key)).append('"'); + sb.append(": "); sb.append(values.toJSONString(CollectionSerializer.readValue(buffer, protocolVersion), protocolVersion)); } http://git-wip-us.apache.org/repos/asf/cassandra/blob/b7be1980/src/java/org/apache/cassandra/db/marshal/SetType.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/db/marshal/SetType.java b/src/java/org/apache/cassandra/db/marshal/SetType.java index 372555a..78aac25 100644 --- a/src/java/org/apache/cassandra/db/marshal/SetType.java +++ b/src/java/org/apache/cassandra/db/marshal/SetType.java @@ -20,6 +20,7 @@ package org.apache.cassandra.db.marshal; import java.nio.ByteBuffer; import java.util.*; +import org.apache.cassandra.cql3.Json; import org.apache.cassandra.cql3.Sets; import org.apache.cassandra.cql3.Term; import org.apache.cassandra.db.Cell; @@ -154,6 +155,9 @@ public class SetType<T> extends CollectionType<Set<T>> @Override public Term fromJSONObject(Object parsed) throws MarshalException { + if (parsed instanceof String) + parsed = Json.decodeJson((String) parsed); + if (!(parsed instanceof List)) throw new MarshalException(String.format( "Expected a list (representing a set), but got a %s: %s", parsed.getClass().getSimpleName(), parsed)); http://git-wip-us.apache.org/repos/asf/cassandra/blob/b7be1980/src/java/org/apache/cassandra/db/marshal/TupleType.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/db/marshal/TupleType.java b/src/java/org/apache/cassandra/db/marshal/TupleType.java index 6093137..e874cb6 100644 --- a/src/java/org/apache/cassandra/db/marshal/TupleType.java +++ b/src/java/org/apache/cassandra/db/marshal/TupleType.java @@ -25,10 +25,7 @@ import java.util.List; import com.google.common.base.Objects; -import org.apache.cassandra.cql3.CQL3Type; -import org.apache.cassandra.cql3.Constants; -import org.apache.cassandra.cql3.Term; -import org.apache.cassandra.cql3.Tuples; +import org.apache.cassandra.cql3.*; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.SyntaxException; import org.apache.cassandra.serializers.*; @@ -233,6 +230,9 @@ public class TupleType extends AbstractType<ByteBuffer> @Override public Term fromJSONObject(Object parsed) throws MarshalException { + if (parsed instanceof String) + parsed = Json.decodeJson((String) parsed); + if (!(parsed instanceof List)) throw new MarshalException(String.format( "Expected a list representation of a tuple, but got a %s: %s", parsed.getClass().getSimpleName(), parsed)); http://git-wip-us.apache.org/repos/asf/cassandra/blob/b7be1980/src/java/org/apache/cassandra/db/marshal/UserType.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/db/marshal/UserType.java b/src/java/org/apache/cassandra/db/marshal/UserType.java index 45c5f0e..5879d6b 100644 --- a/src/java/org/apache/cassandra/db/marshal/UserType.java +++ b/src/java/org/apache/cassandra/db/marshal/UserType.java @@ -145,6 +145,9 @@ public class UserType extends TupleType @Override public Term fromJSONObject(Object parsed) throws MarshalException { + if (parsed instanceof String) + parsed = Json.decodeJson((String) parsed); + if (!(parsed instanceof Map)) throw new MarshalException(String.format( "Expected a map, but got a %s: %s", parsed.getClass().getSimpleName(), parsed)); http://git-wip-us.apache.org/repos/asf/cassandra/blob/b7be1980/test/unit/org/apache/cassandra/cql3/JsonTest.java ---------------------------------------------------------------------- diff --git a/test/unit/org/apache/cassandra/cql3/JsonTest.java b/test/unit/org/apache/cassandra/cql3/JsonTest.java index 47d6ddd..305502e 100644 --- a/test/unit/org/apache/cassandra/cql3/JsonTest.java +++ b/test/unit/org/apache/cassandra/cql3/JsonTest.java @@ -843,6 +843,93 @@ public class JsonTest extends CQLTester } @Test + public void testInsertJsonSyntaxWithNonNativeMapKeys() throws Throwable + { + // JSON doesn't allow non-string keys, so we accept string representations of any type as map keys and + // return maps with string keys when necessary. + + String typeName = createType("CREATE TYPE %s (a int)"); + createTable("CREATE TABLE %s (" + + "k int PRIMARY KEY, " + + "intmap map<int, boolean>, " + + "bigintmap map<bigint, boolean>, " + + "varintmap map<varint, boolean>, " + + "booleanmap map<boolean, boolean>, " + + "floatmap map<float, boolean>, " + + "doublemap map<double, boolean>, " + + "decimalmap map<decimal, boolean>, " + + "tuplemap map<frozen<tuple<int, text>>, boolean>, " + + "udtmap map<frozen<" + typeName + ">, boolean>, " + + "setmap map<frozen<set<int>>, boolean>, " + + "listmap map<frozen<list<int>>, boolean>, " + + "textsetmap map<frozen<set<text>>, boolean>, " + + "nestedsetmap map<frozen<map<set<text>, text>>, boolean>, " + + "frozensetmap frozen<map<set<int>, boolean>>)"); + + // int keys + execute("INSERT INTO %s JSON ?", "{\"k\": 0, \"intmap\": {\"0\": true, \"1\": false}}"); + assertRows(execute("SELECT JSON k, intmap FROM %s"), row("{\"k\": 0, \"intmap\": {\"0\": true, \"1\": false}}")); + + // bigint keys + execute("INSERT INTO %s JSON ?", "{\"k\": 0, \"bigintmap\": {\"0\": true, \"1\": false}}"); + assertRows(execute("SELECT JSON k, bigintmap FROM %s"), row("{\"k\": 0, \"bigintmap\": {\"0\": true, \"1\": false}}")); + + // varint keys + execute("INSERT INTO %s JSON ?", "{\"k\": 0, \"varintmap\": {\"0\": true, \"1\": false}}"); + assertRows(execute("SELECT JSON k, varintmap FROM %s"), row("{\"k\": 0, \"varintmap\": {\"0\": true, \"1\": false}}")); + + // boolean keys + execute("INSERT INTO %s JSON ?", "{\"k\": 0, \"booleanmap\": {\"true\": true, \"false\": false}}"); + assertRows(execute("SELECT JSON k, booleanmap FROM %s"), row("{\"k\": 0, \"booleanmap\": {\"false\": false, \"true\": true}}")); + + // float keys + execute("INSERT INTO %s JSON ?", "{\"k\": 0, \"floatmap\": {\"1.23\": true, \"4.56\": false}}"); + assertRows(execute("SELECT JSON k, floatmap FROM %s"), row("{\"k\": 0, \"floatmap\": {\"1.23\": true, \"4.56\": false}}")); + + // double keys + execute("INSERT INTO %s JSON ?", "{\"k\": 0, \"doublemap\": {\"1.23\": true, \"4.56\": false}}"); + assertRows(execute("SELECT JSON k, doublemap FROM %s"), row("{\"k\": 0, \"doublemap\": {\"1.23\": true, \"4.56\": false}}")); + + // decimal keys + execute("INSERT INTO %s JSON ?", "{\"k\": 0, \"decimalmap\": {\"1.23\": true, \"4.56\": false}}"); + assertRows(execute("SELECT JSON k, decimalmap FROM %s"), row("{\"k\": 0, \"decimalmap\": {\"1.23\": true, \"4.56\": false}}")); + + // tuple<int, text> keys + execute("INSERT INTO %s JSON ?", "{\"k\": 0, \"tuplemap\": {\"[0, \\\"a\\\"]\": true, \"[1, \\\"b\\\"]\": false}}"); + assertRows(execute("SELECT JSON k, tuplemap FROM %s"), row("{\"k\": 0, \"tuplemap\": {\"[0, \\\"a\\\"]\": true, \"[1, \\\"b\\\"]\": false}}")); + + // UDT keys + execute("INSERT INTO %s JSON ?", "{\"k\": 0, \"udtmap\": {\"{\\\"a\\\": 0}\": true, \"{\\\"a\\\": 1}\": false}}"); + assertRows(execute("SELECT JSON k, udtmap FROM %s"), row("{\"k\": 0, \"udtmap\": {\"{\\\"a\\\": 0}\": true, \"{\\\"a\\\": 1}\": false}}")); + + // set<int> keys + execute("INSERT INTO %s JSON ?", "{\"k\": 0, \"setmap\": {\"[0, 1, 2]\": true, \"[3, 4, 5]\": false}}"); + assertRows(execute("SELECT JSON k, setmap FROM %s"), row("{\"k\": 0, \"setmap\": {\"[0, 1, 2]\": true, \"[3, 4, 5]\": false}}")); + + // list<int> keys + execute("INSERT INTO %s JSON ?", "{\"k\": 0, \"listmap\": {\"[0, 1, 2]\": true, \"[3, 4, 5]\": false}}"); + assertRows(execute("SELECT JSON k, listmap FROM %s"), row("{\"k\": 0, \"listmap\": {\"[0, 1, 2]\": true, \"[3, 4, 5]\": false}}")); + + // set<text> keys + execute("INSERT INTO %s JSON ?", "{\"k\": 0, \"textsetmap\": {\"[\\\"0\\\", \\\"1\\\"]\": true, \"[\\\"3\\\", \\\"4\\\"]\": false}}"); + assertRows(execute("SELECT JSON k, textsetmap FROM %s"), row("{\"k\": 0, \"textsetmap\": {\"[\\\"0\\\", \\\"1\\\"]\": true, \"[\\\"3\\\", \\\"4\\\"]\": false}}")); + + // map<set<text>, text> keys + String innerKey1 = "[\"0\", \"1\"]"; + String fullKey1 = String.format("{\"%s\": \"%s\"}", new String(Json.JSON_STRING_ENCODER.quoteAsString(innerKey1)), "a"); + String stringKey1 = new String(Json.JSON_STRING_ENCODER.quoteAsString(fullKey1)); + String innerKey2 = "[\"3\", \"4\"]"; + String fullKey2 = String.format("{\"%s\": \"%s\"}", new String(Json.JSON_STRING_ENCODER.quoteAsString(innerKey2)), "b"); + String stringKey2 = new String(Json.JSON_STRING_ENCODER.quoteAsString(fullKey2)); + execute("INSERT INTO %s JSON ?", "{\"k\": 0, \"nestedsetmap\": {\"" + stringKey1 + "\": true, \"" + stringKey2 + "\": false}}"); + assertRows(execute("SELECT JSON k, nestedsetmap FROM %s"), row("{\"k\": 0, \"nestedsetmap\": {\"" + stringKey1 + "\": true, \"" + stringKey2 + "\": false}}")); + + // set<int> keys in a frozen map + execute("INSERT INTO %s JSON ?", "{\"k\": 0, \"frozensetmap\": {\"[0, 1, 2]\": true, \"[3, 4, 5]\": false}}"); + assertRows(execute("SELECT JSON k, frozensetmap FROM %s"), row("{\"k\": 0, \"frozensetmap\": {\"[0, 1, 2]\": true, \"[3, 4, 5]\": false}}")); + } + + @Test public void testInsertJsonSyntaxWithTuplesAndUDTs() throws Throwable { String typeName = createType("CREATE TYPE %s (a int, b frozen<set<int>>, c tuple<int, int>)");