You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by br...@apache.org on 2020/07/22 17:56:01 UTC

[cassandra] branch cassandra-3.0 updated: Fix empty/null json string representation

This is an automated email from the ASF dual-hosted git repository.

brandonwilliams pushed a commit to branch cassandra-3.0
in repository https://gitbox.apache.org/repos/asf/cassandra.git


The following commit(s) were added to refs/heads/cassandra-3.0 by this push:
     new 64fa75d  Fix empty/null json string representation
64fa75d is described below

commit 64fa75dd92f2b555ee1650b0c140f911c3b32b97
Author: Bereng <be...@gmail.com>
AuthorDate: Wed Jul 22 12:15:12 2020 +0200

    Fix empty/null json string representation
    
    Patch by Berenguer Blasi, reviewed by brandonwilliams for CASSANDRA-1589
---
 CHANGES.txt                                        |   1 +
 .../apache/cassandra/db/marshal/AbstractType.java  |   3 +-
 .../apache/cassandra/db/marshal/DecimalType.java   |   7 +-
 .../apache/cassandra/db/marshal/DoubleType.java    |   2 +
 .../org/apache/cassandra/db/marshal/FloatType.java |   2 +
 .../cassandra/db/marshal/InetAddressType.java      |   7 +-
 .../org/apache/cassandra/db/marshal/Int32Type.java |   3 +-
 .../apache/cassandra/db/marshal/IntegerType.java   |   3 +-
 .../org/apache/cassandra/db/marshal/LongType.java  |   3 +-
 .../org/apache/cassandra/db/marshal/ShortType.java |   3 +-
 .../apache/cassandra/db/marshal/TimestampType.java |   7 +-
 .../org/apache/cassandra/cql3/EmptyValuesTest.java | 240 +++++++++++++++++++++
 .../apache/cassandra/db/marshal/UUIDTypeTest.java  |   8 +
 13 files changed, 279 insertions(+), 10 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 138be5d..43f4a69 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 3.0.21
+ * Fix empty/null json string representation (CASSANDRA-15896)
  * Backport CASSANDRA-12189: escape string literals (CASSANDRA-15948)
  * Avoid hinted handoff per-host throttle being arounded to 0 in large cluster (CASSANDRA-15859)
  * Avoid emitting empty range tombstones from RangeTombstoneList (CASSANDRA-15924)
diff --git a/src/java/org/apache/cassandra/db/marshal/AbstractType.java b/src/java/org/apache/cassandra/db/marshal/AbstractType.java
index a15dd48..b14af70 100644
--- a/src/java/org/apache/cassandra/db/marshal/AbstractType.java
+++ b/src/java/org/apache/cassandra/db/marshal/AbstractType.java
@@ -26,6 +26,7 @@ import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -150,7 +151,7 @@ public abstract class AbstractType<T> implements Comparator<ByteBuffer>
      */
     public String toJSONString(ByteBuffer buffer, int protocolVersion)
     {
-        return '"' + getSerializer().deserialize(buffer).toString() + '"';
+        return '"' + Objects.toString(getSerializer().deserialize(buffer), "") + '"';
     }
 
     /* validate that the byte array is a valid sequence for the type we are supposed to be comparing */
diff --git a/src/java/org/apache/cassandra/db/marshal/DecimalType.java b/src/java/org/apache/cassandra/db/marshal/DecimalType.java
index 17d91d3..43b2711 100644
--- a/src/java/org/apache/cassandra/db/marshal/DecimalType.java
+++ b/src/java/org/apache/cassandra/db/marshal/DecimalType.java
@@ -19,6 +19,7 @@ package org.apache.cassandra.db.marshal;
 
 import java.math.BigDecimal;
 import java.nio.ByteBuffer;
+import java.util.Objects;
 
 import org.apache.cassandra.cql3.CQL3Type;
 import org.apache.cassandra.cql3.Constants;
