1use crate::coven_schema_definitions::{
13 BLOB_MAKE_REMOTE_INTENTS_COLUMNS, BLOB_MAKE_REMOTE_INTENTS_V0_COLUMNS, CLOUD_OUTBOX_COLUMNS,
14 CLOUD_OUTBOX_V0_COLUMNS, OBJECT_OWNERSHIP_TRIGGERS,
15};
16use crate::{query_mapped_rows, DbError};
17
18macro_rules! coven_tables {
19 ($visit:ident) => {
20 $visit!(
21 protocol_state,
22 "
23 key TEXT PRIMARY KEY,
24 value TEXT NOT NULL
25"
26 );
27 $visit!(
28 retained_replay_baselines,
29 "
30 singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
31 generation INTEGER NOT NULL CHECK (generation >= 0),
32 exact_cut TEXT NOT NULL CHECK (json_valid(exact_cut)),
33 schema_version INTEGER NOT NULL CHECK (schema_version >= 0),
34 routing_hash TEXT NOT NULL CHECK (length(routing_hash) = 64),
35 image_payload_hash TEXT NOT NULL CHECK (length(image_payload_hash) = 64),
36 authority_hash TEXT NOT NULL CHECK (length(authority_hash) = 64)
37"
38 );
39 $visit!(
40 retained_replay_blob_leases,
41 "
42 namespace TEXT NOT NULL,
43 blob_id TEXT NOT NULL,
44 PRIMARY KEY (namespace, blob_id)
45"
46 );
47 $visit!(
48 circle_bootstrap_coverage,
49 "
50 circle_id TEXT PRIMARY KEY,
51 control_coord TEXT NOT NULL CHECK (json_valid(control_coord)),
52 activation_commit TEXT NOT NULL CHECK (json_valid(activation_commit)),
53 exact_cut TEXT NOT NULL CHECK (json_valid(exact_cut)),
54 image_hash TEXT NOT NULL CHECK (length(image_hash) = 64),
55 bootstrap_ref BLOB NOT NULL CHECK (length(bootstrap_ref) > 0)
56"
57 );
58 $visit!(
59 circle_close_exclusions,
60 "
61 circle_id TEXT PRIMARY KEY,
62 close_id TEXT NOT NULL,
63 excluded_registration TEXT NOT NULL CHECK (json_valid(excluded_registration)),
64 successor_control TEXT NOT NULL CHECK (json_valid(successor_control)),
65 activating_commit TEXT NOT NULL CHECK (json_valid(activating_commit))
66"
67 );
68 $visit!(
69 retained_merge_materializations,
70 "
71 device_id TEXT NOT NULL,
72 seq INTEGER NOT NULL CHECK (seq > 0),
73 commit_ref TEXT NOT NULL CHECK (json_valid(commit_ref)),
74 input_hash TEXT NOT NULL CHECK (length(input_hash) = 64),
75 canonical_input BLOB NOT NULL CHECK (length(canonical_input) > 0),
76 PRIMARY KEY (device_id, seq),
77 UNIQUE (device_id, seq, commit_ref, input_hash)
78"
79 );
80 $visit!(
81 merge_retraction_cleanups,
82 "
83 device_id TEXT NOT NULL,
84 seq INTEGER NOT NULL CHECK (seq > 0),
85 commit_ref TEXT NOT NULL CHECK (json_valid(commit_ref)),
86 cleanup_hash TEXT NOT NULL CHECK (length(cleanup_hash) = 64),
87 canonical_cleanup BLOB NOT NULL CHECK (length(canonical_cleanup) > 0),
88 PRIMARY KEY (device_id, seq),
89 UNIQUE (commit_ref)
90"
91 );
92 $visit!(
93 materialized_commits,
94 "
95 device_id TEXT NOT NULL,
96 seq INTEGER NOT NULL CHECK (seq > 0),
97 commit_ref TEXT NOT NULL CHECK (json_valid(commit_ref)),
98 retained_commit_ref TEXT NOT NULL CHECK (json_valid(retained_commit_ref)),
99 retained_input_hash TEXT NOT NULL CHECK (length(retained_input_hash) = 64),
100 PRIMARY KEY (device_id, seq),
101 FOREIGN KEY (device_id, seq, retained_commit_ref, retained_input_hash)
102 REFERENCES retained_merge_materializations(device_id, seq, commit_ref, input_hash)
103"
104 );
105 $visit!(
106 stream_activations,
107 "
108 activation_id TEXT PRIMARY KEY CHECK (length(activation_id) = 64),
109 author_stream_id TEXT NOT NULL UNIQUE CHECK (length(author_stream_id) = 64),
110 activation BLOB NOT NULL,
111 activating_commit TEXT NOT NULL CHECK (json_valid(activating_commit))
112"
113 );
114 $visit!(
115 snapshot_coverage,
116 "
117 device_id TEXT PRIMARY KEY,
118 seq INTEGER NOT NULL CHECK (seq > 0),
119 commit_ref TEXT NOT NULL CHECK (json_valid(commit_ref)),
120 snapshot_hash TEXT NOT NULL CHECK (length(snapshot_hash) = 64)
121"
122 );
123 $visit!(
124 store_publication_current,
125 "
126 singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
127 record_hash TEXT NOT NULL UNIQUE CHECK (length(record_hash) = 64),
128 record_bytes BLOB NOT NULL CHECK (length(record_bytes) > 0),
129 provider_version TEXT NOT NULL CHECK (length(provider_version) > 0)
130"
131 );
132 $visit!(
133 local_blob_refs,
134 "
135 table_name TEXT NOT NULL,
136 row_id TEXT NOT NULL,
137 column_name TEXT NOT NULL,
138 row_stamp TEXT NOT NULL,
139 namespace TEXT NOT NULL,
140 blob_id TEXT NOT NULL,
141 path TEXT NOT NULL,
142 plaintext_size INTEGER NOT NULL CHECK (plaintext_size >= 0),
143 plaintext_hash TEXT NOT NULL CHECK (length(plaintext_hash) = 64),
144 PRIMARY KEY (table_name, row_id, column_name, row_stamp)
145"
146 );
147 $visit!(cloud_outbox, CLOUD_OUTBOX_COLUMNS);
148 $visit!(blob_make_remote_intents, BLOB_MAKE_REMOTE_INTENTS_COLUMNS);
149 $visit!(
150 local_cleanup_intents,
151 "
152 namespace TEXT NOT NULL,
153 blob_id TEXT NOT NULL,
154 copy_identity TEXT NOT NULL CHECK (copy_identity = 'local' OR length(copy_identity) = 64),
155 PRIMARY KEY (namespace, blob_id, copy_identity)
156"
157 );
158 $visit!(
159 published_blob_drop_intents,
160 "
161 seq INTEGER NOT NULL CHECK (seq > 0),
162 namespace TEXT NOT NULL,
163 blob_id TEXT NOT NULL,
164 size INTEGER NOT NULL CHECK (size >= 0),
165 plaintext_hash TEXT NOT NULL,
166 locator_hash TEXT NOT NULL,
167 disposition TEXT NOT NULL CHECK (disposition IN ('drop', 'cache', 'pin')),
168 PRIMARY KEY (seq, namespace, blob_id, locator_hash)
169"
170 );
171 $visit!(
172 store_writes,
173 "
174 ordinal INTEGER PRIMARY KEY AUTOINCREMENT,
175 write_id TEXT NOT NULL UNIQUE,
176 status TEXT NOT NULL CHECK (json_valid(status)),
177 affected_rows TEXT CHECK (affected_rows IS NULL OR json_valid(affected_rows)),
178 changeset_hash TEXT CHECK (changeset_hash IS NULL OR length(changeset_hash) = 64),
179 base TEXT CHECK (base IS NULL OR json_valid(base)),
180 blob_facts TEXT CHECK (blob_facts IS NULL OR json_valid(blob_facts)),
181 prepared TEXT CHECK (prepared IS NULL OR json_valid(prepared))
182"
183 );
184 $visit!(
185 store_write_blob_leases,
186 "
187 write_id TEXT NOT NULL,
188 namespace TEXT NOT NULL,
189 blob_id TEXT NOT NULL,
190 PRIMARY KEY (write_id, namespace, blob_id),
191 FOREIGN KEY (write_id) REFERENCES store_writes(write_id)
192"
193 );
194 $visit!(
195 store_write_partitions,
196 "
197 write_id TEXT NOT NULL,
198 audience TEXT NOT NULL,
199 control_coord TEXT,
200 changeset_hash TEXT NOT NULL CHECK (length(changeset_hash) = 64),
201 PRIMARY KEY (write_id, audience),
202 FOREIGN KEY (write_id) REFERENCES store_writes(write_id) ON DELETE CASCADE,
203 CHECK (
204 (audience IN ('store', 'local') AND control_coord IS NULL)
205 OR
206 (audience NOT IN ('store', 'local') AND json_valid(control_coord))
207 )
208"
209 );
210 $visit!(
211 remote_objects,
212 "
213 object_id TEXT PRIMARY KEY CHECK (length(object_id) = 64),
214 state TEXT NOT NULL CHECK (json_valid(state))
215"
216 );
217 $visit!(
218 retained_replay_objects,
219 "
220 device_id TEXT NOT NULL,
221 seq INTEGER NOT NULL CHECK (seq > 0),
222 commit_ref TEXT NOT NULL CHECK (json_valid(commit_ref)),
223 input_hash TEXT NOT NULL CHECK (length(input_hash) = 64),
224 object_id TEXT NOT NULL CHECK (length(object_id) = 64),
225 PRIMARY KEY (device_id, seq, object_id),
226 FOREIGN KEY (device_id, seq, commit_ref, input_hash)
227 REFERENCES retained_merge_materializations(device_id, seq, commit_ref, input_hash),
228 FOREIGN KEY (object_id) REFERENCES remote_objects(object_id)
229"
230 );
231 $visit!(
232 protocol_inert_objects,
233 "
234 object_id TEXT PRIMARY KEY CHECK (length(object_id) = 64),
235 state TEXT NOT NULL CHECK (json_valid(state))
236"
237 );
238 $visit!(
239 reclaimed_store_packages,
240 "
241 object_id TEXT PRIMARY KEY CHECK (length(object_id) = 64),
242 authorization_hash TEXT NOT NULL UNIQUE CHECK (length(authorization_hash) = 64),
243 state TEXT NOT NULL CHECK (json_valid(state)),
244 FOREIGN KEY (authorization_hash) REFERENCES store_reclaim_operations(authorization_hash)
245"
246 );
247 $visit!(
248 store_write_packages,
249 "
250 write_id TEXT NOT NULL,
251 audience TEXT NOT NULL,
252 remote_object_id TEXT NOT NULL CHECK (length(remote_object_id) = 64),
253 PRIMARY KEY (write_id, audience),
254 FOREIGN KEY (write_id) REFERENCES store_writes(write_id) ON DELETE CASCADE,
255 FOREIGN KEY (remote_object_id) REFERENCES remote_objects(object_id)
256"
257 );
258 $visit!(
259 store_write_blobs,
260 "
261 write_id TEXT NOT NULL,
262 audience TEXT NOT NULL,
263 locator_hash TEXT NOT NULL CHECK (length(locator_hash) = 64),
264 remote_object_id TEXT NOT NULL CHECK (length(remote_object_id) = 64),
265 spool_path TEXT,
266 PRIMARY KEY (write_id, audience, remote_object_id),
267 FOREIGN KEY (write_id) REFERENCES store_writes(write_id) ON DELETE CASCADE,
268 FOREIGN KEY (remote_object_id) REFERENCES remote_objects(object_id)
269"
270 );
271 $visit!(
272 blob_locators,
273 "
274 remote_object_id TEXT PRIMARY KEY CHECK (length(remote_object_id) = 64),
275 locator_hash TEXT NOT NULL CHECK (length(locator_hash) = 64),
276 FOREIGN KEY (remote_object_id) REFERENCES remote_objects(object_id)
277"
278 );
279 $visit!(
280 row_blob_locators,
281 "
282 table_name TEXT NOT NULL,
283 row_id TEXT NOT NULL,
284 column_name TEXT NOT NULL,
285 row_stamp TEXT NOT NULL,
286 audience_authority TEXT NOT NULL CHECK (json_valid(audience_authority)),
287 remote_object_id TEXT NOT NULL CHECK (length(remote_object_id) = 64),
288 PRIMARY KEY (table_name, row_id, column_name, row_stamp),
289 FOREIGN KEY (remote_object_id) REFERENCES blob_locators(remote_object_id)
290"
291 );
292 $visit!(
293 outbound_membership_mutation,
294 "
295 singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
296 intent_hash TEXT NOT NULL CHECK (length(intent_hash) = 64),
297 plan_bytes BLOB NOT NULL,
298 progress_bytes BLOB NOT NULL
299"
300 );
301 $visit!(
302 outbound_store_snapshot,
303 "
304 singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
305 snapshot_ref TEXT NOT NULL CHECK (json_valid(snapshot_ref)),
306 meta_prepared TEXT NOT NULL CHECK (json_valid(meta_prepared)),
307 image_ref TEXT NOT NULL CHECK (json_valid(image_ref)),
308 rollup_ref TEXT NOT NULL CHECK (json_valid(rollup_ref)),
309 meta_bytes BLOB NOT NULL,
310 blobs TEXT NOT NULL CHECK (json_valid(blobs))
311"
312 );
313 $visit!(
314 published_store_snapshot,
315 "
316 generation INTEGER PRIMARY KEY CHECK (generation >= 0),
317 snapshot_ref TEXT NOT NULL CHECK (json_valid(snapshot_ref)),
318 successor_slot TEXT NOT NULL CHECK (json_valid(successor_slot)),
319 meta_bytes BLOB NOT NULL
320"
321 );
322 $visit!(
323 snapshot_blob_spool_cleanup,
324 "
325 path TEXT PRIMARY KEY
326"
327 );
328 $visit!(
333 payload_storage,
334 "
335 payload_hash TEXT PRIMARY KEY CHECK (length(payload_hash) = 64),
336 payload_size INTEGER NOT NULL CHECK (payload_size >= 0),
337 storage TEXT NOT NULL CHECK (storage IN ('inline', 'file')),
338 compressed_bytes BLOB,
339 compressed_size INTEGER NOT NULL CHECK (compressed_size > 0),
340 CHECK (
341 (storage = 'inline' AND compressed_bytes IS NOT NULL
342 AND compressed_size = length(compressed_bytes)
343 AND compressed_size <= 65536)
344 OR
345 (storage = 'file' AND compressed_bytes IS NULL)
346 )
347"
348 );
349 $visit!(
353 payload_cleanup,
354 "
355 payload_hash TEXT PRIMARY KEY CHECK (length(payload_hash) = 64)
356 REFERENCES payload_storage(payload_hash)
357"
358 );
359 $visit!(
366 payload_owners,
367 "
368 payload_hash TEXT NOT NULL CHECK (length(payload_hash) = 64),
369 owner_key TEXT NOT NULL CHECK (length(owner_key) > 0),
370 PRIMARY KEY (payload_hash, owner_key),
371 FOREIGN KEY (payload_hash) REFERENCES payload_storage(payload_hash)
372"
373 );
374 $visit!(
375 outbound_circle_snapshot,
376 "
377 circle_id TEXT PRIMARY KEY,
378 snapshot_ref TEXT NOT NULL CHECK (json_valid(snapshot_ref)),
379 meta_prepared TEXT NOT NULL CHECK (json_valid(meta_prepared)),
380 image_ref TEXT NOT NULL CHECK (json_valid(image_ref)),
381 meta_bytes BLOB NOT NULL,
382 blobs TEXT NOT NULL CHECK (json_valid(blobs))
383"
384 );
385 $visit!(
386 published_circle_snapshot,
387 "
388 circle_id TEXT NOT NULL,
389 generation INTEGER NOT NULL CHECK (generation >= 0),
390 snapshot_ref TEXT NOT NULL CHECK (json_valid(snapshot_ref)),
391 successor_slot TEXT NOT NULL CHECK (json_valid(successor_slot)),
392 cut TEXT NOT NULL CHECK (json_valid(cut)),
393 meta_bytes BLOB NOT NULL,
394 PRIMARY KEY (circle_id, generation)
395"
396 );
397 $visit!(
398 outbound_store_acks,
399 "
400 singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
401 ack_ref TEXT NOT NULL CHECK (json_valid(ack_ref)),
402 ack_bytes BLOB NOT NULL,
403 prepared_object TEXT NOT NULL CHECK (json_valid(prepared_object)),
404 activation TEXT NOT NULL CHECK (json_valid(activation))
405"
406 );
407 $visit!(
408 published_store_acks,
409 "
410 singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
411 ack_ref TEXT NOT NULL CHECK (json_valid(ack_ref)),
412 successor_slot TEXT NOT NULL CHECK (json_valid(successor_slot)),
413 -- What that acknowledgement asserted, and the commit that carried it, so the
414 -- next cycle can tell whether it still holds. NULL on the acknowledgements
415 -- installed while bootstrapping a device, which computed no assertion of
416 -- their own: the first cycle after one of those has no basis to skip, so it
417 -- acknowledges and records what it said.
418 standing TEXT CHECK (standing IS NULL OR json_valid(standing))
419"
420 );
421 $visit!(
422 outbound_circle_acks,
423 "
424 circle_id TEXT PRIMARY KEY,
425 ack_ref TEXT NOT NULL CHECK (json_valid(ack_ref)),
426 ack_bytes BLOB NOT NULL,
427 prepared_object TEXT NOT NULL CHECK (json_valid(prepared_object))
428"
429 );
430 $visit!(
431 published_circle_acks,
432 "
433 circle_id TEXT PRIMARY KEY,
434 ack_ref TEXT NOT NULL CHECK (json_valid(ack_ref)),
435 successor_slot TEXT NOT NULL CHECK (json_valid(successor_slot)),
436 store_cut TEXT NOT NULL CHECK (json_valid(store_cut)),
437 control_coord TEXT NOT NULL CHECK (json_valid(control_coord))
438"
439 );
440 $visit!(
441 activated_circle_acks,
442 "
443 circle_id TEXT NOT NULL,
444 device_id TEXT NOT NULL,
445 ack_ref TEXT NOT NULL CHECK (json_valid(ack_ref)),
446 activating_commit TEXT NOT NULL CHECK (json_valid(activating_commit)),
447 PRIMARY KEY (circle_id, device_id)
448"
449 );
450 $visit!(
451 activated_store_acks,
452 "
453 device_id TEXT PRIMARY KEY,
454 ack_ref TEXT NOT NULL CHECK (json_valid(ack_ref)),
455 activating_commit TEXT NOT NULL CHECK (json_valid(activating_commit))
456"
457 );
458 $visit!(
459 store_device_exclusion_freezes,
460 "
461 proposal_id TEXT PRIMARY KEY CHECK (length(proposal_id) = 64),
462 proposal_ref TEXT NOT NULL CHECK (json_valid(proposal_ref)),
463 target_cut TEXT NOT NULL CHECK (json_valid(target_cut))
464"
465 );
466 $visit!(
467 outbound_store_device_exclusion,
468 "
469 operation_id TEXT PRIMARY KEY CHECK (length(operation_id) = 64),
470 active_key INTEGER UNIQUE CHECK (active_key IS NULL OR active_key = 1),
471 state TEXT NOT NULL CHECK (json_valid(state))
472"
473 );
474 $visit!(
475 store_reclaim_operations,
476 "
477 authorization_hash TEXT PRIMARY KEY CHECK (length(authorization_hash) = 64),
478 state TEXT NOT NULL CHECK (json_valid(state)),
479 stuck_error TEXT CHECK (stuck_error IS NULL OR length(stuck_error) > 0)
480"
481 );
482 $visit!(
483 store_protocol_root_authority,
484 "
485 singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
486 store_root_hash TEXT NOT NULL CHECK (length(store_root_hash) = 64),
487 store_protocol_root_bytes BLOB NOT NULL,
488 store_root_object TEXT NOT NULL CHECK (json_valid(store_root_object))
489"
490 );
491 $visit!(
492 local_store_protocol_root,
493 "
494 singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
495 store_root_hash TEXT NOT NULL CHECK (length(store_root_hash) = 64),
496 store_protocol_root_bytes BLOB NOT NULL,
497 prepared_object TEXT NOT NULL CHECK (json_valid(prepared_object))
498"
499 );
500 $visit!(
501 local_store_device_registration,
502 "
503 singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
504 device_id TEXT NOT NULL UNIQUE,
505 registration_hash TEXT NOT NULL UNIQUE CHECK (length(registration_hash) = 64),
506 registration_bytes BLOB NOT NULL,
507 prepared_object TEXT NOT NULL CHECK (json_valid(prepared_object)),
508 initial_ack_ref TEXT NOT NULL CHECK (json_valid(initial_ack_ref)),
509 initial_ack_bytes BLOB NOT NULL,
510 initial_ack_prepared TEXT NOT NULL CHECK (json_valid(initial_ack_prepared)),
511 state TEXT NOT NULL CHECK (json_valid(state))
512"
513 );
514 $visit!(
515 local_owner_recovery_publication,
516 "
517 singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
518 registration_hash TEXT NOT NULL UNIQUE CHECK (length(registration_hash) = 64),
519 publication TEXT NOT NULL CHECK (json_valid(publication)),
520 FOREIGN KEY (registration_hash)
521 REFERENCES local_store_device_registration(registration_hash) ON DELETE CASCADE
522"
523 );
524 $visit!(
525 local_store_founder_graph,
526 "
527 singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
528 membership_graph TEXT NOT NULL CHECK (json_valid(membership_graph))
529"
530 );
531 $visit!(
532 store_device_registration_activations,
533 "
534 device_id TEXT PRIMARY KEY,
535 registration_hash TEXT NOT NULL CHECK (length(registration_hash) = 64),
536 author_pubkey TEXT NOT NULL,
537 device_signing_pubkey TEXT NOT NULL,
538 registration_bytes BLOB NOT NULL,
539 registration_object TEXT NOT NULL CHECK (json_valid(registration_object)),
540 activation_authority TEXT NOT NULL CHECK (json_valid(activation_authority)),
541 UNIQUE (device_id, registration_hash),
542 UNIQUE (registration_object)
543"
544 );
545 $visit!(
546 store_device_states,
547 "
548 state_hash TEXT PRIMARY KEY CHECK (length(state_hash) = 64),
549 state TEXT NOT NULL CHECK (json_valid(state)),
550 CHECK (json_extract(state, '$.state_hash') IS state_hash)
551"
552 );
553 $visit!(
554 store_device_state_snapshots,
555 "
556 commit_ref TEXT PRIMARY KEY CHECK (json_valid(commit_ref)),
557 state_hash TEXT NOT NULL REFERENCES store_device_states(state_hash)
558"
559 );
560 $visit!(
561 store_author_exclusion_activations,
562 "
563 exclusion_ref TEXT PRIMARY KEY CHECK (json_valid(exclusion_ref)),
564 accepted_cut TEXT NOT NULL CHECK (json_valid(accepted_cut)),
565 activation_commit TEXT NOT NULL CHECK (json_valid(activation_commit)),
566 activation_head TEXT NOT NULL CHECK (json_valid(activation_head))
567"
568 );
569 $visit!(
570 circle_control_activations,
571 "
572 circle_id TEXT NOT NULL,
573 control_coord TEXT NOT NULL CHECK (json_valid(control_coord)),
574 stream_id TEXT NOT NULL,
575 seq INTEGER NOT NULL CHECK (seq > 0),
576 commit_hash TEXT NOT NULL CHECK (length(commit_hash) = 64),
577 control_bytes BLOB NOT NULL,
578 PRIMARY KEY (circle_id, control_coord),
579 UNIQUE (circle_id, stream_id, seq)
580"
581 );
582 $visit!(
583 circle_operations,
584 "
585 operation_id TEXT PRIMARY KEY,
586 circle_id TEXT NOT NULL UNIQUE,
587 prepared BLOB NOT NULL,
588 phase TEXT NOT NULL CHECK (json_valid(phase))
589"
590 );
591 $visit!(
597 circle_operation_uploads,
598 "
599 operation_id TEXT NOT NULL,
600 step TEXT NOT NULL,
601 PRIMARY KEY (operation_id, step),
602 FOREIGN KEY (operation_id) REFERENCES circle_operations(operation_id) ON DELETE CASCADE
603"
604 );
605 $visit!(
606 circle_access_cache,
607 "
608 circle_id TEXT NOT NULL,
609 control_coord TEXT NOT NULL CHECK (json_valid(control_coord)),
610 owner_pubkey TEXT NOT NULL,
611 disposition TEXT NOT NULL CHECK (disposition IN ('active', 'inactive')),
612 PRIMARY KEY (circle_id, control_coord, owner_pubkey),
613 FOREIGN KEY (circle_id, control_coord)
614 REFERENCES circle_control_activations(circle_id, control_coord)
615"
616 );
617 $visit!(
618 circle_current_state,
619 "
620 circle_id TEXT PRIMARY KEY,
621 state BLOB NOT NULL
622"
623 );
624 };
625}
626
627macro_rules! coven_routing_tables {
628 ($visit:ident) => {
629 $visit!(
630 _coven_audience,
631 "
632 routing_id TEXT PRIMARY KEY,
633 circle_id TEXT,
634 _updated_at TEXT NOT NULL
635"
636 );
637 $visit!(
638 _coven_row_routes,
639 "
640 routing_id TEXT PRIMARY KEY,
641 table_name TEXT NOT NULL,
642 row_id TEXT NOT NULL,
643 _updated_at TEXT NOT NULL,
644 UNIQUE (table_name, row_id)
645"
646 );
647 };
648}
649
650#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
651#[serde(deny_unknown_fields)]
652pub struct CovenSchemaManifest {
653 objects: Vec<CovenSchemaObject>,
654 tables: Vec<CovenTableShape>,
655}
656
657impl CovenSchemaManifest {
658 pub fn is_empty(&self) -> bool {
659 self.objects.is_empty() && self.tables.is_empty()
660 }
661}
662
663#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
664#[serde(deny_unknown_fields)]
665struct CovenSchemaObject {
666 kind: String,
667 name: String,
668 table_name: String,
669 sql: Option<String>,
670}
671
672#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
673#[serde(deny_unknown_fields)]
674struct CovenTableShape {
675 name: String,
676 columns: i64,
677 without_rowid: bool,
678 strict: bool,
679}
680
681fn normalize_schema_sql(sql: &str) -> String {
682 let bytes = sql.as_bytes();
683 let mut normalized = String::with_capacity(sql.len());
684 let mut index = 0;
685 let mut pending_separator = false;
686 let mut quote = None;
687
688 while index < bytes.len() {
689 let byte = bytes[index];
690 if let Some(delimiter) = quote {
691 normalized.push(char::from(byte));
692 if byte == delimiter {
693 if bytes.get(index + 1) == Some(&delimiter) {
694 normalized.push(char::from(delimiter));
695 index += 1;
696 } else {
697 quote = None;
698 }
699 }
700 index += 1;
701 continue;
702 }
703
704 if byte == b'-' && bytes.get(index + 1) == Some(&b'-') {
705 index += 2;
706 while index < bytes.len() && !matches!(bytes[index], b'\n' | b'\r') {
707 index += 1;
708 }
709 pending_separator = true;
710 continue;
711 }
712 if byte.is_ascii_whitespace() {
713 pending_separator = true;
714 index += 1;
715 continue;
716 }
717 if matches!(byte, b'\'' | b'"' | b'`') {
718 if pending_separator
719 && normalized
720 .as_bytes()
721 .last()
722 .is_some_and(u8::is_ascii_alphanumeric)
723 {
724 normalized.push(' ');
725 }
726 pending_separator = false;
727 quote = Some(byte);
728 normalized.push(char::from(byte));
729 index += 1;
730 continue;
731 }
732 if pending_separator
733 && byte.is_ascii_alphanumeric()
734 && normalized
735 .as_bytes()
736 .last()
737 .is_some_and(u8::is_ascii_alphanumeric)
738 {
739 normalized.push(' ');
740 }
741 pending_separator = false;
742 normalized.push(char::from(byte.to_ascii_lowercase()));
743 index += 1;
744 }
745
746 normalized
747}
748
749fn all_coven_table_names() -> std::collections::BTreeSet<&'static str> {
750 let mut names = std::collections::BTreeSet::new();
751 macro_rules! collect_name {
752 ($name:ident, $columns:expr) => {
753 names.insert(stringify!($name));
754 };
755 }
756
757 coven_tables!(collect_name);
758 coven_routing_tables!(collect_name);
759 names
760}
761
762#[cfg(test)]
763pub(crate) fn all_table_names() -> std::collections::BTreeSet<&'static str> {
764 all_coven_table_names()
765}
766
767#[cfg(any(test, feature = "test-utils"))]
768#[derive(Clone, Copy)]
769pub struct DatabaseTestTable(pub &'static str);
770
771#[cfg(any(test, feature = "test-utils"))]
772impl DatabaseTestTable {
773 pub fn named(name: &'static str) -> Self {
774 assert!(
775 all_coven_table_names().contains(name),
776 "{name:?} is not a Coven-owned table"
777 );
778 Self(name)
779 }
780}
781
782pub(crate) fn live_coven_schema_manifest(
783 conn: &rusqlite::Connection,
784) -> rusqlite::Result<CovenSchemaManifest> {
785 let names = all_coven_table_names();
786 let mut objects = conn
787 .prepare(
788 "SELECT type, name, tbl_name, sql
789 FROM main.sqlite_schema
790 WHERE type IN ('table', 'index', 'trigger')
791 ORDER BY type, name",
792 )?
793 .query_map([], |row| {
794 Ok(CovenSchemaObject {
795 kind: row.get(0)?,
796 name: row.get(1)?,
797 table_name: row.get(2)?,
798 sql: row
799 .get::<_, Option<String>>(3)?
800 .map(|sql| normalize_schema_sql(&sql)),
801 })
802 })?
803 .collect::<rusqlite::Result<Vec<_>>>()?;
804 objects.retain(|object| names.contains(object.table_name.as_str()));
805
806 let mut tables = conn
807 .prepare("PRAGMA main.table_list")?
808 .query_map([], |row| {
809 Ok(CovenTableShape {
810 name: row.get(1)?,
811 columns: row.get(3)?,
812 without_rowid: row.get::<_, i64>(4)? != 0,
813 strict: row.get::<_, i64>(5)? != 0,
814 })
815 })?
816 .collect::<rusqlite::Result<Vec<_>>>()?;
817 tables.retain(|table| names.contains(table.name.as_str()));
818 tables.sort_by(|left, right| left.name.cmp(&right.name));
819
820 Ok(CovenSchemaManifest { objects, tables })
821}
822
823fn build_expected_coven_schema_manifest(
824 include_routing: bool,
825) -> rusqlite::Result<CovenSchemaManifest> {
826 let conn = rusqlite::Connection::open_in_memory()?;
827 apply_coven_schema(&conn)?;
828 if include_routing {
829 apply_coven_routing_schema(&conn)?;
830 }
831 live_coven_schema_manifest(&conn)
832}
833
834fn recreate_table(conn: &rusqlite::Connection, table: &str, columns: &str) -> rusqlite::Result<()> {
835 conn.execute_batch(&format!(
836 "DROP TABLE {table}; CREATE TABLE {table} ({columns}) STRICT;"
837 ))
838}
839
840fn build_expected_coven_schema_v0_manifest(
841 include_routing: bool,
842) -> rusqlite::Result<CovenSchemaManifest> {
843 let conn = rusqlite::Connection::open_in_memory()?;
844 apply_coven_schema(&conn)?;
845 if include_routing {
846 apply_coven_routing_schema(&conn)?;
847 }
848 recreate_table(&conn, "cloud_outbox", CLOUD_OUTBOX_V0_COLUMNS)?;
849 recreate_table(
850 &conn,
851 "blob_make_remote_intents",
852 BLOB_MAKE_REMOTE_INTENTS_V0_COLUMNS,
853 )?;
854 live_coven_schema_manifest(&conn)
855}
856
857static EXPECTED_COVEN_SCHEMA: std::sync::LazyLock<Result<CovenSchemaManifest, rusqlite::Error>> =
858 std::sync::LazyLock::new(|| build_expected_coven_schema_manifest(false));
859static EXPECTED_ROUTED_COVEN_SCHEMA: std::sync::LazyLock<
860 Result<CovenSchemaManifest, rusqlite::Error>,
861> = std::sync::LazyLock::new(|| build_expected_coven_schema_manifest(true));
862static EXPECTED_COVEN_SCHEMA_V0: std::sync::LazyLock<Result<CovenSchemaManifest, rusqlite::Error>> =
863 std::sync::LazyLock::new(|| build_expected_coven_schema_v0_manifest(false));
864static EXPECTED_ROUTED_COVEN_SCHEMA_V0: std::sync::LazyLock<
865 Result<CovenSchemaManifest, rusqlite::Error>,
866> = std::sync::LazyLock::new(|| build_expected_coven_schema_v0_manifest(true));
867
868pub fn expected_coven_schema_manifest(
869 include_routing: bool,
870) -> Result<&'static CovenSchemaManifest, DbError> {
871 let expected = if include_routing {
872 &*EXPECTED_ROUTED_COVEN_SCHEMA
873 } else {
874 &*EXPECTED_COVEN_SCHEMA
875 };
876 expected.as_ref().map_err(DbError::ExpectedSchema)
877}
878
879pub(crate) fn expected_coven_schema_v0_manifest(
880 include_routing: bool,
881) -> Result<&'static CovenSchemaManifest, DbError> {
882 let expected = if include_routing {
883 &*EXPECTED_ROUTED_COVEN_SCHEMA_V0
884 } else {
885 &*EXPECTED_COVEN_SCHEMA_V0
886 };
887 expected.as_ref().map_err(DbError::ExpectedSchema)
888}
889
890pub(crate) fn recreate_current_transition_tables(
891 conn: &rusqlite::Connection,
892) -> rusqlite::Result<()> {
893 recreate_table(conn, "cloud_outbox", CLOUD_OUTBOX_COLUMNS)?;
894 recreate_table(
895 conn,
896 "blob_make_remote_intents",
897 BLOB_MAKE_REMOTE_INTENTS_COLUMNS,
898 )
899}
900
901#[cfg(any(test, feature = "test-utils"))]
902pub(crate) fn downgrade_coven_schema_to_v0_for_test(
903 conn: &rusqlite::Connection,
904 include_routing: bool,
905) -> Result<(), DbError> {
906 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
907 recreate_table(&tx, "cloud_outbox", CLOUD_OUTBOX_V0_COLUMNS).map_err(DbError::from)?;
908 recreate_table(
909 &tx,
910 "blob_make_remote_intents",
911 BLOB_MAKE_REMOTE_INTENTS_V0_COLUMNS,
912 )
913 .map_err(DbError::from)?;
914 let manifest = serde_json::to_string(expected_coven_schema_v0_manifest(include_routing)?)
915 .map_err(DbError::from)?;
916 tx.execute(
917 "UPDATE protocol_state SET value = ?2 WHERE key = ?1",
918 (crate::COVEN_SCHEMA_MANIFEST_STATE_KEY, manifest),
919 )
920 .map_err(DbError::from)?;
921 tx.execute(
922 "DELETE FROM protocol_state WHERE key = ?1",
923 [crate::COVEN_SCHEMA_VERSION_STATE_KEY],
924 )
925 .map_err(DbError::from)?;
926 tx.commit().map_err(DbError::from)
927}
928
929pub(crate) fn apply_coven_schema(conn: &rusqlite::Connection) -> rusqlite::Result<()> {
934 macro_rules! apply_table {
935 ($name:ident, $columns:expr) => {
936 conn.execute_batch(&format!(
937 "CREATE TABLE IF NOT EXISTS {} ({}) STRICT;",
938 stringify!($name),
939 $columns,
940 ))?;
941 };
942 }
943
944 coven_tables!(apply_table);
945 conn.execute_batch(
946 "CREATE INDEX IF NOT EXISTS store_device_state_snapshots_by_state
947 ON store_device_state_snapshots(state_hash);",
948 )?;
949 conn.execute_batch(OBJECT_OWNERSHIP_TRIGGERS)?;
950 Ok(())
951}
952
953pub(crate) fn apply_coven_routing_schema(conn: &rusqlite::Connection) -> rusqlite::Result<()> {
957 macro_rules! apply_table {
958 ($name:ident, $columns:expr) => {
959 conn.execute_batch(&format!(
960 "CREATE TABLE IF NOT EXISTS {} ({}) STRICT, WITHOUT ROWID;",
961 stringify!($name),
962 $columns,
963 ))?;
964 };
965 }
966
967 coven_routing_tables!(apply_table);
968 Ok(())
969}
970
971pub fn is_reserved_table_name(name: &str) -> bool {
974 macro_rules! matches_table {
975 ($table:ident, $columns:expr) => {
976 if name == stringify!($table) {
977 return true;
978 }
979 };
980 }
981
982 coven_tables!(matches_table);
983 coven_routing_tables!(matches_table);
984 false
985}
986
987pub(crate) fn user_table_names(conn: &rusqlite::Connection) -> rusqlite::Result<Vec<String>> {
991 let tables = query_mapped_rows(
992 conn,
993 "SELECT name FROM main.sqlite_schema
994 WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
995 ORDER BY name",
996 [],
997 |row| row.get::<_, String>(0),
998 )?;
999 Ok(tables)
1000}
1001
1002#[cfg(test)]
1006pub(crate) fn table_names() -> Vec<&'static str> {
1007 let mut names = Vec::new();
1008 macro_rules! collect_name {
1009 ($name:ident, $columns:expr) => {
1010 names.push(stringify!($name));
1011 };
1012 }
1013
1014 coven_tables!(collect_name);
1015 names
1016}
1017
1018#[cfg(test)]
1019#[path = "coven_schema_tests.rs"]
1020mod tests;