Read the library's sidecars, so a cull done elsewhere arrives
Judgements only ever travelled outward. A rating went to the catalog and to the photograph's sidecar, the sidecar reached the server, and there it stopped: the scan indexes files, `derived_sync` exchanges thumbnails, face shards and collections, `dr_catalog::merge` reconciles everything in a catalog except `versions.rating` and `versions.flag`, and the one sidecar reader that existed ran when a single photograph was opened in develop and handed its answer to the develop graph. `JobKind::ReadSidecar` was declared for exactly this when the job queue was written and was never enqueued or handled anywhere. The grid draws `versions.rating`. So a day of culling on the tablet could not reach the laptop by any path the application had, and the laptop's catalog says so plainly: 23,568 images, one of them judged. `pull_sidecars` closes it, off the back of work the scan already does. `dr_sync::scan` reports the `.drsc` files it meets in listings it was making anyway — no extra request, and a directory whose ETag is unchanged is still pruned before it is listed at all. A new `sidecars` table records the ETag of each one this device has taken in, so the fetch is one GET per sidecar that genuinely changed rather than one per photograph. A library nobody has edited costs nothing. The judgement is taken rather than maximised. The sidecar is the authoritative store and the fuse has already settled any contest between devices on `revision`, so lowering a rating from four to one on the tablet lowers it here — taking the larger would have refused every demotion the photographer ever made, which is most of what a second pass over a shoot is. A zero is the exception: it means *never judged*, not "judged zero", so a sidecar carrying none cannot erase a star this device holds. That is `merge_judgement`'s asymmetry and it carries the same known cost — clearing a rating does not propagate. A sidecar names a stem, so both halves of a RAW-and-JPEG pair are judged: they are one photograph (FR-CAT-11) sharing one document, and judging only one of them would leave the grid disagreeing with itself over which it drew. The `LIKE` that finds them is a filter, not the decision — `sidecar_path` is applied to every candidate, because a folder is entitled to contain a `%` and a rating landing on the wrong frame would be silent and permanent. Failing to read one is not a failure to scan: the ETag goes unrecorded, the ratings already here stay where they are, and the next scan tries again. The count is reported to the status line as well as the log, because a grid that silently gains three hundred stars is indistinguishable from one that has gone wrong — and because while this number was structurally zero there was nothing to tell the photographer their cull had not arrived. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,7 @@ use rusqlite::Connection;
|
||||
use crate::error::CatalogError;
|
||||
|
||||
/// Schema version this build writes and understands.
|
||||
pub const SCHEMA_VERSION: i64 = 12;
|
||||
pub const SCHEMA_VERSION: i64 = 13;
|
||||
|
||||
/// Apply migrations up to [`SCHEMA_VERSION`].
|
||||
///
|
||||
@@ -112,6 +112,13 @@ pub fn migrate(conn: &Connection) -> Result<i64, CatalogError> {
|
||||
tx.commit()?;
|
||||
}
|
||||
|
||||
if from < 13 {
|
||||
let tx = conn.unchecked_transaction()?;
|
||||
tx.execute_batch(V13)?;
|
||||
tx.pragma_update(None, "user_version", 13)?;
|
||||
tx.commit()?;
|
||||
}
|
||||
|
||||
Ok(from)
|
||||
}
|
||||
|
||||
@@ -521,6 +528,47 @@ const V12: &str = r#"
|
||||
DELETE FROM face_index WHERE source_edge <= 1024;
|
||||
"#;
|
||||
|
||||
const V13: &str = r#"
|
||||
-- TRACES: FR-CAT-8 | FR-NC-9
|
||||
-- Which sidecars this device has read, and at what ETag.
|
||||
--
|
||||
-- The sidecar is the authoritative store for a rating and an edit, and until
|
||||
-- this table existed nothing ever read one back into the catalog: judgements
|
||||
-- travelled outward only. A cull done on a tablet reached the server and
|
||||
-- stopped there, because the scan indexes photographs, the derived sync moves
|
||||
-- thumbnails and collections, and the one reader that existed ran when a single
|
||||
-- photograph was opened in develop and fed only the develop graph. The grid
|
||||
-- draws `versions.rating`, so another device's afternoon of culling was
|
||||
-- invisible on this one -- permanently, by every path the app had.
|
||||
--
|
||||
-- What this holds is the ETag, not the content. It is the record of what has
|
||||
-- already been taken in, so a pull fetches only what changed: `dr_sync::scan`
|
||||
-- reports every sidecar it saw in listings it was making anyway, and this
|
||||
-- decides which of them are worth a GET.
|
||||
--
|
||||
-- Keyed on the sidecar's own remote path rather than on an image id. One
|
||||
-- sidecar can describe two images -- a RAW and the JPEG beside it are one
|
||||
-- photograph (FR-CAT-11) and share a document -- and a path is what the scan
|
||||
-- reports and what a fetch addresses, so keying on anything else would mean
|
||||
-- deriving one from the other in two places.
|
||||
--
|
||||
-- Rebuildable like the rest of the catalog: losing this table costs one pass
|
||||
-- that re-reads every sidecar and reaches exactly the same state.
|
||||
--
|
||||
-- `IF NOT EXISTS` because NFR-R5 asks for migrations that are idempotent on
|
||||
-- retry, and this one can genuinely be re-entered: a catalog whose
|
||||
-- `user_version` was rewound -- by a rollback to an older build, or by a
|
||||
-- recovery -- would otherwise fail its next open on a table it already has.
|
||||
CREATE TABLE IF NOT EXISTS sidecars (
|
||||
root_id INTEGER NOT NULL REFERENCES roots(id) ON DELETE CASCADE,
|
||||
path TEXT NOT NULL,
|
||||
etag TEXT,
|
||||
-- Unix seconds, for diagnosing a pull that is not making progress.
|
||||
read_at INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY(root_id, path)
|
||||
);
|
||||
"#;
|
||||
|
||||
const V9: &str = r#"
|
||||
-- TRACES: FR-CULL-8
|
||||
-- A record that face detection has *run* on an image, distinct from what it
|
||||
|
||||
Reference in New Issue
Block a user