@@ -71,9 +72,9 @@ public class DecimalType extends AbstractType<BigDecimal>
     {
         try
         {
-            return new Constants.Value(getSerializer().serialize(new BigDecimal(parsed.toString())));
+            return new Constants.Value(fromString(Objects.toString(parsed)));
         }
-        catch (NumberFormatException exc)
+        catch (NumberFormatException | MarshalException exc)
         {
             throw new MarshalException(String.format("Value '%s' is not a valid representation of a decimal value", parsed));
         }
@@ -82,7 +83,7 @@ public class DecimalType extends AbstractType<BigDecimal>
     @Override
     public String toJSONString(ByteBuffer buffer, int protocolVersion)
     {
-        return getSerializer().deserialize(buffer).toString();
+        return Objects.toString(getSerializer().deserialize(buffer), "\"\"");
     }
 
     public CQL3Type asCQL3Type()
diff --git a/src/java/org/apache/cassandra/db/marshal/DoubleType.java b/src/java/org/apache/cassandra/db/marshal/DoubleType.java
index d39059b..17d9cf5 100644
--- a/src/java/org/apache/cassandra/db/marshal/DoubleType.java
+++ b/src/java/org/apache/cassandra/db/marshal/DoubleType.java
@@ -86,6 +86,8 @@ public class DoubleType extends AbstractType<Double>
     public String toJSONString(ByteBuffer buffer, int protocolVersion)
     {
         Double value = getSerializer().deserialize(buffer);
+        if (value == null)
+            return "\"\"";
         // JSON does not support NaN, Infinity and -Infinity values. Most of the parser convert them into null.
         if (value.isNaN() || value.isInfinite())
             return "null";
diff --git a/src/java/org/apache/cassandra/db/marshal/FloatType.java b/src/java/org/apache/cassandra/db/marshal/FloatType.java
index 58f5702..0ff02a3 100644
--- a/src/java/org/apache/cassandra/db/marshal/FloatType.java
+++ b/src/java/org/apache/cassandra/db/marshal/FloatType.java
@@ -85,6 +85,8 @@ public class FloatType extends AbstractType<Float>
     public String toJSONString(ByteBuffer buffer, int protocolVersion)
     {
         Float value = getSerializer().deserialize(buffer);
+        if (value == null)
+            return "\"\"";
         // JSON does not support NaN, Infinity and -Infinity values. Most of the parser convert them into null.
         if (value.isNaN() || value.isInfinite())
             return "null";
diff --git a/src/java/org/apache/cassandra/db/marshal/InetAddressType.java b/src/java/org/apache/cassandra/db/marshal/InetAddressType.java
index 7ffb9c7..393f09e 100644
--- a/src/java/org/apache/cassandra/db/marshal/InetAddressType.java
+++ b/src/java/org/apache/cassandra/db/marshal/InetAddressType.java
@@ -73,10 +73,15 @@ public class InetAddressType extends AbstractType<InetAddress>
         }
     }
 
+    private String toString(InetAddress inet)
+    {
+        return inet != null ? inet.getHostAddress() : "";
+    }
+
     @Override
     public String toJSONString(ByteBuffer buffer, int protocolVersion)
     {
-        return '"' + getSerializer().deserialize(buffer).getHostAddress() + '"';
+        return '"' + toString(getSerializer().deserialize(buffer)) + '"';
     }
 
     public CQL3Type asCQL3Type()
diff --git a/src/java/org/apache/cassandra/db/marshal/Int32Type.java b/src/java/org/apache/cassandra/db/marshal/Int32Type.java
index 770a76d..9ac7d5b 100644
--- a/src/java/org/apache/cassandra/db/marshal/Int32Type.java
+++ b/src/java/org/apache/cassandra/db/marshal/Int32Type.java
@@ -18,6 +18,7 @@
 package org.apache.cassandra.db.marshal;
 
 import java.nio.ByteBuffer;
+import java.util.Objects;
 
 import org.apache.cassandra.cql3.CQL3Type;
 import org.apache.cassandra.cql3.Constants;
@@ -97,7 +98,7 @@ public class Int32Type extends AbstractType<Integer>
     @Override
     public String toJSONString(ByteBuffer buffer, int protocolVersion)
     {
-        return getSerializer().deserialize(buffer).toString();
+        return Objects.toString(getSerializer().deserialize(buffer), "\"\"");
     }
 
     public CQL3Type asCQL3Type()
diff --git a/src/java/org/apache/cassandra/db/marshal/IntegerType.java b/src/java/org/apache/cassandra/db/marshal/IntegerType.java
index 8f4ba44..f65856e 100644
--- a/src/java/org/apache/cassandra/db/marshal/IntegerType.java
+++ b/src/java/org/apache/cassandra/db/marshal/IntegerType.java
@@ -19,6 +19,7 @@ package org.apache.cassandra.db.marshal;
 
 import java.math.BigInteger;
 import java.nio.ByteBuffer;
+import java.util.Objects;
 
 import org.apache.cassandra.cql3.CQL3Type;
 import org.apache.cassandra.cql3.Constants;
@@ -165,7 +166,7 @@ public final class IntegerType extends AbstractType<BigInteger>
     @Override
     public String toJSONString(ByteBuffer buffer, int protocolVersion)
     {
-        return getSerializer().deserialize(buffer).toString();
+        return Objects.toString(getSerializer().deserialize(buffer), "\"\"");
     }
 
     @Override
diff --git a/src/java/org/apache/cassandra/db/marshal/LongType.java b/src/java/org/apache/cassandra/db/marshal/LongType.java
index 8a1528a..d63229f 100644
--- a/src/java/org/apache/cassandra/db/marshal/LongType.java
+++ b/src/java/org/apache/cassandra/db/marshal/LongType.java
@@ -18,6 +18,7 @@
 package org.apache.cassandra.db.marshal;
 
 import java.nio.ByteBuffer;
+import java.util.Objects;
 
 import org.apache.cassandra.cql3.CQL3Type;
 import org.apache.cassandra.cql3.Constants;
@@ -99,7 +100,7 @@ public class LongType extends AbstractType<Long>
     @Override
     public String toJSONString(ByteBuffer buffer, int protocolVersion)
     {
-        return getSerializer().deserialize(buffer).toString();
+        return Objects.toString(getSerializer().deserialize(buffer), "\"\"");
     }
 
     @Override
diff --git a/src/java/org/apache/cassandra/db/marshal/ShortType.java b/src/java/org/apache/cassandra/db/marshal/ShortType.java
index 482fd81..22f134a 100644
--- a/src/java/org/apache/cassandra/db/marshal/ShortType.java
+++ b/src/java/org/apache/cassandra/db/marshal/ShortType.java
@@ -18,6 +18,7 @@
 package org.apache.cassandra.db.marshal;
 
 import java.nio.ByteBuffer;
+import java.util.Objects;
 
 import org.apache.cassandra.cql3.CQL3Type;
 import org.apache.cassandra.cql3.Constants;
@@ -77,7 +78,7 @@ public class ShortType extends AbstractType<Short>
     @Override
     public String toJSONString(ByteBuffer buffer, int protocolVersion)
     {
-        return getSerializer().deserialize(buffer).toString();
+        return Objects.toString(getSerializer().deserialize(buffer), "\"\"");
     }
 
     @Override
diff --git a/src/java/org/apache/cassandra/db/marshal/TimestampType.java b/src/java/org/apache/cassandra/db/marshal/TimestampType.java
index 45b08d9..a2d92ba 100644
--- a/src/java/org/apache/cassandra/db/marshal/TimestampType.java
+++ b/src/java/org/apache/cassandra/db/marshal/TimestampType.java
@@ -87,10 +87,15 @@ public class TimestampType extends AbstractType<Date>
         }
     }
 
