Skip to content

[perf](udf) Write terminal UDF output directly to Parquet - #541

Draft
kaka11chen wants to merge 1 commit into
AstroVela:mainfrom
kaka11chen:feat/udf-arrow-parquet-direct-write
Draft

kaka11chen wants to merge 1 commit into
AstroVela:mainfrom
kaka11chen:feat/udf-arrow-parquet-direct-write

Conversation

@kaka11chen

Copy link
Copy Markdown
Contributor

Summary

  • Fuse a terminal distributed Python UDF with Parquet COPY and stream its Arrow output directly into a PyArrow ParquetWriter, avoiding the Arrow-to-DuckDB-to-Parquet round trip.
  • Preserve declared schemas, empty-file behavior, six-column worker COPY statistics, and staging/direct-write/retry cleanup across subprocess task/actor and Ray task/actor backends.
  • Propagate equivalent compression, row-group, dictionary-page, and Parquet-version settings. Non-terminal UDF plans and COPY options that cannot be represented by the direct writer now fail explicitly; there is intentionally no compatibility fallback.
  • Fence Ray output-lease disposition callbacks so a handoff/release failure cannot be overtaken by successful query completion or affect another query slot.

Related issue

N/A

Documentation impact

  • No user-facing documentation update is required.
  • Documentation is updated in this PR or in a linked website PR.
  • A follow-up issue is required in
    AstroVela/vane-website.

The public Relation.write_parquet API is unchanged. The intentional behavior change for unsupported terminal-UDF COPY shapes/options is documented in the summary above.

Validation

  • Incremental Release native install: SKBUILD_BUILD_DIR="$PWD/build/python-release" SKBUILD_CMAKE_BUILD_TYPE=Release uv pip install . --no-build-isolation.
  • Affected UDF, Arrow Parquet sink, local task/actor, and Ray task/actor tests: 82 passed.
  • Output-lease callback race test: 20 consecutive passes.
  • Native distributed suite: VCPKG_INSTALLED_DIR="$PWD/vcpkg_installed" scripts/run_native_tests.sh "[distributed]" — 187 test cases and 7,988 assertions passed.
  • Release suite: scripts/run_release_tests.sh — 538 passed (15 deselected), plus 11 real-Ray tests passed (542 deselected).
  • Complete fast suite: scripts/run_fast_tests.sh — 6,206 passed, 602 skipped, 120 deselected, 7 xfailed, and 1 xpassed in the non-Ray shard; 103 passed and 6 skipped in the shared-Ray shard; all seven CPU-only Ray-owner tests passed, with the unavailable vLLM test skipped.
  • Formatting and static checks: scripts/format workspace --changed --check, pre-commit on every changed file, and git diff --check passed.
  • Environment: CPU-only local and local-Ray runners. Direct-write E2E coverage used synthetic 257-row local input (batch_size=64, row_group_size=32) and 17-row Ray input (batch_size=4, row_group_size=3) across task and actor backends.

Checklist

  • The change is focused and includes tests or a reason tests are unnecessary.
  • Public behavior and compatibility impact are documented.
  • New dependencies, copied code, model assets, and datasets have compatible
    licenses and are recorded where required.
  • No credentials, private endpoints, personal paths, generated data, model
    weights, or build artifacts are included.
  • Security implications of UDFs, serialization, remote code, network access,
    and untrusted input have been considered.
  • Native changes were compiled; Python-only, documentation, and workflow
    changes passed relevant checks.

Copilot AI lite review requested due to automatic review settings August 10, 2026 16:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9425e9857b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +100 to +102
if "://" in directory or directory.startswith("file:"):
filesystem, filesystem_directory = pafs.FileSystem.from_uri(directory)
file_path = posixpath.join(filesystem_directory.rstrip("/"), file_name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve DuckDB filesystem credentials for remote outputs

When a terminal UDF writes to a remote URI using credentials configured through DuckDB (for example, an S3 CREATE SECRET), constructing a new PyArrow filesystem with FileSystem.from_uri() bypasses DuckDB's filesystem instance and its secret/configuration state. The same write_parquet call that previously used DuckDB's configured filesystem will therefore fail unless equivalent credentials happen to be available independently to PyArrow on every worker; the direct sink must propagate the configured filesystem credentials or decline fusion for these destinations.

Useful? React with 👍 / 👎.

Comment on lines +61 to +66
def _expected_schema(payload: dict[str, Any]) -> pa.Schema:
names = [str(value) for value in payload.get(_EXPECTED_NAMES) or []]
type_names = [str(value) for value in payload.get(_EXPECTED_TYPES) or []]
if not names or len(names) != len(type_names):
raise ValueError("terminal Arrow Parquet sink requires matching expected names and types")
return pa.schema([pa.field(name, _arrow_type_from_name(type_name)) for name, type_name in zip(names, type_names)])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Support all valid declared UDF output types

For a terminal UDF whose declared schema contains a valid DuckDB type not handled by _arrow_type_from_duckdb_pytype, such as UUID, TIMESTAMPTZ, or UHUGEINT, this unconditional schema reconstruction raises unsupported DuckDB type id before any output is written. Nonempty outputs of those types previously flowed through DuckDB's Arrow conversion and Parquet writer, so either map their Arrow representations here or avoid the fused path for unsupported schemas.

Useful? React with 👍 / 👎.

Comment on lines +469 to +472
options.emplace_back("row_group_size", Value::UBIGINT(bind_data.row_group_size));
options.emplace_back("dictionary_pagesize_limit", Value::UBIGINT(bind_data.string_dictionary_page_size_limit));
options.emplace_back("version", Value(bind_data.parquet_version == ParquetVersion::V2 ? "2.6" : "1.0"));
options.emplace_back("data_page_version", Value("1.0"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep nanosecond timestamps writable with default Parquet settings

When the terminal UDF schema includes TIMESTAMP_NS, the default DuckDB ParquetVersion::V1 is translated to PyArrow version="1.0"; PyArrow cannot represent nanosecond timestamps in that format version without coercion and raises when values would lose precision. DuckDB's V1/V2 setting controls permitted encodings rather than PyArrow's logical-type format version, so this mapping makes a previously writable default-schema case fail; use a PyArrow format version that supports nanoseconds while configuring encodings separately.

Useful? React with 👍 / 👎.

@kaka11chen
kaka11chen marked this pull request as draft August 11, 2026 03:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants