Merge pull request #37048 from Avogar/fix-array-map-nothing

Add default implementation for Nothing in functions
This commit is contained in:
Nikolai Kochetov 2022-05-25 19:10:40 +02:00 committed by GitHub
commit ff98c24d44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 235 additions and 13 deletions

View File

@ -16,7 +16,7 @@ class SerializationNothing : public SimpleTextSerialization
private:
[[noreturn]] static void throwNoSerialization()
{
throw Exception("Serialization is not implemented", ErrorCodes::NOT_IMPLEMENTED);
throw Exception("Serialization is not implemented for type Nothing", ErrorCodes::NOT_IMPLEMENTED);
}
public:
void serializeBinary(const Field &, WriteBuffer &) const override { throwNoSerialization(); }

View File

@ -98,6 +98,7 @@ protected:
}
bool useDefaultImplementationForNulls() const override { return false; }
bool useDefaultImplementationForNothing() const override { return false; }
bool useDefaultImplementationForLowCardinalityColumns() const override { return false; }
private:

View File

@ -2516,6 +2516,8 @@ protected:
}
bool useDefaultImplementationForNulls() const override { return false; }
/// CAST(Nothing, T) -> T
bool useDefaultImplementationForNothing() const override { return false; }
bool useDefaultImplementationForConstants() const override { return true; }
bool useDefaultImplementationForLowCardinalityColumns() const override { return false; }
ColumnNumbers getArgumentsThatAreAlwaysConstant() const override { return {1}; }

View File

@ -50,7 +50,11 @@ public:
return expr_columns.getByName(signature->return_name).column;
}
bool useDefaultImplementationForNulls() const override { return false; }
bool useDefaultImplementationForNulls() const override { return false; }
/// It's possible if expression_actions contains function that don't use
/// default implementation for Nothing.
/// Example: arrayMap(x -> CAST(x, 'UInt8'), []);
bool useDefaultImplementationForNothing() const override { return false; }
private:
ExpressionActionsPtr expression_actions;
@ -118,6 +122,10 @@ public:
String getName() const override { return "FunctionCapture"; }
bool useDefaultImplementationForNulls() const override { return false; }
/// It's possible if expression_actions contains function that don't use
/// default implementation for Nothing and one of captured columns can be Nothing
/// Example: SELECT arrayMap(x -> [x, arrayElement(y, 0)], []), [] as y
bool useDefaultImplementationForNothing() const override { return false; }
bool useDefaultImplementationForLowCardinalityColumns() const override { return false; }
ColumnPtr executeImpl(const ColumnsWithTypeAndName & arguments, const DataTypePtr &, size_t input_rows_count) const override
@ -247,6 +255,8 @@ public:
String getName() const override { return name; }
bool useDefaultImplementationForNulls() const override { return false; }
/// See comment in ExecutableFunctionCapture.
bool useDefaultImplementationForNothing() const override { return false; }
bool useDefaultImplementationForLowCardinalityColumns() const override { return false; }
DataTypePtr getReturnTypeImpl(const ColumnsWithTypeAndName &) const override { return return_type; }
size_t getNumberOfArguments() const override { return capture->captured_types.size(); }

View File

