blob: 87b88b7465e4c4d373de66bec66b59fd3de43c39 [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.
################################################################################
import datetime
from array import array
from pyflink.common import Row
from pyflink.java_gateway import get_gateway
from pyflink.table.types import (
_array_type_mappings,
_to_java_data_type,
ArrayType,
DataType,
DateType,
DayTimeIntervalType,
LocalZonedTimestampType,
MapType,
MultisetType,
RowType,
TimeType,
TimestampType,
)
from pyflink.util.api_stability_decorators import Internal
@Internal()
def _to_java_literal_value(value, data_type: DataType = None):
"""Converts Python-only literal values into objects accepted by Py4J."""
if data_type is None:
return _to_java_inferred_literal_value(value)
return _to_java_typed_literal_value(value, data_type)
def _to_java_inferred_literal_value(value):
if value is None:
return value
gateway = get_gateway()
jvm = gateway.jvm
if isinstance(value, datetime.datetime):
return _to_java_typed_literal_value(value, TimestampType())
elif isinstance(value, datetime.date):
return _to_java_typed_literal_value(value, DateType())
elif isinstance(value, datetime.time):
return _to_java_typed_literal_value(value, TimeType())
elif isinstance(value, datetime.timedelta):
return _to_java_typed_literal_value(
value,
DayTimeIntervalType(DayTimeIntervalType.DayTimeResolution.DAY_TO_SECOND),
)
elif isinstance(value, array):
if value.typecode not in _array_type_mappings:
raise TypeError(f"not supported type: array({value.typecode})")
element_data_type = _to_java_data_type(_array_type_mappings[value.typecode])
j_array = jvm.java.lang.reflect.Array.newInstance(
element_data_type.getConversionClass(), len(value)
)
for pos, element in enumerate(value):
j_array[pos] = element
if not value:
array_data_type = ArrayType(_array_type_mappings[value.typecode]).not_null()
return (
jvm.org.apache.flink.table.utils.python.PythonTableUtils
.createInferredArrayValue(j_array, _to_java_data_type(array_data_type))
)
return j_array
elif isinstance(value, (list, tuple)):
j_values = jvm.java.util.ArrayList()
for element in value:
j_values.add(_to_java_inferred_literal_value(element))
return j_values
elif isinstance(value, Row):
return _to_java_row(value)
return value
def _to_java_instant(value, jvm):
utc_value = value.astimezone(datetime.timezone.utc)
epoch = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)
delta = utc_value - epoch
# Avoid floating-point timestamp conversion for pre-epoch and subsecond values.
seconds = delta.days * 86400 + delta.seconds
return jvm.java.time.Instant.ofEpochSecond(seconds, utc_value.microsecond * 1000)
def _to_java_local_datetime(value, jvm):
return jvm.java.time.LocalDateTime.ofInstant(
_to_java_instant(value, jvm), jvm.java.time.ZoneId.systemDefault()
)
def _to_java_typed_literal_value(value, data_type: DataType):
if value is None or data_type._conversion_cls:
return value
jvm = get_gateway().jvm
if isinstance(data_type, DateType) and isinstance(value, datetime.datetime):
value = value.date()
if isinstance(data_type, DateType) and isinstance(value, datetime.date):
return jvm.java.time.LocalDate.of(value.year, value.month, value.day)
elif isinstance(data_type, TimeType) and isinstance(value, datetime.time):
if value.utcoffset() is not None:
# TIME has no date. Use the Unix epoch date to match PyFlink's existing transport
# conversion when rendering an offset time in the client JVM's local time zone.
date_time = datetime.datetime.combine(datetime.date(1970, 1, 1), value)
return _to_java_local_datetime(date_time, jvm).toLocalTime()
return jvm.java.time.LocalTime.of(
value.hour, value.minute, value.second, value.microsecond * 1000
)
elif isinstance(data_type, TimestampType) and isinstance(value, datetime.datetime):
if value.utcoffset() is not None:
# Match PyFlink's TIMESTAMP transport conversion by rendering the instant in the
# client JVM's local time zone before dropping the zone information.
return _to_java_local_datetime(value, jvm)
return jvm.java.time.LocalDateTime.of(
value.year,
value.month,
value.day,
value.hour,
value.minute,
value.second,
value.microsecond * 1000,
)
elif isinstance(data_type, LocalZonedTimestampType) and isinstance(
value, datetime.datetime
):
if value.utcoffset() is None:
value = value.astimezone()
return _to_java_instant(value, jvm)
elif isinstance(data_type, DayTimeIntervalType) and isinstance(
value, datetime.timedelta
):
seconds = value.days * 86400 + value.seconds
return jvm.java.time.Duration.ofSeconds(seconds, value.microseconds * 1000)
elif isinstance(data_type, ArrayType) and isinstance(value, (list, tuple, array)):
j_values = jvm.java.util.ArrayList()
for element in value:
j_values.add(_to_java_typed_literal_value(element, data_type.element_type))
return j_values
elif isinstance(data_type, MultisetType) and isinstance(value, dict):
j_values = jvm.java.util.HashMap()
for element, count in value.items():
j_values.put(
_to_java_typed_literal_value(element, data_type.element_type), count
)
return j_values
elif isinstance(data_type, MapType) and isinstance(value, dict):
j_values = jvm.java.util.HashMap()
for key, map_value in value.items():
j_values.put(
_to_java_typed_literal_value(key, data_type.key_type),
_to_java_typed_literal_value(map_value, data_type.value_type),
)
return j_values
elif isinstance(data_type, RowType):
if isinstance(value, Row):
return _to_java_row(value, data_type)
elif isinstance(value, dict):
j_values = jvm.java.util.HashMap()
for field in data_type.fields:
j_values.put(
field.name,
_to_java_typed_literal_value(value.get(field.name), field.data_type),
)
return j_values
elif isinstance(value, (list, tuple)):
j_values = jvm.java.util.ArrayList()
for pos, field_value in enumerate(value):
if pos < len(data_type.fields):
field_value = _to_java_typed_literal_value(
field_value, data_type.fields[pos].data_type
)
else:
field_value = _to_java_inferred_literal_value(field_value)
j_values.add(field_value)
return j_values
return value
def _to_java_row(value: Row, data_type: RowType = None):
jvm = get_gateway().jvm
if hasattr(value, "_fields"):
j_row = jvm.org.apache.flink.types.Row.withNames(value.get_row_kind().to_j_row_kind())
field_names = (
value._fields
if data_type is None
else [field.name for field in data_type.fields]
)
for pos, field_name in enumerate(field_names):
field_value = value[field_name]
if data_type is not None:
field_value = _to_java_typed_literal_value(
field_value, data_type.fields[pos].data_type
)
else:
field_value = _to_java_inferred_literal_value(field_value)
j_row.setField(field_name, field_value)
if data_type is not None:
# Keep undeclared fields so Java validates the original Row arity.
for field_name in value._fields:
if field_name not in field_names:
j_row.setField(
field_name,
_to_java_inferred_literal_value(value[field_name]),
)
return j_row
j_row = jvm.org.apache.flink.types.Row.withPositions(
value.get_row_kind().to_j_row_kind(), len(value)
)
for pos, field_value in enumerate(value):
if data_type is not None and pos < len(data_type.fields):
field_value = _to_java_typed_literal_value(
field_value, data_type.fields[pos].data_type
)
else:
field_value = _to_java_inferred_literal_value(field_value)
j_row.setField(pos, field_value)
return j_row