1use super::InputDatabase;
8use crate::database::{start_call, Database, DatabaseInner};
9use crate::snapshot_error::{invalid, SnapshotError};
10use crate::snapshot_layout::{SnapshotLayout, TableLayout};
11use crate::snapshot_metadata::SnapshotMetadata;
12use crate::sqlite::DatabaseConnection;
13use crate::write_encoding::{encoded, sql_value};
14use crate::write_metadata::MetadataFailure;
15use crate::write_schema::WriteSchema;
16use crate::DbError;
17use coven_format::{
18 admin::SnapshotId,
19 merge_fields,
20 sealed_snapshot::SnapshotObjectPrefix,
21 snapshot::{section, SnapshotHeader, SnapshotRecord},
22 snapshot_rows::{AuthoredRecord, MergeRow},
23 snapshot_stream::SnapshotChunkDecoder,
24 value::Value,
25 write::WriteHeader,
26};
27use coven_foundation::locks::read_unpoisoned;
28use rusqlite::{params, params_from_iter};
29use std::collections::BTreeSet;
30use std::sync::Arc;
31
32pub(super) fn create_tables(db: &DatabaseConnection) -> Result<(), DbError> {
37 db.batch("PRAGMA temp_store=FILE;")?;
38 crate::snapshot_metadata::create_table(db)
39}
40
41struct StagedTables {
44 id: i64,
45 tables: Vec<String>,
46}
47
48pub(crate) fn table(staging: i64, name: &str) -> String {
50 format!("temp._coven_staged_{staging}_{name}")
51}
52
53#[path = "staged_damage.rs"]
54mod damage;
55use damage::with_staged_digest;
56#[cfg(any(test, feature = "test-utils"))]
57pub use damage::StagedDamage;
58pub use damage::StagedDigest;
59
60fn create_staged(
61 db: &DatabaseConnection,
62 layout: &SnapshotLayout,
63 staging: i64,
64) -> Result<Vec<String>, DbError> {
65 let mut tables = Vec::new();
66 for table in &layout.tables {
67 db.batch(&table.create_staged(staging))?;
68 tables.push(table.staged(staging));
69 }
70 let name = |name: &str| table(staging, name).trim_start_matches("temp.").to_owned();
71 db.batch(&format!(
72 "CREATE TEMP TABLE {}(table_name TEXT NOT NULL,key BLOB NOT NULL,audience TEXT NOT NULL,generation BLOB NOT NULL,device BLOB NOT NULL,number BLOB NOT NULL);
73 CREATE TEMP TABLE {}(table_name TEXT NOT NULL,column_name TEXT NOT NULL,PRIMARY KEY(table_name,column_name)) WITHOUT ROWID;
74 CREATE TEMP TABLE {}(write_id BLOB NOT NULL,audience TEXT NOT NULL,header BLOB NOT NULL,PRIMARY KEY(write_id,audience)) WITHOUT ROWID;
75 CREATE TEMP TABLE {}(write_id BLOB NOT NULL,audience TEXT NOT NULL,table_name TEXT NOT NULL,key BLOB NOT NULL,record BLOB NOT NULL,admission BLOB,stop BLOB);
76 CREATE TEMP TABLE {}(table_name TEXT NOT NULL,key BLOB NOT NULL,audience TEXT NOT NULL,write_id BLOB NOT NULL,creator BLOB NOT NULL);
77 CREATE TEMP TABLE {}(section INTEGER NOT NULL,evidence INTEGER NOT NULL CHECK(evidence IN (0,1)),record BLOB NOT NULL);",
78 name("generations"),
79 name("columns"),
80 name("parts"),
81 name("authored"),
82 name("creations"),
83 name("records"),
84 ))?;
85 for name in ["generations", "columns", "parts", "authored", "creations", "records"] {
86 tables.push(table(staging, name));
87 }
88 Ok(tables)
89}
90
91fn remove(db: &DatabaseConnection, staging: &StagedTables) -> Result<(), DbError> {
93 for table in &staging.tables {
94 db.batch(&format!("DROP TABLE {table}"))?;
95 }
96 crate::snapshot_metadata::remove(db, staging.id)
97}
98
99fn remove_all(db: &DatabaseConnection, stagings: &[SnapshotStaging]) -> Result<(), DbError> {
102 stagings
103 .iter()
104 .filter_map(|staging| staging.tables.as_ref())
105 .try_for_each(|tables| remove(db, tables))
106}
107
108struct Inserts {
111 layout: SnapshotLayout,
112 rows: Vec<String>,
114 generations: String,
115 columns: String,
116 parts: String,
117 authored: String,
118 creations: String,
119 records: String,
120}
121
122impl Inserts {
123 fn new(layout: SnapshotLayout, staging: i64) -> Self {
124 let rows = layout
125 .tables
126 .iter()
127 .map(|table| {
128 format!(
129 "INSERT INTO {}({}) VALUES({})",
130 table.staged(staging),
131 table.names().join(","),
132 vec!["?"; table.width()].join(",")
133 )
134 })
135 .collect();
136 let insert = |name: &str, columns: &str| {
137 let count = columns.split(',').count();
138 format!(
139 "INSERT INTO {}({columns}) VALUES({})",
140 table(staging, name),
141 (1..=count).map(|i| format!("?{i}")).collect::<Vec<_>>().join(",")
142 )
143 };
144 Self {
145 layout,
146 rows,
147 generations: insert("generations", "table_name,key,audience,generation,device,number"),
148 columns: insert("columns", "table_name,column_name"),
149 parts: insert("parts", "write_id,audience,header"),
150 authored: insert("authored", "write_id,audience,table_name,key,record,admission,stop"),
151 creations: insert("creations", "table_name,key,audience,write_id,creator"),
152 records: insert("records", "section,evidence,record"),
153 }
154 }
155}
156
157impl InputDatabase {
158 fn begin_staging(&self, schema: &WriteSchema) -> Result<(StagedTables, Inserts), DbError> {
159 let id = self.stagings.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
160 let layout = SnapshotLayout::new(schema);
161 let tables = self.write(|db| create_staged(db, &layout, id))?;
162 Ok((StagedTables { id, tables }, Inserts::new(layout, id)))
163 }
164
165 #[cfg(any(test, feature = "test-utils"))]
167 fn next_finish_damage(&self) -> Option<StagedDamage> {
168 let mut damage = coven_foundation::locks::lock_unpoisoned(&self.damage_finishing);
169 let (made, count) = (*damage)?;
170 *damage = (count > 1).then_some((made, count - 1));
171 Some(made)
172 }
173
174 fn remove_staging(&self, staging: &StagedTables) -> Result<(), DbError> {
176 super::with_locked(&self.writer, |writer| writer.transaction(|db| remove(db, staging)))
177 }
178
179 pub(crate) fn read_staged<T, S: AsMut<[SnapshotStaging]>>(
184 &self,
185 stagings: &mut S,
186 consume: impl FnOnce(
187 &crate::input_preparation::PreparedInputs<'_>,
188 &mut S,
189 ) -> Result<T, DbError>,
190 ) -> Result<T, DbError> {
191 super::with_locked(&self.writer, |writer| {
192 #[cfg(any(test, feature = "test-utils"))]
193 if self.damage_staged.swap(false, std::sync::atomic::Ordering::SeqCst) {
194 let first = stagings.as_mut().iter().find_map(|staging| staging.tables.as_ref());
195 let first = first.map(|tables| tables.id).ok_or(DbError::Invariant {
196 what: "a test damages a staging that holds its records",
197 })?;
198 writer.transaction(|db| damage::damage(db, first, StagedDamage::Record))?;
199 }
200 let read = writer.transaction(|db| {
201 let inputs = crate::input_preparation::PreparedInputs::new(db)?;
202 let result = consume(&inputs, stagings)?;
203 inputs.finish()?;
204 remove_all(db, stagings.as_mut())?;
205 Ok(result)
206 });
207 let result = match read {
208 Ok(result) => Ok(result),
209 Err(error) => {
210 let error = with_staged_digest(writer, stagings.as_mut(), error);
211 let removed = writer.transaction(|db| {
212 #[cfg(any(test, feature = "test-utils"))]
213 if self.fail_removal.swap(false, std::sync::atomic::Ordering::SeqCst) {
214 return Err(damage::removal_failure());
215 }
216 remove_all(db, stagings.as_mut())
217 });
218 match removed {
219 Ok(()) => Err(error),
220 Err(removal) => {
223 return Err(DbError::StagingCleanup {
224 operation: Box::new(error),
225 cleanup: Box::new(removal),
226 })
227 }
228 }
229 }
230 };
231 stagings.as_mut().iter_mut().for_each(SnapshotStaging::taken);
232 result
233 })
234 }
235}
236
237pub struct SnapshotStaging {
243 database: Database,
244 expected: SnapshotId,
245 tables: Option<StagedTables>,
247 phase: Phase,
248 batches: usize,
250 time: std::time::Duration,
251}
252
253enum Phase {
255 Streaming(Box<StagingState>),
261 Staged {
263 header: SnapshotHeader,
264 refusal: Option<(usize, DbError)>,
268 },
269 Failed,
271}
272
273struct StagingState {
278 decoder: SnapshotChunkDecoder,
279 inserts: Inserts,
280 records: i64,
281 columns: BTreeSet<(String, String)>,
282 part: Option<StagedPart>,
283 refusal: Option<(usize, DbError)>,
284}
285
286struct StagedPart {
289 write: Vec<u8>,
290 header: WriteHeader,
291 staged: bool,
292}
293
294impl Drop for SnapshotStaging {
295 fn drop(&mut self) {
296 let Some(tables) = self.tables.take() else {
297 return;
298 };
299 let bugs = self.database.bugs.clone();
304 let remove = move |inner: &DatabaseInner| {
305 let removed =
306 coven_foundation::task::caught_sync("remove an abandoned snapshot staging", || {
307 inner.call(|inner| inner.inputs.remove_staging(&tables))
308 });
309 match removed {
310 Ok(Ok(())) => {}
311 Ok(Err(error)) => {
312 if !coven_foundation::task::report_carried(bugs.as_ref(), &error) {
313 tracing::error!(%error, "removing an abandoned snapshot staging failed");
314 }
315 }
316 Err(bug) => coven_foundation::task::report(bugs.as_ref(), &bug),
317 }
318 Ok::<_, DbError>(())
319 };
320 if tokio::runtime::Handle::try_current().is_ok() {
321 let removing = start_call(
322 "remove an abandoned snapshot staging",
323 &self.database.bugs,
324 self.database.inner.clone(),
325 remove,
326 );
327 #[cfg(test)]
328 coven_foundation::locks::lock_unpoisoned(&self.database.writer.abandoned_stagings)
329 .push(removing);
330 #[cfg(not(test))]
331 drop(removing);
332 } else if let Some(inner) = read_unpoisoned(&self.database.inner).as_ref() {
333 let _removed = remove(inner);
334 }
335 }
336}
337
338impl Database {
339 pub async fn stage_snapshot(
342 &self,
343 id: SnapshotId,
344 prefix: SnapshotObjectPrefix,
345 ) -> Result<SnapshotStaging, DbError> {
346 let (tables, inserts) =
347 self.call(|inner| inner.inputs.begin_staging(&inner.access.write_schema)).await?;
348 Ok(SnapshotStaging {
349 database: self.clone(),
350 expected: id,
351 tables: Some(tables),
352 phase: Phase::Streaming(Box::new(StagingState {
353 decoder: SnapshotChunkDecoder::new(prefix),
354 inserts,
355 records: 0,
356 columns: BTreeSet::new(),
357 part: None,
358 refusal: None,
359 })),
360 batches: 0,
361 time: std::time::Duration::ZERO,
362 })
363 }
364
365 #[cfg(any(test, feature = "test-utils"))]
368 pub async fn input_temp_store(&self) -> Result<u8, DbError> {
369 self.call(|inner| {
370 super::with_locked(&inner.inputs.writer, |writer| {
371 writer.query_row("PRAGMA temp_store", [], |row| row.get(0))
372 })
373 })
374 .await
375 }
376
377 #[cfg(any(test, feature = "test-utils"))]
380 pub async fn damage_next_staged_read(&self) -> Result<(), DbError> {
381 self.call(|inner| {
382 inner.inputs.damage_staged.store(true, std::sync::atomic::Ordering::SeqCst);
383 Ok::<_, DbError>(())
384 })
385 .await
386 }
387
388 #[cfg(any(test, feature = "test-utils"))]
393 pub async fn damage_staged_finishes(
394 &self,
395 damage: StagedDamage,
396 count: u32,
397 ) -> Result<(), DbError> {
398 self.call(move |inner| {
399 *coven_foundation::locks::lock_unpoisoned(&inner.inputs.damage_finishing) =
400 Some((damage, count));
401 Ok::<_, DbError>(())
402 })
403 .await
404 }
405
406 #[cfg(any(test, feature = "test-utils"))]
409 pub async fn staged_snapshot_records(&self) -> Result<u64, DbError> {
410 self.call(|inner| {
411 super::with_locked(&inner.inputs.writer, |writer| {
412 let tables = writer.query(
413 "SELECT name FROM temp.sqlite_schema WHERE type='table' AND name GLOB '_coven_staged_*'",
414 [],
415 |row| row.get::<_, String>(0),
416 )?;
417 let mut count: i64 = writer.query_row(
418 "SELECT count(*) FROM temp._coven_snapshot_writes",
419 [],
420 |row| row.get(0),
421 )?;
422 for table in tables {
423 count += writer.query_row(
424 &format!("SELECT count(*) FROM temp.{}", crate::sql::identifier(&table)),
425 [],
426 |row| row.get::<_, i64>(0),
427 )?;
428 }
429 Ok::<_, DbError>(count)
430 })
431 })
432 .await
433 .map(|count| count as u64)
434 }
435}
436
437pub(crate) async fn refuse(stagings: Vec<SnapshotStaging>, error: DbError) -> DbError {
441 let mut error = error;
442 for staging in stagings {
443 if let Err(cleanup) = staging.discard().await {
444 error =
445 DbError::StagingCleanup { operation: Box::new(error), cleanup: Box::new(cleanup) };
446 }
447 }
448 error
449}
450
451impl SnapshotStaging {
452 fn id(&self) -> Result<i64, DbError> {
453 self.tables.as_ref().map(|tables| tables.id).ok_or(DbError::Invariant {
454 what: "a staged snapshot is read once, before it is discarded",
455 })
456 }
457
458 pub async fn stage(&mut self, plaintext: Vec<u8>) -> Result<(), DbError> {
461 let staging = self.id()?;
462 let Phase::Streaming(mut state) = std::mem::replace(&mut self.phase, Phase::Failed) else {
463 return Err(DbError::Invariant {
464 what: "a snapshot stages only until its stream ends or fails",
465 });
466 };
467 let expected = self.expected.clone();
468 let (state, time) = self
469 .database
470 .call(move |inner| {
471 let started = inner.clock.elapsed();
472 inner.inputs.write(|db| {
473 stage(
474 db,
475 &inner.access.write_schema,
476 &expected,
477 staging,
478 &mut state,
479 &plaintext,
480 )
481 })?;
482 Ok::<_, DbError>((state, inner.clock.elapsed() - started))
483 })
484 .await?;
485 self.phase = Phase::Streaming(state);
486 self.batches += 1;
487 self.time += time;
488 Ok(())
489 }
490
491 pub async fn finish(&mut self) -> Result<(), DbError> {
495 let staging = self.id()?;
496 let Phase::Streaming(state) = std::mem::replace(&mut self.phase, Phase::Failed) else {
497 return Err(DbError::Invariant {
498 what: "a snapshot stages only until its stream ends or fails",
499 });
500 };
501 let expected = self.expected.clone();
502 let records = state.records;
503 let (header, refusal) = self
504 .database
505 .call(move |inner| {
506 #[cfg(any(test, feature = "test-utils"))]
507 if let Some(made) = inner.inputs.next_finish_damage() {
508 inner.inputs.write(|db| damage::damage(db, staging, made))?;
509 }
510 inner.inputs.write(|db| {
511 state.decoder.finish().map_err(SnapshotError::Format)?;
512 let header = state
513 .decoder
514 .header()
515 .ok_or_else(|| invalid("snapshot has no header"))?
516 .clone();
517 check_header(&inner.access.write_schema, &expected, &header)?;
518 SnapshotMetadata::staged(db, staging).validate(header.covers)?;
519 Ok((header, state.refusal))
520 })
521 })
522 .await?;
523 tracing::info!(
524 audience = ?header.id.audience,
525 records,
526 batches = self.batches,
527 time = ?self.time,
528 "staged snapshot"
529 );
530 self.phase = Phase::Staged { header, refusal };
531 Ok(())
532 }
533
534 pub async fn discard(mut self) -> Result<(), DbError> {
536 let Some(tables) = self.tables.take() else {
537 return Ok(());
538 };
539 self.database.call(move |inner| inner.inputs.remove_staging(&tables)).await
540 }
541
542 pub(crate) fn staged_by(&self, database: &Database) -> bool {
545 Arc::ptr_eq(&self.database.inner, &database.inner)
546 }
547
548 pub(crate) fn prepare<'a>(
551 &mut self,
552 inputs: &crate::input_preparation::PreparedInputs<'a>,
553 authorship: &dyn crate::Authorship,
554 ) -> Result<crate::snapshot_preparation::PreparedSnapshot<'a>, DbError> {
555 let staging = self.id()?;
556 let Phase::Staged { header, refusal } = &mut self.phase else {
557 return Err(DbError::Invariant {
558 what: "a snapshot is read once its stream checked out to its end",
559 });
560 };
561 inputs.snapshot(staging, header.clone(), refusal.take(), authorship)
562 }
563
564 fn taken(&mut self) {
566 self.tables = None;
567 }
568}
569
570fn stage(
571 db: &DatabaseConnection,
572 schema: &WriteSchema,
573 expected: &SnapshotId,
574 staging: i64,
575 state: &mut StagingState,
576 mut bytes: &[u8],
577) -> Result<(), DbError> {
578 #[cfg(any(test, feature = "test-utils"))]
579 if state.records == 0 {
580 db.record_work(|work| work.snapshot_reads += 1);
581 }
582 let metadata = SnapshotMetadata::staged(db, staging);
583 let StagingState { decoder, inserts, records, columns, part, refusal } = state;
584 loop {
585 let headed = decoder.header().is_some();
586 let record =
587 decoder.next_record(&mut bytes, &metadata).map_err(MetadataFailure::snapshot)?;
588 if !headed {
589 if let Some(header) = decoder.header() {
590 check_header(schema, expected, header)?;
591 }
592 }
593 let Some(record) = record else {
594 return Ok(());
595 };
596 if let SnapshotRecord::Write(write) = &record {
597 metadata.stage(write)?;
598 }
599 *records += 1;
600 if refusal.is_some() {
603 continue;
604 }
605 let staged = RecordStaging { db, schema, inserts };
606 let section = record.section();
607 if let Err(refused) = staged.record(record, decoder.frame(), columns, part)? {
608 *refusal = Some((section, refused));
609 }
610 }
611}
612
613struct RecordStaging<'a> {
615 db: &'a DatabaseConnection,
616 schema: &'a WriteSchema,
617 inserts: &'a Inserts,
618}
619
620type Checked = Result<(), DbError>;
622
623impl RecordStaging<'_> {
624 fn record(
627 &self,
628 record: SnapshotRecord,
629 frame: &[u8],
630 columns: &mut BTreeSet<(String, String)>,
631 part: &mut Option<StagedPart>,
632 ) -> Result<Checked, DbError> {
633 match &record {
634 SnapshotRecord::Write(_) => return Ok(Ok(())),
635 SnapshotRecord::Column(column) => {
636 self.db.internal_execute(
637 &self.inserts.columns,
638 params![column.table, column.column],
639 )?;
640 columns.insert((column.table.clone(), column.column.clone()));
641 return Ok(Ok(()));
642 }
643 SnapshotRecord::Merge(row) => return self.merged(row, columns),
644 SnapshotRecord::Authored(authored) => return self.authored(authored, part),
645 SnapshotRecord::Creation(creation) => {
646 if let Err(refused) = self.schema.row_table(&creation.row) {
647 return Ok(Err(refused));
648 }
649 self.db.internal_execute(
650 &self.inserts.creations,
651 params![
652 creation.row.table,
653 creation.row.key,
654 &creation.row.audience,
655 encoded(merge_fields::encode_write_id(&creation.write))?,
656 encoded(merge_fields::encode_change_id(&creation.creator))?
657 ],
658 )?;
659 return Ok(Ok(()));
660 }
661 SnapshotRecord::Loss(_)
662 | SnapshotRecord::Evidence(_)
663 | SnapshotRecord::Admin(_)
664 | SnapshotRecord::KeyCopy(_)
665 | SnapshotRecord::Announcement(_)
666 | SnapshotRecord::Claim(_) => {}
667 }
668 let section = record.section();
669 let evidence = section == section::EVIDENCE;
670 self.db
671 .internal_execute(&self.inserts.records, params![section as i64, evidence, frame])?;
672 Ok(Ok(()))
673 }
674
675 fn merged(
677 &self,
678 merged: &MergeRow,
679 columns: &BTreeSet<(String, String)>,
680 ) -> Result<Checked, DbError> {
681 if let Err(refused) = check_merged(self.schema, merged, columns) {
682 return Ok(Err(refused));
683 }
684 let state = &merged.state;
685 let row = state.row();
686 let at = self.inserts.layout.position(&row.table);
687 let layout = &self.inserts.layout.tables[at];
688 let generation = state.generation();
689 let writer = state.generations()[&generation];
690 let mut values = vec![rusqlite::types::Value::Null; layout.width()];
691 values[TableLayout::KEY] = row.key.clone().into();
692 values[TableLayout::AUDIENCE] = (&row.audience).into();
693 values[TableLayout::PRESENT] = state.present().into();
694 values[TableLayout::GENERATION] = generation.to_be_bytes().to_vec().into();
695 values[TableLayout::DEVICE] = writer.device.0.to_be_bytes().to_vec().into();
696 values[TableLayout::NUMBER] = writer.number.to_be_bytes().to_vec().into();
697 for (j, name) in layout.columns.iter().enumerate() {
698 if let Some(cell) = state.cells().get(name) {
699 values[layout.value_at(j)] = sql_value(&cell.value.value);
700 values[layout.setter_at(j)] = cell.write.device.0.to_be_bytes().to_vec().into();
701 values[layout.setter_at(j) + 1] = cell.write.number.to_be_bytes().to_vec().into();
702 }
703 }
704 put_parents(layout, &mut values, |j| {
705 state.cells().get(&layout.columns[j]).map(|cell| &cell.value)
706 });
707 self.db.internal_execute(&self.inserts.rows[at], params_from_iter(values))?;
708 for (earlier, write) in state.generations() {
709 if *earlier == generation {
710 continue;
711 }
712 self.db.internal_execute(
713 &self.inserts.generations,
714 params![
715 row.table,
716 row.key,
717 &row.audience,
718 earlier.to_be_bytes().as_slice(),
719 write.device,
720 write.number.to_be_bytes().as_slice()
721 ],
722 )?;
723 }
724 Ok(Ok(()))
725 }
726
727 fn authored(
731 &self,
732 authored: &AuthoredRecord,
733 part: &mut Option<StagedPart>,
734 ) -> Result<Checked, DbError> {
735 let change = match authored {
736 AuthoredRecord::Part(new) => {
737 *part = Some(StagedPart {
738 write: encoded(merge_fields::encode_write_id(&new.header.position))?,
739 header: new.header.clone(),
740 staged: false,
741 });
742 return Ok(Ok(()));
743 }
744 AuthoredRecord::Change(change) => change,
745 };
746 let part = part.as_mut().ok_or(DbError::Invariant {
747 what: "the snapshot decoder gives a kept change after its part",
748 })?;
749 let row = change.record.row();
750 let audience = &row.audience;
751 if !part.staged {
752 self.db.internal_execute(
753 &self.inserts.parts,
754 params![
755 part.write,
756 audience,
757 encoded(merge_fields::encode_write_header(&part.header))?
758 ],
759 )?;
760 part.staged = true;
761 }
762 let form = crate::write_sources::KeptForm::new(self.schema, part.header.schema_version);
763 if !form.conforms(&change.record) {
764 return Ok(Err(invalid("kept change holds its columns unlike its schema")));
765 }
766 self.db.internal_execute(
767 &self.inserts.authored,
768 params![
769 part.write,
770 audience,
771 row.table,
772 row.key,
773 change.record.encode()?,
774 crate::write_sources::stored_verdict(&change.verdict)?,
775 change.stop.map(|stop| stop.get().to_be_bytes().to_vec())
776 ],
777 )?;
778 Ok(Ok(()))
779 }
780}
781
782fn check_merged(
784 schema: &WriteSchema,
785 merged: &MergeRow,
786 columns: &BTreeSet<(String, String)>,
787) -> Result<(), DbError> {
788 let state = &merged.state;
789 let row = state.row();
790 let table = schema.row_columns(
791 row,
792 state.present(),
793 state.cells().iter().map(|(name, cell)| (name, &cell.value)),
794 )?;
795 for foreign in &table.foreign_keys {
796 let mut setters = foreign
797 .columns
798 .iter()
799 .filter_map(|column| state.cells().get(crate::write_rows::column_name(table, column)));
800 if let Some(first) = setters.next() {
801 if setters.any(|cell| cell.write != first.write) {
802 return Err(invalid("multi-column reference setters disagree"));
803 }
804 }
805 }
806 if state.generations().is_empty() {
807 return Err(invalid("merge row has no generations"));
808 }
809 for name in state.cells().keys() {
810 if !columns.contains(&(row.table.clone(), name.clone())) {
811 return Err(invalid("merge cell has no column record"));
812 }
813 }
814 Ok(())
815}
816
817fn put_parents<'a>(
819 layout: &TableLayout,
820 values: &mut [rusqlite::types::Value],
821 column: impl Fn(usize) -> Option<&'a coven_merge::ColumnValue<Value>>,
822) {
823 for (q, (j, key)) in layout.pairs.iter().enumerate() {
824 let at = layout.parent_at(q);
825 match column(*j).and_then(|value| value.parents.get(key)) {
826 Some(parent) => {
827 values[at] = parent.row.key.clone().into();
828 values[at + 1] = (&parent.row.audience).into();
829 values[at + 2] = parent.generation.to_be_bytes().to_vec().into();
830 }
831 None => {
832 values[at] = rusqlite::types::Value::Null;
833 values[at + 1] = rusqlite::types::Value::Null;
834 values[at + 2] = rusqlite::types::Value::Null;
835 }
836 }
837 }
838}
839
840fn check_header(
841 schema: &WriteSchema,
842 expected: &SnapshotId,
843 header: &SnapshotHeader,
844) -> Result<(), DbError> {
845 if header.id != *expected {
846 return Err(invalid("snapshot identity differs from the requested object"));
847 }
848 if !schema.versions.contains(&header.schema_version) {
849 return Err(SnapshotError::Schema {
850 snapshot: header.schema_version,
851 database: *schema.versions.end(),
852 }
853 .into());
854 }
855 Ok(())
856}
857
858#[cfg(test)]
859#[path = "snapshot_staging_tests.rs"]
860mod tests;