Skip to content

[FLINK-40190][python] Add PyFlink DataFrame creation and conversion APIs - #28934

Open
auroflow wants to merge 7 commits into
apache:masterfrom
auroflow:codex/flink-40190-dataframe-conversion
Open

[FLINK-40190][python] Add PyFlink DataFrame creation and conversion APIs#28934
auroflow wants to merge 7 commits into
apache:masterfrom
auroflow:codex/flink-40190-dataframe-conversion

Conversation

@auroflow

@auroflow auroflow commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

What is the purpose of the change

This pull request completes the data creation and conversion APIs for the PyFlink DataFrame API described in FLINK-40190.

It adds creation from pandas DataFrames, PyArrow tables, PyFlink Tables, dictionaries, records, and integer ranges. It also supports conversion back to Table and pandas, explicit column names, and event-time watermarks where applicable.

Brief change log

  • Add from_pandas(), from_arrow(), from_table(), and range(), and complete the existing from_dict() and from_records() creators.
  • Add DataFrame.to_table() and DataFrame.to_pandas().

Verifying this change

This change added tests and can be verified as follows:

  • Added plain unit tests for schema and watermark validation, record normalization, range behavior, Arrow batch splitting, and empty Arrow streams.
  • Added planner-backed tests for pandas and Arrow schema inference, positional renaming, typed empty inputs, watermark metadata, and Table identity.
  • Added wrapper tests for to_table() and to_pandas().
  • Added an integration smoke test covering from_arrow() → DataFrame transformation → to_pandas().

Does this pull request potentially affect one of the following parts:

  • Dependencies (does it add or upgrade a dependency): no
  • The public API, i.e., is any changed class annotated with @Public(Evolving): yes
  • The serializers: yes — Python in-memory and Arrow source serialization only; no state or persisted serialization format changes
  • The runtime per-record code paths (performance sensitive): no
  • Anything that affects deployment or recovery: JobManager (and its components), Checkpointing, Kubernetes/Yarn, ZooKeeper: no
  • The S3 file system connector: no

Documentation

  • Does this pull request introduce a new feature? yes
  • If yes, how is the feature documented? PyDocs and public API docstrings

Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)

Generated-by: Codex (GPT-5)

Add pandas, Arrow, Table, and range creators with strict schema and watermark validation. Add DataFrame conversion wrappers and schema-aware in-memory and Arrow source paths.

Generated-by: Codex (GPT-5)
Align timezone-aware Arrow timestamps with existing Table API semantics, delegate pandas creation to the Arrow path, and add split-aware Arrow IPC serialization. Refine watermark and row helpers with focused tests.

Generated-by: Codex (GPT-5)
Combine inferred-schema row conversion with DataFrame creation and let range use its known BIGINT schema directly.

Generated-by: Codex (GPT-5)
@flinkbot

flinkbot commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

CI report:

Bot commands The @flinkbot bot supports the following commands:
  • @flinkbot run azure re-run the last Azure build

Reject ranges whose emitted values exceed signed BIGINT bounds before creating the underlying table. Cover valid boundary values and ascending and descending overflow cases.

Generated-by: Codex (GPT-5)
@auroflow
auroflow force-pushed the codex/flink-40190-dataframe-conversion branch from f53446b to 654b9ca Compare August 6, 2026 09:40
Clarify that to_table does not execute a job, simplify the DataFrame creation and results reference pages, and exercise from_pandas through filtering, projection, and conversion in the existing pandas round-trip integration smoke test.

Generated-by: Codex (GPT-5)
@auroflow
auroflow force-pushed the codex/flink-40190-dataframe-conversion branch from 66e5281 to e693442 Compare August 7, 2026 03:02
@auroflow
auroflow marked this pull request as ready for review August 7, 2026 03:09
Inline the single-use Arrow IPC writer into TableEnvironment._from_arrow and remove its helper-specific tests. This also avoids the NamedTemporaryFile wrapper type mismatch reported by mypy.

Generated-by: Codex (GPT-5)
return column_names


def _parse_watermark(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this under the _WaterMarkSpec class? Like:

class Watermark:


     @classmethod
     def parse(cls, ....):
         ....
         return cls(....)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I moved it under _WaterMarkSpec.

return _WatermarkSpec(*watermark)


def _normalize_watermark_row_type(row_type: RowType, watermark: _WatermarkSpec) -> RowType:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this under the _WaterMarkSpec class? Like:

class Watermark:

    def normalize_row_type(self, row_type: RowType) -> RowType:
        ...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved under _WaterMarkSpec.

table_schema = (
Schema.new_builder()
.from_row_data_type(row_type)
.watermark(watermark.column, watermark.expression)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since the watermark is a NamedTuple, you can simplify the call by unpacking it like:

schema.watermark(*watermark)

I feel like this both reads better and is more concise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, I changed this to unpacking.

return RowType(fields, row_type._nullable)


def _resolve_watermark_schema(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This helper function feels shallow and with only 2 uses a little unecessary. Thoughts on inlining the logic at the call sites like:

if watermark:
    row_type = watermark.normalize_row_type(row_type)
    table_schema = ...
else:
    table_schema = None

this feels a easier to follow to me

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I inlined this helper function.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since _WatermarkSpec is a NamedTuple you should add a quick test to verify unpacking order. This would help guard against errors caused by someone changing the internal field order or values. In particular, this would enable us to be confident that:

schema.watermark(*watermark)

won't quietly break in the future.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a test here to verify unpacking order.

@github-actions github-actions Bot added the community-reviewed PR has been reviewed by the community. label Aug 11, 2026
Encapsulate watermark parsing and row-type normalization, inline schema construction, and verify watermark unpacking order.

Generated-by: Codex (GPT-5)
names = _resolve_column_names(table.column_names, schema)
row_type = RowType(
[
RowField(name, from_arrow_type(field.type, field.nullable))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from_arrow_type() maps timezone-bearing Arrow timestamps to TIMESTAMP, and _from_arrow then casts away the timezone. With Asia/Shanghai, 2026-01-01 00:00+08:00 round-trips as naïve 2025-12-31 16:00, which also changes downstream event-time behavior.

import pyarrow as pa

return from_arrow(
pa.Table.from_pandas(pdf, preserve_index=False),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pa.Table.from_pandas rejects duplicate pandas column names before from_arrow can apply the supplied positional schema. Consequently a DataFrame with columns ['x', 'x'] cannot be converted even with schema=['left', 'right'], contrary to the positional-renaming contract. Assign temporary unique names or convert columns by position before constructing the Arrow table.

.watermark(*watermark_spec)
.build()
)
result = get_or_create_table_environment()._from_arrow(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arrow null fields become Flink NullType here, producing a DataFrame whose schema resolves but whose execution always fails with FlinkTypeFactory's 'null type is reserved' ValidationException. This occurs for common inputs such as pd.DataFrame({'x': [None]}), and the name-only schema offers no escape hatch. Reject these fields during creation or support an explicit typed schema.

raise TypeError("data must be a mapping")
if not data:
raise ValueError("data must not be empty")
watermark_spec = _WatermarkSpec.parse(watermark)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add an example about watermark usage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community-reviewed PR has been reviewed by the community.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants