Skip to content

feat(store): lift GRDB message store out of LXMFSwift into app-owned MessageStore/ - #219

Merged
torlando-tech merged 2 commits into
mainfrom
feat/android-parity-message-store
Oct 12, 2026
Merged

torlando-tech merged 2 commits into
mainfrom
feat/android-parity-message-store

Conversation

@torlando-tech

Copy link
Copy Markdown
Owner

Summary

Phase 0-1 of the Android-parity store lift: re-architect iOS messaging persistence so the GRDB store is app-owned (Sources/ColumbaApp/Services/MessageStore/) with zero dependency on the LXMFSwift wire library, matching Android's :data module pattern.

The NE Python inbound writer keeps landing in the same frozen-schema lxmf-swift.db; only the location and dependencies of the store's Swift code change.

Changes

New app-owned store (Sources/ColumbaApp/Services/MessageStore/ — GRDB + Foundation only, no LXMFSwift / RNSAPI / ReticulumSwift):

  • MessageRow.swift — GRDB row for the messages table (24 columns, byte-identical to LXMFSwift MessageRecord minus the LXMessage-coupled init/toLXMessage).
  • ConversationRecordStore.swift — GRDB row for conversations (struct named ConversationStoreRow to avoid collision with the SwiftUI ConversationRow view).
  • GRDBVocabulary.swift — frozen GRDBState (0x00..0xFF) and GRDBDeliveryMethod (0x01/02/03/05) enums, byte-identical to LXMFSwift and the NE Python.
  • MessageStore.swift — GRDB actor, migrations v1-v6 byte-identical to LXMFSwift's LXMFDatabase; the 5 LXMessage-returning methods re-pointed to a row-based surface.

