M12 — the Android client, end to end #2

Merged
bvandeusen merged 86 commits from dev into main 2026-08-21 08:53:58 -04:00
Showing only changes of commit 75b2d096ec - Show all commits
+21 -21
View File
@@ -161,11 +161,7 @@ pub fn collect(conn: &Connection, limit: usize) -> rusqlite::Result<Vec<Change>>
Ok(out)
}
fn collect_deletes(
conn: &Connection,
out: &mut Vec<Change>,
limit: usize,
) -> rusqlite::Result<()> {
fn collect_deletes(conn: &Connection, out: &mut Vec<Change>, limit: usize) -> rusqlite::Result<()> {
let mut stmt = conn.prepare(
"SELECT entity, id, deleted_at FROM pending_deletes ORDER BY deleted_at LIMIT ?1",
)?;
@@ -227,9 +223,8 @@ fn collect_labels(conn: &Connection, out: &mut Vec<Change>, limit: usize) -> rus
fn collect_notes(conn: &Connection, out: &mut Vec<Change>, limit: usize) -> rusqlite::Result<()> {
let remaining = limit.saturating_sub(out.len());
let ids: Vec<String> = {
let mut stmt = conn.prepare(
"SELECT id FROM notes WHERE dirty = 1 ORDER BY updated_at LIMIT ?1",
)?;
let mut stmt =
conn.prepare("SELECT id FROM notes WHERE dirty = 1 ORDER BY updated_at LIMIT ?1")?;
let rows = stmt.query_map(params![remaining as i64], |r| r.get::<_, String>(0))?;
rows.collect::<rusqlite::Result<Vec<String>>>()?
};
@@ -301,9 +296,8 @@ fn note_change(conn: &Connection, id: &str) -> rusqlite::Result<Change> {
// server from the body; sending them as label_ids would convert them into manual
// assignments that no longer disappear when the #tag is removed from the text.
let label_ids = {
let mut stmt = conn.prepare(
"SELECT label_id FROM note_labels WHERE note_id = ?1 AND via_tag = 0",
)?;
let mut stmt =
conn.prepare("SELECT label_id FROM note_labels WHERE note_id = ?1 AND via_tag = 0")?;
let rows = stmt.query_map(params![id], |r| r.get::<_, String>(0))?;
rows.collect::<rusqlite::Result<Vec<String>>>()?
};
@@ -391,8 +385,13 @@ pub fn apply_results(
// retried, and surface the reason. A duplicate label name is the
// realistic case and only a human can resolve it.
summary.rejected += 1;
let reason = result.error.clone().unwrap_or_else(|| result.status.clone());
summary.errors.push(format!("{} {}: {reason}", change.entity, change.id));
let reason = result
.error
.clone()
.unwrap_or_else(|| result.status.clone());
summary
.errors
.push(format!("{} {}: {reason}", change.entity, change.id));
}
}
}
@@ -413,11 +412,7 @@ pub fn apply_results(
Ok(summary)
}
fn clear_dirty(
conn: &Connection,
change: &Change,
revision: Option<i64>,
) -> rusqlite::Result<()> {
fn clear_dirty(conn: &Connection, change: &Change, revision: Option<i64>) -> rusqlite::Result<()> {
// A delete has no local row left to update.
if change.op == "delete" {
return Ok(());
@@ -560,8 +555,10 @@ mod tests {
}
fn dirty_count(conn: &Connection) -> i64 {
conn.query_row("SELECT COUNT(*) FROM notes WHERE dirty = 1", [], |r| r.get(0))
.expect("count")
conn.query_row("SELECT COUNT(*) FROM notes WHERE dirty = 1", [], |r| {
r.get(0)
})
.expect("count")
}
#[test]
@@ -746,6 +743,9 @@ mod tests {
let change = Change::delete("note", "n1".into(), "2026-07-26T00:00:00.000Z".into());
let json = serde_json::to_string(&change).expect("serialize");
assert!(json.contains("\"op\":\"delete\""), "got {json}");
assert!(!json.contains("body"), "a delete carries no content: {json}");
assert!(
!json.contains("body"),
"a delete carries no content: {json}"
);
}
}