Skip to content

fix(archive): preserve microseconds and pre-epoch SQLite history - #184

Draft
TimeToBuildBob wants to merge 17 commits into
ActivityWatch:masterfrom
TimeToBuildBob:feat/device-archive-fidelity-78c9
Draft

TimeToBuildBob wants to merge 17 commits into
ActivityWatch:masterfrom
TimeToBuildBob:feat/device-archive-fidelity-78c9

Conversation

@TimeToBuildBob

@TimeToBuildBob TimeToBuildBob commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Archive-fidelity continuation of ActivityWatch/activitywatch#302. Stacked on draft #183 at 4ac2146, which itself depends on schema #177. Do not merge this before those prerequisites; the incremental archive commit is 60917e0. The existing implementation branch is leased to another session, so this follow-up does not mutate it.

  • Preserve full Python microseconds in Event construction, timestamp assignment and JSON instead of silently truncating to milliseconds.
  • Use exact integer microseconds for SQLite writes/replacements and query bounds; subtract endpoints before converting legacy REAL-valued durations.
  • Unbounded reads/counts, metadata and snapshot iterators include pre-1970 history.
  • Preserve raw event rows, metadata text, bucket names and row IDs through the device migration. No schema/version bump or data rewrite in the incremental commit.

Reproduction and verification

The initial 21-case fixture failed on the base: .123457 became .123000, old float arithmetic lost additional digits for distant dates, and a 1960 snapshot event vanished. After repair:

  • Isolated serial aw-core suite: 422 passed, 5 skipped.
  • Repository mypy: 37 files passed; pinned Ruff, whitespace and normal commit hooks passed.
  • Exact timestamp/duration checks across four write paths and years 1, 1960, 2026, 2300, 9998; explicit exact range boundaries, offsets, timestamp assignment, JSON round-trip, legacy REAL readability and untouched raw rows; migration includes negative integer timestamps and exact metadata text.
  • Independent read-only review identified a naive-bound compatibility regression. Reproduced, fixed with astimezone(UTC) preserving the previous local-time interpretation, and regression-tested.
  • Brain make test-fast: 1505 passed, 1 skipped; make typecheck: 286 package / 1803 script files passed. These are workspace checks, not aw-core evidence.

Boundaries

Python datetime supports microseconds, not Rust's full nanosecond domain. Existing legacy REAL values retain their raw SQL comparison boundaries; sub-microsecond noise cannot be represented exactly by datetime. Bucket.get keeps its existing outward millisecond rounding for backend compatibility. No data is rewritten to pretend otherwise.

This remains draft: the current #183 has independent known creation/snapshot blockers, and the shared HTTP/migration/same-name/privacy/reader-refresh acceptance matrix is not complete. Both Rust global duplicate-name guards remain. No watcher or V2 rollout, production database changes, or release claim.

Continuation: prerequisite correctness blockers repaired in 4a04a57

The leased prerequisite branch remains unchanged. This follow-up now also contains the fixes for all three concrete #183 P1 findings:

  • Three (or more) explicitly scoped devices may create the same exact name; existing exact names are not mistaken for global alias conflicts.
  • BEGIN IMMEDIATE encloses creation alias checks plus INSERT, preventing competing SQLite connections from splitting the two spellings.
  • Snapshot automatic commit() is a no-op, including through scoped handles; ordinary reads and default iterators retain the pinned view. The owning caller still closes the query-only connection.

All six new fixture cases failed before repair. Final isolated aw-core suite 428 passed / 5 skipped, mypy 37 files, pinned Ruff and hooks green. A second bounded read-only review found no concrete correctness issue; the reviewer could not execute Python under its read-only allowlist, so all executable verification is parent-run. Brain second fast/type pass: 1505 passed / 1 skipped, 286/1803 files. #183 itself still contains the old bugs until the implementation owner integrates the two incremental commits; do not resolve its review threads based solely on this sibling draft.

Migration-discovery repair: 538886d (current head)

Reproduced the review's startup blocker with WAL/SHM entries listed before sqlite-testing.v1.db: discovery selected companions and backups. detect_db_files now accepts only main .db files and exact three-component versioned names. New test verifies both discovery and the real migration copy. Final suite 429 passed / 5 skipped, mypy 37 files; third brain fast/type pass 1505/1 skipped, 286/1803 files; normal hooks passed. Replied on the finding with the commit and evidence. No WAL/SHM deletion or source data rewrite.

…_id, id))

Adds `device_id` to the `buckets` table so buckets from different devices can
share a bucket name without colliding. Python mirror of ActivityWatch/aw-server-rust#786.

**Before:** `UNIQUE(id)` — two devices' `aw-watcher-window_hostname` buckets
can't coexist in the same db.
**After:** `UNIQUE(device_id, id)` — each device's buckets are scoped independently.

Changes:
- `LATEST_VERSION` 1 → 2 (new file: `sqlite.v2.db`)
- `CREATE_BUCKETS_TABLE`: add `device_id TEXT NOT NULL DEFAULT 'local'`, replace
  inline `UNIQUE` on `id` with table-level `UNIQUE(device_id, id)`
- `create_bucket()`: add `device_id` param (default `"local"`)
- `buckets()`, `buckets_with_last_updated()`, `get_metadata()`: include `device_id`
- `migration.py`: add `sqlite_v1_to_v2()` to copy v1 data with `device_id='local'`;
  `check_for_migration()` now detects and runs it on first open of the new db file

Part of ActivityWatch/activitywatch#302 (hostname-free bucket IDs).

Git-Session-Id: 02ce
- Add device_id param to AbstractStorage.create_bucket() so mypy accepts
  migration.py's call with device_id="local" (fixes CI typecheck failure)
- Add device_id param to PeeweeStorage and MemoryStorage to satisfy override
  compatibility with the updated abstract signature
- Fix sqlite_v1_to_v2(): clear event.id before insert_many so events get
  INSERTed rather than UPDATE-attempted on an empty destination database
  (fixes migrated buckets silently having zero events — Greptile P1)
- Fix check_for_migration(): prefer SQLite v1 over Peewee; running both into
  the same destination caused IntegrityError on UNIQUE(device_id, id) when
  users had both files (Greptile P1)
- Add in-place schema upgrade in SqliteStorage.__init__() via ALTER TABLE ADD
  COLUMN so databases opened via custom filepath= get device_id without
  failing on SELECT/INSERT queries (Greptile P1)

Git-Session-Id: 5ec50786-5c09-5bb0-b4f1-0ffd16b82ac1
…UE(device_id,id) on legacy dbs; commit migration tail

Git-Session-Id: 0cec7590-494f-538b-9531-87ebf5dff828
…ned error

Git-Session-Id: 9a545a26-3da2-5263-b86a-df4f86f42f20
…ce_id in memory/peewee backends

- Migration: skip buckets already present in the v2 datastore and rename the
  v1 db to *.migrated-v1.db on success so it is not re-detected on next
  startup (re-running previously raised IntegrityError and blocked startup).
- MemoryStorage: include device_id in bucket metadata (API contract parity
  with the sqlite backend).
- PeeweeStorage: report device_id='local' in json() without a schema change
  (existing peewee DBs have no such column).
- Add tests/test_sqlite_migration.py covering migration, rename, and rerun.

Git-Session-Id: b4d76fbc-0b40-5323-a560-63560f9e7534
Git-Session-Id: 44e46ae0-002f-5190-958f-74c5450808fa
Git-Session-Id: d421133f-910e-51a5-b6fd-2d6bb107f2e6
Git-Session-Id: d421133f-910e-51a5-b6fd-2d6bb107f2e6
Git-Session-Id: ee485138-49ef-455b-9c15-d08a96a24bd7
Git-Session-Id: ee485138-49ef-455b-9c15-d08a96a24bd7
Git-Session-Id: 9d1828c6-192e-58ea-946a-4405cde28acf
Git-Session-Id: 931478ca-2d2b-48c5-bb99-0bb5ad608478
Open an independent query-only connection that pins a WAL reader
snapshot so long-lived exports do not hold the writer connection.

Git-Session-Id: aeaa4ffa-df33-4fe0-8487-f35c72996602
Git-Session-Id: 0dc232f4-6721-4652-9e55-bd51fe8eec69
@greptile-apps

greptile-apps Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 4/5

[High impact] The PR reaches shared event storage and persisted SQLite archives.

Fix migration source discovery before merging; WAL companion files can prevent startup.

Findings

  1. P1 Upgrade opens the wrong file ▶

Summary

This PR adds device-scoped SQLite access and preserves full Python microseconds.

  • Archived events keep their full microsecond times.
  • SQLite keeps same-named buckets separate for each device.
  • SQLite archives migrate and export from a stable read view.

TimeToBuildBob acknowledged independent creation/snapshot blockers and deferred metadata-repair/alias acceptance checks in the PR description. Those concerns are not repeated as findings.

TimeToBuildBob explicitly identified creation and snapshot blockers in #183, including reads that can end a snapshot transaction. TimeToBuildBob also stated that metadata-repair/alias interactions still need the shared acceptance matrix.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Open SQLite v2] --> B[Discover SQLite v1 files]
    B --> C[Take first matching filename]
    C --> D{Main database?}
    D -->|Yes| E[Copy buckets and raw event values]
    E --> F[Commit data and completion marker]
    D -->|WAL or shared-memory file| G[Table read fails]
    G --> H[Startup stops]
Loading

Reviews (1) · Last reviewed commit: "fix(archive): preserve microseconds and ..." · Reviewed by Greptile

Comment thread aw_datastore/migration.py
Comment on lines +43 to +46
sqlite_v1_files = detect_db_files(data_dir, sqlite_name, 1)
if len(sqlite_v1_files) > 0:
v1_path = os.path.join(data_dir, sqlite_v1_files[0])
sqlite_v1_to_v2(datastore, v1_path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Upgrade opens the wrong file

detect_db_files accepts sqlite.v1.db, sqlite.v1.db-wal, and sqlite.v1.db-shm. This code takes the first match from unsorted os.listdir output. If a companion file comes first, sqlite_v1_to_v2 opens it as the source database and fails when reading its tables, preventing server startup.

Select the exact main database filename and add a discovery test with companion files listed first.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed on current head 4a04a57: running the existing discovery function with WAL/SHM entries before the main file returns all three and selects sqlite.v1.db-wal. This finding remains open. The implementation branch and worktree are currently leased to another session, so I have handed the repair to that owner rather than editing concurrently: select the exact main database, test companion-first ordering and companion-only absence, then refresh the dependent ActivityWatch/aw-server#201 core pin. No fix has been pushed by this monitoring pass.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 538886d. Reproduced with sqlite-testing.v1.db-wal and -shm listed before the main file: discovery returned companions/backups. Discovery now admits only main .db files and exact three-component versioned names. The regression verifies discovery and an actual v1→v2 copy; isolated suite 429 passed / 5 skipped, mypy 37 files. Existing WAL/SHM files are neither opened as sources nor deleted.

Git-Session-Id: 0dc232f4-6721-4652-9e55-bd51fe8eec69
Git-Session-Id: 0dc232f4-6721-4652-9e55-bd51fe8eec69
Git-Session-Id: c2d218fa-690b-4e77-96e3-959267ad65f3
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