MessageRepository re-scoped to the collector (Android's MessageCollector role): LXMFSwift.LXMFDatabase property → MessageStore; the 5 LXMessage-welded methods rewritten to row-based. The only remaining LXMFSwift references are the wire codec (LXMessage.unpackFromBytes) in recoverFields/normalizedFieldMap — legitimately kept, exactly as Android's MessageCollector touches LXMF-kt.

Tests:

  • 14 new MessageStoreTests: schema-freeze vs the NE writer (PK/FK/column set), state + method vocabulary round-trips through the mappers, frozen byte values, unknown-method sentinel (0) round-trip, save/read round-trip, inbound-unread increment + source-hash conversation key, cross-process (app reads a raw NE-style INSERT row), makeRow field-map + replyTo + sentinel, outbound-id loading.
  • Updated MicronParserTests adapter tests to the app-owned row types.
  • Updated 2 static contract tests (test_qr_contact_send_path_contract, test_reported_runtime_bugs) to assert the unknown-method invariant at its new location.

Verification

  • 547/547 ColumbaAppTests green (14 new + all 533 existing).
  • Static Python suite: 0 new failures vs pristine main (the 12 remaining are pre-existing, in rns_bridge._durable_mode, unrelated).
  • ModelB device build SUCCEEDED (arm64 + NE + codesign); all 4 store .o files confirmed compiled into the ModelB target.
  • Installed in-place on the iPhone (container B43B0B8A), running; diag.log shows zero GRDB/migration/schema/store errors — the app opens the existing shared DB cleanly through the new byte-identical migrations.
  • First outbound send returned a real 64-char hash (f51bac69...) — the store's outbound path works end-to-end.

Deferred (not in this PR)

  • Phase 2 (full LXMFSwift + ReticulumSwift removal from the graph): LXMFSwift is a remote SPM package (pinned 60186710) and the one remaining live use is the wire codec unpackFromBytes (NE rows carry signed wire; packed_lxmf is dual-meaning). Full removal needs a native wire codec + Telemetry repoint + reconciler/contract-test teardown — a distinct larger lift.
  • Live touch-test (inbound + outbound over the mesh): blocked by a pre-existing NE 16s restart churn (started ~3.5h before this work, PID changes every cycle, RNode BLE attach can't finish, mid-restart sends return empty hash). Not a regression from this change — see diagnosis. Fixed separately.

…MessageStore/

Phase 0-1 of the Android-parity store lift: re-architect iOS messaging
persistence so the GRDB store is app-owned (Sources/ColumbaApp/Services/
MessageStore/) with zero dependency on the LXMFSwift wire library, matching
Android's :data module pattern.

New app-owned store (GRDB + Foundation only, no LXMFSwift / RNSAPI / ReticulumSwift):
- MessageRow.swift: GRDB row for messages table (24 columns, byte-identical
  to LXMFSwift MessageRecord minus the LXMessage-coupled init/toLXMessage)
- ConversationRecordStore.swift: GRDB row for conversations table (struct
  named ConversationStoreRow to avoid collision with SwiftUI ConversationRow)
- GRDBVocabulary.swift: frozen GRDBState (0x00..0xFF) and GRDBDeliveryMethod
  (0x01/02/03/05) enums, byte-identical to LXMFSwift and NE Python
- MessageStore.swift: GRDB actor with migrations v1-v6 byte-identical to
  LXMFSwift LXMFDatabase; 5 LXMessage-returning methods re-pointed to
  row-based surface

MessageRepository re-scoped to the collector (Android's MessageCollector
role): property LXMFSwift.LXMFDatabase -> MessageStore; 5 LXMessage-welded
methods rewritten to row-based; only remaining LXMFSwift refs are the wire
codec (LXMessage.unpackFromBytes) in recoverFields/normalizedFieldMap.

Tests: 14 new MessageStoreTests (schema freeze vs NE writer, state/method
vocabulary round-trips, frozen byte values, unknown-method sentinel, save/
read round-trip, inbound-unread increment, cross-process app-reading-NE-row,
makeRow field-map + replyTo + sentinel, outbound-id loading). Updated
MicronParserTests adapter tests to app-owned row types. Updated 2 static
contract tests (qr_contact_send_path, reported_runtime_bugs) to assert the
unknown-method invariant at its new location.

Verified: 547/547 ColumbaAppTests green; static Python suite 0 new failures
vs pristine main; ModelB device build SUCCEEDED (arm64 + NE + codesign);
all 4 store .o files compiled into the ModelB target; installed in-place on
iPhone (container B43B0B8A), running, zero GRDB/migration errors in diag.log.

Note: LXMFSwift is retained as a transitive SPM dependency for the wire
codec. Phase 2 (full LXMFSwift + ReticulumSwift removal) is deferred - it
requires a native wire codec + Telemetry repoint + reconciler/contract-test
teardown and is a distinct larger lift.
@greptile-apps

greptile-apps Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Critical impact] The PR appears safe to merge; the remaining schema-test gap is non-blocking.

Fix All in Claude CodeFindings

  1. P2 Schema checks miss important details ▶
Fix with agent prompt
### Issue 1
Tests/ColumbaAppTests/MessageStoreTests.swift:55-61
`testMessageSchemaMatchesNEWriterContract` checks column names, the primary key, and only the foreign table's name. It does not check column types, defaults, required values, or the foreign key's columns and delete behavior. Those are part of the shared database contract too. For example, removing `.cascade` would still pass these checks while breaking conversation deletion. Extend the schema checks to cover these properties for both tables.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Moves message persistence into the app-owned MessageStore, while keeping the shared lxmf-swift.db and the wire codec in MessageRepository.

  • The app now owns the GRDB message store.
  • MessageRepository saves and fetches app-owned message rows.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  App["App callers"] --> Repository["MessageRepository"]
  Repository --> Store["MessageStore"]
  Store --> DB[("Shared lxmf-swift.db")]
  NE["Network Extension Python writer"] --> DB
  Repository --> Codec["LXMFSwift wire codec"]
Loading

Reviews (2) · Last reviewed commit: "test(store): strengthen schema-freeze te..." · Reviewed by Greptile

Comment thread Tests/ColumbaAppTests/MessageStoreTests.swift
…L, FK on_delete

Greptile P2 (PR #219): the schema-freeze test only asserted column names,
PK, and FK table name - not column types, NOT NULL, or FK on_delete
behavior. A dropped .cascade would still pass while breaking conversation
deletion. Extended both tests to assert the full frozen contract:

- columnTypes: all 25 messages columns + all 13 conversations columns
- notNullColumns: the exact NOT NULL set for both tables
- foreignKeyDetails: (table, from, to, on_delete) tuple per FK

Three new introspection helpers (columnTypes, notNullColumns,
foreignKeyDetails) added alongside the existing schemaColumns/PK/FK/index
helpers. All 547 tests green.
@torlando-tech
torlando-tech merged commit 987a7f0 into main Oct 12, 2026
4 checks passed
@torlando-tech
torlando-tech deleted the feat/android-parity-message-store branch October 12, 2026 14:45
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