add NumericRangeIndex interface and BoundFilter support (#12830)

add NumericRangeIndex interface and BoundFilter support
changes:
* NumericRangeIndex interface, like LexicographicalRangeIndex but for numbers
* BoundFilter now uses NumericRangeIndex if comparator is numeric and there is no extractionFn
* NestedFieldLiteralColumnIndexSupplier.java now supports supplying NumericRangeIndex for single typed numeric nested literal columns

* better faster stronger and (ever so slightly) more understandable

* more tests, fix bug

* fix style
This commit is contained in:
Clint Wylie 2022-07-29 18:58:49 -07:00 committed by GitHub
parent d52abe7b38
commit 189e8b9d18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 1754 additions and 228 deletions

View File

@ -165,7 +165,13 @@ public class SqlNestedDataBenchmark
"SELECT JSON_VALUE(nested, '$.nesteder.long2' RETURNING BIGINT) FROM foo WHERE JSON_VALUE(nested, '$.nesteder.long2' RETURNING BIGINT) IN (1, 19, 21, 23, 25, 26, 46)",
// 24, 25
"SELECT long2 FROM foo WHERE long2 IN (1, 19, 21, 23, 25, 26, 46) GROUP BY 1",
"SELECT JSON_VALUE(nested, '$.nesteder.long2' RETURNING BIGINT) FROM foo WHERE JSON_VALUE(nested, '$.nesteder.long2' RETURNING BIGINT) IN (1, 19, 21, 23, 25, 26, 46) GROUP BY 1"
"SELECT JSON_VALUE(nested, '$.nesteder.long2' RETURNING BIGINT) FROM foo WHERE JSON_VALUE(nested, '$.nesteder.long2' RETURNING BIGINT) IN (1, 19, 21, 23, 25, 26, 46) GROUP BY 1",
// 26, 27
"SELECT SUM(long1) FROM foo WHERE double3 < 1005.0 AND double3 > 1000.0",
"SELECT SUM(JSON_VALUE(nested, '$.long1' RETURNING BIGINT)) FROM foo WHERE JSON_VALUE(nested, '$.nesteder.double3' RETURNING DOUBLE) < 1005.0 AND JSON_VALUE(nested, '$.nesteder.double3' RETURNING DOUBLE) > 1000.0",
// 28, 29
"SELECT SUM(long1) FROM foo WHERE double3 < 2000.0 AND double3 > 1000.0",
"SELECT SUM(JSON_VALUE(nested, '$.long1' RETURNING BIGINT)) FROM foo WHERE JSON_VALUE(nested, '$.nesteder.double3' RETURNING DOUBLE) < 2000.0 AND JSON_VALUE(nested, '$.nesteder.double3' RETURNING DOUBLE) > 1000.0"
);
@Param({"5000000"})
@ -203,7 +209,11 @@ public class SqlNestedDataBenchmark
"22",
"23",
"24",
"25"
"25",
"26",
"27",
"28",
"29"
})
private String query;

View File

@ -523,7 +523,7 @@ public class InDimFilter extends AbstractOptimizableDimFilter implements Filter
private final Supplier<DruidFloatPredicate> floatPredicateSupplier;
private final Supplier<DruidDoublePredicate> doublePredicateSupplier;
InFilterDruidPredicateFactory(
public InFilterDruidPredicateFactory(
final ExtractionFn extractionFn,
final ValuesSet values
)

View File

@ -0,0 +1,42 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.druid.segment.column;
import javax.annotation.Nullable;
/**
* An optimized column value {@link BitmapColumnIndex} provider for specialized processing of numeric value ranges.
* This index does not match null values, union the results of this index with {@link NullValueIndex} if null values
* should be considered part of the value range.
*/
public interface NumericRangeIndex
{
/**
* Get a {@link BitmapColumnIndex} corresponding to the values supplied in the specified range. If supplied starting
* value is null, the range will begin at the first non-null value in the underlying value dictionary. If the end
* value is null, the range will extend to the last value in the underlying value dictionary.
*/
BitmapColumnIndex forRange(
@Nullable Number startValue,
boolean startStrict,
@Nullable Number endValue,
boolean endStrict
);
}

View File

@ -48,6 +48,7 @@ import org.apache.druid.segment.column.ColumnIndexCapabilities;
import org.apache.druid.segment.column.ColumnIndexSupplier;
import org.apache.druid.segment.column.LexicographicalRangeIndex;
import org.apache.druid.segment.column.NullValueIndex;
import org.apache.druid.segment.column.NumericRangeIndex;
import org.apache.druid.segment.vector.VectorColumnSelectorFactory;
import javax.annotation.Nullable;
@ -75,16 +76,13 @@ public class BoundFilter implements Filter
if (!Filters.checkFilterTuningUseIndex(boundDimFilter.getDimension(), selector, filterTuning)) {
return null;
}
if (supportShortCircuit()) {
if (supportStringShortCircuit()) {
final ColumnIndexSupplier indexSupplier = selector.getIndexSupplier(boundDimFilter.getDimension());
if (indexSupplier == null) {
return Filters.makeNullIndex(doesMatchNull(), selector);
}
final LexicographicalRangeIndex rangeIndex = indexSupplier.as(LexicographicalRangeIndex.class);
if (rangeIndex == null) {
// column
return null;
}
if (rangeIndex != null) {
final BitmapColumnIndex rangeBitmaps = rangeIndex.forRange(
boundDimFilter.getLower(),
boundDimFilter.isLowerStrict(),
@ -95,6 +93,43 @@ public class BoundFilter implements Filter
if (boundDimFilter.hasLowerBound() && !NullHandling.isNullOrEquivalent(boundDimFilter.getLower())) {
return rangeBitmaps;
} else {
return wrapRangeIndexWithNullValueIndex(indexSupplier, rangeBitmaps);
}
}
}
if (supportNumericShortCircuit()) {
final ColumnIndexSupplier indexSupplier = selector.getIndexSupplier(boundDimFilter.getDimension());
if (indexSupplier == null) {
return Filters.makeNullIndex(doesMatchNull(), selector);
}
final NumericRangeIndex rangeIndex = indexSupplier.as(NumericRangeIndex.class);
if (rangeIndex != null) {
final Number lower = boundDimFilter.hasLowerBound() ? Double.parseDouble(boundDimFilter.getLower()) : null;
final Number upper = boundDimFilter.hasUpperBound() ? Double.parseDouble(boundDimFilter.getUpper()) : null;
final BitmapColumnIndex rangeBitmaps = rangeIndex.forRange(
lower,
boundDimFilter.isLowerStrict(),
upper,
boundDimFilter.isUpperStrict()
);
// preserve sad backwards compatible behavior where bound filter matches 'null' if the lower bound is not set
if (boundDimFilter.hasLowerBound() && !NullHandling.isNullOrEquivalent(boundDimFilter.getLower())) {
return rangeBitmaps;
} else {
return wrapRangeIndexWithNullValueIndex(indexSupplier, rangeBitmaps);
}
}
}
// fall back to predicate based index if it is available
return Filters.makePredicateIndex(boundDimFilter.getDimension(), selector, getPredicateFactory());
}
@Nullable
private BitmapColumnIndex wrapRangeIndexWithNullValueIndex(
ColumnIndexSupplier indexSupplier,
BitmapColumnIndex rangeIndex
)
{
final NullValueIndex nulls = indexSupplier.as(NullValueIndex.class);
if (nulls == null) {
return null;
@ -105,7 +140,7 @@ public class BoundFilter implements Filter
@Override
public ColumnIndexCapabilities getIndexCapabilities()
{
return rangeBitmaps.getIndexCapabilities().merge(nullBitmap.getIndexCapabilities());
return rangeIndex.getIndexCapabilities().merge(nullBitmap.getIndexCapabilities());
}
@Override
@ -113,7 +148,7 @@ public class BoundFilter implements Filter
{
return Math.min(
1.0,
rangeBitmaps.estimateSelectivity(totalRows) + nullBitmap.estimateSelectivity(totalRows)
rangeIndex.estimateSelectivity(totalRows) + nullBitmap.estimateSelectivity(totalRows)
);
}
@ -122,24 +157,26 @@ public class BoundFilter implements Filter
{
return bitmapResultFactory.union(
ImmutableList.of(
rangeBitmaps.computeBitmapResult(bitmapResultFactory),
rangeIndex.computeBitmapResult(bitmapResultFactory),
nullBitmap.computeBitmapResult(bitmapResultFactory)
)
);
}
};
}
} else {
return Filters.makePredicateIndex(boundDimFilter.getDimension(), selector, getPredicateFactory());
}
}
private boolean supportShortCircuit()
private boolean supportStringShortCircuit()
{
// Optimization for lexicographic bounds with no extractionFn => binary search through the index
return boundDimFilter.getOrdering().equals(StringComparators.LEXICOGRAPHIC) && extractionFn == null;
}
private boolean supportNumericShortCircuit()
{
// Optimization for numeric bounds with no extractionFn => binary search through the index
return boundDimFilter.getOrdering().equals(StringComparators.NUMERIC) && extractionFn == null;
}
@Override
public ValueMatcher makeMatcher(ColumnSelectorFactory factory)
{

View File

@ -35,23 +35,27 @@ import it.unimi.dsi.fastutil.longs.LongIterator;
import it.unimi.dsi.fastutil.longs.LongSet;
import org.apache.druid.collections.bitmap.BitmapFactory;
import org.apache.druid.collections.bitmap.ImmutableBitmap;
import org.apache.druid.common.config.NullHandling;
import org.apache.druid.common.guava.GuavaUtils;
import org.apache.druid.query.BitmapResultFactory;
import org.apache.druid.query.filter.DruidDoublePredicate;
import org.apache.druid.query.filter.DruidLongPredicate;
import org.apache.druid.query.filter.DruidPredicateFactory;
import org.apache.druid.segment.IntListUtils;
import org.apache.druid.segment.column.BitmapColumnIndex;
import org.apache.druid.segment.column.ColumnIndexSupplier;
import org.apache.druid.segment.column.ColumnType;
import org.apache.druid.segment.column.DruidPredicateIndex;
import org.apache.druid.segment.column.LexicographicalRangeIndex;
import org.apache.druid.segment.column.NullValueIndex;
import org.apache.druid.segment.column.NumericRangeIndex;
import org.apache.druid.segment.column.SimpleBitmapColumnIndex;
import org.apache.druid.segment.column.SimpleImmutableBitmapIndex;
import org.apache.druid.segment.column.SimpleImmutableBitmapIterableIndex;
import org.apache.druid.segment.column.StringValueSetIndex;
import org.apache.druid.segment.data.FixedIndexed;
import org.apache.druid.segment.data.GenericIndexed;
import org.apache.druid.segment.data.Indexed;
import javax.annotation.Nullable;
import java.util.Iterator;
@ -120,6 +124,8 @@ public class NestedFieldLiteralColumnIndexSupplier implements ColumnIndexSupplie
case LONG:
if (clazz.equals(StringValueSetIndex.class)) {
return (T) new NestedLongLiteralValueSetIndex();
} else if (clazz.equals(NumericRangeIndex.class)) {
return (T) new NestedLongLiteralNumericRangeIndex();
} else if (clazz.equals(DruidPredicateIndex.class)) {
return (T) new NestedLongLiteralPredicateIndex();
}
@ -127,6 +133,8 @@ public class NestedFieldLiteralColumnIndexSupplier implements ColumnIndexSupplie
case DOUBLE:
if (clazz.equals(StringValueSetIndex.class)) {
return (T) new NestedDoubleLiteralValueSetIndex();
} else if (clazz.equals(NumericRangeIndex.class)) {
return (T) new NestedDoubleLiteralNumericRangeIndex();
} else if (clazz.equals(DruidPredicateIndex.class)) {
return (T) new NestedDoubleLiteralPredicateIndex();
}
@ -153,47 +161,106 @@ public class NestedFieldLiteralColumnIndexSupplier implements ColumnIndexSupplie
return bitmap == null ? bitmapFactory.makeEmptyImmutableBitmap() : bitmap;
}
private IntIntPair getGlobalRange(
@Nullable String startValue,
/**
* Gets a value range from a global dictionary and maps it to a range on the local {@link #dictionary}.
* The starting index of the resulting range is inclusive, while the endpoint is exclusive [start, end)
*/
private <T> IntIntPair getLocalRangeFromDictionary(
@Nullable T startValue,
boolean startStrict,
@Nullable String endValue,
@Nullable T endValue,
boolean endStrict,
int rangeStart,
int rangeEnd,
GlobalIndexGetFunction getFn
Indexed<T> globalDictionary,
int adjust
)
{
int startIndex, endIndex;
int globalStartIndex, globalEndIndex;
int localStartIndex, localEndIndex;
if (startValue == null) {
startIndex = rangeStart;
globalStartIndex = adjust == 0 ? 1 : adjust; // global index 0 is always the null value
} else {
final int found = getFn.indexOf(startValue);
final int found = globalDictionary.indexOf(startValue);
if (found >= 0) {
startIndex = startStrict ? found + 1 : found;
globalStartIndex = adjust + (startStrict ? found + 1 : found);
} else {
startIndex = -(found + 1);
globalStartIndex = adjust + (-(found + 1));
}
}
// with starting global index settled, now lets find starting local index
int localFound = dictionary.indexOf(globalStartIndex);
if (localFound < 0) {
// the first valid global index is not within the local dictionary, so the insertion point is where we begin
localStartIndex = -(localFound + 1);
} else {
// valid global index in local dictionary, start here
localStartIndex = localFound;
}
if (endValue == null) {
endIndex = rangeEnd;
globalEndIndex = globalDictionary.size() + adjust;
} else {
final int found = getFn.indexOf(endValue);
final int found = globalDictionary.indexOf(endValue);
if (found >= 0) {
endIndex = endStrict ? found : found + 1;
globalEndIndex = adjust + (endStrict ? found : found + 1);
} else {
endIndex = -(found + 1);
globalEndIndex = adjust + (-(found + 1));
}
}
globalEndIndex = Math.max(globalStartIndex, globalEndIndex);
// end index is not inclusive, so we find the last value in the local dictionary that falls within the range
int localEndFound = dictionary.indexOf(globalEndIndex - 1);
if (localEndFound < 0) {
localEndIndex = -localEndFound;
} else {
// add 1 because the last valid global end value is in the local dictionary, and end index is exclusive
localEndIndex = localEndFound + 1;
}
endIndex = Math.max(startIndex, endIndex);
return new IntIntImmutablePair(startIndex, endIndex);
return new IntIntImmutablePair(localStartIndex, Math.min(dictionary.size(), localEndIndex));
}
@FunctionalInterface
interface GlobalIndexGetFunction
private <T> BitmapColumnIndex makeRangeIndex(
@Nullable T startValue,
boolean startStrict,
@Nullable T endValue,
boolean endStrict,
Indexed<T> globalDictionary,
int adjust
)
{
int indexOf(String value);
final IntIntPair localRange = getLocalRangeFromDictionary(
startValue,
startStrict,
endValue,
endStrict,
globalDictionary,
adjust
);
final int startIndex = localRange.leftInt();
final int endIndex = localRange.rightInt();
return new SimpleImmutableBitmapIterableIndex()
{
@Override
public Iterable<ImmutableBitmap> getBitmapIterable()
{
return () -> new Iterator<ImmutableBitmap>()
{
final IntIterator rangeIterator = IntListUtils.fromTo(startIndex, endIndex).iterator();
@Override
public boolean hasNext()
{
return rangeIterator.hasNext();
}
@Override
public ImmutableBitmap next()
{
return getBitmap(rangeIterator.nextInt());
}
};
}
};
}
private class NestedStringLiteralValueSetIndex implements StringValueSetIndex
@ -274,7 +341,6 @@ public class NestedFieldLiteralColumnIndexSupplier implements ColumnIndexSupplie
private class NestedStringLiteralLexicographicalRangeIndex implements LexicographicalRangeIndex
{
@Override
public BitmapColumnIndex forRange(
@Nullable String startValue,
@ -283,70 +349,14 @@ public class NestedFieldLiteralColumnIndexSupplier implements ColumnIndexSupplie
boolean endStrict
)
{
return new SimpleImmutableBitmapIterableIndex()
{
@Override
public Iterable<ImmutableBitmap> getBitmapIterable()
{
final IntIntPair range = getGlobalRange(
startValue,
return makeRangeIndex(
NullHandling.emptyToNullIfNeeded(startValue),
startStrict,
endValue,
NullHandling.emptyToNullIfNeeded(endValue),
endStrict,
0,
globalDictionary.size(),
globalDictionary::indexOf
globalDictionary,
0
);
final int start = range.leftInt(), end = range.rightInt();
// iterates over the range of values in the global dictionary, mapping to relevant range in the local
// dictionary, skipping duplicates
return () -> new Iterator<ImmutableBitmap>()
{
int currentGlobalIndex = start;
// initialize to -1 because findNext uses this field to check for duplicates, and could legitimately find
// 0 for the first candidate
@SuppressWarnings("UnusedAssignment")
int currentLocalIndex = -1;
{
currentLocalIndex = findNext();
}
private int findNext()
{
int candidateLocalIndex = Math.abs(dictionary.indexOf(currentGlobalIndex));
while (currentGlobalIndex < end && candidateLocalIndex == currentLocalIndex) {
currentGlobalIndex++;
candidateLocalIndex = Math.abs(dictionary.indexOf(currentGlobalIndex));
}
if (currentGlobalIndex < end) {
currentGlobalIndex++;
return candidateLocalIndex;
} else {
return -1;
}
}
@Override
public boolean hasNext()
{
return currentLocalIndex != -1;
}
@Override
public ImmutableBitmap next()
{
int cur = currentLocalIndex;
if (cur == -1) {
throw new NoSuchElementException();
}
currentLocalIndex = findNext();
return getBitmap(cur);
}
};
}
};
}
@Override
@ -363,67 +373,53 @@ public class NestedFieldLiteralColumnIndexSupplier implements ColumnIndexSupplie
@Override
public Iterable<ImmutableBitmap> getBitmapIterable()
{
final IntIntPair stringsRange = getGlobalRange(
final IntIntPair range = getLocalRangeFromDictionary(
startValue,
startStrict,
endValue,
endStrict,
0,
globalDictionary.size(),
globalDictionary::indexOf
globalDictionary,
0
);
// iterates over the range of values in the global dictionary, mapping to relevant range in the local
// dictionary, skipping duplicates
final int start = range.leftInt(), end = range.rightInt();
return () -> new Iterator<ImmutableBitmap>()
{
int currentGlobalIndex = stringsRange.leftInt();
final int end = stringsRange.rightInt();
// initialize to -1 because findNext uses this field to check for duplicates, and could legitimately find
// 0 for the first candidate
@SuppressWarnings("UnusedAssignment")
int currentLocalIndex = -1;
int currIndex = start;
int found;
{
currentLocalIndex = findNext();
found = findNext();
}
private int findNext()
{
int candidateLocalIndex = Math.abs(dictionary.indexOf(currentGlobalIndex));
while (currentGlobalIndex < end && shouldSkipGlobal(candidateLocalIndex)) {
currentGlobalIndex++;
candidateLocalIndex = Math.abs(dictionary.indexOf(currentGlobalIndex));
while (currIndex < end && !matcher.apply(globalDictionary.get(dictionary.get(currIndex)))) {
currIndex++;
}
if (currentGlobalIndex < end) {
currentGlobalIndex++;
return candidateLocalIndex;
if (currIndex < end) {
return currIndex++;
} else {
return -1;
}
}
private boolean shouldSkipGlobal(int candidate)
{
return currentLocalIndex == candidate || !matcher.apply(globalDictionary.get(currentGlobalIndex));
}
@Override
public boolean hasNext()
{
return currentLocalIndex != -1;
return found != -1;
}
@Override
public ImmutableBitmap next()
{
int cur = currentLocalIndex;
int cur = found;
if (cur == -1) {
throw new NoSuchElementException();
}
currentLocalIndex = findNext();
found = findNext();
return getBitmap(cur);
}
};
@ -492,10 +488,8 @@ public class NestedFieldLiteralColumnIndexSupplier implements ColumnIndexSupplie
}
}
private class NestedLongLiteralValueSetIndex implements StringValueSetIndex
{
@Override
public BitmapColumnIndex forValue(@Nullable String value)
{
@ -592,6 +586,27 @@ public class NestedFieldLiteralColumnIndexSupplier implements ColumnIndexSupplie
}
}
private class NestedLongLiteralNumericRangeIndex implements NumericRangeIndex
{
@Override
public BitmapColumnIndex forRange(
@Nullable Number startValue,
boolean startStrict,
@Nullable Number endValue,
boolean endStrict
)
{
return makeRangeIndex(
startValue != null ? startValue.longValue() : null,
startStrict,
endValue != null ? endValue.longValue() : null,
endStrict,
globalLongDictionary,
adjustLongId
);
}
}
private class NestedLongLiteralPredicateIndex implements DruidPredicateIndex
{
@Override
@ -658,7 +673,6 @@ public class NestedFieldLiteralColumnIndexSupplier implements ColumnIndexSupplie
private class NestedDoubleLiteralValueSetIndex implements StringValueSetIndex
{
@Override
public BitmapColumnIndex forValue(@Nullable String value)
{
@ -755,6 +769,27 @@ public class NestedFieldLiteralColumnIndexSupplier implements ColumnIndexSupplie
}
}
private class NestedDoubleLiteralNumericRangeIndex implements NumericRangeIndex
{
@Override
public BitmapColumnIndex forRange(
@Nullable Number startValue,
boolean startStrict,
@Nullable Number endValue,
boolean endStrict
)
{
return makeRangeIndex(
startValue != null ? startValue.doubleValue() : null,
startStrict,
endValue != null ? endValue.doubleValue() : null,
endStrict,
globalDoubleDictionary,
adjustDoubleId
);
}
}
private class NestedDoubleLiteralPredicateIndex implements DruidPredicateIndex
{
@Override
@ -818,7 +853,7 @@ public class NestedFieldLiteralColumnIndexSupplier implements ColumnIndexSupplie
}
}
private abstract class NestedAnyLiteralIndex
private abstract class NestedVariantLiteralIndex
{
IntList getIndexes(@Nullable String value)
{
@ -858,7 +893,7 @@ public class NestedFieldLiteralColumnIndexSupplier implements ColumnIndexSupplie
/**
* {@link StringValueSetIndex} but for variant typed nested literal columns
*/
private class NestedVariantLiteralValueSetIndex extends NestedAnyLiteralIndex implements StringValueSetIndex
private class NestedVariantLiteralValueSetIndex extends NestedVariantLiteralIndex implements StringValueSetIndex
{
@Override
public BitmapColumnIndex forValue(@Nullable String value)
@ -937,9 +972,8 @@ public class NestedFieldLiteralColumnIndexSupplier implements ColumnIndexSupplie
/**
* {@link DruidPredicateIndex} but for variant typed nested literal columns
*/
private class NestedVariantLiteralPredicateIndex extends NestedAnyLiteralIndex implements DruidPredicateIndex
private class NestedVariantLiteralPredicateIndex extends NestedVariantLiteralIndex implements DruidPredicateIndex
{
@Override
public BitmapColumnIndex forPredicate(DruidPredicateFactory matcherFactory)
{

View File

@ -852,7 +852,6 @@ public class CalciteNestedDataQueryTest extends BaseCalciteQueryTest
.build()
),
ImmutableList.of(
new Object[]{NullHandling.defaultStringValue(), 4L},
new Object[]{"100", 2L}
)
);
@ -954,6 +953,7 @@ public class CalciteNestedDataQueryTest extends BaseCalciteQueryTest
.build()
),
ImmutableList.of(
new Object[]{NullHandling.defaultStringValue(), 4L},
new Object[]{"100", 2L}
)
);
@ -1052,7 +1052,6 @@ public class CalciteNestedDataQueryTest extends BaseCalciteQueryTest
.build()
),
ImmutableList.of(
new Object[]{NullHandling.defaultStringValue(), 4L},
new Object[]{"2.02", 2L}
)
);
@ -1154,6 +1153,7 @@ public class CalciteNestedDataQueryTest extends BaseCalciteQueryTest
.build()
),
ImmutableList.of(
new Object[]{NullHandling.defaultStringValue(), 4L},
new Object[]{"2.02", 2L}
)
);