You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@druid.apache.org by gi...@apache.org on 2023/06/25 16:35:24 UTC

[druid] branch master updated: SqlResults: Coerce arrays to lists for VARCHAR. (#14260)

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

gian pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/master by this push:
     new d7c9c2f367 SqlResults: Coerce arrays to lists for VARCHAR. (#14260)
d7c9c2f367 is described below

commit d7c9c2f3671d1e9cefff463eb5cb557e9e882f38
Author: Gian Merlino <gi...@gmail.com>
AuthorDate: Sun Jun 25 09:35:18 2023 -0700

    SqlResults: Coerce arrays to lists for VARCHAR. (#14260)
    
    * SqlResults: Coerce arrays to lists for VARCHAR.
    
    Useful for STRING_TO_MV, which returns VARCHAR at the SQL layer and an
    ExprEval with String[] at the native layer.
    
    * Fix style.
    
    * Improve test coverage.
    
    * Remove unnecessary throws.
---
 .../apache/druid/sql/calcite/run/SqlResults.java   |  98 ++++++----
 .../calcite/CalciteMultiValueStringQueryTest.java  | 196 ++++++++++++++++++--
 .../druid/sql/calcite/run/SqlResultsTest.java      | 206 ++++++++++++++++++---
 3 files changed, 429 insertions(+), 71 deletions(-)

diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/run/SqlResults.java b/sql/src/main/java/org/apache/druid/sql/calcite/run/SqlResults.java
index d48eaa8742..ab400b97a3 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/run/SqlResults.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/run/SqlResults.java
@@ -39,7 +39,7 @@ import org.apache.druid.sql.calcite.planner.PlannerContext;
 import org.joda.time.DateTime;
 import org.joda.time.DateTimeZone;
 
-import java.io.IOException;
+import javax.annotation.Nullable;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -70,21 +70,23 @@ public class SqlResults
         coercedValue = String.valueOf(value);
       } else if (value instanceof Boolean) {
         coercedValue = String.valueOf(value);
-      } else if (value instanceof Collection) {
-        // Iterate through the collection, coercing each value. Useful for handling selects of multi-value dimensions.
-        final List<String> valueStrings =
-            ((Collection<?>) value).stream()
-                                   .map(v -> (String) coerce(jsonMapper, context, v, sqlTypeName))
-                                   .collect(Collectors.toList());
+      } else {
+        final Object maybeList = maybeCoerceArrayToList(value, false);
 
-        try {
-          coercedValue = jsonMapper.writeValueAsString(valueStrings);
-        }
-        catch (IOException e) {
-          throw new RuntimeException(e);
+        // Check if "maybeList" was originally a Collection of some kind, or was able to be coerced to one.
+        // Then Iterate through the collection, coercing each value. Useful for handling multi-value dimensions.
+        if (maybeList instanceof Collection) {
+          final List<String> valueStrings =
+              ((Collection<?>) maybeList)
+                  .stream()
+                  .map(v -> (String) coerce(jsonMapper, context, v, sqlTypeName))
+                  .collect(Collectors.toList());
+
+          // Must stringify since the caller is expecting CHAR_TYPES.
+          coercedValue = coerceUsingObjectMapper(jsonMapper, valueStrings, sqlTypeName);
+        } else {
+          throw cannotCoerce(value, sqlTypeName);
         }
-      } else {
-        throw new ISE("Cannot coerce [%s] to %s", value.getClass().getName(), sqlTypeName);
       }
     } else if (value == null) {
       coercedValue = null;
@@ -93,12 +95,14 @@ public class SqlResults
     } else if (sqlTypeName == SqlTypeName.TIMESTAMP) {
       return Calcites.jodaToCalciteTimestamp(coerceDateTime(value, sqlTypeName), context.getTimeZone());
     } else if (sqlTypeName == SqlTypeName.BOOLEAN) {
-      if (value instanceof String) {
+      if (value instanceof Boolean) {
+        coercedValue = value;
+      } else if (value instanceof String) {
         coercedValue = Evals.asBoolean(((String) value));
       } else if (value instanceof Number) {
         coercedValue = Evals.asBoolean(((Number) value).longValue());
       } else {
-        throw new ISE("Cannot coerce [%s] to %s", value.getClass().getName(), sqlTypeName);
+        throw cannotCoerce(value, sqlTypeName);
       }
     } else if (sqlTypeName == SqlTypeName.INTEGER) {
       if (value instanceof String) {
@@ -106,38 +110,33 @@ public class SqlResults
       } else if (value instanceof Number) {
         coercedValue = ((Number) value).intValue();
       } else {
-        throw new ISE("Cannot coerce [%s] to %s", value.getClass().getName(), sqlTypeName);
+        throw cannotCoerce(value, sqlTypeName);
       }
     } else if (sqlTypeName == SqlTypeName.BIGINT) {
       try {
         coercedValue = DimensionHandlerUtils.convertObjectToLong(value);
       }
       catch (Exception e) {
-        throw new ISE("Cannot coerce [%s] to %s", value.getClass().getName(), sqlTypeName);
+        throw cannotCoerce(value, sqlTypeName);
       }
     } else if (sqlTypeName == SqlTypeName.FLOAT) {
       try {
         coercedValue = DimensionHandlerUtils.convertObjectToFloat(value);
       }
       catch (Exception e) {
-        throw new ISE("Cannot coerce [%s] to %s", value.getClass().getName(), sqlTypeName);
+        throw cannotCoerce(value, sqlTypeName);
       }
     } else if (SqlTypeName.FRACTIONAL_TYPES.contains(sqlTypeName)) {
       try {
         coercedValue = DimensionHandlerUtils.convertObjectToDouble(value);
       }
       catch (Exception e) {
-        throw new ISE("Cannot coerce [%s] to %s", value.getClass().getName(), sqlTypeName);
+        throw cannotCoerce(value, sqlTypeName);
       }
     } else if (sqlTypeName == SqlTypeName.OTHER) {
       // Complex type, try to serialize if we should, else print class name
       if (context.isSerializeComplexValues()) {
-        try {
-          coercedValue = jsonMapper.writeValueAsString(value);
-        }
-        catch (JsonProcessingException jex) {
-          throw new ISE(jex, "Cannot coerce [%s] to %s", value.getClass().getName(), sqlTypeName);
-        }
+        coercedValue = coerceUsingObjectMapper(jsonMapper, value, sqlTypeName);
       } else {
         coercedValue = value.getClass().getName();
       }
@@ -148,12 +147,7 @@ public class SqlResults
         } else if (value instanceof NlsString) {
           coercedValue = ((NlsString) value).getValue();
         } else {
-          try {
-            coercedValue = jsonMapper.writeValueAsString(value);
-          }
-          catch (IOException e) {
-            throw new RuntimeException(e);
-          }
+          coercedValue = coerceUsingObjectMapper(jsonMapper, value, sqlTypeName);
         }
       } else {
         // the protobuf jdbc handler prefers lists (it actually can't handle java arrays as sql arrays, only java lists)
@@ -161,18 +155,22 @@ public class SqlResults
         // here if needed
         coercedValue = maybeCoerceArrayToList(value, true);
         if (coercedValue == null) {
-          throw new ISE("Cannot coerce [%s] to %s", value.getClass().getName(), sqlTypeName);
+          throw cannotCoerce(value, sqlTypeName);
         }
       }
     } else {
-      throw new ISE("Cannot coerce [%s] to %s", value.getClass().getName(), sqlTypeName);
+      throw cannotCoerce(value, sqlTypeName);
     }
 
     return coercedValue;
   }
 
