| 55 |
55 |
|
|
| 56 |
56 |
|
/// The committed HLC for a row, if any — the oracle `CleanChanges::gated` uses to
|
| 57 |
57 |
|
/// drop remote changes that are not newer than what this device already holds.
|
| 58 |
|
- |
pub(crate) fn committed_hlc(conn: &Connection, table: &str, row_id: &str) -> Option<Hlc> {
|
| 59 |
|
- |
conn.query_row(
|
| 60 |
|
- |
"SELECT hlc FROM hlc_ledger WHERE table_name = ?1 AND row_id = ?2",
|
| 61 |
|
- |
rusqlite::params![table, row_id],
|
| 62 |
|
- |
|r| r.get::<_, String>(0),
|
| 63 |
|
- |
)
|
| 64 |
|
- |
.optional()
|
| 65 |
|
- |
.ok()
|
| 66 |
|
- |
.flatten()
|
| 67 |
|
- |
.and_then(|s| serde_json::from_str(&s).ok())
|
|
58 |
+ |
///
|
|
59 |
+ |
/// A genuine query error propagates. It was previously swallowed as "no committed
|
|
60 |
+ |
/// HLC", which is the one answer that gates a remote change *in* (`is_none_or`), so
|
|
61 |
+ |
/// a failed ledger read could let a stale remote value clobber a newer local one.
|
|
62 |
+ |
/// A *corrupt* stored HLC still degrades to `None` with a warning, matching how
|
|
63 |
+ |
/// `load_clock` treats a corrupt `last_hlc` — one bad ledger row must not wedge the
|
|
64 |
+ |
/// entire sync.
|
|
65 |
+ |
pub(crate) fn committed_hlc(conn: &Connection, table: &str, row_id: &str) -> Result<Option<Hlc>> {
|
|
66 |
+ |
let stored: Option<String> = conn
|
|
67 |
+ |
.query_row(
|
|
68 |
+ |
"SELECT hlc FROM hlc_ledger WHERE table_name = ?1 AND row_id = ?2",
|
|
69 |
+ |
rusqlite::params![table, row_id],
|
|
70 |
+ |
|r| r.get::<_, String>(0),
|
|
71 |
+ |
)
|
|
72 |
+ |
.optional()?;
|
|
73 |
+ |
Ok(stored.and_then(|s| match serde_json::from_str(&s) {
|
|
74 |
+ |
Ok(hlc) => Some(hlc),
|
|
75 |
+ |
Err(e) => {
|
|
76 |
+ |
tracing::warn!("corrupt committed HLC for {table}/{row_id} ({e}); treating as absent");
|
|
77 |
+ |
None
|
|
78 |
+ |
}
|
|
79 |
+ |
}))
|
| 68 |
80 |
|
}
|
| 69 |
81 |
|
|
| 70 |
82 |
|
/// Record the committed HLC for a row (one of our pushes, or an applied remote).
|
| 198 |
210 |
|
fn ledger_roundtrip_and_gate() {
|
| 199 |
211 |
|
let conn = mem();
|
| 200 |
212 |
|
let node = DeviceId::new(Uuid::from_u128(7));
|
| 201 |
|
- |
assert!(committed_hlc(&conn, "tags", "r1").is_none());
|
|
213 |
+ |
assert!(committed_hlc(&conn, "tags", "r1").unwrap().is_none());
|
| 202 |
214 |
|
let older = Hlc {
|
| 203 |
215 |
|
wall_ms: 100,
|
| 204 |
216 |
|
counter: 0,
|
| 210 |
222 |
|
node,
|
| 211 |
223 |
|
};
|
| 212 |
224 |
|
set_committed(&conn, "tags", "r1", &newer).unwrap();
|
| 213 |
|
- |
assert_eq!(committed_hlc(&conn, "tags", "r1"), Some(newer));
|
|
225 |
+ |
assert_eq!(committed_hlc(&conn, "tags", "r1").unwrap(), Some(newer));
|
| 214 |
226 |
|
// A stale remote (older HLC) must be recognisable as not-newer.
|
| 215 |
|
- |
assert!(older < committed_hlc(&conn, "tags", "r1").unwrap());
|
|
227 |
+ |
assert!(older < committed_hlc(&conn, "tags", "r1").unwrap().unwrap());
|
| 216 |
228 |
|
// Upsert keeps the latest.
|
| 217 |
229 |
|
set_committed(&conn, "tags", "r1", &older).unwrap();
|
| 218 |
|
- |
assert_eq!(committed_hlc(&conn, "tags", "r1"), Some(older));
|
|
230 |
+ |
assert_eq!(committed_hlc(&conn, "tags", "r1").unwrap(), Some(older));
|
|
231 |
+ |
}
|
|
232 |
+ |
|
|
233 |
+ |
#[test]
|
|
234 |
+ |
fn committed_hlc_propagates_query_error() {
|
|
235 |
+ |
// A genuine read failure must surface as an error, not collapse to "no
|
|
236 |
+ |
// committed HLC" (which gates a stale remote change *in*).
|
|
237 |
+ |
let conn = mem();
|
|
238 |
+ |
conn.execute_batch("DROP TABLE hlc_ledger").unwrap();
|
|
239 |
+ |
assert!(committed_hlc(&conn, "tags", "r1").is_err());
|
|
240 |
+ |
}
|
|
241 |
+ |
|
|
242 |
+ |
#[test]
|
|
243 |
+ |
fn committed_hlc_degrades_corrupt_row_to_none() {
|
|
244 |
+ |
// A corrupt stored HLC is not a query error: it degrades to None (with a
|
|
245 |
+ |
// warning) so one bad ledger row can't wedge the whole sync.
|
|
246 |
+ |
let conn = mem();
|
|
247 |
+ |
conn.execute(
|
|
248 |
+ |
"INSERT INTO hlc_ledger (table_name, row_id, hlc) VALUES ('tags', 'r1', ?1)",
|
|
249 |
+ |
rusqlite::params!["{not valid json"],
|
|
250 |
+ |
)
|
|
251 |
+ |
.unwrap();
|
|
252 |
+ |
assert_eq!(committed_hlc(&conn, "tags", "r1").unwrap(), None);
|
| 219 |
253 |
|
}
|
| 220 |
254 |
|
|
| 221 |
255 |
|
#[test]
|