blob: d168c6ff4bd55e75b60c13d5cf78b702eebe18d2 [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 base64
import unittest
from pypaimon.globalindex.indexed_split import IndexedSplit
from pypaimon.read.split_serializer import (
_decode_modified_utf8, _decode_str_array, deserialize_split_v1)
from pypaimon.schema.data_types import AtomicType, DataField
# Hand-built BinaryArray<string> == ["id", "longcolumn12"]: hits both the inline
# (<=7 bytes) and var-pointer (>7) element encodings.
_BINARY_ARRAY_STR = (
bytes([0x02, 0x00, 0x00, 0x00, # n = 2
0x00, 0x00, 0x00, 0x00, # null bitset
0x69, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x82, # "id" inline (len 2)
0x0C, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00]) # ptr len=12 off=24
+ b"longcolumn12" + bytes(4))
# Golden SplitSerializer v1: DataSplit, partition [2026, 7], bucket 3,
# bucket_path "dt=20260706/bucket-3", files file-a/file-b, dv on file-b.
# Captured from the Java SplitSerializer (the reference wire format); live
# pypaimon_rust byte-compat is covered by native_plan_integration_test.
_GOLDEN_DATA_SPLIT_V1 = base64.b64decode(
"U1BMSVRfVjEAAAABAAAAAd7D0jAsGexmAAAACAAAAAAAAAAqAAAAHAAAAAIAAAAAAAAAAOoHAAAA"
"AAAABwAAAAAAAAAAAAADABRkdD0yMDI2MDcwNi9idWNrZXQtMwEAAAAIAAAAAAAAAAACAAABcAAA"
"QA8AAAAAZmlsZS1hAIYKAAAAAAAAAAoAAAAAAAAAFAAAAKgAAAAUAAAAwAAAAEgAAADYAAAASAAA"
"ACABAAAAAAAAAAAAAGQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAABoAQAAZAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEA"
"AAAAAAAAAAEAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAADAAAACAA"
"AAAMAAAAMAAAAAgAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAADAAAACAAAAAMAAAAMAAAAAgAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABcAAAQA8AAAAAZmlsZS1iAIYKAAAAAAAAAAoAAAAA"
"AAAAFAAAAKgAAAAUAAAAwAAAAEgAAADYAAAASAAAACABAAAAAAAAAAAAAMgAAAAAAAAAAAAAAAAA"
"AAABAAAAAAAAAAgAAABoAQAAZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAsAAAAAAAAAAAAAAAAAAAEAAAAA"
"AAAAABQAAAAAAAAAAAAAAAAAAAAAAAAADAAAACAAAAAMAAAAMAAAAAgAAABAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAACAAAAAMAAAAMAAAAAgA"
"AABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA"
"AAIAAQAJZHYvZmlsZS1iAAAAAAAAAAIAAAAAAAAACgAAAAAAAAADAAE="
)
# Same split serialized as an IndexedSplit (type id 3): identical DataSplit body
# with a trailing row-ranges/scores section that the reader skips.
_GOLDEN_INDEXED_SPLIT_V1 = base64.b64decode(
"U1BMSVRfVjEAAAABAAAAA/L54FRCJC4xAAAAAd7D0jAsGexmAAAACAAAAAAAAAAqAAAAHAAAAAIA"
"AAAAAAAAAOoHAAAAAAAABwAAAAAAAAAAAAADABRkdD0yMDI2MDcwNi9idWNrZXQtMwEAAAAIAAAA"
"AAAAAAACAAABcAAAQA8AAAAAZmlsZS1hAIYKAAAAAAAAAAoAAAAAAAAAFAAAAKgAAAAUAAAAwAAA"
"AEgAAADYAAAASAAAACABAAAAAAAAAAAAAGQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAABoAQAA"
"ZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAEAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAoAAAAAAAAAAAAAAAAA"
"AAAAAAAADAAAACAAAAAMAAAAMAAAAAgAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAACAAAAAMAAAAMAAAAAgAAABAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABcAAAQA8AAAAAZmlsZS1iAIYK"
"AAAAAAAAAAoAAAAAAAAAFAAAAKgAAAAUAAAAwAAAAEgAAADYAAAASAAAACABAAAAAAAAAAAAAMgA"
"AAAAAAAAAAAAAAAAAAABAAAAAAAAAAgAAABoAQAAZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAsAAAAAAAAA"
"AAAAAAAAAAEAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAADAAAACAAAAAMAAAAMAAAAAgAAABA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAACAA"
"AAAMAAAAMAAAAAgAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAQAAAAIAAQAJZHYvZmlsZS1iAAAAAAAAAAIAAAAAAAAACgAAAAAAAAADAAEAAAAC"
"AAAAAAAAAAEAAAAAAAAABAAAAAAAAAALAAAAAAAAAA0BAAAAAz8AAAA+gAAAPgAAAA=="
)
class SplitSerializerTest(unittest.TestCase):
def _partition_fields(self):
return [DataField(0, 'y', AtomicType('INT')),
DataField(1, 'm', AtomicType('INT'))]
def test_deserialize_data_split_v1_golden(self):
split = deserialize_split_v1(_GOLDEN_DATA_SPLIT_V1, self._partition_fields())
self.assertIsNotNone(split.snapshot_id)
self.assertEqual(split.bucket, 3)
self.assertEqual(list(split.partition.values), [2026, 7])
self.assertEqual([f.file_name for f in split.files], ['file-a', 'file-b'])
self.assertEqual(
[f.file_path for f in split.files],
['dt=20260706/bucket-3/file-a', 'dt=20260706/bucket-3/file-b'])
self.assertEqual([f.level for f in split.files], [0, 1])
self.assertEqual(split.files[0].max_sequence_number, 100)
self.assertEqual(split.files[1].max_sequence_number, 200)
self.assertEqual(len(split.data_deletion_files), 2)
self.assertIsNone(split.data_deletion_files[0])
dv = split.data_deletion_files[1]
self.assertEqual(
(dv.dv_index_path, dv.offset, dv.length, dv.cardinality),
('dv/file-b', 2, 10, 3))
def test_decodes_min_max_keys_with_key_fields(self):
# Trimmed primary keys -> per-file min/max keys are decoded for PK
# merge-on-read. The golden files carry keys [1..10] and [11..20].
key_fields = [DataField(0, 'k', AtomicType('BIGINT'))]
split = deserialize_split_v1(
_GOLDEN_DATA_SPLIT_V1, self._partition_fields(), key_fields)
self.assertEqual([list(f.min_key.values) for f in split.files], [[1], [11]])
self.assertEqual([list(f.max_key.values) for f in split.files], [[10], [20]])
def test_keys_stay_empty_without_key_fields(self):
# Append tables pass no key fields; keys stay empty (unchanged behavior).
split = deserialize_split_v1(_GOLDEN_DATA_SPLIT_V1, self._partition_fields())
self.assertEqual([list(f.min_key.values) for f in split.files], [[], []])
self.assertEqual([list(f.max_key.values) for f in split.files], [[], []])
def test_deserialize_indexed_split_v1_golden(self):
# type id 3 -> IndexedSplit; row_ranges/scores must be preserved (dropping
# them would make the reader scan the whole file, not the ANN/row-id result).
split = deserialize_split_v1(_GOLDEN_INDEXED_SPLIT_V1, self._partition_fields())
self.assertIsInstance(split, IndexedSplit)
self.assertIsNotNone(split.snapshot_id) # delegates to the inner DataSplit
self.assertEqual(split.bucket, 3)
self.assertEqual([f.file_name for f in split.files], ['file-a', 'file-b'])
self.assertEqual(
[(r.from_, r.to) for r in split.row_ranges()], [(1, 4), (11, 13)])
self.assertEqual(split.scores(), [0.5, 0.25, 0.125])
def test_decode_modified_utf8_supplementary_char(self):
# Java writeUTF encodes U+1F600 as a CESU-8 surrogate pair; must recombine.
self.assertEqual(
_decode_modified_utf8(bytes([0xED, 0xA0, 0xBD, 0xED, 0xB8, 0x80])),
'\U0001F600')
def test_decode_str_array_inline_and_pointer(self):
# Covers both element encodings: inline (<=7 bytes) and var pointer (>7).
self.assertEqual(_decode_str_array(_BINARY_ARRAY_STR), ['id', 'longcolumn12'])
self.assertIsNone(_decode_str_array(None))
# Empty array: count 0 + empty null bitset.
self.assertEqual(_decode_str_array(bytes([0, 0, 0, 0])), [])
def test_bad_magic_and_version_raise(self):
good = _GOLDEN_DATA_SPLIT_V1
pf = self._partition_fields()
with self.assertRaisesRegex(ValueError, "magic"):
deserialize_split_v1(b'\x00' * 8 + good[8:], pf)
bad_version = good[:8] + (99).to_bytes(4, 'big') + good[12:]
with self.assertRaisesRegex(ValueError, "version"):
deserialize_split_v1(bad_version, pf)
def test_bad_indexed_magic_and_version_raise(self):
# IndexedSplit magic/version follow the 16-byte SplitSerializer header.
good = _GOLDEN_INDEXED_SPLIT_V1
pf = self._partition_fields()
with self.assertRaisesRegex(ValueError, "IndexedSplit magic"):
deserialize_split_v1(good[:16] + b'\x00' * 8 + good[24:], pf)
bad_version = good[:24] + (99).to_bytes(4, 'big') + good[28:]
with self.assertRaisesRegex(ValueError, "IndexedSplit version"):
deserialize_split_v1(bad_version, pf)
if __name__ == '__main__':
unittest.main()