-
+  /**
+   * Attempt to coerce a value to {@link List}. If it cannot be coerced, either return the original value (if mustCoerce
+   * is false) or return null (if mustCoerce is true).
+   */
   @VisibleForTesting
+  @Nullable
   static Object maybeCoerceArrayToList(Object value, boolean mustCoerce)
   {
     if (value instanceof List) {
@@ -222,11 +220,39 @@ public class SqlResults
     } else if (value instanceof DateTime) {
       dateTime = (DateTime) value;
     } else {
-      throw new ISE("Cannot coerce[%s] to %s", value.getClass().getName(), sqlType);
+      throw cannotCoerce(value, sqlType);
     }
     return dateTime;
   }
 
+  private static String coerceUsingObjectMapper(
+      final ObjectMapper jsonMapper,
+      final Object value,
+      final SqlTypeName sqlTypeName
+  )
+  {
+    try {
+      return jsonMapper.writeValueAsString(value);
+    }
+    catch (JsonProcessingException e) {
+      throw cannotCoerce(e, value, sqlTypeName);
+    }
+  }
+
+  private static IllegalStateException cannotCoerce(
+      final Throwable t,
+      final Object value,
+      final SqlTypeName sqlTypeName
+  )
+  {
+    return new ISE(t, "Cannot coerce [%s] to [%s]", value == null ? "null" : value.getClass().getName(), sqlTypeName);
+  }
+
+  private static IllegalStateException cannotCoerce(final Object value, final SqlTypeName sqlTypeName)
+  {
+    return cannotCoerce(null, value, sqlTypeName);
+  }
+
   /**
    * Context for {@link #coerce(ObjectMapper, Context, Object, SqlTypeName)}
    */
diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteMultiValueStringQueryTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteMultiValueStringQueryTest.java
index d016f17953..f17ef78c17 100644
--- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteMultiValueStringQueryTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteMultiValueStringQueryTest.java
@@ -27,8 +27,12 @@ import org.apache.druid.java.util.common.StringUtils;
 import org.apache.druid.java.util.common.granularity.Granularities;
 import org.apache.druid.math.expr.ExpressionProcessing;
 import org.apache.druid.query.Druids;
+import org.apache.druid.query.aggregation.CountAggregatorFactory;
+import org.apache.druid.query.aggregation.ExpressionLambdaAggregatorFactory;
+import org.apache.druid.query.aggregation.FilteredAggregatorFactory;
 import org.apache.druid.query.aggregation.LongSumAggregatorFactory;
 import org.apache.druid.query.dimension.DefaultDimensionSpec;
+import org.apache.druid.query.expression.TestExprMacroTable;
 import org.apache.druid.query.filter.AndDimFilter;
 import org.apache.druid.query.filter.ExpressionDimFilter;
 import org.apache.druid.query.filter.InDimFilter;
@@ -1010,7 +1014,7 @@ public class CalciteMultiValueStringQueryTest extends BaseCalciteQueryTest
       );
     }
     testQuery(
-        "SELECT STRING_TO_MV(CONCAT(MV_TO_STRING(dim3, ','), ',d'), ','), SUM(cnt) FROM druid.numfoo WHERE MV_LENGTH(dim3) > 0 GROUP BY 1 ORDER BY 2 DESC",
+        "SELECT STRING_TO_MV(CONCAT(MV_TO_STRING(dim3, ','), ',d'), ','), SUM(cnt) FROM druid.numfoo WHERE MV_LENGTH(dim3) > 0 GROUP BY 1 ORDER BY 2 DESC, 1",
         ImmutableList.of(
             GroupByQuery.builder()
                         .setDataSource(CalciteTests.DATASOURCE3)
@@ -1025,18 +1029,21 @@ public class CalciteMultiValueStringQueryTest extends BaseCalciteQueryTest
                             )
                         )
                         .setDimFilter(bound("v0", "0", null, true, false, null, StringComparators.NUMERIC))
-                        .setDimensions(
-                            dimensions(
-                                new DefaultDimensionSpec("v1", "_d0", ColumnType.STRING)
-                            )
-                        )
+                        .setDimensions(dimensions(new DefaultDimensionSpec("v1", "_d0", ColumnType.STRING)))
                         .setAggregatorSpecs(aggregators(new LongSumAggregatorFactory("a0", "cnt")))
                         .setLimitSpec(new DefaultLimitSpec(
-                            ImmutableList.of(new OrderByColumnSpec(
-                                "a0",
-                                OrderByColumnSpec.Direction.DESCENDING,
-                                StringComparators.NUMERIC
-                            )),
+                            ImmutableList.of(
+                                new OrderByColumnSpec(
+                                    "a0",
+                                    OrderByColumnSpec.Direction.DESCENDING,
+                                    StringComparators.NUMERIC
+                                ),
+                                new OrderByColumnSpec(
+                                    "_d0",
+                                    OrderByColumnSpec.Direction.ASCENDING,
+                                    StringComparators.LEXICOGRAPHIC
+                                )
+                            ),
                             Integer.MAX_VALUE
                         ))
                         .setContext(QUERY_CONTEXT_DEFAULT)
