| # 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. | |
| """Key serializer for global indexes.""" | |
| from abc import ABC, abstractmethod | |
| import datetime | |
| from decimal import Context, Decimal, ROUND_HALF_UP | |
| import math | |
| import re | |
| from typing import Callable, Tuple | |
| import struct | |
| from pypaimon.schema.data_types import AtomicType | |
| from pypaimon.schema.data_types import DataType | |
| class KeySerializer(ABC): | |
| """Interface for serializing, deserializing, and comparing global index keys.""" | |
| @abstractmethod | |
| def serialize(self, key: object) -> bytes: | |
| """Serialize a key to bytes.""" | |
| pass | |
| @abstractmethod | |
| def deserialize(self, data: bytes) -> object: | |
| """Deserialize bytes to a key.""" | |
| pass | |
| @abstractmethod | |
| def create_comparator(self) -> Callable[[object, object], int]: | |
| """Create a comparator function for keys.""" | |
| pass | |
| class StringSerializer(KeySerializer): | |
| """Serializer for STRING type.""" | |
| def serialize(self, key: object) -> bytes: | |
| if isinstance(key, str): | |
| return key.encode('utf-8') | |
| return str(key).encode('utf-8') | |
| def deserialize(self, data: bytes) -> object: | |
| return data.decode('utf-8') | |
| def create_comparator(self) -> Callable[[object, object], int]: | |
| def compare(a: object, b: object) -> int: | |
| str_a = a if isinstance(a, str) else str(a) | |
| str_b = b if isinstance(b, str) else str(b) | |
| if str_a < str_b: | |
| return -1 | |
| if str_a > str_b: | |
| return 1 | |
| return 0 | |
| return compare | |
| class LongSerializer(KeySerializer): | |
| """Serializer for BIGINT type.""" | |
| def serialize(self, key: object) -> bytes: | |
| return struct.pack('<q', int(key)) | |
| def deserialize(self, data: bytes) -> object: | |
| return struct.unpack('<q', data)[0] | |
| def create_comparator(self) -> Callable[[object, object], int]: | |
| def compare(a: object, b: object) -> int: | |
| long_a = int(a) | |
| long_b = int(b) | |
| if long_a < long_b: | |
| return -1 | |
| if long_a > long_b: | |
| return 1 | |
| return 0 | |
| return compare | |
| class ByteSerializer(KeySerializer): | |
| """Serializer for TINYINT type.""" | |
| def serialize(self, key: object) -> bytes: | |
| return struct.pack('<b', int(key)) | |
| def deserialize(self, data: bytes) -> object: | |
| return struct.unpack('<b', data)[0] | |
| def create_comparator(self) -> Callable[[object, object], int]: | |
| return _numeric_compare | |
| class ShortSerializer(KeySerializer): | |
| """Serializer for SMALLINT type.""" | |
| def serialize(self, key: object) -> bytes: | |
| return struct.pack('<h', int(key)) | |
| def deserialize(self, data: bytes) -> object: | |
| return struct.unpack('<h', data)[0] | |
| def create_comparator(self) -> Callable[[object, object], int]: | |
| return _numeric_compare | |
| class IntSerializer(KeySerializer): | |
| """Serializer for INT type.""" | |
| def serialize(self, key: object) -> bytes: | |
| return struct.pack('<i', int(key)) | |
| def deserialize(self, data: bytes) -> object: | |
| return struct.unpack('<i', data)[0] | |
| def create_comparator(self) -> Callable[[object, object], int]: | |
| def compare(a: object, b: object) -> int: | |
| int_a = int(a) | |
| int_b = int(b) | |
| if int_a < int_b: | |
| return -1 | |
| if int_a > int_b: | |
| return 1 | |
| return 0 | |
| return compare | |
| class BooleanSerializer(KeySerializer): | |
| """Serializer for BOOLEAN type.""" | |
| def serialize(self, key: object) -> bytes: | |
| return b'\x01' if bool(key) else b'\x00' | |
| def deserialize(self, data: bytes) -> object: | |
| return data[0] == 1 | |
| def create_comparator(self) -> Callable[[object, object], int]: | |
| def compare(a: object, b: object) -> int: | |
| return _cmp(bool(a), bool(b)) | |
| return compare | |
| class FloatSerializer(KeySerializer): | |
| """Serializer for FLOAT type.""" | |
| def serialize(self, key: object) -> bytes: | |
| return struct.pack('<f', float(key)) | |
| def deserialize(self, data: bytes) -> object: | |
| return struct.unpack('<f', data)[0] | |
| def create_comparator(self) -> Callable[[object, object], int]: | |
| return _float_compare | |
| class DoubleSerializer(KeySerializer): | |
| """Serializer for DOUBLE type.""" | |
| def serialize(self, key: object) -> bytes: | |
| return struct.pack('<d', float(key)) | |
| def deserialize(self, data: bytes) -> object: | |
| return struct.unpack('<d', data)[0] | |
| def create_comparator(self) -> Callable[[object, object], int]: | |
| return _float_compare | |
| class DecimalSerializer(KeySerializer): | |
| """Serializer for DECIMAL type.""" | |
| def __init__(self, precision: int, scale: int): | |
| self._precision = precision | |
| self._scale = scale | |
| def serialize(self, key: object) -> bytes: | |
| unscaled = _decimal_unscaled(key, self._scale) | |
| if self._precision <= 18: | |
| return struct.pack('<q', unscaled) | |
| return _signed_big_endian_bytes(unscaled) | |
| def deserialize(self, data: bytes) -> object: | |
| if self._precision <= 18: | |
| unscaled = struct.unpack('<q', data)[0] | |
| else: | |
| unscaled = int.from_bytes(data, byteorder='big', signed=True) | |
| sign, digits, _ = Decimal(unscaled).as_tuple() | |
| return Decimal((sign, digits, -self._scale)) | |
| def create_comparator(self) -> Callable[[object, object], int]: | |
| def compare(a: object, b: object) -> int: | |
| return _cmp(Decimal(a), Decimal(b)) | |
| return compare | |
| class DateSerializer(IntSerializer): | |
| """Serializer for DATE type.""" | |
| def serialize(self, key: object) -> bytes: | |
| return struct.pack('<i', _date_to_epoch_days(key)) | |
| def deserialize(self, data: bytes) -> object: | |
| days = struct.unpack('<i', data)[0] | |
| return datetime.date(1970, 1, 1) + datetime.timedelta(days=days) | |
| def create_comparator(self) -> Callable[[object, object], int]: | |
| def compare(a: object, b: object) -> int: | |
| return _cmp(_date_to_epoch_days(a), _date_to_epoch_days(b)) | |
| return compare | |
| class TimeSerializer(IntSerializer): | |
| """Serializer for TIME type.""" | |
| def serialize(self, key: object) -> bytes: | |
| return struct.pack('<i', _time_to_millis(key)) | |
| def deserialize(self, data: bytes) -> object: | |
| millis = struct.unpack('<i', data)[0] | |
| seconds, millis = divmod(millis, 1000) | |
| minutes, second = divmod(seconds, 60) | |
| hour, minute = divmod(minutes, 60) | |
| return datetime.time(hour, minute, second, millis * 1000) | |
| def create_comparator(self) -> Callable[[object, object], int]: | |
| def compare(a: object, b: object) -> int: | |
| return _cmp(_time_to_millis(a), _time_to_millis(b)) | |
| return compare | |
| class TimestampSerializer(KeySerializer): | |
| """Serializer for TIMESTAMP and TIMESTAMP WITH LOCAL TIME ZONE types.""" | |
| def __init__(self, precision: int, local_time_zone: bool = False): | |
| self._precision = precision | |
| self._local_time_zone = local_time_zone | |
| def serialize(self, key: object) -> bytes: | |
| millis, nano_of_millisecond = _timestamp_to_millis_nanos( | |
| key, self._precision, self._local_time_zone) | |
| if self._precision <= 3: | |
| return struct.pack('<q', millis) | |
| return struct.pack('<q', millis) + _write_var_len_int(nano_of_millisecond) | |
| def deserialize(self, data: bytes) -> object: | |
| millis = struct.unpack('<q', data[:8])[0] | |
| nano_of_millisecond = 0 | |
| if self._precision > 3 and len(data) > 8: | |
| nano_of_millisecond, _ = _read_var_len_int(data, 8) | |
| total_micros = millis * 1000 + nano_of_millisecond // 1000 | |
| return ( | |
| datetime.datetime(1970, 1, 1) | |
| + datetime.timedelta(microseconds=total_micros) | |
| ) | |
| def create_comparator(self) -> Callable[[object, object], int]: | |
| def compare(a: object, b: object) -> int: | |
| return _cmp( | |
| _timestamp_to_millis_nanos(a, self._precision, self._local_time_zone), | |
| _timestamp_to_millis_nanos(b, self._precision, self._local_time_zone), | |
| ) | |
| return compare | |
| def _cmp(a, b) -> int: | |
| if a < b: | |
| return -1 | |
| if a > b: | |
| return 1 | |
| return 0 | |
| def _numeric_compare(a: object, b: object) -> int: | |
| return _cmp(int(a), int(b)) | |
| def _float_compare(a: object, b: object) -> int: | |
| left = float(a) | |
| right = float(b) | |
| left_nan = math.isnan(left) | |
| right_nan = math.isnan(right) | |
| if left_nan or right_nan: | |
| if left_nan and right_nan: | |
| return 0 | |
| return 1 if left_nan else -1 | |
| return _cmp(left, right) | |
| def _parse_decimal_params(type_name: str) -> Tuple[int, int]: | |
| if type_name in ("DECIMAL", "NUMERIC"): | |
| return 10, 0 | |
| match = re.fullmatch(r'(?:DECIMAL|NUMERIC)\((\d+),\s*(\d+)\)', type_name) | |
| if match: | |
| return int(match.group(1)), int(match.group(2)) | |
| match = re.fullmatch(r'(?:DECIMAL|NUMERIC)\((\d+)\)', type_name) | |
| if match: | |
| return int(match.group(1)), 0 | |
| raise ValueError(f"Invalid decimal type: {type_name}") | |
| def _parse_precision(type_name: str, default: int) -> int: | |
| match = re.search(r'\((\d+)\)', type_name) | |
| return int(match.group(1)) if match else default | |
| def _decimal_unscaled(value: object, scale: int) -> int: | |
| decimal_value = value if isinstance(value, Decimal) else Decimal(str(value)) | |
| # Index bytes must not depend on the caller's decimal precision or traps. | |
| # Reserve one extra digit for a carry when rounding to the declared scale. | |
| context = Context(prec=max(1, decimal_value.adjusted() + scale + 2)) | |
| quant = Decimal((0, (1,), -scale)) | |
| rounded = decimal_value.quantize(quant, rounding=ROUND_HALF_UP, context=context) | |
| return int(rounded.scaleb(scale, context=context)) | |
| def _signed_big_endian_bytes(value: int) -> bytes: | |
| length = max(1, (value.bit_length() + 8) // 8) | |
| while True: | |
| try: | |
| encoded = value.to_bytes(length, byteorder='big', signed=True) | |
| break | |
| except OverflowError: | |
| length += 1 | |
| while len(encoded) > 1: | |
| if encoded[0] == 0x00 and encoded[1] < 0x80: | |
| encoded = encoded[1:] | |
| elif encoded[0] == 0xFF and encoded[1] >= 0x80: | |
| encoded = encoded[1:] | |
| else: | |
| break | |
| return encoded | |
| def _date_to_epoch_days(value: object) -> int: | |
| if isinstance(value, datetime.datetime): | |
| value = value.date() | |
| if isinstance(value, datetime.date): | |
| return (value - datetime.date(1970, 1, 1)).days | |
| return int(value) | |
| def _time_to_millis(value: object) -> int: | |
| if isinstance(value, datetime.time): | |
| return ( | |
| ((value.hour * 60 + value.minute) * 60 + value.second) * 1000 | |
| + value.microsecond // 1000 | |
| ) | |
| return int(value) | |
| def _timestamp_to_millis_nanos( | |
| value: object, precision: int = 6, local_time_zone: bool = False | |
| ) -> Tuple[int, int]: | |
| if hasattr(value, "get_millisecond") and hasattr(value, "get_nano_of_millisecond"): | |
| return int(value.get_millisecond()), int(value.get_nano_of_millisecond()) | |
| if isinstance(value, datetime.datetime): | |
| # LTZ keys represent instants; ordinary TIMESTAMP keys retain wall time. | |
| # Naive LTZ values (including deserialized keys) already represent UTC. | |
| if local_time_zone and value.utcoffset() is not None: | |
| value = value.astimezone(datetime.timezone.utc) | |
| delta = value.replace(tzinfo=None) - datetime.datetime(1970, 1, 1) | |
| total_micros = ( | |
| delta.days * 86_400_000_000 | |
| + delta.seconds * 1_000_000 | |
| + delta.microseconds | |
| ) | |
| millis, micros_in_milli = divmod(total_micros, 1000) | |
| return millis, micros_in_milli * 1000 | |
| if precision <= 3: | |
| return int(value), 0 | |
| micros = int(value) | |
| millis, micros_in_milli = divmod(micros, 1000) | |
| return millis, micros_in_milli * 1000 | |
| def _write_var_len_int(value: int) -> bytes: | |
| if value < 0: | |
| raise ValueError("negative value: v=%s" % value) | |
| result = bytearray() | |
| while value & ~0x7F: | |
| result.append((value & 0x7F) | 0x80) | |
| value >>= 7 | |
| result.append(value) | |
| return bytes(result) | |
| def _read_var_len_int(data: bytes, offset: int = 0): | |
| shift = 0 | |
| result = 0 | |
| while True: | |
| b = data[offset] | |
| offset += 1 | |
| result |= (b & 0x7F) << shift | |
| if (b & 0x80) == 0: | |
| return result, offset | |
| shift += 7 | |
| def create_serializer(data_type: DataType) -> KeySerializer: | |
| if not isinstance(data_type, AtomicType): | |
| raise ValueError( | |
| f"Key serializer only support AtomicType yet, meet {data_type.__class__}") | |
| type_name = data_type.type.upper() | |
| if type_name == 'BOOLEAN': | |
| return BooleanSerializer() | |
| if type_name == 'TINYINT': | |
| return ByteSerializer() | |
| if type_name == 'SMALLINT': | |
| return ShortSerializer() | |
| if type_name in ('CHAR', 'VARCHAR', 'STRING') or type_name.startswith('CHAR') or type_name.startswith('VARCHAR'): | |
| return StringSerializer() | |
| if type_name in ('INT', 'INTEGER'): | |
| return IntSerializer() | |
| if type_name == 'BIGINT': | |
| return LongSerializer() | |
| if type_name == 'FLOAT': | |
| return FloatSerializer() | |
| if type_name == 'DOUBLE': | |
| return DoubleSerializer() | |
| if type_name.startswith('DECIMAL') or type_name.startswith('NUMERIC'): | |
| precision, scale = _parse_decimal_params(type_name) | |
| return DecimalSerializer(precision, scale) | |
| if type_name == 'DATE': | |
| return DateSerializer() | |
| if type_name.startswith('TIME') and not type_name.startswith('TIMESTAMP'): | |
| return TimeSerializer() | |
| if type_name.startswith('TIMESTAMP'): | |
| is_ltz = type_name.startswith('TIMESTAMP_LTZ') or 'WITH LOCAL TIME ZONE' in type_name | |
| return TimestampSerializer(_parse_precision(type_name, 6), local_time_zone=is_ltz) | |
| raise ValueError(f"DataType: {data_type} is not supported by global index now.") |