feat(storage): in-memory stub - chunked upload (S06 Step 3)

Summary

S06 plan Step 3 — in-memory stub gains the chunked-upload Session surface (NewSession, Session.Write, Session.ReadFrom, Session.Close) and the conformance suite gains RunChunkedUpload. ResumeSession, GetSessionStatus, Session.Commit, and Session.Cancel still return storage.ErrUnsupportedMethod and ship in Step 4.

What changes

  • internal/storage/stub/session.goNewSession generates a UUIDv4, inserts an upload_sessions row (size_bytes=0, hash_state=nil, expires_at, created_at, updated_at) and seeds a *bytes.Buffer staging entry. Write appends to the staging buffer + SHA-256 hasher. ReadFrom uses bytes.Buffer.ReadFrom for in-place buffer growth (one fewer byte copy than io.ReadAll + Write) and hashes only the newly-read tail. Close marshals the hasher via encoding.BinaryMarshaler and mirrors size_bytes + hash_state + updated_at into the session row.
  • binaryHash interface — bundles hash.Hash, encoding.BinaryMarshaler, and encoding.BinaryUnmarshaler. stubSession.hasher is typed binaryHash so the marshal/unmarshal pair is part of the method set; the single-form sha256.New().(binaryHash) assertion at construction trips fast on any stdlib regression. Step 4 ResumeSession consumes the same field via UnmarshalBinary.
  • internal/storage/stub/stub.goobjectStore.staging is now map[sessionKey]*bytes.Buffer (was []byte); Write/ReadFrom mutate the buffer in place across calls without re-assigning the map entry. Stale unused lints on sessionKey are dropped; sessionRow lints updated to reference Step 4 consumption.
  • internal/storage/testsuites/chunked_upload.go (new) — RunChunkedUpload(t, factory) with four groups:
    • NewSession: fresh session has SizeBytes()==0 + non-nil UploadID(); two sessions have distinct upload IDs.
    • Write: single write returns full byte count; multiple writes accumulate (Step 3 acceptance criterion); zero-byte write is a no-op.
    • ReadFrom: streams reader bytes; matches an equivalent Write sequence on the same payload; empty reader is a no-op.
    • Close: preserves SizeBytes() accuracy (Step 3 acceptance); does not finalize the blob — BlobInfo misses and OpenBlob errors (Step 3 acceptance); succeeds on an empty session.
    • All assertions are interface-level; Step 8's real PostgreSQL impl will re-run the same suite unchanged.
  • internal/storage/stub/stub_test.goTestStub_ConformanceSuite now runs both RunSimpleOps and RunChunkedUpload as parallel subtests; the placeholder `ErrUnsupportedMethod` test now covers the four remaining session-lifecycle methods (Resume/GetStatus/Commit/Cancel) and is deleted in Step 4.

Refs

Test plan

  • go build ./...
  • go test -race ./internal/storage/...
  • mise exec -- golangci-lint run ./internal/storage/... — 0 issues
  • Conformance suite passes against stub.New factory (visible in test output)

🤖 Generated with Claude Code

MR size

Reviewable LOC: ~915 (~736 from the initial commit + a smaller follow-up addressing review feedback). This exceeds the project's 500-LOC ceiling from docs/dev/development-model.md.

The size is split roughly two-thirds tests / one-third implementation:

  • ~470 LOC is the new internal/storage/testsuites/chunked_upload.go — the parameterized conformance suite for the Session surface. The plan's Step 3 acceptance criterion requires the conformance cases to land with the implementation so that Step 8's real PostgreSQL impl re-runs the same contract.
  • ~180 LOC is the stub implementation (session.go Write/ReadFrom/Close, binaryHash interface, the *bytes.Buffer staging refactor in stub.go).
  • The remaining LOC is the test wiring (stub_test.go) and a small dispatcher-line in testsuites.go.

Splitting the conformance suite into a follow-up would leave the stub unverified at merge time and would invert the plan's "freeze the contract before Step 8" property. Reviewer ack on the size is requested in lieu of a split.

Review feedback (follow-up commit)

fix(storage): address review feedback on Step 3 stub resolves W1 (Session.ReadFrom partial-bytes / hasher-staging divergence), W2 (CtxCanceled subtests for the new Session surface), W3 (failing-reader subtest), and O1 (drop the post-lock ctx.Err() check in NewSession for consistency with the other methods).

Edited by Pawel Rozlach

Merge request reports

Loading
Loading