Skip to content

feat: stream large Parquet uploads via presigned session API (0.7.1) - #46

Merged
eddietejeda merged 2 commits into
mainfrom
feat/streaming-multipart-upload
Jul 16, 2026
Merged

feat: stream large Parquet uploads via presigned session API (0.7.1)#46
eddietejeda merged 2 commits into
mainfrom
feat/streaming-multipart-upload

Conversation

@eddietejeda

Copy link
Copy Markdown
Contributor

Summary

  • Replaces the full-file f.read() in upload_parquet() with the presigned upload session API (POST /v1/uploads), so only one part_size chunk is resident in memory at a time during large Parquet loads
  • For multipart mode: iterates over part_urls, reading and PUTting one chunk per URL, collecting ETags for finalize
  • For single mode: reads the whole file (small files do not need chunking) and does a single PUT
  • Falls back to the original POST /v1/files path if the server returns 501 (storage backends without presigned URL support)
  • Uses urllib3.PoolManager directly (already in the dependency chain -- no new deps)
  • Bumps version to 0.7.1

Context

Root cause of the jaffle_shop Snowflake ingest OOM: upload_parquet() called f.read() on each Parquet file before uploading, so the entire file lived in memory. Intermediate mitigation (DATA_WRITER__FILE_MAX_BYTES=200MB in dlthubworker) limits file size, but loading still peaks at 200MB per file. This fix eliminates that peak.

After this ships: bump hotdata-dlt-destination lockfile and publish 0.9.2, then rebuild the dlthubworker Docker image. Once deployed, the 200MB cap in ingest_job.py can be loosened or removed.

Replaces the full-file f.read() in upload_parquet() with the presigned
upload session API, so only one part_size chunk (not the entire file) is
in memory at a time. Falls back to POST /v1/files when the server returns
501 (backends without presigned URL support). Bumps to 0.7.1.
collected.append(
FinalizeUploadPart(
part_number=i + 1,
e_tag=resp.headers["ETag"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

super nit: resp.headers["ETag"] raises a bare KeyError if a storage backend/proxy omits the ETag on a 2xx PUT, which would surface as an opaque traceback rather than the clean RuntimeError used for the other failure paths here. Consider guarding it, e.g. etag = resp.headers.get("ETag") and raising a descriptive error if missing. (not blocking)

Comment thread tests/test_databases.py
part_urls=None,
url=None,
)
return SimpleNamespace(mode=mode, **{**defaults, **kw})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: the session/finalized responses are faked with SimpleNamespace, so the attribute names the production code depends on (session.mode, session.url, session.part_urls, session.part_size, session.headers, session.finalize_token, finalized.upload_id) are never checked against the real generated hotdata response models. If those model field names drift from these assumptions, the tests still pass but the upload breaks at runtime. Worth at least one assertion that constructs the real response model, or a comment noting the field names are pinned to the SDK version. (not blocking)

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

Streaming upload logic is sound. Only non-blocking nits left inline.

@eddietejeda
eddietejeda enabled auto-merge (squash) July 16, 2026 00:03

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

Cycle 2: code is unchanged from cycle 1. No blocking issues. The two prior threads (ETag KeyError guard, test model-field pinning) remain valid non-blocking nits and stand as-is.

@eddietejeda
eddietejeda merged commit e0c65d0 into main Jul 16, 2026
4 checks passed
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.

1 participant