This is an automated email from the ASF dual-hosted git repository.
dianfu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink.git
The following commit(s) were added to refs/heads/master by this push:
new 0910fc925db [FLINK-40187][python] Introduce DataType class in
DataFrame API (#28843)
0910fc925db is described below
commit 0910fc925dbe28965f1e15aff8aba5faa651630a
Author: Liu Liu <[email protected]>
AuthorDate: Mon Aug 3 10:53:14 2026 +0800
[FLINK-40187][python] Introduce DataType class in DataFrame API (#28843)
---
.../docs/reference/pyflink.dataframe/datatype.rst | 20 ++
flink-python/pyflink/dataframe/datatype.py | 387 ++++++++++++++++++++-
.../pyflink/dataframe/tests/test_datatype.py | 372 +++++++++++++++++++-
flink-python/pyflink/table/expression.py | 11 +-
flink-python/pyflink/table/tests/test_types.py | 22 +-
flink-python/pyflink/table/types.py | 28 +-
6 files changed, 807 insertions(+), 33 deletions(-)
diff --git a/flink-python/docs/reference/pyflink.dataframe/datatype.rst
b/flink-python/docs/reference/pyflink.dataframe/datatype.rst
index 9d9b19b5cf5..c439b3a4a47 100644
--- a/flink-python/docs/reference/pyflink.dataframe/datatype.rst
+++ b/flink-python/docs/reference/pyflink.dataframe/datatype.rst
@@ -34,5 +34,25 @@ Example::
:toctree: api/
DataType
+ DataType.int8
+ DataType.int16
+ DataType.int32
DataType.int64
+ DataType.float32
+ DataType.float64
+ DataType.decimal
DataType.string
+ DataType.fixed_size_string
+ DataType.binary
+ DataType.fixed_size_binary
+ DataType.bool
+ DataType.null
+ DataType.date
+ DataType.time
+ DataType.timestamp
+ DataType.timestamp_ltz
+ DataType.list
+ DataType.map
+ DataType.struct
+ DataType.not_null
+ DataType.nullable
diff --git a/flink-python/pyflink/dataframe/datatype.py
b/flink-python/pyflink/dataframe/datatype.py
index c22f4ba6d68..84676885a6f 100644
--- a/flink-python/pyflink/dataframe/datatype.py
+++ b/flink-python/pyflink/dataframe/datatype.py
@@ -16,11 +16,33 @@
# limitations under the License.
################################################################################
-from pyflink.table.types import DataType as TableDataType, DataTypes
+import datetime
+import decimal
+import types
+from functools import partial
+from typing import Any, Callable, Dict, List, Optional, Tuple, Union,
get_args, get_origin
+
+from pyflink.table.types import DataType as TableDataType, DataTypes, NullType
from pyflink.util.api_stability_decorators import PublicEvolving
__all__ = ["DataType"]
+_PEP_604_UNION_TYPE = getattr(types, "UnionType", None)
+
+_BASIC_TYPE_HINT_FACTORIES: Dict[Any, Callable[[], TableDataType]] = {
+ bool: DataTypes.BOOLEAN,
+ int: DataTypes.BIGINT,
+ float: DataTypes.DOUBLE,
+ str: DataTypes.STRING,
+ bytes: DataTypes.BYTES,
+ bytearray: DataTypes.BYTES,
+ decimal.Decimal: partial(DataTypes.DECIMAL, 38, 18),
+ datetime.date: DataTypes.DATE,
+ datetime.time: DataTypes.TIME,
+ datetime.datetime: DataTypes.TIMESTAMP,
+ Any: DataTypes.STRING,
+}
+
@PublicEvolving()
class DataType:
@@ -39,8 +61,13 @@ class DataType:
"""
def __init__(self, table_data_type: TableDataType):
+ if isinstance(table_data_type, NullType) and not
table_data_type._nullable:
+ raise ValueError("NULL data type must be nullable")
self._table_data_type = table_data_type
+ def __repr__(self) -> str:
+ return f"DataType({self._table_data_type!r})"
+
@PublicEvolving()
def __eq__(self, other: object) -> bool:
if not isinstance(other, DataType):
@@ -49,7 +76,55 @@ class DataType:
@PublicEvolving()
def __hash__(self) -> int:
- return hash(str(self._table_data_type))
+ return hash(repr(self._table_data_type))
+
+ @PublicEvolving()
+ def not_null(self) -> "DataType":
+ """
+ Return a non-nullable version of this data type.
+
+ .. versionadded:: 2.4.0
+ """
+ return DataType(self._table_data_type.not_null())
+
+ @PublicEvolving()
+ def nullable(self) -> "DataType":
+ """
+ Return a nullable version of this data type.
+
+ .. versionadded:: 2.4.0
+ """
+ return DataType(self._table_data_type.nullable())
+
+ @classmethod
+ @PublicEvolving()
+ def int8(cls) -> "DataType":
+ """
+ Create an 8-bit signed integer type.
+
+ .. versionadded:: 2.4.0
+ """
+ return cls(DataTypes.TINYINT())
+
+ @classmethod
+ @PublicEvolving()
+ def int16(cls) -> "DataType":
+ """
+ Create a 16-bit signed integer type.
+
+ .. versionadded:: 2.4.0
+ """
+ return cls(DataTypes.SMALLINT())
+
+ @classmethod
+ @PublicEvolving()
+ def int32(cls) -> "DataType":
+ """
+ Create a 32-bit signed integer type.
+
+ .. versionadded:: 2.4.0
+ """
+ return cls(DataTypes.INT())
@classmethod
@PublicEvolving()
@@ -66,6 +141,39 @@ class DataType:
"""
return cls(DataTypes.BIGINT())
+ @classmethod
+ @PublicEvolving()
+ def float32(cls) -> "DataType":
+ """
+ Create a 32-bit floating point type.
+
+ .. versionadded:: 2.4.0
+ """
+ return cls(DataTypes.FLOAT())
+
+ @classmethod
+ @PublicEvolving()
+ def float64(cls) -> "DataType":
+ """
+ Create a 64-bit floating point type.
+
+ .. versionadded:: 2.4.0
+ """
+ return cls(DataTypes.DOUBLE())
+
+ @classmethod
+ @PublicEvolving()
+ def decimal(cls, precision: int, scale: int) -> "DataType":
+ """
+ Create a decimal type with the given precision and scale.
+
+ :param precision: Total number of digits.
+ :param scale: Number of digits to the right of the decimal point.
+
+ .. versionadded:: 2.4.0
+ """
+ return cls(DataTypes.DECIMAL(precision, scale))
+
@classmethod
@PublicEvolving()
def string(cls) -> "DataType":
@@ -81,5 +189,280 @@ class DataType:
"""
return cls(DataTypes.STRING())
+ @classmethod
+ @PublicEvolving()
+ def fixed_size_string(cls, length: int) -> "DataType":
+ """
+ Create a fixed-length character string type.
+
+ :param length: Number of characters.
+
+ .. versionadded:: 2.4.0
+ """
+ return cls(DataTypes.CHAR(length))
+
+ @classmethod
+ @PublicEvolving()
+ def binary(cls) -> "DataType":
+ """
+ Create a variable-length binary string type.
+
+ .. versionadded:: 2.4.0
+ """
+ return cls(DataTypes.BYTES())
+
+ @classmethod
+ @PublicEvolving()
+ def fixed_size_binary(cls, length: int) -> "DataType":
+ """
+ Create a fixed-length binary string type.
+
+ :param length: Number of bytes.
+
+ .. versionadded:: 2.4.0
+ """
+ return cls(DataTypes.BINARY(length))
+
+ @classmethod
+ @PublicEvolving()
+ def bool(cls) -> "DataType":
+ """
+ Create a boolean type.
+
+ .. versionadded:: 2.4.0
+ """
+ return cls(DataTypes.BOOLEAN())
+
+ @classmethod
+ @PublicEvolving()
+ def null(cls) -> "DataType":
+ """
+ Create a null type.
+
+ .. versionadded:: 2.4.0
+ """
+ return cls(DataTypes.NULL())
+
+ @classmethod
+ @PublicEvolving()
+ def date(cls) -> "DataType":
+ """
+ Create a date type.
+
+ .. versionadded:: 2.4.0
+ """
+ return cls(DataTypes.DATE())
+
+ @classmethod
+ @PublicEvolving()
+ def time(cls, precision: int = 0) -> "DataType":
+ """
+ Create a time type.
+
+ :param precision: Number of fractional-second digits.
+
+ .. versionadded:: 2.4.0
+ """
+ return cls(DataTypes.TIME(precision))
+
+ @classmethod
+ @PublicEvolving()
+ def timestamp(cls, precision: int = 6) -> "DataType":
+ """
+ Create a timestamp type without a time zone.
+
+ :param precision: Number of fractional-second digits.
+
+ .. versionadded:: 2.4.0
+ """
+ return cls(DataTypes.TIMESTAMP(precision))
+
+ @classmethod
+ @PublicEvolving()
+ def timestamp_ltz(cls, precision: int = 6) -> "DataType":
+ """
+ Create a timestamp type with a local time zone.
+
+ :param precision: Number of fractional-second digits.
+
+ .. versionadded:: 2.4.0
+ """
+ return cls(DataTypes.TIMESTAMP_LTZ(precision))
+
+ @classmethod
+ @PublicEvolving()
+ def list(cls, dtype: "DataType") -> "DataType":
+ """
+ Create a list type.
+
+ :param dtype: Type of each list element.
+
+ Example::
+
+ >>> import pyflink.dataframe as pf
+ >>> scores_type = pf.DataType.list(pf.DataType.int32())
+
+ .. versionadded:: 2.4.0
+ """
+ return cls(DataTypes.ARRAY(dtype._to_table_data_type()))
+
+ @classmethod
+ @PublicEvolving()
+ def map(cls, key_type: "DataType", value_type: "DataType") -> "DataType":
+ """
+ Create a map type.
+
+ :param key_type: Type of each map key.
+ :param value_type: Type of each map value.
+
+ Example::
+
+ >>> import pyflink.dataframe as pf
+ >>> config_type = pf.DataType.map(
+ ... pf.DataType.string(),
+ ... pf.DataType.int64(),
+ ... )
+
+ .. versionadded:: 2.4.0
+ """
+ return cls(
+ DataTypes.MAP(
+ key_type._to_table_data_type(),
+ value_type._to_table_data_type(),
+ )
+ )
+
+ @classmethod
+ @PublicEvolving()
+ def struct(
+ cls,
+ fields: Union[
+ Dict[str, "DataType"],
+ List[Tuple[str, "DataType"]],
+ ],
+ ) -> "DataType":
+ """
+ Create a struct type with named fields.
+
+ ``fields`` may be an insertion-ordered dictionary or a list of name
and type pairs.
+
+ :param fields: Field names and their data types.
+
+ Example::
+
+ >>> import pyflink.dataframe as pf
+ >>> person_type = pf.DataType.struct({
+ ... "name": pf.DataType.string(),
+ ... "age": pf.DataType.int32(),
+ ... })
+
+ Fields may also be passed as a list of name and type pairs::
+
+ >>> person_type = pf.DataType.struct([
+ ... ("name", pf.DataType.string()),
+ ... ("age", pf.DataType.int32()),
+ ... ])
+
+ .. versionadded:: 2.4.0
+ """
+ field_items = fields.items() if isinstance(fields, dict) else fields
+
+ return cls(
+ DataTypes.ROW(
+ [
+ DataTypes.FIELD(name, data_type._to_table_data_type())
+ for name, data_type in field_items
+ ]
+ )
+ )
+
+ @classmethod
+ def _from_type_hint(cls, type_hint: Any) -> "DataType":
+ def infer_union_type(hint: Any, arguments: Tuple[Any, ...]) ->
"DataType":
+ non_none_types = [
+ argument for argument in arguments if argument is not
type(None)
+ ]
+ if len(non_none_types) == 1:
+ return infer(non_none_types[0]).nullable()
+
+ raise TypeError(
+ f"Cannot infer DataType from type hint '{hint}'. "
+ "Please specify the data type explicitly."
+ )
+
+ def infer_basic_type(hint: Any) -> Optional["DataType"]:
+ factory = _BASIC_TYPE_HINT_FACTORIES.get(hint)
+ return cls(factory()) if factory is not None else None
+
+ def infer(hint: Any) -> "DataType":
+ origin = get_origin(hint)
+ arguments = get_args(hint)
+
+ if origin is Union or (
+ _PEP_604_UNION_TYPE is not None
+ and origin is _PEP_604_UNION_TYPE
+ ):
+ return infer_union_type(hint, arguments)
+
+ if origin is list:
+ if not arguments:
+ raise TypeError(
+ "Cannot infer DataType from list without type
argument. "
+ "Use list[T], for example list[int]."
+ )
+ return cls.list(infer(arguments[0]))
+
+ if origin is dict:
+ if len(arguments) != 2:
+ raise TypeError(
+ "Cannot infer DataType from dict without key and value
type arguments. "
+ "Use dict[K, V], for example dict[str, int]."
+ )
+ return cls.map(
+ infer(arguments[0]),
+ infer(arguments[1]),
+ )
+
+ data_type = infer_basic_type(hint)
+ if data_type is not None:
+ return data_type
+
+ raise TypeError(
+ f"Cannot infer DataType from type hint '{hint}'. "
+ "Please specify the data type explicitly."
+ )
+
+ return infer(type_hint)
+
+ @classmethod
+ def _from_sql(cls, sql_type: str) -> "DataType":
+ """
+ Create a data type from its SQL representation.
+
+ :param sql_type: SQL data type string, such as ``INT`` or
``ARRAY<BIGINT>``.
+ :raises ValueError: If the SQL data type cannot be parsed.
+ """
+ from py4j.protocol import Py4JJavaError
+
+ from pyflink.java_gateway import get_gateway
+ from pyflink.table.types import _from_java_data_type
+ from pyflink.util.exceptions import JavaException
+
+ try:
+ gateway = get_gateway()
+ j_logical_type = (
+
gateway.jvm.org.apache.flink.table.types.logical.utils.LogicalTypeParser.parse(
+ sql_type,
+ gateway.jvm.Thread.currentThread().getContextClassLoader(),
+ )
+ )
+ j_data_type = (
+ gateway.jvm.org.apache.flink.table.types.utils.TypeConversions
+ .fromLogicalToDataType(j_logical_type)
+ )
+ return cls(_from_java_data_type(j_data_type))
+ except (JavaException, Py4JJavaError) as exc:
+ raise ValueError(str(exc)) from None
+
def _to_table_data_type(self) -> TableDataType:
return self._table_data_type
diff --git a/flink-python/pyflink/dataframe/tests/test_datatype.py
b/flink-python/pyflink/dataframe/tests/test_datatype.py
index 756bba371ea..f129f9a5eb6 100644
--- a/flink-python/pyflink/dataframe/tests/test_datatype.py
+++ b/flink-python/pyflink/dataframe/tests/test_datatype.py
@@ -16,26 +16,128 @@
# limitations under the License.
################################################################################
+import datetime
+import decimal
+import sys
import unittest
+from typing import Any, List, Optional, Union
import pyflink.dataframe as pf
from pyflink.table import DataTypes
-from pyflink.util.api_stability_decorators import PublicEvolving
+from pyflink.testing.test_case_utils import PyFlinkTestCase
class DataTypeTests(unittest.TestCase):
- def test_public_factory_surface(self):
+ def test_public_api_surface(self):
public_methods = {
name for name in dir(pf.DataType) if not name.startswith("_")
}
- self.assertEqual(public_methods, {"int64", "string"})
+ self.assertEqual(
+ public_methods,
+ {
+ "binary",
+ "bool",
+ "date",
+ "decimal",
+ "fixed_size_binary",
+ "fixed_size_string",
+ "float32",
+ "float64",
+ "int8",
+ "int16",
+ "int32",
+ "int64",
+ "list",
+ "map",
+ "not_null",
+ "null",
+ "nullable",
+ "string",
+ "struct",
+ "time",
+ "timestamp",
+ "timestamp_ltz",
+ },
+ )
- def test_int64_maps_to_table_bigint(self):
- self.assertEqual(pf.DataType.int64()._to_table_data_type(),
DataTypes.BIGINT())
+ def test_scalar_factories_map_to_table_types(self):
+ expected_types = {
+ "binary": DataTypes.BYTES(),
+ "bool": DataTypes.BOOLEAN(),
+ "date": DataTypes.DATE(),
+ "float32": DataTypes.FLOAT(),
+ "float64": DataTypes.DOUBLE(),
+ "int8": DataTypes.TINYINT(),
+ "int16": DataTypes.SMALLINT(),
+ "int32": DataTypes.INT(),
+ "int64": DataTypes.BIGINT(),
+ "null": DataTypes.NULL(),
+ "string": DataTypes.STRING(),
+ }
+
+ for factory_name, expected_type in expected_types.items():
+ with self.subTest(factory_name=factory_name):
+ data_type = getattr(pf.DataType, factory_name)()
+ self.assertEqual(data_type._to_table_data_type(),
expected_type)
+
+ def test_parameterized_factories_map_to_table_types(self):
+ test_cases = [
+ (
+ "decimal",
+ pf.DataType.decimal,
+ {"precision": 10, "scale": 3},
+ DataTypes.DECIMAL(10, 3),
+ ),
+ (
+ "fixed_size_binary",
+ pf.DataType.fixed_size_binary,
+ {"length": 16},
+ DataTypes.BINARY(16),
+ ),
+ (
+ "fixed_size_string",
+ pf.DataType.fixed_size_string,
+ {"length": 12},
+ DataTypes.CHAR(12),
+ ),
+ ("time_default", pf.DataType.time, {}, DataTypes.TIME(0)),
+ (
+ "time_precision",
+ pf.DataType.time,
+ {"precision": 3},
+ DataTypes.TIME(3),
+ ),
+ (
+ "timestamp_default",
+ pf.DataType.timestamp,
+ {},
+ DataTypes.TIMESTAMP(6),
+ ),
+ (
+ "timestamp_precision",
+ pf.DataType.timestamp,
+ {"precision": 3},
+ DataTypes.TIMESTAMP(3),
+ ),
+ (
+ "timestamp_ltz_default",
+ pf.DataType.timestamp_ltz,
+ {},
+ DataTypes.TIMESTAMP_LTZ(6),
+ ),
+ (
+ "timestamp_ltz_precision",
+ pf.DataType.timestamp_ltz,
+ {"precision": 3},
+ DataTypes.TIMESTAMP_LTZ(3),
+ ),
+ ]
- def test_string_maps_to_table_string(self):
- self.assertEqual(pf.DataType.string()._to_table_data_type(),
DataTypes.STRING())
+ for factory_name, factory, arguments, expected_type in test_cases:
+ with self.subTest(factory_name=factory_name):
+ data_type = factory(**arguments)
+ self.assertEqual(data_type._to_table_data_type(),
expected_type)
def test_logically_equal_types_compare_and_hash_equally(self):
first_int = pf.DataType.int64()
@@ -46,19 +148,253 @@ class DataTypeTests(unittest.TestCase):
self.assertNotEqual(first_int, string)
self.assertEqual(len({first_int, second_int, string}), 2)
- def test_equality_and_hash_are_public_evolving(self):
- for method in [pf.DataType.__eq__, pf.DataType.__hash__]:
- with self.subTest(method=method.__name__):
- self.assertIn(
- PublicEvolving,
- getattr(method, "__stability_decorators", set()),
+ def test_nullability_participates_in_equality_and_hashing(self):
+ nullable = pf.DataType.int32()
+ first_non_nullable = nullable.not_null()
+ second_non_nullable = pf.DataType.int32().not_null()
+
+ self.assertNotEqual(nullable, first_non_nullable)
+ self.assertEqual(first_non_nullable, second_non_nullable)
+ self.assertEqual(hash(first_non_nullable), hash(second_non_nullable))
+ self.assertEqual(len({nullable, first_non_nullable}), 2)
+
+ def test_nullability_modifiers_preserve_the_original_type(self):
+ original = pf.DataType.int32()
+
+ non_nullable = original.not_null()
+ nullable_again = non_nullable.nullable()
+
+ self.assertEqual(original._to_table_data_type(), DataTypes.INT())
+ self.assertEqual(
+ non_nullable._to_table_data_type(),
+ DataTypes.INT().not_null(),
+ )
+ self.assertEqual(nullable_again._to_table_data_type(), DataTypes.INT())
+
+ def test_null_type_cannot_be_made_non_nullable(self):
+ with self.assertRaisesRegex(ValueError, "NULL"):
+ pf.DataType.null().not_null()
+
+ def test_list_preserves_its_element_type(self):
+ list_type = pf.DataType.list(dtype=pf.DataType.int32().not_null())
+
+ self.assertEqual(
+ list_type._to_table_data_type(),
+ DataTypes.ARRAY(DataTypes.INT().not_null()),
+ )
+
+ def test_map_preserves_its_key_and_value_types(self):
+ map_type = pf.DataType.map(
+ key_type=pf.DataType.string(),
+ value_type=pf.DataType.int64(),
+ )
+
+ self.assertEqual(
+ map_type._to_table_data_type(),
+ DataTypes.MAP(DataTypes.STRING(), DataTypes.BIGINT()),
+ )
+
+ def test_struct_preserves_dict_insertion_order(self):
+ struct_type = pf.DataType.struct(
+ fields={
+ "name": pf.DataType.string(),
+ "age": pf.DataType.int32().not_null(),
+ }
+ )
+
+ self.assertEqual(
+ struct_type._to_table_data_type(),
+ DataTypes.ROW(
+ [
+ DataTypes.FIELD("name", DataTypes.STRING()),
+ DataTypes.FIELD("age", DataTypes.INT().not_null()),
+ ]
+ ),
+ )
+
+ def test_struct_preserves_list_field_order(self):
+ struct_type = pf.DataType.struct(
+ fields=[
+ ("age", pf.DataType.int32().not_null()),
+ ("name", pf.DataType.string()),
+ ]
+ )
+
+ self.assertEqual(
+ struct_type._to_table_data_type(),
+ DataTypes.ROW(
+ [
+ DataTypes.FIELD("age", DataTypes.INT().not_null()),
+ DataTypes.FIELD("name", DataTypes.STRING()),
+ ]
+ ),
+ )
+
+ def test_from_basic_python_type_hints(self):
+ expected_types = {
+ bool: pf.DataType.bool(),
+ int: pf.DataType.int64(),
+ float: pf.DataType.float64(),
+ str: pf.DataType.string(),
+ bytes: pf.DataType.binary(),
+ bytearray: pf.DataType.binary(),
+ decimal.Decimal: pf.DataType.decimal(38, 18),
+ datetime.date: pf.DataType.date(),
+ datetime.time: pf.DataType.time(),
+ datetime.datetime: pf.DataType.timestamp(),
+ Any: pf.DataType.string(),
+ }
+
+ for python_type, expected_type in expected_types.items():
+ with self.subTest(python_type=python_type):
+ self.assertEqual(
+ pf.DataType._from_type_hint(python_type),
+ expected_type,
+ )
+
+ def test_from_optional_type_hint(self):
+ self.assertEqual(
+ pf.DataType._from_type_hint(Optional[int]),
+ pf.DataType.int64(),
+ )
+
+ @unittest.skipIf(
+ sys.version_info < (3, 10),
+ "PEP 604 union types require Python 3.10 or later",
+ )
+ def test_from_pep_604_union_type_hint(self):
+ self.assertEqual(
+ pf.DataType._from_type_hint(int | None),
+ pf.DataType.int64(),
+ )
+ self.assertEqual(
+ pf.DataType._from_type_hint(list[int | None]),
+ pf.DataType.list(dtype=pf.DataType.int64()),
+ )
+ with self.assertRaises(TypeError):
+ pf.DataType._from_type_hint(int | str)
+
+ def test_from_list_type_hint(self):
+ self.assertEqual(
+ pf.DataType._from_type_hint(list[int]),
+ pf.DataType.list(dtype=pf.DataType.int64()),
+ )
+
+ def test_from_dict_type_hint(self):
+ self.assertEqual(
+ pf.DataType._from_type_hint(dict[str, float]),
+ pf.DataType.map(
+ key_type=pf.DataType.string(),
+ value_type=pf.DataType.float64(),
+ ),
+ )
+
+ def test_from_type_hint_rejects_ambiguous_or_incomplete_hints(self):
+ invalid_hints = [
+ List,
+ complex,
+ ]
+
+ for type_hint in invalid_hints:
+ with self.subTest(type_hint=type_hint):
+ with self.assertRaises(TypeError):
+ pf.DataType._from_type_hint(type_hint)
+
+ def test_from_type_hint_reports_ambiguous_union_error(self):
+ with self.assertRaises(TypeError) as context:
+ pf.DataType._from_type_hint(Union[int, str])
+
+ self.assertEqual(
+ "Cannot infer DataType from type hint 'typing.Union[int, str]'. "
+ "Please specify the data type explicitly.",
+ str(context.exception),
+ )
+
+ def test_repr_preserves_type_parameters_and_nested_nullability(self):
+ self.assertEqual(
+ repr(
+ pf.DataType.list(
+ pf.DataType.struct(
+ [
+ (
+ "amount",
+ pf.DataType.decimal(10, 2).not_null(),
+ )
+ ]
+ ).not_null()
+ )
+ ),
+ "DataType(ArrayType("
+ "RowType(RowField(amount, DecimalType(10, 2, false), ...), false),
"
+ "true))",
+ )
+
+
+class DataTypeExpressionTests(PyFlinkTestCase):
+ def test_expression_casts_accept_dataframe_data_types(self):
+ test_cases = [
+ ("cast", "cast(value, DOUBLE)"),
+ ("try_cast", "TRY_CAST(value, DOUBLE)"),
+ ]
+
+ for operation, expected_expression in test_cases:
+ with self.subTest(operation=operation):
+ expression = getattr(pf.col("value"), operation)(
+ pf.DataType.float64()
)
- def test_nullability_modifiers_are_not_exposed(self):
- for data_type in [pf.DataType.int64(), pf.DataType.string()]:
- with self.subTest(data_type=data_type):
- self.assertFalse(hasattr(data_type, "not_null"))
- self.assertFalse(hasattr(data_type, "nullable"))
+ self.assertEqual(str(expression), expected_expression)
+
+
+class DataTypeSqlTests(PyFlinkTestCase):
+ def test_from_sql_parses_scalar_and_nested_types(self):
+ test_cases = [
+ ("INT", pf.DataType.int32()),
+ (
+ "DECIMAL(10, 3) NOT NULL",
+ pf.DataType.decimal(10, 3).not_null(),
+ ),
+ (
+ "ROW<name STRING, scores ARRAY<DOUBLE NOT NULL>>",
+ pf.DataType.struct(
+ fields=[
+ ("name", pf.DataType.string()),
+ (
+ "scores",
+ pf.DataType.list(
+ dtype=pf.DataType.float64().not_null()
+ ),
+ ),
+ ]
+ ),
+ ),
+ ]
+
+ for sql_type, expected_type in test_cases:
+ with self.subTest(sql_type=sql_type):
+ self.assertEqual(pf.DataType._from_sql(sql_type),
expected_type)
+
+ def test_from_sql_preserves_timestamp_precision(self):
+ self.assertEqual(
+ pf.DataType._from_sql("TIMESTAMP(9)"),
+ pf.DataType.timestamp(precision=9),
+ )
+
+ def test_from_sql_preserves_timestamp_ltz_precision(self):
+ self.assertEqual(
+ pf.DataType._from_sql("TIMESTAMP_LTZ(3)"),
+ pf.DataType.timestamp_ltz(precision=3),
+ )
+
+ def test_from_sql_supports_null_type(self):
+ self.assertEqual(
+ pf.DataType._from_sql("NULL"),
+ pf.DataType.null(),
+ )
+
+ def test_from_sql_reports_parser_errors_as_value_errors(self):
+ with self.assertRaises(ValueError):
+ pf.DataType._from_sql("VARCHAR(test)")
if __name__ == "__main__":
diff --git a/flink-python/pyflink/table/expression.py
b/flink-python/pyflink/table/expression.py
index 82c49f2edc6..aa3b952f215 100644
--- a/flink-python/pyflink/table/expression.py
+++ b/flink-python/pyflink/table/expression.py
@@ -20,7 +20,12 @@ from typing import Union, TypeVar, Generic, Any
from pyflink import add_version_doc
from pyflink.java_gateway import get_gateway
-from pyflink.table.types import DataType, DataTypes, _to_java_data_type
+from pyflink.table.types import (
+ DataType,
+ DataTypes,
+ _TableDataTypeLike,
+ _to_java_data_type,
+)
from pyflink.util.api_stability_decorators import PublicEvolving
from pyflink.util.java_utils import to_jarray
@@ -886,7 +891,7 @@ class Expression(Generic[T]):
"""
return _binary_op("asArgument")(self, name)
- def cast(self, data_type: DataType) -> 'Expression':
+ def cast(self, data_type: _TableDataTypeLike) -> 'Expression':
"""
Returns a new value being cast to type type.
A cast error throws an exception and fails the job.
@@ -900,7 +905,7 @@ class Expression(Generic[T]):
"""
return _binary_op("cast")(self, _to_java_data_type(data_type))
- def try_cast(self, data_type: DataType) -> 'Expression':
+ def try_cast(self, data_type: _TableDataTypeLike) -> 'Expression':
"""
Like cast, but in case of error, returns NULL rather than failing the
job.
diff --git a/flink-python/pyflink/table/tests/test_types.py
b/flink-python/pyflink/table/tests/test_types.py
index d3bd37c6ffd..f8124ba5e8c 100644
--- a/flink-python/pyflink/table/tests/test_types.py
+++ b/flink-python/pyflink/table/tests/test_types.py
@@ -128,6 +128,18 @@ class UTCOffsetTimezone(datetime.tzinfo):
class TypesTests(PyFlinkTestCase):
+ def test_row_type_repr_includes_nullability(self):
+ row_type = RowType([RowField("id", BigIntType())])
+
+ self.assertEqual(
+ "RowType(RowField(id, BigIntType(true), ...), true)",
+ repr(row_type),
+ )
+ self.assertEqual(
+ "RowType(RowField(id, BigIntType(true), ...), false)",
+ repr(row_type.not_null()),
+ )
+
def test_infer_schema(self):
from decimal import Decimal
@@ -170,14 +182,14 @@ class TypesTests(PyFlinkTestCase):
'DoubleType(true)',
"ArrayType(DoubleType(false), true)",
"ArrayType(BigIntType(true), true)",
- 'RowType(RowField(_1, BigIntType(true), ...))',
- 'RowType(RowField(x, DoubleType(true), ...),RowField(y,
DoubleType(true), ...))',
+ 'RowType(RowField(_1, BigIntType(true), ...), true)',
+ 'RowType(RowField(x, DoubleType(true), ...),RowField(y,
DoubleType(true), ...), true)',
'MapType(VarCharType(2147483647, false), BigIntType(true), true)',
'VarBinaryType(2147483647, true)',
'DecimalType(38, 18, true)',
- 'RowType(RowField(a, BigIntType(true), ...))',
- 'RowType(RowField(a, BigIntType(true), ...))',
- 'RowType(RowField(a, BigIntType(true), ...))',
+ 'RowType(RowField(a, BigIntType(true), ...), true)',
+ 'RowType(RowField(a, BigIntType(true), ...), true)',
+ 'RowType(RowField(a, BigIntType(true), ...), true)',
]
schema = _infer_schema_from_data([data])
diff --git a/flink-python/pyflink/table/types.py
b/flink-python/pyflink/table/types.py
index b62b55af9e1..964097f25e2 100644
--- a/flink-python/pyflink/table/types.py
+++ b/flink-python/pyflink/table/types.py
@@ -29,7 +29,7 @@ from functools import reduce
from threading import RLock
from py4j.java_gateway import get_java_class
-from typing import List, Union
+from typing import List, Protocol, Union
from pyflink.common.types import _create_row
from pyflink.util.api_stability_decorators import PublicEvolving
@@ -123,6 +123,14 @@ class DataType(object):
return obj
+class _SupportsToTableDataType(Protocol):
+ def _to_table_data_type(self) -> DataType:
+ ...
+
+
+_TableDataTypeLike = Union[DataType, _SupportsToTableDataType]
+
+
class AtomicType(DataType):
"""
An internal type used to represent everything that is not
@@ -1216,7 +1224,8 @@ class RowType(DataType):
raise TypeError('RowType keys should be strings, integers or
slices')
def __repr__(self):
- return "RowType(%s)" % ",".join(repr(field) for field in self)
+ fields = ",".join(repr(field) for field in self)
+ return f"RowType({fields}, {str(self._nullable).lower()})"
def field_names(self):
"""
@@ -1709,7 +1718,8 @@ def _from_java_data_type(j_data_type):
elif is_instance_of(logical_type, gateway.jvm.TimeType):
data_type = DataTypes.TIME(logical_type.getPrecision(),
logical_type.isNullable())
elif is_instance_of(logical_type, gateway.jvm.TimestampType):
- data_type = DataTypes.TIMESTAMP(precision=3,
nullable=logical_type.isNullable())
+ data_type = DataTypes.TIMESTAMP(
+ precision=logical_type.getPrecision(),
nullable=logical_type.isNullable())
elif is_instance_of(logical_type, gateway.jvm.BooleanType):
data_type = DataTypes.BOOLEAN(logical_type.isNullable())
elif is_instance_of(logical_type, gateway.jvm.TinyIntType):
@@ -1729,7 +1739,8 @@ def _from_java_data_type(j_data_type):
TypeError("Unsupported type: %s, ZonedTimestampType is not
supported yet."
% j_data_type)
elif is_instance_of(logical_type, gateway.jvm.LocalZonedTimestampType):
- data_type =
DataTypes.TIMESTAMP_WITH_LOCAL_TIME_ZONE(nullable=logical_type.isNullable())
+ data_type = DataTypes.TIMESTAMP_WITH_LOCAL_TIME_ZONE(
+ precision=logical_type.getPrecision(),
nullable=logical_type.isNullable())
elif is_instance_of(logical_type, gateway.jvm.DayTimeIntervalType) or \
is_instance_of(logical_type,
gateway.jvm.YearMonthIntervalType):
data_type = _from_java_interval_type(logical_type)
@@ -1759,6 +1770,8 @@ def _from_java_data_type(j_data_type):
% type_info)
elif is_instance_of(logical_type, gateway.jvm.RawType):
data_type = RawType()
+ elif is_instance_of(logical_type, gateway.jvm.NullType):
+ data_type = DataTypes.NULL()
else:
raise TypeError("Unsupported type: %s, it is not supported yet in
current python type"
" system" % j_data_type)
@@ -1821,10 +1834,15 @@ def _from_java_data_type(j_data_type):
TypeError("Unsupported data type: %s" % j_data_type)
-def _to_java_data_type(data_type: DataType):
+def _to_java_data_type(data_type: _TableDataTypeLike):
"""
Converts the specified Python DataType to Java DataType.
"""
+ if not isinstance(data_type, DataType):
+ to_table_data_type = getattr(data_type, "_to_table_data_type", None)
+ if callable(to_table_data_type):
+ data_type = to_table_data_type()
+
gateway = get_gateway()
JDataTypes = gateway.jvm.org.apache.flink.table.api.DataTypes