Repository navigation
feat(datastore): add explicit device-scoped SQLite handles - #183
TimeToBuildBob wants to merge 17 commits into
Conversation
…_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
[Medium impact] The changes reach SQLite-backed datastore workflows and existing database startup. The PR needs fixes for scoped creation, concurrent alias creation, and snapshot reads before merging. FindingsSummaryAdds device-scoped SQLite handles and hostname aliases while keeping stored bucket names unchanged.
Diagram%%{init: {'theme': 'neutral'}}%%
flowchart TD
D[Datastore] --> S[Device-scoped handle]
S --> C[Shared writer connection]
C --> B[Bucket selected by device and name]
B --> E[Events selected by bucket row]
C --> P[snapshot]
P --> R[Separate query-only connection]
R --> T[Pinned read transaction]
T --> I[iter_events with flush false]
T --> G[Ordinary event reads]
G --> X[Commit ends snapshot]
Reviews (2) · Last reviewed commit: "feat(sqlite): add a dedicated read-only ..." · Reviewed by Greptile |
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
|
@greptileai review |
| unscoped = copy.copy(self) | ||
| unscoped.device_scope = None | ||
| global_row = unscoped._bucket_row(bucket_id, required=False) |
There was a problem hiding this comment.
Once two devices have a bucket named window, for_device("third").create_bucket("window", ...) always fails. The global lookup raises on the two existing exact matches, even though the caller selected a device and that device has no bucket yet. This blocks the same-name device routing this PR adds. Check for alias redirection without requiring global exact names to be unique.
| device_id = self.device_scope | ||
| if not device_id: | ||
| raise ValueError("Device scope must not be empty") | ||
| selected = self.for_device(device_id)._bucket_row(bucket_id, required=False) |
There was a problem hiding this comment.
Concurrent creation splits history
The alias checks run before creation acquires a write lock. Two connections can both pass them while the names are absent, then create window_host and window with hostname host on the same device. Both inserts satisfy the database constraint, but they split the history that sequential creation rejects. Hold the write lock across the checks and insertion, as the event insert paths now do.
| snap.conn.execute("PRAGMA query_only=ON") | ||
| snap._owner = None | ||
| snap.enable_lazy_commit = False | ||
| # Deferred BEGIN does not pin a WAL snapshot until a real table read. | ||
| # `SELECT 1` is a constant and does not establish the reader snapshot. | ||
| snap.conn.execute("BEGIN") | ||
| snap.conn.execute("SELECT 1 FROM sqlite_master LIMIT 1").fetchone() |
There was a problem hiding this comment.
The new snapshot() handle loses its pinned view when used with get_event, get_events, or get_eventcount. Each method calls commit(), which ends the snapshot's read transaction because its _owner is None. Later writer commits can then appear in these reads and subsequent export iteration, even with flush=False, mixing data from different times. Keep automatic read-time commits from ending snapshot transactions.
Convergence adjudication: do not merge yet
Repair handoff: the PR branch and worktree are still leased to the existing implementation owner until 14:16 UTC. I requested a handoff and sent the three executable reproductions through coordination rather than bypassing that ownership. The three valid Greptile threads remain open pending actual fixes; our own reviewer has no open findings to dispose. |
Git-Session-Id: 0dc232f4-6721-4652-9e55-bd51fe8eec69
Git-Session-Id: 0dc232f4-6721-4652-9e55-bd51fe8eec69
Git-Session-Id: 0dc232f4-6721-4652-9e55-bd51fe8eec69
Git-Session-Id: c2d218fa-690b-4e77-96e3-959267ad65f3
|
Integrated the previously leased follow-up #184 into this branch; both are now at 26001d3. This includes all three reproduced creation/snapshot P1 repairs, archive microsecond/pre-epoch preservation, and WAL-safe migration source discovery. Shared real-HTTP testing against Rust then exposed a further alias-query failure: CRUD resolved hostname aliases but query existence checked literal catalogue membership. 26001d3 uses has_bucket; both alias directions and device-isolated query results have regression coverage. Final isolated core suite: 431 passed / 5 skipped; repository mypy37, pinned Ruff and normal hooks pass. ActivityWatch/aw-server#200 pins and installs this exact Git SHA; its package slice passes 156 tests including eight shared HTTP cases with the Rust binary. This remains a draft stacked on #177; full identity/same-name Rust acceptance and release are not claimed. |
|
Freshness gate: deferring convergence adjudication. Current head 26001d3 is four commits ahead of the newest Greptile pass (4ac2146); no bob-ai-review marker exists. The integration update above reports repairs, but it is not current-head review provenance. All 11 CI checks pass; the detector still records three unresolved P1 threads and stable_rounds=0. No classifications, dismissals, thread resolutions or merge recommendation made on this unreviewed head, and no review retriggered. Await automatic current-head review before adjudication; keep this draft behind #177. |
Summary
Storage-side Python parity groundwork for ActivityWatch/activitywatch#302 and draft ActivityWatch/aw-server-rust#836. Stacked on #177 at
c6f6a20fb4a9; do not merge before that schema PR. This draft does not claim Python HTTP parity or enable watcher/V2 adoption.Datastore.for_deviceandSqliteStorage.for_devicehandles select(device_id, id)without encoded string keys. Scoped handles share the connection/commit owner but have separate Bucket-object caches.device_buckets()exposes grouped storage metadata. Unsupported Memory/Peewee scoped routing fails closed.cfd2db8adds exact-first bidirectional hostname aliases for SQLite lookups and all event operations. Complete underscore-bearing hostname metadata is matched, unknown names excluded, stored IDs preserved. Creation refuses silent alias splits, global alias redirection and acknowledgement of a different scoped/global target.4ac2146addsSqliteStorage.snapshot(): a dedicated query-only connection that pins a WAL reader snapshot after a real table read (sqlite_master, notSELECT 1). Long-lived exports can release the writer without seeing later commits.Verification
for_device; cached-global ambiguity and two independent-review findings reproduced as failures before repair.SELECT 1; asqlite_masterread pins it. Isolated serial suite at4ac2146: 392 passed, 5 skipped. Repository mypy: 37 files passed. Pinned Ruff and whitespace checks pass.make test-fast: 1505 passed, 1 skipped;make typecheck: 286 package / 1803 script files passed. These are not aw-core verification.XDG_DATA_HOME. No production DB changed.Remaining boundary
Python HTTP device routes, server UUID/sentinel normalization, grouped HTTP import/export, query/heartbeat cache integration, metadata-repair/alias interaction acceptance, non-SQLite parity and shared Rust/Python acceptance are not implemented here. The Rust draft keeps both global name guards; this preparatory Python storage API is not permission to remove them. Existing schema migration policy discussions on #177 remain separate; no migration policy is changed by this diff.