From e5f3f612f86db25680f70d6c7b1bb189349840ba Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sun, 4 Oct 2026 12:32:20 +0200 Subject: [PATCH] refactor(core,fuse): a node made here goes by its local id until it lands (schema 41) A file or folder made on the mount used to get a minted `local~-` placeholder. It now gets `local~`, a function of the row it is given first (`Db::add_local_row`), so the stand-in never changes and always leads back to the row, and through it to the real uid once the node lands. `Db::lid_of` reads it without a lookup. Migration V41 rewrites the placeholders still stored to their row's stand-in: the row and its node_json, its children, the ops on it or into it (uid, parent, sidecar uid, a rename's original parent), and its pins. Step 6a of the plan for 3.0.0 phase 3. --- crates/pdfs-core/src/db/migrations.rs | 59 +++++++++- crates/pdfs-core/src/db/mod.rs | 2 +- crates/pdfs-core/src/db/nodes.rs | 54 ++++++++- crates/pdfs-core/src/db/ops.rs | 22 +++- crates/pdfs-core/src/db/tests.rs | 161 +++++++++++++++++++++++--- crates/pdfs-fuse/src/lib.rs | 58 +++++----- docs/CHANGELOG.md | 5 +- docs/MILESTONE-3.0.0-STATUS.md | 9 +- 8 files changed, 312 insertions(+), 58 deletions(-) diff --git a/crates/pdfs-core/src/db/migrations.rs b/crates/pdfs-core/src/db/migrations.rs index c564771..f73f9f5 100644 --- a/crates/pdfs-core/src/db/migrations.rs +++ b/crates/pdfs-core/src/db/migrations.rs @@ -11,7 +11,7 @@ use super::Db; use crate::Result; /// Current schema version. Bump on every forward migration added below. -pub(super) const SCHEMA_VERSION: i64 = 40; +pub(super) const SCHEMA_VERSION: i64 = 41; impl Db { pub(super) fn migrate(&self) -> Result<()> { @@ -411,6 +411,19 @@ impl Db { tx.execute_batch(MIGRATION_V40)?; } } + if current < 41 { + // Needs both tables; on a database that never had them there is + // nothing to rewrite. + let tables: i64 = tx.query_row( + "SELECT COUNT(*) FROM sqlite_master + WHERE type = 'table' AND name IN ('nodes', 'pending_op', 'pins')", + [], + |row| row.get(0), + )?; + if tables == 3 { + tx.execute_batch(MIGRATION_V41)?; + } + } tx.execute( "INSERT INTO sync_state (key, value) VALUES ('schema_version', ?1) ON CONFLICT(key) DO UPDATE SET value = excluded.value", @@ -1245,3 +1258,47 @@ ALTER TABLE nodes ADD COLUMN parent_lid INTEGER; UPDATE nodes SET parent_lid = (SELECT p.lid FROM nodes p WHERE p.uid = nodes.parent_uid); CREATE INDEX idx_nodes_parent_lid ON nodes(parent_lid); "; + +/// Schema v41: a node made on this machine goes by `local~` until it +/// lands, a function of its row, where it used to get a `local~-` +/// placeholder. Every placeholder still stored is rewritten to its row's +/// stand-in: the row itself and its `node_json`, its children, the ops on it +/// or into it, and its pins. A placeholder op with no row keeps its uid and +/// drains as before. +const MIGRATION_V41: &str = " +CREATE TEMP TABLE local_v41 AS + SELECT uid AS old, 'local~' || lid AS new, lid FROM nodes + WHERE uid LIKE 'local~%' AND uid <> 'local~' || lid; +UPDATE pending_op SET uid = (SELECT new FROM local_v41 WHERE old = pending_op.uid) + WHERE uid IN (SELECT old FROM local_v41); +UPDATE pending_op SET parent_uid = (SELECT new FROM local_v41 WHERE old = pending_op.parent_uid) + WHERE parent_uid IN (SELECT old FROM local_v41); +UPDATE pending_op + SET meta_json = json_set(meta_json, '$.uid', + (SELECT new FROM local_v41 WHERE old = json_extract(pending_op.meta_json, '$.uid'))) + WHERE json_valid(meta_json) + AND json_extract(meta_json, '$.uid') IN (SELECT old FROM local_v41); +UPDATE pending_op + SET meta_json = json_set(meta_json, '$.original_parent_uid', + (SELECT new FROM local_v41 + WHERE old = json_extract(pending_op.meta_json, '$.original_parent_uid'))) + WHERE json_valid(meta_json) + AND json_extract(meta_json, '$.original_parent_uid') IN (SELECT old FROM local_v41); +UPDATE pins SET uid = (SELECT new FROM local_v41 WHERE old = pins.uid) + WHERE uid IN (SELECT old FROM local_v41); +UPDATE nodes + SET node_json = json_set(node_json, '$.parent_uid.link_id', + (SELECT CAST(lid AS TEXT) FROM local_v41 WHERE old = nodes.parent_uid)) + WHERE parent_uid IN (SELECT old FROM local_v41) + AND json_valid(node_json) + AND json_extract(node_json, '$.parent_uid.volume_id') = 'local'; +UPDATE nodes SET parent_uid = (SELECT new FROM local_v41 WHERE old = nodes.parent_uid) + WHERE parent_uid IN (SELECT old FROM local_v41); +UPDATE nodes + SET node_json = json_set(node_json, '$.uid.link_id', CAST(lid AS TEXT)) + WHERE lid IN (SELECT lid FROM local_v41) + AND json_valid(node_json) + AND json_extract(node_json, '$.uid.volume_id') = 'local'; +UPDATE nodes SET uid = 'local~' || lid WHERE lid IN (SELECT lid FROM local_v41); +DROP TABLE local_v41; +"; diff --git a/crates/pdfs-core/src/db/mod.rs b/crates/pdfs-core/src/db/mod.rs index 32a1701..611e0e3 100644 --- a/crates/pdfs-core/src/db/mod.rs +++ b/crates/pdfs-core/src/db/mod.rs @@ -58,7 +58,7 @@ pub use nodes::{PublishedSharedRoot, SearchHit, StoredNode}; pub use ops::{ AttachedBlob, CreateLanding, CreateRetired, FAILING_ATTEMPTS, LOCAL_VOLUME, OP_CREATE, OP_MKDIR, OP_RENAME, OP_REVISION, OP_TRASH, PARK_EXPIRY_MS, PARK_UNTIL, PendingCounts, - PendingOp, RenameMeta, op_supersedes, + PendingOp, RenameMeta, local_lid, local_uid, op_supersedes, }; pub use photos::{StoredPhoto, StoredPlace, THUMB_HAVE, THUMB_NONE, THUMB_UNKNOWN, TimelineRow}; pub use pins::PinRow; diff --git a/crates/pdfs-core/src/db/nodes.rs b/crates/pdfs-core/src/db/nodes.rs index 4672594..33f8da4 100644 --- a/crates/pdfs-core/src/db/nodes.rs +++ b/crates/pdfs-core/src/db/nodes.rs @@ -6,7 +6,7 @@ use std::collections::HashSet; use rusqlite::{OptionalExtension, Transaction, params}; use super::Db; -use super::ops::{OP_CREATE, OP_MKDIR, OP_RENAME, wake_ops_waiting_for_tx}; +use super::ops::{OP_CREATE, OP_MKDIR, OP_RENAME, local_lid, local_uid, wake_ops_waiting_for_tx}; use crate::{Access, Result}; use proton_drive_rs::proton_sdk::ids::{LinkId, NodeUid, VolumeId}; use proton_drive_rs::{Node, NodeKind}; @@ -593,14 +593,21 @@ impl Db { /// The local id of the node stored under `uid`, or `None` when it has no /// row. It stays the same for as long as the row exists, including when a - /// create lands and the row takes the uid Drive gave it. + /// create lands and the row takes the uid Drive gave it: a node made on + /// this machine is found by its stand-in ([`local_uid`]) before and after. pub fn lid_of(&self, uid: &str) -> Result> { let conn = self.read(); - Ok(conn - .query_row("SELECT lid FROM nodes WHERE uid = ?1", params![uid], |r| { + let row = match local_lid(uid) { + Some(lid) => { + conn.query_row("SELECT lid FROM nodes WHERE lid = ?1", params![lid], |r| { + r.get(0) + }) + } + None => conn.query_row("SELECT lid FROM nodes WHERE uid = ?1", params![uid], |r| { r.get(0) - }) - .optional()?) + }), + }; + Ok(row.optional()?) } /// The local id of each node, in order, giving a row to each that has none. @@ -644,6 +651,41 @@ impl Db { Ok(lids) } + /// Give a node being made on this machine its row under `parent_uid`, and + /// return its local id. The row goes by [`local_uid`] of that id until + /// the node lands. + /// + /// Like [`Db::lids_for`], only a stub: the caller upserts the node. + pub fn add_local_row( + &self, + parent_uid: &str, + name: &str, + is_dir: bool, + mtime: i64, + ) -> Result { + let mut conn = self.conn.lock(); + let tx = conn.transaction()?; + tx.execute( + "INSERT INTO nodes (uid, parent_uid, name, is_dir, mtime, trashed, parent_lid) + VALUES (NULL, ?1, ?2, ?3, ?4, 0, + COALESCE(?5, (SELECT lid FROM nodes WHERE uid = ?1)))", + params![ + parent_uid, + name, + is_dir as i64, + mtime, + local_lid(parent_uid) + ], + )?; + let lid = tx.last_insert_rowid(); + tx.execute( + "UPDATE nodes SET uid = ?2 WHERE lid = ?1", + params![lid, local_uid(lid)], + )?; + tx.commit()?; + Ok(lid) + } + /// Read the persisted incremental-sync cursor (a `DriveEventId`), if any. /// The daemon resumes from this on restart instead of reseeding to the /// server head, so changes made while unmounted are still applied (P2). diff --git a/crates/pdfs-core/src/db/ops.rs b/crates/pdfs-core/src/db/ops.rs index 28446b5..10e43cc 100644 --- a/crates/pdfs-core/src/db/ops.rs +++ b/crates/pdfs-core/src/db/ops.rs @@ -205,13 +205,33 @@ const PARENT_NOW: &str = "COALESCE((SELECT uid FROM nodes WHERE lid = pending_op.parent_lid), parent_uid)"; /// The volume id given to a node that exists only on this machine, so far. A -/// real [`NodeUid`] is `{volume}~{link}`, so a placeholder is `local~` and +/// real [`NodeUid`] is `{volume}~{link}`, so a placeholder is `local~` and /// round-trips through the same `Display`/parse path as any other uid. /// /// Nothing bearing this volume may be handed to the API — it would 404. The /// drain replaces it with the uid the server assigns. pub const LOCAL_VOLUME: &str = "local"; +/// The uid a node made on this machine goes by until Drive gives it one: its +/// row's local id under [`LOCAL_VOLUME`]. +/// +/// A function of the row, so it never changes: whatever still holds it once +/// the node has landed finds the node, and its real uid, through +/// [`local_lid`]. +pub fn local_uid(lid: i64) -> String { + format!("{LOCAL_VOLUME}~{lid}") +} + +/// The local id a [`local_uid`] stands for, or `None` for any other uid, +/// including a placeholder minted before schema 41. +pub fn local_lid(uid: &str) -> Option { + let digits = uid.strip_prefix(LOCAL_VOLUME)?.strip_prefix('~')?; + if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) { + return None; + } + digits.parse().ok() +} + /// A `next_attempt_at` far enough in the future that [`Db::next_due_op`] never /// selects the op (ms since epoch, ≈ year 2223). Used to *park* a queued create /// for a transient file — a browser's in-flight `*.crdownload`/`*.part`, an diff --git a/crates/pdfs-core/src/db/tests.rs b/crates/pdfs-core/src/db/tests.rs index 25872c8..5556a0b 100644 --- a/crates/pdfs-core/src/db/tests.rs +++ b/crates/pdfs-core/src/db/tests.rs @@ -3897,15 +3897,25 @@ fn migration_v36_gives_every_node_its_rowid_as_lid() { assert_eq!(rowids.len(), 3); let db = Db::open(&path).unwrap(); + // The placeholder goes by its row's stand-in from V41 on. + let stand_in = |uid: &str, rowid: i64| match uid == local.to_string() { + true => local_uid(rowid), + false => uid.to_string(), + }; for (uid, rowid) in &rowids { - assert_eq!(db.lid_of(uid).unwrap(), Some(*rowid), "{uid}"); + let uid = stand_in(uid, *rowid); + assert_eq!(db.lid_of(&uid).unwrap(), Some(*rowid), "{uid}"); } let found = db.search("report", 10).unwrap(); assert_eq!(found.len(), 1, "the search index still finds the node"); assert_eq!(found[0].node.uid, uid("kept")); let ops = db.pending_ops().unwrap(); assert_eq!(ops.len(), 1); - assert_eq!(ops[0].uid, local.to_string()); + let (_, dir) = rowids + .iter() + .find(|(uid, _)| *uid == local.to_string()) + .unwrap(); + assert_eq!(ops[0].uid, local_uid(*dir)); assert_eq!(ops[0].attempts, 1); assert_eq!(ops[0].last_error.as_deref(), Some("network unreachable")); @@ -3916,6 +3926,93 @@ fn migration_v36_gives_every_node_its_rowid_as_lid() { let _ = std::fs::remove_file(&path); } +#[test] +fn migration_v41_gives_every_placeholder_its_rows_stand_in() { + let path = std::env::temp_dir().join(format!( + "pdfs-db-v40-fixture-{}-{}.db", + std::process::id(), + now_test_id() + )); + let _ = std::fs::remove_file(&path); + let (dir, inner) = { + let db = Db::open(&path).unwrap(); + db.upsert_node(&folder("root", None, "My Files")).unwrap(); + let old_dir = NodeUid::new(VolumeId::from("local"), LinkId::from("1700000000000-0")); + let old_inner = NodeUid::new(VolumeId::from("local"), LinkId::from("1700000000000-1")); + let mut dir = folder("x", Some("root"), "Offline folder"); + dir.uid = old_dir.clone(); + db.upsert_node(&dir).unwrap(); + let mut inner = file("x", "root", "inner.txt", 0); + inner.uid = old_inner.clone(); + inner.parent_uid = Some(old_dir.clone()); + db.upsert_node(&inner).unwrap(); + let op = |kind: &str, uid: &NodeUid, parent: &str, meta: Option| PendingOp { + id: 0, + kind: kind.to_string(), + uid: uid.to_string(), + parent_uid: Some(parent.to_string()), + name: Some("n".to_string()), + blob_path: None, + meta_json: meta, + created_at: 1, + attempts: 0, + last_error: None, + next_attempt_at: 0, + }; + db.enqueue_op(&op(OP_MKDIR, &old_dir, &uid("root").to_string(), None)) + .unwrap(); + let staged = json!({"uid": old_inner.to_string(), "len": 0}).to_string(); + db.enqueue_op(&op( + OP_CREATE, + &old_inner, + &old_dir.to_string(), + Some(staged), + )) + .unwrap(); + db.pin_add(&old_dir.to_string(), "/Offline folder", true) + .unwrap(); + let lids = ( + db.lid_of(&old_dir.to_string()).unwrap().unwrap(), + db.lid_of(&old_inner.to_string()).unwrap().unwrap(), + ); + drop(db); + rusqlite::Connection::open(&path) + .unwrap() + .execute( + "UPDATE sync_state SET value = '40' WHERE key = 'schema_version'", + [], + ) + .unwrap(); + lids + }; + + let db = Db::open(&path).unwrap(); + let (dir_uid, inner_uid) = (local_uid(dir), local_uid(inner)); + assert_eq!(db.uid_of_lid(dir).unwrap(), Some(dir_uid.clone())); + assert_eq!(db.uid_of_lid(inner).unwrap(), Some(inner_uid.clone())); + let stored = db.node_by_uid(&inner_uid).unwrap().unwrap(); + assert_eq!( + stored.uid.to_string(), + inner_uid, + "node_json follows the row" + ); + assert_eq!( + stored.parent_uid.map(|p| p.to_string()), + Some(dir_uid.clone()) + ); + let ops = db.pending_ops().unwrap(); + assert_eq!(ops[0].uid, dir_uid); + assert_eq!(ops[1].uid, inner_uid); + assert_eq!(ops[1].parent_uid, Some(dir_uid.clone())); + let staged: serde_json::Value = + serde_json::from_str(ops[1].meta_json.as_deref().unwrap()).unwrap(); + assert_eq!(staged["uid"], inner_uid.as_str()); + let pins: Vec = db.pin_list().unwrap().into_iter().map(|p| p.uid).collect(); + assert_eq!(pins, vec![dir_uid]); + drop(db); + let _ = std::fs::remove_file(&path); +} + /// A database as 2.8.2 wrote it (`sqlite3 .dump`, schema 35), with queued ops /// in every state the drain leaves them in: due, parked, failing, deferred for /// access, and claimed by a worker when the daemon stopped. The creates below @@ -3935,36 +4032,45 @@ fn a_2_8_database_migrates_with_every_node_and_op_intact() { let (nodes, ops) = migrates_intact(&path); assert_eq!((nodes, ops), (8, 6)); let db = Db::open(&path).unwrap(); + let lid_named = |name: &str| -> i64 { + db.conn + .lock() + .query_row("SELECT lid FROM nodes WHERE name = ?1", [name], |r| { + r.get(0) + }) + .unwrap() + }; + let (dir, inner_lid) = (lid_named("Offline folder"), lid_named("inner.txt")); let queued = db.pending_ops().unwrap(); - let inner = queued.iter().find(|op| op.uid == "local~inner").unwrap(); - assert_eq!(inner.parent_uid.as_deref(), Some("local~dir")); + let inner = queued + .iter() + .find(|op| op.uid == local_uid(inner_lid)) + .unwrap(); + assert_eq!(inner.parent_uid, Some(local_uid(dir))); let lids: (Option, Option) = db .conn .lock() .query_row( - "SELECT lid, parent_lid FROM pending_op WHERE uid = 'local~inner'", - [], + "SELECT lid, parent_lid FROM pending_op WHERE id = ?1", + [inner.id], |r| Ok((r.get(0)?, r.get(1)?)), ) .unwrap(); assert_eq!( lids, - ( - db.lid_of("local~inner").unwrap(), - db.lid_of("local~dir").unwrap() - ), + (Some(inner_lid), Some(dir)), "the migration names the op's node and parent by local id" ); let parent_lid: Option = db .conn .lock() .query_row( - "SELECT parent_lid FROM nodes WHERE uid = 'local~inner'", - [], + "SELECT parent_lid FROM nodes WHERE lid = ?1", + [inner_lid], |r| r.get(0), ) .unwrap(); - assert_eq!(parent_lid, db.lid_of("local~dir").unwrap()); + assert_eq!(parent_lid, Some(dir)); let parked = db.parked_create_ops().unwrap(); assert_eq!(parked.len(), 1); assert_eq!(parked[0].name.as_deref(), Some("movie.part")); @@ -3997,7 +4103,7 @@ fn a_copy_of_a_real_database_migrates_with_every_node_and_op_intact() { /// Opens the database at `path` and checks that the migration kept every node /// on its row id and every queued op as it was. Returns how many of each. fn migrates_intact(path: &std::path::Path) -> (usize, usize) { - let (version, nodes, ops, sealed) = { + let (version, nodes, mut ops, sealed) = { let conn = rusqlite::Connection::open(path).unwrap(); let version: String = conn .query_row( @@ -4017,6 +4123,33 @@ fn migrates_intact(path: &std::path::Path) -> (usize, usize) { let sealed = rows(&conn, "SELECT * FROM own_sealed_rev ORDER BY uid"); (version, nodes, ops, sealed) }; + // A node made on this machine goes by its row's stand-in from schema 41 + // on, in its ops as everywhere else. + let stand_ins: Vec<(String, String)> = nodes + .iter() + .filter(|(uid, rowid)| uid.starts_with("local~") && *uid != local_uid(*rowid)) + .map(|(uid, rowid)| (uid.clone(), local_uid(*rowid))) + .collect(); + let nodes: Vec<(String, i64)> = nodes + .into_iter() + .map( + |(uid, rowid)| match stand_ins.iter().any(|(old, _)| *old == uid) { + true => (local_uid(rowid), rowid), + false => (uid, rowid), + }, + ) + .collect(); + for value in ops.iter_mut().flatten() { + if let rusqlite::types::Value::Text(text) = value { + for (old, new) in &stand_ins { + if text == old { + *text = new.clone(); + } else { + *text = text.replace(&format!("\"{old}\""), &format!("\"{new}\"")); + } + } + } + } let db = Db::open(path).unwrap(); { let conn = rusqlite::Connection::open(path).unwrap(); diff --git a/crates/pdfs-fuse/src/lib.rs b/crates/pdfs-fuse/src/lib.rs index 3b5c757..7cdd86f 100644 --- a/crates/pdfs-fuse/src/lib.rs +++ b/crates/pdfs-fuse/src/lib.rs @@ -3650,14 +3650,17 @@ impl Core { hold: bool, ) -> Result { self.require_uid_writable(parent_uid)?; - let uid = mint_local_uid(); + // The row the tree would give it anyway, now: the node goes by its + // local id until it lands. + let lid = self + .db + .add_local_row(&parent_uid.to_string(), name, is_dir, now_secs()) + .map_err(|e| { + error!(%parent_uid, name, error = %e, "giving a local node its row failed"); + Errno::EIO + })?; + let uid = local_node_uid(lid); let node = local_node(uid.clone(), parent_uid.clone(), name.to_string(), is_dir); - // The row the tree would give it anyway, now, so the op is queued with - // the node's local id. - self.db.lids_for(&[&node]).map_err(|e| { - error!(%parent_uid, name, error = %e, "giving a local node its row failed"); - Errno::EIO - })?; let op = PendingOp { id: 0, kind: if is_dir { OP_MKDIR } else { OP_CREATE }.to_string(), @@ -5752,21 +5755,12 @@ fn parse_node_uid(s: &str) -> Option { parse_uid(s) } -/// Distinguishes placeholder uids minted by [`mint_local_uid`] within one run. -static LOCAL_UID_SEQ: AtomicU64 = AtomicU64::new(0); - -/// Invent a uid for a node created while offline, so it can be interned, listed -/// and written to before the server has ever heard of it (offline.md Phase 3b). -/// -/// Uniqueness only has to hold among this machine's undrained ops, so the clock -/// (which separates runs) plus a counter (which separates nodes within a run) is -/// enough without taking on a uuid dependency. -fn mint_local_uid() -> NodeUid { - let seq = LOCAL_UID_SEQ.fetch_add(1, Ordering::Relaxed); - NodeUid::new( - VolumeId::from(LOCAL_VOLUME), - LinkId::from(format!("{}-{seq}", now_millis())), - ) +/// The uid a node made on this machine goes by until it lands, so it can be +/// interned, listed and written to before the server has ever heard of it +/// (offline.md Phase 3b): its row's local id, as [`pdfs_core::db::local_uid`] +/// has it. +fn local_node_uid(lid: i64) -> NodeUid { + NodeUid::new(VolumeId::from(LOCAL_VOLUME), LinkId::from(lid.to_string())) } /// Whether this node exists only on this machine, so far. Such a uid is @@ -7022,24 +7016,24 @@ mod local_uid_tests { use super::*; #[test] - fn a_minted_uid_is_recognisable_and_round_trips() { - let uid = mint_local_uid(); + fn a_local_node_uid_is_recognisable_and_round_trips() { + let uid = local_node_uid(42); assert!(is_local_uid(&uid)); assert!(is_local_uid_str(&uid.to_string())); + assert_eq!(uid.to_string(), pdfs_core::db::local_uid(42)); // It has to survive the trip through `pending_op.uid` as text, like any - // other uid does. + // other uid does, and give back the row it stands for. let parsed = parse_node_uid(&uid.to_string()).expect("parses back"); assert_eq!(parsed, uid); + assert_eq!(pdfs_core::db::local_lid(&uid.to_string()), Some(42)); } #[test] - fn minted_uids_are_distinct_within_a_run() { + fn local_node_uids_are_distinct_per_row() { // Two files created in the same millisecond must not collide — the whole // queue is keyed by uid. - let a = mint_local_uid(); - let b = mint_local_uid(); - assert_ne!(a, b); + assert_ne!(local_node_uid(1), local_node_uid(2)); } #[test] @@ -7056,7 +7050,7 @@ mod local_uid_tests { #[test] fn a_placeholder_file_reports_itself_as_empty_and_unsealed() { let parent = NodeUid::new(VolumeId::from("vol1"), LinkId::from("dir")); - let node = local_node(mint_local_uid(), parent.clone(), "notes.txt".into(), false); + let node = local_node(local_node_uid(7), parent.clone(), "notes.txt".into(), false); assert_eq!(node.name, "notes.txt"); assert_eq!(node.parent_uid, Some(parent)); @@ -7080,7 +7074,7 @@ mod local_uid_tests { #[test] fn a_placeholder_folder_is_a_folder() { let parent = NodeUid::new(VolumeId::from("vol1"), LinkId::from("root")); - let node = local_node(mint_local_uid(), parent, "photos".into(), true); + let node = local_node(local_node_uid(7), parent, "photos".into(), true); assert!(node.is_folder()); } } @@ -8552,7 +8546,7 @@ mod tests { assert_before(truncate, "require_uid_writable", "create_scratch"); for (signature, first_side_effect) in [ - ("fn queue_local_node(", "mint_local_uid"), + ("fn queue_local_node(", "add_local_row"), ("fn queue_rename(", "queue_rename_authorized"), ("fn queue_trash(", "replace_ops_with_trash"), ] { diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 634cd78..66f3d58 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -11,10 +11,11 @@ scratch (user data in `staging/` and `recovery/` is never touched by this). ## [Unreleased] -Schema: **40**. The queue table is rebuilt so an id is never given out twice; queued changes +Schema: **41**. The queue table is rebuilt so an id is never given out twice; queued changes are kept. Queued changes get a column for the reason Drive refused them, and name their file and folder by local id as well as by Drive's id. Stored files and folders name their parent -folder by local id too. +folder by local id too. A file or folder made on this machine that Drive does not have yet goes +by its local id, in the queue, the stored tree and pins alike. ### Added - **Sync issues.** A change Drive refuses because the storage is full, access was taken away, diff --git a/docs/MILESTONE-3.0.0-STATUS.md b/docs/MILESTONE-3.0.0-STATUS.md index 0ab14b1..a7c8918 100644 --- a/docs/MILESTONE-3.0.0-STATUS.md +++ b/docs/MILESTONE-3.0.0-STATUS.md @@ -37,7 +37,14 @@ Tracks `docs/MILESTONE-3.0.0.md`. Update it with every commit that moves a phase - [ ] Queries over children and subtrees by `parent_lid`, and no rewrite of the children's `parent_uid` when a folder lands (R3). Both go with the placeholders: until then the strings are kept in step. - - [ ] No more placeholders: `nodes.uid` is `NULL` until a node lands. + - [ ] No more placeholders: `nodes.uid` is `NULL` until a node lands. In four parts: + - [x] 6a: a node made here goes by `local~`, a function of its row, instead of a + minted `local~-`; schema 41 rewrites the old placeholders (nodes, ops, pins). + - [ ] 6b: ops by lid; a landing create rewrites no other op. + - [ ] 6c: a local node's row has `uid NULL`; children and subtrees by `parent_lid`; pins + by lid. + - [ ] 6d: in memory, `pending`, `hidden` and `creating` by lid; `is_local_uid` checks + become `remote_uid().is_none()`. - [ ] An executor that runs ops in dependency order, many at once. 3. **Speed (§7)**: run independent ops in parallel, batch trashes and moves; measure 1,000 files and 100 folders on Wi-Fi against LAN (target: within 10 %).