blob: fd8012bba8e41342ffa3e50735001b25cf6026f8 [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.
"""Regression tests for timestamp global index keys with time zones."""
import datetime
import os
import tempfile
import unittest
from parameterized import parameterized
from pypaimon.filesystem.local_file_io import LocalFileIO
from pypaimon.execution.python.globalindex.bitmap.bitmap_index_reader import BitmapIndexReader
from pypaimon.execution.python.globalindex.bitmap.bitmap_index_writer import BitmapIndexWriter
from pypaimon.execution.python.globalindex.btree.btree_index_reader import BTreeIndexReader
from pypaimon.execution.python.globalindex.btree.btree_index_writer import BTreeIndexWriter
from pypaimon.globalindex.global_index_meta import GlobalIndexIOMeta
from pypaimon.execution.python.globalindex.key_serializer import create_serializer
from pypaimon.schema.data_types import AtomicType
class TimestampKeySerializerTest(unittest.TestCase):
@parameterized.expand([
('TIMESTAMP_LTZ(3)', 123000),
('TIMESTAMP_LTZ(6)', 123456),
('TIMESTAMP(3) WITH LOCAL TIME ZONE', 123000),
('TIMESTAMP(6) WITH LOCAL TIME ZONE', 123456),
])
def test_same_instant_has_identical_key(self, type_name, microsecond):
serializer = create_serializer(AtomicType(type_name))
compare = serializer.create_comparator()
for year in (1969, 2026):
utc = datetime.datetime(year, 1, 1, microsecond=microsecond, tzinfo=datetime.timezone.utc)
expected = serializer.serialize(utc)
for hours in (-7, 0, 8):
offset = datetime.timezone(datetime.timedelta(hours=hours))
local = utc.astimezone(offset)
self.assertEqual(expected, serializer.serialize(local))
self.assertEqual(0, compare(utc, local))
self.assertEqual(utc.replace(tzinfo=None), serializer.deserialize(expected))
self.assertEqual(expected, serializer.serialize(serializer.deserialize(expected)))
earlier = utc.replace(tzinfo=datetime.timezone(datetime.timedelta(hours=8)))
self.assertLess(compare(earlier, utc), 0)
def test_timestamp_without_zone_preserves_wall_time(self):
serializer = create_serializer(AtomicType('TIMESTAMP(6)'))
naive = datetime.datetime(2026, 1, 1, 8, 0, 0, 123456)
aware = naive.replace(tzinfo=datetime.timezone(datetime.timedelta(hours=8)))
self.assertEqual(serializer.serialize(naive), serializer.serialize(aware))
self.assertEqual(0, serializer.create_comparator()(naive, aware))
self.assertEqual(naive, serializer.deserialize(serializer.serialize(aware)))
@parameterized.expand([
('btree', BTreeIndexWriter, BTreeIndexReader),
('bitmap', BitmapIndexWriter, BitmapIndexReader),
])
def test_index_queries_accept_equivalent_time_zones(self, name, writer_class, reader_class):
serializer = create_serializer(AtomicType('TIMESTAMP_LTZ(6)'))
utc = datetime.datetime(2026, 1, 1, microsecond=123456, tzinfo=datetime.timezone.utc)
offset = datetime.timezone(datetime.timedelta(hours=8))
local = utc.astimezone(offset)
with tempfile.TemporaryDirectory() as directory:
file_io = LocalFileIO()
writer = writer_class(file_io, directory, serializer)
# UTC is the representation used by Arrow for LTZ columns and by
# existing indexes. Query literals may carry a different offset.
for row_id in range(3):
writer.write(utc + datetime.timedelta(seconds=row_id - 1), row_id)
entry = writer.finish()[0]
reader = reader_class(serializer, file_io, directory, GlobalIndexIOMeta(
file_name=entry.file_name,
file_size=os.path.getsize(os.path.join(directory, entry.file_name)),
metadata=entry.meta,
))
try:
self.assertEqual([1], reader.visit_equal(local).results().to_list())
self.assertEqual([1], reader.visit_in([local]).results().to_list())
self.assertEqual([0], reader.visit_less_than(local).results().to_list())
self.assertEqual([2], reader.visit_greater_than(local).results().to_list())
finally:
reader.close()