/
githubmirror
/
spark
Обзор
Документация
Войти
/
githubmirror
/
spark
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
python/pyspark/sql/variant_utils.py
821 строка
36 KB
Ruifeng Zheng
[SPARK-56329][PYTHON] Fix all E721 type comparison violations
03 апр 2026, 13:19
Не верифицирован
03 апр 2026, 13:19
5f5fc89
Код
Авторство
О чём код?
# # 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 decimal import datetime import json import struct from array import array from typing import Any, Callable, Dict, List, NamedTuple, Tuple from pyspark.errors import PySparkValueError from zoneinfo import ZoneInfo class VariantUtils: """ A utility class for VariantVal. Adapted from library at: org.apache.spark.types.variant.VariantUtil """ BASIC_TYPE_BITS = 2 BASIC_TYPE_MASK = 0x3 TYPE_INFO_MASK = 0x3F # The inclusive maximum value of the type info value. It is the size limit of `SHORT_STR`. MAX_SHORT_STR_SIZE = 0x3F # Below is all possible basic type values. # Primitive value. The type info value must be one of the values in the below section. PRIMITIVE = 0 # Short string value. The type info value is the string size, which must be in `[0, # MAX_SHORT_STR_SIZE]`. # The string content bytes directly follow the header byte. SHORT_STR = 1 # Object value. The content contains a size, a list of field ids, a list of field offsets, and # the actual field data. The length of the id list is `size`, while the length of the offset # list is `size + 1`, where the last offset represent the total size of the field data. The # fields in an object must be sorted by the field name in alphabetical order. Duplicate field # names in one object are not allowed. # We use 5 bits in the type info to specify the integer type of the object header: it should # be 0_b4_b3b2_b1b0 (MSB is 0), where: # - b4 specifies the type of size. When it is 0/1, `size` is a little-endian 1/4-byte # unsigned integer. # - b3b2/b1b0 specifies the integer type of id and offset. When the 2 bits are 0/1/2, the # list contains 1/2/3-byte little-endian unsigned integers. OBJECT = 2 # Array value. The content contains a size, a list of field offsets, and the actual element # data. It is similar to an object without the id list. The length of the offset list # is `size + 1`, where the last offset represent the total size of the element data. # Its type info should be: 000_b2_b1b0: # - b2 specifies the type of size. # - b1b0 specifies the integer type of offset. ARRAY = 3 # Below is all possible type info values for `PRIMITIVE`. # JSON Null value. Empty content. NULL = 0 # True value. Empty content. TRUE = 1 # False value. Empty content. FALSE = 2 # 1-byte little-endian signed integer. INT1 = 3 # 2-byte little-endian signed integer. INT2 = 4 # 4-byte little-endian signed integer. INT4 = 5 # 4-byte little-endian signed integer. INT8 = 6 # 8-byte IEEE double. DOUBLE = 7 # 4-byte decimal. Content is 1-byte scale + 4-byte little-endian signed integer. DECIMAL4 = 8 # 8-byte decimal. Content is 1-byte scale + 8-byte little-endian signed integer. DECIMAL8 = 9 # 16-byte decimal. Content is 1-byte scale + 16-byte little-endian signed integer. DECIMAL16 = 10 # Date value. Content is 4-byte little-endian signed integer that represents the number of days # from the Unix epoch. DATE = 11 # Timestamp value. Content is 8-byte little-endian signed integer that represents the number of # microseconds elapsed since the Unix epoch, 1970-01-01 00:00:00 UTC. This is a timezone-aware # field and when reading into a Python datetime object defaults to the UTC timezone. TIMESTAMP = 12 # Timestamp_ntz value. It has the same content as `TIMESTAMP` but should always be interpreted # as if the local time zone is UTC. TIMESTAMP_NTZ = 13 # 4-byte IEEE float. FLOAT = 14 # Binary value. The content is (4-byte little-endian unsigned integer representing the binary # size) + (size bytes of binary content). BINARY = 15 # Long string value. The content is (4-byte little-endian unsigned integer representing the # string size) + (size bytes of string content). LONG_STR = 16 VERSION = 1 # The lower 4 bits of the first metadata byte contain the version. VERSION_MASK = 0x0F U8_MAX = 0xFF U16_MAX = 0xFFFF U24_MAX = 0xFFFFFF U24_SIZE = 3 U32_SIZE = 4 I8_MAX = 0x7F I8_MIN = -0x80 I16_MAX = 0x7FFF I16_MIN = -0x8000 I32_MAX = 0x7FFFFFFF I32_MIN = -0x80000000 I64_MAX = 0x7FFFFFFFFFFFFFFF I64_MIN = -0x8000000000000000 EPOCH = datetime.datetime( year=1970, month=1, day=1, hour=0, minute=0, second=0, tzinfo=datetime.timezone.utc ) EPOCH_NTZ = datetime.datetime(year=1970, month=1, day=1, hour=0, minute=0, second=0) MAX_DECIMAL4_PRECISION = 9 MAX_DECIMAL4_VALUE = 10**MAX_DECIMAL4_PRECISION MAX_DECIMAL8_PRECISION = 18 MAX_DECIMAL8_VALUE = 10**MAX_DECIMAL8_PRECISION MAX_DECIMAL16_PRECISION = 38 MAX_DECIMAL16_VALUE = 10**MAX_DECIMAL16_PRECISION @classmethod def to_json(cls, value: bytes, metadata: bytes, zone_id: str = "UTC") -> str: """ Convert the VariantVal to a JSON string. The `zone_id` parameter denotes the time zone that timestamp fields should be parsed in. It defaults to "UTC". The list of valid zone IDs can found by importing the `zoneinfo` module and running `zoneinfo.available_timezones()`. :return: JSON string """ return cls._to_json(value, metadata, 0, zone_id) @classmethod def to_python(cls, value: bytes, metadata: bytes) -> str: """ Convert the VariantVal to a nested Python object of Python data types. :return: Python representation of the Variant nested structure """ return cls._to_python(value, metadata, 0) @classmethod def parse_json(cls, json_str: str) -> Tuple[bytes, bytes]: """ Parses the JSON string and creates the Variant binary (value, metadata) :return: tuple of 2 binary values (value, metadata) """ builder = VariantBuilder() return builder.build(json_str) @classmethod def _read_long(cls, data: bytes, pos: int, num_bytes: int, signed: bool) -> int: cls._check_index(pos, len(data)) cls._check_index(pos + num_bytes - 1, len(data)) return int.from_bytes(data[pos : pos + num_bytes], byteorder="little", signed=signed) @classmethod def _check_index(cls, pos: int, length: int) -> None: if pos < 0 or pos >= length: raise PySparkValueError(errorClass="MALFORMED_VARIANT", messageParameters={}) @classmethod def _get_type_info(cls, value: bytes, pos: int) -> Tuple[int, int]: """ Returns the (basic_type, type_info) pair from the given position in the value. """ basic_type = value[pos] & VariantUtils.BASIC_TYPE_MASK type_info = (value[pos] >> VariantUtils.BASIC_TYPE_BITS) & VariantUtils.TYPE_INFO_MASK return (basic_type, type_info) @classmethod def _get_metadata_key(cls, metadata: bytes, id: int) -> str: """ Returns the key string from the dictionary in the metadata, corresponding to `id`. """ cls._check_index(0, len(metadata)) offset_size = ((metadata[0] >> 6) & 0x3) + 1 dict_size = cls._read_long(metadata, 1, offset_size, signed=False) if id >= dict_size: raise PySparkValueError(errorClass="MALFORMED_VARIANT", messageParameters={}) string_start = 1 + (dict_size + 2) * offset_size offset = cls._read_long(metadata, 1 + (id + 1) * offset_size, offset_size, signed=False) next_offset = cls._read_long( metadata, 1 + (id + 2) * offset_size, offset_size, signed=False ) if offset > next_offset: raise PySparkValueError(errorClass="MALFORMED_VARIANT", messageParameters={}) cls._check_index(string_start + next_offset - 1, len(metadata)) return metadata[string_start + offset : (string_start + next_offset)].decode("utf-8") @classmethod def _get_boolean(cls, value: bytes, pos: int) -> bool: cls._check_index(pos, len(value)) basic_type, type_info = cls._get_type_info(value, pos) if basic_type != VariantUtils.PRIMITIVE or ( type_info != VariantUtils.TRUE and type_info != VariantUtils.FALSE ): raise PySparkValueError(errorClass="MALFORMED_VARIANT", messageParameters={}) return type_info == VariantUtils.TRUE @classmethod def _get_long(cls, value: bytes, pos: int) -> int: cls._check_index(pos, len(value)) basic_type, type_info = cls._get_type_info(value, pos) if basic_type != VariantUtils.PRIMITIVE: raise PySparkValueError(errorClass="MALFORMED_VARIANT", messageParameters={}) if type_info == VariantUtils.INT1: return cls._read_long(value, pos + 1, 1, signed=True) elif type_info == VariantUtils.INT2: return cls._read_long(value, pos + 1, 2, signed=True) elif type_info == VariantUtils.INT4 or type_info == VariantUtils.DATE: return cls._read_long(value, pos + 1, 4, signed=True) elif type_info == VariantUtils.INT8: return cls._read_long(value, pos + 1, 8, signed=True) raise PySparkValueError(errorClass="MALFORMED_VARIANT", messageParameters={}) @classmethod def _get_date(cls, value: bytes, pos: int) -> datetime.date: cls._check_index(pos, len(value)) basic_type, type_info = cls._get_type_info(value, pos) if basic_type != VariantUtils.PRIMITIVE: raise PySparkValueError(errorClass="MALFORMED_VARIANT", messageParameters={}) if type_info == VariantUtils.DATE: days_since_epoch = cls._read_long(value, pos + 1, 4, signed=True) return datetime.date.fromordinal(VariantUtils.EPOCH.toordinal() + days_since_epoch) raise PySparkValueError(errorClass="MALFORMED_VARIANT", messageParameters={}) @classmethod def _get_timestamp(cls, value: bytes, pos: int, zone_id: str) -> datetime.datetime: cls._check_index(pos, len(value)) basic_type, type_info = cls._get_type_info(value, pos) if basic_type != VariantUtils.PRIMITIVE: raise PySparkValueError(errorClass="MALFORMED_VARIANT", messageParameters={}) if type_info == VariantUtils.TIMESTAMP_NTZ: microseconds_since_epoch = cls._read_long(value, pos + 1, 8, signed=True) return VariantUtils.EPOCH_NTZ + datetime.timedelta( microseconds=microseconds_since_epoch ) if type_info == VariantUtils.TIMESTAMP: microseconds_since_epoch = cls._read_long(value, pos + 1, 8, signed=True) return ( VariantUtils.EPOCH + datetime.timedelta(microseconds=microseconds_since_epoch) ).astimezone(ZoneInfo(zone_id)) raise PySparkValueError(errorClass="MALFORMED_VARIANT", messageParameters={}) @classmethod def _get_string(cls, value: bytes, pos: int) -> str: cls._check_index(pos, len(value)) basic_type, type_info = cls._get_type_info(value, pos) if basic_type == VariantUtils.SHORT_STR or ( basic_type == VariantUtils.PRIMITIVE and type_info == VariantUtils.LONG_STR ): start = 0 length = 0 if basic_type == VariantUtils.SHORT_STR: start = pos + 1 length = type_info else: start = pos + 1 + VariantUtils.U32_SIZE length = cls._read_long(value, pos + 1, VariantUtils.U32_SIZE, signed=False) cls._check_index(start + length - 1, len(value)) return value[start : start + length].decode("utf-8") raise PySparkValueError(errorClass="MALFORMED_VARIANT", messageParameters={}) @classmethod def _get_double(cls, value: bytes, pos: int) -> float: cls._check_index(pos, len(value)) basic_type, type_info = cls._get_type_info(value, pos) if basic_type != VariantUtils.PRIMITIVE: raise PySparkValueError(errorClass="MALFORMED_VARIANT", messageParameters={}) if type_info == VariantUtils.FLOAT: cls._check_index(pos + 4, len(value)) return struct.unpack("<f", value[pos + 1 : pos + 5])[0] elif type_info == VariantUtils.DOUBLE: cls._check_index(pos + 8, len(value)) return struct.unpack("<d", value[pos + 1 : pos + 9])[0] raise PySparkValueError(errorClass="MALFORMED_VARIANT", messageParameters={}) @classmethod def _check_decimal(cls, unscaled: int, scale: int, max_unscaled: int, max_scale: int) -> None: # max_unscaled == 10**max_scale, but we pass a literal parameter to avoid redundant # computation. if unscaled >= max_unscaled or unscaled <= -max_unscaled or scale > max_scale: raise PySparkValueError(errorClass="MALFORMED_VARIANT", messageParameters={}) @classmethod def _get_decimal(cls, value: bytes, pos: int) -> decimal.Decimal: cls._check_index(pos, len(value)) basic_type, type_info = cls._get_type_info(value, pos) if basic_type != VariantUtils.PRIMITIVE: raise PySparkValueError(errorClass="MALFORMED_VARIANT", messageParameters={}) scale = value[pos + 1] unscaled = 0 if type_info == VariantUtils.DECIMAL4: unscaled = cls._read_long(value, pos + 2, 4, signed=True) cls._check_decimal(unscaled, scale, cls.MAX_DECIMAL4_VALUE, cls.MAX_DECIMAL4_PRECISION) elif type_info == VariantUtils.DECIMAL8: unscaled = cls._read_long(value, pos + 2, 8, signed=True) cls._check_decimal(unscaled, scale, cls.MAX_DECIMAL8_VALUE, cls.MAX_DECIMAL8_PRECISION) elif type_info == VariantUtils.DECIMAL16: cls._check_index(pos + 17, len(value)) unscaled = int.from_bytes(value[pos + 2 : pos + 18], byteorder="little", signed=True) cls._check_decimal( unscaled, scale, cls.MAX_DECIMAL16_VALUE, cls.MAX_DECIMAL16_PRECISION ) else: raise PySparkValueError(errorClass="MALFORMED_VARIANT", messageParameters={}) return decimal.Decimal(unscaled) * (decimal.Decimal(10) ** (-scale)) @classmethod def _get_binary(cls, value: bytes, pos: int) -> bytes: cls._check_index(pos, len(value)) basic_type, type_info = cls._get_type_info(value, pos) if basic_type != VariantUtils.PRIMITIVE or type_info != VariantUtils.BINARY: raise PySparkValueError(errorClass="MALFORMED_VARIANT", messageParameters={}) start = pos + 1 + VariantUtils.U32_SIZE length = cls._read_long(value, pos + 1, VariantUtils.U32_SIZE, signed=False) cls._check_index(start + length - 1, len(value)) return bytes(value[start : start + length]) @classmethod def _get_type(cls, value: bytes, pos: int) -> Any: """ Returns the Python type of the Variant at the given position. """ cls._check_index(pos, len(value)) basic_type, type_info = cls._get_type_info(value, pos) if basic_type == VariantUtils.SHORT_STR: return str elif basic_type == VariantUtils.OBJECT: return dict elif basic_type == VariantUtils.ARRAY: return array elif type_info == VariantUtils.NULL: return type(None) elif type_info == VariantUtils.TRUE or type_info == VariantUtils.FALSE: return bool elif ( type_info == VariantUtils.INT1 or type_info == VariantUtils.INT2 or type_info == VariantUtils.INT4 or type_info == VariantUtils.INT8 ): return int elif type_info == VariantUtils.DOUBLE or type_info == VariantUtils.FLOAT: return float elif ( type_info == VariantUtils.DECIMAL4 or type_info == VariantUtils.DECIMAL8 or type_info == VariantUtils.DECIMAL16 ): return decimal.Decimal elif type_info == VariantUtils.BINARY: return bytes elif type_info == VariantUtils.DATE: return datetime.date elif type_info == VariantUtils.TIMESTAMP or type_info == VariantUtils.TIMESTAMP_NTZ: return datetime.datetime elif type_info == VariantUtils.LONG_STR: return str raise PySparkValueError(errorClass="MALFORMED_VARIANT", messageParameters={}) @classmethod def _to_json(cls, value: bytes, metadata: bytes, pos: int, zone_id: str) -> str: variant_type = cls._get_type(value, pos) if variant_type is dict: def handle_object(key_value_pos_list: List[Tuple[str, int]]) -> str: key_value_list = [ json.dumps(key) + ":" + cls._to_json(value, metadata, value_pos, zone_id) for (key, value_pos) in key_value_pos_list ] return "{" + ",".join(key_value_list) + "}" return cls._handle_object(value, metadata, pos, handle_object) elif variant_type is array: def handle_array(value_pos_list: List[int]) -> str: value_list = [ cls._to_json(value, metadata, value_pos, zone_id) for value_pos in value_pos_list ] return "[" + ",".join(value_list) + "]" return cls._handle_array(value, pos, handle_array) else: value = cls._get_scalar(variant_type, value, metadata, pos, zone_id) if value is None: return "null" if isinstance(value, bool): return "true" if value else "false" if isinstance(value, str): return json.dumps(value) if isinstance(value, bytes): # decoding simply converts byte array to string return '"' + base64.b64encode(value).decode("utf-8") + '"' if isinstance(value, (datetime.date, datetime.datetime)): return '"' + str(value) + '"' return str(value) @classmethod def _to_python(cls, value: bytes, metadata: bytes, pos: int) -> Any: variant_type = cls._get_type(value, pos) if variant_type is dict: def handle_object(key_value_pos_list: List[Tuple[str, int]]) -> Dict[str, Any]: key_value_list = [ (key, cls._to_python(value, metadata, value_pos)) for (key, value_pos) in key_value_pos_list ] return dict(key_value_list) return cls._handle_object(value, metadata, pos, handle_object) elif variant_type is array: def handle_array(value_pos_list: List[int]) -> List[Any]: value_list = [ cls._to_python(value, metadata, value_pos) for value_pos in value_pos_list ] return value_list return cls._handle_array(value, pos, handle_array) else: return cls._get_scalar(variant_type, value, metadata, pos, zone_id="UTC") @classmethod def _get_scalar( cls, variant_type: Any, value: bytes, metadata: bytes, pos: int, zone_id: str ) -> Any: if isinstance(None, variant_type): return None elif variant_type is bool: return cls._get_boolean(value, pos) elif variant_type is int: return cls._get_long(value, pos) elif variant_type is str: return cls._get_string(value, pos) elif variant_type is float: return cls._get_double(value, pos) elif variant_type is decimal.Decimal: return cls._get_decimal(value, pos) elif variant_type is bytes: return cls._get_binary(value, pos) elif variant_type is datetime.date: return cls._get_date(value, pos) elif variant_type is datetime.datetime: return cls._get_timestamp(value, pos, zone_id) else: raise PySparkValueError(errorClass="MALFORMED_VARIANT", messageParameters={}) @classmethod def _handle_object( cls, value: bytes, metadata: bytes, pos: int, func: Callable[[List[Tuple[str, int]]], Any] ) -> Any: """ Parses the variant object at position `pos`. Calls `func` with a list of (key, value position) pairs of the object. """ cls._check_index(pos, len(value)) basic_type, type_info = cls._get_type_info(value, pos) if basic_type != VariantUtils.OBJECT: raise PySparkValueError(errorClass="MALFORMED_VARIANT", messageParameters={}) large_size = ((type_info >> 4) & 0x1) != 0 size_bytes = VariantUtils.U32_SIZE if large_size else 1 num_fields = cls._read_long(value, pos + 1, size_bytes, signed=False) id_size = ((type_info >> 2) & 0x3) + 1 offset_size = ((type_info) & 0x3) + 1 id_start = pos + 1 + size_bytes offset_start = id_start + num_fields * id_size data_start = offset_start + (num_fields + 1) * offset_size key_value_pos_list = [] for i in range(num_fields): id = cls._read_long(value, id_start + id_size * i, id_size, signed=False) offset = cls._read_long( value, offset_start + offset_size * i, offset_size, signed=False ) value_pos = data_start + offset if metadata is not None: key_value_pos_list.append((cls._get_metadata_key(metadata, id), value_pos)) else: key_value_pos_list.append(("", value_pos)) return func(key_value_pos_list) @classmethod def _handle_array(cls, value: bytes, pos: int, func: Callable[[List[int]], Any]) -> Any: """ Parses the variant array at position `pos`. Calls `func` with a list of element positions of the array. """ cls._check_index(pos, len(value)) basic_type, type_info = cls._get_type_info(value, pos) if basic_type != VariantUtils.ARRAY: raise PySparkValueError(errorClass="MALFORMED_VARIANT", messageParameters={}) large_size = ((type_info >> 2) & 0x1) != 0 size_bytes = VariantUtils.U32_SIZE if large_size else 1 num_fields = cls._read_long(value, pos + 1, size_bytes, signed=False) offset_size = (type_info & 0x3) + 1 offset_start = pos + 1 + size_bytes data_start = offset_start + (num_fields + 1) * offset_size value_pos_list = [] for i in range(num_fields): offset = cls._read_long( value, offset_start + offset_size * i, offset_size, signed=False ) element_pos = data_start + offset value_pos_list.append(element_pos) return func(value_pos_list) class FieldEntry(NamedTuple): """ Info about an object field """ key: str id: int offset: int class VariantBuilder: """ A utility class for building VariantVal. """ DEFAULT_SIZE_LIMIT = 16 * 1024 * 1024 def __init__(self, size_limit: int = DEFAULT_SIZE_LIMIT): self.value = bytearray() self.dictionary = dict[str, int]() self.dictionary_keys = list[bytes]() self.size_limit = size_limit def build(self, json_str: str) -> Tuple[bytes, bytes]: parsed = json.loads(json_str, parse_float=self._handle_float) self._process_parsed_json(parsed) num_keys = len(self.dictionary_keys) dictionary_string_size = sum(len(key) for key in self.dictionary_keys) # Determine the number of bytes required per offset entry. # The largest offset is the one-past-the-end value, which is total string size. It's very # unlikely that the number of keys could be larger, but incorporate that into the # calculation in case of pathological data. max_size = max(dictionary_string_size, num_keys) if max_size > self.size_limit: raise PySparkValueError(errorClass="VARIANT_SIZE_LIMIT_EXCEEDED", messageParameters={}) offset_size = self._get_integer_size(max_size) offset_start = 1 + offset_size string_start = offset_start + (num_keys + 1) * offset_size metadata_size = string_start + dictionary_string_size if metadata_size > self.size_limit: raise PySparkValueError(errorClass="VARIANT_SIZE_LIMIT_EXCEEDED", messageParameters={}) metadata = bytearray() header_byte = VariantUtils.VERSION | ((offset_size - 1) << 6) metadata.extend(header_byte.to_bytes(1, byteorder="little")) metadata.extend(num_keys.to_bytes(offset_size, byteorder="little")) # write offsets current_offset = 0 for key in self.dictionary_keys: metadata.extend(current_offset.to_bytes(offset_size, byteorder="little")) current_offset += len(key) metadata.extend(current_offset.to_bytes(offset_size, byteorder="little")) # write key data for key in self.dictionary_keys: metadata.extend(key) return (bytes(self.value), bytes(metadata)) def _process_parsed_json(self, parsed: Any) -> None: if type(parsed) is dict: fields = list[FieldEntry]() start = len(self.value) for key, value in parsed.items(): id = self._add_key(key) fields.append(FieldEntry(key, id, len(self.value) - start)) self._process_parsed_json(value) self._finish_writing_object(start, fields) elif type(parsed) is list: offsets = [] start = len(self.value) for elem in parsed: offsets.append(len(self.value) - start) self._process_parsed_json(elem) self._finish_writing_array(start, offsets) elif type(parsed) is str: self._append_string(parsed) elif type(parsed) is int: if not self._append_int(parsed): self._process_parsed_json(self._handle_float(str(parsed))) elif type(parsed) is float: self._append_float(parsed) elif type(parsed) is decimal.Decimal: self._append_decimal(parsed) elif type(parsed) is bool: self._append_boolean(parsed) elif parsed is None: self._append_null() else: raise PySparkValueError(errorClass="MALFORMED_VARIANT", messageParameters={}) # Choose the smallest unsigned integer type that can store `value`. It must be within # [0, U24_MAX]. def _get_integer_size(self, value: int) -> int: if value <= VariantUtils.U8_MAX: return 1 if value <= VariantUtils.U16_MAX: return 2 return VariantUtils.U24_SIZE def _check_capacity(self, additional: int) -> None: required = len(self.value) + additional if required > self.size_limit: raise PySparkValueError(errorClass="VARIANT_SIZE_LIMIT_EXCEEDED", messageParameters={}) def _primitive_header(self, type: int) -> bytes: return bytes([(type << 2) | VariantUtils.PRIMITIVE]) def _short_string_header(self, size: int) -> bytes: return bytes([size << 2 | VariantUtils.SHORT_STR]) def _array_header(self, large_size: bool, offset_size: int) -> bytes: return bytes( [ ( (large_size << (VariantUtils.BASIC_TYPE_BITS + 2)) | ((offset_size - 1) << VariantUtils.BASIC_TYPE_BITS) | VariantUtils.ARRAY ) ] ) def _object_header(self, large_size: bool, id_size: int, offset_size: int) -> bytes: return bytes( [ ( (large_size << (VariantUtils.BASIC_TYPE_BITS + 4)) | ((id_size - 1) << (VariantUtils.BASIC_TYPE_BITS + 2)) | ((offset_size - 1) << VariantUtils.BASIC_TYPE_BITS) | VariantUtils.OBJECT ) ] ) # Add a key to the variant dictionary. If the key already exists, the dictionary is # not modified. In either case, return the id of the key. def _add_key(self, key: str) -> int: if key in self.dictionary: return self.dictionary[key] id = len(self.dictionary_keys) self.dictionary[key] = id self.dictionary_keys.append(key.encode("utf-8")) return id def _handle_float(self, num_str: str) -> Any: # a float can be a decimal if it only contains digits, '-', or '-'. if all([ch.isdecimal() or ch == "-" or ch == "." for ch in num_str]): dec = decimal.Decimal(num_str) precision = len(dec.as_tuple().digits) scale = -int(dec.as_tuple().exponent) if ( scale <= VariantUtils.MAX_DECIMAL16_PRECISION and precision <= VariantUtils.MAX_DECIMAL16_PRECISION ): return dec return float(num_str) def _append_boolean(self, b: bool) -> None: self._check_capacity(1) self.value.extend(self._primitive_header(VariantUtils.TRUE if b else VariantUtils.FALSE)) def _append_null(self) -> None: self._check_capacity(1) self.value.extend(self._primitive_header(VariantUtils.NULL)) def _append_string(self, s: str) -> None: text = s.encode("utf-8") long_str = len(text) > VariantUtils.MAX_SHORT_STR_SIZE additional = (1 + VariantUtils.U32_SIZE) if long_str else 1 self._check_capacity(additional + len(text)) if long_str: self.value.extend(self._primitive_header(VariantUtils.LONG_STR)) self.value.extend(len(text).to_bytes(VariantUtils.U32_SIZE, byteorder="little")) else: self.value.extend(self._short_string_header(len(text))) self.value.extend(text) def _append_int(self, i: int) -> bool: self._check_capacity(1 + 8) if i >= VariantUtils.I8_MIN and i <= VariantUtils.I8_MAX: self.value.extend(self._primitive_header(VariantUtils.INT1)) self.value.extend(i.to_bytes(1, byteorder="little", signed=True)) elif i >= VariantUtils.I16_MIN and i <= VariantUtils.I16_MAX: self.value.extend(self._primitive_header(VariantUtils.INT2)) self.value.extend(i.to_bytes(2, byteorder="little", signed=True)) elif i >= VariantUtils.I32_MIN and i <= VariantUtils.I32_MAX: self.value.extend(self._primitive_header(VariantUtils.INT4)) self.value.extend(i.to_bytes(4, byteorder="little", signed=True)) elif i >= VariantUtils.I64_MIN and i <= VariantUtils.I64_MAX: self.value.extend(self._primitive_header(VariantUtils.INT8)) self.value.extend(i.to_bytes(8, byteorder="little", signed=True)) else: return False return True # Append a decimal value to the variant builder. The caller should guarantee that its precision # and scale fit into `MAX_DECIMAL16_PRECISION`. def _append_decimal(self, d: decimal.Decimal) -> None: self._check_capacity(2 + 16) precision = len(d.as_tuple().digits) scale = -int(d.as_tuple().exponent) unscaled = int("".join(map(str, d.as_tuple().digits))) unscaled = -unscaled if d < 0 else unscaled if ( scale <= VariantUtils.MAX_DECIMAL4_PRECISION and precision <= VariantUtils.MAX_DECIMAL4_PRECISION ): self.value.extend(self._primitive_header(VariantUtils.DECIMAL4)) self.value.extend(scale.to_bytes(1, byteorder="little")) self.value.extend(unscaled.to_bytes(4, byteorder="little", signed=True)) elif ( scale <= VariantUtils.MAX_DECIMAL8_PRECISION and precision <= VariantUtils.MAX_DECIMAL8_PRECISION ): self.value.extend(self._primitive_header(VariantUtils.DECIMAL8)) self.value.extend(scale.to_bytes(1, byteorder="little")) self.value.extend(unscaled.to_bytes(8, byteorder="little", signed=True)) else: assert ( scale <= VariantUtils.MAX_DECIMAL16_PRECISION and precision <= VariantUtils.MAX_DECIMAL16_PRECISION ) self.value.extend(self._primitive_header(VariantUtils.DECIMAL16)) self.value.extend(scale.to_bytes(1, byteorder="little")) self.value.extend(unscaled.to_bytes(16, byteorder="little", signed=True)) def _append_float(self, f: float) -> None: self._check_capacity(1 + 8) self.value.extend(self._primitive_header(VariantUtils.DOUBLE)) self.value.extend(struct.pack("<d", f)) # Finish writing a variant array after all of its elements have already been written. def _finish_writing_array(self, start: int, offsets: List[int]) -> None: data_size = len(self.value) - start num_offsets = len(offsets) large_size = num_offsets > VariantUtils.U8_MAX size_bytes = VariantUtils.U32_SIZE if large_size else 1 offset_size = self._get_integer_size(data_size) # The space for header byte, object size, and offset list. header_size = 1 + size_bytes + (num_offsets + 1) * offset_size self._check_capacity(header_size) self.value.extend(bytearray(header_size)) # Shift the just-written element data to make room for the header section. self.value[start + header_size :] = bytes(self.value[start : start + data_size]) # Write the header byte, num offsets offset_start = start + 1 + size_bytes self.value[start : start + 1] = self._array_header(large_size, offset_size) self.value[start + 1 : offset_start] = num_offsets.to_bytes(size_bytes, byteorder="little") # write offset list offset_list = bytearray() for offset in offsets: offset_list.extend(offset.to_bytes(offset_size, byteorder="little")) offset_list.extend(data_size.to_bytes(offset_size, byteorder="little")) self.value[offset_start : offset_start + len(offset_list)] = offset_list # Finish writing a variant object after all of its fields have already been written. def _finish_writing_object(self, start: int, fields: List[FieldEntry]) -> None: num_fields = len(fields) # object fields are from a python dictionary, so keys are already distinct fields.sort(key=lambda f: f.key) max_id = 0 for field in fields: max_id = max(max_id, field.id) data_size = len(self.value) - start large_size = num_fields > VariantUtils.U8_MAX size_bytes = VariantUtils.U32_SIZE if large_size else 1 id_size = self._get_integer_size(max_id) offset_size = self._get_integer_size(data_size) # The space for header byte, object size, id list, and offset list. header_size = 1 + size_bytes + num_fields * id_size + (num_fields + 1) * offset_size self._check_capacity(header_size) self.value.extend(bytearray(header_size)) # Shift the just-written field data to make room for the object header section. self.value[start + header_size :] = self.value[start : start + data_size] # Write the header byte, num fields, id list, offset list self.value[start : start + 1] = self._object_header(large_size, id_size, offset_size) self.value[start + 1 : start + 1 + size_bytes] = num_fields.to_bytes( size_bytes, byteorder="little" ) id_start = start + 1 + size_bytes offset_start = id_start + num_fields * id_size id_list = bytearray() offset_list = bytearray() for field in fields: id_list.extend(field.id.to_bytes(id_size, byteorder="little")) offset_list.extend(field.offset.to_bytes(offset_size, byteorder="little")) offset_list.extend(data_size.to_bytes(offset_size, byteorder="little")) self.value[id_start : id_start + len(id_list)] = id_list self.value[offset_start : offset_start + len(offset_list)] = offset_list