appcore_sync_sqlite/
conflict.rs1use crate::{SqliteSyncError, SqliteSyncResult, SqliteSyncStore};
13use appcore_sync::{
14 SyncConflict, SyncConflictResolutionReceipt, SyncConflictResolutionRequest, SyncConflictStore,
15 SyncError, SyncResult, MAX_SYNC_CONFLICTS,
16};
17use rusqlite::{params, Connection, OptionalExtension, TransactionBehavior};
18
19const MAX_CONFLICT_BYTES: usize = 8_192;
20const MAX_RESOLUTION_BYTES: usize = 4_096;
21
22#[derive(Clone, Debug)]
24pub struct SqliteSyncConflictStore {
25 store: SqliteSyncStore,
26}
27impl SqliteSyncStore {
28 pub fn conflict_store(&self) -> SqliteSyncConflictStore {
30 SqliteSyncConflictStore {
31 store: self.clone(),
32 }
33 }
34}
35impl SyncConflictStore for SqliteSyncConflictStore {
36 fn record(&self, conflict: SyncConflict) -> SyncResult<()> {
37 conflict.validate()?;
38 self.store
39 .with_connection(|connection| {
40 let transaction = connection
41 .transaction_with_behavior(TransactionBehavior::Immediate)
42 .map_err(SqliteSyncError::database)?;
43 let existing: Option<String> = transaction
44 .query_row(
45 "SELECT CASE WHEN length(CAST(evidence AS BLOB)) <= 8192 THEN evidence ELSE NULL END FROM appcore_sync_conflict WHERE conflict_id = ?1",
46 [&conflict.conflict_id],
47 |row| row.get(0),
48 )
49 .optional()
50 .map_err(SqliteSyncError::database)?;
51 if let Some(existing) = existing {
52 if decode_conflict(&existing)? == conflict {
53 return Ok(());
54 }
55 return Err(SqliteSyncError::CorruptRecord("conflict identity"));
56 }
57 let count: i64 = transaction
58 .query_row("SELECT COUNT(*) FROM appcore_sync_conflict", [], |row| {
59 row.get(0)
60 })
61 .map_err(SqliteSyncError::database)?;
62 if count >= MAX_SYNC_CONFLICTS as i64 {
63 return Err(SqliteSyncError::CapacityExceeded("conflict"));
64 }
65 let encoded = serde_json::to_string(&conflict)
66 .map_err(|_| SqliteSyncError::CorruptRecord("conflict"))?;
67 if encoded.len() > MAX_CONFLICT_BYTES {
68 return Err(SqliteSyncError::CapacityExceeded("conflict record"));
69 }
70 transaction
71 .execute(
72 "INSERT INTO appcore_sync_conflict(conflict_id, evidence) VALUES (?1, ?2)",
73 params![conflict.conflict_id, encoded],
74 )
75 .map_err(SqliteSyncError::database)?;
76 transaction.commit().map_err(SqliteSyncError::database)
77 })
78 .map_err(|error| match error {
79 SqliteSyncError::CapacityExceeded("conflict") => SyncError::ConflictStoreFull,
80 other => other.sync(),
81 })
82 }
83 fn resolve(
84 &self,
85 request: SyncConflictResolutionRequest,
86 ) -> SyncResult<SyncConflictResolutionReceipt> {
87 request.validate()?;
88 self.store
89 .with_connection(|connection| {
90 let transaction = connection
91 .transaction_with_behavior(TransactionBehavior::Immediate)
92 .map_err(SqliteSyncError::database)?;
93 let row: Option<(String, Option<String>)> = transaction.query_row(
94 "SELECT CASE WHEN length(CAST(evidence AS BLOB)) <= 8192 AND (resolution IS NULL OR length(CAST(resolution AS BLOB)) <= 4096) THEN evidence ELSE NULL END, CASE WHEN length(CAST(resolution AS BLOB)) <= 4096 THEN resolution ELSE NULL END FROM appcore_sync_conflict WHERE conflict_id = ?1",
95 [&request.conflict_id], |row| Ok((row.get(0)?, row.get(1)?)),
96 ).optional().map_err(SqliteSyncError::database)?;
97 let Some((evidence, resolution)) = row else {
98 return Err(SqliteSyncError::CorruptRecord("conflict missing"));
99 };
100 let conflict = decode_conflict(&evidence)?;
101 if conflict.conflict_id != request.conflict_id { return Err(SqliteSyncError::IntegrityFailed); }
102 if let Some(resolution) = resolution {
103 let mut receipt = decode_resolution(&resolution, &conflict.conflict_id)?;
104 if receipt.idempotency_key != request.idempotency_key
105 || receipt.resolution != request.resolution
106 {
107 return Err(SqliteSyncError::CorruptRecord("conflict resolved"));
108 }
109 receipt.idempotent = true;
110 return Ok(receipt);
111 }
112 let receipt = SyncConflictResolutionReceipt {
113 conflict_id: request.conflict_id.clone(),
114 idempotency_key: request.idempotency_key,
115 resolution: request.resolution,
116 idempotent: false,
117 resolved_at_ms: request.resolved_at_ms,
118 };
119 let encoded = serde_json::to_string(&receipt)
120 .map_err(|_| SqliteSyncError::CorruptRecord("resolution"))?;
121 if encoded.len() > MAX_RESOLUTION_BYTES {
122 return Err(SqliteSyncError::CapacityExceeded("resolution"));
123 }
124 transaction
125 .execute(
126 "UPDATE appcore_sync_conflict SET resolution = ?1 WHERE conflict_id = ?2",
127 params![encoded, receipt.conflict_id],
128 )
129 .map_err(SqliteSyncError::database)?;
130 transaction.commit().map_err(SqliteSyncError::database)?;
131 Ok(receipt)
132 })
133 .map_err(|error| match error {
134 SqliteSyncError::CorruptRecord("conflict missing") => {
135 SyncError::ConflictNotFound(request.conflict_id.clone())
136 }
137 SqliteSyncError::CorruptRecord("conflict resolved") => {
138 SyncError::ConflictAlreadyResolved(request.conflict_id.clone())
139 }
140 other => other.sync(),
141 })
142 }
143 fn list(&self) -> SyncResult<Vec<SyncConflict>> {
144 self.store
145 .with_connection(|connection| {
146 read_conflicts(connection)
147 .map(|records| records.into_iter().map(|(conflict, _)| conflict).collect())
148 })
149 .map_err(SqliteSyncError::sync)
150 }
151}
152
153pub(crate) fn validate_conflicts(connection: &Connection) -> SqliteSyncResult<()> {
154 read_conflicts(connection).map(|_| ())
155}
156fn read_conflicts(
157 connection: &Connection,
158) -> SqliteSyncResult<Vec<(SyncConflict, Option<SyncConflictResolutionReceipt>)>> {
159 let mut statement = connection.prepare("SELECT conflict_id, CASE WHEN length(CAST(evidence AS BLOB)) <= 8192 AND (resolution IS NULL OR length(CAST(resolution AS BLOB)) <= 4096) THEN evidence ELSE NULL END, resolution FROM appcore_sync_conflict ORDER BY conflict_id LIMIT 4097")
160 .map_err(SqliteSyncError::database)?;
161 let rows = statement
162 .query_map([], |row| {
163 Ok((
164 row.get::<_, String>(0)?,
165 row.get::<_, String>(1)?,
166 row.get::<_, Option<String>>(2)?,
167 ))
168 })
169 .map_err(SqliteSyncError::database)?;
170 let mut records = Vec::new();
171 for row in rows {
172 let (id, encoded, resolution) = row.map_err(SqliteSyncError::database)?;
173 let conflict = decode_conflict(&encoded)?;
174 if conflict.conflict_id != id || records.len() >= MAX_SYNC_CONFLICTS {
175 return Err(SqliteSyncError::IntegrityFailed);
176 }
177 let resolution = resolution
178 .map(|value| decode_resolution(&value, &id))
179 .transpose()?;
180 records.push((conflict, resolution));
181 }
182 Ok(records)
183}
184fn decode_conflict(encoded: &str) -> SqliteSyncResult<SyncConflict> {
185 if encoded.len() > MAX_CONFLICT_BYTES {
186 return Err(SqliteSyncError::IntegrityFailed);
187 }
188 let value: SyncConflict =
189 serde_json::from_str(encoded).map_err(|_| SqliteSyncError::IntegrityFailed)?;
190 value
191 .validate()
192 .map_err(|_| SqliteSyncError::IntegrityFailed)?;
193 Ok(value)
194}
195fn decode_resolution(encoded: &str, id: &str) -> SqliteSyncResult<SyncConflictResolutionReceipt> {
196 if encoded.len() > MAX_RESOLUTION_BYTES {
197 return Err(SqliteSyncError::IntegrityFailed);
198 }
199 let value: SyncConflictResolutionReceipt =
200 serde_json::from_str(encoded).map_err(|_| SqliteSyncError::IntegrityFailed)?;
201 if value.conflict_id != id || value.idempotent {
202 return Err(SqliteSyncError::IntegrityFailed);
203 }
204 SyncConflictResolutionRequest {
205 conflict_id: value.conflict_id.clone(),
206 idempotency_key: value.idempotency_key.clone(),
207 resolution: value.resolution,
208 resolved_at_ms: value.resolved_at_ms,
209 }
210 .validate()
211 .map_err(|_| SqliteSyncError::IntegrityFailed)?;
212 Ok(value)
213}