aven_core/sync/persistence/
parent_liveness.rs1use anyhow::Result;
2use serde_json::Value;
3use sqlx::{Row, SqliteConnection};
4
5use crate::change_log::op_type;
6use crate::db::{Database, begin_immediate};
7
8impl Database {
9 pub async fn reconcile_server_attachment_parents(&self) -> Result<()> {
11 let mut conn = self.acquire_writer().await?;
12 let mut tx = begin_immediate(&mut conn).await?;
13 let parents: Vec<(String, String)> = sqlx::query_as(
14 "SELECT workspace_id, task_id FROM server_task_tombstones
15 UNION SELECT workspace_id, task_id FROM server_blob_references",
16 )
17 .fetch_all(&mut *tx)
18 .await?;
19 for (workspace, task) in parents {
20 reconcile_parent(&mut tx, &workspace, &task).await?;
21 }
22 crate::attachments::lifecycle::reconcile_liveness_in_transaction(
23 &mut tx,
24 &crate::attachments::lifecycle::SystemClock,
25 )
26 .await?;
27 tx.commit().await?;
28 Ok(())
29 }
30}
31
32pub(super) async fn reconcile_parent(
33 conn: &mut SqliteConnection,
34 workspace: &str,
35 task: &str,
36) -> Result<()> {
37 let rows = sqlx::query(
38 "SELECT change_id, op_type, payload, base_version FROM changes
39 WHERE entity_type = 'task' AND entity_id = ? AND server_seq IS NOT NULL
40 AND json_extract(payload, '$.workspace_id') = ?
41 AND (op_type = 'create_task'
42 OR (op_type IN ('set_field', 'resolve_field') AND field = 'deleted'))
43 ORDER BY server_seq",
44 )
45 .bind(task)
46 .bind(workspace)
47 .fetch_all(&mut *conn)
48 .await?;
49 let mut version: Option<String> = None;
50 let mut deleted = false;
51 let mut contested = false;
52 for row in rows {
53 let id: String = row.try_get("change_id")?;
54 let operation: String = row.try_get("op_type")?;
55 let payload: Value = serde_json::from_str(row.try_get("payload")?)?;
56 if operation == op_type::CREATE_TASK {
57 if version.is_none() {
58 version = Some(
59 payload["task_field_version_seed"]
60 .as_str()
61 .unwrap_or(&id)
62 .to_owned(),
63 );
64 }
65 continue;
66 }
67 let base: Option<String> = row.try_get("base_version")?;
68 if version.is_none() || (operation != op_type::RESOLVE_FIELD && base != version) {
69 contested = true;
70 continue;
71 }
72 deleted = payload["value"].as_str() == Some("1");
73 version = Some(id);
74 }
75 let unreferenced = deleted && version.is_some() && !contested;
78 sqlx::query(
79 "INSERT INTO server_task_tombstones(workspace_id, task_id, deleted)
80 VALUES (?, ?, ?)
81 ON CONFLICT(workspace_id, task_id) DO UPDATE SET deleted = excluded.deleted",
82 )
83 .bind(workspace)
84 .bind(task)
85 .bind(unreferenced)
86 .execute(&mut *conn)
87 .await?;
88 Ok(())
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use serde_json::json;
95
96 async fn project(history: &[(&str, &str, Option<&str>, &str)]) -> bool {
97 let (_temp, mut conn) = crate::test_support::test_conn().await;
98 for (index, (id, op, base, value)) in history.iter().enumerate() {
99 let payload = if *op == op_type::CREATE_TASK {
100 json!({"workspace_id": "workspace", "task_field_version_seed": value})
101 } else {
102 json!({"workspace_id": "workspace", "value": value})
103 };
104 sqlx::query(
105 "INSERT INTO changes(change_id, client_id, local_seq, entity_type, entity_id,
106 field, op_type, payload, base_version, created_at, server_seq)
107 VALUES (?, 'client', ?, 'task', 'task', 'deleted', ?, ?, ?, 'now', ?)",
108 )
109 .bind(id)
110 .bind(index as i64 + 1)
111 .bind(op)
112 .bind(payload.to_string())
113 .bind(base)
114 .bind(index as i64 + 1)
115 .execute(&mut *conn)
116 .await
117 .unwrap();
118 }
119 reconcile_parent(&mut conn, "workspace", "task")
120 .await
121 .unwrap();
122 sqlx::query_scalar("SELECT deleted FROM server_task_tombstones WHERE task_id = 'task'")
123 .fetch_one(&mut *conn)
124 .await
125 .unwrap()
126 }
127
128 #[tokio::test]
129 async fn ordered_deletion_restore_and_custom_seed() {
130 let mut history = vec![
131 ("create", op_type::CREATE_TASK, None, "seed"),
132 ("delete", op_type::SET_FIELD, Some("seed"), "1"),
133 ];
134 assert!(project(&history).await);
135 history.push(("restore", op_type::SET_FIELD, Some("delete"), "0"));
136 assert!(!project(&history).await);
137 history.push(("delete2", op_type::SET_FIELD, Some("restore"), "1"));
138 assert!(project(&history).await);
139 }
140
141 #[tokio::test]
142 async fn stale_delete_and_force_resolution_keep_protection() {
143 let mut history = vec![
144 ("create", op_type::CREATE_TASK, None, "seed"),
145 ("delete", op_type::SET_FIELD, Some("seed"), "1"),
146 ("restore", op_type::SET_FIELD, Some("delete"), "0"),
147 ("stale", op_type::SET_FIELD, Some("seed"), "1"),
148 ];
149 assert!(!project(&history).await);
150 history.push(("resolve", op_type::RESOLVE_FIELD, Some("restore"), "1"));
151 assert!(!project(&history).await);
152 }
153
154 #[tokio::test]
155 async fn equal_value_conflicts_and_missing_creation_are_conservative() {
156 assert!(
157 !project(&[
158 ("create", op_type::CREATE_TASK, None, "seed"),
159 ("delete", op_type::SET_FIELD, Some("seed"), "1"),
160 ("other", op_type::SET_FIELD, Some("seed"), "1"),
161 ])
162 .await
163 );
164 assert!(!project(&[("delete", op_type::SET_FIELD, None, "1")]).await);
165 assert!(!project(&[]).await);
166 }
167}