1use crate::{Error, Result};
2use pulsify_protocol::{
3 validate_vector, PatchRow, VersionVector, MAX_MESSAGE_ROWS, MAX_PATCH_BYTES, MAX_SAFE_INTEGER,
4 MAX_VECTOR_ENTRIES,
5};
6use rusqlite::{params, Connection, OptionalExtension, Transaction, TransactionBehavior};
7use std::collections::{BTreeMap, BTreeSet};
8use std::path::Path;
9use std::time::Duration;
10
11pub const SCHEMA: &str = "
13CREATE TABLE IF NOT EXISTS pulsify_op_rows (
14 doc_id TEXT NOT NULL, sid INTEGER NOT NULL, time_start INTEGER NOT NULL,
15 span INTEGER NOT NULL CHECK(span > 0), time_end INTEGER GENERATED ALWAYS AS (time_start + span) STORED,
16 dependencies TEXT NOT NULL, operations BLOB NOT NULL, ready INTEGER NOT NULL DEFAULT 0,
17 PRIMARY KEY(doc_id, sid, time_start)
18) WITHOUT ROWID;
19CREATE INDEX IF NOT EXISTS pulsify_rows_frontier ON pulsify_op_rows(doc_id, sid, time_end);
20CREATE INDEX IF NOT EXISTS pulsify_rows_pending ON pulsify_op_rows(doc_id) WHERE ready = 0;
21CREATE TABLE IF NOT EXISTS pulsify_doc_clocks (
22 doc_id TEXT NOT NULL, sid INTEGER NOT NULL, clock INTEGER NOT NULL,
23 PRIMARY KEY(doc_id, sid)
24) WITHOUT ROWID;
25CREATE VIEW IF NOT EXISTS pulsify_dangling_op_rows AS SELECT * FROM pulsify_op_rows WHERE ready = 0;
26";
27
28#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
29pub struct AppendResult {
30 pub inserted: usize,
31 pub ready: usize,
32 pub pending: usize,
33}
34
35pub struct SqliteStore {
38 db: Connection,
39}
40
41impl SqliteStore {
42 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
44 let db = Connection::open(path)?;
45 db.busy_timeout(Duration::from_secs(5))?;
46 db.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL;")?;
47 Self::new(db)
48 }
49 pub fn open_in_memory() -> Result<Self> {
50 Self::new(Connection::open_in_memory()?)
51 }
52 pub fn new(db: Connection) -> Result<Self> {
54 db.execute_batch(SCHEMA)?;
55 Ok(Self { db })
56 }
57 pub fn connection(&self) -> &Connection {
58 &self.db
59 }
60
61 pub fn append(&mut self, rows: &[PatchRow]) -> Result<AppendResult> {
64 let Some(doc_id) = validate_rows(rows)? else {
65 return Ok(AppendResult::default());
66 };
67 let tx = self
68 .db
69 .transaction_with_behavior(TransactionBehavior::Immediate)?;
70 let result = append_rows(&tx, doc_id, rows)?;
71 tx.commit()?;
72 Ok(result)
73 }
74
75 pub fn vector(&self, doc_id: &str) -> Result<VersionVector> {
76 validate_doc_id(doc_id)?;
77 read_vector(&self.db, doc_id)
78 }
79
80 pub fn missing(&self, doc_id: &str, remote: &VersionVector) -> Result<Vec<PatchRow>> {
82 validate_doc_id(doc_id)?;
83 validate_vector(remote)?;
84 read_missing(&self.db, doc_id, remote)
85 }
86
87 pub fn sync_state(
89 &mut self,
90 doc_id: &str,
91 remote: &VersionVector,
92 ) -> Result<(Vec<PatchRow>, VersionVector)> {
93 validate_doc_id(doc_id)?;
94 validate_vector(remote)?;
95 let tx = self.db.transaction()?;
96 let rows = read_missing(&tx, doc_id, remote)?;
97 let vector = read_vector(&tx, doc_id)?;
98 tx.commit()?;
99 Ok((rows, vector))
100 }
101}
102
103pub fn validate_doc_id(doc_id: &str) -> Result<()> {
104 if doc_id.is_empty() || doc_id.contains('\0') || doc_id.encode_utf16().count() > 1024 {
105 return Err(Error::new("INVALID_MESSAGE", "Invalid document ID"));
106 }
107 Ok(())
108}
109
110fn validate_rows(rows: &[PatchRow]) -> Result<Option<&str>> {
111 if rows.len() > MAX_MESSAGE_ROWS {
112 return Err(Error::new("LIMIT_EXCEEDED", "Too many patch rows"));
113 }
114 let Some(first) = rows.first() else {
115 return Ok(None);
116 };
117 let doc_id = first.doc_id.as_str();
118 validate_doc_id(doc_id)?;
119 for row in rows {
120 validate_vector(&row.dependencies)?;
121 if row.doc_id != doc_id
122 || row.sid < 65536
123 || row.sid > MAX_SAFE_INTEGER
124 || row.clock < 1
125 || row.span < 1
126 || row.clock.saturating_add(row.span) > MAX_SAFE_INTEGER
127 || row.operations.is_empty()
128 || row.operations.len() > MAX_PATCH_BYTES
129 || own_clock(&row.dependencies, row.sid) > row.clock
130 {
131 return Err(Error::new("INVALID_MESSAGE", "Invalid storage segment"));
132 }
133 }
134 Ok(Some(doc_id))
135}
136
137fn own_clock(vv: &VersionVector, sid: u64) -> u64 {
138 vv.iter()
139 .find(|(id, _)| *id == sid)
140 .map_or(0, |(_, clock)| *clock)
141}
142
143fn conflict(message: &str) -> Error {
144 Error::new("CONFLICT", message)
145}
146
147struct Segment {
149 clock: u64,
150 span: u64,
151 dependencies: VersionVector,
152}
153
154fn assert_compatible(row: &PatchRow, other: &Segment) -> Result<()> {
155 if row.clock < other.clock + other.span && other.clock < row.clock + row.span {
156 return Err(conflict("Overlapping immutable storage segments"));
157 }
158 let (previous_end, next_dependency) = if row.clock < other.clock {
159 (
160 row.clock + row.span,
161 own_clock(&other.dependencies, row.sid),
162 )
163 } else {
164 (
165 other.clock + other.span,
166 own_clock(&row.dependencies, row.sid),
167 )
168 };
169 if next_dependency < previous_end {
170 return Err(conflict("Actor history fork"));
171 }
172 Ok(())
173}
174
175fn append_rows(tx: &Transaction<'_>, doc_id: &str, rows: &[PatchRow]) -> Result<AppendResult> {
176 let mut get = tx.prepare_cached(
177 "SELECT span,dependencies,operations FROM pulsify_op_rows WHERE doc_id=?1 AND sid=?2 AND time_start=?3",
178 )?;
179 let mut neighbours = tx.prepare_cached(
180 "SELECT time_start,span,dependencies FROM pulsify_op_rows WHERE doc_id=?1 AND sid=?2 AND
181 (time_start=(SELECT MAX(time_start) FROM pulsify_op_rows WHERE doc_id=?1 AND sid=?2 AND time_start<?3)
182 OR time_start=(SELECT MIN(time_start) FROM pulsify_op_rows WHERE doc_id=?1 AND sid=?2 AND time_start>?3))",
183 )?;
184 let mut insert = tx.prepare_cached(
185 "INSERT INTO pulsify_op_rows(doc_id,sid,time_start,span,dependencies,operations) VALUES(?,?,?,?,?,?)",
186 )?;
187 let mut inserted = 0;
188 for row in rows {
189 let (sid, clock) = (row.sid as i64, row.clock as i64);
190 let existing = get
191 .query_row(params![doc_id, sid, clock], |r| {
192 Ok((
193 r.get::<_, i64>(0)?,
194 r.get::<_, String>(1)?,
195 r.get::<_, Vec<u8>>(2)?,
196 ))
197 })
198 .optional()?;
199 if let Some((span, dependencies, operations)) = existing {
200 let dependencies: VersionVector = serde_json::from_str(&dependencies)?;
201 if span as u64 != row.span
202 || dependencies != row.dependencies
203 || operations != row.operations
204 {
205 return Err(conflict("Conflicting storage segment"));
206 }
207 continue;
208 }
209 let found = neighbours
210 .query_map(params![doc_id, sid, clock], |r| {
211 Ok((
212 r.get::<_, i64>(0)?,
213 r.get::<_, i64>(1)?,
214 r.get::<_, String>(2)?,
215 ))
216 })?
217 .collect::<rusqlite::Result<Vec<_>>>()?;
218 for (clock, span, dependencies) in found {
219 let segment = Segment {
220 clock: clock as u64,
221 span: span as u64,
222 dependencies: serde_json::from_str(&dependencies)?,
223 };
224 assert_compatible(row, &segment)?;
225 }
226 insert.execute(params![
227 doc_id,
228 sid,
229 clock,
230 row.span as i64,
231 serde_json::to_string(&row.dependencies)?,
232 row.operations,
233 ])?;
234 inserted += 1;
235 }
236 let mut frontier: BTreeMap<u64, u64> = read_vector(tx, doc_id)?.into_iter().collect();
237 let mut pending = tx
238 .prepare_cached(
239 "SELECT sid,time_start,time_end,dependencies FROM pulsify_op_rows WHERE doc_id=? AND ready=0 ORDER BY sid,time_start",
240 )?
241 .query_map([doc_id], |r| {
242 Ok((
243 r.get::<_, i64>(0)? as u64,
244 r.get::<_, i64>(1)? as u64,
245 r.get::<_, i64>(2)? as u64,
246 r.get::<_, String>(3)?,
247 ))
248 })?
249 .collect::<rusqlite::Result<Vec<_>>>()?
250 .into_iter()
251 .map(|(sid, start, end, dependencies)| {
252 Ok((sid, start, end, serde_json::from_str(&dependencies)?))
253 })
254 .collect::<Result<Vec<(u64, u64, u64, VersionVector)>>>()?;
255 let actors: BTreeSet<u64> = frontier
256 .keys()
257 .copied()
258 .chain(pending.iter().map(|row| row.0))
259 .collect();
260 if actors.len() > MAX_VECTOR_ENTRIES {
261 return Err(Error::new(
262 "LIMIT_EXCEEDED",
263 "Document actor limit exceeded",
264 ));
265 }
266 let mut promote = tx.prepare_cached(
267 "UPDATE pulsify_op_rows SET ready=1 WHERE doc_id=? AND sid=? AND time_start=?",
268 )?;
269 let mut ready = 0;
270 let mut changed_actors = BTreeSet::new();
271 loop {
272 let mut changed = false;
273 let mut waiting = Vec::new();
274 for row in pending {
275 let (sid, start, end, ref dependencies) = row;
276 let contiguous = start == frontier.get(&sid).copied().unwrap_or(1);
277 let covered = dependencies
278 .iter()
279 .all(|(actor, clock)| frontier.get(actor).copied().unwrap_or(0) >= *clock);
280 if !contiguous || !covered {
281 waiting.push(row);
282 continue;
283 }
284 promote.execute(params![doc_id, sid as i64, start as i64])?;
285 let clock = frontier.entry(sid).or_insert(0);
286 *clock = (*clock).max(end);
287 changed_actors.insert(sid);
288 ready += 1;
289 changed = true;
290 }
291 pending = waiting;
292 if !changed {
293 break;
294 }
295 }
296 let mut advance = tx.prepare_cached(
297 "INSERT INTO pulsify_doc_clocks(doc_id,sid,clock) VALUES(?,?,?) ON CONFLICT(doc_id,sid) DO UPDATE SET clock=MAX(clock,excluded.clock)",
298 )?;
299 for sid in changed_actors {
300 advance.execute(params![doc_id, sid as i64, frontier[&sid] as i64])?;
301 }
302 Ok(AppendResult {
303 inserted,
304 ready,
305 pending: pending.len(),
306 })
307}
308
309fn read_vector(db: &Connection, doc_id: &str) -> Result<VersionVector> {
310 Ok(db
311 .prepare_cached("SELECT sid,clock FROM pulsify_doc_clocks WHERE doc_id=? ORDER BY sid")?
312 .query_map([doc_id], |r| {
313 Ok((r.get::<_, i64>(0)? as u64, r.get::<_, i64>(1)? as u64))
314 })?
315 .collect::<rusqlite::Result<_>>()?)
316}
317
318fn read_missing(db: &Connection, doc_id: &str, remote: &VersionVector) -> Result<Vec<PatchRow>> {
319 let map = |r: &rusqlite::Row<'_>| {
320 Ok((
321 r.get::<_, i64>(0)?,
322 r.get::<_, i64>(1)?,
323 r.get::<_, i64>(2)?,
324 r.get::<_, String>(3)?,
325 r.get::<_, Vec<u8>>(4)?,
326 ))
327 };
328 let found = if remote.is_empty() {
329 db.prepare_cached(
330 "SELECT sid,time_start,span,dependencies,operations FROM pulsify_op_rows WHERE doc_id=? AND ready=1 ORDER BY sid,time_start",
331 )?
332 .query_map([doc_id], map)?
333 .collect::<rusqlite::Result<Vec<_>>>()?
334 } else {
335 db.prepare_cached(
336 "WITH remote(sid,clock) AS (SELECT json_extract(value,'$[0]'),json_extract(value,'$[1]') FROM json_each(?1))
337 SELECT o.sid,o.time_start,o.span,o.dependencies,o.operations FROM pulsify_op_rows o LEFT JOIN remote r ON r.sid=o.sid
338 WHERE o.doc_id=?2 AND o.ready=1 AND o.time_end>COALESCE(r.clock,0) ORDER BY o.sid,o.time_start",
339 )?
340 .query_map(params![serde_json::to_string(remote)?, doc_id], map)?
341 .collect::<rusqlite::Result<Vec<_>>>()?
342 };
343 found
344 .into_iter()
345 .map(|(sid, clock, span, dependencies, operations)| {
346 Ok(PatchRow {
347 doc_id: doc_id.to_owned(),
348 sid: sid as u64,
349 clock: clock as u64,
350 span: span as u64,
351 dependencies: serde_json::from_str(&dependencies)?,
352 operations,
353 })
354 })
355 .collect()
356}
357
358#[cfg(test)]
359pub(crate) mod tests {
360 use super::*;
361
362 pub const A: u64 = 65536;
363 pub const B: u64 = 65537;
364
365 pub fn row(sid: u64, clock: u64, span: u64, dependencies: &[(u64, u64)]) -> PatchRow {
366 PatchRow {
367 doc_id: "doc".into(),
368 sid,
369 clock,
370 span,
371 dependencies: dependencies.to_vec(),
372 operations: vec![sid as u8, clock as u8, span as u8],
373 }
374 }
375 pub fn next(sid: u64, clock: u64, span: u64) -> PatchRow {
377 if clock == 1 {
378 row(sid, 1, span, &[])
379 } else {
380 row(sid, clock, span, &[(sid, clock)])
381 }
382 }
383 fn count(store: &SqliteStore, sql: &str) -> i64 {
384 store.connection().query_row(sql, [], |r| r.get(0)).unwrap()
385 }
386
387 #[test]
388 fn stores_and_reads_contiguous_rows() {
389 let mut store = SqliteStore::open_in_memory().unwrap();
390 let result = store
391 .append(&[next(A, 1, 2), next(A, 3, 1), next(B, 1, 4)])
392 .unwrap();
393 assert_eq!(
394 result,
395 AppendResult {
396 inserted: 3,
397 ready: 3,
398 pending: 0
399 }
400 );
401 assert_eq!(store.vector("doc").unwrap(), vec![(A, 4), (B, 5)]);
402 assert_eq!(store.missing("doc", &vec![]).unwrap().len(), 3);
403 let missing = store.missing("doc", &vec![(A, 3)]).unwrap();
404 assert_eq!(missing, vec![next(A, 3, 1), next(B, 1, 4)]);
405 assert!(store
406 .missing("doc", &vec![(A, 4), (B, 5)])
407 .unwrap()
408 .is_empty());
409 assert!(store.missing("other", &vec![]).unwrap().is_empty());
410 assert!(store.vector("other").unwrap().is_empty());
411 let stored: String = store
412 .connection()
413 .query_row(
414 "SELECT dependencies FROM pulsify_op_rows WHERE sid=? AND time_start=3",
415 [A as i64],
416 |r| r.get(0),
417 )
418 .unwrap();
419 assert_eq!(stored, "[[65536,3]]");
420 }
421
422 #[test]
423 fn exact_duplicates_are_idempotent() {
424 let mut store = SqliteStore::open_in_memory().unwrap();
425 store.append(&[next(A, 1, 2)]).unwrap();
426 let again = store.append(&[next(A, 1, 2), next(A, 1, 2)]).unwrap();
427 assert_eq!(again.inserted, 0);
428 assert_eq!(count(&store, "SELECT COUNT(*) FROM pulsify_op_rows"), 1);
429 assert_eq!(store.append(&[]).unwrap(), AppendResult::default());
430 }
431
432 #[test]
433 fn conflicts_roll_back_the_whole_batch() {
434 let mut store = SqliteStore::open_in_memory().unwrap();
435 store.append(&[next(A, 1, 2)]).unwrap();
436 let mut changed = next(A, 1, 2);
437 changed.operations = vec![9];
438 let cases = [
439 (changed, "Conflicting storage segment"),
440 (next(A, 2, 2), "Overlapping immutable storage segments"),
441 (row(A, 3, 1, &[(A, 2)]), "Actor history fork"),
442 ];
443 for (bad, message) in cases {
444 let error = store.append(&[next(B, 1, 1), bad]).unwrap_err();
445 assert_eq!((error.code, error.message.as_str()), ("CONFLICT", message));
446 assert_eq!(count(&store, "SELECT COUNT(*) FROM pulsify_op_rows"), 1);
447 assert_eq!(store.vector("doc").unwrap(), vec![(A, 3)]);
448 }
449 store.append(&[row(A, 5, 1, &[(A, 3)])]).unwrap();
451 let error = store.append(&[next(A, 3, 2)]).unwrap_err();
452 assert_eq!(error.message, "Actor history fork");
453 let error = store
454 .append(&[next(A, 6, 1), row(A, 7, 1, &[(A, 4)])])
455 .unwrap_err();
456 assert_eq!(error.message, "Actor history fork");
457 assert_eq!(count(&store, "SELECT COUNT(*) FROM pulsify_op_rows"), 2);
458 }
459
460 #[test]
461 fn gaps_and_missing_dependencies_stay_pending_until_promoted() {
462 let mut store = SqliteStore::open_in_memory().unwrap();
463 let gap = store.append(&[next(A, 3, 2)]).unwrap();
464 assert_eq!((gap.inserted, gap.ready, gap.pending), (1, 0, 1));
465 let dependent = store.append(&[row(B, 1, 1, &[(A, 5)])]).unwrap();
466 assert_eq!((dependent.ready, dependent.pending), (0, 2));
467 assert!(store.vector("doc").unwrap().is_empty());
468 assert!(store.missing("doc", &vec![]).unwrap().is_empty());
469 assert_eq!(
470 count(&store, "SELECT COUNT(*) FROM pulsify_dangling_op_rows"),
471 2
472 );
473 let filled = store.append(&[next(A, 1, 2)]).unwrap();
474 assert_eq!((filled.inserted, filled.ready, filled.pending), (1, 3, 0));
475 assert_eq!(store.vector("doc").unwrap(), vec![(A, 5), (B, 2)]);
476 assert_eq!(
477 store.missing("doc", &vec![]).unwrap(),
478 vec![next(A, 1, 2), next(A, 3, 2), row(B, 1, 1, &[(A, 5)])]
479 );
480 assert_eq!(
481 count(&store, "SELECT COUNT(*) FROM pulsify_op_rows WHERE ready=0"),
482 0
483 );
484 }
485
486 #[test]
487 fn rejects_invalid_rows_and_documents() {
488 let mut store = SqliteStore::open_in_memory().unwrap();
489 let mut other_doc = next(B, 1, 1);
490 other_doc.doc_id = "other".into();
491 let mut empty = next(A, 1, 1);
492 empty.operations.clear();
493 for rows in [
494 vec![next(A, 1, 1), other_doc],
495 vec![row(1, 1, 1, &[])],
496 vec![row(A, 0, 1, &[])],
497 vec![row(A, 1, 0, &[])],
498 vec![row(A, 2, 1, &[(A, 3)])],
499 vec![row(A, 1, 1, &[(B, 1), (A, 1)])],
500 vec![empty],
501 ] {
502 assert_eq!(store.append(&rows).unwrap_err().code, "INVALID_MESSAGE");
503 }
504 let too_many: Vec<_> = (1..=MAX_MESSAGE_ROWS as u64 + 1)
505 .map(|clock| next(A, clock, 1))
506 .collect();
507 assert_eq!(store.append(&too_many).unwrap_err().code, "LIMIT_EXCEEDED");
508 assert_eq!(store.vector("").unwrap_err().code, "INVALID_MESSAGE");
509 assert_eq!(store.vector("a\0b").unwrap_err().code, "INVALID_MESSAGE");
510 assert_eq!(
511 store.vector(&"x".repeat(1025)).unwrap_err().code,
512 "INVALID_MESSAGE"
513 );
514 assert_eq!(
515 store
516 .missing("doc", &vec![(B, 1), (A, 1)])
517 .unwrap_err()
518 .code,
519 "INVALID_MESSAGE"
520 );
521 assert_eq!(count(&store, "SELECT COUNT(*) FROM pulsify_op_rows"), 0);
522 }
523
524 #[test]
525 fn limits_the_number_of_actors() {
526 let mut store = SqliteStore::open_in_memory().unwrap();
527 let rows: Vec<_> = (0..MAX_VECTOR_ENTRIES as u64)
528 .map(|i| next(A + i, 1, 1))
529 .collect();
530 store.append(&rows).unwrap();
531 let error = store
532 .append(&[next(A + MAX_VECTOR_ENTRIES as u64, 2, 1)])
533 .unwrap_err();
534 assert_eq!(error.code, "LIMIT_EXCEEDED");
535 assert_eq!(store.vector("doc").unwrap().len(), MAX_VECTOR_ENTRIES);
536 assert_eq!(
537 count(&store, "SELECT COUNT(*) FROM pulsify_op_rows"),
538 MAX_VECTOR_ENTRIES as i64
539 );
540 }
541
542 #[test]
543 fn persists_across_reopen() {
544 let path = std::env::temp_dir().join(format!(
545 "pulsify-sqlite-{}-{:?}.sqlite",
546 std::process::id(),
547 std::thread::current().id()
548 ));
549 let _ = std::fs::remove_file(&path);
550 {
551 let mut store = SqliteStore::open(&path).unwrap();
552 store.append(&[next(A, 1, 3), next(A, 5, 1)]).unwrap();
553 }
554 let mut store = SqliteStore::open(&path).unwrap();
555 assert_eq!(store.vector("doc").unwrap(), vec![(A, 4)]);
556 assert_eq!(store.append(&[next(A, 4, 1)]).unwrap().ready, 2);
557 let (rows, vector) = store.sync_state("doc", &vec![(A, 4)]).unwrap();
558 assert_eq!(vector, vec![(A, 6)]);
559 assert_eq!(rows, vec![next(A, 4, 1), next(A, 5, 1)]);
560 drop(store);
561 for suffix in ["", "-wal", "-shm"] {
562 let _ = std::fs::remove_file(format!("{}{suffix}", path.display()));
563 }
564 }
565}