This is an automated email from the ASF dual-hosted git repository.
karan pushed a commit to branch 28.0.0
in repository https://gitbox.apache.org/repos/asf/druid.git
The following commit(s) were added to refs/heads/28.0.0 by this push:
new 392c1b1ee59 Introduce natural comparator for types that don't have a
StringComparator (#15145) (#15166)
392c1b1ee59 is described below
commit 392c1b1ee597db84789f4e7a966ebde15f2fa676
Author: Laksh Singla <[email protected]>
AuthorDate: Mon Oct 16 15:06:53 2023 +0530
Introduce natural comparator for types that don't have a StringComparator
(#15145) (#15166)
Fixes a bug when executing queries with the ordering of arrays
---
.../msq/querykit/groupby/GroupByQueryKit.java | 9 +
.../org/apache/druid/msq/exec/MSQArraysTest.java | 334 +++++++++++++++------
.../org/apache/druid/msq/exec/MSQSelectTest.java | 3 -
.../druid/jackson/StringComparatorModule.java | 3 +-
.../apache/druid/query/filter/BoundDimFilter.java | 3 +-
.../GrouperBufferComparatorUtils.java | 7 +-
.../epinephelinae/RowBasedGrouperHelper.java | 3 +-
.../druid/query/ordering/StringComparator.java | 2 +
.../druid/query/ordering/StringComparators.java | 56 +++-
.../query/ordering/StringComparatorsTest.java | 55 ++--
.../druid/sql/calcite/filtration/Bounds.java | 2 +-
.../apache/druid/sql/calcite/planner/Calcites.java | 9 +-
.../druid/sql/calcite/planner/CalcitesTest.java | 16 +
13 files changed, 380 insertions(+), 122 deletions(-)
diff --git
a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/groupby/GroupByQueryKit.java
b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/groupby/GroupByQueryKit.java
index 4ea56f73881..b12048a315f 100644
---
a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/groupby/GroupByQueryKit.java
+++
b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/groupby/GroupByQueryKit.java
@@ -311,8 +311,17 @@ public class GroupByQueryKit implements
QueryKit<GroupByQuery>
}
}
+ /**
+ * Only allow ordering the queries from the MSQ engine, ignoring the
comparator that is set in the query. This
+ * function checks if it is safe to do so, which is the case if the natural
comparator is used for the dimension.
+ * Since MSQ executes the queries planned by the SQL layer, this is a sanity
check as we always add the natural
+ * comparator for the dimensions there
+ */
private static boolean isNaturalComparator(final ValueType type, final
StringComparator comparator)
{
+ if (StringComparators.NATURAL.equals(comparator)) {
+ return true;
+ }
return ((type == ValueType.STRING &&
StringComparators.LEXICOGRAPHIC.equals(comparator))
|| (type.isNumeric() &&
StringComparators.NUMERIC.equals(comparator)))
&& !type.isArray();
diff --git
a/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQArraysTest.java
b/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQArraysTest.java
index d2696f23282..2b152cfbe1c 100644
---
a/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQArraysTest.java
+++
b/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQArraysTest.java
@@ -30,9 +30,11 @@ import org.apache.druid.msq.indexing.MSQTuningConfig;
import org.apache.druid.msq.indexing.destination.TaskReportMSQDestination;
import org.apache.druid.msq.test.MSQTestBase;
import org.apache.druid.msq.util.MultiStageQueryContext;
+import org.apache.druid.query.DataSource;
import org.apache.druid.query.NestedDataTestUtils;
import org.apache.druid.query.Query;
import org.apache.druid.query.expression.TestExprMacroTable;
+import org.apache.druid.query.scan.ScanQuery;
import org.apache.druid.segment.column.ColumnType;
import org.apache.druid.segment.column.RowSignature;
import org.apache.druid.segment.virtual.ExpressionVirtualColumn;
@@ -43,6 +45,7 @@ import org.apache.druid.sql.calcite.planner.ColumnMappings;
import org.apache.druid.timeline.SegmentId;
import org.apache.druid.utils.CompressionUtils;
import org.hamcrest.CoreMatchers;
+import org.junit.Before;
import org.junit.Test;
import org.junit.internal.matchers.ThrowableMessageMatcher;
import org.junit.runner.RunWith;
@@ -67,6 +70,9 @@ import java.util.Map;
@RunWith(Parameterized.class)
public class MSQArraysTest extends MSQTestBase
{
+ private String dataFileNameJsonString;
+ private String dataFileSignatureJsonString;
+ private DataSource dataFileExternalDataSource;
@Parameterized.Parameters(name = "{index}:with context {0}")
public static Collection<Object[]> data()
@@ -86,6 +92,40 @@ public class MSQArraysTest extends MSQTestBase
@Parameterized.Parameter(1)
public Map<String, Object> context;
+ @Before
+ public void setup() throws IOException
+ {
+ // Read the file and make the name available to the tests
+ File dataFile = temporaryFolder.newFile();
+ final InputStream resourceStream =
NestedDataTestUtils.class.getClassLoader()
+
.getResourceAsStream(NestedDataTestUtils.ARRAY_TYPES_DATA_FILE);
+ final InputStream decompressing = CompressionUtils.decompress(
+ resourceStream,
+ NestedDataTestUtils.ARRAY_TYPES_DATA_FILE
+ );
+ Files.copy(decompressing, dataFile.toPath(),
StandardCopyOption.REPLACE_EXISTING);
+ decompressing.close();
+
+ dataFileNameJsonString =
queryFramework().queryJsonMapper().writeValueAsString(dataFile);
+
+ RowSignature dataFileSignature = RowSignature.builder()
+ .add("timestamp",
ColumnType.STRING)
+ .add("arrayString",
ColumnType.STRING_ARRAY)
+ .add("arrayStringNulls",
ColumnType.STRING_ARRAY)
+ .add("arrayLong",
ColumnType.LONG_ARRAY)
+ .add("arrayLongNulls",
ColumnType.LONG_ARRAY)
+ .add("arrayDouble",
ColumnType.DOUBLE_ARRAY)
+ .add("arrayDoubleNulls",
ColumnType.DOUBLE_ARRAY)
+ .build();
+ dataFileSignatureJsonString =
queryFramework().queryJsonMapper().writeValueAsString(dataFileSignature);
+
+ dataFileExternalDataSource = new ExternalDataSource(
+ new LocalInputSource(null, null, ImmutableList.of(dataFile)),
+ new JsonInputFormat(null, null, null, null, null),
+ dataFileSignature
+ );
+ }
+
/**
* Tests the behaviour of INSERT query when arrayIngestMode is set to none
(default) and the user tries to ingest
* string arrays
@@ -135,7 +175,7 @@ public class MSQArraysTest extends MSQTestBase
* Tests the INSERT query when 'auto' type is set
*/
@Test
- public void testInsertArraysAutoType() throws IOException
+ public void testInsertArraysAutoType()
{
List<Object[]> expectedRows = Arrays.asList(
new Object[]{1672531200000L, null, null, null},
@@ -164,18 +204,6 @@ public class MSQArraysTest extends MSQTestBase
final Map<String, Object> adjustedContext = new HashMap<>(context);
adjustedContext.put(MultiStageQueryContext.CTX_USE_AUTO_SCHEMAS, true);
- final File tmpFile = temporaryFolder.newFile();
- final InputStream resourceStream =
NestedDataTestUtils.class.getClassLoader()
-
.getResourceAsStream(NestedDataTestUtils.ARRAY_TYPES_DATA_FILE);
- final InputStream decompressing = CompressionUtils.decompress(
- resourceStream,
- NestedDataTestUtils.ARRAY_TYPES_DATA_FILE
- );
- Files.copy(decompressing, tmpFile.toPath(),
StandardCopyOption.REPLACE_EXISTING);
- decompressing.close();
-
- final String toReadFileNameAsJson =
queryFramework().queryJsonMapper().writeValueAsString(tmpFile);
-
testIngestQuery().setSql(" INSERT INTO foo1 SELECT\n"
+ " TIME_PARSE(\"timestamp\") as __time,\n"
+ " arrayString,\n"
@@ -183,7 +211,7 @@ public class MSQArraysTest extends MSQTestBase
+ " arrayDouble\n"
+ "FROM TABLE(\n"
+ " EXTERN(\n"
- + " '{ \"files\": [" + toReadFileNameAsJson +
"],\"type\":\"local\"}',\n"
+ + " '{ \"files\": [" + dataFileNameJsonString
+ "],\"type\":\"local\"}',\n"
+ " '{\"type\": \"json\"}',\n"
+ " '[{\"name\": \"timestamp\", \"type\":
\"STRING\"}, {\"name\": \"arrayString\", \"type\": \"COMPLEX<json>\"},
{\"name\": \"arrayLong\", \"type\": \"COMPLEX<json>\"}, {\"name\":
\"arrayDouble\", \"type\": \"COMPLEX<json>\"}]'\n"
+ " )\n"
@@ -200,39 +228,11 @@ public class MSQArraysTest extends MSQTestBase
* types as well
*/
@Test
- public void testInsertArraysWithStringArraysAsMVDs() throws IOException
+ public void testInsertArraysWithStringArraysAsMVDs()
{
- RowSignature rowSignatureWithoutTimeAndStringColumns =
- RowSignature.builder()
- .add("arrayLong", ColumnType.LONG_ARRAY)
- .add("arrayLongNulls", ColumnType.LONG_ARRAY)
- .add("arrayDouble", ColumnType.DOUBLE_ARRAY)
- .add("arrayDoubleNulls", ColumnType.DOUBLE_ARRAY)
- .build();
-
-
- RowSignature fileSignature = RowSignature.builder()
- .add("timestamp",
ColumnType.STRING)
- .add("arrayString",
ColumnType.STRING_ARRAY)
- .add("arrayStringNulls",
ColumnType.STRING_ARRAY)
-
.addAll(rowSignatureWithoutTimeAndStringColumns)
- .build();
-
final Map<String, Object> adjustedContext = new HashMap<>(context);
adjustedContext.put(MultiStageQueryContext.CTX_ARRAY_INGEST_MODE, "mvd");
- final File tmpFile = temporaryFolder.newFile();
- final InputStream resourceStream =
NestedDataTestUtils.class.getClassLoader()
-
.getResourceAsStream(NestedDataTestUtils.ARRAY_TYPES_DATA_FILE);
- final InputStream decompressing = CompressionUtils.decompress(
- resourceStream,
- NestedDataTestUtils.ARRAY_TYPES_DATA_FILE
- );
- Files.copy(decompressing, tmpFile.toPath(),
StandardCopyOption.REPLACE_EXISTING);
- decompressing.close();
-
- final String toReadFileNameAsJson =
queryFramework().queryJsonMapper().writeValueAsString(tmpFile);
-
testIngestQuery().setSql(" INSERT INTO foo1 SELECT\n"
+ " TIME_PARSE(\"timestamp\") as __time,\n"
+ " arrayString,\n"
@@ -243,9 +243,9 @@ public class MSQArraysTest extends MSQTestBase
+ " arrayDoubleNulls\n"
+ "FROM TABLE(\n"
+ " EXTERN(\n"
- + " '{ \"files\": [" + toReadFileNameAsJson +
"],\"type\":\"local\"}',\n"
+ + " '{ \"files\": [" + dataFileNameJsonString
+ "],\"type\":\"local\"}',\n"
+ " '{\"type\": \"json\"}',\n"
- + " '" +
queryFramework().queryJsonMapper().writeValueAsString(fileSignature) + "'\n"
+ + " '" + dataFileSignatureJsonString + "'\n"
+ " )\n"
+ ") PARTITIONED BY ALL")
.setQueryContext(adjustedContext)
@@ -262,7 +262,7 @@ public class MSQArraysTest extends MSQTestBase
* array types
*/
@Test
- public void testInsertArraysAsArrays() throws IOException
+ public void testInsertArraysAsArrays()
{
final List<Object[]> expectedRows = Arrays.asList(
new Object[]{
@@ -403,11 +403,6 @@ public class MSQArraysTest extends MSQTestBase
.add("arrayDoubleNulls", ColumnType.DOUBLE_ARRAY)
.build();
- RowSignature fileSignature = RowSignature.builder()
- .add("timestamp",
ColumnType.STRING)
-
.addAll(rowSignatureWithoutTimeColumn)
- .build();
-
RowSignature rowSignature = RowSignature.builder()
.add("__time", ColumnType.LONG)
.addAll(rowSignatureWithoutTimeColumn)
@@ -416,18 +411,6 @@ public class MSQArraysTest extends MSQTestBase
final Map<String, Object> adjustedContext = new HashMap<>(context);
adjustedContext.put(MultiStageQueryContext.CTX_ARRAY_INGEST_MODE, "array");
- final File tmpFile = temporaryFolder.newFile();
- final InputStream resourceStream =
NestedDataTestUtils.class.getClassLoader()
-
.getResourceAsStream(NestedDataTestUtils.ARRAY_TYPES_DATA_FILE);
- final InputStream decompressing = CompressionUtils.decompress(
- resourceStream,
- NestedDataTestUtils.ARRAY_TYPES_DATA_FILE
- );
- Files.copy(decompressing, tmpFile.toPath(),
StandardCopyOption.REPLACE_EXISTING);
- decompressing.close();
-
- final String toReadFileNameAsJson =
queryFramework().queryJsonMapper().writeValueAsString(tmpFile);
-
testIngestQuery().setSql(" INSERT INTO foo1 SELECT\n"
+ " TIME_PARSE(\"timestamp\") as __time,\n"
+ " arrayString,\n"
@@ -438,9 +421,9 @@ public class MSQArraysTest extends MSQTestBase
+ " arrayDoubleNulls\n"
+ "FROM TABLE(\n"
+ " EXTERN(\n"
- + " '{ \"files\": [" + toReadFileNameAsJson +
"],\"type\":\"local\"}',\n"
+ + " '{ \"files\": [" + dataFileNameJsonString
+ "],\"type\":\"local\"}',\n"
+ " '{\"type\": \"json\"}',\n"
- + " '" +
queryFramework().queryJsonMapper().writeValueAsString(fileSignature) + "'\n"
+ + " '" + dataFileSignatureJsonString + "'\n"
+ " )\n"
+ ") PARTITIONED BY ALL")
.setQueryContext(adjustedContext)
@@ -451,26 +434,26 @@ public class MSQArraysTest extends MSQTestBase
}
@Test
- public void testSelectOnArraysWithArrayIngestModeAsNone() throws IOException
+ public void testSelectOnArraysWithArrayIngestModeAsNone()
{
testSelectOnArrays("none");
}
@Test
- public void testSelectOnArraysWithArrayIngestModeAsMVD() throws IOException
+ public void testSelectOnArraysWithArrayIngestModeAsMVD()
{
testSelectOnArrays("mvd");
}
@Test
- public void testSelectOnArraysWithArrayIngestModeAsArray() throws IOException
+ public void testSelectOnArraysWithArrayIngestModeAsArray()
{
testSelectOnArrays("array");
}
// Tests the behaviour of the select with the given arrayIngestMode. The
expectation should be the same, since the
// arrayIngestMode should only determine how the array gets ingested at the
end.
- public void testSelectOnArrays(String arrayIngestMode) throws IOException
+ public void testSelectOnArrays(String arrayIngestMode)
{
final List<Object[]> expectedRows = Arrays.asList(
new Object[]{
@@ -611,11 +594,6 @@ public class MSQArraysTest extends MSQTestBase
.add("arrayDoubleNulls", ColumnType.DOUBLE_ARRAY)
.build();
- RowSignature fileSignature = RowSignature.builder()
- .add("timestamp",
ColumnType.STRING)
-
.addAll(rowSignatureWithoutTimeColumn)
- .build();
-
RowSignature rowSignature = RowSignature.builder()
.add("__time", ColumnType.LONG)
.addAll(rowSignatureWithoutTimeColumn)
@@ -634,24 +612,8 @@ public class MSQArraysTest extends MSQTestBase
final Map<String, Object> adjustedContext = new HashMap<>(context);
adjustedContext.put(MultiStageQueryContext.CTX_ARRAY_INGEST_MODE,
arrayIngestMode);
- final File tmpFile = temporaryFolder.newFile();
- final InputStream resourceStream =
NestedDataTestUtils.class.getClassLoader()
-
.getResourceAsStream(NestedDataTestUtils.ARRAY_TYPES_DATA_FILE);
- final InputStream decompressing = CompressionUtils.decompress(
- resourceStream,
- NestedDataTestUtils.ARRAY_TYPES_DATA_FILE
- );
- Files.copy(decompressing, tmpFile.toPath(),
StandardCopyOption.REPLACE_EXISTING);
- decompressing.close();
-
- final String toReadFileNameAsJson =
queryFramework().queryJsonMapper().writeValueAsString(tmpFile);
-
Query<?> expectedQuery = newScanQueryBuilder()
- .dataSource(new ExternalDataSource(
- new LocalInputSource(null, null, ImmutableList.of(tmpFile)),
- new JsonInputFormat(null, null, null, null, null),
- fileSignature
- ))
+ .dataSource(dataFileExternalDataSource)
.intervals(querySegmentSpec(Filtration.eternity()))
.columns(
"arrayDouble",
@@ -681,9 +643,9 @@ public class MSQArraysTest extends MSQTestBase
+ " arrayDoubleNulls\n"
+ "FROM TABLE(\n"
+ " EXTERN(\n"
- + " '{ \"files\": [" + toReadFileNameAsJson +
"],\"type\":\"local\"}',\n"
+ + " '{ \"files\": [" + dataFileNameJsonString
+ "],\"type\":\"local\"}',\n"
+ " '{\"type\": \"json\"}',\n"
- + " '" +
queryFramework().queryJsonMapper().writeValueAsString(fileSignature) + "'\n"
+ + " '" + dataFileSignatureJsonString + "'\n"
+ " )\n"
+ ")")
.setQueryContext(adjustedContext)
@@ -708,6 +670,192 @@ public class MSQArraysTest extends MSQTestBase
.verifyResults();
}
+ @Test
+ public void testScanWithOrderByOnStringArray()
+ {
+ final List<Object[]> expectedRows = Arrays.asList(
+ new Object[]{Arrays.asList("d", "e")},
+ new Object[]{Arrays.asList("d", "e")},
+ new Object[]{Arrays.asList("b", "c")},
+ new Object[]{Arrays.asList("b", "c")},
+ new Object[]{Arrays.asList("a", "b", "c")},
+ new Object[]{Arrays.asList("a", "b", "c")},
+ new Object[]{Arrays.asList("a", "b")},
+ new Object[]{Arrays.asList("a", "b")},
+ new Object[]{Arrays.asList("a", "b")},
+ new Object[]{Arrays.asList("a", "b")},
+ new Object[]{null},
+ new Object[]{null},
+ new Object[]{null},
+ new Object[]{null}
+ );
+
+
+ RowSignature rowSignature = RowSignature.builder()
+ .add("arrayString",
ColumnType.STRING_ARRAY)
+ .build();
+
+ RowSignature scanSignature = RowSignature.builder()
+ .add("arrayString",
ColumnType.STRING_ARRAY)
+ .build();
+
+ Query<?> expectedQuery = newScanQueryBuilder()
+ .dataSource(dataFileExternalDataSource)
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .columns("arrayString")
+ .orderBy(Collections.singletonList(new
ScanQuery.OrderBy("arrayString", ScanQuery.Order.DESCENDING)))
+ .context(defaultScanQueryContext(context, scanSignature))
+ .build();
+
+ testSelectQuery().setSql("SELECT\n"
+ + " arrayString\n"
+ + "FROM TABLE(\n"
+ + " EXTERN(\n"
+ + " '{ \"files\": [" + dataFileNameJsonString
+ "],\"type\":\"local\"}',\n"
+ + " '{\"type\": \"json\"}',\n"
+ + " '" + dataFileSignatureJsonString + "'\n"
+ + " )\n"
+ + ")\n"
+ + "ORDER BY arrayString DESC")
+ .setQueryContext(context)
+ .setExpectedMSQSpec(MSQSpec
+ .builder()
+ .query(expectedQuery)
+ .columnMappings(new
ColumnMappings(ImmutableList.of(
+ new
ColumnMapping("arrayString", "arrayString")
+ )))
+
.tuningConfig(MSQTuningConfig.defaultConfig())
+
.destination(TaskReportMSQDestination.INSTANCE)
+ .build()
+ )
+ .setExpectedRowSignature(rowSignature)
+ .setExpectedResultRows(expectedRows)
+ .verifyResults();
+ }
+
+ @Test
+ public void testScanWithOrderByOnLongArray()
+ {
+ final List<Object[]> expectedRows = Arrays.asList(
+ new Object[]{null},
+ new Object[]{null},
+ new Object[]{null},
+ new Object[]{null},
+ new Object[]{Arrays.asList(1L, 2L, 3L)},
+ new Object[]{Arrays.asList(1L, 2L, 3L)},
+ new Object[]{Arrays.asList(1L, 2L, 3L)},
+ new Object[]{Arrays.asList(1L, 2L, 3L)},
+ new Object[]{Arrays.asList(1L, 2L, 3L, 4L)},
+ new Object[]{Arrays.asList(1L, 2L, 3L, 4L)},
+ new Object[]{Arrays.asList(1L, 4L)},
+ new Object[]{Arrays.asList(1L, 4L)},
+ new Object[]{Arrays.asList(2L, 3L)},
+ new Object[]{Arrays.asList(2L, 3L)}
+ );
+
+ RowSignature rowSignature = RowSignature.builder()
+ .add("arrayLong",
ColumnType.LONG_ARRAY)
+ .build();
+
+ RowSignature scanSignature = RowSignature.builder()
+ .add("arrayLong",
ColumnType.LONG_ARRAY)
+ .build();
+
+ Query<?> expectedQuery = newScanQueryBuilder()
+ .dataSource(dataFileExternalDataSource)
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .columns("arrayLong")
+ .orderBy(Collections.singletonList(new ScanQuery.OrderBy("arrayLong",
ScanQuery.Order.ASCENDING)))
+ .context(defaultScanQueryContext(context, scanSignature))
+ .build();
+
+ testSelectQuery().setSql("SELECT\n"
+ + " arrayLong\n"
+ + "FROM TABLE(\n"
+ + " EXTERN(\n"
+ + " '{ \"files\": [" + dataFileNameJsonString
+ "],\"type\":\"local\"}',\n"
+ + " '{\"type\": \"json\"}',\n"
+ + " '" + dataFileSignatureJsonString + "'\n"
+ + " )\n"
+ + ")\n"
+ + "ORDER BY arrayLong")
+ .setQueryContext(context)
+ .setExpectedMSQSpec(MSQSpec
+ .builder()
+ .query(expectedQuery)
+ .columnMappings(new
ColumnMappings(ImmutableList.of(
+ new
ColumnMapping("arrayLong", "arrayLong")
+ )))
+
.tuningConfig(MSQTuningConfig.defaultConfig())
+
.destination(TaskReportMSQDestination.INSTANCE)
+ .build()
+ )
+ .setExpectedRowSignature(rowSignature)
+ .setExpectedResultRows(expectedRows)
+ .verifyResults();
+ }
+
+ @Test
+ public void testScanWithOrderByOnDoubleArray()
+ {
+ final List<Object[]> expectedRows = Arrays.asList(
+ new Object[]{null},
+ new Object[]{null},
+ new Object[]{null},
+ new Object[]{null},
+ new Object[]{Arrays.asList(1.1d, 2.2d, 3.3d)},
+ new Object[]{Arrays.asList(1.1d, 2.2d, 3.3d)},
+ new Object[]{Arrays.asList(1.1d, 2.2d, 3.3d)},
+ new Object[]{Arrays.asList(1.1d, 2.2d, 3.3d)},
+ new Object[]{Arrays.asList(1.1d, 3.3d)},
+ new Object[]{Arrays.asList(1.1d, 3.3d)},
+ new Object[]{Arrays.asList(2.2d, 3.3d, 4.0d)},
+ new Object[]{Arrays.asList(2.2d, 3.3d, 4.0d)},
+ new Object[]{Arrays.asList(3.3d, 4.4d, 5.5d)},
+ new Object[]{Arrays.asList(3.3d, 4.4d, 5.5d)}
+ );
+
+ RowSignature rowSignature = RowSignature.builder()
+ .add("arrayDouble",
ColumnType.DOUBLE_ARRAY)
+ .build();
+
+ RowSignature scanSignature = RowSignature.builder()
+ .add("arrayDouble",
ColumnType.DOUBLE_ARRAY)
+ .build();
+
+ Query<?> expectedQuery = newScanQueryBuilder()
+ .dataSource(dataFileExternalDataSource)
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .columns("arrayDouble")
+ .orderBy(Collections.singletonList(new
ScanQuery.OrderBy("arrayDouble", ScanQuery.Order.ASCENDING)))
+ .context(defaultScanQueryContext(context, scanSignature))
+ .build();
+
+ testSelectQuery().setSql("SELECT\n"
+ + " arrayDouble\n"
+ + "FROM TABLE(\n"
+ + " EXTERN(\n"
+ + " '{ \"files\": [" + dataFileNameJsonString
+ "],\"type\":\"local\"}',\n"
+ + " '{\"type\": \"json\"}',\n"
+ + " '" + dataFileSignatureJsonString + "'\n"
+ + " )\n"
+ + ")\n"
+ + "ORDER BY arrayDouble")
+ .setQueryContext(context)
+ .setExpectedMSQSpec(MSQSpec
+ .builder()
+ .query(expectedQuery)
+ .columnMappings(new
ColumnMappings(ImmutableList.of(
+ new
ColumnMapping("arrayDouble", "arrayDouble")
+ )))
+
.tuningConfig(MSQTuningConfig.defaultConfig())
+
.destination(TaskReportMSQDestination.INSTANCE)
+ .build()
+ )
+ .setExpectedRowSignature(rowSignature)
+ .setExpectedResultRows(expectedRows)
+ .verifyResults();
+ }
private List<Object[]> expectedMultiValueFooRowsToArray()
{
diff --git
a/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQSelectTest.java
b/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQSelectTest.java
index 30bbb850fbc..219af6a3188 100644
---
a/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQSelectTest.java
+++
b/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQSelectTest.java
@@ -89,7 +89,6 @@ import org.junit.runners.Parameterized;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
-import javax.annotation.Nonnull;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
@@ -2534,7 +2533,6 @@ public class MSQSelectTest extends MSQTestBase
.verifyResults();
}
- @Nonnull
private List<Object[]> expectedMultiValueFooRowsGroup()
{
ArrayList<Object[]> expected = new ArrayList<>();
@@ -2553,7 +2551,6 @@ public class MSQSelectTest extends MSQTestBase
return expected;
}
- @Nonnull
private List<Object[]> expectedMultiValueFooRowsGroupByList()
{
ArrayList<Object[]> expected = new ArrayList<>();
diff --git
a/processing/src/main/java/org/apache/druid/jackson/StringComparatorModule.java
b/processing/src/main/java/org/apache/druid/jackson/StringComparatorModule.java
index 4b7ea2953db..d0e1f1128bf 100644
---
a/processing/src/main/java/org/apache/druid/jackson/StringComparatorModule.java
+++
b/processing/src/main/java/org/apache/druid/jackson/StringComparatorModule.java
@@ -37,7 +37,8 @@ public class StringComparatorModule extends SimpleModule
new NamedType(StringComparators.AlphanumericComparator.class,
StringComparators.ALPHANUMERIC_NAME),
new NamedType(StringComparators.StrlenComparator.class,
StringComparators.STRLEN_NAME),
new NamedType(StringComparators.NumericComparator.class,
StringComparators.NUMERIC_NAME),
- new NamedType(StringComparators.VersionComparator.class,
StringComparators.VERSION_NAME)
+ new NamedType(StringComparators.VersionComparator.class,
StringComparators.VERSION_NAME),
+ new NamedType(StringComparators.NaturalComparator.class,
StringComparators.NATURAL_NAME)
);
}
diff --git
a/processing/src/main/java/org/apache/druid/query/filter/BoundDimFilter.java
b/processing/src/main/java/org/apache/druid/query/filter/BoundDimFilter.java
index 17cecc41151..412ec7e5726 100644
--- a/processing/src/main/java/org/apache/druid/query/filter/BoundDimFilter.java
+++ b/processing/src/main/java/org/apache/druid/query/filter/BoundDimFilter.java
@@ -100,7 +100,8 @@ public class BoundDimFilter extends
AbstractOptimizableDimFilter implements DimF
boolean orderingIsAlphanumeric =
this.ordering.equals(StringComparators.ALPHANUMERIC);
Preconditions.checkState(
alphaNumeric == orderingIsAlphanumeric,
- "mismatch between alphanumeric and ordering property");
+ "mismatch between alphanumeric and ordering property"
+ );
}
}
this.extractionFn = extractionFn;
diff --git
a/processing/src/main/java/org/apache/druid/query/groupby/epinephelinae/GrouperBufferComparatorUtils.java
b/processing/src/main/java/org/apache/druid/query/groupby/epinephelinae/GrouperBufferComparatorUtils.java
index 08ba4ba2ee6..109ddb66f3d 100644
---
a/processing/src/main/java/org/apache/druid/query/groupby/epinephelinae/GrouperBufferComparatorUtils.java
+++
b/processing/src/main/java/org/apache/druid/query/groupby/epinephelinae/GrouperBufferComparatorUtils.java
@@ -434,6 +434,11 @@ public class GrouperBufferComparatorUtils
private static boolean isPrimitiveComparable(boolean pushLimitDown,
@Nullable StringComparator stringComparator)
{
- return !pushLimitDown || stringComparator == null ||
stringComparator.equals(StringComparators.NUMERIC);
+ return !pushLimitDown
+ || stringComparator == null
+ || stringComparator.equals(StringComparators.NUMERIC)
+ // NATURAL isn't set for numeric types, however if it is, then that
would mean that we are ordering the
+ // numeric type with its natural comparator (which is NUMERIC)
+ || stringComparator.equals(StringComparators.NATURAL);
}
}
diff --git
a/processing/src/main/java/org/apache/druid/query/groupby/epinephelinae/RowBasedGrouperHelper.java
b/processing/src/main/java/org/apache/druid/query/groupby/epinephelinae/RowBasedGrouperHelper.java
index c5e561c315f..8c3e485ef7c 100644
---
a/processing/src/main/java/org/apache/druid/query/groupby/epinephelinae/RowBasedGrouperHelper.java
+++
b/processing/src/main/java/org/apache/druid/query/groupby/epinephelinae/RowBasedGrouperHelper.java
@@ -1116,7 +1116,8 @@ public class RowBasedGrouperHelper
final StringComparator comparator = comparators.get(i);
final ColumnType fieldType = fieldTypes.get(i);
- if (fieldType.isNumeric() &&
comparator.equals(StringComparators.NUMERIC)) {
+ if (fieldType.isNumeric()
+ && (comparator.equals(StringComparators.NUMERIC) ||
comparator.equals(StringComparators.NATURAL))) {
// use natural comparison
if (fieldType.is(ValueType.DOUBLE)) {
// sometimes doubles can become floats making the round trip from
serde, make sure to coerce them both
diff --git
a/processing/src/main/java/org/apache/druid/query/ordering/StringComparator.java
b/processing/src/main/java/org/apache/druid/query/ordering/StringComparator.java
index 8e66baf7df3..44d03adce44 100644
---
a/processing/src/main/java/org/apache/druid/query/ordering/StringComparator.java
+++
b/processing/src/main/java/org/apache/druid/query/ordering/StringComparator.java
@@ -41,6 +41,8 @@ public abstract class StringComparator implements
Comparator<String>
return StringComparators.NUMERIC;
case StringComparators.VERSION_NAME:
return StringComparators.VERSION;
+ case StringComparators.NATURAL_NAME:
+ return StringComparators.NATURAL;
default:
throw new IAE("Unknown string comparator[%s]", type);
}
diff --git
a/processing/src/main/java/org/apache/druid/query/ordering/StringComparators.java
b/processing/src/main/java/org/apache/druid/query/ordering/StringComparators.java
index 4fdcc5c6f3a..650b259ed56 100644
---
a/processing/src/main/java/org/apache/druid/query/ordering/StringComparators.java
+++
b/processing/src/main/java/org/apache/druid/query/ordering/StringComparators.java
@@ -22,6 +22,8 @@ package org.apache.druid.query.ordering;
import com.google.common.collect.Ordering;
import com.google.common.primitives.Ints;
import org.apache.druid.common.guava.GuavaUtils;
+import org.apache.druid.error.DruidException;
+import org.apache.druid.java.util.common.StringUtils;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import java.math.BigDecimal;
@@ -34,25 +36,28 @@ public class StringComparators
public static final String NUMERIC_NAME = "numeric";
public static final String STRLEN_NAME = "strlen";
public static final String VERSION_NAME = "version";
+ public static final String NATURAL_NAME = "natural";
public static final StringComparator LEXICOGRAPHIC = new
LexicographicComparator();
public static final StringComparator ALPHANUMERIC = new
AlphanumericComparator();
public static final StringComparator NUMERIC = new NumericComparator();
public static final StringComparator STRLEN = new StrlenComparator();
public static final StringComparator VERSION = new VersionComparator();
+ public static final StringComparator NATURAL = new NaturalComparator();
public static final int LEXICOGRAPHIC_CACHE_ID = 0x01;
public static final int ALPHANUMERIC_CACHE_ID = 0x02;
public static final int NUMERIC_CACHE_ID = 0x03;
public static final int STRLEN_CACHE_ID = 0x04;
public static final int VERSION_CACHE_ID = 0x05;
+ public static final int NATURAL_CACHE_ID = 0x06;
/**
* Comparison using the natural comparator of {@link String}.
*
* Note that this is not equivalent to comparing UTF-8 byte arrays; see
javadocs for
- * {@link
org.apache.druid.java.util.common.StringUtils#compareUnicode(String, String)}
and
- * {@link
org.apache.druid.java.util.common.StringUtils#compareUtf8UsingJavaStringOrdering(byte[],
byte[])}.
+ * {@link StringUtils#compareUnicode(String, String)} and
+ * {@link StringUtils#compareUtf8UsingJavaStringOrdering(byte[], byte[])}.
*/
public static class LexicographicComparator extends StringComparator
{
@@ -492,4 +497,51 @@ public class StringComparators
return new byte[]{(byte) VERSION_CACHE_ID};
}
}
+
+ /**
+ * NaturalComparator refers to the natural ordering of the type that it
refers.
+ *
+ * For example, if the type is Long, the natural ordering would be numeric
+ * if the type is an array, the natural ordering would be lexicographic
comparison of the natural ordering of the
+ * elements in the arrays.
+ *
+ * It is a sigil value for the dimension that we can handle in the execution
layer, and don't need the comparator for.
+ * It is also a placeholder for dimensions that we don't have a comparator
for (like arrays), but is a required for
+ * planning
+ */
+ public static class NaturalComparator extends StringComparator
+ {
+ @Override
+ public int compare(String o1, String o2)
+ {
+ throw DruidException.defensive("compare() should not be called for the
NaturalComparator");
+ }
+
+ @Override
+ public String toString()
+ {
+ return StringComparators.NATURAL_NAME;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) {
+ return true;
+ }
+ return o != null && getClass() == o.getClass();
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return 0;
+ }
+
+ @Override
+ public byte[] getCacheKey()
+ {
+ return new byte[]{(byte) NATURAL_CACHE_ID};
+ }
+ }
}
diff --git
a/processing/src/test/java/org/apache/druid/query/ordering/StringComparatorsTest.java
b/processing/src/test/java/org/apache/druid/query/ordering/StringComparatorsTest.java
index 7fa0a3b82ca..db80a598802 100644
---
a/processing/src/test/java/org/apache/druid/query/ordering/StringComparatorsTest.java
+++
b/processing/src/test/java/org/apache/druid/query/ordering/StringComparatorsTest.java
@@ -22,6 +22,7 @@ package org.apache.druid.query.ordering;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
+import org.apache.druid.error.DruidException;
import org.apache.druid.jackson.DefaultObjectMapper;
import org.junit.Assert;
import org.junit.Test;
@@ -33,6 +34,8 @@ import java.util.List;
public class StringComparatorsTest
{
+ private static final ObjectMapper JSON_MAPPER = new DefaultObjectMapper();
+
private void commonTest(StringComparator comparator)
{
// equality test
@@ -156,65 +159,83 @@ public class StringComparatorsTest
Assert.assertTrue(StringComparators.VERSION.compare("1.0-SNAPSHOT",
"1.0-Final") < 0);
}
+ @Test
+ public void testNaturalComparator()
+ {
+ Assert.assertThrows(DruidException.class, () ->
StringComparators.NATURAL.compare("str1", "str2"));
+ }
+
@Test
public void testLexicographicComparatorSerdeTest() throws IOException
{
- ObjectMapper jsonMapper = new DefaultObjectMapper();
String expectJsonSpec = "{\"type\":\"lexicographic\"}";
- String jsonSpec =
jsonMapper.writeValueAsString(StringComparators.LEXICOGRAPHIC);
+ String jsonSpec =
JSON_MAPPER.writeValueAsString(StringComparators.LEXICOGRAPHIC);
Assert.assertEquals(expectJsonSpec, jsonSpec);
- Assert.assertEquals(StringComparators.LEXICOGRAPHIC,
jsonMapper.readValue(expectJsonSpec, StringComparator.class));
+ Assert.assertEquals(StringComparators.LEXICOGRAPHIC,
JSON_MAPPER.readValue(expectJsonSpec, StringComparator.class));
String makeFromJsonSpec = "\"lexicographic\"";
Assert.assertEquals(
StringComparators.LEXICOGRAPHIC,
- jsonMapper.readValue(makeFromJsonSpec, StringComparator.class)
+ JSON_MAPPER.readValue(makeFromJsonSpec, StringComparator.class)
);
}
@Test
public void testAlphanumericComparatorSerdeTest() throws IOException
{
- ObjectMapper jsonMapper = new DefaultObjectMapper();
String expectJsonSpec = "{\"type\":\"alphanumeric\"}";
- String jsonSpec =
jsonMapper.writeValueAsString(StringComparators.ALPHANUMERIC);
+ String jsonSpec =
JSON_MAPPER.writeValueAsString(StringComparators.ALPHANUMERIC);
Assert.assertEquals(expectJsonSpec, jsonSpec);
- Assert.assertEquals(StringComparators.ALPHANUMERIC,
jsonMapper.readValue(expectJsonSpec, StringComparator.class));
+ Assert.assertEquals(StringComparators.ALPHANUMERIC,
JSON_MAPPER.readValue(expectJsonSpec, StringComparator.class));
String makeFromJsonSpec = "\"alphanumeric\"";
- Assert.assertEquals(StringComparators.ALPHANUMERIC,
jsonMapper.readValue(makeFromJsonSpec, StringComparator.class));
+ Assert.assertEquals(StringComparators.ALPHANUMERIC,
JSON_MAPPER.readValue(makeFromJsonSpec, StringComparator.class));
}
@Test
public void testStrlenComparatorSerdeTest() throws IOException
{
- ObjectMapper jsonMapper = new DefaultObjectMapper();
String expectJsonSpec = "{\"type\":\"strlen\"}";
- String jsonSpec = jsonMapper.writeValueAsString(StringComparators.STRLEN);
+ String jsonSpec = JSON_MAPPER.writeValueAsString(StringComparators.STRLEN);
Assert.assertEquals(expectJsonSpec, jsonSpec);
- Assert.assertEquals(StringComparators.STRLEN,
jsonMapper.readValue(expectJsonSpec, StringComparator.class));
+ Assert.assertEquals(StringComparators.STRLEN,
JSON_MAPPER.readValue(expectJsonSpec, StringComparator.class));
String makeFromJsonSpec = "\"strlen\"";
- Assert.assertEquals(StringComparators.STRLEN,
jsonMapper.readValue(makeFromJsonSpec, StringComparator.class));
+ Assert.assertEquals(StringComparators.STRLEN,
JSON_MAPPER.readValue(makeFromJsonSpec, StringComparator.class));
}
@Test
public void testNumericComparatorSerdeTest() throws IOException
{
- ObjectMapper jsonMapper = new DefaultObjectMapper();
String expectJsonSpec = "{\"type\":\"numeric\"}";
- String jsonSpec = jsonMapper.writeValueAsString(StringComparators.NUMERIC);
+ String jsonSpec =
JSON_MAPPER.writeValueAsString(StringComparators.NUMERIC);
Assert.assertEquals(expectJsonSpec, jsonSpec);
- Assert.assertEquals(StringComparators.NUMERIC,
jsonMapper.readValue(expectJsonSpec, StringComparator.class));
+ Assert.assertEquals(StringComparators.NUMERIC,
JSON_MAPPER.readValue(expectJsonSpec, StringComparator.class));
String makeFromJsonSpec = "\"numeric\"";
- Assert.assertEquals(StringComparators.NUMERIC,
jsonMapper.readValue(makeFromJsonSpec, StringComparator.class));
+ Assert.assertEquals(StringComparators.NUMERIC,
JSON_MAPPER.readValue(makeFromJsonSpec, StringComparator.class));
makeFromJsonSpec = "\"NuMeRiC\"";
- Assert.assertEquals(StringComparators.NUMERIC,
jsonMapper.readValue(makeFromJsonSpec, StringComparator.class));
+ Assert.assertEquals(StringComparators.NUMERIC,
JSON_MAPPER.readValue(makeFromJsonSpec, StringComparator.class));
+ }
+
+ @Test
+ public void testNaturalComparatorSerdeTest() throws IOException
+ {
+ String expectJsonSpec = "{\"type\":\"natural\"}";
+
+ String jsonSpec =
JSON_MAPPER.writeValueAsString(StringComparators.NATURAL);
+ Assert.assertEquals(expectJsonSpec, jsonSpec);
+ Assert.assertEquals(StringComparators.NATURAL,
JSON_MAPPER.readValue(expectJsonSpec, StringComparator.class));
+
+ String makeFromJsonSpec = "\"natural\"";
+ Assert.assertEquals(StringComparators.NATURAL,
JSON_MAPPER.readValue(makeFromJsonSpec, StringComparator.class));
+
+ makeFromJsonSpec = "\"NaTuRaL\"";
+ Assert.assertEquals(StringComparators.NATURAL,
JSON_MAPPER.readValue(makeFromJsonSpec, StringComparator.class));
}
}
diff --git
a/sql/src/main/java/org/apache/druid/sql/calcite/filtration/Bounds.java
b/sql/src/main/java/org/apache/druid/sql/calcite/filtration/Bounds.java
index b41833d7b33..a1c310522c8 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/filtration/Bounds.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/filtration/Bounds.java
@@ -198,7 +198,7 @@ public class Bounds
public static BoundDimFilter interval(final BoundRefKey boundRefKey, final
Interval interval)
{
if (!boundRefKey.getComparator().equals(StringComparators.NUMERIC)) {
- // Interval comparison only works with NUMERIC comparator.
+ // Interval comparison only works with NUMERIC comparator
throw new ISE("Comparator must be NUMERIC but was[%s]",
boundRefKey.getComparator());
}
diff --git
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/Calcites.java
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/Calcites.java
index 913790fd944..22b0aacfa05 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/Calcites.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/Calcites.java
@@ -40,7 +40,6 @@ import org.apache.calcite.util.TimeString;
import org.apache.calcite.util.TimestampString;
import org.apache.druid.java.util.common.DateTimes;
import org.apache.druid.java.util.common.IAE;
-import org.apache.druid.java.util.common.ISE;
import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.math.expr.ExpressionProcessing;
import org.apache.druid.math.expr.ExpressionProcessingConfig;
@@ -208,12 +207,18 @@ public class Calcites
SqlTypeName.INT_TYPES.contains(sqlTypeName);
}
+ /**
+ * Returns the natural StringComparator associated with the RelDataType
+ */
public static StringComparator getStringComparatorForRelDataType(RelDataType
dataType)
{
final ColumnType valueType = getColumnTypeForRelDataType(dataType);
return getStringComparatorForValueType(valueType);
}
+ /**
+ * Returns the natural StringComparator associated with the given ColumnType
+ */
public static StringComparator getStringComparatorForValueType(ColumnType
valueType)
{
if (valueType.isNumeric()) {
@@ -221,7 +226,7 @@ public class Calcites
} else if (valueType.is(ValueType.STRING)) {
return StringComparators.LEXICOGRAPHIC;
} else {
- throw new ISE("Unrecognized valueType[%s]", valueType);
+ return StringComparators.NATURAL;
}
}
diff --git
a/sql/src/test/java/org/apache/druid/sql/calcite/planner/CalcitesTest.java
b/sql/src/test/java/org/apache/druid/sql/calcite/planner/CalcitesTest.java
index 676c0b7fab6..576a57cc937 100644
--- a/sql/src/test/java/org/apache/druid/sql/calcite/planner/CalcitesTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/planner/CalcitesTest.java
@@ -20,6 +20,8 @@
package org.apache.druid.sql.calcite.planner;
import com.google.common.collect.ImmutableSortedSet;
+import org.apache.druid.query.ordering.StringComparators;
+import org.apache.druid.segment.column.ColumnType;
import org.apache.druid.sql.calcite.util.CalciteTestBase;
import org.junit.Assert;
import org.junit.Test;
@@ -52,4 +54,18 @@ public class CalcitesTest extends CalciteTestBase
Assert.assertEquals("x", Calcites.findUnusedPrefixForDigits("x",
ImmutableSortedSet.of("foo", "xa", "_x")));
Assert.assertEquals("__x", Calcites.findUnusedPrefixForDigits("x",
ImmutableSortedSet.of("foo", "x1a", "_x90")));
}
+
+ @Test
+ public void testGetStringComparatorForColumnType()
+ {
+ Assert.assertEquals(StringComparators.LEXICOGRAPHIC,
Calcites.getStringComparatorForValueType(ColumnType.STRING));
+ Assert.assertEquals(StringComparators.NUMERIC,
Calcites.getStringComparatorForValueType(ColumnType.LONG));
+ Assert.assertEquals(StringComparators.NUMERIC,
Calcites.getStringComparatorForValueType(ColumnType.FLOAT));
+ Assert.assertEquals(StringComparators.NUMERIC,
Calcites.getStringComparatorForValueType(ColumnType.DOUBLE));
+ Assert.assertEquals(StringComparators.NATURAL,
Calcites.getStringComparatorForValueType(ColumnType.STRING_ARRAY));
+ Assert.assertEquals(StringComparators.NATURAL,
Calcites.getStringComparatorForValueType(ColumnType.LONG_ARRAY));
+ Assert.assertEquals(StringComparators.NATURAL,
Calcites.getStringComparatorForValueType(ColumnType.DOUBLE_ARRAY));
+ Assert.assertEquals(StringComparators.NATURAL,
Calcites.getStringComparatorForValueType(ColumnType.NESTED_DATA));
+ Assert.assertEquals(StringComparators.NATURAL,
Calcites.getStringComparatorForValueType(ColumnType.UNKNOWN_COMPLEX));
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]