1use crate::db::AgentDB;
10use crate::error::{AgentDbError, Result};
11use rusqlite::{params, OptionalExtension};
12use serde::{Deserialize, Serialize};
13use std::sync::{Arc, Mutex};
14use uuid::Uuid;
15
16pub struct HybridClock {
24 physical: i64,
25 logical: u32,
26 #[allow(dead_code)]
27 node_id: String,
28}
29
30impl HybridClock {
31 pub fn new(node_id: &str) -> Self {
33 Self {
34 physical: now_ms(),
35 logical: 0,
36 node_id: node_id.to_string(),
37 }
38 }
39
40 pub fn now(&mut self) -> i64 {
44 let wall = now_ms();
45 if wall > self.physical {
46 self.physical = wall;
47 self.logical = 0;
48 } else {
49 self.logical += 1;
51 }
52 pack(self.physical, self.logical)
53 }
54
55 pub fn update(&mut self, remote_ts: i64) {
57 let (r_phys, r_log) = unpack(remote_ts);
58 let wall = now_ms();
59 let new_phys = wall.max(self.physical).max(r_phys);
60 if new_phys == self.physical && new_phys == r_phys {
61 self.logical = self.logical.max(r_log) + 1;
62 } else if new_phys == self.physical {
63 self.logical += 1;
64 } else if new_phys == r_phys {
65 self.logical = r_log + 1;
66 } else {
67 self.logical = 0;
68 }
69 self.physical = new_phys;
70 }
71}
72
73#[inline]
74fn pack(physical_ms: i64, logical: u32) -> i64 {
75 (physical_ms << 16) | (logical as i64 & 0xFFFF)
76}
77
78#[inline]
79fn unpack(ts: i64) -> (i64, u32) {
80 let phys = ts >> 16;
81 let log = (ts & 0xFFFF) as u32;
82 (phys, log)
83}
84
85fn now_ms() -> i64 {
86 std::time::SystemTime::now()
87 .duration_since(std::time::UNIX_EPOCH)
88 .unwrap_or_default()
89 .as_millis() as i64
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
96#[serde(rename_all = "lowercase")]
97pub enum OpType {
98 Insert,
99 Update,
100 Delete,
101}
102
103impl OpType {
104 fn as_str(&self) -> &'static str {
105 match self {
106 OpType::Insert => "insert",
107 OpType::Update => "update",
108 OpType::Delete => "delete",
109 }
110 }
111
112 fn from_str(s: &str) -> Result<Self> {
113 match s {
114 "insert" => Ok(OpType::Insert),
115 "update" => Ok(OpType::Update),
116 "delete" => Ok(OpType::Delete),
117 other => Err(AgentDbError::InvalidArgument(format!(
118 "unknown op_type: {other}"
119 ))),
120 }
121 }
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct SyncOp {
127 pub op_id: String,
129 pub hlc_ts: i64,
131 pub node_id: String,
133 pub table_name: String,
135 pub record_id: String,
137 pub op_type: OpType,
139 pub payload: Option<serde_json::Value>,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct PeerStatus {
146 pub peer_id: String,
148 pub last_synced_hlc: i64,
150 pub endpoint: Option<String>,
152 pub ops_pending: usize,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize, Default)]
158pub struct SyncResult {
159 pub applied: usize,
161 pub conflicts: usize,
163 pub skipped: usize,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
169pub enum ConflictStrategy {
170 LastWriterWins,
172 FirstWriterWins,
174}
175
176pub struct SyncEngine {
185 conn: Arc<Mutex<rusqlite::Connection>>,
186 clock: HybridClock,
187 node_id: String,
188 strategy: ConflictStrategy,
189}
190
191impl SyncEngine {
192 pub fn new(db: &AgentDB, node_id: &str) -> Result<Self> {
196 let conn = db.conn_arc();
198 {
199 let c = conn.lock().unwrap();
200 c.execute_batch(
201 "
202 CREATE TABLE IF NOT EXISTS _adb_sync_log (
203 op_id TEXT PRIMARY KEY,
204 hlc_ts INTEGER NOT NULL,
205 node_id TEXT NOT NULL,
206 table_name TEXT NOT NULL,
207 record_id TEXT NOT NULL,
208 op_type TEXT NOT NULL CHECK(op_type IN ('insert','update','delete')),
209 payload TEXT,
210 created_at TEXT DEFAULT (datetime('now'))
211 );
212 CREATE INDEX IF NOT EXISTS idx_sync_log_hlc
213 ON _adb_sync_log(hlc_ts);
214 CREATE INDEX IF NOT EXISTS idx_sync_log_row
215 ON _adb_sync_log(table_name, record_id);
216
217 CREATE TABLE IF NOT EXISTS _adb_sync_peers (
218 peer_id TEXT PRIMARY KEY,
219 last_synced_hlc INTEGER NOT NULL DEFAULT 0,
220 endpoint TEXT,
221 updated_at TEXT DEFAULT (datetime('now'))
222 );
223 ",
224 )?;
225 }
226 Ok(Self {
227 conn,
228 clock: HybridClock::new(node_id),
229 node_id: node_id.to_string(),
230 strategy: ConflictStrategy::LastWriterWins,
231 })
232 }
233
234 pub fn record_mutation(
236 &mut self,
237 table: &str,
238 record_id: &str,
239 op: OpType,
240 payload: Option<serde_json::Value>,
241 ) -> Result<String> {
242 let op_id = Uuid::new_v4().to_string();
243 let hlc_ts = self.clock.now();
244 let payload_str = payload.as_ref().map(|v| v.to_string());
245 let conn = self.conn.lock().unwrap();
246 conn.execute(
247 "INSERT INTO _adb_sync_log
248 (op_id, hlc_ts, node_id, table_name, record_id, op_type, payload)
249 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
250 params![
251 op_id,
252 hlc_ts,
253 self.node_id,
254 table,
255 record_id,
256 op.as_str(),
257 payload_str,
258 ],
259 )?;
260 Ok(op_id)
261 }
262
263 pub fn get_ops_since(&self, since_hlc: i64) -> Result<Vec<SyncOp>> {
265 let conn = self.conn.lock().unwrap();
266 let mut stmt = conn.prepare(
267 "SELECT op_id, hlc_ts, node_id, table_name, record_id, op_type, payload
268 FROM _adb_sync_log
269 WHERE hlc_ts > ?1
270 ORDER BY hlc_ts ASC",
271 )?;
272 let rows = stmt.query_map(params![since_hlc], parse_sync_op)?;
273 rows.map(|r| r.map_err(AgentDbError::Sqlite)).collect()
274 }
275
276 pub fn apply_remote_ops(&mut self, ops: Vec<SyncOp>) -> Result<SyncResult> {
285 let mut result = SyncResult::default();
286
287 for op in ops {
288 self.clock.update(op.hlc_ts);
290
291 let existing = {
292 let conn = self.conn.lock().unwrap();
293 conn.query_row(
294 "SELECT op_id, hlc_ts FROM _adb_sync_log
295 WHERE table_name = ?1 AND record_id = ?2
296 LIMIT 1",
297 params![op.table_name, op.record_id],
298 |row| Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?)),
299 )
300 .optional()?
301 };
302
303 match existing {
304 None => {
305 self.insert_op(&op)?;
307 result.applied += 1;
308 }
309 Some((existing_op_id, existing_hlc)) => {
310 result.conflicts += 1;
311 let incoming_wins = match self.strategy {
312 ConflictStrategy::LastWriterWins => op.hlc_ts > existing_hlc,
313 ConflictStrategy::FirstWriterWins => op.hlc_ts < existing_hlc,
314 };
315 if incoming_wins {
316 let conn = self.conn.lock().unwrap();
318 conn.execute(
319 "DELETE FROM _adb_sync_log WHERE op_id = ?1",
320 params![existing_op_id],
321 )?;
322 drop(conn);
323 self.insert_op(&op)?;
324 result.applied += 1;
325 } else {
326 result.skipped += 1;
327 }
328 }
329 }
330 }
331
332 Ok(result)
333 }
334
335 pub fn add_peer(&self, peer_id: &str, endpoint: Option<&str>) -> Result<()> {
337 let conn = self.conn.lock().unwrap();
338 conn.execute(
339 "INSERT INTO _adb_sync_peers (peer_id, last_synced_hlc, endpoint)
340 VALUES (?1, 0, ?2)
341 ON CONFLICT(peer_id) DO UPDATE SET
342 endpoint = excluded.endpoint,
343 updated_at = datetime('now')",
344 params![peer_id, endpoint],
345 )?;
346 Ok(())
347 }
348
349 pub fn sync_status(&self) -> Result<Vec<PeerStatus>> {
351 let conn = self.conn.lock().unwrap();
352 let mut stmt = conn.prepare(
353 "SELECT peer_id, last_synced_hlc, endpoint
354 FROM _adb_sync_peers
355 ORDER BY peer_id",
356 )?;
357 let rows = stmt.query_map([], |row| {
358 Ok((
359 row.get::<_, String>(0)?,
360 row.get::<_, i64>(1)?,
361 row.get::<_, Option<String>>(2)?,
362 ))
363 })?;
364
365 let mut statuses = Vec::new();
366 for row in rows {
367 let (peer_id, last_synced_hlc, endpoint) = row.map_err(AgentDbError::Sqlite)?;
368 let ops_pending: i64 = conn.query_row(
370 "SELECT COUNT(*) FROM _adb_sync_log WHERE hlc_ts > ?1",
371 params![last_synced_hlc],
372 |r| r.get(0),
373 )?;
374 statuses.push(PeerStatus {
375 peer_id,
376 last_synced_hlc,
377 endpoint,
378 ops_pending: ops_pending as usize,
379 });
380 }
381 Ok(statuses)
382 }
383
384 pub fn set_strategy(&mut self, strategy: ConflictStrategy) {
386 self.strategy = strategy;
387 }
388
389 fn insert_op(&self, op: &SyncOp) -> Result<()> {
392 let payload_str = op.payload.as_ref().map(|v| v.to_string());
393 let conn = self.conn.lock().unwrap();
394 conn.execute(
395 "INSERT OR REPLACE INTO _adb_sync_log
396 (op_id, hlc_ts, node_id, table_name, record_id, op_type, payload)
397 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
398 params![
399 op.op_id,
400 op.hlc_ts,
401 op.node_id,
402 op.table_name,
403 op.record_id,
404 op.op_type.as_str(),
405 payload_str,
406 ],
407 )?;
408 Ok(())
409 }
410}
411
412fn parse_sync_op(row: &rusqlite::Row) -> rusqlite::Result<SyncOp> {
415 let op_type_str: String = row.get(5)?;
416 let payload_str: Option<String> = row.get(6)?;
417 let payload = payload_str.and_then(|s| serde_json::from_str(&s).ok());
418 let op_type = OpType::from_str(&op_type_str).map_err(|_| {
419 rusqlite::Error::FromSqlConversionFailure(
420 5,
421 rusqlite::types::Type::Text,
422 Box::new(std::fmt::Error),
423 )
424 })?;
425 Ok(SyncOp {
426 op_id: row.get(0)?,
427 hlc_ts: row.get(1)?,
428 node_id: row.get(2)?,
429 table_name: row.get(3)?,
430 record_id: row.get(4)?,
431 op_type,
432 payload,
433 })
434}