@@ -1046,6 +1053,173 @@ public class CalciteMultiValueStringQueryTest extends BaseCalciteQueryTest
     );
   }
 
+  @Test
+  public void testSelectAndFilterByStringToMV()
+  {
+    // Cannot vectorize due to usage of expressions.
+    cannotVectorize();
+
+    testBuilder()
+        .sql("SELECT STRING_TO_MV(CONCAT(MV_TO_STRING(dim3, ','), ',d'), ',') FROM druid.numfoo "
+             + "WHERE MV_CONTAINS(STRING_TO_MV(CONCAT(MV_TO_STRING(dim3, ','), ',d'), ','), 'd')")
+        .expectedQuery(
+            newScanQueryBuilder()
+                .dataSource(CalciteTests.DATASOURCE3)
+                .intervals(querySegmentSpec(Filtration.eternity()))
+                .virtualColumns(
+                    expressionVirtualColumn(
+                        "v0",
+                        "string_to_array(concat(array_to_string(\"dim3\",','),',d'),',')",
+                        ColumnType.STRING
+                    )
+                )
+                .filters(expressionFilter(
+                    "array_contains(string_to_array(concat(array_to_string(\"dim3\",','),',d'),','),'d')"))
+                .columns("v0")
+                .context(QUERY_CONTEXT_DEFAULT)
+                .build()
+        )
+        .expectedResults(
+            NullHandling.sqlCompatible() ?
+            ImmutableList.of(
+                new Object[]{"[\"a\",\"b\",\"d\"]"},
+                new Object[]{"[\"b\",\"c\",\"d\"]"},
+                new Object[]{"[\"d\",\"d\"]"},
+                new Object[]{"[\"\",\"d\"]"}
+            ) : ImmutableList.of(
+                new Object[]{"[\"a\",\"b\",\"d\"]"},
+                new Object[]{"[\"b\",\"c\",\"d\"]"},
+                new Object[]{"[\"d\",\"d\"]"},
+                new Object[]{"[\"\",\"d\"]"},
+                new Object[]{"[\"\",\"d\"]"},
+                new Object[]{"[\"\",\"d\"]"}
+            )
+        )
+        .run();
+  }
+
+  @Test
+  public void testStringToMVOfConstant()
+  {
+    // Cannot vectorize due to usage of expressions.
+    cannotVectorize();
+
+    testBuilder()
+        .sql("SELECT m1, STRING_TO_MV('a,b', ',') AS mv FROM druid.numfoo GROUP BY 1")
+        .expectedQuery(
+            GroupByQuery.builder()
+                        .setDataSource(CalciteTests.DATASOURCE3)
+                        .setInterval(querySegmentSpec(Filtration.eternity()))
+                        .setGranularity(Granularities.ALL)
+                        .setPostAggregatorSpecs(ImmutableList.of(expressionPostAgg("p0", "string_to_array('a,b',',')")))
+                        .setDimensions(dimensions(new DefaultDimensionSpec("m1", "_d0", ColumnType.FLOAT)))
+                        .setContext(QUERY_CONTEXT_DEFAULT)
+                        .build()
+        )
+        .expectedResults(
+            ImmutableList.of(
+                new Object[]{1.0f, "[\"a\",\"b\"]"},
+                new Object[]{2.0f, "[\"a\",\"b\"]"},
+                new Object[]{3.0f, "[\"a\",\"b\"]"},
+                new Object[]{4.0f, "[\"a\",\"b\"]"},
+                new Object[]{5.0f, "[\"a\",\"b\"]"},
+                new Object[]{6.0f, "[\"a\",\"b\"]"}
+            )
+        )
+        .run();
+  }
+
+  @Test
+  public void testStringToMVOfConstantGroupedBy()
+  {
+    // Cannot vectorize due to usage of expressions.
+    cannotVectorize();
+
+    testBuilder()
+        .sql("SELECT m1, STRING_TO_MV('a,b', ',') AS mv FROM druid.numfoo GROUP BY 1, 2")
+        .expectedQuery(
+            GroupByQuery.builder()
+                        .setDataSource(CalciteTests.DATASOURCE3)
+                        .setInterval(querySegmentSpec(Filtration.eternity()))
+                        .setGranularity(Granularities.ALL)
+                        .setVirtualColumns(
+                            expressionVirtualColumn("v0", "string_to_array('a,b',',')", ColumnType.STRING)
+                        )
+                        .setDimensions(dimensions(
+                            new DefaultDimensionSpec("m1", "_d0", ColumnType.FLOAT),
+                            new DefaultDimensionSpec("v0", "_d1", ColumnType.STRING)
+                        ))
+                        .setContext(QUERY_CONTEXT_DEFAULT)
+                        .build()
+        )
+        .expectedResults(
+            ImmutableList.of(
+                new Object[]{1.0f, "a"},
+                new Object[]{1.0f, "b"},
+                new Object[]{2.0f, "a"},
+                new Object[]{2.0f, "b"},
+                new Object[]{3.0f, "a"},
+                new Object[]{3.0f, "b"},
+                new Object[]{4.0f, "a"},
+                new Object[]{4.0f, "b"},
+                new Object[]{5.0f, "a"},
+                new Object[]{5.0f, "b"},
+                new Object[]{6.0f, "a"},
+                new Object[]{6.0f, "b"}
+            )
+        )
+        .run();
+  }
+
+  @Test
+  public void testStringToMVOfStringAgg()
+  {
+    // Cannot vectorize due to usage of expressions.
+    cannotVectorize();
+
+    final String expectedResult;
+
+    if (NullHandling.sqlCompatible()) {
+      expectedResult = "[\"\",\"10.1\",\"2\",\"1\",\"def\",\"abc\"]";
+    } else {
+      expectedResult = "[\"10.1\",\"2\",\"1\",\"def\",\"abc\"]";
+    }
+
+    testBuilder()
+        .sql("SELECT STRING_TO_MV(STRING_AGG(dim1, ','), ',') AS mv, COUNT(*) cnt FROM druid.numfoo")
+        .expectedQuery(
+            Druids.newTimeseriesQueryBuilder()
+                  .dataSource(CalciteTests.DATASOURCE3)
+                  .intervals(querySegmentSpec(Filtration.eternity()))
+                  .aggregators(
+                      new FilteredAggregatorFactory(
+                          new ExpressionLambdaAggregatorFactory(
+                              "a0",
+                              ImmutableSet.of("dim1"),
+                              "__acc",
+                              "[]",
+                              "[]",
+                              true,
+                              false,
+                              false,
+                              "array_append(\"__acc\", \"dim1\")",
+                              "array_concat(\"__acc\", \"a0\")",
+                              null,
+                              "if(array_length(o) == 0, null, array_to_string(o, ','))",
+                              ExpressionLambdaAggregatorFactory.DEFAULT_MAX_SIZE_BYTES,
+                              TestExprMacroTable.INSTANCE
+                          ),
+                          not(selector("dim1", null, null))
+                      ),
+                      new CountAggregatorFactory("a1")
+                  )
+                  .postAggregators(expressionPostAgg("p0", "string_to_array(\"a0\",',')"))
+                  .context(QUERY_CONTEXT_DEFAULT)
+                  .build()
+        )
+        .expectedResults(ImmutableList.of(new Object[]{expectedResult, 6L}))
+        .run();
+  }
 
   @Test
   public void testMultiValueListFilter()
diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/run/SqlResultsTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/run/SqlResultsTest.java
index d19e218075..954170fffe 100644
--- a/sql/src/test/java/org/apache/druid/sql/calcite/run/SqlResultsTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/run/SqlResultsTest.java
@@ -19,16 +19,39 @@
 
 package org.apache.druid.sql.calcite.run;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSortedSet;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.druid.common.config.NullHandling;
+import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.segment.TestHelper;
 import org.apache.druid.segment.data.ComparableList;
 import org.apache.druid.segment.data.ComparableStringArray;
+import org.apache.druid.testing.InitializedNullHandlingTest;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.MatcherAssert;
+import org.joda.time.DateTimeZone;
 import org.junit.Assert;
+import org.junit.Before;
 import org.junit.Test;
+import org.junit.internal.matchers.ThrowableMessageMatcher;
 
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 
-public class SqlResultsTest
+public class SqlResultsTest extends InitializedNullHandlingTest
 {
+  private static final SqlResults.Context DEFAULT_CONTEXT = new SqlResults.Context(DateTimeZone.UTC, true, false);
+
+  private ObjectMapper jsonMapper;
+
+  @Before
+  public void setUp()
+  {
+    jsonMapper = TestHelper.JSON_MAPPER;
+  }
 
   @Test
   public void testCoerceStringArrays()
@@ -38,10 +61,10 @@ public class SqlResultsTest
     final ComparableStringArray comparableStringArray = ComparableStringArray.of(new String[]{"x", "y", "z", null});
     final String[] stringArray2 = new String[]{"x", "y", "z", null};
 
-    assertCoerced(stringList, stringList, true);
-    assertCoerced(stringList, stringArray, true);
-    assertCoerced(stringList, stringArray2, true);
-    assertCoerced(stringList, comparableStringArray, true);
+    assertCoerceArrayToList(stringList, stringList);
+    assertCoerceArrayToList(stringList, stringArray);
+    assertCoerceArrayToList(stringList, stringArray2);
+    assertCoerceArrayToList(stringList, comparableStringArray);
   }
 
   @Test
@@ -53,11 +76,11 @@ public class SqlResultsTest
     final List<Long> list = Arrays.asList(1L, 2L, 3L);
     final long[] array = new long[]{1L, 2L, 3L};
 
-    assertCoerced(listWithNull, listWithNull, true);
-    assertCoerced(listWithNull, arrayWithNull, true);
-    assertCoerced(listWithNull, comparableList, true);
-    assertCoerced(list, list, true);
-    assertCoerced(list, array, true);
+    assertCoerceArrayToList(listWithNull, listWithNull);
+    assertCoerceArrayToList(listWithNull, arrayWithNull);
+    assertCoerceArrayToList(listWithNull, comparableList);
+    assertCoerceArrayToList(list, list);
+    assertCoerceArrayToList(list, array);
   }
 
   @Test
@@ -69,11 +92,11 @@ public class SqlResultsTest
     final List<Double> list = Arrays.asList(1.1, 2.2, 3.3);
     final double[] array = new double[]{1.1, 2.2, 3.3};
 
-    assertCoerced(listWithNull, listWithNull, true);
-    assertCoerced(listWithNull, arrayWithNull, true);
-    assertCoerced(listWithNull, comparableList, true);
-    assertCoerced(list, list, true);
-    assertCoerced(list, array, true);
+    assertCoerceArrayToList(listWithNull, listWithNull);
+    assertCoerceArrayToList(listWithNull, arrayWithNull);
+    assertCoerceArrayToList(listWithNull, comparableList);
+    assertCoerceArrayToList(list, list);
+    assertCoerceArrayToList(list, array);
   }
 
   @Test
@@ -85,11 +108,11 @@ public class SqlResultsTest
     final List<Float> list = Arrays.asList(1.1f, 2.2f, 3.3f);
     final float[] array = new float[]{1.1f, 2.2f, 3.3f};
 
-    assertCoerced(listWithNull, listWithNull, true);
-    assertCoerced(listWithNull, arrayWithNull, true);
-    assertCoerced(listWithNull, comparableList, true);
-    assertCoerced(list, list, true);
-    assertCoerced(list, array, true);
+    assertCoerceArrayToList(listWithNull, listWithNull);
+    assertCoerceArrayToList(listWithNull, arrayWithNull);
+    assertCoerceArrayToList(listWithNull, comparableList);
+    assertCoerceArrayToList(list, list);
+    assertCoerceArrayToList(list, array);
   }
 
   @Test
