Skip to content

feat(Session DB): local SQLDelight session storage + upload outbox - #22

Merged
xameYeoh merged 12 commits into
mainfrom
feat/session-db
Oct 16, 2026
Merged

xameYeoh merged 12 commits into
mainfrom
feat/session-db

Conversation

@xameYeoh

@xameYeoh xameYeoh commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Adds the local persistence layer for sessions in shared (KMP, SQLDelight): schema, domain models, repository interfaces, SQLDelight-backed implementations, Koin wiring, and host tests. Also records the storage decisions in /docs.

Schema (shared/src/commonMain/sqldelight/.../db/)

  • sessions — keyed by uuid; type/status as enums, tags as List<String>, times as kotlin.time.Instant (epoch millis), measurement_interval_millis, optional session_token.
  • measurement_streams — FK → sessions ON DELETE CASCADE; unique (session_uuid, sensor_name) so findOrCreate is idempotent.
  • measurements — PRIMARY KEY (stream_id, time) + INSERT OR IGNORE. Same dedup key as the backend's v3 rule; replaces legacy (session_id, stream_id, time) index. One table, one write path — no separate active-session buffer table.
  • pending_uploads — upload outbox holding only keys (stream_id, time), composite FK → measurements ON DELETE CASCADE. measurements stays insert-only.

Foreign keys are enabled per connection on both platforms (Android driver onOpen callback, iOS extendedConfig.foreignKeyConstraints) — otherwise SQLite ignores ON DELETE CASCADE.

Domain + repositories

  • Models: Session, MeasurementStream, Measurement (domain/session).
  • SessionRepository, MeasurementStreamRepository, MeasurementRepository, UploadOutbox interfaces, with Local* implementations on Dispatchers.IO.
  • MeasurementRepository has two write paths:
    • insertAll — data from elsewhere (downloaded/followed/explored/fixed). Stored only, never enqueued.
    • insertRecorded — data recorded on this phone. Stores and enqueues in one transaction, enqueuing only newly stored rows.
  • UploadOutbox.pending(sessionUuid, limit) returns oldest-first; acknowledge deletes outbox rows by natural key (correct when BLE sync backfills older timestamps after newer ones were sent).
  • Registered in DatabaseModule (Koin).

Docs

  • DOMAIN_KNOWLEDGE.md — averaging is display-only; stored measurements are never averaged/down-sampled/deleted. Dedup key rule updated to (stream_id, time).
  • API_V3.md — outbox rules: only locally recorded data is enqueued; 200 ≠ stored (split on finished_at before sending); sync clock before first batch; track per row, never a time cursor; blind retry is safe.
  • KMP_ARCHITECTURE.md, ble_mobile_app_guide.md — aligned with the above.

Open decision (noted in API_V3.md): how frames past the server-side finished_at leave the outbox — bound pending by finished_at, or add a state column to pending_uploads.

Tests

androidHostTest (JVM SQLite driver added to that source set):

  • SessionRepositoryTest — full-field round-trip, null location, observeAll re-emits on insert, finish sets status + end time together, updateStatus leaves end time alone.
  • MeasurementRepositoryTest — findOrCreate idempotent, dedup on (stream, time) incl. partial-overlap counts, same timestamp across streams, location round-trip, lastTime, session delete cascades to streams + measurements.
  • UploadOutboxTest — insertRecorded stores + enqueues, insertAll never enqueues, acknowledged duplicate not re-enqueued, pending oldest-first/limited/scoped per session, acknowledge keeps the measurement, failing batch is atomic, session delete clears pending uploads.

Out of scope

Uploader itself, read-side averaging, UI wiring.

🤖 Generated with Claude Code

xameYeoh and others added 12 commits October 15, 2026 16:28
The rewrite drops the legacy in-place AveragingService. Measurements are
kept at native resolution; averaging happens only on the read side (graph,
map, stats). Replaces load-bearing rule #4 (native-interval gate) with
"raw measurements are never averaged, down-sampled or deleted in storage",
removes averaging from the RecordingEngine sketch, and marks the V2
sparse-interval gotcha as legacy-only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…p key

The rewrite has no active_sessions_measurements rolling buffer; the live
view is a windowed query on measurements. The dedup key is now
PRIMARY KEY (stream_id, time) — the legacy session_id in the index was
redundant, and v3 dedups on (stream, timestamp) as well. Updates
KMP_ARCHITECTURE §5 and DOMAIN_KNOWLEDGE §4 / load-bearing rule #5.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Records how the v3 upload traps constrain the local outbox: only mobile
sessions are uploaded by the phone (downloaded rows must land as
uploaded), uploaded means "accepted in a 200 batch" so frames past
finished_at must be split out first, clock sync before sending to avoid a
poisoned invalid_epoch batch, per-row marking instead of a time cursor,
and blind retry on transport errors.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Replaces the measurements.uploaded flag design: upload state lives in a
key-only pending_uploads table (composite FK to measurements, cascade),
written by insertRecorded and cleared by UploadOutbox.acknowledge.
Downloaded/followed/explored data goes through insertAll and never enters
the outbox, and measurements stays insert-only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Remove the `uploaded` column from the `measurements` table and move upload queue tracking into a dedicated `pending_uploads` outbox table.

- Introduce `UploadOutbox` domain interface and `LocalUploadOutbox` implementation for querying pending measurements and acknowledging server syncs.
- Add `insertRecorded()` to `MeasurementRepository` to atomically store and enqueue newly recorded measurements, leaving `insertAll()` for non-uploadable imported data.
- Remove the `uploaded` column from `Measurement.sq` and property from the `Measurement` domain model.
- Register `UploadOutbox` in `DatabaseModule` and add `UploadOutboxTest` host tests.
@xameYeoh
xameYeoh merged commit 6c3a7e7 into main Oct 16, 2026
2 checks passed
@xameYeoh
xameYeoh deleted the feat/session-db branch October 16, 2026 06: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.

1 participant