1use super::{DbError, StoreDatabase, StoreSession};
2use coven_protocol::store_commit::{
3 ObjectHash, StoreBatchCommitRef, StoreDeviceExclusionRef, StoreDeviceHeadRef,
4};
5use coven_protocol::write::WriteId;
6use rusqlite::OptionalExtension;
7use std::collections::BTreeSet;
8
9#[derive(Clone, Copy, Debug)]
10pub enum AuthorExclusionLocatorTamper {
11 Missing,
12 ExclusionReference,
13 AcceptedCut,
14 ActivationCommit,
15 ActivationHead,
16}
17
18struct PreparedWriteTransfer {
19 write: (String, String, String, String, String, String),
20 partitions: Vec<(String, Option<String>, String)>,
21 packages: Vec<(String, String)>,
22 blobs: Vec<(String, String, String, Option<String>)>,
23 remotes: Vec<(String, String)>,
24 payload_claims: Vec<(String, BTreeSet<ObjectHash>)>,
25 payloads: Vec<(ObjectHash, Vec<u8>)>,
26}
27
28impl StoreSession<'_> {
29 fn seed_prepared_audience_write_for_test(
30 &self,
31 write_id: &WriteId,
32 changeset_hash: ObjectHash,
33 ) -> Result<(), DbError> {
34 let base = serde_json::to_string(&crate::StoreWriteBase {
35 dependencies: std::collections::BTreeMap::new(),
36 })
37 .map_err(|error| DbError::context("serialize test Store write base", error))?;
38 crate::DatabaseTestSql::for_store(self.conn, self.store_dir).transaction(|transaction| {
39 transaction
40 .execute(
41 "INSERT INTO store_writes
42 (write_id, status, affected_rows, changeset_hash, base, blob_facts)
43 VALUES (?1, '\"pending\"', '[]', ?2, ?3, '{\"blobs\":[]}')",
44 rusqlite::params![write_id.as_str(), changeset_hash.to_string(), base],
45 )
46 .map_err(DbError::from)?;
47 transaction.set_payload_owner_claims(
48 &crate::payload_store::store_write_owner_key(write_id),
49 &BTreeSet::from([changeset_hash]),
50 )
51 })
52 }
53
54 fn persist_prepared_audience_objects_for_test(
55 &self,
56 write_id: &WriteId,
57 remotes: &[coven_protocol::remote_object::RemoteObjectRecord],
58 packages: &[crate::PreparedAudiencePackage],
59 blobs: &[crate::PreparedAudienceBlob],
60 ) -> Result<(), DbError> {
61 crate::DatabaseTestSql::for_store(self.conn, self.store_dir).transaction(|transaction| {
62 for remote in remotes {
63 transaction
64 .execute(
65 "INSERT INTO remote_objects (object_id, state) VALUES (?1, ?2)",
66 rusqlite::params![
67 remote.object_id().to_string(),
68 serde_json::to_string(remote).map_err(|error| {
69 DbError::context("serialize prepared remote object", error)
70 })?,
71 ],
72 )
73 .map_err(DbError::from)?;
74 }
75 transaction.persist_prepared_audience_objects(self.store_dir, write_id, packages, blobs)
76 })
77 }
78
79 pub(crate) fn capture_test_changeset(&self, statements: &[String]) -> Result<Vec<u8>, DbError> {
80 let tables = self
81 .synced_tables
82 .iter()
83 .map(|table| table.name().to_string())
84 .collect::<Vec<_>>();
85 crate::DatabaseTestSql::for_store(self.conn, self.store_dir)
86 .capture_changeset(&tables, statements)
87 }
88
89 pub(crate) fn apply_test_changeset(&self, bytes: &[u8]) -> Result<crate::ApplyResult, DbError> {
90 crate::resolve_and_apply_changeset(
91 self.conn,
92 self.store_dir,
93 bytes,
94 self.synced_tables,
95 self.hlc.wall_now_ms(),
96 )
97 }
98
99 fn export_prepared_write(&self, write_id: &WriteId) -> Result<PreparedWriteTransfer, DbError> {
100 let connection = self.conn;
101 let write = connection
102 .query_row(
103 "SELECT status, affected_rows, changeset_hash,
104 base, blob_facts, prepared
105 FROM store_writes WHERE write_id = ?1",
106 [write_id.as_str()],
107 |row| {
108 Ok((
109 row.get(0)?,
110 row.get(1)?,
111 row.get(2)?,
112 row.get(3)?,
113 row.get(4)?,
114 row.get(5)?,
115 ))
116 },
117 )
118 .map_err(DbError::from)?;
119 let partitions = {
120 let mut statement = connection
121 .prepare(
122 "SELECT audience, control_coord, changeset_hash
123 FROM store_write_partitions WHERE write_id = ?1 ORDER BY audience",
124 )
125 .map_err(DbError::from)?;
126 let rows = statement
127 .query_map([write_id.as_str()], |row| {
128 Ok((row.get(0)?, row.get(1)?, row.get(2)?))
129 })
130 .map_err(DbError::from)?
131 .collect::<Result<Vec<_>, _>>()
132 .map_err(DbError::from)?;
133 rows
134 };
135 let packages = {
136 let mut statement = connection
137 .prepare(
138 "SELECT audience, remote_object_id
139 FROM store_write_packages WHERE write_id = ?1 ORDER BY audience",
140 )
141 .map_err(DbError::from)?;
142 let rows = statement
143 .query_map([write_id.as_str()], |row| Ok((row.get(0)?, row.get(1)?)))
144 .map_err(DbError::from)?
145 .collect::<Result<Vec<_>, _>>()
146 .map_err(DbError::from)?;
147 rows
148 };
149 let blobs = {
150 let mut statement = connection
151 .prepare(
152 "SELECT audience, locator_hash, remote_object_id, spool_path
153 FROM store_write_blobs WHERE write_id = ?1
154 ORDER BY audience, remote_object_id",
155 )
156 .map_err(DbError::from)?;
157 let rows = statement
158 .query_map([write_id.as_str()], |row| {
159 Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))
160 })
161 .map_err(DbError::from)?
162 .collect::<Result<Vec<_>, _>>()
163 .map_err(DbError::from)?;
164 rows
165 };
166 let remotes = {
167 let mut statement = connection
168 .prepare("SELECT object_id, state FROM remote_objects ORDER BY object_id")
169 .map_err(DbError::from)?;
170 let rows = statement
171 .query_map([], |row| {
172 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
173 })
174 .map_err(DbError::from)?
175 .collect::<Result<Vec<_>, _>>()
176 .map_err(DbError::from)?;
177 rows
178 };
179 let mut owner_keys = vec![crate::payload_store::store_write_owner_key(write_id)];
180 for (object_id, _) in &remotes {
181 let object_id = object_id
182 .parse()
183 .map_err(|error| DbError::context("parse transferred remote object id", error))?;
184 owner_keys.push(crate::payload_store::remote_object_owner_key(object_id));
185 }
186 let mut payload_claims = Vec::new();
187 let mut payload_hashes = BTreeSet::new();
188 for owner_key in owner_keys {
189 let claims = {
190 let mut statement = connection
191 .prepare(
192 "SELECT payload_hash FROM payload_owners
193 WHERE owner_key = ?1 ORDER BY payload_hash",
194 )
195 .map_err(DbError::from)?;
196 let claims = statement
197 .query_map([&owner_key], |row| row.get::<_, String>(0))
198 .map_err(DbError::from)?
199 .map(|encoded| {
200 encoded.map_err(DbError::from)?.parse().map_err(|error| {
201 DbError::context("parse transferred payload hash", error)
202 })
203 })
204 .collect::<Result<BTreeSet<ObjectHash>, DbError>>()?;
205 claims
206 };
207 payload_hashes.extend(claims.iter().copied());
208 if !claims.is_empty() {
209 payload_claims.push((owner_key, claims));
210 }
211 }
212 let payloads = payload_hashes
213 .into_iter()
214 .map(|hash| {
215 Ok((
216 hash,
217 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir)
218 .payload(hash)?,
219 ))
220 })
221 .collect::<Result<Vec<_>, DbError>>()?;
222 Ok(PreparedWriteTransfer {
223 write,
224 partitions,
225 packages,
226 blobs,
227 remotes,
228 payload_claims,
229 payloads,
230 })
231 }
232
233 fn import_prepared_write(
234 &self,
235 write_id: &WriteId,
236 transfer: PreparedWriteTransfer,
237 ) -> Result<(), DbError> {
238 let transaction = self.conn.unchecked_transaction().map_err(DbError::from)?;
239 for (expected_hash, bytes) in &transfer.payloads {
240 let actual_hash =
241 crate::payload_store::write_payload_blocking(&transaction, self.store_dir, bytes)?;
242 if actual_hash != *expected_hash {
243 return Err(DbError::Message(format!(
244 "transferred payload expected {expected_hash} but stored as {actual_hash}"
245 )));
246 }
247 }
248 for (object_id, state) in transfer.remotes {
249 let imported = transaction
250 .execute(
251 "INSERT INTO remote_objects (object_id, state) VALUES (?1, ?2)
252 ON CONFLICT(object_id) DO UPDATE SET state = excluded.state
253 WHERE remote_objects.state = excluded.state",
254 (object_id, state),
255 )
256 .map_err(DbError::from)?;
257 if imported != 1 {
258 return Err(DbError::Message(
259 "prepared write remote object conflicts with restored state".to_string(),
260 ));
261 }
262 }
263 transaction
264 .execute(
265 "INSERT INTO store_writes
266 (write_id, status, affected_rows, changeset_hash,
267 base, blob_facts, prepared)
268 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
269 rusqlite::params![
270 write_id.as_str(),
271 transfer.write.0,
272 transfer.write.1,
273 transfer.write.2,
274 transfer.write.3,
275 transfer.write.4,
276 transfer.write.5,
277 ],
278 )
279 .map_err(DbError::from)?;
280 for (audience, control, changeset_hash) in transfer.partitions {
281 transaction
282 .execute(
283 "INSERT INTO store_write_partitions
284 (write_id, audience, control_coord, changeset_hash)
285 VALUES (?1, ?2, ?3, ?4)",
286 rusqlite::params![write_id.as_str(), audience, control, changeset_hash],
287 )
288 .map_err(DbError::from)?;
289 }
290 for (audience, object_id) in transfer.packages {
291 transaction
292 .execute(
293 "INSERT INTO store_write_packages
294 (write_id, audience, remote_object_id) VALUES (?1, ?2, ?3)",
295 rusqlite::params![write_id.as_str(), audience, object_id],
296 )
297 .map_err(DbError::from)?;
298 }
299 for (audience, locator_hash, object_id, spool_path) in transfer.blobs {
300 transaction
301 .execute(
302 "INSERT INTO store_write_blobs
303 (write_id, audience, locator_hash, remote_object_id, spool_path)
304 VALUES (?1, ?2, ?3, ?4, ?5)",
305 rusqlite::params![
306 write_id.as_str(),
307 audience,
308 locator_hash,
309 object_id,
310 spool_path
311 ],
312 )
313 .map_err(DbError::from)?;
314 }
315 for (owner_key, claims) in transfer.payload_claims {
316 crate::payload_store::set_payload_owner_claims_on(&transaction, &owner_key, &claims)?;
317 }
318 transaction.commit().map_err(DbError::from)
319 }
320
321 fn register_external_blob_for_test(
322 &self,
323 reference: &coven_protocol::blob::RowBlobRef,
324 path: &std::path::Path,
325 ) -> Result<(), DbError> {
326 crate::DatabaseTestSql::new(self.conn).register_external_blob(reference, path)
327 }
328
329 fn enqueue_blob_upload_for_test(
330 &self,
331 root_table: &str,
332 root_id: &str,
333 reference: &coven_protocol::blob::RowBlobRef,
334 source_path: &std::path::Path,
335 created_at: &str,
336 ) -> Result<(), DbError> {
337 crate::DatabaseTestSql::new(self.conn).enqueue_blob_upload(
338 root_table,
339 root_id,
340 &format!("{root_table}/{root_id}"),
341 reference,
342 source_path,
343 false,
344 created_at,
345 )
346 }
347
348 fn cleanup_intent_count_for_test(
349 &self,
350 namespace: &str,
351 blob_id: &str,
352 ) -> Result<i64, DbError> {
353 self.conn
354 .query_row(
355 "SELECT COUNT(*) FROM local_cleanup_intents
356 WHERE namespace = ?1 AND blob_id = ?2",
357 (namespace, blob_id),
358 |row| row.get(0),
359 )
360 .map_err(DbError::from)
361 }
362
363 fn coven_table_exists_for_test(
364 &self,
365 table: crate::DatabaseTestTable,
366 ) -> Result<bool, DbError> {
367 self.conn
368 .query_row(
369 "SELECT EXISTS(
370 SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?1
371 )",
372 [table.0],
373 |row| row.get(0),
374 )
375 .map_err(DbError::from)
376 }
377
378 fn install_store_write_failure_trigger_for_test(&self) -> Result<(), DbError> {
379 self.conn
380 .execute_batch(
381 "CREATE TRIGGER fail_store_write_journal
382 BEFORE INSERT ON store_writes
383 BEGIN
384 SELECT RAISE(ABORT, 'injected Store write journal failure');
385 END;",
386 )
387 .map_err(DbError::from)
388 }
389
390 fn remove_store_write_failure_trigger_for_test(&self) -> Result<(), DbError> {
391 self.conn
392 .execute_batch("DROP TRIGGER fail_store_write_journal")
393 .map_err(DbError::from)
394 }
395
396 fn write_blob_facts_for_test(&self, write_id: &WriteId) -> Result<String, DbError> {
397 self.conn
398 .query_row(
399 "SELECT blob_facts FROM store_writes WHERE write_id = ?1",
400 [write_id.as_str()],
401 |row| row.get(0),
402 )
403 .map_err(DbError::from)
404 }
405
406 fn install_test_active_circle(&self, label: &str) -> coven_protocol::circle::CircleId {
407 self.install_test_active_circle_with_control(label).0
408 }
409
410 fn install_test_active_circle_with_control(
411 &self,
412 label: &str,
413 ) -> (
414 coven_protocol::circle::CircleId,
415 coven_protocol::circle::CircleControlCoord,
416 ) {
417 let database = crate::DatabaseTestSql::new(self.conn);
418 database.install_test_active_circle(label)
419 }
420
421 fn install_test_inactive_circle(&self, label: &str) -> coven_protocol::circle::CircleId {
422 let database = crate::DatabaseTestSql::new(self.conn);
423 let (circle_id, _) = database.install_test_inactive_circle(label);
424 circle_id
425 }
426
427 fn insert_write_status_for_test(
428 &self,
429 write_id: &WriteId,
430 status: &str,
431 base: &str,
432 ) -> Result<(), DbError> {
433 let transaction = self.conn.unchecked_transaction().map_err(DbError::from)?;
434 let changeset_hash =
435 crate::payload_store::write_payload_blocking(&transaction, self.store_dir, b"")?;
436 let owner_key = crate::payload_store::store_write_owner_key(write_id);
437 transaction
438 .execute(
439 r#"INSERT INTO store_writes
440 (write_id, status, affected_rows, changeset_hash, base, blob_facts)
441 VALUES (?1, ?2, '[]', ?3, ?4, '{"blobs":[]}')"#,
442 (write_id.as_str(), status, changeset_hash.to_string(), base),
443 )
444 .map_err(DbError::from)?;
445 crate::payload_store::set_payload_owner_claims_on(
446 &transaction,
447 &owner_key,
448 &BTreeSet::from([changeset_hash]),
449 )?;
450 transaction.commit().map_err(DbError::from)
451 }
452
453 fn delete_write_for_test(&self, write_id: &WriteId) -> Result<(), DbError> {
454 let transaction = self.conn.unchecked_transaction().map_err(DbError::from)?;
455 crate::payload_store::release_payload_owner_on(
456 &transaction,
457 &crate::payload_store::store_write_owner_key(write_id),
458 )?;
459 transaction
460 .execute(
461 "DELETE FROM store_writes WHERE write_id = ?1",
462 [write_id.as_str()],
463 )
464 .map_err(DbError::from)?;
465 transaction.commit().map_err(DbError::from)
466 }
467
468 fn store_write_partition_for_test(&self, write_id: &WriteId) -> Result<Vec<u8>, DbError> {
469 let encoded: String = self
470 .conn
471 .query_row(
472 "SELECT changeset_hash FROM store_write_partitions
473 WHERE write_id = ?1 AND audience = 'store'",
474 [write_id.as_str()],
475 |row| row.get(0),
476 )
477 .map_err(DbError::from)?;
478 let hash = encoded
479 .parse()
480 .map_err(|error| DbError::context("parse captured changeset hash", error))?;
481 Ok(
482 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir)
483 .payload(hash)?,
484 )
485 }
486
487 fn store_write_journal_counts_for_test(&self) -> Result<(i64, i64), DbError> {
490 let writes: i64 = self
491 .conn
492 .query_row("SELECT COUNT(*) FROM store_writes", [], |row| row.get(0))
493 .map_err(DbError::from)?;
494 let claims: i64 = self
495 .conn
496 .query_row(
497 "SELECT COUNT(*) FROM payload_owners WHERE owner_key LIKE 'store-write:%'",
498 [],
499 |row| row.get(0),
500 )
501 .map_err(DbError::from)?;
502 Ok((writes, claims))
503 }
504
505 fn write_blob_lease_count_for_test(&self, write_id: &WriteId) -> Result<i64, DbError> {
506 self.conn
507 .query_row(
508 "SELECT COUNT(*) FROM store_write_blob_leases WHERE write_id = ?1",
509 [write_id.as_str()],
510 |row| row.get(0),
511 )
512 .map_err(DbError::from)
513 }
514
515 fn latest_materialized_commit_coordinate_for_test(&self) -> Result<(String, u64), DbError> {
516 let (device_id, sequence): (String, i64) = self
517 .conn
518 .query_row(
519 "SELECT device_id, seq
520 FROM materialized_commits
521 ORDER BY seq DESC
522 LIMIT 1",
523 [],
524 |row| Ok((row.get(0)?, row.get(1)?)),
525 )
526 .map_err(DbError::from)?;
527 let sequence = u64::try_from(sequence).map_err(|error| {
528 DbError::context(
529 format!("materialized commit sequence {sequence} is invalid"),
530 error,
531 )
532 })?;
533 Ok((device_id, sequence))
534 }
535
536 fn test_query_optional_text(&mut self, sql: &str) -> Result<Option<String>, DbError> {
548 self.conn
549 .query_row(sql, [], |row| row.get::<_, Option<String>>(0))
550 .optional()
551 .map(Option::flatten)
552 .map_err(DbError::from)
553 }
554
555 fn replay_row_count_for_test(
556 &mut self,
557 root: &coven_protocol::store_commit::StoreRootRef,
558 table: &str,
559 ) -> Result<i64, DbError> {
560 let transaction = self.conn.unchecked_transaction().map_err(DbError::from)?;
561 let replay =
562 crate::store::store_session::StoreTransaction::new(&transaction, self.store_dir)
563 .replay_projection_with_authority(
564 self.verified_store_authority,
565 root,
566 self.blob_decls,
567 self.gates,
568 self.synced_tables,
569 None,
570 &BTreeSet::new(),
571 None,
572 crate::ReplayJournal::Owed,
573 coven_protocol::membership::LocalStoreMembership::Current,
574 )?;
575 let count = replay.row_count(table)?;
576 transaction.rollback().map_err(DbError::from)?;
577 Ok(count)
578 }
579
580 fn compare_circle_bootstrap_replay_with_missing_coverage_for_test(
581 &mut self,
582 root: &coven_protocol::store_commit::StoreRootRef,
583 routing_key: &coven_protocol::circle::RowRoutingKey,
584 historical_id: &str,
585 late_id: &str,
586 ) -> Result<(i64, i64, i64, i64), DbError> {
587 let transaction = self.conn.unchecked_transaction().map_err(DbError::from)?;
588 let retained =
589 crate::store::store_session::StoreTransaction::new(&transaction, self.store_dir)
590 .replay_projection_with_authority(
591 self.verified_store_authority,
592 root,
593 self.blob_decls,
594 self.gates,
595 self.synced_tables,
596 Some(routing_key),
597 &BTreeSet::new(),
598 None,
599 crate::ReplayJournal::Omit,
600 coven_protocol::membership::LocalStoreMembership::Current,
601 )?;
602 let retained_count = retained.document_count(historical_id)?;
603 let retained_late_count = retained.document_count(late_id)?;
604 transaction
605 .execute("DELETE FROM circle_bootstrap_coverage", [])
606 .map_err(DbError::from)?;
607 let sabotaged =
608 crate::store::store_session::StoreTransaction::new(&transaction, self.store_dir)
609 .replay_projection_with_authority(
610 self.verified_store_authority,
611 root,
612 self.blob_decls,
613 self.gates,
614 self.synced_tables,
615 Some(routing_key),
616 &BTreeSet::new(),
617 None,
618 crate::ReplayJournal::Omit,
619 coven_protocol::membership::LocalStoreMembership::Current,
620 )?;
621 let sabotaged_count = sabotaged.document_count(historical_id)?;
622 let sabotaged_late_count = sabotaged.document_count(late_id)?;
623 transaction.rollback().map_err(DbError::from)?;
624 Ok((
625 retained_count,
626 retained_late_count,
627 sabotaged_count,
628 sabotaged_late_count,
629 ))
630 }
631
632 fn circle_bootstrap_coverage_count_for_test(
633 &self,
634 circle_id: coven_protocol::circle::CircleId,
635 ) -> Result<i64, DbError> {
636 self.conn
637 .query_row(
638 "SELECT COUNT(*) FROM circle_bootstrap_coverage WHERE circle_id = ?1",
639 [circle_id.to_string()],
640 |row| row.get(0),
641 )
642 .map_err(DbError::from)
643 }
644
645 fn reject_missing_circle_bootstrap_payload_claim_for_test(
646 &self,
647 circle_id: coven_protocol::circle::CircleId,
648 ) -> Result<String, DbError> {
649 let transaction = self.conn.unchecked_transaction().map_err(DbError::from)?;
650 transaction
651 .execute(
652 "DELETE FROM payload_owners WHERE owner_key = ?1",
653 [crate::payload_store::circle_bootstrap_coverage_owner_key(
654 circle_id,
655 )],
656 )
657 .map_err(DbError::from)?;
658 let error =
659 crate::store::store_session::StoreTransaction::new(&transaction, self.store_dir)
660 .circle_bootstrap_replay_inputs()
661 .expect_err("Circle bootstrap replay must require its payload claim");
662 transaction.rollback().map_err(DbError::from)?;
663 Ok(error.to_string())
664 }
665
666 fn reject_changed_circle_bootstrap_image_hash_for_test(
667 &mut self,
668 circle_id: coven_protocol::circle::CircleId,
669 root: &coven_protocol::store_commit::StoreRootRef,
670 activation_commit: &StoreBatchCommitRef,
671 ) -> Result<String, DbError> {
672 let transaction = self.conn.unchecked_transaction().map_err(DbError::from)?;
673 transaction
674 .execute(
675 "UPDATE circle_bootstrap_coverage
676 SET image_hash = ?2
677 WHERE circle_id = ?1",
678 rusqlite::params![
679 circle_id.to_string(),
680 ObjectHash::digest(b"corrupt Circle bootstrap image hash").to_string(),
681 ],
682 )
683 .map_err(DbError::from)?;
684 let retained =
685 crate::store::store_session::StoreTransaction::new(&transaction, self.store_dir)
686 .load_retained_merge_materialization_by_ref(
687 root,
688 self.verified_store_authority,
689 activation_commit,
690 )?;
691 let error =
692 crate::store::store_session::StoreTransaction::new(&transaction, self.store_dir)
693 .record_circle_bootstrap_coverage(
694 self.verified_store_authority,
695 root,
696 activation_commit,
697 retained.circle_activations(),
698 )
699 .expect_err("changed image hash must conflict with its exact reference");
700 transaction.rollback().map_err(DbError::from)?;
701 Ok(error.to_string())
702 }
703
704 fn author_exclusion_activation_evidence_for_test(
705 &self,
706 exclusion: &str,
707 ) -> Result<(String, String), DbError> {
708 self.conn
709 .query_row(
710 "SELECT accepted_cut, activation_head
711 FROM store_author_exclusion_activations
712 WHERE exclusion_ref = ?1",
713 [exclusion],
714 |row| Ok((row.get(0)?, row.get(1)?)),
715 )
716 .map_err(DbError::from)
717 }
718
719 fn sole_author_exclusion_activation_evidence_for_test(
720 &self,
721 ) -> Result<(String, String, String, String), DbError> {
722 crate::test_support::author_exclusion_activation_evidence(self.conn)
723 }
724
725 fn latest_local_write_facts_for_test(&self) -> Result<(String, i64, i64), DbError> {
726 crate::DatabaseTestSql::for_store(self.conn, self.store_dir).latest_local_write_facts()
727 }
728
729 fn install_retracted_device_state_failure_trigger_for_test(&self) -> Result<(), DbError> {
730 crate::DatabaseTestSql::new(self.conn).install_retracted_device_state_failure_trigger()
731 }
732
733 fn prepared_write_count_for_test(&self, write_id: &WriteId) -> Result<i64, DbError> {
734 crate::DatabaseTestSql::new(self.conn).prepared_write_count(write_id)
735 }
736
737 fn install_indexed_shared_blobs_for_test(
738 &self,
739 write_id: &WriteId,
740 records: Vec<coven_protocol::remote_object::RemoteObjectRecord>,
741 ) -> Result<(), DbError> {
742 crate::DatabaseTestSql::for_store(self.conn, self.store_dir)
743 .install_indexed_shared_blobs(write_id, records)
744 }
745
746 fn begin_remote_candidate_nonactivation_for_test(
751 &self,
752 object_id: coven_protocol::store_commit::ObjectHash,
753 nonactivation: coven_protocol::remote_object::CandidateNonactivation,
754 ) -> Result<(), DbError> {
755 let transaction = self.conn.unchecked_transaction().map_err(DbError::from)?;
756 crate::begin_remote_candidate_nonactivation_on(&transaction, object_id, nonactivation)?;
757 transaction.commit().map_err(DbError::from)
758 }
759
760 fn replace_blob_row_stamp_for_test(
761 &self,
762 table: &str,
763 row_id: &str,
764 stamp: &str,
765 ) -> Result<(), DbError> {
766 self.conn
767 .execute(
768 &format!(
769 "UPDATE {} SET _updated_at = ?2 WHERE id = ?1",
770 crate::quote_ident(table)
771 ),
772 (row_id, stamp),
773 )
774 .map(|_| ())
775 .map_err(DbError::from)
776 }
777
778 fn replace_blob_row_facts_for_test(
779 &self,
780 table: &str,
781 row_id: &str,
782 size: i64,
783 hash: &str,
784 stamp: &str,
785 ) -> Result<(), DbError> {
786 self.conn
787 .execute(
788 &format!(
789 "UPDATE {} SET size = ?2, hash = ?3, _updated_at = ?4 WHERE id = ?1",
790 crate::quote_ident(table)
791 ),
792 rusqlite::params![row_id, size, hash, stamp],
793 )
794 .map(|_| ())
795 .map_err(DbError::from)
796 }
797
798 fn complete_note_blob_transition_to_remote_for_test(
799 &self,
800 reference: &coven_protocol::blob::RowBlobRef,
801 note_id: &str,
802 ) -> Result<(), DbError> {
803 let transaction = self.conn.unchecked_transaction().map_err(DbError::from)?;
804 crate::ExternalBlobRecords::new(&transaction).clear(reference)?;
805 transaction
806 .execute("UPDATE notes SET shared = 1 WHERE id = ?1", [note_id])
807 .map_err(DbError::from)?;
808 transaction.commit().map_err(DbError::from)
809 }
810
811 fn plant_blob_namespace_collision_for_test(
812 &self,
813 id: &str,
814 local_hash: &str,
815 remote_hash: &str,
816 ) -> Result<(), DbError> {
817 let transaction = self.conn.unchecked_transaction().map_err(DbError::from)?;
818 transaction
819 .execute(
820 "INSERT INTO notes (id, title, shared, _updated_at, created_at)
821 VALUES ('note-local', 'x', 0, '0000000001000-0000-dev1', '2026-01-01'),
822 ('note-remote', 'x', 1, '0000000001000-0000-dev1', '2026-01-01')",
823 [],
824 )
825 .map_err(DbError::from)?;
826 transaction
827 .execute(
828 "INSERT INTO note_photos
829 (id, note_id, kind, size, hash, _updated_at, created_at)
830 VALUES (?1, 'note-local', 'attach', 17, ?2,
831 '0000000001000-0000-dev1', '2026-01-01')",
832 rusqlite::params![id, local_hash],
833 )
834 .map_err(DbError::from)?;
835 transaction
836 .execute(
837 "INSERT INTO note_covers
838 (id, note_id, size, hash, _updated_at, created_at)
839 VALUES (?1, 'note-remote', 18, ?2,
840 '0000000001000-0000-dev1', '2026-01-01')",
841 rusqlite::params![id, remote_hash],
842 )
843 .map_err(DbError::from)?;
844 transaction.commit().map_err(DbError::from)
845 }
846
847 fn plant_note_cover_blob_row_for_test(
848 &self,
849 id: &str,
850 note_id: &str,
851 size: i64,
852 hash: &str,
853 ) -> Result<(), DbError> {
854 self.conn
855 .execute(
856 "INSERT INTO note_covers
857 (id, note_id, size, hash, _updated_at, created_at)
858 VALUES (?1, ?2, ?3, ?4, '0000000001000-0000-dev1', '2026-01-01')",
859 rusqlite::params![id, note_id, size, hash],
860 )
861 .map(|_| ())
862 .map_err(DbError::from)
863 }
864
865 fn circle_bootstrap_failure_state_for_test(
866 &self,
867 blob_id: &str,
868 circle_id: coven_protocol::circle::CircleId,
869 control: &str,
870 remote_object_id: String,
871 ) -> Result<(bool, bool, bool, bool), DbError> {
872 crate::DatabaseTestSql::new(self.conn).circle_bootstrap_failure_state(
873 blob_id,
874 circle_id,
875 control,
876 remote_object_id,
877 )
878 }
879
880 fn circle_bootstrap_replay_for_control_for_test(
881 &self,
882 circle_id: coven_protocol::circle::CircleId,
883 control: &coven_protocol::circle::CircleControlCoord,
884 ) -> Result<Option<coven_protocol::circle_activation::VerifiedCircleImage>, DbError> {
885 Ok(crate::DatabaseTestSql::for_store(self.conn, self.store_dir)
886 .circle_bootstrap_replay_inputs()?
887 .into_iter()
888 .find_map(|(_, bootstrap)| {
889 (bootstrap.circle_id() == circle_id && bootstrap.control() == control)
890 .then_some(bootstrap)
891 }))
892 }
893
894 fn forge_circle_close_exclusion_for_test(
895 &self,
896 circle_id: coven_protocol::circle::CircleId,
897 ) -> Result<(), DbError> {
898 crate::DatabaseTestSql::new(self.conn).forge_circle_close_exclusion(circle_id)
899 }
900
901 fn tamper_author_exclusion_locator_for_test(
902 &self,
903 exclusion: StoreDeviceExclusionRef,
904 candidate: &StoreBatchCommitRef,
905 tamper: AuthorExclusionLocatorTamper,
906 ) -> Result<(), DbError> {
907 let connection = self.conn;
908 let exact = serde_json::to_string(&exclusion)
909 .map_err(|error| DbError::context("serialize exact exclusion reference", error))?;
910 let affected = match tamper {
911 AuthorExclusionLocatorTamper::Missing => connection.execute(
912 "DELETE FROM store_author_exclusion_activations
913 WHERE exclusion_ref = ?1",
914 [&exact],
915 ),
916 AuthorExclusionLocatorTamper::ExclusionReference => {
917 let mut wrong = exclusion;
918 wrong.outcome_hash = ObjectHash::digest(b"wrong exclusion reference");
919 let wrong = serde_json::to_string(&wrong).map_err(|error| {
920 DbError::context("serialize wrong exclusion reference", error)
921 })?;
922 connection.execute(
923 "UPDATE store_author_exclusion_activations
924 SET exclusion_ref = ?1 WHERE exclusion_ref = ?2",
925 (&wrong, &exact),
926 )
927 }
928 AuthorExclusionLocatorTamper::AcceptedCut => {
929 let cut: String = connection
930 .query_row(
931 "SELECT accepted_cut
932 FROM store_author_exclusion_activations
933 WHERE exclusion_ref = ?1",
934 [&exact],
935 |row| row.get(0),
936 )
937 .map_err(DbError::from)?;
938 let mut cut: std::collections::BTreeMap<
939 coven_protocol::causal_grants::AuthorStreamId,
940 StoreBatchCommitRef,
941 > = serde_json::from_str(&cut)
942 .map_err(|error| DbError::context("parse exclusion accepted cut", error))?;
943 cut.insert(
944 coven_protocol::causal_grants::AuthorStreamId::from_digest(ObjectHash::digest(
945 b"wrong exclusion accepted-cut stream",
946 )),
947 candidate.clone(),
948 );
949 let wrong = serde_json::to_string(&cut).map_err(|error| {
950 DbError::context("serialize wrong exclusion accepted cut", error)
951 })?;
952 connection.execute(
953 "UPDATE store_author_exclusion_activations
954 SET accepted_cut = ?1 WHERE exclusion_ref = ?2",
955 (&wrong, &exact),
956 )
957 }
958 AuthorExclusionLocatorTamper::ActivationCommit => {
959 let wrong = serde_json::to_string(candidate).map_err(|error| {
960 DbError::context("serialize wrong exclusion activation commit", error)
961 })?;
962 connection.execute(
963 "UPDATE store_author_exclusion_activations
964 SET activation_commit = ?1 WHERE exclusion_ref = ?2",
965 (&wrong, &exact),
966 )
967 }
968 AuthorExclusionLocatorTamper::ActivationHead => {
969 let head: String = connection
970 .query_row(
971 "SELECT activation_head
972 FROM store_author_exclusion_activations
973 WHERE exclusion_ref = ?1",
974 [&exact],
975 |row| row.get(0),
976 )
977 .map_err(DbError::from)?;
978 let mut head: StoreDeviceHeadRef = serde_json::from_str(&head)
979 .map_err(|error| DbError::context("parse exclusion activation head", error))?;
980 head.head_hash = ObjectHash::digest(b"wrong exclusion activation head");
981 let wrong = serde_json::to_string(&head).map_err(|error| {
982 DbError::context("serialize wrong exclusion activation head", error)
983 })?;
984 connection.execute(
985 "UPDATE store_author_exclusion_activations
986 SET activation_head = ?1 WHERE exclusion_ref = ?2",
987 (&wrong, &exact),
988 )
989 }
990 }
991 .map_err(DbError::from)?;
992 if affected != 1 {
993 return Err(DbError::Message(format!(
994 "locator tamper {tamper:?} changed {affected} rows"
995 )));
996 }
997 Ok(())
998 }
999}
1000
1001mod blob;
1002mod database;
1003mod device_exclusion;