Skip to content

snowflake-jdbc v0.0.1

Pre-release
Pre-release

Choose a tag to compare

SNOW-3704961: chunked streaming PUT — upload RPC + Python file_stream

**Jira:** SNOW-3704961

## What
First of a stacked series replacing the buffered
`ConnectionUploadStream` RPC (single `bytes` field, whole payload
materialized in the wrapper) with a **chunked** one that keeps wrapper
memory bounded to one chunk.

Reference-matched to legacy JDBC's `FileBackedOutputStream`: the wrapper
streams the source in bounded chunks across the FFI; the core
re-materializes them into a re-readable hybrid buffer (RAM up to 128
MiB, spill to temp file beyond) and runs the **existing** upload path
unchanged — so SHA-256 digest, retry, CSE encryption, auto-compress, and
multipart all keep working exactly as for a file-path PUT.

## Design decision: store-and-forward upload (not true streaming)
The wrapper streams the source across the FFI in bounded chunks, but the
**core re-materializes the whole payload** (RAM up to 128 MiB, then
spilled to a temp file) **before** the GS round-trip and cloud upload
begin. This deliberately matches the reference drivers (JDBC
`FileBackedOutputStream`, legacy Python); it is **not** a true
byte-for-byte streaming pipeline. Documented in code at
`sf_core/src/apis/database_driver_v1/stream_transfer.rs` (module
"Operational constraints").

**Pluses**
- Reuses the existing, battle-tested file-path PUT pipeline
**unchanged**: SHA-256 digest, client-side encryption, auto-compress,
multipart, and **full retry** — including whole-transfer restart on
mid-upload credential/stage-token expiry (which requires a re-readable
source).
- Behavioral parity with JDBC / legacy Python; low risk — no new code in
the crypto/retry-critical path.
- Fixes the original problem: the **wrapper** never buffers the whole
file (bounded to ~one chunk).

**Minuses**
- The **core** is not zero-disk for uploads above the 128 MiB in-RAM
threshold: it needs local temp-disk headroom ≈ payload size, does
full-file disk I/O before the first byte reaches cloud storage, and
cannot overlap receive with upload (no streamed progress). A small /
read-only / full tmpdir fails the upload.

**Alternative considered & rejected:** a genuinely-streaming multipart
upload (overlap receive + upload, zero core disk). Rejected for this
stack: it *loses* whole-transfer retry (a consumed stream can't be
replayed), forces the client to pre-hash + pre-measure and drops
core-side auto-compress, and needs a new streaming-multipart
implementation in the crypto-critical path — substantially bigger and
riskier. Left as a documented future escape hatch. (Download, by
contrast, **is** true zero-disk streaming — see the download PRs.)

## Changes
- **Proto:** `UploadStreamHandle` +
`ConnectionUploadStreamBegin/Chunk/Finish/Abort` messages & RPCs (old
`ConnectionUploadStream` kept — JDBC still uses it until the
JDBC-migration PR).
- **Rust core:** `SpooledBuffer` (RAM→temp-file), `UploadStreamSession`
+ begin/chunk/finish/abort handlers, `run_put_stream_via_gs(ByteSource)`
extraction (old RPC delegates to it), handle-manager + proto glue.
- **Python:** `CHUNK_SIZE` (8 MiB) + `upload_stream_*` wrappers on
`CoreDriver`/`AsyncCoreDriver`; `cursor.execute(sql,
file_stream=<BinaryIO>)` (sync + async) via `_execute_upload_stream`.
Best-effort abort on failure never masks the original error. No
client-side PUT regex — the core validates.

## Tests
- **Unit** (mocked core): chunk-loop reassembly, abort-on-error,
finish-fails-then-abort-fails (original error propagates),
large/real-file/empty payloads, disk-spill (Mem→File) via the RPC
handler, sync + async.
- **e2e** (live Snowflake): upload+LS, multi-chunk round-trip,
`AUTO_COMPRESS`→`.gz`, non-PUT `ProgrammingError`.
- `cargo test -p sf_core` green (via `lld`; the sandbox's system `ld`
can't link the nix glibc/openssl — CI/proper build hosts unaffected).

## Full stack (draft series — review & merge bottom-up)

| # | PR | Scope |
|---|----|-------|
| 1 | #767 | chunked streaming **PUT** — upload RPC + Python
`file_stream` (base: `main`) |
| 2 | #813 | zero-disk **S3** streaming in `file_manager` (1/3 of former
#774) |
| 3 | #814 | chunked **download-stream RPCs** (S3) — proto + handlers +
FFI + cancellation (2/3) |
| 4 | #774 | Python **`cursor.download_stream`** over the chunk RPC
(3/3) |
| 5 | #780 | chunked streaming **GET (GCS + Azure)** — zero-disk
producers |
| 6 | #782 | migrate **JDBC** to the chunked RPCs; delete the old
buffered RPCs |

➡️ **You are here: #767**

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
GitOrigin-RevId: 75a52a16fca742f1ecbbd5de126d0840db51d508