This is an automated email from the ASF dual-hosted git repository.
Jackie-Jiang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git
The following commit(s) were added to refs/heads/master by this push:
new 902d3864bed Fix wrong results from MinMax selection order-by combine
under null handling (#19724)
902d3864bed is described below
commit 902d3864bed591d7820c3409f2ab03dca02968c0
Author: rohit <[email protected]>
AuthorDate: Wed Oct 7 03:12:52 2026 +0530
Fix wrong results from MinMax selection order-by combine under null
handling (#19724)
---
...xValueBasedSelectionOrderByCombineOperator.java | 49 +++++--
.../combine/SelectionCombineOperatorTest.java | 148 ++++++++++++++++++++-
.../pinot/queries/NullQueriesFluentTest.java | 137 +++++++++++++++++++
3 files changed, 321 insertions(+), 13 deletions(-)
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/operator/combine/MinMaxValueBasedSelectionOrderByCombineOperator.java
b/pinot-core/src/main/java/org/apache/pinot/core/operator/combine/MinMaxValueBasedSelectionOrderByCombineOperator.java
index 86ae288aca1..b349ed7ef6e 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/operator/combine/MinMaxValueBasedSelectionOrderByCombineOperator.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/operator/combine/MinMaxValueBasedSelectionOrderByCombineOperator.java
@@ -35,6 +35,7 @@ import
org.apache.pinot.core.operator.blocks.results.MetadataResultsBlock;
import org.apache.pinot.core.operator.blocks.results.SelectionResultsBlock;
import org.apache.pinot.core.query.request.context.QueryContext;
import org.apache.pinot.core.query.selection.SelectionOperatorUtils;
+import org.apache.pinot.segment.spi.datasource.DataSource;
import org.apache.pinot.segment.spi.datasource.DataSourceMetadata;
import org.apache.pinot.spi.data.Schema;
import org.apache.pinot.spi.exception.QueryErrorCode;
@@ -51,6 +52,14 @@ import org.slf4j.LoggerFactory;
/// - 1. Sort all the segments by the column min/max value
/// - 2. Keep processing segments until we get enough documents to fulfill the
LIMIT and OFFSET requirement
/// - 3. Skip processing the segments that cannot add values to the final
result
+///
+/// With null handling enabled, the column min/max value cannot bound the null
rows: a null is stored as the column's
+/// default null value, and the metadata has no null semantics. Null rows are
handled as follows:
+/// - When nulls sort first (the default for descending order, or explicit
`NULLS FIRST`), a segment that may hold null
+/// rows (it has a null value vector) is treated like a segment without
min/max value: it is processed before the
+/// other segments and never skipped. Segments without a null value vector
are skipped as usual.
+/// - When nulls sort last, null rows cannot beat a non-null boundary value,
so segment skipping applies as usual.
+/// - A null row value is never used as the boundary value; the previous
boundary value is kept instead.
@SuppressWarnings({"rawtypes", "unchecked"})
public class MinMaxValueBasedSelectionOrderByCombineOperator
extends BaseSingleBlockCombineOperator<SelectionResultsBlock> {
@@ -81,10 +90,13 @@ public class MinMaxValueBasedSelectionOrderByCombineOperator
OrderByExpressionContext firstOrderByExpression =
orderByExpressions.get(0);
assert firstOrderByExpression.getExpression().getType() ==
ExpressionContext.Type.IDENTIFIER;
String firstOrderByColumn =
firstOrderByExpression.getExpression().getIdentifier();
+ // isNullsLast() is false for DESC even when null handling is disabled, so
the null handling check is required
+ boolean nullsRankFirst = queryContext.isNullHandlingEnabled() &&
!firstOrderByExpression.isNullsLast();
_minMaxValueContexts = new ArrayList<>(_numOperators);
for (Operator<BaseResultsBlock> operator : _operators) {
- _minMaxValueContexts.add(new MinMaxValueContext(operator,
firstOrderByColumn, queryContext.getSchema()));
+ _minMaxValueContexts.add(
+ new MinMaxValueContext(operator, firstOrderByColumn,
queryContext.getSchema(), nullsRankFirst));
}
if (firstOrderByExpression.isAsc()) {
// For ascending order, sort on column min value in ascending order
@@ -210,9 +222,11 @@ public class
MinMaxValueBasedSelectionOrderByCombineOperator
List<Object[]> rows = resultsBlock.getRows();
assert rows != null;
int numRows = rows.size();
- if (numRows >= _numRowsToKeep) {
- // Segment result has enough rows, update the boundary value
- Comparable segmentBoundaryValue = (Comparable) rows.get(numRows -
1)[0];
+ // Update the boundary value if the segment result has enough rows
+ // NOTE: A null row value cannot be compared with the segment min/max
value, so it is never used as the boundary
+ // value. The previous boundary value is kept, which is still
valid.
+ Comparable segmentBoundaryValue = numRows >= _numRowsToKeep ?
(Comparable) rows.get(numRows - 1)[0] : null;
+ if (segmentBoundaryValue != null) {
if (boundaryValue == null) {
boundaryValue = segmentBoundaryValue;
} else {
@@ -273,9 +287,14 @@ public class
MinMaxValueBasedSelectionOrderByCombineOperator
numBlocksMerged++;
// Update the boundary value if enough rows are collected
+ // NOTE: A null row value is never used as the boundary value; the
previous one is kept. A null global boundary
+ // would only fall back to the thread boundary, so this preserves
segment skipping across threads.
List<Object[]> rows = mergedBlock.getRows();
if (rows.size() == _numRowsToKeep) {
- _globalBoundaryValue.set((Comparable) rows.get(_numRowsToKeep - 1)[0]);
+ Comparable boundaryValue = (Comparable) rows.get(_numRowsToKeep -
1)[0];
+ if (boundaryValue != null) {
+ _globalBoundaryValue.set(boundaryValue);
+ }
}
}
return mergedBlock;
@@ -303,12 +322,22 @@ public class
MinMaxValueBasedSelectionOrderByCombineOperator
final Comparable _minValue;
final Comparable _maxValue;
- MinMaxValueContext(Operator<BaseResultsBlock> operator, String column,
Schema schema) {
+ MinMaxValueContext(Operator<BaseResultsBlock> operator, String column,
Schema schema,
+ boolean nullsRankFirst) {
_operator = operator;
- DataSourceMetadata dataSourceMetadata =
- operator.getIndexSegment().getDataSource(column,
schema).getDataSourceMetadata();
- _minValue = dataSourceMetadata.getMinValue();
- _maxValue = dataSourceMetadata.getMaxValue();
+ DataSource dataSource = operator.getIndexSegment().getDataSource(column,
schema);
+ // When nulls sort first, min/max value cannot bound a segment that may
hold null rows. Leave them unset so that
+ // the segment is processed first and never skipped.
+ // NOTE: Immutable segments only have a null value vector when they
contain nulls. Mutable segments always have
+ // one when null handling is enabled for the column, so they are
conservatively never skipped.
+ if (nullsRankFirst && dataSource.getNullValueVector() != null) {
+ _minValue = null;
+ _maxValue = null;
+ } else {
+ DataSourceMetadata dataSourceMetadata =
dataSource.getDataSourceMetadata();
+ _minValue = dataSourceMetadata.getMinValue();
+ _maxValue = dataSourceMetadata.getMaxValue();
+ }
}
}
}
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/operator/combine/SelectionCombineOperatorTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/operator/combine/SelectionCombineOperatorTest.java
index 845b7a283d7..e5aa65bc7e4 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/operator/combine/SelectionCombineOperatorTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/operator/combine/SelectionCombineOperatorTest.java
@@ -21,7 +21,9 @@ package org.apache.pinot.core.operator.combine;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.apache.commons.io.FileUtils;
@@ -35,6 +37,8 @@ import
org.apache.pinot.core.query.request.context.QueryContext;
import
org.apache.pinot.core.query.request.context.utils.QueryContextConverterUtils;
import org.apache.pinot.core.util.QueryMultiThreadingUtils;
import
org.apache.pinot.segment.local.indexsegment.immutable.ImmutableSegmentLoader;
+import org.apache.pinot.segment.local.indexsegment.mutable.MutableSegmentImpl;
+import
org.apache.pinot.segment.local.indexsegment.mutable.MutableSegmentImplTestUtils;
import
org.apache.pinot.segment.local.segment.creator.impl.SegmentIndexCreationDriverImpl;
import org.apache.pinot.segment.local.segment.readers.GenericRowRecordReader;
import org.apache.pinot.segment.spi.IndexSegment;
@@ -58,7 +62,8 @@ import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
-/// Test for [SelectionOnlyCombineOperator] and
[SelectionOrderByCombineOperator].
+/// Test for [SelectionOnlyCombineOperator], [SelectionOrderByCombineOperator]
and
+/// [MinMaxValueBasedSelectionOrderByCombineOperator].
public class SelectionCombineOperatorTest {
private static final File TEMP_DIR = new File(FileUtils.getTempDirectory(),
"SelectionCombineOperatorTest");
private static final String RAW_TABLE_NAME = "testTable";
@@ -77,7 +82,20 @@ public class SelectionCombineOperatorTest {
private static final PlanMaker PLAN_MAKER = new InstancePlanMakerImplV2();
private static final ExecutorService EXECUTOR =
Executors.newCachedThreadPool();
+ // Segments for the min/max value based segment skipping tests under null
handling
+ private static final TableConfig NULL_HANDLING_TABLE_CONFIG =
+ new
TableConfigBuilder(TableType.OFFLINE).setTableName(RAW_TABLE_NAME).setNullHandlingEnabled(true).build();
+ private static final Schema COLUMN_BASED_NULL_HANDLING_SCHEMA =
+ new Schema.SchemaBuilder().setEnableColumnBasedNullHandling(true)
+ .addDimensionField(INT_COLUMN, FieldSpec.DataType.INT, fieldSpec ->
fieldSpec.setNullable(false)).build();
+ private static final String NULL_HANDLING_OPTIONS = "SET
enableNullHandling=true; ";
+
private List<IndexSegment> _indexSegments;
+ private IndexSegment _highSegment;
+ private IndexSegment _lowSegment;
+ private IndexSegment _lowSegmentWithNulls;
+ private IndexSegment _highNonNullableSegment;
+ private IndexSegment _lowNonNullableSegment;
@BeforeClass
public void setUp()
@@ -87,6 +105,34 @@ public class SelectionCombineOperatorTest {
for (int i = 0; i < NUM_SEGMENTS; i++) {
_indexSegments.add(createOfflineSegment(i));
}
+ _highSegment = createSegment("highSegment", NULL_HANDLING_TABLE_CONFIG,
SCHEMA, 100, 101, 102);
+ _lowSegment = createSegment("lowSegment", NULL_HANDLING_TABLE_CONFIG,
SCHEMA, 5, 6, 7);
+ _lowSegmentWithNulls = createSegment("lowSegmentWithNulls",
NULL_HANDLING_TABLE_CONFIG, SCHEMA, 5, null, null);
+ _highNonNullableSegment =
+ createSegment("highNonNullableSegment", TABLE_CONFIG,
COLUMN_BASED_NULL_HANDLING_SCHEMA, 100, 101, 102);
+ _lowNonNullableSegment =
+ createSegment("lowNonNullableSegment", TABLE_CONFIG,
COLUMN_BASED_NULL_HANDLING_SCHEMA, 5, 6, 7);
+ }
+
+ private IndexSegment createSegment(String segmentName, TableConfig
tableConfig, Schema schema, Integer... values)
+ throws Exception {
+ List<GenericRow> records = new ArrayList<>(values.length);
+ for (Integer value : values) {
+ GenericRow record = new GenericRow();
+ record.putValue(INT_COLUMN, value);
+ records.add(record);
+ }
+
+ SegmentGeneratorConfig segmentGeneratorConfig = new
SegmentGeneratorConfig(tableConfig, schema);
+ segmentGeneratorConfig.setTableName(RAW_TABLE_NAME);
+ segmentGeneratorConfig.setSegmentName(segmentName);
+ segmentGeneratorConfig.setOutDir(TEMP_DIR.getPath());
+
+ SegmentIndexCreationDriverImpl driver = new
SegmentIndexCreationDriverImpl();
+ driver.init(segmentGeneratorConfig, new GenericRowRecordReader(records));
+ driver.build();
+
+ return ImmutableSegmentLoader.load(new File(TEMP_DIR, segmentName),
ReadMode.mmap);
}
private IndexSegment createOfflineSegment(int index)
@@ -304,10 +350,102 @@ public class SelectionCombineOperatorTest {
assertEquals(combineResult.getNumTotalDocs(), NUM_SEGMENTS *
NUM_RECORDS_PER_SEGMENT);
}
+ /// Under null handling, a segment without nulls is still skipped when its
max cannot beat the boundary, even though
+ /// nulls sort first under `DESC`.
+ @Test
+ public void
selectionOrderByMinMaxSkipsSegmentWithoutNullsUnderNullHandling() {
+ SelectionResultsBlock combineResult =
+ getSingleThreadCombineResult(NULL_HANDLING_OPTIONS + "SELECT * FROM
testTable ORDER BY intColumn DESC LIMIT 3",
+ List.of(_highSegment, _lowSegment));
+ assertEquals(getIntColumnValues(combineResult), Arrays.asList(102, 101,
100));
+ assertEquals(combineResult.getNumSegmentsProcessed(), 2);
+ assertEquals(combineResult.getNumSegmentsMatched(), 1);
+ }
+
+ /// When nulls sort last, a segment with nulls is still skipped when its max
cannot beat the boundary: its nulls rank
+ /// below any non-null boundary.
+ @Test
+ public void selectionOrderByMinMaxSkipsSegmentWithNullsWhenNullsSortLast() {
+ SelectionResultsBlock combineResult = getSingleThreadCombineResult(
+ NULL_HANDLING_OPTIONS + "SELECT * FROM testTable ORDER BY intColumn
DESC NULLS LAST LIMIT 3",
+ List.of(_highSegment, _lowSegmentWithNulls));
+ assertEquals(getIntColumnValues(combineResult), Arrays.asList(102, 101,
100));
+ assertEquals(combineResult.getNumSegmentsProcessed(), 2);
+ assertEquals(combineResult.getNumSegmentsMatched(), 1);
+ }
+
+ /// When nulls sort first, a segment with nulls must not be skipped: the
segment min/max does not account for its
+ /// null rows.
+ @Test
+ public void
selectionOrderByMinMaxProcessesSegmentWithNullsWhenNullsSortFirst() {
+ SelectionResultsBlock combineResult =
+ getSingleThreadCombineResult(NULL_HANDLING_OPTIONS + "SELECT * FROM
testTable ORDER BY intColumn DESC LIMIT 3",
+ List.of(_highSegment, _lowSegmentWithNulls));
+ assertEquals(getIntColumnValues(combineResult), Arrays.asList(null, null,
102));
+ assertEquals(combineResult.getNumSegmentsProcessed(), 2);
+ assertEquals(combineResult.getNumSegmentsMatched(), 2);
+ }
+
+ /// A consuming segment always has a null value vector for a nullable
column, so it is never skipped when nulls sort
+ /// first, even if it currently holds no null rows.
+ @Test
+ public void selectionOrderByMinMaxProcessesMutableSegmentWhenNullsSortFirst()
+ throws IOException {
+ MutableSegmentImpl mutableSegment =
+ MutableSegmentImplTestUtils.createMutableSegmentImpl(SCHEMA, Set.of(),
Set.of(), Set.of(), false, true);
+ try {
+ for (Integer value : new Integer[]{5, 6, 7}) {
+ GenericRow row = new GenericRow();
+ row.putValue(INT_COLUMN, value);
+ mutableSegment.index(row, null);
+ }
+ SelectionResultsBlock combineResult = getSingleThreadCombineResult(
+ NULL_HANDLING_OPTIONS + "SELECT * FROM testTable ORDER BY intColumn
DESC LIMIT 3",
+ List.of(_highSegment, mutableSegment));
+ assertEquals(getIntColumnValues(combineResult), Arrays.asList(102, 101,
100));
+ assertEquals(combineResult.getNumSegmentsProcessed(), 2);
+ assertEquals(combineResult.getNumSegmentsMatched(), 2);
+ } finally {
+ mutableSegment.destroy();
+ }
+ }
+
+ /// With column-based null handling, a non-nullable column has no nulls, so
segment skipping still applies.
+ @Test
+ public void selectionOrderByMinMaxSkipsSegmentForNonNullableColumn() {
+ SelectionResultsBlock combineResult =
+ getSingleThreadCombineResult(NULL_HANDLING_OPTIONS + "SELECT * FROM
testTable ORDER BY intColumn DESC LIMIT 3",
+ List.of(_highNonNullableSegment, _lowNonNullableSegment));
+ assertEquals(getIntColumnValues(combineResult), Arrays.asList(102, 101,
100));
+ assertEquals(combineResult.getNumSegmentsProcessed(), 2);
+ assertEquals(combineResult.getNumSegmentsMatched(), 1);
+ }
+
+ private static List<Integer> getIntColumnValues(SelectionResultsBlock
combineResult) {
+ List<Integer> values = new ArrayList<>();
+ for (Object[] row : combineResult.getRows()) {
+ values.add((Integer) row[0]);
+ }
+ return values;
+ }
+
private SelectionResultsBlock getCombineResult(@Language("sql") String
query) {
+ return getCombineResult(QueryContextConverterUtils.getQueryContext(query),
_indexSegments);
+ }
+
+ /// Runs the query with a single thread, which processes the segments in
min/max order and makes the segment skipping
+ /// deterministic. `SET maxExecutionThreads` would not apply here: the
server plan maker is what copies it into the
+ /// query context.
+ private SelectionResultsBlock getSingleThreadCombineResult(@Language("sql")
String query,
+ List<IndexSegment> indexSegments) {
QueryContext queryContext =
QueryContextConverterUtils.getQueryContext(query);
- List<PlanNode> planNodes = new ArrayList<>(NUM_SEGMENTS);
- for (IndexSegment indexSegment : _indexSegments) {
+ queryContext.setMaxExecutionThreads(1);
+ return getCombineResult(queryContext, indexSegments);
+ }
+
+ private SelectionResultsBlock getCombineResult(QueryContext queryContext,
List<IndexSegment> indexSegments) {
+ List<PlanNode> planNodes = new ArrayList<>(indexSegments.size());
+ for (IndexSegment indexSegment : indexSegments) {
planNodes.add(PLAN_MAKER.makeSegmentPlanNode(new
SegmentContext(indexSegment), queryContext));
}
queryContext.setEndTimeMs(System.currentTimeMillis() +
Server.DEFAULT_QUERY_EXECUTOR_TIMEOUT_MS);
@@ -321,6 +459,10 @@ public class SelectionCombineOperatorTest {
for (IndexSegment indexSegment : _indexSegments) {
indexSegment.destroy();
}
+ for (IndexSegment indexSegment : List.of(_highSegment, _lowSegment,
_lowSegmentWithNulls, _highNonNullableSegment,
+ _lowNonNullableSegment)) {
+ indexSegment.destroy();
+ }
FileUtils.deleteDirectory(TEMP_DIR);
}
}
diff --git
a/pinot-core/src/test/java/org/apache/pinot/queries/NullQueriesFluentTest.java
b/pinot-core/src/test/java/org/apache/pinot/queries/NullQueriesFluentTest.java
index 85e80ab92db..eee751662bd 100644
---
a/pinot-core/src/test/java/org/apache/pinot/queries/NullQueriesFluentTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/queries/NullQueriesFluentTest.java
@@ -22,6 +22,7 @@ import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
+import java.util.Map;
import org.apache.commons.io.FileUtils;
import org.apache.pinot.spi.config.table.FieldConfig;
import org.apache.pinot.spi.config.table.TableConfig;
@@ -49,6 +50,39 @@ public class NullQueriesFluentTest {
.addSingleValueDimension("stringCol", FieldSpec.DataType.STRING)
.build();
+ // Selection ORDER BY on an identifier with a LIMIT plans
MinMaxValueBasedSelectionOrderByCombineOperator, which skips
+ // segments whose min/max cannot beat the current boundary. The tests below
put two segments on one instance so the
+ // skip can happen. With a single instance, BaseQueriesTest serves it as two
identical servers, so the broker merges
+ // two copies of the combine result before applying the LIMIT. The comments
on each assertion give the combine
+ // (per-server) result the expectation is derived from.
+ private static final TableConfig NULL_HANDLING_TABLE_CONFIG = new
TableConfigBuilder(TableType.OFFLINE)
+ .setTableName("testTable")
+ .setNullHandlingEnabled(true)
+ .build();
+
+ private static final TableConfig PLAIN_TABLE_CONFIG = new
TableConfigBuilder(TableType.OFFLINE)
+ .setTableName("testTable")
+ .build();
+
+ private static final Schema INT_SCHEMA = new Schema.SchemaBuilder()
+ .setSchemaName("testTable")
+ .addSingleValueDimension("intCol", FieldSpec.DataType.INT)
+ .build();
+
+ private static final Schema INT_CUSTOM_DEFAULT_NULL_SCHEMA = new
Schema.SchemaBuilder()
+ .setSchemaName("testTable")
+ .addSingleValueDimension("intCol", FieldSpec.DataType.INT, 1000)
+ .build();
+
+ private static final Schema INT_COLUMN_BASED_SCHEMA = new
Schema.SchemaBuilder()
+ .setSchemaName("testTable")
+ .setEnableColumnBasedNullHandling(true)
+ .addDimensionField("intCol", FieldSpec.DataType.INT, fieldSpec ->
fieldSpec.setNullable(true))
+ .build();
+
+ // One thread processes the segments in min/max order, which makes the
segment skip deterministic
+ private static final Map<String, String> SINGLE_THREAD =
Map.of("maxExecutionThreads", "1");
+
private File _baseDir;
@BeforeClass
@@ -109,4 +143,107 @@ public class NullQueriesFluentTest {
new Object[]{"2025-09-23 17:38:00.0"}
);
}
+
+ /// The segment with nulls has max 5, below the boundary 100 set by the
first segment, but its nulls sort first.
+ @Test
+ public void testMinMaxCombineOrderByDescKeepsNullsFirst() {
+ givenHighAndLowWithNullsSegments(INT_SCHEMA, NULL_HANDLING_TABLE_CONFIG)
+ // Combine result: [null, null, 104, 103, 102]; the two copies hold 4
nulls, so LIMIT 5 keeps one 104
+ .whenQuery("select intCol from testTable order by intCol desc limit 5")
+ .thenResultIs(new Object[]{null}, new Object[]{null}, new
Object[]{null}, new Object[]{null},
+ new Object[]{104})
+ // Combine result: [null, null, 104, 103, 102]
+ .whenQuery("select intCol from testTable order by intCol desc nulls
first limit 5")
+ .thenResultIs(new Object[]{null}, new Object[]{null}, new
Object[]{null}, new Object[]{null},
+ new Object[]{104});
+ }
+
+ @Test
+ public void
testMinMaxCombineOrderByDescKeepsNullsFirstColumnBasedNullHandling() {
+ givenHighAndLowWithNullsSegments(INT_COLUMN_BASED_SCHEMA,
PLAIN_TABLE_CONFIG)
+ // Combine result: [null, null, 104, 103, 102]
+ .whenQuery("select intCol from testTable order by intCol desc limit 5")
+ .thenResultIs(new Object[]{null}, new Object[]{null}, new
Object[]{null}, new Object[]{null},
+ new Object[]{104});
+ }
+
+ /// Orders where nulls sort last are not affected by the segment skip.
+ @Test
+ public void testMinMaxCombineNullsLast() {
+ givenHighAndLowWithNullsSegments(INT_SCHEMA, NULL_HANDLING_TABLE_CONFIG)
+ // Combine result: [104, 103, 102]
+ .whenQuery("select intCol from testTable order by intCol desc nulls
last limit 3")
+ .thenResultIs(new Object[]{104}, new Object[]{104}, new Object[]{103})
+ // Combine result: [5, 100, 101]
+ .whenQuery("select intCol from testTable order by intCol limit 3")
+ .thenResultIs(new Object[]{5}, new Object[]{5}, new Object[]{100})
+ // Combine result: [5, 100, 101]
+ .whenQuery("select intCol from testTable order by intCol asc nulls
last limit 3")
+ .thenResultIs(new Object[]{5}, new Object[]{5}, new Object[]{100});
+ }
+
+ /// The stored default null value (1000) gives the segment with nulls min
500, above the boundary 3 set by the first
+ /// segment, but its nulls sort first.
+ @Test
+ public void
testMinMaxCombineOrderByAscNullsFirstWithCustomDefaultNullValue() {
+ FluentQueryTest.withBaseDir(_baseDir)
+ .withExtraQueryOptions(SINGLE_THREAD)
+ .withNullHandling(true)
+ .givenTable(INT_CUSTOM_DEFAULT_NULL_SCHEMA, NULL_HANDLING_TABLE_CONFIG)
+ .onFirstInstance(
+ new Object[]{1},
+ new Object[]{2},
+ new Object[]{3}
+ ).andSegment(
+ new Object[]{500},
+ new Object[]{null},
+ new Object[]{null}
+ )
+ // Combine result: [null, null, 1]
+ .whenQuery("select intCol from testTable order by intCol asc nulls
first limit 3")
+ .thenResultIs(new Object[]{null}, new Object[]{null}, new
Object[]{null});
+ }
+
+ /// The segment with nulls (max 95) is not skipped by the boundary 80, and
the last of its top 3 rows is null, which
+ /// must not be used as a boundary value.
+ @Test
+ public void testMinMaxCombineNullBoundaryValue() {
+ FluentQueryTest.withBaseDir(_baseDir)
+ .withExtraQueryOptions(SINGLE_THREAD)
+ .withNullHandling(true)
+ .givenTable(INT_SCHEMA, NULL_HANDLING_TABLE_CONFIG)
+ .onFirstInstance(
+ new Object[]{100},
+ new Object[]{90},
+ new Object[]{80}
+ ).andSegment(
+ new Object[]{95},
+ new Object[]{null},
+ new Object[]{null}
+ )
+ // Combine result: [100, 95, 90]
+ .whenQuery("select intCol from testTable order by intCol desc nulls
last limit 3")
+ .thenResultIs(new Object[]{100}, new Object[]{100}, new Object[]{95})
+ // Combine result: [null, null, 100]
+ .whenQuery("select intCol from testTable order by intCol desc limit 3")
+ .thenResultIs(new Object[]{null}, new Object[]{null}, new
Object[]{null});
+ }
+
+ private FluentQueryTest.OnFirstInstance
givenHighAndLowWithNullsSegments(Schema schema, TableConfig tableConfig) {
+ return FluentQueryTest.withBaseDir(_baseDir)
+ .withExtraQueryOptions(SINGLE_THREAD)
+ .withNullHandling(true)
+ .givenTable(schema, tableConfig)
+ .onFirstInstance(
+ new Object[]{100},
+ new Object[]{101},
+ new Object[]{102},
+ new Object[]{103},
+ new Object[]{104}
+ ).andSegment(
+ new Object[]{5},
+ new Object[]{null},
+ new Object[]{null}
+ );
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]