You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@datasketches.apache.org by jm...@apache.org on 2021/07/28 20:36:18 UTC

[datasketches-server] branch test_coverage updated: finish update coverage, use HttpStatus class as source of status codes eveerywhere -- and actually add the files this time

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

jmalkin pushed a commit to branch test_coverage
in repository https://gitbox.apache.org/repos/asf/datasketches-server.git


The following commit(s) were added to refs/heads/test_coverage by this push:
     new c3f59da  finish update coverage, use HttpStatus class as source of status codes eveerywhere -- and actually add the files this time
c3f59da is described below

commit c3f59da094e29c3cc7d562eab36894eb77d4f8cf
Author: Jon Malkin <jm...@users.noreply.github.com>
AuthorDate: Wed Jul 28 13:35:15 2021 -0700

    finish update coverage, use HttpStatus class as source of status codes eveerywhere -- and actually add the files this time
---
 .../server/BaseSketchesQueryHandler.java           |  24 +-
 .../datasketches/server/SketchConstants.java       |   3 -
 .../apache/datasketches/server/SketchServer.java   |   1 +
 .../datasketches/server/SketchServerConfig.java    |  47 ++-
 .../apache/datasketches/server/SketchStorage.java  |   5 +-
 .../apache/datasketches/server/UpdateHandler.java  | 108 +++---
 .../apache/datasketches/server/ServerTestBase.java |  33 +-
 .../datasketches/server/SketchStorageTest.java     |   4 +-
 .../datasketches/server/UpdateHandlerTest.java     | 383 +++++++++++++++++++--
 src/test/resources/test_config.json                | 134 ++++---
 10 files changed, 588 insertions(+), 154 deletions(-)

diff --git a/src/main/java/org/apache/datasketches/server/BaseSketchesQueryHandler.java b/src/main/java/org/apache/datasketches/server/BaseSketchesQueryHandler.java
index 211733a..7a1bcf6 100644
--- a/src/main/java/org/apache/datasketches/server/BaseSketchesQueryHandler.java
+++ b/src/main/java/org/apache/datasketches/server/BaseSketchesQueryHandler.java
@@ -27,10 +27,11 @@ import java.io.Reader;
 import java.net.URLDecoder;
 
 import org.apache.datasketches.Family;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.eclipse.jetty.http.HttpStatus;
 import org.eclipse.jetty.server.Request;
 import org.eclipse.jetty.server.handler.AbstractHandler;
 
-import com.google.gson.GsonBuilder;
 import com.google.gson.JsonArray;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
@@ -59,7 +60,7 @@ public abstract class BaseSketchesQueryHandler extends AbstractHandler {
    * Basic query handler. Assumes calls must include a JSON query.
    * @param sketches The sketches database to use
    */
-  BaseSketchesQueryHandler(final SketchStorage sketches) {
+  BaseSketchesQueryHandler(@NonNull final SketchStorage sketches) {
     this(sketches, false);
   }
 
@@ -68,10 +69,7 @@ public abstract class BaseSketchesQueryHandler extends AbstractHandler {
    * @param sketches The sketches database to use
    * @param queryExempt <tt>true</tt> if a query is not required, otherwise <tt>false</tt>
    */
-  BaseSketchesQueryHandler(final SketchStorage sketches, final boolean queryExempt) {
-    if (sketches == null) {
-      throw new IllegalArgumentException("Cannot initialize handler with SketchStorage == null");
-    }
+  BaseSketchesQueryHandler(@NonNull final SketchStorage sketches, final boolean queryExempt) {
     this.sketches = sketches;
     this.queryExempt = queryExempt;
   }
@@ -89,7 +87,7 @@ public abstract class BaseSketchesQueryHandler extends AbstractHandler {
       response.setContentType("text/html");
       query = JsonParser.parseString(URLDecoder.decode(request.getQueryString(), "utf-8"));
     } else {
-      response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
+      response.setStatus(HttpStatus.METHOD_NOT_ALLOWED_405);
       baseRequest.setHandled(true);
     }
 
@@ -134,27 +132,27 @@ public abstract class BaseSketchesQueryHandler extends AbstractHandler {
           }
         }
       } else {
-        //result = callProcessQuery((JsonObject) query);
         result = processQuery((JsonObject) query);
       }
 
       if (result != null) {
-        //response.getWriter().print(result.toString());
-        response.getWriter().print(new GsonBuilder().setPrettyPrinting().create().toJson(result));
+        response.getWriter().print(result);
+        //response.getWriter().print(new GsonBuilder().setPrettyPrinting().create().toJson(result));
       }
 
       // we're ok if we reach here without an exception
-      response.setStatus(HttpServletResponse.SC_OK);
+      response.setStatus(HttpStatus.OK_200);
     } catch (final Exception e) {
       final JsonObject error = new JsonObject();
       error.addProperty(ERROR_KEY, e.getMessage());
-      response.setStatus(UNPROCESSABLE_ENTITY);
+      response.getWriter().print(error);
+      response.setStatus(HttpStatus.UNPROCESSABLE_ENTITY_422);
     }
 
     baseRequest.setHandled(true);
   }
 
