blob: a64e1604a61882f5c25d727d40bf0838d2e16693 [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.
"""Java decimal schema evolution: decimal text, HALF_UP, then precision."""
from decimal import Context, Decimal, InvalidOperation, localcontext, ROUND_HALF_UP
import re
import pyarrow as pa
from pypaimon.utils.numeric_utils import java_double_decimal_text
_DECIMAL_TEXT = re.compile(r'[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?\Z')
def cast_decimal(array, target):
"""Return None for conversions outside the existing decimal read support.
FLOAT values from Arrow are already widened to Python double. Java does
the same widening before BigDecimal.valueOf, retaining the float's binary
rounding. A valid decimal overflowing the target is NULL; invalid text
fails the conversion instead of silently becoming NULL.
"""
floating = pa.types.is_floating(array.type)
integer = pa.types.is_signed_integer(array.type)
text = pa.types.is_string(array.type)
if not pa.types.is_decimal(target) or not (integer or floating or text or pa.types.is_decimal(array.type)):
return None
values = []
# The caller's rounding traps and exponent limits must not affect reads.
# One extra digit allows rounding to carry, before the precision check.
context = Context(prec=target.precision + 1, rounding=ROUND_HALF_UP,
Emin=-999999, Emax=999999, clamp=0, traps=[InvalidOperation])
with localcontext(context):
quantum = Decimal(1).scaleb(-target.scale)
for value in array.to_pylist():
if value is None:
values.append(None)
continue
if floating:
value = java_double_decimal_text(value)
if integer:
value = Decimal(value)
if floating or text:
if _DECIMAL_TEXT.fullmatch(value) is None:
raise pa.ArrowInvalid('Invalid decimal schema-evolution value: %s' % value)
try:
value = Decimal(value)
except InvalidOperation as error:
raise pa.ArrowInvalid('Invalid decimal schema-evolution value: %s' % value) from error
# BigDecimal validates the adjusted input scale, then setScale and
# BigInteger.TEN.pow check their supported range before precision.
# Zero bypasses the power check, but still needs a valid input scale.
input_scale = -value.as_tuple().exponent
if not -(2 ** 31) <= input_scale < 2 ** 31:
raise pa.ArrowInvalid('Invalid decimal schema-evolution input scale')
power = abs(target.scale - input_scale)
if not value.is_zero() and (power > 2 ** 31 - 1 or 4 * power // 32 > (2 ** 31 - 1) // 32 + 1):
raise pa.ArrowInvalid("Decimal scale conversion exceeds Java's supported range")
# Avoid quantizing values whose integer part already overflows.
if not value.is_zero() and value.adjusted() + target.scale + 1 > target.precision:
values.append(None)
continue
try:
rounded = value.quantize(quantum, rounding=ROUND_HALF_UP)
except InvalidOperation as error:
raise pa.ArrowInvalid('Invalid decimal schema-evolution value: %s' % value) from error
values.append(rounded if len(rounded.as_tuple().digits) <= target.precision else None)
return pa.array(values, type=target)