critical priority low complexity database pending database specialist Tier 2

Acceptance Criteria

OfflineSearchRepository implements IOfflineSearchRepository from task-001 with identical method signatures to IContactSearchRepository
searchByName(query) executes a Drift LIKE query on CachedContactsTable.name using LOWER(name) LIKE LOWER('%query%') to handle Norwegian characters (æ, ø, å) case-insensitively
searchByOrganisation(query) executes a Drift LIKE query on CachedContactsTable.organisation with the same case-insensitive pattern
searchByNotes(keyword) executes a Drift LIKE query on CachedNotesTable.content, joins to CachedContactsTable on contactId, and returns ContactSearchResult instances
The unified search() method mirrors the SupabaseSearchRepository behaviour: parallel execution, deduplication by contact id with name match priority, and identical sort order
All returned ContactSearchResult objects are structurally identical to those returned by SupabaseSearchRepository for the same underlying data — no extra or missing fields
If the cache is empty, all search methods return an empty list (no exception thrown)
Unit tests pass using an in-memory Drift database seeded with test data

Technical Requirements

frameworks
Flutter
Drift (drift: ^2.x)
data models
CachedContactsTable
CachedNotesTable
ContactSearchResult
performance requirements
LIKE queries on the in-memory and on-disk cache must return results in under 100ms for a dataset of up to 10,000 cached contacts
security requirements
Sanitise query strings before passing to LIKE expressions — Drift parameterised queries handle SQL injection prevention, but ensure no raw string interpolation into query expressions

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Place the class in lib/features/contact_search/data/local/offline_search_repository.dart. Inject the AppDatabase via constructor. Define a Drift DAO (ContactSearchDao) annotated with @DriftAccessor(tables: [CachedContactsTable, CachedNotesTable]) and define the LIKE queries there. For case-insensitive Norwegian LIKE, use Drift's CustomExpression or the expression API: CustomExpression("LOWER(name) LIKE LOWER('%${query.replaceAll("'", "''")}%')").

Alternatively, store a pre-lowercased search_name column in CachedContactsTable at sync time to avoid LOWER() at query time. For the notes join, use a Drift join query: select().from(cachedNotesTable).join([innerJoin(cachedContactsTable, cachedContactsTable.id.equalsExp(cachedNotesTable.contactId))]).where(cachedNotesTable.content.like(...)). Map the TypedResult rows to ContactSearchResult using the same mapping logic as SupabaseSearchRepository to guarantee interface parity.

Testing Requirements

Write unit tests using NativeDatabase.memory() as the Drift backend to avoid filesystem dependencies. Seed the in-memory database with 5–10 CachedContact rows and 3–5 CachedNote rows covering Norwegian names with æ, ø, å characters. Test: (1) searchByName with a partial Norwegian name returns the correct contact, (2) searchByOrganisation with a partial org name returns matching contacts, (3) searchByNotes with a keyword present in one note returns the linked contact, (4) search with a query matching both a name and a notes field deduplicates to a single result with matchedField.name, (5) searchByName with a query matching zero rows returns an empty list without throwing. Run all tests with flutter_test.

Epic Risks (3)
high impact medium prob security

Supabase RLS policies may not correctly scope ilike search results to the authenticated user's organisation and chapter, causing data leakage across organisations or empty result sets for valid queries.

Mitigation & Contingency

Mitigation: Reuse and extend existing RLS query builder patterns from the contact-list-management feature. Write integration tests against a seeded multi-organisation test database to verify cross-org isolation before merging.

Contingency: If RLS scoping is insufficient, add an explicit organisation_id filter in the Dart query builder layer as a defence-in-depth measure while the Supabase policy is corrected.

medium impact medium prob integration

Adding new Drift tables for the contact cache may conflict with existing migrations or schema versions in the contact-list-management feature if both features cache the same contacts table, causing migration failures on user devices.

Mitigation & Contingency

Mitigation: Audit existing Drift schema versions from contact-list-management before writing new migrations. Reuse existing cache tables if the schema already covers required fields; only add missing fields via ALTER or new version.

Contingency: If schema conflict occurs, consolidate into a single shared cache table owned by contact-list-management and expose a DAO interface to the search feature, avoiding duplicated schema ownership.

medium impact medium prob scope

The offline cache may surface significantly stale contact data if sync has not run recently, leading coordinators to act on outdated information (wrong phone numbers, changed assignments).

Mitigation & Contingency

Mitigation: Store and surface the last-sync timestamp prominently in the UI layer. Trigger a background cache refresh on app foreground when connectivity is detected.

Contingency: If staleness becomes a reported UX issue, implement a maximum-age threshold that shows a warning banner when the cache is older than a configurable limit (e.g. 24 hours).