@ -9,6 +9,7 @@
#include <Columns/ColumnTuple.h>
#include <Columns/ColumnLowCardinality.h>
#include <Columns/ColumnSparse.h>
#include <Columns/ColumnNothing.h>
#include <DataTypes/DataTypeNothing.h>
#include <DataTypes/DataTypeNullable.h>
#include <DataTypes/Native.h>
@ -203,6 +204,32 @@ ColumnPtr IExecutableFunction::defaultImplementationForNulls(
return nullptr;
}
ColumnPtr IExecutableFunction::defaultImplementationForNothing(
const ColumnsWithTypeAndName & args, const DataTypePtr & result_type, size_t input_rows_count) const
{
if (!useDefaultImplementationForNothing())
return nullptr;
bool is_nothing_type_presented = false;
for (const auto & arg : args)
is_nothing_type_presented |= isNothing(arg.type);
if (!is_nothing_type_presented)
return nullptr;
if (!isNothing(result_type))
throw Exception(
ErrorCodes::LOGICAL_ERROR,
"Function {} with argument with type Nothing and default implementation for Nothing "
"is expected to return result with type Nothing, got {}",
getName(),
result_type->getName());
if (input_rows_count > 0)
throw Exception(ErrorCodes::ILLEGAL_COLUMN, "Cannot create non-empty column with type Nothing");
return ColumnNothing::create(0);
}
ColumnPtr IExecutableFunction::executeWithoutLowCardinalityColumns(
const ColumnsWithTypeAndName & args, const DataTypePtr & result_type, size_t input_rows_count, bool dry_run) const
{
@ -212,6 +239,9 @@ ColumnPtr IExecutableFunction::executeWithoutLowCardinalityColumns(
if (auto res = defaultImplementationForNulls(args, result_type, input_rows_count, dry_run))
return res;
if (auto res = defaultImplementationForNothing(args, result_type, input_rows_count))
return res;
ColumnPtr res;
if (dry_run)
res = executeDryRunImpl(args, result_type, input_rows_count);
@ -436,6 +466,15 @@ DataTypePtr IFunctionOverloadResolver::getReturnTypeWithoutLowCardinality(const
}
}
if (!arguments.empty() && useDefaultImplementationForNothing())
{
for (const auto & arg : arguments)
{
if (isNothing(arg.type))
return std::make_shared<DataTypeNothing>();
}
}
return getReturnTypeImpl(arguments);
}

View File

@ -63,6 +63,11 @@ protected:
*/
virtual bool useDefaultImplementationForNulls() const { return true; }
/** Default implementation in presence of arguments with type Nothing is the following:
* If some of arguments have type Nothing then default implementation is to return constant column with type Nothing
*/
virtual bool useDefaultImplementationForNothing() const { return true; }
/** If the function have non-zero number of arguments,
* and if all arguments are constant, that we could automatically provide default implementation:
* arguments are converted to ordinary columns with single value, then function is executed as usual,
@ -100,6 +105,9 @@ private:
ColumnPtr defaultImplementationForNulls(
const ColumnsWithTypeAndName & args, const DataTypePtr & result_type, size_t input_rows_count, bool dry_run) const;
ColumnPtr defaultImplementationForNothing(
const ColumnsWithTypeAndName & args, const DataTypePtr & result_type, size_t input_rows_count) const;
ColumnPtr executeWithoutLowCardinalityColumns(
const ColumnsWithTypeAndName & args, const DataTypePtr & result_type, size_t input_rows_count, bool dry_run) const;
@ -166,8 +174,8 @@ public:
/** If function isSuitableForConstantFolding then, this method will be called during query analyzis
* if some arguments are constants. For example logical functions (AndFunction, OrFunction) can
* return they result based on some constant arguments.
* Arguments are passed without modifications, useDefaultImplementationForNulls, useDefaultImplementationForConstants,
* useDefaultImplementationForLowCardinality are not applied.
* Arguments are passed without modifications, useDefaultImplementationForNulls, useDefaultImplementationForNothing,
* useDefaultImplementationForConstants, useDefaultImplementationForLowCardinality are not applied.
*/
virtual ColumnPtr getConstantResultForNonConstArguments(
const ColumnsWithTypeAndName & /* arguments */, const DataTypePtr & /* result_type */) const { return nullptr; }
@ -354,7 +362,13 @@ protected:
*/
virtual bool useDefaultImplementationForNulls() const { return true; }
/** If useDefaultImplementationForNulls() is true, then change arguments for getReturnType() and build().
/** If useDefaultImplementationForNothing() is true, then change arguments for getReturnType() and build():
* if some of arguments are Nothing then don't call getReturnType(), call build() with return_type = Nothing,
* Otherwise build returns build(arguments, getReturnType(arguments));
*/
virtual bool useDefaultImplementationForNothing() const { return true; }
/** If useDefaultImplementationForLowCardinalityColumns() is true, then change arguments for getReturnType() and build().
* If function arguments has low cardinality types, convert them to ordinary types.
* getReturnType returns ColumnLowCardinality if at least one argument type is ColumnLowCardinality.
*/
@ -403,6 +417,11 @@ public:
*/
virtual bool useDefaultImplementationForNulls() const { return true; }
/** Default implementation in presence of arguments with type Nothing is the following:
* If some of arguments have type Nothing then default implementation is to return constant column with type Nothing
*/
virtual bool useDefaultImplementationForNothing() const { return true; }
/** If the function have non-zero number of arguments,
* and if all arguments are constant, that we could automatically provide default implementation:
* arguments are converted to ordinary columns with single value, then function is executed as usual,

View File

@ -27,6 +27,7 @@ protected:
}
bool useDefaultImplementationForNulls() const final { return function->useDefaultImplementationForNulls(); }
bool useDefaultImplementationForNothing() const final { return function->useDefaultImplementationForNothing(); }
bool useDefaultImplementationForConstants() const final { return function->useDefaultImplementationForConstants(); }
bool useDefaultImplementationForLowCardinalityColumns() const final { return function->useDefaultImplementationForLowCardinalityColumns(); }
bool useDefaultImplementationForSparseColumns() const final { return function->useDefaultImplementationForSparseColumns(); }
@ -124,6 +125,7 @@ public:
DataTypePtr getReturnTypeImpl(const ColumnsWithTypeAndName & arguments) const override { return function->getReturnTypeImpl(arguments); }
bool useDefaultImplementationForNulls() const override { return function->useDefaultImplementationForNulls(); }
bool useDefaultImplementationForNothing() const override { return function->useDefaultImplementationForNothing(); }
bool useDefaultImplementationForLowCardinalityColumns() const override { return function->useDefaultImplementationForLowCardinalityColumns(); }
bool useDefaultImplementationForSparseColumns() const override { return function->useDefaultImplementationForSparseColumns(); }
bool canBeExecutedOnLowCardinalityDictionary() const override { return function->canBeExecutedOnLowCardinalityDictionary(); }

View File

@ -6,6 +6,7 @@
#include <Columns/ColumnConst.h>
#include <Columns/ColumnFunction.h>
#include <Columns/ColumnMap.h>
#include <Columns/ColumnNullable.h>
#include <Columns/IColumn.h>
#include <Common/Exception.h>
@ -16,11 +17,13 @@
#include <DataTypes/DataTypeFunction.h>
#include <DataTypes/DataTypeLowCardinality.h>
#include <DataTypes/DataTypeMap.h>
#include <DataTypes/DataTypesNumber.h>
#include <Functions/FunctionHelpers.h>
#include <Functions/IFunction.h>
#include <Interpreters/Context_fwd.h>
#include <Interpreters/castColumn.h>
#include <IO/WriteHelpers.h>
@ -156,7 +159,7 @@ public:
DataTypePtr nested_type = data_type->getNestedType();
if (Impl::needBoolean() && !WhichDataType(nested_type).isUInt8())
if (Impl::needBoolean() && !isUInt8(nested_type))
throw Exception("The only argument for function " + getName() + " must be array of UInt8. Found "
+ arguments[0].type->getName() + " instead", ErrorCodes::ILLEGAL_TYPE_OF_ARGUMENT);
@ -180,8 +183,14 @@ public:
/// The types of the remaining arguments are already checked in getLambdaArgumentTypes.
DataTypePtr return_type = removeLowCardinality(data_type_function->getReturnType());
if (Impl::needBoolean() && !WhichDataType(return_type).isUInt8())
throw Exception("Expression for function " + getName() + " must return UInt8, found "
/// Special cases when we need boolean lambda result:
/// - lambda may return Nullable(UInt8) column, in this case after lambda execution we will
/// replace all NULLs with 0 and return nested UInt8 column.
/// - lambda may return Nothing or Nullable(Nothing) because of default implementation of functions
/// for these types. In this case we will just create UInt8 const column full of 0.
if (Impl::needBoolean() && !isUInt8(removeNullable(return_type)) && !isNothing(removeNullable(return_type)))
throw Exception("Expression for function " + getName() + " must return UInt8 or Nullable(UInt8), found "
+ return_type->getName(), ErrorCodes::ILLEGAL_TYPE_OF_ARGUMENT);
static_assert(
@ -316,11 +325,37 @@ public:
auto * replicated_column_function = typeid_cast<ColumnFunction *>(replicated_column_function_ptr.get());
replicated_column_function->appendArguments(arrays);
auto lambda_result = replicated_column_function->reduce().column;
if (lambda_result->lowCardinality())
lambda_result = lambda_result->convertToFullColumnIfLowCardinality();
auto lambda_result = replicated_column_function->reduce();
if (lambda_result.column->lowCardinality())
lambda_result.column = lambda_result.column->convertToFullColumnIfLowCardinality();
return Impl::execute(*column_first_array, lambda_result);
if (Impl::needBoolean())
{
/// If result column is Nothing or Nullable(Nothing), just create const UInt8 column with 0 value.
if (isNothing(removeNullable(lambda_result.type)))
{
auto result_type = std::make_shared<DataTypeUInt8>();
lambda_result.column = result_type->createColumnConst(lambda_result.column->size(), 0);
}
/// If result column is Nullable(UInt8), then extract nested column and write 0 in all rows
/// when we have NULL.
else if (lambda_result.column->isNullable())
{
auto result_column = IColumn::mutate(std::move(lambda_result.column));
auto * column_nullable = assert_cast<ColumnNullable *>(result_column.get());
auto & null_map = column_nullable->getNullMapData();
auto nested_column = IColumn::mutate(std::move(column_nullable->getNestedColumnPtr()));
auto & nested_data = assert_cast<ColumnUInt8 *>(nested_column.get())->getData();
for (size_t i = 0; i != nested_data.size(); ++i)
{
if (null_map[i])
nested_data[i] = 0;
}
lambda_result.column = std::move(nested_column);
}
}
return Impl::execute(*column_first_array, lambda_result.column);
}
}
};

View File

@ -20,6 +20,8 @@ public:
}
bool useDefaultImplementationForNulls() const override { return false; }
/// array(..., Nothing, ...) -> Array(..., Nothing, ...)
bool useDefaultImplementationForNothing() const override { return false; }
bool useDefaultImplementationForConstants() const override { return true; }
bool isVariadic() const override { return true; }

View File

@ -7,6 +7,12 @@
namespace DB
{
namespace ErrorCodes
{
extern const int ILLEGAL_COLUMN;
}
namespace
{
@ -45,6 +51,9 @@ public:
{
const ColumnPtr & col = arguments[0].column;
if (arguments[0].type->onlyNull() && !col->empty())
throw Exception(ErrorCodes::ILLEGAL_COLUMN, "Cannot create non-empty column with type Nothing");
if (const auto * nullable_col = checkAndGetColumn<ColumnNullable>(*col))
return nullable_col->getNestedColumnPtr();
else

View File

@ -52,6 +52,7 @@ public:
ColumnNumbers getArgumentsThatAreAlwaysConstant() const override { return {1}; }
bool useDefaultImplementationForNulls() const override { return false; }
bool useDefaultImplementationForNothing() const override { return false; }
bool useDefaultImplementationForConstants() const override { return false; }
bool useDefaultImplementationForLowCardinalityColumns() const override { return true; }
bool isSuitableForShortCircuitArgumentsExecution(const DataTypesWithConstInfo & /*arguments*/) const override { return false; }
@ -194,6 +195,7 @@ private:
bool isVariadic() const override { return true; }
bool useDefaultImplementationForNulls() const override { return impl.useDefaultImplementationForNulls(); }
bool useDefaultImplementationForNothing() const override { return impl.useDefaultImplementationForNothing(); }
bool useDefaultImplementationForLowCardinalityColumns() const override { return impl.useDefaultImplementationForLowCardinalityColumns();}
bool useDefaultImplementationForConstants() const override { return impl.useDefaultImplementationForConstants();}
bool isSuitableForShortCircuitArgumentsExecution(const DataTypesWithConstInfo & arguments) const override

