Skip to content

Add MacDive SQLite import (Milestone 3 of 4)#256

Merged
ericgriffin merged 1 commit into
mainfrom
feature/macdive-sqlite
Apr 23, 2026
Merged

Add MacDive SQLite import (Milestone 3 of 4)#256
ericgriffin merged 1 commit into
mainfrom
feature/macdive-sqlite

Conversation

@ericgriffin
Copy link
Copy Markdown
Member

Summary

Third of four milestones addressing MacDive import issues. Closes
GH #179 (MacDive SQLite import). Gives users the most complete
migration path from MacDive: all dive metadata plus tags, critters,
gear, events, service records, certifications.

Stacked on PR #254 (M2). Merge order: #252#254 → this.

What landed

  • ImportFormat.macdiveSqlite with MacDive (SQLite) source override.
  • _detectFormat extended: SQLite → Shearwater check → MacDive check → generic.
  • BPlistDecoder (Apple binary plist v00) in lib/core/utils/bplist/
    — supports dict, array, string (ASCII + UTF-16BE), int, real, bool,
    null, data, date, and (added during real-sample probing) UID markers
    for NSKeyedArchiver format.
  • MacDiveDbReader: schema validation, typed row graph, filters null-FK
    tombstones in ZTANKANDGAS.
  • MacDiveDiveMapper: joins raw rows into a unified ImportPayload
    matching M2's key conventions. Reuses MacDiveUnitConverter and
    MacDiveValueMapper from M2.
  • MacDiveSqliteParser: ImportParser implementation wrapping reader
    • mapper.
  • ImportDuplicateChecker: first-pass exact match on source_uuid
    via a new bulk-query helper (DiveRepository.getSourceUuidByDiveId),
    keeps Dive entity unchanged.
  • Gated real-sample test: 540 dives / 373 sites / 33 buddies / 39 tags /
    32 gear against user's 6.7MB DB.

Known limitation

  • ZDIVE.ZSAMPLES (MacDive's proprietary profile-sample BLOB) is NOT
    decoded in M3. MacDive's format isn't bplist and doesn't match any
    common compression at any offset (tried zlib/gzip/lzma at 0/4/8/12).
    Users who want profile time-series data should use MacDive UDDF
    import (M1) — that decodes MacDive's UDDF profile correctly.
    M3 is the "rich metadata" path; UDDF is the "sample data" path.

Test plan

  • flutter test — full suite passes
  • flutter analyze — clean
  • dart format — clean
  • Synthetic-DB tests for reader, mapper, parser
  • BPlist decoder golden tests (Python-plistlib fixtures + real MacDive ZTIMEZONE BLOB)
  • flutter test --tags=real-data --run-skipped against user's 6.7MB MacDive.sqlite
  • Manual: import the SQLite in the running app, confirm tags/critters/gear populate on dive detail pages
  • Manual: import the same dives via UDDF then SQLite (or vice versa); confirm duplicate checker's source_uuid path prevents re-creation

Related

What's still deferred

  • M4 (photos): cross-format photo linking. Extends M2's XML parser
    and M3's SQLite reader to emit imageRefs on payloads, adds a
    photo-linking wizard step with path rebasing. Separate plan at
    docs/superpowers/plans/2026-04-21-macdive-photo-import.md.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new MacDive SQLite import pathway to the universal import system, aiming to provide a “rich metadata” migration path (vs. UDDF for profile samples) and improving cross-format deduplication via source_uuid.

Changes:

  • Introduces ImportFormat.macdiveSqlite with detector + parser/reader/mapper wiring for MacDive Core Data SQLite files.
  • Adds a binary plist (bplist00) decoder plus golden tests/fixtures to support MacDive Core Data BLOB decoding needs (notably ZTIMEZONE).
  • Enhances dive duplicate detection with a first-pass exact match on source_uuid, backed by a new bulk repository query.

Reviewed changes

Copilot reviewed 31 out of 35 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
test/fixtures/macdive_sqlite/build_synthetic_db.dart Builds a minimal MacDive-shaped SQLite DB fixture for unit/integration tests.
test/fixtures/macdive_sqlite/bplist_samples/small_dict.bplist Golden bplist fixture (dict) for decoder tests.
test/fixtures/macdive_sqlite/bplist_samples/sample_array.bplist Golden bplist fixture (array) for decoder tests.
test/fixtures/macdive_sqlite/bplist_samples/nested.bplist Golden bplist fixture (nested structures) for decoder tests.
test/fixtures/macdive_sqlite/bplist_samples/macdive_ztimezone.bplist Real-sample MacDive ZTIMEZONE bplist fixture for decoder coverage.
test/features/weather/data/repositories/weather_repository_test.mocks.dart Updates mocks for new DiveRepository.getSourceUuidByDiveId.
test/features/universal_import/presentation/providers/universal_import_notifier_test.dart Adds tests for MacDive SQLite detection and parser output (synthetic DB).
test/features/universal_import/presentation/providers/import_consolidation_service_test.mocks.dart Updates mocks for new getSourceUuidByDiveId.
test/features/universal_import/data/services/macdive_dive_mapper_test.dart Validates MacDive SQLite mapping output shape from synthetic DB.
test/features/universal_import/data/services/macdive_db_reader_test.dart Tests MacDive SQLite schema detection + row-graph reading.
test/features/universal_import/data/services/import_duplicate_checker_test.dart Adds coverage for source_uuid-based dive duplicate detection precedence.
test/features/universal_import/data/parsers/macdive_sqlite_real_sample_test.dart Adds gated real-sample regression test for MacDive SQLite parsing.
test/features/universal_import/data/parsers/macdive_sqlite_parser_test.dart Parser unit tests for valid/invalid SQLite inputs.
test/features/universal_import/data/models/import_enums_test.dart Updates enum/value counts and display names for new format/override option.
test/features/import_wizard/data/adapters/universal_adapter_test.mocks.dart Updates mocks for new getSourceUuidByDiveId.
test/features/import_wizard/data/adapters/healthkit_adapter_test.mocks.dart Updates mocks for new getSourceUuidByDiveId.
test/features/import_wizard/data/adapters/dive_computer_adapter_test.mocks.dart Updates mocks for new getSourceUuidByDiveId.
test/features/import_wizard/data/adapters/dive_computer_adapter_reimport_test.mocks.dart Updates mocks for new getSourceUuidByDiveId.
test/features/dive_import/presentation/providers/dive_import_notifier_test.mocks.dart Updates mocks for new getSourceUuidByDiveId.
test/features/dive_import/data/services/uddf_entity_importer_test.mocks.dart Updates mocks for new getSourceUuidByDiveId.
test/features/dive_computer/data/services/dive_import_service_test.mocks.dart Updates mocks for new getSourceUuidByDiveId.
test/core/utils/bplist/bplist_decoder_test.dart Golden + validation tests for the new bplist decoder.
lib/features/universal_import/presentation/providers/universal_import_providers.dart Wires MacDive SQLite detection + parser selection + passes UUID map to dup checker.
lib/features/universal_import/data/services/macdive_raw_types.dart Introduces typed raw row models + logbook container for MacDive SQLite reads.
lib/features/universal_import/data/services/macdive_dive_mapper.dart Maps MacDive row graph into the unified ImportPayload shape.
lib/features/universal_import/data/services/macdive_db_reader.dart Reads MacDive SQLite into typed row graph; schema validation + junction aggregation.
lib/features/universal_import/data/services/import_duplicate_checker.dart Adds “pass 0” exact match by sourceUuid before fuzzy matching.
lib/features/universal_import/data/parsers/macdive_sqlite_parser.dart New ImportParser orchestrating schema check + reader + mapper + error payloads.
lib/features/universal_import/data/models/import_enums.dart Adds ImportFormat.macdiveSqlite and source override option.
lib/features/import_wizard/data/adapters/universal_adapter.dart Passes UUID map to duplicate checker during wizard flow.
lib/features/dive_log/data/repositories/dive_repository_impl.dart Adds getSourceUuidByDiveId() bulk helper for import dedup.
lib/core/utils/bplist/bplist_object.dart Adds bplist AST types + convenience accessors.
lib/core/utils/bplist/bplist_decoder.dart Implements bplist00 decoder (subset) used for MacDive Core Data blobs.
docs/superpowers/plans/2026-04-21-macdive-sqlite-import.md Updates implementation plan/status notes and the ZSAMPLES descope.
CHANGELOG.md Documents new MacDive SQLite import and limitations.

Comment thread test/fixtures/macdive_sqlite/build_synthetic_db.dart
Comment thread CHANGELOG.md
Comment thread lib/features/universal_import/data/services/macdive_db_reader.dart
Comment thread lib/core/utils/bplist/bplist_decoder.dart Outdated
Comment thread test/features/universal_import/data/parsers/macdive_sqlite_real_sample_test.dart Outdated
Comment thread lib/core/utils/bplist/bplist_object.dart Outdated
Comment thread lib/features/dive_log/data/repositories/dive_repository_impl.dart Outdated
@ericgriffin ericgriffin mentioned this pull request Apr 22, 2026
7 tasks
@ericgriffin ericgriffin moved this from Backlog to In review in Submersion Release Tracker Apr 22, 2026
@ericgriffin ericgriffin self-assigned this Apr 22, 2026
ericgriffin added a commit that referenced this pull request Apr 23, 2026
…, UUID scoping)

- bplist: UID now reads 1-4 bytes big-endian per spec (low nibble =
  byteCount - 1); regression tests added for 1/2/4-byte indexes and the
  BPlistUID docstring now matches the decoder.
- MacDive DB reader: wrap optional-table reads (_readBuddies / _readTags
  / _readGear / _readTanks) in _selectOrEmpty so missing tables yield
  empty collections, matching the docstring; docstring tightened to
  call out the required/optional split explicitly.
- dive_repository.getSourceUuidByDiveId: optional diverId parameter
  that joins to dives.diver_id, matching the scope the
  universal_adapter already uses for existingDives. 4 targeted tests.
- CHANGELOG: rewrote the MacDive SQLite entry to describe only what
  actually ships (dives/sites/buddies/tags/gear plus per-dive tank/gas
  linkage); added a Known Limitations bullet for the
  critters/events/service-records/certifications gap. Inline code
  comments in the mapper document why dive dateTime follows M2's
  absolute-UTC convention and why diveToGearPks is collected-but-not-
  emitted, both tracked as cross-parser follow-up work.
- real-sample test: replaced hard-coded dev path with
  MACDIVE_SQLITE_REAL_SAMPLE_PATH env var; group-level skip with a
  clear reason when unset; StateError guard for --run-skipped.
- synthetic fixture: ZTANKANDGAS UUIDs renamed tag-uuid-* to
  tankandgas-uuid-*.
- Regenerated 9 mock files for the new diverId parameter.
Copilot AI review requested due to automatic review settings April 23, 2026 04:13
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 32 out of 36 changed files in this pull request and generated 7 comments.

Comment thread lib/core/utils/bplist/bplist_decoder.dart Outdated
Comment thread lib/features/universal_import/data/services/macdive_db_reader.dart
Comment thread lib/features/universal_import/data/services/macdive_raw_types.dart
Comment thread lib/core/utils/bplist/bplist_decoder.dart
Comment thread lib/features/universal_import/data/services/macdive_dive_mapper.dart Outdated
ericgriffin added a commit that referenced this pull request Apr 23, 2026
…robe dedup, doc fixes)

Round 2 of Copilot's review on PR #256 — 7 items, 5 implemented and 2
doc/rename fixes.

- equipmentRefs (the big one): my earlier pushback was wrong.
  `UddfEntityImporter` DOES resolve `diveData['equipmentRefs']` via
  `equipmentIdMapping`, so SQLite imports now emit per-dive
  equipmentRefs pulled from `diveToGearPks`. Each gear item gets a
  stable `uddfId` (MacDive UUID, with name fallback) so the importer
  can link gear back to dives the same way M2 XML would. Also filters
  name-less gear rows so `_importEquipment` doesn't silently drop
  entries while `equipmentIdMapping` keeps stale refs.
- Format detector: probe SQLite table names ONCE per input, not once
  per flavor check. Previously detection wrote the full byte array to
  a temp file + opened sqlite twice (once for Shearwater, once for
  MacDive). Added `probeSqliteTableNames` on ShearwaterDbReader plus
  sync `matchesTables` on both readers so the detector can probe once
  and match all flavors.
- Doc fix: `BPlistDecoder.decode` docstring no longer lists UIDs as
  unsupported — they've been supported since M3.
- Doc fix: explicit comment at the bplist int-decode case explaining
  the 16-byte best-effort low-64-bit truncation behavior (implicit
  via Dart int overflow on `(value << 8) | byte`).
- Doc+code: `MacDiveDbReader.readAll` now validates required tables
  up front and throws a clear FormatException if any are missing, so
  the docstring's "validated up-front" promise matches reality even
  for callers that bypass `isMacDiveDb`.
- Rename: `samplesBplist` and `rawDataBplist` → `samplesBlob` /
  `rawDataBlob`. The data in these fields is NOT bplist (ZSAMPLES is
  MacDive-proprietary, ZRAWDATA is raw sensor dump); the old names
  would have misled future callers into trying to decode them.
- Tests: 2 new synthetic-DB parser tests covering uddfId emission and
  equipmentRefs resolution; 1 new real-sample test verifying every
  emitted equipmentRef points at an equipment uddfId.
@ericgriffin ericgriffin force-pushed the feature/macdive-native-xml branch from af121c3 to 28227e3 Compare April 23, 2026 04:56
Base automatically changed from feature/macdive-native-xml to main April 23, 2026 05:09
Adds direct import from MacDive's Core Data SQLite database, the most
complete MacDive metadata path. Captures the same entity set as MacDive
XML — dives, sites, buddies, tags, and gear inventory — plus per-dive
tank/gas mix linkage from ZTANKANDGAS and per-dive equipmentRefs
linking dives to their gear. Cross-format deduplication via source UUIDs
means re-importing the same dives via MacDive UDDF/XML/SQLite won't
create duplicates.

## What landed

- `ImportFormat.macdiveSqlite` with `MacDive (SQLite)` source override.
- `_detectFormat` extended: SQLite → Shearwater check → MacDive check →
  generic. Single SQLite probe reused across flavors to avoid doubling
  temp-file I/O on large DBs.
- `BPlistDecoder` (Apple binary plist v00) in `lib/core/utils/bplist/`
  — supports dict, array, string (ASCII + UTF-16BE), int (1..8 byte;
  16-byte best-effort low-64-bit), real, bool, null, data, date, and
  NSKeyedArchiver UID markers (1..4 bytes per spec).
- `MacDiveDbReader`: schema validation (required tables enforced up
  front; optional tables read safely), typed row graph, filters null-FK
  tombstones in ZTANKANDGAS.
- `MacDiveDiveMapper`: joins raw rows into a unified `ImportPayload`
  matching M2's key conventions. Emits per-dive `equipmentRefs` so
  `UddfEntityImporter.equipmentIdMapping` can link gear back to dives.
  Gear maps require `name` and carry a stable `uddfId` (MacDive gear
  UUID, with name fallback).
- `MacDiveSqliteParser`: `ImportParser` implementation wrapping reader
  + mapper.
- `ImportDuplicateChecker`: first-pass exact match on `source_uuid`
  via a new `DiveRepository.getSourceUuidByDiveId({diverId})` helper
  that optionally scopes the UUID map to one diver.

## Known limitations

- `ZDIVE.ZSAMPLES` (MacDive's proprietary profile-sample BLOB) is NOT
  decoded. MacDive's format isn't bplist and doesn't match common
  compressions. Users who want profile time-series data should use
  MacDive UDDF import (M1). M3 is the rich-metadata path; UDDF is the
  sample-data path.
- Critters (marine-life sightings), dive events, service records, and
  certifications are read into the typed row graph but not yet emitted
  into the import payload — tracked as follow-up work (the unified
  importer lacks entity types for critters/events/service records).

## Test coverage

- Synthetic-DB tests: reader, mapper, parser (`macdive_db_reader_test`,
  `macdive_dive_mapper_test`, `macdive_sqlite_parser_test`).
- BPlist decoder golden tests (Python-plistlib fixtures + real MacDive
  ZTIMEZONE BLOB). Regression tests for 1/2/4-byte UID indexes.
- Gated real-sample regression against a 6.7MB MacDive.sqlite (enable
  via `MACDIVE_SQLITE_REAL_SAMPLE_PATH`): 540 dives / 373 sites /
  33 buddies / 39 tags / 32 gear, equipmentRefs resolution verified.
- New `dive_repository_new_methods_test` coverage for scoped and
  unscoped `getSourceUuidByDiveId`.

Closes #179.
@ericgriffin ericgriffin force-pushed the feature/macdive-sqlite branch from 9512cfb to eea760b Compare April 23, 2026 06:09
@ericgriffin ericgriffin merged commit abba5a1 into main Apr 23, 2026
16 checks passed
@github-project-automation github-project-automation Bot moved this from In review to Done in Submersion Release Tracker Apr 23, 2026
@ericgriffin ericgriffin deleted the feature/macdive-sqlite branch April 23, 2026 06:52
ericgriffin added a commit that referenced this pull request Apr 23, 2026
Follow-up to the MacDive SQLite importer (PR #256) which emits
profile: [] for every dive. Spec proposes a two-phase approach:
investigation spike with a go/no-go gate, then decoder implementation
if the spike confirms feasibility. Stacks on feature/macdive-sqlite.
readme42 pushed a commit to readme42/submersion that referenced this pull request Apr 24, 2026
MacDiveDiveMapper now decodes ZRAWDATA via parseRawDiveData (same API
ShearwaterDiveMapper uses for Shearwater Cloud). toPayload becomes
async and threads warnings through. _vendorProductFromZComputer maps
ZCOMPUTER strings to libdivecomputer (vendor, product) pairs.

Sample projection matches ShearwaterDiveMapper.mergeWithParsedDive
exactly (same keys, same conditional emission). Dives without
ZRAWDATA or with unmapped computers emit profile:[] silently;
decode failures emit profile:[] + ImportWarning.

Closes the ZRAWDATA part of the MacDive SQLite profile gap from submersion-app#256.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants