fix(core): a folder listed just as its create lands is listed from the database (B177)

This commit is contained in:
nvrl committed 2026-10-04 22:41:57 +02:00
1 parent 4f141c78ca
commit 0f3c06f982
5 files changed
+75 -5

No files matched your search

+17 -4
View File
@@ -533,9 +533,10 @@ impl Db {
/// re-enumerates from the remote on next access. /// re-enumerates from the remote on next access.
pub fn set_listed(&self, uid: &NodeUid, listed: bool) -> Result<()> { pub fn set_listed(&self, uid: &NodeUid, listed: bool) -> Result<()> {
let conn = self.conn.lock(); let conn = self.conn.lock();
let (key, uid) = row_key(&uid.to_string());
conn.execute( conn.execute(
"UPDATE nodes SET listed = ?2 WHERE uid = ?1", &format!("UPDATE nodes SET listed = ?2 WHERE {key} = ?1"),
params![uid.to_string(), listed as i64], params![uid, listed as i64],
)?; )?;
Ok(()) Ok(())
} }
@@ -694,10 +695,11 @@ impl Db {
/// server head, so changes made while unmounted are still applied (P2). /// server head, so changes made while unmounted are still applied (P2).
pub fn children_if_listed(&self, parent: &NodeUid) -> Result<Option<Vec<Node>>> { pub fn children_if_listed(&self, parent: &NodeUid) -> Result<Option<Vec<Node>>> {
let conn = self.read(); let conn = self.read();
let (key, folder) = row_key(&parent.to_string());
let listed: Option<i64> = conn let listed: Option<i64> = conn
.query_row( .query_row(
"SELECT listed FROM nodes WHERE uid = ?1", &format!("SELECT listed FROM nodes WHERE {key} = ?1"),
params![parent.to_string()], params![folder],
|r| r.get(0), |r| r.get(0),
) )
.optional()?; .optional()?;
@@ -792,6 +794,17 @@ pub(super) fn node_lid(conn: &rusqlite::Connection, uid: &str) -> Result<Option<
Ok(row.optional()?) Ok(row.optional()?)
} }
/// The column and value that find `uid`'s own row. A stand-in is found by its
/// local id: a create that lands gives the row the uid Drive made before the
/// tree has it, and the tree asks by the stand-in until then (`docs/BUGS.md`
/// B177).
fn row_key(uid: &str) -> (&'static str, Value) {
match local_lid(uid) {
Some(lid) => ("lid", Value::Integer(lid)),
None => ("uid", Value::Text(uid.to_owned())),
}
}
/// The column and value a row names `parent` by: its local id when it has a /// The column and value a row names `parent` by: its local id when it has a
/// row, which the children keep when it lands, or its uid when it has none, /// row, which the children keep when it lands, or its uid when it has none,
/// as a device folder's root does. /// as a device folder's root does.
+31
View File
@@ -3282,6 +3282,37 @@ fn a_stand_in_keeps_its_access_once_its_create_has_landed() {
); );
} }
#[test]
fn a_stand_in_folder_stays_listed_once_its_create_has_landed() {
let db = Db::open_in_memory().unwrap();
db.upsert_node(&folder("parent", None, "My Files")).unwrap();
let parent = uid("parent").to_string();
let lid = db.add_local_row(&parent, "dir", true, 1).unwrap();
let local = local_uid(lid);
let stand_in = NodeUid::new(VolumeId::from("local"), LinkId::from(lid.to_string()));
let mut node = folder("new", Some("parent"), "dir");
node.uid = stand_in.clone();
db.upsert_node(&node).unwrap();
db.set_listed(&stand_in, true).unwrap();
let real = uid("real").to_string();
let (id, _) = db
.enqueue_op(&op_on(OP_MKDIR, &local, Some(&parent)))
.unwrap();
db.claim_next_due_op(1).unwrap().unwrap();
db.finish_create(id, None, &landing(&local, &real, &parent, "dir"), |_| None)
.unwrap();
// The tree still names the folder by its stand-in for a moment; a listing
// asked by it must not go to Drive, which has never heard of it.
assert!(
db.children_if_listed(&stand_in)
.unwrap()
.is_some_and(|kids| kids.is_empty())
);
db.set_listed(&stand_in, false).unwrap();
assert!(db.children_if_listed(&uid("real")).unwrap().is_none());
}
#[test] #[test]
fn a_node_queued_under_both_its_uids_is_sent_one_op_at_a_time() { fn a_node_queued_under_both_its_uids_is_sent_one_op_at_a_time() {
let db = Db::open_in_memory().unwrap(); let db = Db::open_in_memory().unwrap();
+22
View File
@@ -12,6 +12,28 @@ Conventions:
--- ---
## B177 — A folder listed just as its create lands answers "Input/output error"
**Status:** Fixed (unverified).
**Found:** 2026-10-04, by the account run on 378667f, on an on-demand sync folder: B100's
`lstat` of `b100-copy/dir-0/sub-0/file-00.bin` failed with `EIO`. The daemon logged
`enumerate folder children failed folder_uid=local~163903 ... InvalidEncryptedIdFormat` 85 ms
before `dir-0`'s create landed.
**Where:** `Db::children_if_listed` and `Db::set_listed` in `crates/pdfs-core/src/db/nodes.rs`.
**Cause.** A folder made on the mount is marked as listed, so its listing comes from the
database and never from Drive, which has not heard of it. The flag was looked up by uid. In
B173's gap the row already has the uid Drive gave the folder and the tree still asks by the
stand-in, so the lookup found no row, and the listing went to Drive with the stand-in.
**Fix.** A stand-in finds its row by its local id, before and after its create lands.
**Test:** `a_stand_in_folder_stays_listed_once_its_create_has_landed` in
`crates/pdfs-core/src/db/tests.rs`.
---
## B176 — A write closed just as its file's create lands is dropped ## B176 — A write closed just as its file's create lands is dropped
**Status:** Fixed (unverified). **Status:** Fixed (unverified).
+2
View File
@@ -79,6 +79,8 @@ by its local id, in the queue, the stored tree and pins alike.
- **A file replaced by a rename just as it reached Drive is removed from Drive too.** It no - **A file replaced by a rename just as it reached Drive is removed from Drive too.** It no
longer stayed there, and the next rename of its name no longer moved it instead (B175). longer stayed there, and the next rename of its name no longer moved it instead (B175).
- **A write closed just as its file reached Drive is no longer lost** (B176). - **A write closed just as its file reached Drive is no longer lost** (B176).
- **Opening a folder just as it reaches Drive no longer fails with "Input/output error"**
(B177).
- **A file renamed just as its upload finished keeps its new name.** The mount no longer showed - **A file renamed just as its upload finished keeps its new name.** The mount no longer showed
the old name, and a second rename no longer failed with "No such file or directory" (B152, the old name, and a second rename no longer failed with "No such file or directory" (B152,
B172). B172).
+3 -1
View File
@@ -9,7 +9,7 @@ Tracks `docs/MILESTONE-3.0.0.md`. Update it with every commit that moves a phase
- **Phase 2**: local ids, inode = local id, one tree for every mount (2.10.0). - **Phase 2**: local ids, inode = local id, one tree for every mount (2.10.0).
- **Phase 3, first part**: mkdir, create, rename, unlink and rmdir on the mount are recorded - **Phase 3, first part**: mkdir, create, rename, unlink and rmdir on the mount are recorded
locally and sent from the queue; `"local_first": false` brings back the old path (5e0f30c). locally and sent from the queue; `"local_first": false` brings back the old path (5e0f30c).
- Fixes from the simulation and account runs since then: B127 and B141 to B176. - Fixes from the simulation and account runs since then: B127 and B141 to B177.
- **Sync issues, first cut** (§6.2): refusals sorted into issues, shown at once in the app, - **Sync issues, first cut** (§6.2): refusals sorted into issues, shown at once in the app,
tray and CLI; Export button and `pdfs sync export`; `pdfs sync issues`. tray and CLI; Export button and `pdfs sync export`; `pdfs sync issues`.
@@ -121,6 +121,8 @@ Tracks `docs/MILESTONE-3.0.0.md`. Update it with every commit that moves a phase
in the same gap was refused with `EACCES` (B174, fixed). The loop on 378667f failed in the same gap was refused with `EACCES` (B174, fixed). The loop on 378667f failed
seed 8: a file replaced by a rename in that gap stayed on Drive (B175, fixed), and seed 8: a file replaced by a rename in that gap stayed on Drive (B175, fixed), and
seed 12: a write closed as its file's create landed was dropped (B176, fixed). seed 12: a write closed as its file's create landed was dropped (B176, fixed).
The account run on 378667f: a folder listed as its create landed asked Drive by
its stand-in and answered `EIO` (B177, fixed).
5. **Release**: 2.10.0 is tagged; the 2.9.0 tag still waits for the user's go-ahead. 5. **Release**: 2.10.0 is tagged; the 2.9.0 tag still waits for the user's go-ahead.
## After 3.0.0 ## After 3.0.0