blob: 49b0f27356ce89a048cc751013a2b667d29d88e6 [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.
"""Key serializer for global indexes."""
from abc import ABC, abstractmethod
import datetime
from decimal import 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)
return Decimal(unscaled).scaleb(-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):
self._precision = precision
def serialize(self, key: object) -> bytes:
millis, nano_of_millisecond = _timestamp_to_millis_nanos(
key, self._precision)
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),
_timestamp_to_millis_nanos(b, self._precision),
)
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))
quant = Decimal(1).scaleb(-scale)
rounded = decimal_value.quantize(quant, rounding=ROUND_HALF_UP)
return int(rounded.scaleb(scale))
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
) -> 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):
if value.tzinfo is None:
epoch = datetime.datetime(1970, 1, 1)
else:
epoch = datetime.datetime(1970, 1, 1, tzinfo=value.tzinfo)
delta = value - epoch
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'):
return TimestampSerializer(_parse_precision(type_name, 6))
raise ValueError(f"DataType: {data_type} is not supported by global index now.")