blob: dcc47c080c47c90a99bbbf1d616f061f803004de [file]
################################################################################
# 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.
################################################################################
import datetime
import json
import unittest
import pyarrow as pa
from pypaimon.common.predicate_json_parser import (
_convert_literal,
_paimon_type_to_arrow,
extract_referenced_fields,
parse_predicate_to_batch_filter,
)
def _make_leaf(field_name, function, literals=None, field_type="INT"):
d = {
"kind": "LEAF",
"transform": {
"name": "FIELD_REF",
"fieldRef": {"index": 0, "name": field_name, "type": field_type},
},
"function": function,
}
if literals is not None:
d["literals"] = literals
return json.dumps(d)
class TestLeafFunctions(unittest.TestCase):
def setUp(self):
self.batch = pa.RecordBatch.from_pydict({
"id": [1, 2, 3, 4, 5],
"name": ["alice", "bob", "charlie", "alice", "eve"],
"score": [85.5, 92.0, 78.3, 95.1, 60.0],
})
def _filter(self, json_str):
return parse_predicate_to_batch_filter(json_str)(self.batch).to_pylist()
def test_equal(self):
self.assertEqual(
self._filter(_make_leaf("id", "EQUAL", [3])),
[False, False, True, False, False],
)
def test_not_equal(self):
self.assertEqual(
self._filter(_make_leaf("id", "NOT_EQUAL", [3])),
[True, True, False, True, True],
)
def test_less_than(self):
self.assertEqual(
self._filter(_make_leaf("id", "LESS_THAN", [3])),
[True, True, False, False, False],
)
def test_less_or_equal(self):
self.assertEqual(
self._filter(_make_leaf("id", "LESS_OR_EQUAL", [3])),
[True, True, True, False, False],
)
def test_greater_than(self):
self.assertEqual(
self._filter(_make_leaf("id", "GREATER_THAN", [3])),
[False, False, False, True, True],
)
def test_greater_or_equal(self):
self.assertEqual(
self._filter(_make_leaf("id", "GREATER_OR_EQUAL", [3])),
[False, False, True, True, True],
)
def test_is_null(self):
batch = pa.RecordBatch.from_pydict({"val": [1, None, 3, None, 5]})
f = parse_predicate_to_batch_filter(_make_leaf("val", "IS_NULL"))
self.assertEqual(f(batch).to_pylist(), [False, True, False, True, False])
def test_is_not_null(self):
batch = pa.RecordBatch.from_pydict({"val": [1, None, 3, None, 5]})
f = parse_predicate_to_batch_filter(_make_leaf("val", "IS_NOT_NULL"))
self.assertEqual(f(batch).to_pylist(), [True, False, True, False, True])
def test_in(self):
self.assertEqual(
self._filter(_make_leaf("id", "IN", [1, 3, 5])),
[True, False, True, False, True],
)
def test_not_in(self):
self.assertEqual(
self._filter(_make_leaf("id", "NOT_IN", [1, 3, 5])),
[False, True, False, True, False],
)
def test_between(self):
self.assertEqual(
self._filter(_make_leaf("id", "BETWEEN", [2, 4])),
[False, True, True, True, False],
)
def test_not_between(self):
self.assertEqual(
self._filter(_make_leaf("id", "NOT_BETWEEN", [2, 4])),
[True, False, False, False, True],
)
def test_starts_with(self):
self.assertEqual(
self._filter(_make_leaf("name", "STARTS_WITH", ["al"], "STRING")),
[True, False, False, True, False],
)
def test_ends_with(self):
self.assertEqual(
self._filter(_make_leaf("name", "ENDS_WITH", ["e"], "STRING")),
[True, False, True, True, True],
)
def test_contains(self):
self.assertEqual(
self._filter(_make_leaf("name", "CONTAINS", ["li"], "STRING")),
[True, False, True, True, False],
)
def test_like(self):
like_json = json.dumps({
"kind": "LEAF",
"transform": {"name": "FIELD_REF", "fieldRef": {"index": 0, "name": "name", "type": "STRING"}},
"function": "LIKE",
"literals": ["%li%"],
})
self.assertEqual(
parse_predicate_to_batch_filter(like_json)(self.batch).to_pylist(),
[True, False, True, True, False],
)
def test_true(self):
self.assertEqual(
self._filter(_make_leaf("id", "TRUE")),
[True, True, True, True, True],
)
def test_false(self):
self.assertEqual(
self._filter(_make_leaf("id", "FALSE")),
[False, False, False, False, False],
)
class TestCompoundPredicates(unittest.TestCase):
def setUp(self):
self.batch = pa.RecordBatch.from_pydict({
"id": [1, 2, 3, 4, 5],
})
def test_and(self):
pred = json.dumps({
"kind": "COMPOUND",
"function": "AND",
"children": [
{"kind": "LEAF",
"transform": {"name": "FIELD_REF",
"fieldRef": {"index": 0, "name": "id", "type": "INT"}},
"function": "GREATER_THAN", "literals": [2]},
{"kind": "LEAF",
"transform": {"name": "FIELD_REF",
"fieldRef": {"index": 0, "name": "id", "type": "INT"}},
"function": "LESS_THAN", "literals": [5]},
],
})
f = parse_predicate_to_batch_filter(pred)
self.assertEqual(f(self.batch).to_pylist(), [False, False, True, True, False])
def test_or(self):
pred = json.dumps({
"kind": "COMPOUND",
"function": "OR",
"children": [
{"kind": "LEAF",
"transform": {"name": "FIELD_REF",
"fieldRef": {"index": 0, "name": "id", "type": "INT"}},
"function": "EQUAL", "literals": [1]},
{"kind": "LEAF",
"transform": {"name": "FIELD_REF",
"fieldRef": {"index": 0, "name": "id", "type": "INT"}},
"function": "EQUAL", "literals": [5]},
],
})
f = parse_predicate_to_batch_filter(pred)
self.assertEqual(f(self.batch).to_pylist(), [True, False, False, False, True])
class TestPredicateTransforms(unittest.TestCase):
def setUp(self):
self.batch = pa.RecordBatch.from_pydict({
"id": [1, 2, 3, 4, 5],
"name": ["alice", "bob", "charlie", "alice", "eve"],
})
def test_upper_transform(self):
pred = json.dumps({
"kind": "LEAF",
"transform": {"name": "UPPER", "inputs": [{"index": 1, "name": "name", "type": "STRING"}]},
"function": "EQUAL",
"literals": ["ALICE"],
})
f = parse_predicate_to_batch_filter(pred)
self.assertEqual(f(self.batch).to_pylist(), [True, False, False, True, False])
def test_lower_transform(self):
pred = json.dumps({
"kind": "LEAF",
"transform": {"name": "LOWER", "inputs": [{"index": 1, "name": "name", "type": "STRING"}]},
"function": "EQUAL",
"literals": ["bob"],
})
f = parse_predicate_to_batch_filter(pred)
self.assertEqual(f(self.batch).to_pylist(), [False, True, False, False, False])
def test_cast_transform(self):
pred = json.dumps({
"kind": "LEAF",
"transform": {"name": "CAST", "fieldRef": {"index": 0, "name": "id", "type": "INT"}, "type": "BIGINT"},
"function": "GREATER_THAN",
"literals": [3],
})
f = parse_predicate_to_batch_filter(pred)
self.assertEqual(f(self.batch).to_pylist(), [False, False, False, True, True])
def test_null_transform(self):
pred = json.dumps({
"kind": "LEAF",
"transform": {"name": "NULL"},
"function": "IS_NULL",
})
f = parse_predicate_to_batch_filter(pred)
self.assertEqual(f(self.batch).to_pylist(), [True, True, True, True, True])
def test_concat_transform_in_predicate(self):
batch = pa.RecordBatch.from_pydict({
"first": ["john", "jane"],
"last": ["doe", "smith"],
})
pred = json.dumps({
"kind": "LEAF",
"transform": {
"name": "CONCAT",
"inputs": [
{"index": 0, "name": "first", "type": "STRING"},
{"index": 1, "name": "last", "type": "STRING"},
],
},
"function": "EQUAL",
"literals": ["johndoe"],
})
f = parse_predicate_to_batch_filter(pred)
self.assertEqual(f(batch).to_pylist(), [True, False])
def test_concat_ws_transform_in_predicate(self):
batch = pa.RecordBatch.from_pydict({
"first": ["john", "jane"],
"last": ["doe", "smith"],
})
pred = json.dumps({
"kind": "LEAF",
"transform": {
"name": "CONCAT_WS",
"inputs": [
" ",
{"index": 0, "name": "first", "type": "STRING"},
{"index": 1, "name": "last", "type": "STRING"},
],
},
"function": "EQUAL",
"literals": ["john doe"],
})
f = parse_predicate_to_batch_filter(pred)
self.assertEqual(f(batch).to_pylist(), [True, False])
class TestPaimonTypeToArrow(unittest.TestCase):
def test_simple_types(self):
self.assertEqual(_paimon_type_to_arrow("INT"), pa.int32())
self.assertEqual(_paimon_type_to_arrow("BIGINT"), pa.int64())
self.assertEqual(_paimon_type_to_arrow("SMALLINT"), pa.int16())
self.assertEqual(_paimon_type_to_arrow("TINYINT"), pa.int8())
self.assertEqual(_paimon_type_to_arrow("FLOAT"), pa.float32())
self.assertEqual(_paimon_type_to_arrow("DOUBLE"), pa.float64())
self.assertEqual(_paimon_type_to_arrow("STRING"), pa.string())
self.assertEqual(_paimon_type_to_arrow("BOOLEAN"), pa.bool_())
self.assertEqual(_paimon_type_to_arrow("BYTES"), pa.binary())
self.assertEqual(_paimon_type_to_arrow("DATE"), pa.date32())
def test_varchar_char(self):
self.assertEqual(_paimon_type_to_arrow("VARCHAR"), pa.string())
self.assertEqual(_paimon_type_to_arrow("VARCHAR(100)"), pa.string())
self.assertEqual(_paimon_type_to_arrow("CHAR(10)"), pa.string())
def test_timestamp(self):
self.assertEqual(_paimon_type_to_arrow("TIMESTAMP(3)"), pa.timestamp("ms"))
self.assertEqual(_paimon_type_to_arrow("TIMESTAMP(6)"), pa.timestamp("us"))
self.assertEqual(_paimon_type_to_arrow("TIMESTAMP(9)"), pa.timestamp("ns"))
self.assertEqual(_paimon_type_to_arrow("TIMESTAMP(0)"), pa.timestamp("s"))
def test_timestamp_ltz(self):
self.assertEqual(
_paimon_type_to_arrow("TIMESTAMP WITH LOCAL TIME ZONE(6)"),
pa.timestamp("us", tz="UTC"),
)
def test_decimal(self):
self.assertEqual(
_paimon_type_to_arrow("DECIMAL(10, 2)"),
pa.decimal128(10, 2),
)
def test_unsupported_type_raises(self):
with self.assertRaises(ValueError):
_paimon_type_to_arrow("ARRAY<INT>")
class TestConvertLiteral(unittest.TestCase):
def test_none_literal(self):
self.assertIsNone(_convert_literal(None, pa.int32()))
def test_int_passthrough(self):
self.assertEqual(_convert_literal(42, pa.int32()), 42)
def test_string_passthrough(self):
self.assertEqual(_convert_literal("hello", pa.string()), "hello")
def test_timestamp_literal(self):
result = _convert_literal("2024-01-15T10:30:00", pa.timestamp("us"))
self.assertIsInstance(result, pa.Scalar)
def test_timestamp_z_suffix(self):
result = _convert_literal("2024-01-15T10:30:00Z", pa.timestamp("us", tz="UTC"))
self.assertIsInstance(result, pa.Scalar)
def test_date_literal(self):
result = _convert_literal("2024-01-15", pa.date32())
self.assertIsInstance(result, pa.Scalar)
def test_decimal_literal(self):
result = _convert_literal(123.45, pa.decimal128(10, 2))
self.assertIsInstance(result, pa.Scalar)
def test_timestamp_ltz_literal_uses_epoch_seconds(self):
# Jackson serializes java.time.Instant as epoch seconds (with an
# optional fractional part for nanos), not epoch milliseconds.
target_type = pa.timestamp("us", tz="UTC")
result = _convert_literal(1720612345.123456, target_type)
self.assertIsInstance(result, pa.Scalar)
expected = pa.scalar(
datetime.datetime.fromtimestamp(1720612345.123456, tz=datetime.timezone.utc),
type=target_type,
)
self.assertEqual(result.value, expected.value)
def test_time_literal_with_nanos(self):
# Jackson serializes java.time.LocalTime as [hour, minute, second,
# nanoOfSecond]; the nanosecond component must survive as
# sub-second precision on the resulting time scalar.
result = _convert_literal([12, 30, 45, 123456000], pa.time64("us"))
self.assertIsInstance(result, pa.Scalar)
expected = pa.scalar(
datetime.time(12, 30, 45, microsecond=123456), type=pa.time64("us"))
self.assertEqual(result.value, expected.value)
def test_time_literal_without_nanos(self):
result = _convert_literal([12, 30, 45], pa.time64("us"))
self.assertIsInstance(result, pa.Scalar)
expected = pa.scalar(datetime.time(12, 30, 45), type=pa.time64("us"))
self.assertEqual(result.value, expected.value)
class TestExtractReferencedFields(unittest.TestCase):
def test_leaf_field_ref(self):
refs = extract_referenced_fields(json.dumps({
"kind": "LEAF",
"transform": {"name": "FIELD_REF", "fieldRef": {"index": 0, "name": "col1", "type": "INT"}},
"function": "EQUAL",
"literals": [1],
}))
self.assertEqual(refs, {"col1"})
def test_leaf_cast(self):
refs = extract_referenced_fields(json.dumps({
"kind": "LEAF",
"transform": {"name": "CAST", "fieldRef": {"index": 0, "name": "col1", "type": "INT"}, "type": "BIGINT"},
"function": "EQUAL",
"literals": [1],
}))
self.assertEqual(refs, {"col1"})
def test_compound_collects_all(self):
refs = extract_referenced_fields(json.dumps({
"kind": "COMPOUND",
"function": "AND",
"children": [
{"kind": "LEAF",
"transform": {"name": "FIELD_REF",
"fieldRef": {"index": 0, "name": "a", "type": "INT"}},
"function": "EQUAL", "literals": [1]},
{"kind": "LEAF",
"transform": {"name": "UPPER",
"inputs": [{"index": 1, "name": "b", "type": "STRING"}]},
"function": "EQUAL", "literals": ["X"]},
],
}))
self.assertEqual(refs, {"a", "b"})
def test_concat_inputs(self):
refs = extract_referenced_fields(json.dumps({
"kind": "LEAF",
"transform": {
"name": "CONCAT",
"inputs": [
{"index": 0, "name": "first", "type": "STRING"},
"literal",
{"index": 1, "name": "last", "type": "STRING"},
],
},
"function": "EQUAL",
"literals": ["x"],
}))
self.assertEqual(refs, {"first", "last"})
class TestLikeEdgeCases(unittest.TestCase):
def test_dot_is_literal(self):
batch = pa.RecordBatch.from_pydict({"v": ["a.b", "axb", "a.bc"]})
f = parse_predicate_to_batch_filter(json.dumps({
"kind": "LEAF",
"transform": {"name": "FIELD_REF", "fieldRef": {"index": 0, "name": "v", "type": "STRING"}},
"function": "LIKE",
"literals": ["a.b"],
}))
self.assertEqual(f(batch).to_pylist(), [True, False, False])
def test_underscore_matches_single_char(self):
batch = pa.RecordBatch.from_pydict({"v": ["abc", "axc", "ac", "abbc"]})
f = parse_predicate_to_batch_filter(json.dumps({
"kind": "LEAF",
"transform": {"name": "FIELD_REF", "fieldRef": {"index": 0, "name": "v", "type": "STRING"}},
"function": "LIKE",
"literals": ["a_c"],
}))
self.assertEqual(f(batch).to_pylist(), [True, True, False, False])
def test_percent_matches_any(self):
batch = pa.RecordBatch.from_pydict({"v": ["abc", "ac", "axyzc", "def"]})
f = parse_predicate_to_batch_filter(json.dumps({
"kind": "LEAF",
"transform": {"name": "FIELD_REF", "fieldRef": {"index": 0, "name": "v", "type": "STRING"}},
"function": "LIKE",
"literals": ["a%c"],
}))
self.assertEqual(f(batch).to_pylist(), [True, True, True, False])
class TestLikeEscapeSemantics(unittest.TestCase):
def _make_like_batch(self, values):
return pa.RecordBatch.from_pydict(
{"name": values}, schema=pa.schema([("name", pa.string())]))
def _make_like_predicate(self, pattern):
return json.dumps({
"kind": "LEAF",
"transform": {"name": "FIELD_REF", "fieldRef": {"name": "name"}},
"function": "LIKE",
"literals": [pattern]
})
def test_like_simple_percent(self):
batch = self._make_like_batch(["admin", "admin_foo", "user"])
fn = parse_predicate_to_batch_filter(self._make_like_predicate("admin%"))
self.assertEqual(fn(batch).to_pylist(), [True, True, False])
def test_like_simple_underscore(self):
batch = self._make_like_batch(["ab", "abc", "a"])
fn = parse_predicate_to_batch_filter(self._make_like_predicate("a_"))
self.assertEqual(fn(batch).to_pylist(), [True, False, False])
def test_like_escaped_underscore(self):
batch = self._make_like_batch(["admin_foo", "adminXfoo", "admin"])
fn = parse_predicate_to_batch_filter(self._make_like_predicate("admin\\_%"))
self.assertEqual(fn(batch).to_pylist(), [True, False, False])
def test_like_escaped_percent(self):
batch = self._make_like_batch(["100%", "100X", "100"])
fn = parse_predicate_to_batch_filter(self._make_like_predicate("100\\%"))
self.assertEqual(fn(batch).to_pylist(), [True, False, False])
def test_like_escaped_backslash(self):
batch = self._make_like_batch(["a\\b", "a/b", "ab"])
fn = parse_predicate_to_batch_filter(self._make_like_predicate("a\\\\b"))
self.assertEqual(fn(batch).to_pylist(), [True, False, False])
def test_like_regex_special_chars(self):
batch = self._make_like_batch(["a.b", "axb", "a..b"])
fn = parse_predicate_to_batch_filter(self._make_like_predicate("a.b"))
self.assertEqual(fn(batch).to_pylist(), [True, False, False])
def test_like_percent_matches_newline(self):
batch = self._make_like_batch(["hello\nworld", "hello"])
fn = parse_predicate_to_batch_filter(self._make_like_predicate("hello%"))
self.assertEqual(fn(batch).to_pylist(), [True, True])
def test_like_invalid_escape_sequence(self):
import pytest
fn = parse_predicate_to_batch_filter(self._make_like_predicate("admin\\x"))
batch = self._make_like_batch(["admin"])
with pytest.raises((RuntimeError, ValueError), match="Invalid escape sequence"):
fn(batch)
class TestNotInNullSemantics(unittest.TestCase):
"""Java NOT_IN: null field -> false; any null literal -> false for all."""
def test_not_in_null_field_returns_false(self):
batch = pa.RecordBatch.from_pydict(
{"dept": ["eng", None, "sales"]},
schema=pa.schema([("dept", pa.string())]),
)
fn = parse_predicate_to_batch_filter(
_make_leaf("dept", "NOT_IN", ["blocked"], field_type="STRING"))
result = fn(batch).to_pylist()
# null dept must return false, not true
self.assertEqual(result, [True, False, True])
def test_not_in_null_literal_returns_false_for_all(self):
batch = pa.RecordBatch.from_pydict(
{"dept": ["eng", None, "sales"]},
schema=pa.schema([("dept", pa.string())]),
)
fn = parse_predicate_to_batch_filter(
_make_leaf("dept", "NOT_IN", ["blocked", None],
field_type="STRING"))
result = fn(batch).to_pylist()
# any null literal makes NOT_IN false for all rows
self.assertEqual(result, [False, False, False])
def test_not_in_normal(self):
batch = pa.RecordBatch.from_pydict(
{"id": [1, 2, 3]},
schema=pa.schema([("id", pa.int64())]),
)
fn = parse_predicate_to_batch_filter(
_make_leaf("id", "NOT_IN", [2, 3]))
result = fn(batch).to_pylist()
self.assertEqual(result, [True, False, False])
class TestIsNaN(unittest.TestCase):
def test_is_nan_float(self):
batch = pa.RecordBatch.from_pydict(
{"val": [1.0, float("nan"), 3.0, None]},
schema=pa.schema([("val", pa.float64())]),
)
fn = parse_predicate_to_batch_filter(
_make_leaf("val", "IS_NAN", field_type="DOUBLE"))
result = fn(batch).to_pylist()
self.assertEqual(result, [False, True, False, False])
def test_is_nan_no_nans(self):
batch = pa.RecordBatch.from_pydict(
{"val": [1.0, 2.0]},
schema=pa.schema([("val", pa.float64())]),
)
fn = parse_predicate_to_batch_filter(
_make_leaf("val", "IS_NAN", field_type="DOUBLE"))
result = fn(batch).to_pylist()
self.assertEqual(result, [False, False])
class TestNullLiteralDefense(unittest.TestCase):
"""Java LeafBinaryFunction/LeafTernaryFunction: null literal -> false."""
def test_starts_with_null_literal(self):
batch = pa.RecordBatch.from_pydict(
{"name": ["alice", "bob", None]},
schema=pa.schema([("name", pa.string())]),
)
fn = parse_predicate_to_batch_filter(
_make_leaf("name", "STARTS_WITH", [None], field_type="STRING"))
result = fn(batch).to_pylist()
self.assertEqual(result, [False, False, False])
def test_ends_with_null_literal(self):
batch = pa.RecordBatch.from_pydict(
{"name": ["alice", "bob", None]},
schema=pa.schema([("name", pa.string())]),
)
fn = parse_predicate_to_batch_filter(
_make_leaf("name", "ENDS_WITH", [None], field_type="STRING"))
result = fn(batch).to_pylist()
self.assertEqual(result, [False, False, False])
def test_contains_null_literal(self):
batch = pa.RecordBatch.from_pydict(
{"name": ["alice", "bob", None]},
schema=pa.schema([("name", pa.string())]),
)
fn = parse_predicate_to_batch_filter(
_make_leaf("name", "CONTAINS", [None], field_type="STRING"))
result = fn(batch).to_pylist()
self.assertEqual(result, [False, False, False])
def test_like_null_literal(self):
batch = pa.RecordBatch.from_pydict(
{"name": ["alice", "bob", None]},
schema=pa.schema([("name", pa.string())]),
)
fn = parse_predicate_to_batch_filter(
_make_leaf("name", "LIKE", [None], field_type="STRING"))
result = fn(batch).to_pylist()
self.assertEqual(result, [False, False, False])
def test_between_null_lower_literal(self):
batch = pa.RecordBatch.from_pydict(
{"val": [1, 5, 10, None]},
schema=pa.schema([("val", pa.int64())]),
)
fn = parse_predicate_to_batch_filter(
_make_leaf("val", "BETWEEN", [None, 10]))
result = fn(batch).to_pylist()
self.assertEqual(result, [False, False, False, False])
def test_between_null_upper_literal(self):
batch = pa.RecordBatch.from_pydict(
{"val": [1, 5, 10, None]},
schema=pa.schema([("val", pa.int64())]),
)
fn = parse_predicate_to_batch_filter(
_make_leaf("val", "BETWEEN", [1, None]))
result = fn(batch).to_pylist()
self.assertEqual(result, [False, False, False, False])
def test_not_between_null_literal(self):
batch = pa.RecordBatch.from_pydict(
{"val": [1, 5, 10, None]},
schema=pa.schema([("val", pa.int64())]),
)
fn = parse_predicate_to_batch_filter(
_make_leaf("val", "NOT_BETWEEN", [None, 10]))
result = fn(batch).to_pylist()
self.assertEqual(result, [False, False, False, False])
def test_starts_with_normal(self):
"""Verify normal case still works after adding null guard."""
batch = pa.RecordBatch.from_pydict(
{"name": ["alice", "anna", "bob", None]},
schema=pa.schema([("name", pa.string())]),
)
fn = parse_predicate_to_batch_filter(
_make_leaf("name", "STARTS_WITH", ["a"], field_type="STRING"))
result = fn(batch).to_pylist()
self.assertEqual(result, [True, True, False, False])
def test_between_normal(self):
"""Verify normal BETWEEN still works."""
batch = pa.RecordBatch.from_pydict(
{"val": [1, 5, 10, None]},
schema=pa.schema([("val", pa.int64())]),
)
fn = parse_predicate_to_batch_filter(
_make_leaf("val", "BETWEEN", [3, 8]))
result = fn(batch).to_pylist()
self.assertEqual(result, [False, True, False, False])
if __name__ == "__main__":
unittest.main()
class TestInNullHandling(unittest.TestCase):
def test_in_with_null_literal_drops_null(self):
batch = pa.RecordBatch.from_pydict(
{"dept": ["eng", "sales", None]},
schema=pa.schema([("dept", pa.string())]),
)
pred = _make_leaf("dept", "IN", ["eng", None], field_type="STRING")
result = parse_predicate_to_batch_filter(pred)(batch).to_pylist()
self.assertEqual(result, [True, False, False])
def test_in_all_null_literals(self):
batch = pa.RecordBatch.from_pydict(
{"dept": ["eng", "sales"]},
schema=pa.schema([("dept", pa.string())]),
)
pred = _make_leaf("dept", "IN", [None, None], field_type="STRING")
result = parse_predicate_to_batch_filter(pred)(batch).to_pylist()
self.assertEqual(result, [False, False])
def test_in_null_field_value_returns_false(self):
batch = pa.RecordBatch.from_pydict(
{"dept": [None, "eng"]},
schema=pa.schema([("dept", pa.string())]),
)
pred = _make_leaf("dept", "IN", ["eng", "sales"], field_type="STRING")
result = parse_predicate_to_batch_filter(pred)(batch).to_pylist()
self.assertEqual(result, [False, True])
class TestComparisonNullLiteral(unittest.TestCase):
def test_equal_null_literal_returns_false(self):
batch = pa.RecordBatch.from_pydict({"id": [1, 2, 3]})
pred = _make_leaf("id", "EQUAL", [None])
result = parse_predicate_to_batch_filter(pred)(batch).to_pylist()
self.assertEqual(result, [False, False, False])
def test_not_equal_null_literal_returns_false(self):
batch = pa.RecordBatch.from_pydict({"id": [1, 2, 3]})
pred = _make_leaf("id", "NOT_EQUAL", [None])
result = parse_predicate_to_batch_filter(pred)(batch).to_pylist()
self.assertEqual(result, [False, False, False])
def test_less_than_null_literal_returns_false(self):
batch = pa.RecordBatch.from_pydict({"id": [1, 2, 3]})
pred = _make_leaf("id", "LESS_THAN", [None])
result = parse_predicate_to_batch_filter(pred)(batch).to_pylist()
self.assertEqual(result, [False, False, False])
def test_greater_than_null_literal_returns_false(self):
batch = pa.RecordBatch.from_pydict({"id": [1, 2, 3]})
pred = _make_leaf("id", "GREATER_THAN", [None])
result = parse_predicate_to_batch_filter(pred)(batch).to_pylist()
self.assertEqual(result, [False, False, False])
class TestConcatWsAllNull(unittest.TestCase):
def test_concat_ws_all_null_values_returns_empty(self):
pred_json = json.dumps({
"kind": "LEAF",
"transform": {
"name": "CONCAT_WS",
"inputs": [",", None, None],
},
"function": "EQUAL",
"literals": [""],
})
batch = pa.RecordBatch.from_pydict(
{"x": [1, 2, 3]},
schema=pa.schema([("x", pa.int64())]),
)
fn = parse_predicate_to_batch_filter(pred_json)
result = fn(batch).to_pylist()
self.assertEqual(result, [True, True, True])
def test_concat_ws_mixed_null_preserves_row_positions(self):
pred_json = json.dumps({
"kind": "LEAF",
"transform": {
"name": "CONCAT_WS",
"inputs": [
"-",
{"index": 0, "name": "a", "type": "STRING"},
{"index": 1, "name": "b", "type": "STRING"},
],
},
"function": "EQUAL",
"literals": ["x-y"],
})
batch = pa.RecordBatch.from_pydict(
{"a": [None, "x", None, "p"], "b": [None, "y", "z", None]},
schema=pa.schema([("a", pa.string()), ("b", pa.string())]),
)
fn = parse_predicate_to_batch_filter(pred_json)
result = fn(batch).to_pylist()
self.assertEqual(result, [False, True, False, False])