@@ -98,8 +121,117 @@ public class SqlResultsTest
     List<?> nestedList = Arrays.asList(Arrays.asList(1L, 2L, 3L), Arrays.asList(4L, 5L, 6L));
     Object[] nestedArray = new Object[]{new Object[]{1L, 2L, 3L}, new Object[]{4L, 5L, 6L}};
 
-    assertCoerced(nestedList, nestedList, true);
-    assertCoerced(nestedList, nestedArray, true);
+    assertCoerceArrayToList(nestedList, nestedList);
+    assertCoerceArrayToList(nestedList, nestedArray);
+  }
+
+  @Test
+  public void testCoerceBoolean()
+  {
+    assertCoerce(false, false, SqlTypeName.BOOLEAN);
+    assertCoerce(false, "xyz", SqlTypeName.BOOLEAN);
+    assertCoerce(false, 0, SqlTypeName.BOOLEAN);
+    assertCoerce(false, "false", SqlTypeName.BOOLEAN);
+    assertCoerce(true, true, SqlTypeName.BOOLEAN);
+    assertCoerce(true, "true", SqlTypeName.BOOLEAN);
+    assertCoerce(true, 1, SqlTypeName.BOOLEAN);
+    assertCoerce(true, 1.0, SqlTypeName.BOOLEAN);
+    assertCoerce(null, null, SqlTypeName.BOOLEAN);
+
+    assertCannotCoerce(Collections.emptyList(), SqlTypeName.BOOLEAN);
+  }
+
+  @Test
+  public void testCoerceInteger()
+  {
+    assertCoerce(0, 0, SqlTypeName.INTEGER);
+    assertCoerce(1, 1L, SqlTypeName.INTEGER);
+    assertCoerce(1, 1f, SqlTypeName.INTEGER);
+    assertCoerce(1, "1", SqlTypeName.INTEGER);
+    assertCoerce(null, "1.1", SqlTypeName.INTEGER);
+    assertCoerce(null, "xyz", SqlTypeName.INTEGER);
+    assertCoerce(null, null, SqlTypeName.INTEGER);
+
+    assertCannotCoerce(Collections.emptyList(), SqlTypeName.INTEGER);
+    assertCannotCoerce(false, SqlTypeName.INTEGER);
+  }
+
+  @Test
+  public void testCoerceBigint()
+  {
+    assertCoerce(0L, 0, SqlTypeName.BIGINT);
+    assertCoerce(1L, 1L, SqlTypeName.BIGINT);
+    assertCoerce(1L, 1f, SqlTypeName.BIGINT);
+    assertCoerce(null, "1.1", SqlTypeName.BIGINT);
+    assertCoerce(null, "xyz", SqlTypeName.BIGINT);
+    assertCoerce(null, null, SqlTypeName.BIGINT);
+
+    // Inconsistency with FLOAT, INTEGER, DOUBLE.
+    assertCoerce(0L, false, SqlTypeName.BIGINT);
+    assertCoerce(1L, true, SqlTypeName.BIGINT);
+
+    assertCannotCoerce(Collections.emptyList(), SqlTypeName.BIGINT);
+  }
+
+  @Test
+  public void testCoerceFloat()
+  {
+    assertCoerce(0f, 0, SqlTypeName.FLOAT);
+    assertCoerce(1f, 1L, SqlTypeName.FLOAT);
+    assertCoerce(1f, 1f, SqlTypeName.FLOAT);
+    assertCoerce(1.1f, "1.1", SqlTypeName.FLOAT);
+    assertCoerce(null, "xyz", SqlTypeName.FLOAT);
+    assertCoerce(null, null, SqlTypeName.FLOAT);
+
+    assertCannotCoerce(Collections.emptyList(), SqlTypeName.FLOAT);
+    assertCannotCoerce(false, SqlTypeName.FLOAT);
+  }
+
+  @Test
+  public void testCoerceDouble()
+  {
+    assertCoerce(0d, 0, SqlTypeName.DOUBLE);
+    assertCoerce(1d, 1L, SqlTypeName.DOUBLE);
+    assertCoerce(1d, 1f, SqlTypeName.DOUBLE);
+    assertCoerce(1.1d, "1.1", SqlTypeName.DOUBLE);
+    assertCoerce(null, "xyz", SqlTypeName.DOUBLE);
+    assertCoerce(null, null, SqlTypeName.DOUBLE);
+
+    assertCannotCoerce(Collections.emptyList(), SqlTypeName.DOUBLE);
+    assertCannotCoerce(false, SqlTypeName.DOUBLE);
+  }
+
+  @Test
+  public void testCoerceString()
+  {
+    assertCoerce(NullHandling.defaultStringValue(), null, SqlTypeName.VARCHAR);
+    assertCoerce("1", 1, SqlTypeName.VARCHAR);
+    assertCoerce("true", true, SqlTypeName.VARCHAR);
+    assertCoerce("abc", "abc", SqlTypeName.VARCHAR);
+
+    assertCoerce("[\"abc\",\"def\"]", ImmutableList.of("abc", "def"), SqlTypeName.VARCHAR);
+    assertCoerce("[\"abc\",\"def\"]", ImmutableSortedSet.of("abc", "def"), SqlTypeName.VARCHAR);
+    assertCoerce("[\"abc\",\"def\"]", new String[]{"abc", "def"}, SqlTypeName.VARCHAR);
+    assertCoerce("[\"abc\",\"def\"]", new Object[]{"abc", "def"}, SqlTypeName.VARCHAR);
+
+    assertCoerce("[\"abc\"]", ImmutableList.of("abc"), SqlTypeName.VARCHAR);
+    assertCoerce("[\"abc\"]", ImmutableSortedSet.of("abc"), SqlTypeName.VARCHAR);
+    assertCoerce("[\"abc\"]", new String[]{"abc"}, SqlTypeName.VARCHAR);
+    assertCoerce("[\"abc\"]", new Object[]{"abc"}, SqlTypeName.VARCHAR);
+
+    assertCannotCoerce(new Object(), SqlTypeName.VARCHAR);
+  }
+
+  @Test
+  public void testCoerceArrayFails()
+  {
+    assertCannotCoerce("xyz", SqlTypeName.ARRAY);
+  }
+
+  @Test
+  public void testCoerceUnsupportedType()
+  {
+    assertCannotCoerce("xyz", SqlTypeName.VARBINARY);
   }
 
   @Test
