Skip to content

Add Options for DataFrameWriter - #167

Open
pequalsnp wants to merge 8 commits into
apache:masterfrom
pequalsnp:data-fram-write-options
Open

Add Options for DataFrameWriter#167
pequalsnp wants to merge 8 commits into
apache:masterfrom
pequalsnp:data-fram-write-options

Conversation

@pequalsnp

@pequalsnp pequalsnp commented Sep 5, 2025

Copy link
Copy Markdown

What changes were proposed in this pull request?

  • This pipes options through DataFrameWriter into the WriterOperation proto message.

Why are the changes needed?

  • Write options are supported in the API already, this just exposes the ability to set them in golang
  • Write options are needed for many output formats, in my specific case Opensearch

Does this PR introduce any user-facing change?

Yes!

It adds the ability to set options on DataFrameWriter

How was this patch tested?

Added a call to Option in the unit tests.
Added an integration test for writer options

@pequalsnp pequalsnp changed the title [WIP] Add Options for DataFrameWriter Add Options for DataFrameWriter Sep 5, 2025
@pequalsnp
pequalsnp marked this pull request as ready for review September 5, 2025 12:49

@caldempsey caldempsey 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.

Can you add an integration test to demonstrate usage and use a Dataframe option which would affect the displayed results? Maybe something like NULLVALUE -> NA.

See https://github.com/apache/spark-connect-go/tree/master/internal/tests/integration

@pequalsnp

Copy link
Copy Markdown
Author
➜  spark-connect-go git:(data-fram-write-options) ✗ SPARK_HOME=~/spark-4.0.1-bin-hadoop3 make integration
>> TEST, "integration"
ok      github.com/apache/spark-connect-go/internal/tests/integration   6.281s

@pequalsnp

Copy link
Copy Markdown
Author

Can you add an integration test to demonstrate usage and use a Dataframe option which would affect the displayed results? Maybe something like NULLVALUE -> NA.

See https://github.com/apache/spark-connect-go/tree/master/internal/tests/integration

I used the header option on the CSV reader/writer. Created a df, wrote it with a header, read it back with a header, verified there were only 2 lines

@pequalsnp
pequalsnp requested a review from caldempsey December 3, 2025 19:36
@pequalsnp
pequalsnp force-pushed the data-fram-write-options branch from 007e7c4 to c2fc9a0 Compare December 3, 2025 19:38
cantina-larry and others added 4 commits August 20, 2026 14:44
…tion

Reattachable execution was requested but never carried out: the stream discarded
the operationId it was given, and ToTable treated the end of the response stream
as the end of the result. The server ends a reattachable stream every
senderMaxStreamDuration (2m by default) expecting the client to resume, so any
query running longer than that failed.

ToTable now resumes on an early stream end and stops only at ResultComplete.
Empty rotations are normal -- a write emits nothing until it finishes -- so the
loop is bounded by ctx rather than by a retry count.

ToTable takes a context now; every caller already had one in scope.
Two behaviours the reference clients have that the reattach commit did not.

ReleaseExecute. A reattachable execution buffers responses server-side so a
resumed stream can backtrack, and the server only drops them when the client
says it is safe. Send release_until as each response is consumed and release_all
once the result is complete or the execution is abandoned. It is fire-and-forget
on a detached context: releasing is an optimisation, the server ages unreleased
executions out anyway, and failing a query because a cleanup RPC failed would be
worse than the buffering it avoids.

Restart on INVALID_HANDLE.OPERATION_NOT_FOUND / SESSION_NOT_FOUND. The client
sets its own operation id precisely so it can tell that the original ExecutePlan
never reached the server; when it did not, nothing ran and re-issuing it is
safe. That only holds while no response has been consumed -- afterwards a fresh
execution would replay them and duplicate rows, so that case fails instead.

Re-validated against EMR Serverless: a 400s statement still completes (402.5s,
against 402.5s before these additions).
…sumes

Follow-up fixes to the two ReattachExecute commits before this one.

Correctness:

- Only replay a plan that can be shown to have no side effects. Being rooted in
  a relation is not enough: Relation_Sql carries raw SQL text that the server
  applies while planning it -- which is why Sql submits the query as a command
  first -- and Relation_Catalog carries CreateTable and DropTempView. Sql's
  fallback path wraps the query text as a Relation_Sql, so an INSERT was
  reachable and would have been executed a second time. replayableRelation
  lists the shapes that are safe instead, so one it has not been taught about,
  including one added to the proto later, is refused rather than replayed. The
  refusal names the shape it saw, so a missing case is not mistaken for a
  deliberate one.

- Release the Arrow records the read loop accumulates. ReadArrowBatchToRecord
  already retains, and NewTableFromRecords takes its own references to the
  arrays underneath, so the extra Retain leaked a query's entire result.

- Stop reading once ResultComplete arrives. The execution has just been
  disowned with release_all, so an error on the following read would have
  failed a query whose result was complete and no longer recoverable.

- Restart an execution at most once, and refuse to restart into a session the
  server has forgotten. The reference clients do restart on SESSION_NOT_FOUND,
  but they also track the server's own session id across every response and
  fail if it changes; without that check a replay would silently run in a fresh
  empty session, without the temporary views, cached tables and SQL confs the
  caller set up. The divergence and the condition for removing it are recorded
  at the call site.

Resilience:

- Resume a stream that breaks mid-flight, not only one the server ends cleanly.
  Only the first stream comes from the retrying RPC client, so without this a
  long query was less able to survive a blip after its first rotation than
  before it.

- Bound that path with the retry policy's budget and backoff, reset by every
  response received. A clean rotation is paced by the server holding each
  stream open for senderMaxStreamDuration, but a stream that fails on its first
  Recv costs nothing, so it would otherwise have been reattached to in a tight
  loop for as long as the caller's context allowed.

API:

- Add SparkSessionBuilder.WithReattachExecution. Build was the only place
  client options were constructed for real use and it hardcoded the default, so
  the feature was unreachable no matter what the client supported.

Verified with go build, go vet, go test -race ./spark/... and golangci-lint
v2.1.6 (0 issues). Integration tests need a live Spark Connect server and were
not run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Implement ReattachExecute so long-running queries survive stream rotation
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.

3 participants