You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The files aw-sync publishes into the shared folder are sqlite databases in aw-server-rust's internal datastore schema. That schema is undocumented, unversioned as an interchange format, migration-mutable, and — the part that surprised me — not readable by ActivityWatch's own Python server.
The two schemas are structurally incompatible
aw-server-rust/aw-datastore (what lands in the sync folder, read from a real staging db):
CREATETABLEbuckets (
id INTEGERPRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL, type TEXTNOT NULL, client TEXTNOT NULL,
hostname TEXTNOT NULL, created TEXTNOT NULL
, data_deprecated TEXT DEFAULT '{}', data TEXTNOT NULL DEFAULT '{}');
CREATETABLEevents (
id INTEGERPRIMARY KEY AUTOINCREMENT, bucketrow INTEGERNOT NULL,
starttime INTEGERNOT NULL, endtime INTEGERNOT NULL,
data TEXTNOT NULL,
FOREIGN KEY (bucketrow) REFERENCES buckets(id));
aw-core/aw_datastore/storages/sqlite.py:
CREATETABLEIF NOT EXISTS buckets (
rowid INTEGERPRIMARY KEY AUTOINCREMENT,
id TEXT UNIQUE NOT NULL, name TEXT, type TEXTNOT NULL, client TEXTNOT NULL,
hostname TEXTNOT NULL, created TEXTNOT NULL, datastr TEXTNOT NULL);
CREATETABLEIF NOT EXISTS events (
id INTEGERPRIMARY KEY AUTOINCREMENT, bucketrow INTEGERNOT NULL,
starttime INTEGERNOT NULL, endtime INTEGERNOT NULL,
datastr TEXTNOT NULL,
FOREIGN KEY (bucketrow) REFERENCES buckets(rowid));
Differences that matter:
The bucket's string key is buckets.name in Rust and buckets.id in Python. name exists in both and means opposite things — the key in one, a nullable display name in the other.
The bucket's integer PK is id in Rust, rowid in Python, so events.bucketrow points at a differently-named column.
The JSON payload column is data in Rust and datastr in Python, on both tables.
These are not subtle: a query written against one raises "no such column" against the other.
Consequences
A user running aw-server-python cannot read their own sync folder with their own server's storage layer. The bundle still ships aw-server-python as an option, so this is a live split.
The wire format is one implementation's private schema. It changes under migrations — user_version is already at 5 — and the published files carry the evidence: every staging database in my sync folder contains a column literally named data_deprecated, internal migration residue being shipped as the interchange format.
Third-party tooling has to reverse-engineer it. There is no spec, no version marker in the sync folder, and no compatibility statement. sync.rs even carries // TODO: Check for compatible remote db version before opening — so a peer db from a future schema is opened blind.
Measured on a live sync folder — two of three staging databases report journal_mode = wal, and aw-datastore/src/worker.rs enables it deliberately. aw-sync closes the datastore at the end of a pass, so sqlite checkpoints and removes the -wal sidecar; in the steady state the published file is self-contained, which is why no -wal files are currently visible in the folder.
The exposure is the write window. A push into a 273 MB staging database is not instant, and the file syncer is watching the directory: it can begin transferring test.db while a -wal exists and the main file is mid-checkpoint. There is already evidence of the syncer observing concurrent modification here — erb-main3/5a5df0f8-…/test.sync-conflict-20241125-052022-GRUSU5T.db and a second conflict file from the same day.
Publishing a mutable, in-place-updated sqlite file into a directory whose whole purpose is that an external process copies it whenever it changes is a structural mismatch, independent of the schema question.
What this is really asking
Whether the sync folder should contain sqlite at all, versus an explicit, versioned, documented interchange format that is written once and never mutated. That question is under active design review; this issue exists to record the concrete constraints any answer has to satisfy:
readable by both server implementations, and by third-party tools, without reimplementing a private schema
explicitly versioned in the folder, so a peer can refuse or adapt rather than opening blind
safe to publish into a directory an external syncer copies at arbitrary moments
The files aw-sync publishes into the shared folder are sqlite databases in aw-server-rust's internal datastore schema. That schema is undocumented, unversioned as an interchange format, migration-mutable, and — the part that surprised me — not readable by ActivityWatch's own Python server.
The two schemas are structurally incompatible
aw-server-rust/aw-datastore(what lands in the sync folder, read from a real staging db):aw-core/aw_datastore/storages/sqlite.py:Differences that matter:
buckets.namein Rust andbuckets.idin Python.nameexists in both and means opposite things — the key in one, a nullable display name in the other.idin Rust,rowidin Python, soevents.bucketrowpoints at a differently-named column.datain Rust anddatastrin Python, on both tables.These are not subtle: a query written against one raises "no such column" against the other.
Consequences
user_versionis already at 5 — and the published files carry the evidence: every staging database in my sync folder contains a column literally nameddata_deprecated, internal migration residue being shipped as the interchange format.sync.rseven carries// TODO: Check for compatible remote db version before opening— so a peer db from a future schema is opened blind.test.db(see aw-sync: leftovers after #685/#686 — orphaned 2-level staging db, stale -synced-from- buckets, walker enters dot-dirs #689 item 4). A user opening the folder finds N identical filenames in a format nothing outside aw-server-rust can read.Also: WAL
Measured on a live sync folder — two of three staging databases report
journal_mode = wal, andaw-datastore/src/worker.rsenables it deliberately. aw-sync closes the datastore at the end of a pass, so sqlite checkpoints and removes the-walsidecar; in the steady state the published file is self-contained, which is why no-walfiles are currently visible in the folder.The exposure is the write window. A push into a 273 MB staging database is not instant, and the file syncer is watching the directory: it can begin transferring
test.dbwhile a-walexists and the main file is mid-checkpoint. There is already evidence of the syncer observing concurrent modification here —erb-main3/5a5df0f8-…/test.sync-conflict-20241125-052022-GRUSU5T.dband a second conflict file from the same day.Publishing a mutable, in-place-updated sqlite file into a directory whose whole purpose is that an external process copies it whenever it changes is a structural mismatch, independent of the schema question.
What this is really asking
Whether the sync folder should contain sqlite at all, versus an explicit, versioned, documented interchange format that is written once and never mutated. That question is under active design review; this issue exists to record the concrete constraints any answer has to satisfy:
Related: #689 (
test.dbnaming, orphaned dbs), #684 (observability), ActivityWatch/activitywatch#1445.cc @TimeToBuildBob — filing this as a constraints record rather than a proposal; the format decision should wait for the design review.