@@ -108,9 +240,35 @@ public class SqlResultsTest
     Assert.assertNull(SqlResults.maybeCoerceArrayToList("hello", true));
   }
 
-  private static void assertCoerced(Object expected, Object toCoerce, boolean mustCoerce)
+  @Test
+  public void testMayNotCoerce()
+  {
+    Assert.assertEquals("hello", SqlResults.maybeCoerceArrayToList("hello", false));
+  }
+
+  private void assertCoerce(Object expected, Object toCoerce, SqlTypeName typeName)
+  {
+    Assert.assertEquals(
+        StringUtils.format("Coerce [%s] to [%s]", toCoerce, typeName),
+        expected,
+        SqlResults.coerce(jsonMapper, DEFAULT_CONTEXT, toCoerce, typeName)
+    );
+  }
+
+  private void assertCannotCoerce(Object toCoerce, SqlTypeName typeName)
+  {
+    final IllegalStateException e = Assert.assertThrows(
+        StringUtils.format("Coerce [%s] to [%s]", toCoerce, typeName),
+        IllegalStateException.class,
+        () -> SqlResults.coerce(jsonMapper, DEFAULT_CONTEXT, toCoerce, typeName)
+    );
+
+    MatcherAssert.assertThat(e, ThrowableMessageMatcher.hasMessage(CoreMatchers.containsString("Cannot coerce")));
+  }
+
+  private static void assertCoerceArrayToList(Object expected, Object toCoerce)
   {
-    Object coerced = SqlResults.maybeCoerceArrayToList(toCoerce, mustCoerce);
+    Object coerced = SqlResults.maybeCoerceArrayToList(toCoerce, true);
     Assert.assertEquals(expected, coerced);
   }
 }


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