+    private String toString(Date date)
+    {
+        return date != null ? TimestampSerializer.getJsonDateFormatter().format(date) : "";
+    }
+
     @Override
     public String toJSONString(ByteBuffer buffer, int protocolVersion)
     {
-        return '"' + TimestampSerializer.getJsonDateFormatter().format(TimestampSerializer.instance.deserialize(buffer)) + '"';
+        return '"' + toString(TimestampSerializer.instance.deserialize(buffer)) + '"';
     }
 
     @Override
diff --git a/test/unit/org/apache/cassandra/cql3/EmptyValuesTest.java b/test/unit/org/apache/cassandra/cql3/EmptyValuesTest.java
new file mode 100644
index 0000000..6c42a59
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/EmptyValuesTest.java
@@ -0,0 +1,240 @@
+/*
+ * 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.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
+import java.util.regex.Pattern;
+
+import org.apache.commons.io.IOUtils;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.Row;
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.marshal.BytesType;
+import org.apache.cassandra.db.marshal.DecimalType;
+import org.apache.cassandra.db.marshal.DoubleType;
+import org.apache.cassandra.db.marshal.FloatType;
+import org.apache.cassandra.db.marshal.InetAddressType;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.marshal.LongType;
+import org.apache.cassandra.db.marshal.ShortType;
+import org.apache.cassandra.db.marshal.SimpleDateType;
+import org.apache.cassandra.db.marshal.TimeType;
+import org.apache.cassandra.db.marshal.TimeUUIDType;
+import org.apache.cassandra.db.marshal.TimestampType;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.db.marshal.UUIDType;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.transport.Server;
+import org.apache.cassandra.utils.ByteBufferUtil;
+
+import static org.junit.Assume.assumeTrue;
+
+public class EmptyValuesTest extends CQLTester
+{
+    private void verify(String emptyValue) throws Throwable
+    {
+        UntypedResultSet result = execute("SELECT * FROM %s");
+        UntypedResultSet.Row row = result.one();
+        Assert.assertTrue(row.getColumns().stream().anyMatch(c -> c.name.toString().equals("v")));
+        Assert.assertEquals(0, row.getBytes("v").remaining());
+
+        ResultSet resultNet = executeNet(Server.CURRENT_VERSION, "SELECT * FROM %s");
+        Row rowNet = resultNet.one();
+        Assert.assertTrue(rowNet.getColumnDefinitions().contains("v"));
+        Assert.assertEquals(0, rowNet.getBytesUnsafe("v").remaining());
+
+        ResultSet jsonNet = executeNet(Server.CURRENT_VERSION, "SELECT JSON * FROM %s");
+        Row jsonRowNet = jsonNet.one();
+        Assert.assertTrue(jsonRowNet.getString("[json]"), jsonRowNet.getString("[json]").matches(".*\"v\"\\s*:\\s*\"" + Pattern.quote(emptyValue) + "\".*"));
+
+        ColumnFamilyStore cfs = getCurrentColumnFamilyStore();
+        ByteArrayOutputStream buf = new ByteArrayOutputStream();
+        for (SSTableReader ssTable : cfs.getLiveSSTables())
+        {
+            try (PrintStream out = new PrintStream(buf, true))
+            {
+                ProcessBuilder pb = new ProcessBuilder("tools/bin/sstabledump", ssTable.getFilename());
+                Process process = pb.start();
+                process.waitFor();
+                IOUtils.copy(process.getInputStream(), buf);
+            }
+            catch (Throwable t)
+            {
+                Assert.fail(t.getClass().getName());
+            }
+        }
+        
+        String outString = new String(buf.toByteArray(), StandardCharsets.UTF_8);
+        Assert.assertTrue(outString, outString.contains("{ \"name\" : \"v\", \"value\" : \"" + emptyValue + "\" }"));
+    }
+
+    private void verifyPlainInsert(String emptyValue) throws Throwable
+    {
+        execute("TRUNCATE %s");
+
+        // In most cases we cannot insert empty value when we do not bind variables
+        // This is due to the current implementation of org.apache.cassandra.cql3.Constants.Literal.testAssignment
+        // execute("INSERT INTO %s (id, v) VALUES (1, '" + emptyValue + "')");
+        execute("INSERT INTO %s (id, v) VALUES (1, ?)", ByteBufferUtil.EMPTY_BYTE_BUFFER);
+        flush();
+
+        verify(emptyValue);
+    }
+
+    private void verifyJsonInsert(String emptyValue) throws Throwable
+    {
+        execute("TRUNCATE %s");
+        execute("INSERT INTO %s JSON '{\"id\":\"1\",\"v\":\"" + emptyValue + "\"}'");
+        flush();
+
+        verify(emptyValue);
+    }
+
+    @Test
+    public void testEmptyInt() throws Throwable
+    {
+        assumeTrue(Int32Type.instance.isEmptyValueMeaningless());
+        String table = createTable("CREATE TABLE %s (id INT PRIMARY KEY, v INT)");
+        verifyJsonInsert("");
+        verifyPlainInsert("");
+    }
+
+    @Test
+    public void testEmptyText() throws Throwable
+    {
+        assumeTrue(UTF8Type.instance.isEmptyValueMeaningless());
+        String table = createTable("CREATE TABLE %s (id INT PRIMARY KEY, v TEXT)");
+        verifyJsonInsert("");
+        verifyPlainInsert("");
+    }
+
+    @Test
+    public void testEmptyTimestamp() throws Throwable
+    {
+        assumeTrue(TimestampType.instance.isEmptyValueMeaningless());
+        String table = createTable("CREATE TABLE %s (id INT PRIMARY KEY, v TIMESTAMP)");
+        verifyJsonInsert("");
+        verifyPlainInsert("");
+    }
+
+    @Test
+    public void testEmptyUUID() throws Throwable
+    {
+        assumeTrue(UUIDType.instance.isEmptyValueMeaningless());
+        String table = createTable("CREATE TABLE %s (id INT PRIMARY KEY, v UUID)");
+        verifyJsonInsert("");
+        verifyPlainInsert("");
+    }
+
+    @Test
+    public void testEmptyInetAddress() throws Throwable
+    {
+        assumeTrue(InetAddressType.instance.isEmptyValueMeaningless());
+        String table = createTable("CREATE TABLE %s (id INT PRIMARY KEY, v INET)");
+        verifyJsonInsert("");
+        verifyPlainInsert("");
+    }
+
+    @Test
+    public void testEmptyLong() throws Throwable
+    {
+        assumeTrue(LongType.instance.isEmptyValueMeaningless());
+        String table = createTable("CREATE TABLE %s (id INT PRIMARY KEY, v BIGINT)");
+        verifyJsonInsert("");
+        verifyPlainInsert("");
+    }
+
+    @Test
+    public void testEmptyBytes() throws Throwable
+    {
+        assumeTrue(BytesType.instance.isEmptyValueMeaningless());
+        String table = createTable("CREATE TABLE %s (id INT PRIMARY KEY, v BLOB)");
+        verifyJsonInsert("0x");
+        verifyPlainInsert("");
+    }
+
+    @Test
+    public void testEmptyDate() throws Throwable
+    {
+        assumeTrue(SimpleDateType.instance.isEmptyValueMeaningless());
+        String table = createTable("CREATE TABLE %s (id INT PRIMARY KEY, v DATE)");
+        verifyJsonInsert("");
+        verifyPlainInsert("");
+    }
+
+    @Test
+    public void testEmptyDecimal() throws Throwable
+    {
+        assumeTrue(DecimalType.instance.isEmptyValueMeaningless());
+        String table = createTable("CREATE TABLE %s (id INT PRIMARY KEY, v DECIMAL)");
+        verifyJsonInsert("");
+        verifyPlainInsert("");
+    }
+
+    @Test
+    public void testEmptyDouble() throws Throwable
+    {
+        assumeTrue(DoubleType.instance.isEmptyValueMeaningless());
+        String table = createTable("CREATE TABLE %s (id INT PRIMARY KEY, v DOUBLE)");
+        verifyJsonInsert("");
+        verifyPlainInsert("");
+    }
+
+    @Test
+    public void testEmptyFloat() throws Throwable
+    {
+        assumeTrue(FloatType.instance.isEmptyValueMeaningless());
+        String table = createTable("CREATE TABLE %s (id INT PRIMARY KEY, v FLOAT)");
+        verifyJsonInsert("");
+        verifyPlainInsert("");
+    }
+
+    @Test
+    public void testEmptySmallInt() throws Throwable
+    {
+        assumeTrue(ShortType.instance.isEmptyValueMeaningless());
+        String table = createTable("CREATE TABLE %s (id INT PRIMARY KEY, v SMALLINT)");
+        verifyJsonInsert("");
+        verifyPlainInsert("");
+    }
+
+    @Test
+    public void testEmptyTime() throws Throwable
+    {
+        assumeTrue(TimeType.instance.isEmptyValueMeaningless());
+        String table = createTable("CREATE TABLE %s (id INT PRIMARY KEY, v TIME)");
+        verifyJsonInsert("");
+        verifyPlainInsert("");
+    }
+
+    @Test
+    public void testEmptyTimeUUID() throws Throwable
+    {
+        assumeTrue(TimeUUIDType.instance.isEmptyValueMeaningless());
+        String table = createTable("CREATE TABLE %s (id INT PRIMARY KEY, v TIMEUUID)");
+        verifyJsonInsert("");
+        verifyPlainInsert("");
+    }
+}
diff --git a/test/unit/org/apache/cassandra/db/marshal/UUIDTypeTest.java b/test/unit/org/apache/cassandra/db/marshal/UUIDTypeTest.java
index 335860c..74e5ef3 100644
--- a/test/unit/org/apache/cassandra/db/marshal/UUIDTypeTest.java
+++ b/test/unit/org/apache/cassandra/db/marshal/UUIDTypeTest.java
@@ -31,6 +31,7 @@ import java.util.UUID;
 import org.junit.Test;
 
 import junit.framework.Assert;
+import org.apache.cassandra.transport.Server;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.UUIDGen;
 import org.slf4j.Logger;
@@ -43,6 +44,13 @@ public class UUIDTypeTest
 
     UUIDType uuidType = new UUIDType();
 
+    @Test //CASSANDRA-15896
+    public void testToJsonEmptyValue()
+    {
+        String res = uuidType.toJSONString(uuidType.fromJSONObject("").bindAndGet(null), Server.CURRENT_VERSION);
+        assertEquals("\"\"", res);
+    }
+
     @Test
     public void testRandomCompare()
     {


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org