1macro_rules! coven_tables {
14 ($visit:ident) => {
15 $visit!(
16 protocol_state,
17 "
18 key TEXT PRIMARY KEY,
19 value TEXT NOT NULL
20"
21 );
22 $visit!(
23 retained_replay_baselines,
24 "
25 singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
26 generation INTEGER NOT NULL CHECK (generation >= 0),
27 exact_cut TEXT NOT NULL CHECK (json_valid(exact_cut)),
28 schema_version INTEGER NOT NULL CHECK (schema_version >= 0),
29 routing_hash TEXT NOT NULL CHECK (length(routing_hash) = 64),
30 image_hash TEXT NOT NULL CHECK (length(image_hash) = 64),
31 image_bytes BLOB NOT NULL CHECK (length(image_bytes) > 0),
32 authority_bytes BLOB NOT NULL CHECK (length(authority_bytes) > 0)
33"
34 );
35 $visit!(
36 circle_bootstrap_coverage,
37 "
38 circle_id TEXT PRIMARY KEY,
39 control_coord TEXT NOT NULL CHECK (json_valid(control_coord)),
40 activation_commit TEXT NOT NULL CHECK (json_valid(activation_commit)),
41 exact_cut TEXT NOT NULL CHECK (json_valid(exact_cut)),
42 image_hash TEXT NOT NULL CHECK (length(image_hash) = 64),
43 image_bytes BLOB NOT NULL CHECK (length(image_bytes) > 0),
44 bootstrap_ref BLOB NOT NULL CHECK (length(bootstrap_ref) > 0)
45"
46 );
47 $visit!(
48 circle_close_exclusions,
49 "
50 circle_id TEXT PRIMARY KEY,
51 close_id TEXT NOT NULL,
52 excluded_registration TEXT NOT NULL CHECK (json_valid(excluded_registration)),
53 successor_control TEXT NOT NULL CHECK (json_valid(successor_control)),
54 activating_commit TEXT NOT NULL CHECK (json_valid(activating_commit))
55"
56 );
57 $visit!(
58 retained_merge_materializations,
59 "
60 device_id TEXT NOT NULL,
61 seq INTEGER NOT NULL CHECK (seq > 0),
62 commit_ref TEXT NOT NULL CHECK (json_valid(commit_ref)),
63 input_hash TEXT NOT NULL CHECK (length(input_hash) = 64),
64 canonical_input BLOB NOT NULL CHECK (length(canonical_input) > 0),
65 PRIMARY KEY (device_id, seq),
66 UNIQUE (device_id, seq, commit_ref, input_hash)
67"
68 );
69 $visit!(
70 merge_retraction_cleanups,
71 "
72 device_id TEXT NOT NULL,
73 seq INTEGER NOT NULL CHECK (seq > 0),
74 commit_ref TEXT NOT NULL CHECK (json_valid(commit_ref)),
75 cleanup_hash TEXT NOT NULL CHECK (length(cleanup_hash) = 64),
76 canonical_cleanup BLOB NOT NULL CHECK (length(canonical_cleanup) > 0),
77 PRIMARY KEY (device_id, seq),
78 UNIQUE (commit_ref)
79"
80 );
81 $visit!(
82 materialized_commits,
83 "
84 device_id TEXT NOT NULL,
85 seq INTEGER NOT NULL CHECK (seq > 0),
86 commit_ref TEXT NOT NULL CHECK (json_valid(commit_ref)),
87 retained_commit_ref TEXT NOT NULL CHECK (json_valid(retained_commit_ref)),
88 retained_input_hash TEXT NOT NULL CHECK (length(retained_input_hash) = 64),
89 PRIMARY KEY (device_id, seq),
90 FOREIGN KEY (device_id, seq, retained_commit_ref, retained_input_hash)
91 REFERENCES retained_merge_materializations(device_id, seq, commit_ref, input_hash)
92"
93 );
94 $visit!(
95 stream_activations,
96 "
97 activation_id TEXT PRIMARY KEY CHECK (length(activation_id) = 64),
98 author_stream_id TEXT NOT NULL UNIQUE CHECK (length(author_stream_id) = 64),
99 activation BLOB NOT NULL,
100 activating_commit TEXT NOT NULL CHECK (json_valid(activating_commit))
101"
102 );
103 $visit!(
104 snapshot_coverage,
105 "
106 device_id TEXT PRIMARY KEY,
107 seq INTEGER NOT NULL CHECK (seq > 0),
108 commit_ref TEXT NOT NULL CHECK (json_valid(commit_ref)),
109 snapshot_hash TEXT NOT NULL CHECK (length(snapshot_hash) = 64)
110"
111 );
112 $visit!(
113 local_blob_refs,
114 "
115 table_name TEXT NOT NULL,
116 row_id TEXT NOT NULL,
117 column_name TEXT NOT NULL,
118 row_stamp TEXT NOT NULL,
119 namespace TEXT NOT NULL,
120 blob_id TEXT NOT NULL,
121 path TEXT NOT NULL,
122 plaintext_size INTEGER NOT NULL CHECK (plaintext_size >= 0),
123 plaintext_hash TEXT NOT NULL CHECK (length(plaintext_hash) = 64),
124 PRIMARY KEY (table_name, row_id, column_name, row_stamp)
125"
126 );
127 $visit!(
128 cloud_outbox,
129 "
130 id INTEGER PRIMARY KEY AUTOINCREMENT,
131 operation TEXT NOT NULL CHECK (operation IN ('upload', 'delete')),
132 table_name TEXT,
133 row_id TEXT,
134 column_name TEXT,
135 row_stamp TEXT,
136 root_table TEXT,
137 root_id TEXT,
138 row_ref TEXT CHECK (row_ref IS NULL OR json_valid(row_ref)),
139 upload_state TEXT CHECK (upload_state IS NULL OR json_valid(upload_state)),
140 stored_ref TEXT CHECK (stored_ref IS NULL OR json_valid(stored_ref)),
141 source_path TEXT,
142 retain_pinned INTEGER CHECK (retain_pinned IS NULL OR retain_pinned IN (0, 1)),
143 created_at TEXT NOT NULL,
144 attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
145 last_error TEXT,
146 last_attempt_at TEXT,
147 CHECK (
148 (operation = 'upload' AND table_name IS NOT NULL AND row_id IS NOT NULL
149 AND column_name IS NOT NULL AND row_stamp IS NOT NULL
150 AND root_table IS NOT NULL AND root_id IS NOT NULL AND row_ref IS NOT NULL
151 AND stored_ref IS NULL AND source_path IS NOT NULL AND retain_pinned IS NOT NULL
152 AND upload_state IS NOT NULL)
153 OR
154 (operation = 'delete' AND table_name IS NULL AND row_id IS NULL
155 AND column_name IS NULL AND row_stamp IS NULL
156 AND root_table IS NULL AND root_id IS NULL AND row_ref IS NULL
157 AND stored_ref IS NOT NULL AND source_path IS NULL AND retain_pinned IS NULL
158 AND upload_state IS NULL)
159 ),
160 UNIQUE (operation, table_name, row_id, column_name, row_stamp),
161 UNIQUE (stored_ref)
162"
163 );
164 $visit!(
165 blob_make_remote_intents,
166 "
167 root_table TEXT NOT NULL,
168 root_id TEXT NOT NULL,
169 retain_pinned INTEGER NOT NULL CHECK (retain_pinned IN (0, 1)),
170 state TEXT NOT NULL CHECK (state IN ('uploading', 'cancelling', 'publishing')),
171 write_id TEXT UNIQUE,
172 CHECK ((state = 'publishing') = (write_id IS NOT NULL)),
173 PRIMARY KEY (root_table, root_id)
174"
175 );
176 $visit!(
177 local_cleanup_intents,
178 "
179 namespace TEXT NOT NULL,
180 blob_id TEXT NOT NULL,
181 copy_identity TEXT NOT NULL CHECK (copy_identity = 'local' OR length(copy_identity) = 64),
182 PRIMARY KEY (namespace, blob_id, copy_identity)
183"
184 );
185 $visit!(
186 published_blob_drop_intents,
187 "
188 seq INTEGER NOT NULL CHECK (seq > 0),
189 namespace TEXT NOT NULL,
190 blob_id TEXT NOT NULL,
191 size INTEGER NOT NULL CHECK (size >= 0),
192 plaintext_hash TEXT NOT NULL,
193 locator_hash TEXT NOT NULL,
194 disposition TEXT NOT NULL CHECK (disposition IN ('drop', 'cache', 'pin')),
195 PRIMARY KEY (seq, namespace, blob_id, locator_hash)
196"
197 );
198 $visit!(
199 store_writes,
200 "
201 ordinal INTEGER PRIMARY KEY AUTOINCREMENT,
202 write_id TEXT NOT NULL UNIQUE,
203 status TEXT NOT NULL CHECK (json_valid(status)),
204 affected_rows TEXT NOT NULL CHECK (json_valid(affected_rows)),
205 changeset BLOB NOT NULL,
206 inverse_changeset BLOB NOT NULL,
207 base TEXT NOT NULL CHECK (json_valid(base)),
208 blob_facts TEXT NOT NULL CHECK (json_valid(blob_facts)),
209 prepared TEXT CHECK (prepared IS NULL OR json_valid(prepared))
210"
211 );
212 $visit!(
213 store_write_blob_leases,
214 "
215 write_id TEXT NOT NULL,
216 namespace TEXT NOT NULL,
217 blob_id TEXT NOT NULL,
218 PRIMARY KEY (write_id, namespace, blob_id),
219 FOREIGN KEY (write_id) REFERENCES store_writes(write_id)
220"
221 );
222 $visit!(
223 store_write_partitions,
224 "
225 write_id TEXT NOT NULL,
226 audience TEXT NOT NULL,
227 control_coord TEXT,
228 changeset BLOB NOT NULL,
229 PRIMARY KEY (write_id, audience),
230 FOREIGN KEY (write_id) REFERENCES store_writes(write_id) ON DELETE CASCADE,
231 CHECK (
232 (audience IN ('store', 'local') AND control_coord IS NULL)
233 OR
234 (audience NOT IN ('store', 'local') AND json_valid(control_coord))
235 )
236"
237 );
238 $visit!(
239 remote_objects,
240 "
241 object_id TEXT PRIMARY KEY CHECK (length(object_id) = 64),
242 state TEXT NOT NULL CHECK (json_valid(state))
243"
244 );
245 $visit!(
246 retained_replay_objects,
247 "
248 device_id TEXT NOT NULL,
249 seq INTEGER NOT NULL CHECK (seq > 0),
250 commit_ref TEXT NOT NULL CHECK (json_valid(commit_ref)),
251 input_hash TEXT NOT NULL CHECK (length(input_hash) = 64),
252 object_id TEXT NOT NULL CHECK (length(object_id) = 64),
253 PRIMARY KEY (device_id, seq, object_id),
254 FOREIGN KEY (device_id, seq, commit_ref, input_hash)
255 REFERENCES retained_merge_materializations(device_id, seq, commit_ref, input_hash),
256 FOREIGN KEY (object_id) REFERENCES remote_objects(object_id)
257"
258 );
259 $visit!(
260 protocol_inert_objects,
261 "
262 object_id TEXT PRIMARY KEY CHECK (length(object_id) = 64),
263 state TEXT NOT NULL CHECK (json_valid(state))
264"
265 );
266 $visit!(
267 reclaimed_store_packages,
268 "
269 object_id TEXT PRIMARY KEY CHECK (length(object_id) = 64),
270 authorization_hash TEXT NOT NULL UNIQUE CHECK (length(authorization_hash) = 64),
271 state TEXT NOT NULL CHECK (json_valid(state)),
272 FOREIGN KEY (authorization_hash) REFERENCES store_reclaim_operations(authorization_hash)
273"
274 );
275 $visit!(
276 store_write_packages,
277 "
278 write_id TEXT NOT NULL,
279 audience TEXT NOT NULL,
280 remote_object_id TEXT NOT NULL CHECK (length(remote_object_id) = 64),
281 PRIMARY KEY (write_id, audience),
282 FOREIGN KEY (write_id) REFERENCES store_writes(write_id) ON DELETE CASCADE,
283 FOREIGN KEY (remote_object_id) REFERENCES remote_objects(object_id)
284"
285 );
286 $visit!(
287 store_write_blobs,
288 "
289 write_id TEXT NOT NULL,
290 audience TEXT NOT NULL,
291 locator_hash TEXT NOT NULL CHECK (length(locator_hash) = 64),
292 remote_object_id TEXT NOT NULL CHECK (length(remote_object_id) = 64),
293 spool_path TEXT,
294 PRIMARY KEY (write_id, audience, remote_object_id),
295 FOREIGN KEY (write_id) REFERENCES store_writes(write_id) ON DELETE CASCADE,
296 FOREIGN KEY (remote_object_id) REFERENCES remote_objects(object_id)
297"
298 );
299 $visit!(
300 blob_locators,
301 "
302 remote_object_id TEXT PRIMARY KEY CHECK (length(remote_object_id) = 64),
303 locator_hash TEXT NOT NULL CHECK (length(locator_hash) = 64),
304 FOREIGN KEY (remote_object_id) REFERENCES remote_objects(object_id)
305"
306 );
307 $visit!(
308 row_blob_locators,
309 "
310 table_name TEXT NOT NULL,
311 row_id TEXT NOT NULL,
312 column_name TEXT NOT NULL,
313 row_stamp TEXT NOT NULL,
314 audience_authority TEXT NOT NULL CHECK (json_valid(audience_authority)),
315 remote_object_id TEXT NOT NULL CHECK (length(remote_object_id) = 64),
316 PRIMARY KEY (table_name, row_id, column_name, row_stamp),
317 FOREIGN KEY (remote_object_id) REFERENCES blob_locators(remote_object_id)
318"
319 );
320 $visit!(
321 outbound_membership_mutation,
322 "
323 singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
324 intent_hash TEXT NOT NULL CHECK (length(intent_hash) = 64),
325 plan_bytes BLOB NOT NULL,
326 progress_bytes BLOB NOT NULL
327"
328 );
329 $visit!(
330 outbound_store_snapshot,
331 "
332 singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
333 snapshot_ref TEXT NOT NULL CHECK (json_valid(snapshot_ref)),
334 meta_prepared TEXT NOT NULL CHECK (json_valid(meta_prepared)),
335 image_ref TEXT NOT NULL CHECK (json_valid(image_ref)),
336 image_prepared TEXT NOT NULL CHECK (json_valid(image_prepared)),
337 image_bytes BLOB NOT NULL,
338 meta_bytes BLOB NOT NULL,
339 blobs TEXT NOT NULL CHECK (json_valid(blobs))
340"
341 );
342 $visit!(
343 published_store_snapshot,
344 "
345 generation INTEGER PRIMARY KEY CHECK (generation >= 0),
346 snapshot_ref TEXT NOT NULL CHECK (json_valid(snapshot_ref)),
347 successor_slot TEXT NOT NULL CHECK (json_valid(successor_slot)),
348 meta_bytes BLOB NOT NULL
349"
350 );
351 $visit!(
352 snapshot_blob_spool_cleanup,
353 "
354 path TEXT PRIMARY KEY
355"
356 );
357 $visit!(
358 outbound_circle_snapshot,
359 "
360 circle_id TEXT PRIMARY KEY,
361 snapshot_ref TEXT NOT NULL CHECK (json_valid(snapshot_ref)),
362 meta_prepared TEXT NOT NULL CHECK (json_valid(meta_prepared)),
363 image_ref TEXT NOT NULL CHECK (json_valid(image_ref)),
364 image_prepared TEXT NOT NULL CHECK (json_valid(image_prepared)),
365 image_bytes BLOB NOT NULL,
366 meta_bytes BLOB NOT NULL,
367 blobs TEXT NOT NULL CHECK (json_valid(blobs))
368"
369 );
370 $visit!(
371 published_circle_snapshot,
372 "
373 circle_id TEXT NOT NULL,
374 generation INTEGER NOT NULL CHECK (generation >= 0),
375 snapshot_ref TEXT NOT NULL CHECK (json_valid(snapshot_ref)),
376 successor_slot TEXT NOT NULL CHECK (json_valid(successor_slot)),
377 cut TEXT NOT NULL CHECK (json_valid(cut)),
378 meta_bytes BLOB NOT NULL,
379 PRIMARY KEY (circle_id, generation)
380"
381 );
382 $visit!(
383 outbound_store_acks,
384 "
385 singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
386 ack_ref TEXT NOT NULL CHECK (json_valid(ack_ref)),
387 ack_bytes BLOB NOT NULL,
388 prepared_object TEXT NOT NULL CHECK (json_valid(prepared_object)),
389 activation TEXT NOT NULL CHECK (json_valid(activation))
390"
391 );
392 $visit!(
393 published_store_acks,
394 "
395 singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
396 ack_ref TEXT NOT NULL CHECK (json_valid(ack_ref)),
397 successor_slot TEXT NOT NULL CHECK (json_valid(successor_slot))
398"
399 );
400 $visit!(
401 outbound_circle_acks,
402 "
403 circle_id TEXT PRIMARY KEY,
404 ack_ref TEXT NOT NULL CHECK (json_valid(ack_ref)),
405 ack_bytes BLOB NOT NULL,
406 prepared_object TEXT NOT NULL CHECK (json_valid(prepared_object))
407"
408 );
409 $visit!(
410 published_circle_acks,
411 "
412 circle_id TEXT PRIMARY KEY,
413 ack_ref TEXT NOT NULL CHECK (json_valid(ack_ref)),
414 successor_slot TEXT NOT NULL CHECK (json_valid(successor_slot)),
415 store_cut TEXT NOT NULL CHECK (json_valid(store_cut)),
416 control_coord TEXT NOT NULL CHECK (json_valid(control_coord))
417"
418 );
419 $visit!(
420 activated_circle_acks,
421 "
422 circle_id TEXT NOT NULL,
423 device_id TEXT NOT NULL,
424 ack_ref TEXT NOT NULL CHECK (json_valid(ack_ref)),
425 activating_commit TEXT NOT NULL CHECK (json_valid(activating_commit)),
426 PRIMARY KEY (circle_id, device_id)
427"
428 );
429 $visit!(
430 activated_store_acks,
431 "
432 device_id TEXT PRIMARY KEY,
433 ack_ref TEXT NOT NULL CHECK (json_valid(ack_ref)),
434 activating_commit TEXT NOT NULL CHECK (json_valid(activating_commit))
435"
436 );
437 $visit!(
438 store_device_exclusion_freezes,
439 "
440 proposal_id TEXT PRIMARY KEY CHECK (length(proposal_id) = 64),
441 proposal_ref TEXT NOT NULL CHECK (json_valid(proposal_ref)),
442 target_cut TEXT NOT NULL CHECK (json_valid(target_cut))
443"
444 );
445 $visit!(
446 outbound_store_device_exclusion,
447 "
448 operation_id TEXT PRIMARY KEY CHECK (length(operation_id) = 64),
449 active_key INTEGER UNIQUE CHECK (active_key IS NULL OR active_key = 1),
450 state TEXT NOT NULL CHECK (json_valid(state))
451"
452 );
453 $visit!(
454 store_reclaim_operations,
455 "
456 authorization_hash TEXT PRIMARY KEY CHECK (length(authorization_hash) = 64),
457 state TEXT NOT NULL CHECK (json_valid(state))
458"
459 );
460 $visit!(
461 store_protocol_root_authority,
462 "
463 singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
464 store_root_hash TEXT NOT NULL CHECK (length(store_root_hash) = 64),
465 store_protocol_root_bytes BLOB NOT NULL,
466 store_root_object TEXT NOT NULL CHECK (json_valid(store_root_object))
467"
468 );
469 $visit!(
470 local_store_protocol_root,
471 "
472 singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
473 store_root_hash TEXT NOT NULL CHECK (length(store_root_hash) = 64),
474 store_protocol_root_bytes BLOB NOT NULL,
475 prepared_object TEXT NOT NULL CHECK (json_valid(prepared_object))
476"
477 );
478 $visit!(
479 local_store_device_registration,
480 "
481 singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
482 device_id TEXT NOT NULL UNIQUE,
483 registration_hash TEXT NOT NULL UNIQUE CHECK (length(registration_hash) = 64),
484 registration_bytes BLOB NOT NULL,
485 prepared_object TEXT NOT NULL CHECK (json_valid(prepared_object)),
486 initial_ack_ref TEXT NOT NULL CHECK (json_valid(initial_ack_ref)),
487 initial_ack_bytes BLOB NOT NULL,
488 initial_ack_prepared TEXT NOT NULL CHECK (json_valid(initial_ack_prepared)),
489 state TEXT NOT NULL CHECK (json_valid(state))
490"
491 );
492 $visit!(
493 local_store_founder_graph,
494 "
495 singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
496 membership_graph TEXT NOT NULL CHECK (json_valid(membership_graph))
497"
498 );
499 $visit!(
500 store_device_registration_activations,
501 "
502 device_id TEXT PRIMARY KEY,
503 registration_hash TEXT NOT NULL CHECK (length(registration_hash) = 64),
504 author_pubkey TEXT NOT NULL,
505 device_signing_pubkey TEXT NOT NULL,
506 registration_bytes BLOB NOT NULL,
507 registration_object TEXT NOT NULL CHECK (json_valid(registration_object)),
508 activation_authority TEXT NOT NULL CHECK (json_valid(activation_authority)),
509 UNIQUE (device_id, registration_hash),
510 UNIQUE (registration_object)
511"
512 );
513 $visit!(
514 store_device_state_snapshots,
515 "
516 commit_ref TEXT PRIMARY KEY CHECK (json_valid(commit_ref)),
517 state TEXT NOT NULL CHECK (json_valid(state))
518"
519 );
520 $visit!(
521 store_author_exclusion_activations,
522 "
523 exclusion_ref TEXT PRIMARY KEY CHECK (json_valid(exclusion_ref)),
524 accepted_cut TEXT NOT NULL CHECK (json_valid(accepted_cut)),
525 activation_commit TEXT NOT NULL CHECK (json_valid(activation_commit)),
526 activation_head TEXT NOT NULL CHECK (json_valid(activation_head))
527"
528 );
529 $visit!(
530 circle_control_activations,
531 "
532 circle_id TEXT NOT NULL,
533 control_coord TEXT NOT NULL CHECK (json_valid(control_coord)),
534 stream_id TEXT NOT NULL,
535 seq INTEGER NOT NULL CHECK (seq > 0),
536 commit_hash TEXT NOT NULL CHECK (length(commit_hash) = 64),
537 control_bytes BLOB NOT NULL,
538 PRIMARY KEY (circle_id, control_coord),
539 UNIQUE (circle_id, stream_id, seq)
540"
541 );
542 $visit!(
543 circle_operations,
544 "
545 operation_id TEXT PRIMARY KEY,
546 circle_id TEXT NOT NULL UNIQUE,
547 payload BLOB NOT NULL
548"
549 );
550 $visit!(
551 circle_access_cache,
552 "
553 circle_id TEXT NOT NULL,
554 control_coord TEXT NOT NULL CHECK (json_valid(control_coord)),
555 owner_pubkey TEXT NOT NULL,
556 disposition TEXT NOT NULL CHECK (disposition IN ('active', 'inactive')),
557 access_bytes BLOB NOT NULL,
558 PRIMARY KEY (circle_id, control_coord, owner_pubkey),
559 FOREIGN KEY (circle_id, control_coord)
560 REFERENCES circle_control_activations(circle_id, control_coord)
561"
562 );
563 $visit!(
564 circle_roster_cache,
565 "
566 circle_id TEXT NOT NULL,
567 control_coord TEXT NOT NULL CHECK (json_valid(control_coord)),
568 roster_bytes BLOB NOT NULL,
569 PRIMARY KEY (circle_id, control_coord),
570 FOREIGN KEY (circle_id, control_coord)
571 REFERENCES circle_control_activations(circle_id, control_coord)
572"
573 );
574 $visit!(
575 circle_metadata_cache,
576 "
577 circle_id TEXT NOT NULL,
578 control_coord TEXT NOT NULL CHECK (json_valid(control_coord)),
579 metadata_bytes BLOB NOT NULL,
580 PRIMARY KEY (circle_id, control_coord),
581 FOREIGN KEY (circle_id, control_coord)
582 REFERENCES circle_control_activations(circle_id, control_coord)
583"
584 );
585 $visit!(
586 circle_current_state,
587 "
588 circle_id TEXT PRIMARY KEY,
589 state BLOB NOT NULL
590"
591 );
592 };
593}
594
595macro_rules! coven_routing_tables {
596 ($visit:ident) => {
597 $visit!(
598 _coven_audience,
599 "
600 routing_id TEXT PRIMARY KEY,
601 circle_id TEXT,
602 _updated_at TEXT NOT NULL
603"
604 );
605 $visit!(
606 _coven_row_routes,
607 "
608 routing_id TEXT PRIMARY KEY,
609 table_name TEXT NOT NULL,
610 row_id TEXT NOT NULL,
611 _updated_at TEXT NOT NULL,
612 UNIQUE (table_name, row_id)
613"
614 );
615 };
616}
617
618const OBJECT_OWNERSHIP_TRIGGERS: &str = "
619CREATE TRIGGER IF NOT EXISTS remote_object_identity_must_not_be_inert_on_insert
620BEFORE INSERT ON remote_objects
621WHEN EXISTS (
622 SELECT 1 FROM protocol_inert_objects WHERE object_id = NEW.object_id
623)
624BEGIN
625 SELECT RAISE(ABORT, 'remote object identity is protocol-inert');
626END;
627CREATE TRIGGER IF NOT EXISTS remote_object_identity_must_not_be_inert_on_update
628BEFORE UPDATE OF object_id ON remote_objects
629WHEN EXISTS (
630 SELECT 1 FROM protocol_inert_objects WHERE object_id = NEW.object_id
631)
632BEGIN
633 SELECT RAISE(ABORT, 'remote object identity is protocol-inert');
634END;
635CREATE TRIGGER IF NOT EXISTS inert_object_identity_must_not_be_remote_on_insert
636BEFORE INSERT ON protocol_inert_objects
637WHEN EXISTS (
638 SELECT 1 FROM remote_objects WHERE object_id = NEW.object_id
639)
640BEGIN
641 SELECT RAISE(ABORT, 'protocol-inert object identity has active ownership');
642END;
643CREATE TRIGGER IF NOT EXISTS inert_object_identity_must_not_be_remote_on_update
644BEFORE UPDATE OF object_id ON protocol_inert_objects
645WHEN EXISTS (
646 SELECT 1 FROM remote_objects WHERE object_id = NEW.object_id
647)
648BEGIN
649 SELECT RAISE(ABORT, 'protocol-inert object identity has active ownership');
650END;
651CREATE TRIGGER IF NOT EXISTS remote_object_identity_must_not_be_reclaimed_on_insert
652BEFORE INSERT ON remote_objects
653WHEN EXISTS (
654 SELECT 1 FROM reclaimed_store_packages WHERE object_id = NEW.object_id
655)
656BEGIN
657 SELECT RAISE(ABORT, 'remote object identity is a reclaimed Store package');
658END;
659CREATE TRIGGER IF NOT EXISTS remote_object_identity_must_not_be_reclaimed_on_update
660BEFORE UPDATE OF object_id ON remote_objects
661WHEN EXISTS (
662 SELECT 1 FROM reclaimed_store_packages WHERE object_id = NEW.object_id
663)
664BEGIN
665 SELECT RAISE(ABORT, 'remote object identity is a reclaimed Store package');
666END;
667CREATE TRIGGER IF NOT EXISTS inert_object_identity_must_not_be_reclaimed_on_insert
668BEFORE INSERT ON protocol_inert_objects
669WHEN EXISTS (
670 SELECT 1 FROM reclaimed_store_packages WHERE object_id = NEW.object_id
671)
672BEGIN
673 SELECT RAISE(ABORT, 'protocol-inert object identity is a reclaimed Store package');
674END;
675CREATE TRIGGER IF NOT EXISTS inert_object_identity_must_not_be_reclaimed_on_update
676BEFORE UPDATE OF object_id ON protocol_inert_objects
677WHEN EXISTS (
678 SELECT 1 FROM reclaimed_store_packages WHERE object_id = NEW.object_id
679)
680BEGIN
681 SELECT RAISE(ABORT, 'protocol-inert object identity is a reclaimed Store package');
682END;
683CREATE TRIGGER IF NOT EXISTS reclaimed_store_package_identity_must_be_closed_on_insert
684BEFORE INSERT ON reclaimed_store_packages
685WHEN EXISTS (
686 SELECT 1 FROM remote_objects WHERE object_id = NEW.object_id
687 UNION ALL
688 SELECT 1 FROM protocol_inert_objects WHERE object_id = NEW.object_id
689)
690BEGIN
691 SELECT RAISE(ABORT, 'reclaimed Store package identity has another ownership state');
692END;
693CREATE TRIGGER IF NOT EXISTS reclaimed_store_package_identity_must_be_closed_on_update
694BEFORE UPDATE OF object_id ON reclaimed_store_packages
695WHEN EXISTS (
696 SELECT 1 FROM remote_objects WHERE object_id = NEW.object_id
697 UNION ALL
698 SELECT 1 FROM protocol_inert_objects WHERE object_id = NEW.object_id
699)
700BEGIN
701 SELECT RAISE(ABORT, 'reclaimed Store package identity has another ownership state');
702END;
703";
704
705#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
706#[serde(deny_unknown_fields)]
707pub(crate) struct CovenSchemaManifest {
708 objects: Vec<CovenSchemaObject>,
709 tables: Vec<CovenTableShape>,
710}
711
712impl CovenSchemaManifest {
713 pub(crate) fn is_empty(&self) -> bool {
714 self.objects.is_empty() && self.tables.is_empty()
715 }
716}
717
718#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
719#[serde(deny_unknown_fields)]
720struct CovenSchemaObject {
721 kind: String,
722 name: String,
723 table_name: String,
724 sql: Option<String>,
725}
726
727#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
728#[serde(deny_unknown_fields)]
729struct CovenTableShape {
730 name: String,
731 columns: i64,
732 without_rowid: bool,
733 strict: bool,
734}
735
736fn normalize_schema_sql(sql: &str) -> String {
737 let bytes = sql.as_bytes();
738 let mut normalized = String::with_capacity(sql.len());
739 let mut index = 0;
740 let mut pending_separator = false;
741 let mut quote = None;
742
743 while index < bytes.len() {
744 let byte = bytes[index];
745 if let Some(delimiter) = quote {
746 normalized.push(char::from(byte));
747 if byte == delimiter {
748 if bytes.get(index + 1) == Some(&delimiter) {
749 normalized.push(char::from(delimiter));
750 index += 1;
751 } else {
752 quote = None;
753 }
754 }
755 index += 1;
756 continue;
757 }
758
759 if byte == b'-' && bytes.get(index + 1) == Some(&b'-') {
760 index += 2;
761 while index < bytes.len() && !matches!(bytes[index], b'\n' | b'\r') {
762 index += 1;
763 }
764 pending_separator = true;
765 continue;
766 }
767 if byte.is_ascii_whitespace() {
768 pending_separator = true;
769 index += 1;
770 continue;
771 }
772 if matches!(byte, b'\'' | b'"' | b'`') {
773 if pending_separator
774 && normalized
775 .as_bytes()
776 .last()
777 .is_some_and(u8::is_ascii_alphanumeric)
778 {
779 normalized.push(' ');
780 }
781 pending_separator = false;
782 quote = Some(byte);
783 normalized.push(char::from(byte));
784 index += 1;
785 continue;
786 }
787 if pending_separator
788 && byte.is_ascii_alphanumeric()
789 && normalized
790 .as_bytes()
791 .last()
792 .is_some_and(u8::is_ascii_alphanumeric)
793 {
794 normalized.push(' ');
795 }
796 pending_separator = false;
797 normalized.push(char::from(byte.to_ascii_lowercase()));
798 index += 1;
799 }
800
801 normalized
802}
803
804fn all_coven_table_names() -> std::collections::BTreeSet<&'static str> {
805 let mut names = std::collections::BTreeSet::new();
806 macro_rules! collect_name {
807 ($name:ident, $columns:literal) => {
808 names.insert(stringify!($name));
809 };
810 }
811
812 coven_tables!(collect_name);
813 coven_routing_tables!(collect_name);
814 names
815}
816
817#[cfg(test)]
818pub(crate) fn all_table_names() -> std::collections::BTreeSet<&'static str> {
819 all_coven_table_names()
820}
821
822pub(crate) fn live_coven_schema_manifest(
823 conn: &rusqlite::Connection,
824) -> rusqlite::Result<CovenSchemaManifest> {
825 let names = all_coven_table_names();
826 let mut objects = conn
827 .prepare(
828 "SELECT type, name, tbl_name, sql
829 FROM main.sqlite_schema
830 WHERE type IN ('table', 'index', 'trigger')
831 ORDER BY type, name",
832 )?
833 .query_map([], |row| {
834 Ok(CovenSchemaObject {
835 kind: row.get(0)?,
836 name: row.get(1)?,
837 table_name: row.get(2)?,
838 sql: row
839 .get::<_, Option<String>>(3)?
840 .map(|sql| normalize_schema_sql(&sql)),
841 })
842 })?
843 .collect::<rusqlite::Result<Vec<_>>>()?;
844 objects.retain(|object| names.contains(object.table_name.as_str()));
845
846 let mut tables = conn
847 .prepare("PRAGMA main.table_list")?
848 .query_map([], |row| {
849 Ok(CovenTableShape {
850 name: row.get(1)?,
851 columns: row.get(3)?,
852 without_rowid: row.get::<_, i64>(4)? != 0,
853 strict: row.get::<_, i64>(5)? != 0,
854 })
855 })?
856 .collect::<rusqlite::Result<Vec<_>>>()?;
857 tables.retain(|table| names.contains(table.name.as_str()));
858 tables.sort_by(|left, right| left.name.cmp(&right.name));
859
860 Ok(CovenSchemaManifest { objects, tables })
861}
862
863pub(crate) fn expected_coven_schema_manifest(
864 include_routing: bool,
865) -> rusqlite::Result<CovenSchemaManifest> {
866 let conn = rusqlite::Connection::open_in_memory()?;
867 apply_coven_schema(&conn)?;
868 if include_routing {
869 apply_coven_routing_schema(&conn)?;
870 }
871 live_coven_schema_manifest(&conn)
872}
873
874pub(crate) fn apply_coven_schema(conn: &rusqlite::Connection) -> rusqlite::Result<()> {
879 macro_rules! apply_table {
880 ($name:ident, $columns:literal) => {
881 conn.execute_batch(concat!(
882 "CREATE TABLE IF NOT EXISTS ",
883 stringify!($name),
884 " (",
885 $columns,
886 ") STRICT;"
887 ))?;
888 };
889 }
890
891 coven_tables!(apply_table);
892 conn.execute_batch(OBJECT_OWNERSHIP_TRIGGERS)?;
893 Ok(())
894}
895
896pub(crate) fn apply_coven_routing_schema(conn: &rusqlite::Connection) -> rusqlite::Result<()> {
900 macro_rules! apply_table {
901 ($name:ident, $columns:literal) => {
902 conn.execute_batch(concat!(
903 "CREATE TABLE IF NOT EXISTS ",
904 stringify!($name),
905 " (",
906 $columns,
907 ") STRICT, WITHOUT ROWID;"
908 ))?;
909 };
910 }
911
912 coven_routing_tables!(apply_table);
913 Ok(())
914}
915
916pub(crate) fn is_reserved_table_name(name: &str) -> bool {
919 macro_rules! matches_table {
920 ($table:ident, $columns:literal) => {
921 if name == stringify!($table) {
922 return true;
923 }
924 };
925 }
926
927 coven_tables!(matches_table);
928 coven_routing_tables!(matches_table);
929 false
930}
931
932pub(crate) fn user_table_names(conn: &rusqlite::Connection) -> rusqlite::Result<Vec<String>> {
936 let mut statement = conn.prepare(
937 "SELECT name FROM main.sqlite_schema
938 WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
939 ORDER BY name",
940 )?;
941 let tables = statement
942 .query_map([], |row| row.get::<_, String>(0))?
943 .collect::<rusqlite::Result<Vec<_>>>()?;
944 Ok(tables)
945}
946
947#[cfg(test)]
951pub(crate) fn table_names() -> Vec<&'static str> {
952 let mut names = Vec::new();
953 macro_rules! collect_name {
954 ($name:ident, $columns:literal) => {
955 names.push(stringify!($name));
956 };
957 }
958
959 coven_tables!(collect_name);
960 names
961}
962
963#[cfg(test)]
964mod tests {
965 use super::*;
966
967 #[test]
968 fn schema_sql_normalization_ignores_formatting_but_keeps_constraints() {
969 let formatted = normalize_schema_sql(
970 "CREATE TABLE t (
971 id INTEGER PRIMARY KEY, -- explanatory text
972 value TEXT CHECK (length(value) = 64)
973 ) STRICT",
974 );
975 let compact = normalize_schema_sql(
976 "create table t(id integer primary key,value text check(length(value)=64)) strict",
977 );
978 let changed = normalize_schema_sql(
979 "create table t(id integer primary key,value text check(length(value)=32)) strict",
980 );
981
982 assert_eq!(formatted, compact);
983 assert_ne!(formatted, changed);
984 }
985
986 #[test]
991 fn every_bookkeeping_table_is_strict() {
992 let conn = rusqlite::Connection::open_in_memory().expect("open in-memory");
993 apply_coven_schema(&conn).expect("apply coven schema");
994 for name in table_names() {
995 let sql = format!(
996 "PRAGMA table_list({})",
997 crate::sync::session::quote_ident(name)
998 );
999 let mut stmt = conn.prepare(&sql).expect("prepare table_list");
1000 let strict: i64 = stmt
1001 .query_row([], |row| row.get(5))
1002 .unwrap_or_else(|e| panic!("PRAGMA table_list({name}): {e}"));
1003 assert_eq!(strict, 1, "{name} must be STRICT");
1004 }
1005 }
1006
1007 #[test]
1008 fn active_and_protocol_inert_object_identities_are_disjoint() {
1009 let conn = rusqlite::Connection::open_in_memory().expect("open in-memory");
1010 apply_coven_schema(&conn).expect("apply coven schema");
1011 let active_first = "a".repeat(64);
1012 conn.execute(
1013 "INSERT INTO remote_objects (object_id, state) VALUES (?1, '{}')",
1014 [&active_first],
1015 )
1016 .expect("insert active object");
1017 assert!(
1018 conn.execute(
1019 "INSERT INTO protocol_inert_objects (object_id, state) VALUES (?1, '{}')",
1020 [&active_first],
1021 )
1022 .is_err(),
1023 "an active object identity must not also become protocol-inert"
1024 );
1025
1026 let inert_first = "b".repeat(64);
1027 conn.execute(
1028 "INSERT INTO protocol_inert_objects (object_id, state) VALUES (?1, '{}')",
1029 [&inert_first],
1030 )
1031 .expect("insert protocol-inert object");
1032 assert!(
1033 conn.execute(
1034 "INSERT INTO remote_objects (object_id, state) VALUES (?1, '{}')",
1035 [&inert_first],
1036 )
1037 .is_err(),
1038 "a protocol-inert object identity must not return to active ownership"
1039 );
1040 assert!(
1041 conn.execute(
1042 "UPDATE remote_objects SET object_id = ?1 WHERE object_id = ?2",
1043 [&inert_first, &active_first],
1044 )
1045 .is_err(),
1046 "an active identity update must not collide with protocol-inert ownership"
1047 );
1048 assert!(
1049 conn.execute(
1050 "UPDATE protocol_inert_objects SET object_id = ?1 WHERE object_id = ?2",
1051 [&active_first, &inert_first],
1052 )
1053 .is_err(),
1054 "a protocol-inert identity update must not collide with active ownership"
1055 );
1056 }
1057
1058 #[test]
1059 fn circle_operation_journal_has_one_progress_representation() {
1060 let conn = rusqlite::Connection::open_in_memory().expect("open in-memory");
1061 apply_coven_schema(&conn).expect("apply coven schema");
1062 let mut statement = conn
1063 .prepare("PRAGMA table_info(circle_operations)")
1064 .expect("prepare circle_operations table_info");
1065 let columns = statement
1066 .query_map([], |row| row.get::<_, String>(1))
1067 .expect("query circle_operations columns")
1068 .collect::<rusqlite::Result<Vec<_>>>()
1069 .expect("read circle_operations columns");
1070
1071 assert_eq!(columns, ["operation_id", "circle_id", "payload"]);
1072 }
1073
1074 #[test]
1075 fn author_exclusion_activation_locator_has_one_exact_row_shape() {
1076 let conn = rusqlite::Connection::open_in_memory().expect("open in-memory");
1077 apply_coven_schema(&conn).expect("apply coven schema");
1078 let columns = conn
1079 .prepare("PRAGMA table_info(store_author_exclusion_activations)")
1080 .expect("prepare exclusion activation table_info")
1081 .query_map([], |row| {
1082 Ok((row.get::<_, String>(1)?, row.get::<_, i64>(5)?))
1083 })
1084 .expect("query exclusion activation columns")
1085 .map(|row| row.expect("read exclusion activation column"))
1086 .collect::<Vec<_>>();
1087
1088 assert_eq!(
1089 columns,
1090 [
1091 ("exclusion_ref".to_string(), 1),
1092 ("accepted_cut".to_string(), 0),
1093 ("activation_commit".to_string(), 0),
1094 ("activation_head".to_string(), 0),
1095 ]
1096 );
1097 }
1098
1099 #[test]
1100 fn merge_materialization_requires_its_exact_retained_input() {
1101 let conn = rusqlite::Connection::open_in_memory().expect("open in-memory");
1102 conn.pragma_update(None, "foreign_keys", "ON")
1103 .expect("enable foreign keys");
1104 apply_coven_schema(&conn).expect("apply coven schema");
1105
1106 conn.execute(
1107 "INSERT INTO materialized_commits (device_id, seq, commit_ref)
1108 VALUES ('merge-stream', 1, '{}')",
1109 [],
1110 )
1111 .expect_err("Merge materialization without retained input must fail");
1112
1113 conn.execute(
1114 "INSERT INTO retained_merge_materializations
1115 (device_id, seq, commit_ref, input_hash, canonical_input)
1116 VALUES ('merge-stream', 1, '{}', ?1, x'7b7d')",
1117 ["a".repeat(64)],
1118 )
1119 .expect("retain exact Merge input");
1120 conn.execute(
1121 "INSERT INTO materialized_commits
1122 (device_id, seq, commit_ref, retained_commit_ref, retained_input_hash)
1123 VALUES ('merge-stream', 1, '{}', '{}', ?1)",
1124 ["a".repeat(64)],
1125 )
1126 .expect("materialize Merge commit with retained input");
1127 }
1128
1129 #[test]
1130 fn retained_replay_baseline_has_one_closed_active_row() {
1131 let conn = rusqlite::Connection::open_in_memory().expect("open in-memory");
1132 apply_coven_schema(&conn).expect("apply coven schema");
1133 let insert = |singleton: i64, generation: i64| {
1134 conn.execute(
1135 "INSERT INTO retained_replay_baselines
1136 (singleton, generation, exact_cut, schema_version,
1137 routing_hash, image_hash, image_bytes, authority_bytes)
1138 VALUES (?1, ?2, '{}', 1, ?3, ?4, x'01', x'01')",
1139 rusqlite::params![singleton, generation, "a".repeat(64), "b".repeat(64)],
1140 )
1141 };
1142
1143 insert(1, -1).expect_err("baseline generation cannot be negative");
1144 insert(1, 0).expect("insert generation-zero baseline");
1145 insert(1, 1).expect_err("a second active baseline must fail");
1146 insert(2, 1).expect_err("the baseline key must remain the singleton");
1147 insert(0, 1).expect_err("baseline generation cannot use another singleton key");
1148 }
1149
1150 #[test]
1151 fn retained_replay_object_index_requires_both_exact_records() {
1152 let conn = rusqlite::Connection::open_in_memory().expect("open in-memory");
1153 conn.pragma_update(None, "foreign_keys", "ON")
1154 .expect("enable foreign keys");
1155 apply_coven_schema(&conn).expect("apply coven schema");
1156 let input_hash = "a".repeat(64);
1157 let object_id = "b".repeat(64);
1158 conn.execute(
1159 "INSERT INTO retained_merge_materializations
1160 (device_id, seq, commit_ref, input_hash, canonical_input)
1161 VALUES ('merge-stream', 1, '{}', ?1, x'7b7d')",
1162 [&input_hash],
1163 )
1164 .expect("retain exact Merge input");
1165 conn.execute(
1166 "INSERT INTO remote_objects (object_id, state) VALUES (?1, '{}')",
1167 [&object_id],
1168 )
1169 .expect("insert exact remote object");
1170 conn.execute(
1171 "INSERT INTO retained_replay_objects
1172 (device_id, seq, commit_ref, input_hash, object_id)
1173 VALUES ('merge-stream', 1, '{}', ?1, ?2)",
1174 [&input_hash, &object_id],
1175 )
1176 .expect("index retained replay ownership");
1177
1178 assert!(conn
1179 .execute(
1180 "DELETE FROM retained_merge_materializations
1181 WHERE device_id = 'merge-stream' AND seq = 1",
1182 [],
1183 )
1184 .is_err());
1185 assert!(conn
1186 .execute(
1187 "DELETE FROM remote_objects WHERE object_id = ?1",
1188 [&object_id],
1189 )
1190 .is_err());
1191 }
1192
1193 #[test]
1194 fn circle_operation_journal_allows_one_pending_operation_per_circle() {
1195 let conn = rusqlite::Connection::open_in_memory().expect("open in-memory");
1196 apply_coven_schema(&conn).expect("apply coven schema");
1197 conn.execute(
1198 "INSERT INTO circle_operations (operation_id, circle_id, payload)
1199 VALUES ('operation-a', 'circle-a', x'7b7d')",
1200 [],
1201 )
1202 .expect("insert first pending Circle operation");
1203
1204 let error = conn
1205 .execute(
1206 "INSERT INTO circle_operations (operation_id, circle_id, payload)
1207 VALUES ('operation-b', 'circle-a', x'7b7d')",
1208 [],
1209 )
1210 .expect_err("a Circle cannot have a second pending operation");
1211
1212 assert!(error.to_string().contains("circle_operations.circle_id"));
1213 }
1214
1215 #[test]
1216 fn prepared_blob_identity_is_the_exact_remote_object() {
1217 let conn = rusqlite::Connection::open_in_memory().expect("open in-memory");
1218 apply_coven_schema(&conn).expect("apply coven schema");
1219 let primary_key = conn
1220 .prepare("PRAGMA table_info(store_write_blobs)")
1221 .expect("prepare store_write_blobs table_info")
1222 .query_map([], |row| {
1223 Ok((row.get::<_, String>(1)?, row.get::<_, i64>(5)?))
1224 })
1225 .expect("query store_write_blobs columns")
1226 .filter_map(|row| {
1227 let (name, position) = row.expect("read store_write_blobs column");
1228 (position != 0).then_some((position, name))
1229 })
1230 .collect::<std::collections::BTreeMap<_, _>>();
1231
1232 assert_eq!(
1233 primary_key.into_values().collect::<Vec<_>>(),
1234 ["write_id", "audience", "remote_object_id"]
1235 );
1236 }
1237
1238 #[test]
1239 fn routing_tables_are_strict_without_rowid() {
1240 let conn = rusqlite::Connection::open_in_memory().expect("open in-memory");
1241 apply_coven_routing_schema(&conn).expect("apply routing schema");
1242 for name in ["_coven_audience", "_coven_row_routes"] {
1243 let sql = format!(
1244 "PRAGMA table_list({})",
1245 crate::sync::session::quote_ident(name)
1246 );
1247 let (wr, strict): (i64, i64) = conn
1248 .query_row(&sql, [], |row| Ok((row.get(4)?, row.get(5)?)))
1249 .expect("table_list");
1250 assert_eq!((wr, strict), (1, 1), "{name}");
1251 }
1252 }
1253}
1254
1255#[derive(Debug, Clone, PartialEq, Eq)]
1261pub struct ExternalBlob {
1262 pub path: std::path::PathBuf,
1264 pub size: u64,
1267}
1268
1269#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1273pub enum MakeRemoteProgress {
1274 Uploading,
1276 Cancelling,
1278 Publishing,
1280}
1281
1282#[derive(Debug, Clone, PartialEq, Eq)]
1288pub struct QueuedDelete {
1289 pub namespace: String,
1291 pub blob_id: String,
1293 pub attempt_count: u64,
1295 pub last_error: Option<String>,
1297 pub created_at: String,
1299 pub last_attempt_at: Option<String>,
1301}
1302
1303#[derive(Debug, Clone, PartialEq, Eq)]
1312pub struct QueuedUpload {
1313 pub namespace: String,
1315 pub blob_id: String,
1317 pub table_name: String,
1319 pub row_id: String,
1320 pub root_table: String,
1323 pub root_id: String,
1324 pub retain_pinned: bool,
1327 pub attempt_count: u64,
1329 pub last_error: Option<String>,
1331 pub created_at: String,
1333 pub last_attempt_at: Option<String>,
1335}
1336
1337#[derive(Debug, Clone, PartialEq, Eq)]
1338pub struct OutboxEntry {
1339 pub id: i64,
1340 pub attempt_count: i64,
1341 pub last_attempt_at: Option<String>,
1342 pub operation: OutboxOperation,
1343}
1344
1345#[derive(Debug, Clone, PartialEq, Eq)]
1346pub enum OutboxOperation {
1347 Upload {
1348 root_table: String,
1349 root_id: String,
1350 row: crate::blob::RowBlobRef,
1351 source_path: std::path::PathBuf,
1352 retain_pinned: bool,
1353 state: OutboxUploadState,
1354 },
1355 Delete {
1356 stored: crate::blob::locator::StoredBlobRef,
1357 },
1358}
1359
1360#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1361#[serde(rename_all = "snake_case", deny_unknown_fields)]
1362pub enum OutboxUploadState {
1363 Pending,
1364 Prepared {
1365 authority: crate::sync::audience_package::PackageAudience,
1366 stored: crate::blob::locator::StoredBlobRef,
1367 spool_path: std::path::PathBuf,
1368 },
1369 Created {
1370 authority: crate::sync::audience_package::PackageAudience,
1371 stored: crate::blob::locator::StoredBlobRef,
1372 spool_path: std::path::PathBuf,
1373 },
1374}