mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-04 16:56:59 +01:00
refactor(core): rows and ops name a node's parent by local id; a landing rewrites nothing below it
This commit is contained in:
1 parent
85ec91d6b3
commit
9c93860cd8
7 files changed
+301
-166
No files matched your search
+134
-113
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use rusqlite::types::Value;
|
||||||
use rusqlite::{OptionalExtension, Transaction, params};
|
use rusqlite::{OptionalExtension, Transaction, params};
|
||||||
|
|
||||||
use super::Db;
|
use super::Db;
|
||||||
@@ -65,12 +66,13 @@ impl Db {
|
|||||||
/// snapshot while offline, including after a live event expired its TTL.
|
/// snapshot while offline, including after a live event expired its TTL.
|
||||||
pub fn visible_children(&self, parent: &NodeUid) -> Result<Vec<Node>> {
|
pub fn visible_children(&self, parent: &NodeUid) -> Result<Vec<Node>> {
|
||||||
let conn = self.read();
|
let conn = self.read();
|
||||||
let mut stmt = conn.prepare(
|
let (key, parent) = parent_key(&conn, &parent.to_string())?;
|
||||||
"SELECT node_json FROM nodes
|
let mut stmt = conn.prepare(&format!(
|
||||||
WHERE parent_uid = ?1 AND trashed = 0 AND node_json IS NOT NULL
|
"SELECT {NODE_NOW} FROM nodes n LEFT JOIN nodes p ON p.lid = n.parent_lid
|
||||||
ORDER BY name, uid",
|
WHERE n.{key} = ?1 AND n.trashed = 0 AND n.node_json IS NOT NULL
|
||||||
)?;
|
ORDER BY n.name, n.uid"
|
||||||
let rows = stmt.query_map([parent.to_string()], |row| row.get::<_, String>(0))?;
|
))?;
|
||||||
|
let rows = stmt.query_map([parent], |row| row.get::<_, String>(0))?;
|
||||||
let mut nodes = Vec::new();
|
let mut nodes = Vec::new();
|
||||||
for row in rows {
|
for row in rows {
|
||||||
nodes.push(serde_json::from_str(&row?)?);
|
nodes.push(serde_json::from_str(&row?)?);
|
||||||
@@ -307,12 +309,14 @@ impl Db {
|
|||||||
|
|
||||||
/// Check if a folder node has any non-trashed children in the database.
|
/// Check if a folder node has any non-trashed children in the database.
|
||||||
pub fn has_children(&self, parent_uid: &NodeUid) -> Result<bool> {
|
pub fn has_children(&self, parent_uid: &NodeUid) -> Result<bool> {
|
||||||
let uid_str = parent_uid.to_string();
|
|
||||||
let conn = self.read();
|
let conn = self.read();
|
||||||
|
let (key, parent) = parent_key(&conn, &parent_uid.to_string())?;
|
||||||
let count: i64 = conn.query_row(
|
let count: i64 = conn.query_row(
|
||||||
"SELECT COUNT(*) FROM nodes
|
&format!(
|
||||||
WHERE parent_uid = ?1 AND trashed = 0 AND node_json IS NOT NULL",
|
"SELECT COUNT(*) FROM nodes
|
||||||
params![uid_str],
|
WHERE {key} = ?1 AND trashed = 0 AND node_json IS NOT NULL"
|
||||||
|
),
|
||||||
|
params![parent],
|
||||||
|row| row.get(0),
|
|row| row.get(0),
|
||||||
)?;
|
)?;
|
||||||
Ok(count > 0)
|
Ok(count > 0)
|
||||||
@@ -349,11 +353,11 @@ impl Db {
|
|||||||
pub fn path_relative_to(&self, ancestor_uid: &str, uid: &str) -> Result<Option<String>> {
|
pub fn path_relative_to(&self, ancestor_uid: &str, uid: &str) -> Result<Option<String>> {
|
||||||
let conn = self.read();
|
let conn = self.read();
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
"WITH RECURSIVE anc(uid, parent_uid, name, depth) AS (
|
"WITH RECURSIVE anc(uid, parent_lid, name, depth) AS (
|
||||||
SELECT uid, parent_uid, name, 0 FROM nodes WHERE uid = ?1
|
SELECT uid, parent_lid, name, 0 FROM nodes WHERE uid = ?1
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT n.uid, n.parent_uid, n.name, anc.depth + 1
|
SELECT n.uid, n.parent_lid, n.name, anc.depth + 1
|
||||||
FROM nodes n JOIN anc ON n.uid = anc.parent_uid
|
FROM nodes n JOIN anc ON n.lid = anc.parent_lid
|
||||||
WHERE anc.depth < 1024
|
WHERE anc.depth < 1024
|
||||||
)
|
)
|
||||||
SELECT uid, name FROM anc ORDER BY depth",
|
SELECT uid, name FROM anc ORDER BY depth",
|
||||||
@@ -402,12 +406,13 @@ impl Db {
|
|||||||
let conn = self.read();
|
let conn = self.read();
|
||||||
let rows: Vec<HitRow> = if query.chars().count() < TRIGRAM_MIN {
|
let rows: Vec<HitRow> = if query.chars().count() < TRIGRAM_MIN {
|
||||||
let pat = format!("%{}%", like_escape(query));
|
let pat = format!("%{}%", like_escape(query));
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(&format!(
|
||||||
"SELECT node_json, uid, path FROM nodes
|
"SELECT {NODE_NOW}, n.uid, n.path
|
||||||
WHERE name LIKE ?1 ESCAPE '\\' AND trashed = 0 AND node_json IS NOT NULL
|
FROM nodes n LEFT JOIN nodes p ON p.lid = n.parent_lid
|
||||||
AND (?3 IS NULL OR path IS NULL OR path LIKE ?3 ESCAPE '\\')
|
WHERE n.name LIKE ?1 ESCAPE '\\' AND n.trashed = 0 AND n.node_json IS NOT NULL
|
||||||
ORDER BY name LIMIT ?2",
|
AND (?3 IS NULL OR n.path IS NULL OR n.path LIKE ?3 ESCAPE '\\')
|
||||||
)?;
|
ORDER BY n.name LIMIT ?2"
|
||||||
|
))?;
|
||||||
collect_hits(stmt.query_map(params![pat, limit as i64, scope_pat], hit_row)?)?
|
collect_hits(stmt.query_map(params![pat, limit as i64, scope_pat], hit_row)?)?
|
||||||
} else {
|
} else {
|
||||||
// Escape double quotes and quote each term, then combine with AND so
|
// Escape double quotes and quote each term, then combine with AND so
|
||||||
@@ -417,13 +422,14 @@ impl Db {
|
|||||||
.map(|word| format!("\"{}\"", word.replace('"', "\"\"")))
|
.map(|word| format!("\"{}\"", word.replace('"', "\"\"")))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(" AND ");
|
.join(" AND ");
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(&format!(
|
||||||
"SELECT n.node_json, n.uid, n.path
|
"SELECT {NODE_NOW}, n.uid, n.path
|
||||||
FROM nodes_fts f JOIN nodes n ON n.rowid = f.rowid
|
FROM nodes_fts f JOIN nodes n ON n.rowid = f.rowid
|
||||||
|
LEFT JOIN nodes p ON p.lid = n.parent_lid
|
||||||
WHERE f.name MATCH ?1 AND n.trashed = 0 AND n.node_json IS NOT NULL
|
WHERE f.name MATCH ?1 AND n.trashed = 0 AND n.node_json IS NOT NULL
|
||||||
AND (?3 IS NULL OR n.path IS NULL OR n.path LIKE ?3 ESCAPE '\\')
|
AND (?3 IS NULL OR n.path IS NULL OR n.path LIKE ?3 ESCAPE '\\')
|
||||||
ORDER BY f.rank LIMIT ?2",
|
ORDER BY f.rank LIMIT ?2"
|
||||||
)?;
|
))?;
|
||||||
collect_hits(stmt.query_map(params![phrase, limit as i64, scope_pat], hit_row)?)?
|
collect_hits(stmt.query_map(params![phrase, limit as i64, scope_pat], hit_row)?)?
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -460,20 +466,22 @@ impl Db {
|
|||||||
let lane_limit = limit.div_ceil(2);
|
let lane_limit = limit.div_ceil(2);
|
||||||
let mut rows: Vec<HitRow> = if terms.is_empty() {
|
let mut rows: Vec<HitRow> = if terms.is_empty() {
|
||||||
let pat = format!("%{}%", like_escape(query));
|
let pat = format!("%{}%", like_escape(query));
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(&format!(
|
||||||
"SELECT node_json, uid, path FROM nodes
|
"SELECT {NODE_NOW}, n.uid, n.path
|
||||||
WHERE name LIKE ?1 ESCAPE '\\' AND trashed = 0 AND node_json IS NOT NULL
|
FROM nodes n LEFT JOIN nodes p ON p.lid = n.parent_lid
|
||||||
ORDER BY name LIMIT ?2",
|
WHERE n.name LIKE ?1 ESCAPE '\\' AND n.trashed = 0 AND n.node_json IS NOT NULL
|
||||||
)?;
|
ORDER BY n.name LIMIT ?2"
|
||||||
|
))?;
|
||||||
collect_hits(stmt.query_map(params![pat, limit as i64], hit_row)?)?
|
collect_hits(stmt.query_map(params![pat, limit as i64], hit_row)?)?
|
||||||
} else {
|
} else {
|
||||||
let expression = terms.join(" OR ");
|
let expression = terms.join(" OR ");
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(&format!(
|
||||||
"SELECT n.node_json, n.uid, n.path
|
"SELECT {NODE_NOW}, n.uid, n.path
|
||||||
FROM nodes_fts f JOIN nodes n ON n.rowid = f.rowid
|
FROM nodes_fts f JOIN nodes n ON n.rowid = f.rowid
|
||||||
|
LEFT JOIN nodes p ON p.lid = n.parent_lid
|
||||||
WHERE nodes_fts MATCH ?1 AND n.trashed = 0 AND n.node_json IS NOT NULL
|
WHERE nodes_fts MATCH ?1 AND n.trashed = 0 AND n.node_json IS NOT NULL
|
||||||
ORDER BY f.rank LIMIT ?2",
|
ORDER BY f.rank LIMIT ?2"
|
||||||
)?;
|
))?;
|
||||||
collect_hits(stmt.query_map(params![expression, lane_limit as i64], hit_row)?)?
|
collect_hits(stmt.query_map(params![expression, lane_limit as i64], hit_row)?)?
|
||||||
};
|
};
|
||||||
// Trigram rank does not know that a name *starts* with the query, and a
|
// Trigram rank does not know that a name *starts* with the query, and a
|
||||||
@@ -483,12 +491,13 @@ impl Db {
|
|||||||
if !terms.is_empty()
|
if !terms.is_empty()
|
||||||
&& let Some(first) = query.chars().next()
|
&& let Some(first) = query.chars().next()
|
||||||
{
|
{
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(&format!(
|
||||||
"SELECT node_json, uid, path FROM nodes
|
"SELECT {NODE_NOW}, n.uid, n.path
|
||||||
WHERE name COLLATE NOCASE LIKE ?1 ESCAPE '\\'
|
FROM nodes n LEFT JOIN nodes p ON p.lid = n.parent_lid
|
||||||
AND trashed = 0 AND node_json IS NOT NULL
|
WHERE n.name COLLATE NOCASE LIKE ?1 ESCAPE '\\'
|
||||||
ORDER BY name COLLATE NOCASE LIMIT ?2",
|
AND n.trashed = 0 AND n.node_json IS NOT NULL
|
||||||
)?;
|
ORDER BY n.name COLLATE NOCASE LIMIT ?2"
|
||||||
|
))?;
|
||||||
let mut extra = Vec::new();
|
let mut extra = Vec::new();
|
||||||
for pattern in [
|
for pattern in [
|
||||||
format!("{}%", like_escape(query)),
|
format!("{}%", like_escape(query)),
|
||||||
@@ -538,24 +547,20 @@ impl Db {
|
|||||||
/// stored below it may still hold the placeholder.
|
/// stored below it may still hold the placeholder.
|
||||||
pub fn load_all(&self) -> Result<Vec<StoredNode>> {
|
pub fn load_all(&self) -> Result<Vec<StoredNode>> {
|
||||||
let conn = self.read();
|
let conn = self.read();
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(&format!(
|
||||||
"SELECT n.node_json, n.listed, n.lid, p.uid
|
"SELECT {NODE_NOW}, n.listed, n.lid
|
||||||
FROM nodes n LEFT JOIN nodes p ON p.lid = n.parent_lid
|
FROM nodes n LEFT JOIN nodes p ON p.lid = n.parent_lid
|
||||||
WHERE n.node_json IS NOT NULL",
|
WHERE n.node_json IS NOT NULL"
|
||||||
)?;
|
))?;
|
||||||
let rows = stmt.query_map([], |row| {
|
let rows = stmt.query_map([], |row| {
|
||||||
let json: String = row.get(0)?;
|
let json: String = row.get(0)?;
|
||||||
let listed: i64 = row.get(1)?;
|
let listed: i64 = row.get(1)?;
|
||||||
let parent: Option<String> = row.get(3)?;
|
Ok((json, listed != 0, row.get(2)?))
|
||||||
Ok((json, listed != 0, row.get(2)?, parent))
|
|
||||||
})?;
|
})?;
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for row in rows {
|
for row in rows {
|
||||||
let (json, listed, lid, parent) = row?;
|
let (json, listed, lid) = row?;
|
||||||
let mut node: Node = serde_json::from_str(&json)?;
|
let node: Node = serde_json::from_str(&json)?;
|
||||||
if let Some(parent) = parent.as_deref().and_then(parse_node_uid) {
|
|
||||||
node.parent_uid = Some(parent);
|
|
||||||
}
|
|
||||||
out.push(StoredNode { node, listed, lid });
|
out.push(StoredNode { node, listed, lid });
|
||||||
}
|
}
|
||||||
Ok(out)
|
Ok(out)
|
||||||
@@ -568,7 +573,10 @@ impl Db {
|
|||||||
let conn = self.read();
|
let conn = self.read();
|
||||||
let json: Option<String> = conn
|
let json: Option<String> = conn
|
||||||
.query_row(
|
.query_row(
|
||||||
"SELECT node_json FROM nodes WHERE uid = ?1 AND node_json IS NOT NULL",
|
&format!(
|
||||||
|
"SELECT {NODE_NOW} FROM nodes n LEFT JOIN nodes p ON p.lid = n.parent_lid
|
||||||
|
WHERE n.uid = ?1 AND n.node_json IS NOT NULL"
|
||||||
|
),
|
||||||
params![uid],
|
params![uid],
|
||||||
|r| r.get(0),
|
|r| r.get(0),
|
||||||
)
|
)
|
||||||
@@ -613,20 +621,26 @@ impl Db {
|
|||||||
let mut known = tx.prepare_cached("SELECT lid FROM nodes WHERE uid = ?1")?;
|
let mut known = tx.prepare_cached("SELECT lid FROM nodes WHERE uid = ?1")?;
|
||||||
let mut stub = tx.prepare_cached(
|
let mut stub = tx.prepare_cached(
|
||||||
"INSERT INTO nodes (uid, parent_uid, name, is_dir, mtime, trashed, parent_lid)
|
"INSERT INTO nodes (uid, parent_uid, name, is_dir, mtime, trashed, parent_lid)
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, (SELECT lid FROM nodes WHERE uid = ?2))",
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||||
)?;
|
)?;
|
||||||
for node in nodes {
|
for node in nodes {
|
||||||
let uid = node.uid.to_string();
|
let uid = node.uid.to_string();
|
||||||
let lid = match known.query_row(params![uid], |r| r.get(0)).optional()? {
|
let lid = match known.query_row(params![uid], |r| r.get(0)).optional()? {
|
||||||
Some(lid) => lid,
|
Some(lid) => lid,
|
||||||
None => {
|
None => {
|
||||||
|
let parent = node.parent_uid.as_ref().map(ToString::to_string);
|
||||||
|
let parent_lid = match &parent {
|
||||||
|
Some(parent) => node_lid(&tx, parent)?,
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
stub.execute(params![
|
stub.execute(params![
|
||||||
uid,
|
uid,
|
||||||
node.parent_uid.as_ref().map(ToString::to_string),
|
parent,
|
||||||
node.name,
|
node.name,
|
||||||
node.is_folder() as i64,
|
node.is_folder() as i64,
|
||||||
node.modification_time,
|
node.modification_time,
|
||||||
node.trashed as i64,
|
node.trashed as i64,
|
||||||
|
parent_lid,
|
||||||
])?;
|
])?;
|
||||||
let lid = tx.last_insert_rowid();
|
let lid = tx.last_insert_rowid();
|
||||||
adopt_children_tx(&tx, &uid, lid)?;
|
adopt_children_tx(&tx, &uid, lid)?;
|
||||||
@@ -699,11 +713,12 @@ impl Db {
|
|||||||
/// had: a listing dropped as stale may miss or keep a child.
|
/// had: a listing dropped as stale may miss or keep a child.
|
||||||
pub fn known_children(&self, parent: &NodeUid) -> Result<Vec<Node>> {
|
pub fn known_children(&self, parent: &NodeUid) -> Result<Vec<Node>> {
|
||||||
let conn = self.read();
|
let conn = self.read();
|
||||||
let mut stmt = conn.prepare(
|
let (key, parent) = parent_key(&conn, &parent.to_string())?;
|
||||||
"SELECT node_json FROM nodes
|
let mut stmt = conn.prepare(&format!(
|
||||||
WHERE parent_uid = ?1 AND node_json IS NOT NULL AND trashed = 0",
|
"SELECT {NODE_NOW} FROM nodes n LEFT JOIN nodes p ON p.lid = n.parent_lid
|
||||||
)?;
|
WHERE n.{key} = ?1 AND n.node_json IS NOT NULL AND n.trashed = 0"
|
||||||
let rows = stmt.query_map(params![parent.to_string()], |r| r.get::<_, String>(0))?;
|
))?;
|
||||||
|
let rows = stmt.query_map(params![parent], |r| r.get::<_, String>(0))?;
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for json in rows {
|
for json in rows {
|
||||||
out.push(serde_json::from_str(&json?)?);
|
out.push(serde_json::from_str(&json?)?);
|
||||||
@@ -721,13 +736,13 @@ impl Db {
|
|||||||
/// until the create lands (`docs/BUGS.md` B132).
|
/// until the create lands (`docs/BUGS.md` B132).
|
||||||
pub fn queued_children(&self, parent: &NodeUid) -> Result<(Vec<Node>, HashSet<String>)> {
|
pub fn queued_children(&self, parent: &NodeUid) -> Result<(Vec<Node>, HashSet<String>)> {
|
||||||
let conn = self.read();
|
let conn = self.read();
|
||||||
let parent = parent.to_string();
|
let (key, parent) = parent_key(&conn, &parent.to_string())?;
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(&format!(
|
||||||
"SELECT n.node_json FROM nodes n
|
"SELECT {NODE_NOW} FROM nodes n LEFT JOIN nodes p ON p.lid = n.parent_lid
|
||||||
WHERE n.parent_uid = ?1 AND n.node_json IS NOT NULL AND n.trashed = 0
|
WHERE n.{key} = ?1 AND n.node_json IS NOT NULL AND n.trashed = 0
|
||||||
AND EXISTS (SELECT 1 FROM pending_op p
|
AND EXISTS (SELECT 1 FROM pending_op o
|
||||||
WHERE p.uid = n.uid AND p.kind IN (?2, ?3, ?4))",
|
WHERE (o.lid = n.lid OR o.uid = n.uid) AND o.kind IN (?2, ?3, ?4))"
|
||||||
)?;
|
))?;
|
||||||
let rows = stmt.query_map(params![parent, OP_CREATE, OP_MKDIR, OP_RENAME], |r| {
|
let rows = stmt.query_map(params![parent, OP_CREATE, OP_MKDIR, OP_RENAME], |r| {
|
||||||
r.get::<_, String>(0)
|
r.get::<_, String>(0)
|
||||||
})?;
|
})?;
|
||||||
@@ -735,10 +750,10 @@ impl Db {
|
|||||||
for json in rows {
|
for json in rows {
|
||||||
here.push(serde_json::from_str(&json?)?);
|
here.push(serde_json::from_str(&json?)?);
|
||||||
}
|
}
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(&format!(
|
||||||
"SELECT DISTINCT p.uid FROM pending_op p JOIN nodes n ON n.uid = p.uid
|
"SELECT DISTINCT n.uid FROM pending_op p JOIN nodes n ON n.lid = p.lid OR n.uid = p.uid
|
||||||
WHERE p.kind = ?2 AND n.parent_uid IS NOT ?1",
|
WHERE p.kind = ?2 AND n.{key} IS NOT ?1"
|
||||||
)?;
|
))?;
|
||||||
let gone = stmt
|
let gone = stmt
|
||||||
.query_map(params![parent, OP_RENAME], |r| r.get::<_, String>(0))?
|
.query_map(params![parent, OP_RENAME], |r| r.get::<_, String>(0))?
|
||||||
.collect::<std::result::Result<HashSet<_>, _>>()?;
|
.collect::<std::result::Result<HashSet<_>, _>>()?;
|
||||||
@@ -777,8 +792,29 @@ pub(super) fn node_lid(conn: &rusqlite::Connection, uid: &str) -> Result<Option<
|
|||||||
Ok(row.optional()?)
|
Ok(row.optional()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hand a drained placeholder's row, and the rows below it, to the real uid it
|
/// The column and value a row names `parent` by: its local id when it has a
|
||||||
/// landed as.
|
/// row, which the children keep when it lands, or its uid when it has none,
|
||||||
|
/// as a device folder's root does.
|
||||||
|
fn parent_key(conn: &rusqlite::Connection, parent: &str) -> Result<(&'static str, Value)> {
|
||||||
|
Ok(match node_lid(conn, parent)? {
|
||||||
|
Some(lid) => ("parent_lid", Value::Integer(lid)),
|
||||||
|
None => ("parent_uid", Value::Text(parent.to_owned())),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `n.node_json` naming its parent as the parent's row `p`, joined by
|
||||||
|
/// `n.parent_lid`, has it now. A folder that lands takes its real uid in its
|
||||||
|
/// own row only, so a node stored below it still names the stand-in.
|
||||||
|
const NODE_NOW: &str = "CASE WHEN p.uid IS NULL OR p.uid = n.parent_uid THEN n.node_json
|
||||||
|
ELSE json_set(n.node_json, '$.parent_uid', json_object(
|
||||||
|
'volume_id', substr(p.uid, 1, instr(p.uid, '~') - 1),
|
||||||
|
'link_id', substr(p.uid, instr(p.uid, '~') + 1))) END";
|
||||||
|
|
||||||
|
/// Hand a drained placeholder's row to the real uid it landed as.
|
||||||
|
///
|
||||||
|
/// Nothing else is rewritten. The rows below it and the ops made inside it
|
||||||
|
/// name it by its local id, which stays; what reads them for a uid takes the
|
||||||
|
/// one the row has now ([`NODE_NOW`], `PARENT_NOW` in `ops.rs`).
|
||||||
///
|
///
|
||||||
/// Dropping the placeholder row and waiting for the server's copy of the real
|
/// Dropping the placeholder row and waiting for the server's copy of the real
|
||||||
/// node left a window with no row for either uid. A queued child drained in
|
/// node left a window with no row for either uid. A queued child drained in
|
||||||
@@ -800,19 +836,6 @@ pub(super) fn adopt_placeholder_row_tx(
|
|||||||
params![local, real],
|
params![local, real],
|
||||||
)?;
|
)?;
|
||||||
tx.execute("DELETE FROM pins WHERE uid = ?1", params![local])?;
|
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).
|
|
||||||
let real_json = parse_node_uid(real)
|
|
||||||
.map(|uid| serde_json::to_string(&uid))
|
|
||||||
.transpose()?;
|
|
||||||
tx.execute(
|
|
||||||
"UPDATE nodes SET parent_uid = ?2,
|
|
||||||
node_json = CASE WHEN ?3 IS NULL OR node_json IS NULL THEN node_json
|
|
||||||
ELSE json_set(node_json, '$.parent_uid', json(?3)) END
|
|
||||||
WHERE parent_uid = ?1",
|
|
||||||
params![local, real, real_json],
|
|
||||||
)?;
|
|
||||||
let row: Option<(i64, Option<String>)> = tx
|
let row: Option<(i64, Option<String>)> = tx
|
||||||
.query_row(
|
.query_row(
|
||||||
"SELECT lid, node_json FROM nodes WHERE uid = ?1",
|
"SELECT lid, node_json FROM nodes WHERE uid = ?1",
|
||||||
@@ -829,11 +852,6 @@ pub(super) fn adopt_placeholder_row_tx(
|
|||||||
let Some((lid, json)) = row else {
|
let Some((lid, json)) = row else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
// The ops made inside the folder name it by its local id, which stays.
|
|
||||||
tx.execute(
|
|
||||||
"UPDATE pending_op SET parent_uid = ?2 WHERE parent_lid = ?1",
|
|
||||||
params![lid, real],
|
|
||||||
)?;
|
|
||||||
let known: Option<(i64, Option<String>)> = tx
|
let known: Option<(i64, Option<String>)> = tx
|
||||||
.query_row(
|
.query_row(
|
||||||
"SELECT lid, node_json FROM nodes WHERE uid = ?1",
|
"SELECT lid, node_json FROM nodes WHERE uid = ?1",
|
||||||
@@ -908,13 +926,16 @@ fn upsert_node_tx(tx: &Transaction<'_>, node: &Node) -> Result<()> {
|
|||||||
let (parent_lid, path) = match &parent_uid {
|
let (parent_lid, path) = match &parent_uid {
|
||||||
None => (None, String::new()),
|
None => (None, String::new()),
|
||||||
Some(parent) => {
|
Some(parent) => {
|
||||||
let parent_row: Option<(i64, Option<String>)> = tx
|
let parent_row: Option<(i64, Option<String>)> = match node_lid(tx, parent)? {
|
||||||
.query_row(
|
Some(lid) => tx
|
||||||
"SELECT lid, path FROM nodes WHERE uid = ?1",
|
.query_row(
|
||||||
params![parent],
|
"SELECT lid, path FROM nodes WHERE lid = ?1",
|
||||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
params![lid],
|
||||||
)
|
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||||
.optional()?;
|
)
|
||||||
|
.optional()?,
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
let (lid, parent_path) = parent_row.unzip();
|
let (lid, parent_path) = parent_row.unzip();
|
||||||
(
|
(
|
||||||
lid,
|
lid,
|
||||||
@@ -980,7 +1001,7 @@ fn upsert_node_tx(tx: &Transaction<'_>, node: &Node) -> Result<()> {
|
|||||||
|| prior.trashed != node.trashed
|
|| prior.trashed != node.trashed
|
||||||
});
|
});
|
||||||
if node.is_folder() && subtree_moved {
|
if node.is_folder() && subtree_moved {
|
||||||
reindex_subtree_tx(tx, &uid, &path, indexable)?;
|
reindex_subtree_tx(tx, rowid, &path, indexable)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1001,22 +1022,22 @@ fn upsert_node_tx(tx: &Transaction<'_>, node: &Node) -> Result<()> {
|
|||||||
/// this deep.
|
/// this deep.
|
||||||
fn reindex_subtree_tx(
|
fn reindex_subtree_tx(
|
||||||
tx: &Transaction<'_>,
|
tx: &Transaction<'_>,
|
||||||
folder: &str,
|
folder: i64,
|
||||||
folder_path: &str,
|
folder_path: &str,
|
||||||
folder_indexable: bool,
|
folder_indexable: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let descendants: Vec<(i64, String, bool)> = {
|
let descendants: Vec<(i64, String, bool)> = {
|
||||||
let mut stmt = tx.prepare(
|
let mut stmt = tx.prepare(
|
||||||
"WITH RECURSIVE sub(rowid, uid, name, path, blocked, depth) AS (
|
"WITH RECURSIVE sub(rowid, name, path, blocked, depth) AS (
|
||||||
SELECT n.rowid, n.uid, n.name,
|
SELECT n.rowid, n.name,
|
||||||
CASE WHEN ?2 = '' THEN n.name ELSE ?2 || '/' || n.name END,
|
CASE WHEN ?2 = '' THEN n.name ELSE ?2 || '/' || n.name END,
|
||||||
n.trashed, 0
|
n.trashed, 0
|
||||||
FROM nodes n WHERE n.parent_uid = ?1
|
FROM nodes n WHERE n.parent_lid = ?1
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT n.rowid, n.uid, n.name,
|
SELECT n.rowid, n.name,
|
||||||
sub.path || '/' || n.name,
|
sub.path || '/' || n.name,
|
||||||
MAX(sub.blocked, n.trashed), sub.depth + 1
|
MAX(sub.blocked, n.trashed), sub.depth + 1
|
||||||
FROM nodes n JOIN sub ON n.parent_uid = sub.uid
|
FROM nodes n JOIN sub ON n.parent_lid = sub.rowid
|
||||||
WHERE sub.depth < 256
|
WHERE sub.depth < 256
|
||||||
)
|
)
|
||||||
SELECT rowid, path, blocked FROM sub",
|
SELECT rowid, path, blocked FROM sub",
|
||||||
@@ -1057,7 +1078,8 @@ fn reindex_subtree_tx(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn direct_child_uids_tx(tx: &Transaction<'_>, parent: &str) -> Result<Vec<String>> {
|
fn direct_child_uids_tx(tx: &Transaction<'_>, parent: &str) -> Result<Vec<String>> {
|
||||||
let mut stmt = tx.prepare("SELECT uid FROM nodes WHERE parent_uid = ?1")?;
|
let (key, parent) = parent_key(tx, parent)?;
|
||||||
|
let mut stmt = tx.prepare(&format!("SELECT uid FROM nodes WHERE {key} = ?1"))?;
|
||||||
let rows = stmt.query_map([parent], |row| row.get::<_, String>(0))?;
|
let rows = stmt.query_map([parent], |row| row.get::<_, String>(0))?;
|
||||||
let mut uids = Vec::new();
|
let mut uids = Vec::new();
|
||||||
for row in rows {
|
for row in rows {
|
||||||
@@ -1082,14 +1104,14 @@ fn direct_child_uids_tx(tx: &Transaction<'_>, parent: &str) -> Result<Vec<String
|
|||||||
/// ends at one whose parent is a row we have already visited.
|
/// ends at one whose parent is a row we have already visited.
|
||||||
fn node_is_indexable_tx(tx: &Transaction<'_>, uid: &str) -> Result<bool> {
|
fn node_is_indexable_tx(tx: &Transaction<'_>, uid: &str) -> Result<bool> {
|
||||||
let indexable: i64 = tx.query_row(
|
let indexable: i64 = tx.query_row(
|
||||||
"WITH RECURSIVE ancestors(uid, parent_uid, trashed, depth, path) AS (
|
"WITH RECURSIVE ancestors(lid, parent_lid, trashed, depth, path) AS (
|
||||||
SELECT uid, parent_uid, trashed, 0, char(31) || uid || char(31)
|
SELECT lid, parent_lid, trashed, 0, ',' || lid || ','
|
||||||
FROM nodes WHERE uid = ?1
|
FROM nodes WHERE uid = ?1
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT n.uid, n.parent_uid, n.trashed, a.depth + 1,
|
SELECT n.lid, n.parent_lid, n.trashed, a.depth + 1,
|
||||||
a.path || n.uid || char(31)
|
a.path || n.lid || ','
|
||||||
FROM ancestors a JOIN nodes n ON n.uid = a.parent_uid
|
FROM ancestors a JOIN nodes n ON n.lid = a.parent_lid
|
||||||
WHERE instr(a.path, char(31) || n.uid || char(31)) = 0
|
WHERE instr(a.path, ',' || n.lid || ',') = 0
|
||||||
)
|
)
|
||||||
SELECT CASE
|
SELECT CASE
|
||||||
WHEN COUNT(*) = 0 THEN 0
|
WHEN COUNT(*) = 0 THEN 0
|
||||||
@@ -1097,8 +1119,7 @@ fn node_is_indexable_tx(tx: &Transaction<'_>, uid: &str) -> Result<bool> {
|
|||||||
WHEN EXISTS (
|
WHEN EXISTS (
|
||||||
SELECT 1 FROM ancestors a
|
SELECT 1 FROM ancestors a
|
||||||
WHERE a.depth = (SELECT MAX(depth) FROM ancestors)
|
WHERE a.depth = (SELECT MAX(depth) FROM ancestors)
|
||||||
AND a.parent_uid IS NOT NULL
|
AND EXISTS (SELECT 1 FROM nodes p WHERE p.lid = a.parent_lid)
|
||||||
AND EXISTS (SELECT 1 FROM nodes p WHERE p.uid = a.parent_uid)
|
|
||||||
) THEN 0
|
) THEN 0
|
||||||
ELSE 1
|
ELSE 1
|
||||||
END
|
END
|
||||||
@@ -1116,7 +1137,7 @@ fn tombstone_subtree_tx(tx: &Transaction<'_>, root_uid: &str) -> Result<()> {
|
|||||||
SELECT rowid, uid, node_json FROM nodes WHERE uid = ?1
|
SELECT rowid, uid, node_json FROM nodes WHERE uid = ?1
|
||||||
UNION
|
UNION
|
||||||
SELECT n.rowid, n.uid, n.node_json FROM nodes n
|
SELECT n.rowid, n.uid, n.node_json FROM nodes n
|
||||||
JOIN subtree s ON n.parent_uid = s.uid
|
JOIN subtree s ON n.parent_lid = s.rowid
|
||||||
)
|
)
|
||||||
SELECT rowid, uid, node_json FROM subtree",
|
SELECT rowid, uid, node_json FROM subtree",
|
||||||
)?;
|
)?;
|
||||||
|
|||||||
@@ -110,25 +110,29 @@ pub fn op_supersedes(kind: &str) -> bool {
|
|||||||
fn drop_doomed_ops(tx: &rusqlite::Transaction<'_>, uid: &str) -> Result<Vec<String>> {
|
fn drop_doomed_ops(tx: &rusqlite::Transaction<'_>, uid: &str) -> Result<Vec<String>> {
|
||||||
const DOOMED: &str = "
|
const DOOMED: &str = "
|
||||||
WITH RECURSIVE
|
WITH RECURSIVE
|
||||||
below(uid) AS (
|
below(uid, lid) AS (
|
||||||
SELECT ?1
|
SELECT ?1, ?2
|
||||||
UNION
|
UNION
|
||||||
SELECT n.uid FROM nodes n JOIN below b ON n.parent_uid = b.uid
|
SELECT n.uid, n.lid FROM nodes n JOIN below b
|
||||||
|
ON n.parent_lid = b.lid OR (n.parent_lid IS NULL AND n.parent_uid = b.uid)
|
||||||
),
|
),
|
||||||
queued(uid) AS (
|
queued(uid, lid) AS (
|
||||||
SELECT ?1
|
SELECT ?1, ?2
|
||||||
UNION
|
UNION
|
||||||
SELECT p.uid FROM pending_op p
|
SELECT p.uid, p.lid FROM pending_op p
|
||||||
WHERE p.kind IN ('create', 'mkdir')
|
WHERE p.kind IN ('create', 'mkdir')
|
||||||
AND p.parent_uid IN (SELECT uid FROM below)
|
AND (p.parent_lid IN (SELECT lid FROM below)
|
||||||
|
OR p.parent_uid IN (SELECT uid FROM below))
|
||||||
UNION
|
UNION
|
||||||
SELECT p.uid FROM pending_op p JOIN queued q ON p.parent_uid = q.uid
|
SELECT p.uid, p.lid FROM pending_op p JOIN queued q
|
||||||
|
ON p.parent_lid = q.lid OR p.parent_uid = q.uid
|
||||||
),
|
),
|
||||||
doomed(id) AS (
|
doomed(id) AS (
|
||||||
SELECT id FROM pending_op
|
SELECT id FROM pending_op
|
||||||
WHERE uid IN (SELECT uid FROM queued)
|
WHERE uid IN (SELECT uid FROM queued)
|
||||||
OR lid = ?2
|
OR lid IN (SELECT lid FROM queued)
|
||||||
OR (kind = 'revision' AND uid IN (SELECT uid FROM below))
|
OR (kind = 'revision'
|
||||||
|
AND (uid IN (SELECT uid FROM below) OR lid IN (SELECT lid FROM below)))
|
||||||
)";
|
)";
|
||||||
const SENT: &str = "kind IN ('create', 'mkdir') AND (claimed_at <> 0 OR attempts > 0)";
|
const SENT: &str = "kind IN ('create', 'mkdir') AND (claimed_at <> 0 OR attempts > 0)";
|
||||||
let lid = node_lid(tx, uid)?;
|
let lid = node_lid(tx, uid)?;
|
||||||
@@ -147,8 +151,10 @@ fn drop_doomed_ops(tx: &rusqlite::Transaction<'_>, uid: &str) -> Result<Vec<Stri
|
|||||||
"{DOOMED}
|
"{DOOMED}
|
||||||
UPDATE pending_op
|
UPDATE pending_op
|
||||||
SET kind = 'trash', issue = NULL,
|
SET kind = 'trash', issue = NULL,
|
||||||
parent_uid = CASE WHEN parent_uid LIKE '{LOCAL_VOLUME}~%' THEN NULL
|
parent_uid = CASE WHEN {PARENT_NOW} LIKE '{LOCAL_VOLUME}~%' THEN NULL
|
||||||
ELSE parent_uid END
|
ELSE {PARENT_NOW} END,
|
||||||
|
parent_lid = CASE WHEN {PARENT_NOW} LIKE '{LOCAL_VOLUME}~%' THEN NULL
|
||||||
|
ELSE parent_lid END
|
||||||
WHERE id IN (SELECT id FROM doomed) AND {SENT}"
|
WHERE id IN (SELECT id FROM doomed) AND {SENT}"
|
||||||
),
|
),
|
||||||
params![uid, lid],
|
params![uid, lid],
|
||||||
@@ -422,7 +428,7 @@ impl Db {
|
|||||||
&format!(
|
&format!(
|
||||||
"SELECT EXISTS(
|
"SELECT EXISTS(
|
||||||
SELECT 1 FROM pending_op
|
SELECT 1 FROM pending_op
|
||||||
WHERE kind = ?1 AND parent_uid = ?2 AND name = ?3
|
WHERE kind = ?1 AND {PARENT_NOW} = ?2 AND name = ?3
|
||||||
AND uid LIKE '{LOCAL_VOLUME}~%'
|
AND uid LIKE '{LOCAL_VOLUME}~%'
|
||||||
)"
|
)"
|
||||||
),
|
),
|
||||||
@@ -651,8 +657,10 @@ impl Db {
|
|||||||
let tx = conn.transaction()?;
|
let tx = conn.transaction()?;
|
||||||
let row: Option<CreateRow> = tx
|
let row: Option<CreateRow> = tx
|
||||||
.query_row(
|
.query_row(
|
||||||
"SELECT blob_path, meta_json, parent_uid, name, created_at
|
&format!(
|
||||||
FROM pending_op WHERE id = ?1 AND kind <> ?2",
|
"SELECT blob_path, meta_json, {PARENT_NOW}, name, created_at
|
||||||
|
FROM pending_op WHERE id = ?1 AND kind <> ?2"
|
||||||
|
),
|
||||||
params![id, OP_TRASH],
|
params![id, OP_TRASH],
|
||||||
|r| {
|
|r| {
|
||||||
Ok(CreateRow {
|
Ok(CreateRow {
|
||||||
@@ -1112,13 +1120,13 @@ impl Db {
|
|||||||
/// the one query that does.
|
/// the one query that does.
|
||||||
pub fn parked_create_ops(&self) -> Result<Vec<PendingOp>> {
|
pub fn parked_create_ops(&self) -> Result<Vec<PendingOp>> {
|
||||||
let conn = self.read();
|
let conn = self.read();
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(&format!(
|
||||||
"SELECT id, kind, uid, parent_uid, name, blob_path, meta_json, created_at,
|
"SELECT id, kind, {UID_NOW}, {PARENT_NOW}, name, blob_path, meta_json, created_at,
|
||||||
attempts, last_error, next_attempt_at
|
attempts, last_error, next_attempt_at
|
||||||
FROM pending_op
|
FROM pending_op
|
||||||
WHERE next_attempt_at >= ?1 AND kind IN (?2, ?3)
|
WHERE next_attempt_at >= ?1 AND kind IN (?2, ?3)
|
||||||
ORDER BY created_at",
|
ORDER BY created_at"
|
||||||
)?;
|
))?;
|
||||||
let rows = stmt
|
let rows = stmt
|
||||||
.query_map(params![PARK_UNTIL, OP_CREATE, OP_MKDIR], |r| {
|
.query_map(params![PARK_UNTIL, OP_CREATE, OP_MKDIR], |r| {
|
||||||
Ok(PendingOp {
|
Ok(PendingOp {
|
||||||
@@ -1300,9 +1308,11 @@ impl Db {
|
|||||||
) -> Result<bool> {
|
) -> Result<bool> {
|
||||||
let conn = self.conn.lock();
|
let conn = self.conn.lock();
|
||||||
let n = conn.execute(
|
let n = conn.execute(
|
||||||
"UPDATE pending_op SET next_attempt_at = 0
|
&format!(
|
||||||
WHERE id = ?1 AND kind IN (?4, ?5) AND next_attempt_at < ?6
|
"UPDATE pending_op SET next_attempt_at = 0
|
||||||
AND (parent_uid IS NOT ?2 OR name IS NOT ?3)",
|
WHERE id = ?1 AND kind IN (?4, ?5) AND next_attempt_at < ?6
|
||||||
|
AND ({PARENT_NOW} IS NOT ?2 OR name IS NOT ?3)"
|
||||||
|
),
|
||||||
params![id, parent_uid, name, OP_CREATE, OP_MKDIR, PARK_UNTIL],
|
params![id, parent_uid, name, OP_CREATE, OP_MKDIR, PARK_UNTIL],
|
||||||
)?;
|
)?;
|
||||||
Ok(n > 0)
|
Ok(n > 0)
|
||||||
|
|||||||
@@ -64,11 +64,11 @@ impl Db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Whether `uid` is pinned — directly, or because a strict ancestor folder
|
/// Whether `uid` is pinned — directly, or because a strict ancestor folder
|
||||||
/// is pinned recursively. The ancestor check walks `parent_uid` to the root
|
/// is pinned recursively. The ancestor check walks `parent_lid` to the root
|
||||||
/// via a CTE; a direct pin is honoured even when the node has no `nodes` row
|
/// via a CTE; a direct pin is honoured even when the node has no `nodes` row
|
||||||
/// yet (e.g. a CLI that never hydrates the node cache).
|
/// yet (e.g. a CLI that never hydrates the node cache).
|
||||||
///
|
///
|
||||||
/// The walk is depth-capped like [`super::utils::path_of`]: a `parent_uid`
|
/// The walk is depth-capped like [`super::utils::path_of`]: a parent
|
||||||
/// cycle is corrupt data the API can hand us, and `UNION ALL` over one never
|
/// cycle is corrupt data the API can hand us, and `UNION ALL` over one never
|
||||||
/// terminates — while holding the daemon's only SQLite connection, on a path
|
/// terminates — while holding the daemon's only SQLite connection, on a path
|
||||||
/// that runs per cached read.
|
/// that runs per cached read.
|
||||||
@@ -85,11 +85,11 @@ impl Db {
|
|||||||
let anc: Option<i64> = conn
|
let anc: Option<i64> = conn
|
||||||
.query_row(
|
.query_row(
|
||||||
&format!(
|
&format!(
|
||||||
"WITH RECURSIVE anc(uid, parent_uid, depth) AS (
|
"WITH RECURSIVE anc(uid, parent_lid, depth) AS (
|
||||||
SELECT uid, parent_uid, 0 FROM nodes WHERE uid = ?1
|
SELECT uid, parent_lid, 0 FROM nodes WHERE uid = ?1
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT n.uid, n.parent_uid, anc.depth + 1
|
SELECT n.uid, n.parent_lid, anc.depth + 1
|
||||||
FROM nodes n JOIN anc ON n.uid = anc.parent_uid
|
FROM nodes n JOIN anc ON n.lid = anc.parent_lid
|
||||||
WHERE anc.depth < {MAX_PATH_DEPTH}
|
WHERE anc.depth < {MAX_PATH_DEPTH}
|
||||||
)
|
)
|
||||||
SELECT 1 FROM anc a JOIN pins p ON p.uid = a.uid
|
SELECT 1 FROM anc a JOIN pins p ON p.uid = a.uid
|
||||||
@@ -109,10 +109,11 @@ impl Db {
|
|||||||
pub fn pinned_uids(&self) -> Result<Vec<String>> {
|
pub fn pinned_uids(&self) -> Result<Vec<String>> {
|
||||||
let conn = self.read();
|
let conn = self.read();
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
"WITH RECURSIVE sub(uid) AS (
|
"WITH RECURSIVE sub(uid, lid) AS (
|
||||||
SELECT uid FROM pins WHERE recursive = 1
|
SELECT p.uid, n.lid FROM pins p LEFT JOIN nodes n ON n.uid = p.uid
|
||||||
|
WHERE p.recursive = 1
|
||||||
UNION
|
UNION
|
||||||
SELECT n.uid FROM nodes n JOIN sub ON n.parent_uid = sub.uid
|
SELECT n.uid, n.lid FROM nodes n JOIN sub ON n.parent_lid = sub.lid
|
||||||
)
|
)
|
||||||
SELECT uid FROM sub
|
SELECT uid FROM sub
|
||||||
UNION
|
UNION
|
||||||
@@ -126,15 +127,16 @@ impl Db {
|
|||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Every descendant uid of `folder` (all depths), via a `parent_uid` CTE.
|
/// Every descendant uid of `folder` (all depths), via a `parent_lid` CTE.
|
||||||
/// Used to evict a recursively-pinned subtree's cached blobs on unpin.
|
/// Used to evict a recursively-pinned subtree's cached blobs on unpin.
|
||||||
pub fn descendants(&self, folder: &str) -> Result<Vec<String>> {
|
pub fn descendants(&self, folder: &str) -> Result<Vec<String>> {
|
||||||
let conn = self.read();
|
let conn = self.read();
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
"WITH RECURSIVE sub(uid) AS (
|
"WITH RECURSIVE sub(uid, lid) AS (
|
||||||
SELECT uid FROM nodes WHERE parent_uid = ?1
|
SELECT c.uid, c.lid FROM nodes c JOIN nodes f ON c.parent_lid = f.lid
|
||||||
|
WHERE f.uid = ?1
|
||||||
UNION
|
UNION
|
||||||
SELECT n.uid FROM nodes n JOIN sub ON n.parent_uid = sub.uid
|
SELECT n.uid, n.lid FROM nodes n JOIN sub ON n.parent_lid = sub.lid
|
||||||
)
|
)
|
||||||
SELECT uid FROM sub",
|
SELECT uid FROM sub",
|
||||||
)?;
|
)?;
|
||||||
|
|||||||
@@ -107,18 +107,19 @@ impl Db {
|
|||||||
}
|
}
|
||||||
let value = conn
|
let value = conn
|
||||||
.query_row(
|
.query_row(
|
||||||
"WITH RECURSIVE ancestors(uid, parent_uid, depth, path) AS (
|
"WITH RECURSIVE ancestors(uid, parent_uid, parent_lid, depth, path) AS (
|
||||||
SELECT uid, parent_uid, 0, char(31) || uid || char(31)
|
SELECT uid, parent_uid, parent_lid, 0, char(31) || uid || char(31)
|
||||||
FROM nodes WHERE uid = ?1
|
FROM nodes WHERE uid = ?1
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT a.parent_uid, n.parent_uid, a.depth + 1,
|
SELECT COALESCE(n.uid, a.parent_uid), n.parent_uid, n.parent_lid,
|
||||||
a.path || a.parent_uid || char(31)
|
a.depth + 1,
|
||||||
|
a.path || COALESCE(n.uid, a.parent_uid) || char(31)
|
||||||
FROM ancestors a
|
FROM ancestors a
|
||||||
LEFT JOIN nodes n ON n.uid = a.parent_uid
|
LEFT JOIN nodes n ON n.lid = a.parent_lid
|
||||||
WHERE a.parent_uid IS NOT NULL
|
WHERE a.parent_uid IS NOT NULL
|
||||||
AND instr(
|
AND instr(
|
||||||
a.path,
|
a.path,
|
||||||
char(31) || a.parent_uid || char(31)
|
char(31) || COALESCE(n.uid, a.parent_uid) || char(31)
|
||||||
) = 0
|
) = 0
|
||||||
)
|
)
|
||||||
SELECT sa.access
|
SELECT sa.access
|
||||||
|
|||||||
@@ -3900,6 +3900,104 @@ fn a_landed_folder_hands_its_children_over_in_the_stored_node_too() {
|
|||||||
assert_eq!(names, ["a.txt"]);
|
assert_eq!(names, ["a.txt"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A folder made here that lands takes its real uid in its own row only. What
|
||||||
|
/// was made inside it names it by local id, and is found under the real uid.
|
||||||
|
#[test]
|
||||||
|
fn a_landed_folder_rewrites_no_row_or_op_below_it() {
|
||||||
|
let db = Db::open_in_memory().unwrap();
|
||||||
|
db.upsert_node(&folder("root", None, "My Files")).unwrap();
|
||||||
|
let root = uid("root").to_string();
|
||||||
|
let dir_lid = db.add_local_row(&root, "dir", true, 1).unwrap();
|
||||||
|
let local_dir = local_uid(dir_lid);
|
||||||
|
let mut dir = folder("x", Some("root"), "dir");
|
||||||
|
dir.uid = NodeUid::new(VolumeId::from("local"), LinkId::from(dir_lid.to_string()));
|
||||||
|
db.upsert_node(&dir).unwrap();
|
||||||
|
let child_lid = db.add_local_row(&local_dir, "a.txt", false, 1).unwrap();
|
||||||
|
let local_child = local_uid(child_lid);
|
||||||
|
let mut child = file("y", "x", "a.txt", 3);
|
||||||
|
child.uid = NodeUid::new(VolumeId::from("local"), LinkId::from(child_lid.to_string()));
|
||||||
|
child.parent_uid = Some(dir.uid.clone());
|
||||||
|
db.upsert_node(&child).unwrap();
|
||||||
|
db.enqueue_op(&op_on(OP_CREATE, &local_child, Some(&local_dir)))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let real_dir = uid("dir").to_string();
|
||||||
|
db.land_placeholder_row(&local_dir, &real_dir).unwrap();
|
||||||
|
|
||||||
|
let (stored_parent, op_parent): (String, String) = {
|
||||||
|
let conn = db.conn.lock();
|
||||||
|
(
|
||||||
|
conn.query_row(
|
||||||
|
"SELECT parent_uid FROM nodes WHERE lid = ?1",
|
||||||
|
[child_lid],
|
||||||
|
|r| r.get(0),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
conn.query_row("SELECT parent_uid FROM pending_op", [], |r| r.get(0))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
(stored_parent.as_str(), op_parent.as_str()),
|
||||||
|
(local_dir.as_str(), local_dir.as_str()),
|
||||||
|
"the child's row and op are not rewritten"
|
||||||
|
);
|
||||||
|
|
||||||
|
let children = db.known_children(&uid("dir")).unwrap();
|
||||||
|
assert_eq!(children.len(), 1);
|
||||||
|
assert_eq!(children[0].parent_uid, Some(uid("dir")));
|
||||||
|
assert!(db.has_children(&uid("dir")).unwrap());
|
||||||
|
assert_eq!(
|
||||||
|
db.node_path(&local_child).unwrap().as_deref(),
|
||||||
|
Some("dir/a.txt")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
db.pending_ops().unwrap()[0].parent_uid.as_deref(),
|
||||||
|
Some(real_dir.as_str()),
|
||||||
|
"the drain sends the folder's real uid"
|
||||||
|
);
|
||||||
|
|
||||||
|
db.delete_ops_for_uid(&real_dir).unwrap();
|
||||||
|
assert!(
|
||||||
|
db.pending_ops().unwrap().is_empty(),
|
||||||
|
"removing the folder takes the create made inside it"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A create queued inside a folder made here is sent under the folder's real
|
||||||
|
/// uid once it lands. Landing where it was sent is no move.
|
||||||
|
#[test]
|
||||||
|
fn a_create_sent_into_a_landed_folder_lands_where_it_was_queued() {
|
||||||
|
let db = Db::open_in_memory().unwrap();
|
||||||
|
db.upsert_node(&folder("root", None, "My Files")).unwrap();
|
||||||
|
let dir_lid = db
|
||||||
|
.add_local_row(&uid("root").to_string(), "dir", true, 1)
|
||||||
|
.unwrap();
|
||||||
|
let local_dir = local_uid(dir_lid);
|
||||||
|
let child_lid = db.add_local_row(&local_dir, "new.txt", false, 1).unwrap();
|
||||||
|
let local_child = local_uid(child_lid);
|
||||||
|
let (id, _) = db
|
||||||
|
.enqueue_op(&op_on(OP_CREATE, &local_child, Some(&local_dir)))
|
||||||
|
.unwrap();
|
||||||
|
let real_dir = uid("dir").to_string();
|
||||||
|
db.land_placeholder_row(&local_dir, &real_dir).unwrap();
|
||||||
|
|
||||||
|
let claimed = db.claim_next_due_op(1).unwrap().unwrap();
|
||||||
|
assert_eq!(claimed.parent_uid.as_deref(), Some(real_dir.as_str()));
|
||||||
|
let real = uid("new").to_string();
|
||||||
|
db.finish_create(
|
||||||
|
id,
|
||||||
|
Some("/staging/create"),
|
||||||
|
&landing(&local_child, &real, &real_dir, "new.txt"),
|
||||||
|
|_| None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
db.pending_ops().unwrap().is_empty(),
|
||||||
|
"no rename is queued for a file that landed where it was sent"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn migration_v36_gives_every_node_its_rowid_as_lid() {
|
fn migration_v36_gives_every_node_its_rowid_as_lid() {
|
||||||
let path = std::env::temp_dir().join(format!(
|
let path = std::env::temp_dir().join(format!(
|
||||||
|
|||||||
@@ -72,18 +72,18 @@ pub(super) fn path_of(conn: &Connection, uid: &str) -> Result<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve a path by walking `parent_uid` to the root via a recursive CTE.
|
/// Resolve a path by walking `parent_lid` to the root via a recursive CTE.
|
||||||
///
|
///
|
||||||
/// A cycle in `parent_uid` is corrupt data the API can still hand us, and
|
/// A cycle in the parents is corrupt data the API can still hand us, and
|
||||||
/// `UNION ALL` over one never terminates — the walk is therefore depth-capped
|
/// `UNION ALL` over one never terminates — the walk is therefore depth-capped
|
||||||
/// and returns the truncated path rather than hanging the caller.
|
/// and returns the truncated path rather than hanging the caller.
|
||||||
pub(super) fn walk_path_of(conn: &Connection, uid: &str) -> Result<String> {
|
pub(super) fn walk_path_of(conn: &Connection, uid: &str) -> Result<String> {
|
||||||
let mut stmt = conn.prepare(&format!(
|
let mut stmt = conn.prepare(&format!(
|
||||||
"WITH RECURSIVE anc(uid, parent_uid, name, depth) AS (
|
"WITH RECURSIVE anc(parent_lid, parent_uid, name, depth) AS (
|
||||||
SELECT uid, parent_uid, name, 0 FROM nodes WHERE uid = ?1
|
SELECT parent_lid, parent_uid, name, 0 FROM nodes WHERE uid = ?1
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT n.uid, n.parent_uid, n.name, anc.depth + 1
|
SELECT n.parent_lid, n.parent_uid, n.name, anc.depth + 1
|
||||||
FROM nodes n JOIN anc ON n.uid = anc.parent_uid
|
FROM nodes n JOIN anc ON n.lid = anc.parent_lid
|
||||||
WHERE anc.depth < {MAX_PATH_DEPTH}
|
WHERE anc.depth < {MAX_PATH_DEPTH}
|
||||||
)
|
)
|
||||||
SELECT name, parent_uid FROM anc ORDER BY depth DESC"
|
SELECT name, parent_uid FROM anc ORDER BY depth DESC"
|
||||||
|
|||||||
@@ -34,17 +34,20 @@ Tracks `docs/MILESTONE-3.0.0.md`. Update it with every commit that moves a phase
|
|||||||
through `parent_lid`, and a landing folder rewrites the ops inside it by local id.
|
through `parent_lid`, and a landing folder rewrites the ops inside it by local id.
|
||||||
- [x] Node rows link to their parent by lid (`nodes.parent_lid`, schema 40), and a restart
|
- [x] Node rows link to their parent by lid (`nodes.parent_lid`, schema 40), and a restart
|
||||||
places each node under its parent's row as it is now.
|
places each node under its parent's row as it is now.
|
||||||
- [ ] Queries over children and subtrees by `parent_lid`, and no rewrite of the children's
|
- [x] Queries over children, subtrees and ancestors by `parent_lid`, and no rewrite of the
|
||||||
`parent_uid` when a folder lands (R3). Both go with the placeholders: until then the
|
children's `parent_uid` when a folder lands (R3), nor of the ops made inside it (R2).
|
||||||
strings are kept in step.
|
- [ ] No more placeholders: a node made here goes by its local id until it lands. In four
|
||||||
- [ ] No more placeholders: `nodes.uid` is `NULL` until a node lands. In four parts:
|
parts:
|
||||||
- [x] 6a: a node made here goes by `local~<lid>`, a function of its row, instead of a
|
- [x] 6a: a node made here goes by `local~<lid>`, a function of its row, instead of a
|
||||||
minted `local~<ms>-<n>`; schema 41 rewrites the old placeholders (nodes, ops, pins).
|
minted `local~<ms>-<n>`; schema 41 rewrites the old placeholders (nodes, ops, pins).
|
||||||
- [x] 6b: an op is found by its node's local id under either uid, the claim keeps one op
|
- [x] 6b: an op is found by its node's local id under either uid, the claim keeps one op
|
||||||
per local id on the wire, and the drain sends the uid the row has now;
|
per local id on the wire, and the drain sends the uid the row has now;
|
||||||
`remap_local_uid` is gone.
|
`remap_local_uid` is gone.
|
||||||
- [ ] 6c: a local node's row has `uid NULL`; children, subtrees and ops' parents by lid,
|
- [x] 6c: children, subtrees, ancestors and ops' parents by lid, so a landing create
|
||||||
so a landing create rewrites no other row or op; pins by lid.
|
rewrites no other row or op; what is read for a uid names the parent as its row has it
|
||||||
|
now. The row keeps its stand-in in `nodes.uid` rather than `NULL`: it is as stable,
|
||||||
|
and `NULL` would have changed some 40 node queries for no change in behaviour. Pins
|
||||||
|
stay keyed by uid and move with the row in the landing transaction (B148).
|
||||||
- [ ] 6d: in memory, `pending`, `hidden` and `creating` by lid; `is_local_uid` checks
|
- [ ] 6d: in memory, `pending`, `hidden` and `creating` by lid; `is_local_uid` checks
|
||||||
become `remote_uid().is_none()`; a write or rename after landing queues under the
|
become `remote_uid().is_none()`; a write or rename after landing queues under the
|
||||||
stand-in, so `landed_uid`, `follow_landed_create`, `landed_row_uid` and the
|
stand-in, so `landed_uid`, `follow_landed_create`, `landed_row_uid` and the
|
||||||
|
|||||||
Reference in new issue
Block a user