diff --git a/crates/pdfs-core/src/db/nodes.rs b/crates/pdfs-core/src/db/nodes.rs index 766349c..de5c804 100644 --- a/crates/pdfs-core/src/db/nodes.rs +++ b/crates/pdfs-core/src/db/nodes.rs @@ -733,6 +733,13 @@ pub(super) fn adopt_placeholder_row_tx( local: &str, real: &str, ) -> Result<()> { + // A folder pinned before it landed stays pinned: the pin keyed by the + // placeholder was dropped with it (`docs/BUGS.md` B148). + tx.execute( + "UPDATE OR IGNORE pins SET uid = ?2 WHERE uid = ?1", + params![local, real], + )?; + tx.execute("DELETE FROM pins WHERE uid = ?1", params![local])?; // The node a row holds names its parent too, and is what a restart builds // the tree from: a folder that landed with no write to its children after // left them under a parent no row has (`docs/BUGS.md` B140). diff --git a/crates/pdfs-core/src/db/tests.rs b/crates/pdfs-core/src/db/tests.rs index ae4bd6d..af9ad91 100644 --- a/crates/pdfs-core/src/db/tests.rs +++ b/crates/pdfs-core/src/db/tests.rs @@ -3321,6 +3321,53 @@ fn a_landed_folder_keeps_a_row_until_the_server_copy_replaces_it() { assert_eq!(found[0].path, "New folder/inside.txt"); } +/// B148: a folder pinned before it landed is still pinned once it has. +#[test] +fn a_folder_pinned_before_it_landed_stays_pinned() { + let db = Db::open_in_memory().unwrap(); + db.upsert_node(&folder("root", None, "My Files")).unwrap(); + let local = NodeUid::new(VolumeId::from("local"), LinkId::from("dir")); + let mut placeholder = folder("dir", Some("root"), "Keep"); + placeholder.uid = local.clone(); + db.upsert_node(&placeholder).unwrap(); + db.pin_add(&local.to_string(), "Keep", true).unwrap(); + let (id, _) = db + .enqueue_op(&PendingOp { + id: 0, + kind: OP_MKDIR.to_string(), + uid: local.to_string(), + parent_uid: Some(uid("root").to_string()), + name: Some("Keep".to_string()), + blob_path: None, + meta_json: None, + created_at: 1, + attempts: 0, + last_error: None, + next_attempt_at: 0, + }) + .unwrap(); + + db.finish_create( + id, + None, + &landing( + &local.to_string(), + &uid("real").to_string(), + &uid("root").to_string(), + "Keep", + ), + |_| None, + ) + .unwrap(); + + assert!(db.is_pinned(&uid("real").to_string()).unwrap()); + assert!(!db.is_pinned(&local.to_string()).unwrap()); + let pins = db.pin_list().unwrap(); + assert_eq!(pins.len(), 1); + assert_eq!(pins[0].uid, uid("real").to_string()); + assert!(pins[0].recursive); +} + /// A file deleted while its create was on the wire still lands: nothing stops /// an upload in flight. The node it made is unwanted, so a trash of it is /// queued in the same transaction, already claimed by the drain that landed it diff --git a/docs/BUGS.md b/docs/BUGS.md index b8fa298..cb37ba2 100644 --- a/docs/BUGS.md +++ b/docs/BUGS.md @@ -12,6 +12,23 @@ Conventions: --- +## B148 — A folder pinned before it has reached Drive loses its pin + +**Status:** Fixed (unverified). +**Found:** 2026-10-04, while planning the move of queued ops to local ids: every table keyed by +uid was checked for what happens when a placeholder lands, and `pins` was the one no landing +touched. + +**Where:** `adopt_placeholder_row_tx` in `crates/pdfs-core/src/db/nodes.rs`. + +**Cause.** A folder made through the mount has a placeholder uid (`local~…`) until its mkdir lands. +Pinning it then stored the placeholder in `pins.uid`. The landing moved the node row, the children +and the queued ops to the real uid, but not the pin, which then named no node: the folder was no +longer kept offline and the pin list showed an entry for nothing. + +**Fix.** The landing moves the pin to the real uid in the same transaction. A pin the real uid +already has wins, and the placeholder's is dropped. + ## B147 — A new file made under a name just renamed away can share the first file's node **Status:** Fixed (unverified). diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 0ca936e..a1a14e1 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -31,6 +31,8 @@ are kept. Queued changes get a column for the reason Drive refused them. for this release. ### Fixed +- **A folder kept offline before it reached Drive stays kept offline.** Its pin was lost when the + folder landed (B148). - **A queued change no longer holds up the queue forever on a link that stops answering.** Sending a new folder, a delete or a file waits at most 20 seconds for Drive, plus time for the file's size, and then the queue treats the link as down and tries again.