blob: 45352e1ef83629210fa0f901daa321036d150756 [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 unittest
from unittest.mock import patch
import numpy as np
import pyarrow as pa
import pyarrow.compute as pc
from pypaimon.data import variant_replace, variant_set
from pypaimon.data.generic_variant import GenericVariant, _check_variant_sizes
from pypaimon.data.variant_path import (
_apply_edits,
_build_object_value_ordered,
_materialize_value,
_metadata_key_ids,
_metadata_with_keys,
_path_positions,
_rebuilt_offsets,
_validate_value_field_ids,
variant_get,
)
from pypaimon.data.variant_shredding import (
_build_array_value,
_build_object_value,
_encode_scalar_to_value_bytes,
)
# Bytes built by the Java GenericVariantBuilder for
# {"angular_velocity":{"y":1.5,"z":-2.5},
# "linear_acceleration":{"y":0.25,"z":4.0},"processed":true,"seq":7}.
_JAVA_VALUE = bytes.fromhex(
'0204000304050019323335020201020009121c000000000000f83f1c000000000000'
'04c0020201020009121c000000000000d03f1c0000000000001040040c07')
_JAVA_METADATA = bytes.fromhex(
'010600101112252e31616e67756c61725f76656c6f63697479797a6c696e6561725f'
'616363656c65726174696f6e70726f636573736564736571')
_JAVA_PYTHON_VALUE = {
'angular_velocity': {'y': 1.5, 'z': -2.5},
'linear_acceleration': {'y': 0.25, 'z': 4.0},
'processed': True,
'seq': 7,
}
def _variants(values):
return GenericVariant.to_arrow_array([
GenericVariant.from_python(value) if value is not None else None
for value in values
])
def _decode(column):
return [
None if value is None
else GenericVariant.from_arrow_struct(value).to_python()
for value in column.to_pylist()
]
def _sensor_rows(count, offset=0):
return [
{
'angular_velocity': {
'y': float(index + offset),
'z': float(index + offset) + 0.5,
},
'linear_acceleration': {
'y': -float(index + offset),
'z': -float(index + offset) - 0.5,
},
}
for index in range(count)
]
_SENSOR_PATHS = (
'$.angular_velocity.y',
'$.angular_velocity.z',
'$.linear_acceleration.y',
'$.linear_acceleration.z',
)
class TestVariantSetReplace(unittest.TestCase):
def test_existing_paths_match_variant_replace(self):
column = _variants(_sensor_rows(100) + [None])
current = variant_get(
column, {path: pa.float64() for path in _SENSOR_PATHS})
updates = {
path: pc.negate(values) for path, values in current.items()
}
self.assertTrue(
variant_set(column, updates).equals(
variant_replace(column, updates)))
def test_negates_four_double_paths(self):
rows = _sensor_rows(50)
column = _variants(rows)
current = variant_get(
column, {path: pa.float64() for path in _SENSOR_PATHS})
result = variant_set(column, {
path: pc.negate(values) for path, values in current.items()
})
for row, decoded in zip(rows, _decode(result)):
self.assertEqual(decoded, {
'angular_velocity': {
'y': -row['angular_velocity']['y'],
'z': -row['angular_velocity']['z'],
},
'linear_acceleration': {
'y': -row['linear_acceleration']['y'],
'z': -row['linear_acceleration']['z'],
},
})
def test_replaces_root_path(self):
column = _variants([1.5, -2.5])
result = variant_set(column, '$', pa.scalar(3.5, type=pa.float64()))
self.assertEqual(
variant_get(result, '$', pa.float64()).to_pylist(), [3.5, 3.5])
class TestVariantSetInsert(unittest.TestCase):
def test_inserts_bool_and_string_marks(self):
column = _variants([{'value': 1.0}, {'value': 2.0}])
flagged = variant_set(column, '$.processed', pa.scalar(True))
tagged = variant_set(column, '$.tag', pa.scalar('done'))
self.assertEqual(_decode(flagged), [
{'value': 1.0, 'processed': True},
{'value': 2.0, 'processed': True},
])
self.assertEqual(_decode(tagged), [
{'value': 1.0, 'tag': 'done'},
{'value': 2.0, 'tag': 'done'},
])
def test_insert_extends_metadata_dictionary(self):
column = _variants([{'value': 1.0}])
result = variant_set(column, '$.processed', pa.scalar(True))
metadata = result.to_pylist()[0]['metadata']
self.assertEqual(
_metadata_key_ids(metadata), {'value': 0, 'processed': 1})
def test_insert_reuses_metadata_key_and_buffer(self):
metadata = GenericVariant.from_python(
{'value': 0, 'flag': 0}).metadata()
key_ids = _metadata_key_ids(metadata)
value = _build_object_value([
(key_ids['value'],
_encode_scalar_to_value_bytes(1.5, pa.float64())),
])
column = GenericVariant.to_arrow_array(
[GenericVariant(value, metadata)] * 100)
result = variant_set(column, '$.flag', pa.scalar(False))
self.assertEqual(
_decode(result), [{'value': 1.5, 'flag': False}] * 100)
self.assertEqual(
result.to_pylist()[0]['metadata'], metadata)
self.assertEqual(
column.field('metadata').buffers()[2].address,
result.field('metadata').buffers()[2].address,
)
def test_insert_into_empty_object(self):
column = _variants([{}])
result = variant_set(column, '$.first', pa.scalar(7, pa.int64()))
self.assertEqual(_decode(result), [{'first': 7}])
def test_inserts_same_key_into_two_objects(self):
column = _variants([{'left': {}, 'right': {}}])
result = variant_set(column, {
'$.left.flag': pa.scalar(True),
'$.right.flag': pa.scalar(True),
})
metadata = result.to_pylist()[0]['metadata']
self.assertEqual(
_metadata_key_ids(metadata), {'left': 0, 'right': 1, 'flag': 2})
self.assertEqual(_decode(result), [
{'left': {'flag': True}, 'right': {'flag': True}}])
self.assertEqual(
variant_get(result, '$.left.flag', pa.bool_()).to_pylist(),
[True])
def test_inserted_fields_stay_sorted_for_java_binary_search(self):
payload = {'k%02d' % index: float(index) for index in range(40)}
column = _variants([payload])
result = variant_set(column, '$.a_mark', pa.scalar('inserted'))
decoded = _decode(result)[0]
expected = dict(payload)
expected['a_mark'] = 'inserted'
self.assertEqual(decoded, expected)
self.assertEqual(list(decoded), sorted(decoded))
def test_inserted_fields_stay_sorted_by_utf8(self):
key_sup = chr(0x10000)
key_bmp = chr(0xE000)
payload = {key_bmp: 1.0, key_sup: 2.0}
payload.update({'k%02d' % i: float(i) for i in range(40)})
result = variant_set(_variants([payload]), '$.aaa', pa.scalar(3.0))
decoded = _decode(result)[0]
self.assertEqual(decoded[key_sup], 2.0)
self.assertEqual(decoded[key_bmp], 1.0)
self.assertEqual(list(decoded), sorted(
list(payload.keys()) + ['aaa'],
key=lambda name: name.encode('utf-8')))
def test_mixed_rows_in_one_chunk(self):
metadata = GenericVariant.from_python({'a': 0, 'b': 0}).metadata()
key_ids = _metadata_key_ids(metadata)
reversed_fields = _build_object_value_ordered([
(key_ids['b'], _encode_scalar_to_value_bytes(2.0, pa.float64())),
(key_ids['a'], _encode_scalar_to_value_bytes(1.0, pa.float64())),
])
column = pa.concat_arrays([
_variants([{'a': 1.0}, {'a': 1.0, 'mark': 'old'}]),
GenericVariant.to_arrow_array(
[GenericVariant(reversed_fields, metadata)]),
])
result = variant_set(column, '$.mark', pa.scalar('new'))
self.assertEqual(_decode(result), [
{'a': 1.0, 'mark': 'new'},
{'a': 1.0, 'mark': 'new'},
{'b': 2.0, 'a': 1.0, 'mark': 'new'},
])
def test_replace_and_insert_multiple_paths(self):
column = _variants(_sensor_rows(10))
current = variant_get(
column, {path: pa.float64() for path in _SENSOR_PATHS})
updates = {
path: pc.negate(values) for path, values in current.items()
}
updates['$.processed'] = pa.scalar(True, type=pa.bool_())
result = variant_set(column, updates)
decoded = _decode(result)
self.assertTrue(all(row['processed'] is True for row in decoded))
self.assertEqual(
[row['angular_velocity']['y'] for row in decoded],
[-float(index) for index in range(10)],
)
def test_scalar_array_and_chunked_replacements(self):
column = pa.chunked_array([
_variants([{'value': 1.0}, {'value': 2.0}]),
_variants([{'value': 3.0}]),
])
result = variant_set(column, {
'$.value': pa.chunked_array(
[[10.0, 20.0], [30.0]], type=pa.float64()),
'$.rank': pa.array([1, 2, 3], type=pa.int64()),
'$.processed': pa.scalar(True),
})
self.assertIsInstance(result, pa.ChunkedArray)
self.assertEqual(result.num_chunks, 2)
self.assertEqual(_decode(result), [
{'value': 10.0, 'rank': 1, 'processed': True},
{'value': 20.0, 'rank': 2, 'processed': True},
{'value': 30.0, 'rank': 3, 'processed': True},
])
class TestVariantSetNullSemantics(unittest.TestCase):
def test_sql_null_rows_stay_null(self):
column = _variants([None, {'value': 1.0}])
result = variant_set(column, '$.processed', pa.scalar(True))
self.assertEqual(_decode(result), [
None, {'value': 1.0, 'processed': True},
])
self.assertTrue(result.is_null()[0].as_py())
def test_arrow_null_becomes_variant_null(self):
column = _variants([{'value': 1.0}, {'value': 2.0}])
result = variant_set(column, {
'$.value': pa.array([None, -2.0], type=pa.float64()),
'$.mark': pa.array([None, 'done'], type=pa.string()),
})
self.assertEqual(_decode(result), [
{'value': None, 'mark': None},
{'value': -2.0, 'mark': 'done'},
])
self.assertEqual(result.null_count, 0)
def test_untyped_arrow_null_becomes_variant_null(self):
replacements = [
pa.scalar(None),
pa.nulls(2),
pa.chunked_array([pa.nulls(1), pa.nulls(1)]),
]
for replacement in replacements:
with self.subTest(replacement=type(replacement).__name__):
result = variant_set(
_variants([{'value': 1.0}, {'value': 2.0}]),
'$.value',
replacement,
)
self.assertEqual(_decode(result), [
{'value': None}, {'value': None},
])
def test_variant_null_parent_is_not_an_object(self):
column = _variants([{'parent': None}])
with self.assertRaisesRegex(ValueError, "is not an object"):
variant_set(column, '$.parent.child', pa.scalar(1.0))
def test_missing_intermediate_parent_fails(self):
column = _variants([{'other': 1.0}] * 100)
with self.assertRaisesRegex(ValueError, "parent path does not"):
variant_set(column, '$.missing.child', pa.scalar(1.0))
def test_non_object_parent_fails(self):
column = _variants([{'value': 1.0}] * 100)
with self.assertRaisesRegex(ValueError, "is not an object"):
variant_set(column, '$.value.child', pa.scalar(1.0))
def test_replaces_array_element_of_a_different_size(self):
column = _variants([{'items': ['aa', 'bb'], 'n': 1.0}])
result = variant_set(column, '$.items[0]', pa.scalar('cccc'))
self.assertEqual(
_decode(result), [{'items': ['cccc', 'bb'], 'n': 1.0}])
def test_array_insertion_is_not_supported(self):
column = _variants([{'items': [1.0]}])
with self.assertRaisesRegex(ValueError, "not supported"):
variant_set(column, '$.items[3]', pa.scalar(1.0))
result = variant_set(column, '$.items[0]', pa.scalar(-1.0))
self.assertEqual(_decode(result), [{'items': [-1.0]}])
class TestVariantSetLayouts(unittest.TestCase):
def test_sliced_input(self):
base = _variants([
{'value': float(index), 'padding': 'x' * 100}
for index in range(100)
])
column = base.slice(50, 3)
result = variant_set(column, {
'$.value': pa.scalar(-1.0),
'$.processed': pa.scalar(True),
})
self.assertEqual(
[(row['value'], row['processed']) for row in _decode(result)],
[(-1.0, True)] * 3,
)
def test_large_binary_input(self):
column = _variants([{'value': 1.0}])
large = pa.StructArray.from_arrays(
[
column.field('value').cast(pa.large_binary()),
column.field('metadata').cast(pa.large_binary()),
],
names=['value', 'metadata'],
)
result = variant_set(large, '$.processed', pa.scalar(True))
self.assertTrue(pa.types.is_large_binary(result.type[0].type))
self.assertTrue(pa.types.is_large_binary(result.type[1].type))
self.assertEqual(
_decode(result), [{'value': 1.0, 'processed': True}])
def test_preserves_chunk_boundaries_without_combine(self):
column = pa.chunked_array([
_variants([{'value': 1.0}]),
_variants([{'value': 2.0}, {'value': 3.0}]),
])
with patch(
'pypaimon.data.variant_path._rebuilt_offsets',
wraps=_rebuilt_offsets,
) as rebuilt_offsets:
result = variant_set(column, '$.processed', pa.scalar(True))
self.assertEqual(
[len(chunk) for chunk in result.chunks],
[len(chunk) for chunk in column.chunks],
)
# Offsets are rebuilt per chunk, never for the combined column.
self.assertTrue(rebuilt_offsets.called)
self.assertEqual(
max(len(call[0][0])
for call in rebuilt_offsets.call_args_list),
2,
)
self.assertEqual(_decode(result), [
{'value': 1.0, 'processed': True},
{'value': 2.0, 'processed': True},
{'value': 3.0, 'processed': True},
])
def test_offset_overflow_guard_is_low_memory(self):
lengths = np.array([(1 << 31) - 8, 16], dtype=np.int64)
with self.assertRaisesRegex(ValueError, "use LargeBinary"):
_rebuilt_offsets(lengths, '<i')
self.assertEqual(
_rebuilt_offsets(lengths, '<q')[-1], (1 << 31) + 8)
def test_input_is_not_modified(self):
column = _variants([{'value': 1.0}, None, {'value': 2.0}])
original_rows = column.to_pylist()
original_value = column.field('value').buffers()[2].to_pybytes()
original_metadata = (
column.field('metadata').buffers()[2].to_pybytes())
variant_set(column, {
'$.value': pa.scalar(-1.0),
'$.processed': pa.scalar(True),
})
self.assertEqual(column.to_pylist(), original_rows)
self.assertEqual(
column.field('value').buffers()[2].to_pybytes(),
original_value,
)
self.assertEqual(
column.field('metadata').buffers()[2].to_pybytes(),
original_metadata,
)
class TestVariantSetFastPaths(unittest.TestCase):
def test_replace_avoids_full_decode(self):
column = _variants([{'value': float(index)} for index in range(100)])
with patch.object(
GenericVariant, 'to_python',
side_effect=AssertionError("full decode is not allowed")), \
patch.object(
GenericVariant, 'from_python',
side_effect=AssertionError(
"full encode is not allowed")):
result = variant_set(column, '$.value', pa.scalar(-1.0))
self.assertEqual(
variant_get(result, '$.value', pa.float64()).to_pylist(),
[-1.0] * 100,
)
def test_insert_avoids_full_decode(self):
column = _variants([{'value': float(index)} for index in range(100)])
with patch.object(
GenericVariant, 'to_python',
side_effect=AssertionError("full decode is not allowed")), \
patch.object(
GenericVariant, 'from_python',
side_effect=AssertionError(
"full encode is not allowed")):
result = variant_set(column, '$.processed', pa.scalar(True))
self.assertEqual(
variant_get(result, '$.processed', pa.bool_()).to_pylist(),
[True] * 100,
)
def test_replace_fast_path_stays_vectorized(self):
column = _variants(
[{'value': float(index)} for index in range(4096)])
with patch(
'pypaimon.data.variant_path._path_positions',
wraps=_path_positions,
) as slow_path, patch(
'pypaimon.data.variant_path._apply_edits',
wraps=_apply_edits,
) as rebuild:
result = variant_set(column, '$.value', pa.scalar(-1.0))
slow_path.assert_not_called()
rebuild.assert_not_called()
self.assertEqual(
variant_get(result, '$.value', pa.float64()).to_pylist(),
[-1.0] * 4096,
)
def test_insert_avoids_per_row_planning(self):
column = _variants(
[{'value': float(index)} for index in range(4096)])
with patch(
'pypaimon.data.variant_path._path_positions',
wraps=_path_positions,
) as slow_path, patch(
'pypaimon.data.variant_path._metadata_key_ids',
wraps=_metadata_key_ids,
) as metadata_parse:
result = variant_set(column, '$.processed', pa.scalar(True))
slow_path.assert_not_called()
self.assertLessEqual(metadata_parse.call_count, 2)
self.assertEqual(
variant_get(result, '$.processed', pa.bool_()).to_pylist(),
[True] * 4096,
)
def test_insert_fuses_root_validation_with_rebuild(self):
column = _variants([
{'nested': {'value': float(index)}, 'other': float(index)}
for index in range(100)
])
with patch(
'pypaimon.data.variant_path._validate_value_field_ids',
wraps=_validate_value_field_ids,
) as subtree_validation:
result = variant_set(column, '$.processed', pa.scalar(True))
self.assertFalse(any(
args[1] == 0
for args, _ in subtree_validation.call_args_list
))
self.assertEqual(
variant_get(result, '$.processed', pa.bool_()).to_pylist(),
[True] * 100,
)
def test_insert_validates_deep_unmodified_sibling_iteratively(self):
metadata = GenericVariant.from_python(
{'sibling': [], 'target': {}}).metadata()
key_ids = _metadata_key_ids(metadata)
sibling = _encode_scalar_to_value_bytes(1.0, pa.float64())
for _ in range(1020):
sibling = _build_array_value([sibling])
root = _build_object_value([
(key_ids['sibling'], sibling),
(key_ids['target'], _build_object_value([])),
])
column = GenericVariant.to_arrow_array([
GenericVariant(root, metadata),
])
result = variant_set(column, '$.target.new', pa.scalar(True))
self.assertEqual(
variant_get(result, '$.target.new', pa.bool_()).to_pylist(),
[True],
)
def test_insert_rebuilds_deep_modified_path_iteratively(self):
metadata = GenericVariant.from_python({'target': {}}).metadata()
key_ids = _metadata_key_ids(metadata)
target = _build_object_value([])
for _ in range(1020):
target = _build_array_value([target])
root = _build_object_value([
(key_ids['target'], target),
])
column = GenericVariant.to_arrow_array([
GenericVariant(root, metadata),
])
path = '$.target' + '[0]' * 1020 + '.new'
with patch(
'pypaimon.data.variant_path._materialize_value',
wraps=_materialize_value,
) as materialize:
result = variant_set(column, path, pa.scalar(True))
self.assertEqual(materialize.call_count, 1)
self.assertEqual(
variant_get(result, path, pa.bool_()).to_pylist(),
[True],
)
def test_insert_offset_width_boundary_mixed_rows(self):
# Rows crossing the 1-byte offset limit after the insert must be
# rebuilt with a wider offset table inside the same plan group.
rows = []
for index in range(100):
padding = 'x' * (240 if index % 3 == 0 else 10)
rows.append({'value': float(index), 'padding': padding})
column = _variants(rows)
mark = 'm' * 30
result = variant_set(column, '$.mark', pa.scalar(mark))
for index, decoded in enumerate(_decode(result)):
self.assertEqual(decoded, {
'value': float(index),
'padding': rows[index]['padding'],
'mark': mark,
})
class TestVariantSetErrors(unittest.TestCase):
def test_variant_size_limit_boundary(self):
with patch('pypaimon.data.generic_variant._SIZE_LIMIT', 64):
_check_variant_sizes(64, 64)
with self.assertRaisesRegex(
ValueError, 'VARIANT_CONSTRUCTOR_SIZE_LIMIT'):
_check_variant_sizes(65, 64)
with self.assertRaisesRegex(
ValueError, 'VARIANT_CONSTRUCTOR_SIZE_LIMIT'):
_check_variant_sizes(64, 65)
def test_rejects_oversized_value_and_metadata(self):
column = _variants([{'value': 'a'}])
with patch('pypaimon.data.generic_variant._SIZE_LIMIT', 64):
with self.assertRaisesRegex(
ValueError, 'VARIANT_CONSTRUCTOR_SIZE_LIMIT'):
variant_set(column, '$.value', pa.scalar('x' * 128))
with self.assertRaisesRegex(
ValueError, 'VARIANT_CONSTRUCTOR_SIZE_LIMIT'):
variant_set(column, '$.' + 'k' * 128, pa.scalar(True))
def test_rejects_type_and_length_mismatches(self):
column = _variants([{'value': 1.0}, {'value': 2.0}])
cases = [
('$.value', pa.scalar('text'), TypeError, "does not match"),
('$.value', pa.scalar(1.0, type=pa.float32()),
TypeError, "does not match"),
('$.value', pa.array([1.0]), ValueError, "length must match"),
('$.value', 1.0, TypeError, "Arrow Scalar or Array"),
('value', pa.scalar(1.0), ValueError, "Invalid VARIANT path"),
]
for path, replacement, error_type, message in cases:
with self.subTest(path=path):
with self.assertRaisesRegex(error_type, message):
variant_set(column, path, replacement)
with self.assertRaisesRegex(TypeError, "must be omitted"):
variant_set(
column, {'$.value': pa.scalar(1.0)}, pa.scalar(2.0))
def test_rejects_duplicate_and_overlapping_paths(self):
column = _variants([{'x': {'y': 1.0}}])
with self.assertRaisesRegex(ValueError, "must not overlap"):
variant_set(column, {
'$.x': pa.scalar(1.0),
"$['x']": pa.scalar(2.0),
})
with self.assertRaisesRegex(ValueError, "must not overlap"):
variant_set(column, {
'$.x': pa.scalar(1.0),
'$.x.y': pa.scalar(2.0),
})
def test_rejects_malformed_metadata(self):
valid = GenericVariant.from_python({'value': 1.0})
column = pa.StructArray.from_arrays(
[
pa.array([valid.value()]),
pa.array([valid.metadata()[:-2]]),
],
names=['value', 'metadata'],
)
with self.assertRaisesRegex(ValueError, "MALFORMED_VARIANT"):
variant_set(column, '$.processed', pa.scalar(True))
def test_rejects_unknown_field_id_on_insert(self):
metadata = GenericVariant.from_python({'value': 0}).metadata()
orphan = _build_object_value([
(7, _encode_scalar_to_value_bytes(1.0, pa.float64())),
])
column = GenericVariant.to_arrow_array(
[GenericVariant(orphan, metadata)])
with self.assertRaisesRegex(ValueError, "MALFORMED_VARIANT"):
variant_set(column, '$.processed', pa.scalar(True))
def test_rejects_field_id_colliding_with_inserted_key(self):
# 'processed' will be assigned id 1; a corrupt source already using
# id 1 must be rejected rather than silently producing a duplicate.
metadata = GenericVariant.from_python({'value': 0}).metadata()
corrupt = _build_object_value([
(1, _encode_scalar_to_value_bytes(2.0, pa.float64())),
])
column = GenericVariant.to_arrow_array(
[GenericVariant(corrupt, metadata)])
with self.assertRaisesRegex(ValueError, "MALFORMED_VARIANT"):
variant_set(column, '$.processed', pa.scalar(True))
def test_rejects_nested_insert_exposing_invalid_sibling_field_id(self):
metadata = GenericVariant.from_python(
{'a': 0, 'b': 0, 'child': {}, 'sibling': {}}).metadata()
key_ids = _metadata_key_ids(metadata)
corrupt_sibling = _build_object_value([
(
len(key_ids),
_encode_scalar_to_value_bytes(2.0, pa.float64()),
),
])
corrupt_root = _build_object_value([
(key_ids['child'], _build_object_value([])),
(key_ids['sibling'], corrupt_sibling),
])
column = GenericVariant.to_arrow_array([
GenericVariant(corrupt_root, metadata),
])
with self.assertRaisesRegex(ValueError, "MALFORMED_VARIANT"):
variant_set(column, '$.child.new', pa.scalar(True))
def test_rejects_duplicate_source_field_id(self):
metadata = GenericVariant.from_python({'value': 0}).metadata()
corrupt = _build_object_value([
(0, _encode_scalar_to_value_bytes(1.0, pa.float64())),
(0, _encode_scalar_to_value_bytes(2.0, pa.float64())),
])
column = GenericVariant.to_arrow_array(
[GenericVariant(corrupt, metadata)])
with self.assertRaisesRegex(ValueError, "MALFORMED_VARIANT"):
variant_set(column, '$.value', pa.scalar(9.0))
def test_rejects_duplicate_source_field_id_in_peer_row(self):
metadata = GenericVariant.from_python({'a': 0, 'b': 0}).metadata()
duplicate = _build_object_value([
(0, _encode_scalar_to_value_bytes(1.0, pa.float64())),
(0, _encode_scalar_to_value_bytes(2.0, pa.float64())),
])
column = GenericVariant.to_arrow_array([
GenericVariant.from_python({'a': 1.0, 'b': 2.0}),
GenericVariant(duplicate, metadata),
])
for updater in (variant_replace, variant_set):
with self.subTest(updater=updater.__name__):
with self.assertRaisesRegex(
ValueError, "MALFORMED_VARIANT"):
updater(column, '$.a', pa.scalar(9.0))
def test_rejects_truncated_child_offsets(self):
valid = GenericVariant.from_python({'a': 1.0, 'b': 2.0})
truncated = _build_object_value([
(0, _encode_scalar_to_value_bytes(1.0, pa.float64())[:-2]),
(1, _encode_scalar_to_value_bytes(2.0, pa.float64())),
])
column = GenericVariant.to_arrow_array(
[GenericVariant(truncated, valid.metadata())])
original = column.to_pylist()
with self.assertRaisesRegex(ValueError, "MALFORMED_VARIANT"):
variant_set(column, '$.c', pa.scalar(True))
self.assertEqual(column.to_pylist(), original)
class TestVariantSetJavaInterop(unittest.TestCase):
def test_from_python_orders_object_fields_by_utf8(self):
key_sup = chr(0x10000)
key_bmp = chr(0xE000)
payload = {'k%02d' % i: float(i) for i in range(40)}
payload[key_sup] = 1.0
payload[key_bmp] = 2.0
variant = GenericVariant.from_python(payload)
order = list(variant.to_python().keys())
expected = sorted(
list(payload.keys()),
key=lambda name: name.encode('utf-8'))
self.assertEqual(order, expected)
def test_reads_java_generated_variant(self):
column = GenericVariant.to_arrow_array(
[GenericVariant(_JAVA_VALUE, _JAVA_METADATA)])
self.assertEqual(_decode(column), [_JAVA_PYTHON_VALUE])
self.assertEqual(
variant_get(
column, '$.angular_velocity.y', pa.float64()).to_pylist(),
[1.5],
)
def test_updates_java_generated_variant(self):
column = GenericVariant.to_arrow_array(
[GenericVariant(_JAVA_VALUE, _JAVA_METADATA)])
result = variant_set(column, {
'$.angular_velocity.y': pa.scalar(-1.5, type=pa.float64()),
'$.processed': pa.scalar(False),
'$.mark': pa.scalar('py'),
})
expected = {
'angular_velocity': {'y': -1.5, 'z': -2.5},
'linear_acceleration': {'y': 0.25, 'z': 4.0},
'processed': False,
'seq': 7,
'mark': 'py',
}
decoded = _decode(result)[0]
self.assertEqual(decoded, expected)
self.assertEqual(list(decoded), sorted(decoded))
def test_produces_java_equivalent_encoding(self):
# This update was verified to round-trip through the Java
# GenericVariant reader (toJson/getFieldByKey, incl. binary search).
column = _variants([{
'angular_velocity': {'y': -1.5, 'z': 2.5},
'linear_acceleration': {'y': -0.25, 'z': -4.0},
'seq': 7,
}])
result = variant_set(column, {
'$.angular_velocity.y': pa.scalar(1.5, type=pa.float64()),
'$.angular_velocity.z': pa.scalar(-2.5, type=pa.float64()),
'$.linear_acceleration.y': pa.scalar(0.25, type=pa.float64()),
'$.linear_acceleration.z': pa.scalar(4.0, type=pa.float64()),
'$.processed': pa.scalar(True, type=pa.bool_()),
})
decoded = _decode(result)[0]
self.assertEqual(decoded, _JAVA_PYTHON_VALUE)
java_decoded = GenericVariant(
_JAVA_VALUE, _JAVA_METADATA).to_python()
self.assertEqual(decoded, java_decoded)
self.assertEqual(list(decoded), sorted(decoded))
class TestMetadataWithKeys(unittest.TestCase):
def test_reuses_existing_keys(self):
metadata = GenericVariant.from_python({'a': 0, 'b': 0}).metadata()
new_metadata, key_ids, names_by_id = _metadata_with_keys(
metadata, ('b',))
self.assertIsNone(new_metadata)
self.assertEqual(key_ids, {'a': 0, 'b': 1})
self.assertEqual(names_by_id, {0: 'a', 1: 'b'})
def test_appends_missing_keys(self):
metadata = GenericVariant.from_python({'a': 0}).metadata()
new_metadata, key_ids, names_by_id = _metadata_with_keys(
metadata, ('b', 'c'))
self.assertEqual(key_ids, {'a': 0, 'b': 1, 'c': 2})
self.assertEqual(names_by_id, {0: 'a', 1: 'b', 2: 'c'})
self.assertEqual(
_metadata_key_ids(new_metadata), {'a': 0, 'b': 1, 'c': 2})
if __name__ == '__main__':
unittest.main()