-  static Family familyFromString(final String type) throws IllegalArgumentException {
+  static Family familyFromString(@NonNull final String type) throws IllegalArgumentException {
     switch (type.toLowerCase()) {
       case SKETCH_FAMILY_THETA:
         return Family.QUICKSELECT;
diff --git a/src/main/java/org/apache/datasketches/server/SketchConstants.java b/src/main/java/org/apache/datasketches/server/SketchConstants.java
index 1e4059b..c363148 100644
--- a/src/main/java/org/apache/datasketches/server/SketchConstants.java
+++ b/src/main/java/org/apache/datasketches/server/SketchConstants.java
@@ -107,8 +107,5 @@ public final class SketchConstants {
   // server configuration
   public static final int DEFAULT_PORT = 8080;
 
-  // response codes
-  public static final int UNPROCESSABLE_ENTITY = 422; // defined, but not in HttpServletResponse.SC_* codes
-
   public static final String ERROR_KEY = "error";
 }
diff --git a/src/main/java/org/apache/datasketches/server/SketchServer.java b/src/main/java/org/apache/datasketches/server/SketchServer.java
index e0a8a0e..b693d3e 100644
--- a/src/main/java/org/apache/datasketches/server/SketchServer.java
+++ b/src/main/java/org/apache/datasketches/server/SketchServer.java
@@ -165,5 +165,6 @@ public class SketchServer {
 
     final SketchServer sketchServer = new SketchServer(args[0]);
     sketchServer.start();
+    sketchServer.server.join();
   }
 }
diff --git a/src/main/java/org/apache/datasketches/server/SketchServerConfig.java b/src/main/java/org/apache/datasketches/server/SketchServerConfig.java
index 447e36e..97cf391 100644
--- a/src/main/java/org/apache/datasketches/server/SketchServerConfig.java
+++ b/src/main/java/org/apache/datasketches/server/SketchServerConfig.java
@@ -19,6 +19,7 @@
 
 package org.apache.datasketches.server;
 
+import static org.apache.datasketches.server.BaseSketchesQueryHandler.familyFromString;
 import static org.apache.datasketches.server.SketchStorage.isDistinctCounting;
 
 import java.io.IOException;
@@ -51,12 +52,20 @@ class SketchServerConfig {
     public String family;
     public String type;
 
-    SketchInfo(final String name, final int k, final String family, final String type) {
+    SketchInfo(@NonNull final String name, final int k, @NonNull final String family, final String type) {
       this.name = name;
       this.k = k;
       this.family = family;
       this.type = type;
     }
+
+    @Override
+    public String toString() {
+      return "Name  : " + name + "\n" +
+          "k     : " + k + "\n" +
+          "Family: " + family + "\n" +
+          "Type  : " + type + "\n";
+    }
   }
 
   private int port = DEFAULT_PORT;
@@ -83,6 +92,24 @@ class SketchServerConfig {
     return JsonParser.parseReader(reader);
   }
 
+  private String validateSketchInfo() {
+    final StringBuilder sb = new StringBuilder();
+
+    for (final SketchInfo info : sketchList) {
+      if (info.name == null) {
+        sb.append("Missing sketch name: ").append(new Gson().toJson(info)).append("\n");
+      } else if (info.k == 0) { // primitive so cannot be null
+        sb.append("Missing k parameter: ").append(new Gson().toJson(info)).append("\n");
+      } else if (info.family == null) {
+        sb.append("Missing sketch family: ").append(new Gson().toJson(info)).append("\n");
+      } else if (isDistinctCounting(familyFromString(info.family)) && info.type == null) {
+        sb.append("Missing value type: ").append(new Gson().toJson(info)).append("\n");
+      }
+    }
+
+    return sb.length() == 0 ? null : sb.toString();
+  }
+
   private void parseConfig(final JsonElement config) throws IOException {
     final Gson gson = new Gson();
 
@@ -104,10 +131,21 @@ class SketchServerConfig {
         } else if (name.toLowerCase().startsWith(CONFIG_SET_PREFIX)) {
           // set* has a common name and type with an array of name names
           final JsonObject sketchSetInfo = confEntry.get(name).getAsJsonObject();
+          if (!sketchSetInfo.has(CONFIG_K_FIELD)) {
+            throw new IOException("Missing field: \"" + CONFIG_K_FIELD + " \": " + sketchSetInfo);
+          }
           final int k = sketchSetInfo.get(CONFIG_K_FIELD).getAsInt();
+
+          if (!sketchSetInfo.has(CONFIG_FAMILY_FIELD)) {
+            throw new IOException("Missing field: \"" + CONFIG_FAMILY_FIELD + " \": " + sketchSetInfo);
+          }
           final String family = sketchSetInfo.get(CONFIG_FAMILY_FIELD).getAsString();
+
           String type = null;
-          if (isDistinctCounting(BaseSketchesQueryHandler.familyFromString(family))) {
+          if (isDistinctCounting(familyFromString(family))) {
+            if (!sketchSetInfo.has(CONFIG_TYPE_FIELD)) {
+              throw new IOException("Missing field: \"" + CONFIG_TYPE_FIELD + " \": " + sketchSetInfo);
+            }
             type = sketchSetInfo.get(CONFIG_TYPE_FIELD).getAsString();
           }
           final String[] nameList = gson.fromJson(sketchSetInfo.get(CONFIG_SET_NAMES_FIELD).getAsJsonArray(), String[].class);
@@ -119,5 +157,10 @@ class SketchServerConfig {
     } else {
       throw new IOException("Expected JsonArray or JsonObject but none found");
     }
+
+    final String validation = validateSketchInfo();
+    if (validation != null) {
+      throw new IllegalArgumentException(validation);
+    }
   }
 }
diff --git a/src/main/java/org/apache/datasketches/server/SketchStorage.java b/src/main/java/org/apache/datasketches/server/SketchStorage.java
index e8bc491..6811013 100644
--- a/src/main/java/org/apache/datasketches/server/SketchStorage.java
+++ b/src/main/java/org/apache/datasketches/server/SketchStorage.java
@@ -153,13 +153,16 @@ public class SketchStorage {
     sketchMap = new HashMap<>(list.size());
 
     for (final SketchServerConfig.SketchInfo info : list) {
+      if (info.name == null) {
+        throw new IllegalArgumentException("Mussing sketch name:" + info);
+      }
       if (sketchMap.containsKey(info.name)) {
         throw new IllegalArgumentException("Duplicate sketch key: " + info.name);
       }
 
       SketchEntry sketchEntry = null;
       final Family family = BaseSketchesQueryHandler.familyFromString(info.family);
-      final int k = info.k; // to reduce derferences in code later
+      final int k = info.k; // to reduce dereferences in code later
 
       switch (family) {
         case QUICKSELECT:
diff --git a/src/main/java/org/apache/datasketches/server/UpdateHandler.java b/src/main/java/org/apache/datasketches/server/UpdateHandler.java
index 84dc15a..b6ff1ff 100644
--- a/src/main/java/org/apache/datasketches/server/UpdateHandler.java
+++ b/src/main/java/org/apache/datasketches/server/UpdateHandler.java
@@ -32,6 +32,8 @@ import org.apache.datasketches.theta.Union;
 import com.google.gson.JsonArray;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
+import sun.security.acl.AclEntryImpl;
+
 
 import static org.apache.datasketches.server.SketchConstants.*;
 
@@ -76,8 +78,8 @@ public class UpdateHandler extends BaseSketchesQueryHandler {
       final SketchStorage.SketchEntry se = sketches.getSketch(name);
       final JsonElement data = entry.getValue();
 
-      if (name == null || se == null || data == null) {
-        throw new IllegalArgumentException("Attempt to call update with missing name or sketch not found");
+      if (se == null || data == null) {
+        throw new IllegalArgumentException("Sketch not found or update with no data value(s)");
       }
 
       synchronized (name.intern()) {
@@ -98,7 +100,6 @@ public class UpdateHandler extends BaseSketchesQueryHandler {
                                          final JsonArray data) {
     switch (entry.family_) {
       case UNION: // theta
-        assert(entry.type_ != null);
         switch (entry.type_) {
           case FLOAT: case DOUBLE:
             for (final JsonElement e : data) { ((Union) entry.sketch_).update(e.getAsDouble()); }
@@ -113,7 +114,6 @@ public class UpdateHandler extends BaseSketchesQueryHandler {
         break;
 
       case CPC:
-        assert(entry.type_ != null);
         switch (entry.type_) {
           case FLOAT: case DOUBLE:
             for (final JsonElement e : data) { ((CpcSketch) entry.sketch_).update(e.getAsDouble()); }
@@ -128,16 +128,15 @@ public class UpdateHandler extends BaseSketchesQueryHandler {
         break;
 
       case HLL:
-        assert(entry.type_ != null);
         switch (entry.type_) {
           case FLOAT: case DOUBLE:
             for (final JsonElement e : data) { ((HllSketch) entry.sketch_).update(e.getAsDouble()); }
             break;
           case INT: case LONG:
-            for (final JsonElement e : data) { ((CpcSketch) entry.sketch_).update(e.getAsLong()); }
+            for (final JsonElement e : data) { ((HllSketch) entry.sketch_).update(e.getAsLong()); }
             break;
           case STRING: default:
-            for (final JsonElement e : data) { ((CpcSketch) entry.sketch_).update(e.getAsString()); }
+            for (final JsonElement e : data) { ((HllSketch) entry.sketch_).update(e.getAsString()); }
             break;
         }
         break;
@@ -148,18 +147,7 @@ public class UpdateHandler extends BaseSketchesQueryHandler {
 
       case FREQUENCY:
         for (final JsonElement e : data) {
-          if (e.isJsonObject()) {
-            final JsonObject inputPair = e.getAsJsonObject();
-            if (!inputPair.has(QUERY_PAIR_ITEM_FIELD) || !inputPair.has(QUERY_PAIR_WEIGHT_FIELD)) {
-              throw new IllegalArgumentException("Frequent Items input pairs must include both "
-                  + QUERY_PAIR_ITEM_FIELD + " and " + QUERY_PAIR_WEIGHT_FIELD + " values");
-            }
-            final String item = inputPair.get(QUERY_PAIR_ITEM_FIELD).getAsString();
-            final int weight = inputPair.get(QUERY_PAIR_WEIGHT_FIELD).getAsInt();
-            ((ItemsSketch<String>) entry.sketch_).update(item, weight);
-          } else {
-            ((ItemsSketch<String>) entry.sketch_).update(e.getAsString());
-          }
+          applyFrequentItemsUpdate((ItemsSketch<String>) entry.sketch_, e);
         }
         break;
 
@@ -169,18 +157,7 @@ public class UpdateHandler extends BaseSketchesQueryHandler {
 
       case VAROPT:
         for (final JsonElement e : data) {
-          if (e.isJsonObject()) {
-            final JsonObject inputPair = e.getAsJsonObject();
-            if (!inputPair.has(QUERY_PAIR_ITEM_FIELD) || !inputPair.has(QUERY_PAIR_WEIGHT_FIELD)) {
-              throw new IllegalArgumentException("VarOpt input pairs must include both "
-                  + QUERY_PAIR_ITEM_FIELD + " and " + QUERY_PAIR_WEIGHT_FIELD + " values");
-            }
-            final String item = inputPair.get(QUERY_PAIR_ITEM_FIELD).getAsString();
-            final double weight = inputPair.get(QUERY_PAIR_WEIGHT_FIELD).getAsDouble();
-            ((VarOptItemsSketch<String>) entry.sketch_).update(item, weight);
-          } else {
-            ((VarOptItemsSketch<String>) entry.sketch_).update(e.getAsString(), 1.0);
-          }
+          applyVarOptUpdate((VarOptItemsSketch<String>) entry.sketch_, e);
         }
         break;
 
@@ -194,7 +171,6 @@ public class UpdateHandler extends BaseSketchesQueryHandler {
                                           final JsonElement data) {
     switch (entry.family_) {
       case UNION:
-        assert(entry.type_ != null);
         switch (entry.type_) {
           case FLOAT: case DOUBLE:
             ((Union) entry.sketch_).update(data.getAsDouble());
@@ -209,7 +185,6 @@ public class UpdateHandler extends BaseSketchesQueryHandler {
         break;
 
       case CPC:
-        assert(entry.type_ != null);
         switch (entry.type_) {
           case FLOAT: case DOUBLE:
             ((CpcSketch) entry.sketch_).update(data.getAsDouble());
@@ -224,7 +199,6 @@ public class UpdateHandler extends BaseSketchesQueryHandler {
         break;
 
       case HLL:
-        assert(entry.type_ != null);
         switch (entry.type_) {
           case FLOAT: case DOUBLE:
             ((HllSketch) entry.sketch_).update(data.getAsDouble());
@@ -243,18 +217,7 @@ public class UpdateHandler extends BaseSketchesQueryHandler {
         break;
 
       case FREQUENCY:
-        if (data.isJsonObject()) {
-          final JsonObject inputPair = data.getAsJsonObject();
-          if (!inputPair.has(QUERY_PAIR_ITEM_FIELD) || !inputPair.has(QUERY_PAIR_WEIGHT_FIELD)) {
-            throw new IllegalArgumentException("Frequent Items input pairs must include both "
-                + QUERY_PAIR_ITEM_FIELD + " and " + QUERY_PAIR_WEIGHT_FIELD + " values");
-          }
-          final String item = inputPair.get(QUERY_PAIR_ITEM_FIELD).getAsString();
-          final int weight = inputPair.get(QUERY_PAIR_WEIGHT_FIELD).getAsInt();
-          ((ItemsSketch<String>) entry.sketch_).update(item, weight);
-        } else {
-          ((ItemsSketch<String>) entry.sketch_).update(data.getAsString());
-        }
+        applyFrequentItemsUpdate((ItemsSketch<String>) entry.sketch_, data);
         break;
 
       case RESERVOIR:
@@ -262,18 +225,7 @@ public class UpdateHandler extends BaseSketchesQueryHandler {
         break;
 
       case VAROPT:
-        if (data.isJsonObject()) {
-          final JsonObject inputPair = data.getAsJsonObject();
-          if (!inputPair.has(QUERY_PAIR_ITEM_FIELD) || !inputPair.has(QUERY_PAIR_WEIGHT_FIELD)) {
-            throw new IllegalArgumentException("VarOpt input pairs must include both "
-                + QUERY_PAIR_ITEM_FIELD + " and " + QUERY_PAIR_WEIGHT_FIELD + " values");
-          }
-          final String item = inputPair.get(QUERY_PAIR_ITEM_FIELD).getAsString();
-          final double weight = inputPair.get(QUERY_PAIR_WEIGHT_FIELD).getAsDouble();
-          ((VarOptItemsSketch<String>) entry.sketch_).update(item, weight);
-        } else {
-          ((VarOptItemsSketch<String>) entry.sketch_).update(data.getAsString(), 1.0);
-        }
+        applyVarOptUpdate((VarOptItemsSketch<String>) entry.sketch_, data);
         break;
 
       default:
@@ -281,5 +233,45 @@ public class UpdateHandler extends BaseSketchesQueryHandler {
     }
   }
 
+  /**
+   * Handles simple item and (item, weight) pairs for FrequentItems updates
+   * @param fiSketch The frequent items sketch to receive the update
+   * @param data The JSON data, whether an raw value or object
+   */
+  private static void applyFrequentItemsUpdate(final ItemsSketch<String> fiSketch, final JsonElement data) {
+    if (data.isJsonObject()) {
+      final JsonObject inputPair = data.getAsJsonObject();
+      validateItemWeightPair(inputPair); // throws on error;
+      final String item = inputPair.get(QUERY_PAIR_ITEM_FIELD).getAsString();
+      final int weight = inputPair.get(QUERY_PAIR_WEIGHT_FIELD).getAsInt();
+      fiSketch.update(item, weight);
+    } else {
+      fiSketch.update(data.getAsString());
+    }
+  }
+
+  /**
+   * Handles simple item and (item, weight) pairs for VarOpt updates
+   * @param voSketch The varopt sketch to receive the update
+   * @param data The JSON data, whether an raw value or object
+   */
+  private static void applyVarOptUpdate(final VarOptItemsSketch<String> voSketch, final JsonElement data) {
+    if (data.isJsonObject()) {
+      final JsonObject inputPair = data.getAsJsonObject();
+      validateItemWeightPair(inputPair); // throws on error
+      final String item = inputPair.get(QUERY_PAIR_ITEM_FIELD).getAsString();
+      final int weight = inputPair.get(QUERY_PAIR_WEIGHT_FIELD).getAsInt();
+      voSketch.update(item, weight);
+    } else {
+      voSketch.update(data.getAsString(), 1.0);
+    }
+  }
+
+  private static void validateItemWeightPair(final JsonObject inputPair) {
+    if (!inputPair.has(QUERY_PAIR_ITEM_FIELD) || !inputPair.has(QUERY_PAIR_WEIGHT_FIELD)) {
+      throw new IllegalArgumentException("Frequent Items input pairs must include both "
+          + QUERY_PAIR_ITEM_FIELD + " and " + QUERY_PAIR_WEIGHT_FIELD + " values");
+    }
+  }
 }
 
diff --git a/src/test/java/org/apache/datasketches/server/ServerTestBase.java b/src/test/java/org/apache/datasketches/server/ServerTestBase.java
index 8c0d7f5..59cee6c 100644
--- a/src/test/java/org/apache/datasketches/server/ServerTestBase.java
+++ b/src/test/java/org/apache/datasketches/server/ServerTestBase.java
@@ -19,10 +19,11 @@
 
 package org.apache.datasketches.server;
 
+import static org.apache.datasketches.server.SketchConstants.ERROR_KEY;
 import static org.testng.Assert.fail;
 
-import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
+import java.util.Map;
 import java.util.Objects;
 import java.io.DataOutputStream;
 import java.io.InputStreamReader;
@@ -31,9 +32,12 @@ import java.net.URL;
 import java.nio.charset.StandardCharsets;
 
 import org.checkerframework.checker.nullness.qual.NonNull;
+import org.eclipse.jetty.http.HttpStatus;
 import org.testng.annotations.AfterClass;
 import org.testng.annotations.BeforeClass;
 
+
+import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.gson.JsonParser;
 
@@ -51,6 +55,7 @@ public class ServerTestBase {
       server_.start();
       serverUri_ = server_.getURI();
     } catch (final Exception e) {
+      System.err.println(e.getMessage());
       fail();
     }
   }
@@ -66,11 +71,18 @@ public class ServerTestBase {
     }
   }
 
+  static void clearResponse(@NonNull final JsonObject response) {
+    for (final Map.Entry<String, JsonElement> entry : response.entrySet()) {
+      response.remove(entry.getKey());
+    }
+  }
+
   int postData(@NonNull final String path,
                @NonNull final JsonObject data,
                @NonNull final JsonObject response) {
     HttpURLConnection http = null;
     int status = -1;
+    clearResponse(response);
 
     try {
       // set up the POST
@@ -91,14 +103,19 @@ public class ServerTestBase {
       }
 
       status = http.getResponseCode();
-      if (status == HttpServletResponse.SC_OK) {
+      if (status == HttpStatus.OK_200) {
         // read response, if any, and put into a JSON element
         try (final InputStreamReader isr = new InputStreamReader(http.getInputStream())) {
           response.add(RESPONSE_FIELD, JsonParser.parseReader(isr));
         }
+      } else if (status == HttpStatus.UNPROCESSABLE_ENTITY_422) {
+        // read error response and put into a JSON element
+        try (final InputStreamReader isr = new InputStreamReader(http.getErrorStream())) {
+          response.add(ERROR_KEY, JsonParser.parseReader(isr));
+        }
       }
     } catch (final IOException e) {
-      fail();
+        fail();
     } finally {
       if (http != null)
         http.disconnect();
@@ -112,9 +129,10 @@ public class ServerTestBase {
               @NonNull final JsonObject response) {
     HttpURLConnection http = null;
     int status = -1;
+    clearResponse(response);
 
     try {
-      // set up the POST
+      // set up the GET
       final URL url = new URL(serverUri_ + path + "?" + data);
       http = (HttpURLConnection) url.openConnection();
       http.setDoInput(true);
@@ -122,11 +140,16 @@ public class ServerTestBase {
       http.connect();
 
       status = http.getResponseCode();
-      if (status == HttpServletResponse.SC_OK) {
+      if (status == HttpStatus.OK_200) {
         // read response, if any, and put into a JSON element
         try (final InputStreamReader isr = new InputStreamReader(http.getInputStream())) {
           response.add(RESPONSE_FIELD, JsonParser.parseReader(isr));
         }
+      } else if (status == HttpStatus.UNPROCESSABLE_ENTITY_422) {
+        // read error response and put into a JSON element
+        try (final InputStreamReader isr = new InputStreamReader(http.getErrorStream())) {
+          response.add(ERROR_KEY, JsonParser.parseReader(isr));
+        }
       }
     } catch (final IOException e) {
       fail();
diff --git a/src/test/java/org/apache/datasketches/server/SketchStorageTest.java b/src/test/java/org/apache/datasketches/server/SketchStorageTest.java
index 03f4b5c..11d5e40 100644
--- a/src/test/java/org/apache/datasketches/server/SketchStorageTest.java
+++ b/src/test/java/org/apache/datasketches/server/SketchStorageTest.java
@@ -69,7 +69,7 @@ public class SketchStorageTest {
     final SketchStorage storage = new SketchStorage(serverConfig.getSketchList());
     final JsonObject sketches = storage.listSketches();
     assertTrue(sketches.has(RESPONSE_SKETCH_COUNT_FIELD));
-    assertEquals(sketches.get(RESPONSE_SKETCH_COUNT_FIELD).getAsInt(), 15);
-    assertTrue(storage.contains("cpcOfNumbers"));
+    assertEquals(sketches.get(RESPONSE_SKETCH_COUNT_FIELD).getAsInt(), 20);
+    assertTrue(storage.contains("cpcOfFloats"));
   }
 }
diff --git a/src/test/java/org/apache/datasketches/server/UpdateHandlerTest.java b/src/test/java/org/apache/datasketches/server/UpdateHandlerTest.java
index a77369b..024bb5d 100644
--- a/src/test/java/org/apache/datasketches/server/UpdateHandlerTest.java
+++ b/src/test/java/org/apache/datasketches/server/UpdateHandlerTest.java
@@ -25,53 +25,131 @@ import static org.apache.datasketches.server.SketchConstants.UPDATE_PATH;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertTrue;
 
-import javax.servlet.http.HttpServletResponse;
-
 import org.apache.datasketches.cpc.CpcSketch;
+import org.apache.datasketches.hll.HllSketch;
+import org.apache.datasketches.kll.KllFloatsSketch;
+import org.apache.datasketches.sampling.ReservoirItemsSketch;
+import org.apache.datasketches.sampling.VarOptItemsSketch;
+import org.apache.datasketches.theta.CompactSketch;
+import org.apache.datasketches.theta.Union;
+import org.eclipse.jetty.http.HttpStatus;
 import org.testng.annotations.Test;
 
 import com.google.gson.JsonArray;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
-
+import com.google.gson.JsonPrimitive;
 
 public class UpdateHandlerTest extends ServerTestBase {
   /* The tests here are going to be structured very similarly. It might be possible
-   * to find a common framework and reduce the amount o repetition? But not clear with
-   * type erasure as opposed to C++-style templates.
+   * to find a common framework and reduce the amount ofXS repetition?
    */
+  @Test
+  public void emptyUpdate() {
+    final JsonObject response = new JsonObject();
+    JsonObject request = new JsonObject();
+
+    // completely empty is ok
+    assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+    // invalid name with value
+    request.addProperty("sketchDoesNotExist", "validStringValue");
+    assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.UNPROCESSABLE_ENTITY_422);
+
+    // valid name, empty value
+    request = new JsonObject();
+    request.add("hll1", new JsonObject());
+    assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.UNPROCESSABLE_ENTITY_422);
+  }
 
   @Test
-  public void cpcUpdate() {
+  public void cpcUpdate1() {
     final JsonObject response = new JsonObject();
-    final String sketchName = "cpcOfNumbers";
+    final String sketchName = "cpcOfLongs";
     final int nPoints = 1000;
 
     // testing using both GET and POST
-    int status;
-
     JsonObject request = new JsonObject();
     JsonArray data = new JsonArray();
     for (int i = 0; i < nPoints; ++i)
       data.add(i);
     request.add(sketchName, data);
-    status = postData(UPDATE_PATH, request, response);
-    assertEquals(status, HttpServletResponse.SC_OK);
+    assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
 
     request = new JsonObject();
     data = new JsonArray();
     for (int i = nPoints; i < 2 * nPoints; ++i)
       data.add(i);
     request.add(sketchName, data);
-    status = getData(UPDATE_PATH, request, response);
-    assertEquals(status, HttpServletResponse.SC_OK);
+    assertEquals(getData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+    // single-item update
+    request = new JsonObject();
+    request.add(sketchName, new JsonPrimitive(-1));
+    assertEquals(getData(UPDATE_PATH, request, response), HttpStatus.OK_200);
 
     final JsonElement element = response.get(RESPONSE_FIELD);
     assertTrue(element.isJsonNull());
 
     final SketchStorage.SketchEntry entry = server_.getSketch(sketchName);
     final CpcSketch sk = (CpcSketch) entry.sketch_;
-    assertEquals(sk.getEstimate(), 2 * nPoints, 2 * nPoints * 1e-2);
+    assertEquals(entry.type_, ValueType.LONG);
+    assertTrue(2 * nPoints + 1 <= sk.getUpperBound(1));
+    assertTrue(2 * nPoints + 1 >= sk.getLowerBound(1));
+  }
+
+  @Test
+  public void cpcUpdate2() {
+    final JsonObject response = new JsonObject();
+    final String sketchName = "cpcOfStrings";
+    final int nPoints = 5000;
+
+    JsonObject request = new JsonObject();
+    final JsonArray data = new JsonArray();
+    for (int i = 0; i < nPoints; ++i)
+      data.add(i); // not converting to String since should happen upon parsing
+    request.add(sketchName, data);
+    assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+    request = new JsonObject();
+    request.add(sketchName, new JsonPrimitive(-1));
+    assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+    final JsonElement element = response.get(RESPONSE_FIELD);
+    assertTrue(element.isJsonNull());
+
+    final SketchStorage.SketchEntry entry = server_.getSketch(sketchName);
+    final CpcSketch sk = (CpcSketch) entry.sketch_;
+    assertEquals(entry.type_, ValueType.STRING);
+    assertTrue(nPoints + 1 <= sk.getUpperBound(1));
+    assertTrue(nPoints + 1 >= sk.getLowerBound(1));
+  }
+
+  @Test
+  public void cpcUpdate3() {
+    final JsonObject response = new JsonObject();
+    final String sketchName = "cpcOfFloats";
+    final int nPoints = 2500;
+
+    JsonObject request = new JsonObject();
+    final JsonArray data = new JsonArray();
+    for (int i = 0; i < nPoints; ++i)
+      data.add(i * 10.0);
+    request.add(sketchName, data);
+    assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+    request = new JsonObject();
+    request.add(sketchName, new JsonPrimitive(-1));
+    assertEquals(getData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+    final JsonElement element = response.get(RESPONSE_FIELD);
+    assertTrue(element.isJsonNull());
+
+    final SketchStorage.SketchEntry entry = server_.getSketch(sketchName);
+    final CpcSketch sk = (CpcSketch) entry.sketch_;
+    assertEquals(entry.type_, ValueType.FLOAT);
+    assertTrue(nPoints + 1 <= sk.getUpperBound(1));
+    assertTrue(nPoints + 1 >= sk.getLowerBound(1));
   }
 
   @Test
@@ -83,7 +161,7 @@ public class UpdateHandlerTest extends ServerTestBase {
     // single item
     JsonObject request = new JsonObject();
     request.addProperty(sketchName, "item1");
-    assertEquals(postData(UPDATE_PATH, request, response), HttpServletResponse.SC_OK);
+    assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
 
     // item with weight
     request = new JsonObject();
@@ -91,7 +169,7 @@ public class UpdateHandlerTest extends ServerTestBase {
     data.addProperty(QUERY_PAIR_ITEM_FIELD, "item2");
     data.addProperty(QUERY_PAIR_WEIGHT_FIELD, 5);
     request.add(sketchName, data);
-    assertEquals(postData(UPDATE_PATH, request, response), HttpServletResponse.SC_OK);
+    assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
 
     // array of items with and without weights
     request = new JsonObject();
@@ -102,7 +180,7 @@ public class UpdateHandlerTest extends ServerTestBase {
     data.addProperty(QUERY_PAIR_WEIGHT_FIELD, 10);
     dataArray.add(data);
     request.add(sketchName, dataArray);
-    assertEquals(postData(UPDATE_PATH, request, response), HttpServletResponse.SC_OK);
+    assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
 
     final JsonElement element = response.get(RESPONSE_FIELD);
     assertTrue(element.isJsonNull());
@@ -112,31 +190,294 @@ public class UpdateHandlerTest extends ServerTestBase {
     assertEquals(sk.getEstimate("item1"), 2);
     assertEquals(sk.getEstimate("item2"), 5);
     assertEquals(sk.getEstimate("item3"), 10);
+
+    // update with malformed item/weight pair (each independently)
+    request = new JsonObject();
+    data = new JsonObject();
+    data.addProperty("invalid", "item2");
+    data.addProperty(QUERY_PAIR_WEIGHT_FIELD, 5);
+    request.add(sketchName, data);
+    assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.UNPROCESSABLE_ENTITY_422);
+
+    data = new JsonObject();
+    data.addProperty(QUERY_PAIR_ITEM_FIELD, "item2");
+    data.addProperty("invalid", 5);
+    request.add(sketchName, data);
+    assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.UNPROCESSABLE_ENTITY_422);
   }
 
   @Test
   public void hllUpdate() {
-    // update multiple sketches from an array
+    final JsonObject response = new JsonObject();
+    final String sketchName = "hll4";
+    final int nPoints = 100;
+
+    JsonObject request = new JsonObject();
+    final JsonArray data = new JsonArray();
+    for (int i = 0; i < nPoints; ++i)
+      data.add(i); // not converting to String since should happen upon parsing
+    request.add(sketchName, data);
+    assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+    request = new JsonObject();
+    request.add(sketchName, new JsonPrimitive(-1));
+    assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+    final JsonElement element = response.get(RESPONSE_FIELD);
+    assertTrue(element.isJsonNull());
+
+    final SketchStorage.SketchEntry entry = server_.getSketch(sketchName);
+    final HllSketch sk = (HllSketch) entry.sketch_;
+    assertEquals(entry.type_, ValueType.STRING);
+    assertTrue(nPoints + 1 <= sk.getUpperBound(1));
+    assertTrue(nPoints + 1 >= sk.getLowerBound(1));
+  }
+
+  @Test
+  public void hllUpdate2() {
+    final JsonObject response = new JsonObject();
+    final String sketchName = "hllOfInts";
+    final int nPoints = 20; // should be in exact mode
+
+    JsonObject request = new JsonObject();
+    JsonArray data = new JsonArray();
+    for (int i = 0; i < nPoints; ++i)
+      data.add(i);
+    request.add(sketchName, data);
+    assertEquals(getData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+    request = new JsonObject();
+    data = new JsonArray();
+    for (int i = nPoints; i < 2 * nPoints; ++i)
+      data.add(i);
+    request.add(sketchName, data);
+    assertEquals(getData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+    // single-item update
+    request = new JsonObject();
+    request.add(sketchName, new JsonPrimitive(-1));
+    assertEquals(getData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+    final JsonElement element = response.get(RESPONSE_FIELD);
+    assertTrue(element.isJsonNull());
+
+    final SketchStorage.SketchEntry entry = server_.getSketch(sketchName);
+    final HllSketch sk = (HllSketch) entry.sketch_;
+    assertEquals(entry.type_, ValueType.INT);
+    assertTrue(2 * nPoints + 1 <= sk.getUpperBound(1));
+    assertTrue(2 * nPoints + 1 >= sk.getLowerBound(1));
+  }
+
+  @Test
+  public void hllUpdate3() {
+    final JsonObject response = new JsonObject();
+    final String sketchName = "hllOfDoubles";
+    final int nPoints = 479;
+
+    JsonObject request = new JsonObject();
+    final JsonArray data = new JsonArray();
+    for (int i = 0; i < nPoints; ++i)
+      data.add(i * 10.0);
+    request.add(sketchName, data);
+    assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+    request = new JsonObject();
+    request.add(sketchName, new JsonPrimitive(-1));
+    assertEquals(getData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+    final JsonElement element = response.get(RESPONSE_FIELD);
+    assertTrue(element.isJsonNull());
+
+    final SketchStorage.SketchEntry entry = server_.getSketch(sketchName);
+    final HllSketch sk = (HllSketch) entry.sketch_;
+    assertEquals(entry.type_, ValueType.DOUBLE);
+    assertTrue(nPoints + 1 <= sk.getUpperBound(1));
+    assertTrue(nPoints + 1 >= sk.getLowerBound(1));
   }
 
   @Test
   public void kllUpdate() {
+    final JsonObject response = new JsonObject();
+    final String sketchName = "duration";
+    final int nPoints = 953;
+    final int nUpdates = 5;
+
+    for (int j = 0; j < nUpdates; ++j) {
+      // batch update
+      JsonObject request = new JsonObject();
+      final JsonArray data = new JsonArray();
+      for (int i = 0; i < nPoints; ++i)
+        data.add(i * 10.0);
+      request.add(sketchName, data);
+      assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+      assertTrue(response.get(RESPONSE_FIELD).isJsonNull());
 
+      // single update
+      request = new JsonObject();
+      request.add(sketchName, new JsonPrimitive(-1));
+      assertEquals(getData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+      assertTrue(response.get(RESPONSE_FIELD).isJsonNull());
+    }
+
+    final SketchStorage.SketchEntry entry = server_.getSketch(sketchName);
+    final KllFloatsSketch sk = (KllFloatsSketch) entry.sketch_;
+    assertEquals(sk.getN(), (nPoints + 1) * nUpdates);
   }
 
   @Test
-  public void thetaUpdate() {
+  public void thetaUpdate1() {
+    final JsonObject response = new JsonObject();
+    final String sketchName = "theta0";
+    final int nPoints = 1000;
+
+    JsonObject request = new JsonObject();
+    JsonArray data = new JsonArray();
+    for (int i = 0; i < nPoints; ++i)
+      data.add(i);
+    request.add(sketchName, data);
+    assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
 
+    request = new JsonObject();
+    data = new JsonArray();
+    for (int i = nPoints; i < 2 * nPoints; ++i)
+      data.add(i);
+    request.add(sketchName, data);
+    assertEquals(getData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+    // single-item update
+    request = new JsonObject();
+    request.add(sketchName, new JsonPrimitive(-1));
+    assertEquals(getData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+    final JsonElement element = response.get(RESPONSE_FIELD);
+    assertTrue(element.isJsonNull());
+
+    final SketchStorage.SketchEntry entry = server_.getSketch(sketchName);
+    final CompactSketch sk = ((Union) entry.sketch_).getResult();
+    assertEquals(entry.type_, ValueType.INT);
+    assertTrue(2 * nPoints + 1 <= sk.getUpperBound(1));
+    assertTrue(2 * nPoints + 1 >= sk.getLowerBound(1));
   }
 
   @Test
-  public void reservoirUpdate() {
+  public void thetaUpdate2() {
+    final JsonObject response = new JsonObject();
+    final String sketchName = "thetaOfStrings";
+    final int nPoints = 5000;
 
+    JsonObject request = new JsonObject();
+    final JsonArray data = new JsonArray();
+    for (int i = 0; i < nPoints; ++i)
+      data.add(i); // not converting to String since should happen upon parsing
+    request.add(sketchName, data);
+    assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+    request = new JsonObject();
+    request.add(sketchName, new JsonPrimitive(-1));
+    assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+    final JsonElement element = response.get(RESPONSE_FIELD);
+    assertTrue(element.isJsonNull());
+
+    final SketchStorage.SketchEntry entry = server_.getSketch(sketchName);
+    final CompactSketch sk = ((Union) entry.sketch_).getResult();
+    assertEquals(entry.type_, ValueType.STRING);
+    assertTrue(nPoints + 1 <= sk.getUpperBound(1));
+    assertTrue(nPoints + 1 >= sk.getLowerBound(1));
   }
 
   @Test
-  public void voUpdate() {
+  public void thetaUpdate3() {
+    final JsonObject response = new JsonObject();
+    final String sketchName = "thetaOfDoubles";
+    final int nPoints = 1153;
 
+    JsonObject request = new JsonObject();
+    final JsonArray data = new JsonArray();
+    for (int i = 0; i < nPoints; ++i)
+      data.add(i * 10.0);
+    request.add(sketchName, data);
+    assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+    request = new JsonObject();
+    request.add(sketchName, new JsonPrimitive(-1));
+    assertEquals(getData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+    final JsonElement element = response.get(RESPONSE_FIELD);
+    assertTrue(element.isJsonNull());
+
+    final SketchStorage.SketchEntry entry = server_.getSketch(sketchName);
+    final CompactSketch sk = ((Union) entry.sketch_).getResult();
+    assertEquals(entry.type_, ValueType.DOUBLE);
+    assertTrue(nPoints + 1 <= sk.getUpperBound(1));
+    assertTrue(nPoints + 1 >= sk.getLowerBound(1));
   }
 
+  @Test
+  @SuppressWarnings("unchecked")
+  public void reservoirUpdate() {
+    final JsonObject response = new JsonObject();
+    final String sketchName = "rs";
+    final int nPoints = 20;
+    final int nUpdates = 5;
+
+    // just going to use numbers as Strings to avoid creating a dictionary
+    for (int j = 0; j < nUpdates; ++j) {
+      JsonObject request = new JsonObject();
+      final JsonArray data = new JsonArray();
+      for (int i = 0; i < nPoints; ++i)
+        data.add(Integer.toString(i));
+      request.add(sketchName, data);
+      assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+      assertTrue(response.get(RESPONSE_FIELD).isJsonNull());
+
+      // single update
+      request = new JsonObject();
+      request.add(sketchName, new JsonPrimitive("-1"));
+      assertEquals(getData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+      assertTrue(response.get(RESPONSE_FIELD).isJsonNull());
+    }
+
+    final SketchStorage.SketchEntry entry = server_.getSketch(sketchName);
+    final ReservoirItemsSketch<String> sk = (ReservoirItemsSketch<String>) entry.sketch_;
+    assertEquals(sk.getN(), (nPoints + 1) * nUpdates);
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  public void varOptUpdate() {
+    final JsonObject response = new JsonObject();
+    final String sketchName = "vo";
+
+    // single item
+    JsonObject request = new JsonObject();
+    request.addProperty(sketchName, "item1");
+    assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+    assertTrue(response.get(RESPONSE_FIELD).isJsonNull());
+
+    // item with weight
+    request = new JsonObject();
+    JsonObject data = new JsonObject();
+    data.addProperty(QUERY_PAIR_ITEM_FIELD, "item2");
+    data.addProperty(QUERY_PAIR_WEIGHT_FIELD, 5);
+    request.add(sketchName, data);
+    assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+    assertTrue(response.get(RESPONSE_FIELD).isJsonNull());
+
+    // array of items with and without weights
+    request = new JsonObject();
+    final JsonArray dataArray = new JsonArray();
+    dataArray.add("item1"); // that it's a duplicate doens't matter
+    data = new JsonObject();
+    data.addProperty(QUERY_PAIR_ITEM_FIELD, "item3");
+    data.addProperty(QUERY_PAIR_WEIGHT_FIELD, 10);
+    dataArray.add(data);
+    request.add(sketchName, dataArray);
+    assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+    assertTrue(response.get(RESPONSE_FIELD).isJsonNull());
+
+    final SketchStorage.SketchEntry entry = server_.getSketch(sketchName);
+    final VarOptItemsSketch<String> sk = (VarOptItemsSketch<String>) entry.sketch_;
+    assertEquals(sk.getN(), 4);
+  }
 }
diff --git a/src/test/resources/test_config.json b/src/test/resources/test_config.json
index a685c17..2acd6bd 100644
--- a/src/test/resources/test_config.json
+++ b/src/test/resources/test_config.json
@@ -1,56 +1,92 @@
 {
 	"port": 0,
     "sketches_A": [
-	{ "name": "cpcOfNumbers",
-	  "k": 12,
-	  "type": "long",
-	  "family": "cpc"
+		{
+			"name": "cpcOfLongs",
+			"k": 12,
+			"type": "long",
+			"family": "cpc"
+		},
+		{
+			"name": "cpcOfFloats",
+			"k": 12,
+			"type": "float",
+			"family": "cpc"
+		},
+		{
+			"name": "cpcOfStrings",
+			"k": 14,
+			"type": "string",
+			"family": "cpc"
+		},
+		{
+			"name": "thetaOfDoubles",
+			"k": 9,
+			"type": "double",
+			"family": "theta"
+		},
+		{
+			"name": "thetaOfStrings",
+			"k": 16,
+			"type": "string",
+			"family": "theta"
+		}
+	],
+	"set1": {
+		"family": "hll",
+		"type": "string",
+		"k": 14,
+		"names": [
+			"hll1",
+			"hll2",
+			"hll3",
+			"hll4"
+		]
 	},
-	{ "name": "cpcOfStrings",
-	  "k": 14,
-	  "type": "string",
-	  "family": "cpc"
-	}
-    ],
-    "set1": {
-	"family": "hll",
-	"type": "string",
-	"k": 14,
-	"names": [
-	    "hll1",
-	    "hll2",
-	    "hll3",
-	    "hll4"
-	]
-    },
-    "set2": {
-	"k": 12,
-	"family": "theta",
-	"type": "int",
-	"names": [
-	    "theta0",
-	    "theta1",
-	    "theta2",
-	    "theta3",
-	    "theta4"
-	]
-    },
-    "sketches_B": [
-	{ "name": "duration",
-	  "k": "160",
-	  "family": "kll"
+	"set2": {
+		"k": 12,
+		"family": "theta",
+		"type": "int",
+		"names": [
+			"theta0",
+			"theta1",
+			"theta2",
+			"theta3",
+			"theta4"
+		]
 	},
-	{ "name": "topItems",
-	  "k": "128",
-	  "family": "frequency"
-	},
-	{ "name": "rs",
-	  "k": "20",
-	  "family": "reservoir"
-	},
-	{ "name": "vo",
-	  "k": "20",
-	  "family": "varopt"
-	}	
+	"sketches_B": [
+		{
+			"name": "duration",
+			"k": "160",
+			"family": "kll"
+		},
+		{
+			"name": "topItems",
+			"k": "128",
+			"family": "frequency"
+		},
+		{
+			"name": "rs",
+			"k": "20",
+			"family": "reservoir"
+		},
+		{
+			"name": "vo",
+			"k": "20",
+			"family": "varopt"
+		},
+		{
+			"name": "hllOfInts",
+			"family": "hll",
+			"k": 10,
+			"type": "int"
+		},
+		{
+			"name": "hllOfDoubles",
+			"family": "hll",
+			"k": 11,
+			"type": "double"
+		}
     ]
 }

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