View File

@ -27,6 +27,8 @@ public:
bool useDefaultImplementationForNulls() const override { return false; }
bool useDefaultImplementationForNothing() const override { return false; }
bool isSuitableForShortCircuitArgumentsExecution(const DataTypesWithConstInfo & /*arguments*/) const override { return false; }
size_t getNumberOfArguments() const override

View File

@ -65,6 +65,8 @@ public:
bool isSuitableForShortCircuitArgumentsExecution(const DataTypesWithConstInfo & /*arguments*/) const override { return true; }
bool useDefaultImplementationForNulls() const override { return false; }
/// map(..., Nothing) -> Map(..., Nothing)
bool useDefaultImplementationForNothing() const override { return false; }
bool useDefaultImplementationForConstants() const override { return true; }
DataTypePtr getReturnTypeImpl(const DataTypes & arguments) const override

View File

@ -26,6 +26,8 @@ public:
bool useDefaultImplementationForNulls() const override { return false; }
bool useDefaultImplementationForNothing() const override { return false; }
bool isShortCircuit(ShortCircuitSettings & settings, size_t /*number_of_arguments*/) const override
{
settings.enable_lazy_execution_for_first_argument = true;

View File

@ -28,6 +28,7 @@ public:
size_t getNumberOfArguments() const override { return 1; }
bool useDefaultImplementationForNulls() const override { return false; }
bool useDefaultImplementationForNothing() const override { return false; }
bool useDefaultImplementationForConstants() const override { return true; }
bool isSuitableForShortCircuitArgumentsExecution(const DataTypesWithConstInfo & /*arguments*/) const override { return false; }

View File

@ -30,6 +30,8 @@ public:
bool useDefaultImplementationForNulls() const override { return false; }
bool useDefaultImplementationForNothing() const override { return false; }
bool isShortCircuit(ShortCircuitSettings & settings, size_t /*number_of_arguments*/) const override
{
settings.enable_lazy_execution_for_first_argument = false;

View File

@ -52,6 +52,8 @@ public:
bool isSuitableForShortCircuitArgumentsExecution(const DataTypesWithConstInfo & /*arguments*/) const override { return false; }
bool useDefaultImplementationForNulls() const override { return false; }
/// tuple(..., Nothing, ...) -> Tuple(..., Nothing, ...)
bool useDefaultImplementationForNothing() const override { return false; }
bool useDefaultImplementationForConstants() const override { return true; }
DataTypePtr getReturnTypeImpl(const DataTypes & arguments) const override

View File

@ -21,7 +21,7 @@ INSERT INTO type_names VALUES (1, toTypeName([1, 2]), toTypeName((256, -1, 3.14,
-- _NUM_5: Float64 -> Int64
INSERT INTO values_template VALUES ((1), lower(replaceAll('Hella', 'a', 'o')), 1 + 2 + 3, round(-4 * 5.0), nan / CAST('42', 'Int8'), reverse([1, 2, 3])), ((2), lower(replaceAll('Warld', 'a', 'o')), -4 + 5 + 6, round(18446744073709551615 * 1e-19), 1.0 / CAST('0', 'Int8'), reverse([])), ((3), lower(replaceAll('Test', 'a', 'o')), 3 + 2 + 1, round(9223372036854775807 * -1), 6.28 / CAST('2', 'Int8'), reverse([4, 5])), ((4), lower(replaceAll('Expressians', 'a', 'o')), 6 + 5 + 4, round(1 * -9223372036854775807), 127.0 / CAST('127', 'Int8'), reverse([6, 7, 8, 9, 0]));
INSERT INTO values_template_nullable VALUES ((1), lower(replaceAll('Hella', 'a', 'o')), 1 + 2 + 3, arraySort(x -> assumeNotNull(x), [null, NULL])), ((2), lower(replaceAll('Warld', 'b', 'o')), 4 - 5 + 6, arraySort(x -> assumeNotNull(x), [+1, -1, Null])), ((3), lower(replaceAll('Test', 'c', 'o')), 3 + 2 - 1, arraySort(x -> assumeNotNull(x), [1, nUlL, 3.14])), ((4), lower(replaceAll(null, 'c', 'o')), 6 + 5 - null, arraySort(x -> assumeNotNull(x), [3, 2, 1]));
INSERT INTO values_template_nullable VALUES ((1), lower(replaceAll('Hella', 'a', 'o')), 1 + 2 + 3, arraySort(x -> assumeNotNull(x), [null, NULL::Nullable(UInt8)])), ((2), lower(replaceAll('Warld', 'b', 'o')), 4 - 5 + 6, arraySort(x -> assumeNotNull(x), [+1, -1, Null])), ((3), lower(replaceAll('Test', 'c', 'o')), 3 + 2 - 1, arraySort(x -> assumeNotNull(x), [1, nUlL, 3.14])), ((4), lower(replaceAll(null, 'c', 'o')), 6 + 5 - null, arraySort(x -> assumeNotNull(x), [3, 2, 1]));
INSERT INTO values_template_fallback VALUES (1 + x); -- { clientError 62 }
INSERT INTO values_template_fallback VALUES (abs(functionThatDoesNotExists(42))); -- { clientError 46 }

View File

@ -0,0 +1,29 @@
[]
Array(Nothing)
[]
Array(Nothing)
[]
Array(Nothing)
[]
Array(Nothing)
Array(String)
Array(Nothing)
Array(Nothing)
Array(Array(Nothing))
Array(Array(Nothing))
Array(Map(UInt8, Nothing))
Array(Map(UInt8, Nothing))
Array(Tuple(Nothing))
Array(Tuple(UInt8, Nothing))
Nothing
Nothing
Nothing
Nothing
Array(Nothing)
Array(Nothing)
Map(UInt8, Nothing)
Map(UInt8, Nothing)
Tuple(UInt8, Nothing)
Tuple(UInt8, Nothing)
Nothing
Nothing

View File

@ -0,0 +1,40 @@
select arrayMap(x -> 2 * x, []);
select toTypeName(arrayMap(x -> 2 * x, []));
select arrayMap((x, y) -> x + y, [], []);
select toTypeName(arrayMap((x, y) -> x + y, [], []));
select arrayMap((x, y) -> x + y, [], CAST([], 'Array(Int32)'));
select toTypeName(arrayMap((x, y) -> x + y, [], CAST([], 'Array(Int32)')));
select arrayFilter(x -> 2 * x < 0, []);
select toTypeName(arrayFilter(x -> 2 * x < 0, []));
select toTypeName(arrayMap(x -> CAST(x, 'String'), []));
select toTypeName(arrayMap(x -> toInt32(x), []));
select toColumnTypeName(arrayMap(x -> toInt32(x), []));
select toTypeName(arrayMap(x -> [x], []));
select toColumnTypeName(arrayMap(x -> [x], []));
select toTypeName(arrayMap(x ->map(1, x), []));
select toColumnTypeName(arrayMap(x -> map(1, x), []));
select toTypeName(arrayMap(x ->tuple(x), []));
select toColumnTypeName(arrayMap(x -> tuple(1, x), []));
select toTypeName(toInt32(assumeNotNull(materialize(NULL))));
select toColumnTypeName(toInt32(assumeNotNull(materialize(NULL))));
select toTypeName(assumeNotNull(materialize(NULL)));
select toColumnTypeName(assumeNotNull(materialize(NULL)));
select toTypeName([assumeNotNull(materialize(NULL))]);
select toColumnTypeName([assumeNotNull(materialize(NULL))]);
select toTypeName(map(1, assumeNotNull(materialize(NULL))));
select toColumnTypeName(map(1, assumeNotNull(materialize(NULL))));
select toTypeName(tuple(1, assumeNotNull(materialize(NULL))));
select toColumnTypeName(tuple(1, assumeNotNull(materialize(NULL))));
select toTypeName(assumeNotNull(materialize(NULL)) * 2);
select toColumnTypeName(assumeNotNull(materialize(NULL)) * 2);

View File

@ -0,0 +1,10 @@
#!/usr/bin/env bash
CUR_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck source=../shell_config.sh
. "$CUR_DIR"/../shell_config.sh
$CLICKHOUSE_LOCAL -q "SELECT assumeNotNull(NULL)" 2>&1 | grep -q "ILLEGAL_COLUMN" && echo "OK" || echo "FAIL"
$CLICKHOUSE_LOCAL -q "SELECT assumeNotNull(materialize(NULL))" 2>&1 | grep -q "ILLEGAL_TYPE_OF_ARGUMENT" && echo "OK" || echo "FAIL"
$CLICKHOUSE_LOCAL -q "SELECT assumeNotNull(materialize(NULL)) from numbers(10)" 2>&1 | grep -q "ILLEGAL_TYPE_OF_ARGUMENT" && echo "OK" || echo "FAIL"

View File

@ -0,0 +1,4 @@
[]
[]
[2,4]
[1,3]

View File

@ -0,0 +1,4 @@
select arrayFilter(x -> 2 * x > 0, []);
select arrayFilter(x -> 2 * x > 0, [NULL]);
select arrayFilter(x -> x % 2 ? NULL : 1, [1, 2, 3, 4]);
select arrayFilter(x -> x % 2, [1, NULL, 3, NULL]);