Skip to main content

coven_database/
snapshot_staging.rs

1//! A snapshot's checked records, staged on the input connection while its
2//! body streams, until one load or file check reads them (§16.2, §21.2).
3//! Rows, cells, references, source records and creations are staged as
4//! columns of tables of their own, so installing reads fields rather than
5//! decoding records again; the few other records keep their frames.
6
7use 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
32/// Created with the input connection, in its temporary storage, so staged
33/// records go with the connection: no restart finds them. That storage spills
34/// to files once the connection's cache fills; opening refuses a SQLite built
35/// to keep it in memory. Only the snapshot loader names these tables.
36pub(super) fn create_tables(db: &DatabaseConnection) -> Result<(), DbError> {
37    db.batch("PRAGMA temp_store=FILE;")?;
38    crate::snapshot_metadata::create_table(db)
39}
40
41/// One staging's number and the tables that hold its records on the input
42/// connection.
43struct StagedTables {
44    id: i64,
45    tables: Vec<String>,
46}
47
48/// A staging's tables of its own, beside its rows' tables.
49pub(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
91/// Drop a staging's tables whole.
92fn 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
99/// Drop the tables of each of `stagings` that still holds its records, in
100/// the transaction of the operation that ends them.
101fn 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
108/// The statements staging one snapshot runs for its records, written once
109/// for its tables.
110struct Inserts {
111    layout: SnapshotLayout,
112    /// Each synced table's row insert, in the layout's order.
113    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    /// The damage a test asked for to the staging finishing now, if any.
166    #[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    /// Remove a staging's records.
175    fn remove_staging(&self, staging: &StagedTables) -> Result<(), DbError> {
176        super::with_locked(&self.writer, |writer| writer.transaction(|db| remove(db, staging)))
177    }
178
179    /// Run one load or file check over `stagings`, the last phase of each:
180    /// `consume` reads their records in one input transaction, which removes
181    /// them as it commits. When it fails, the records are removed before the
182    /// failure is reported, so nothing staged outlives its operation (§16.2).
183    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                        // The stagings keep their tables, which dropping
221                        // them removes.
222                        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
237/// One snapshot's staging, from its first batch to the load or file check
238/// that reads its records, or the discard that removes them (§16.2). It is
239/// the one holder of what it stages for that whole operation: its steps are
240/// its phases, and the load takes it whole. Dropped while still holding
241/// records, as by a cancelled or panicking caller, it removes them.
242pub struct SnapshotStaging {
243    database: Database,
244    expected: SnapshotId,
245    /// Absent once the load or file check that read them removed them.
246    tables: Option<StagedTables>,
247    phase: Phase,
248    /// The batches staged so far, and the time staging them took.
249    batches: usize,
250    time: std::time::Duration,
251}
252
253/// Where a staging is in its operation.
254enum Phase {
255    /// The body streams in: sync hands it each batch of opened plaintext,
256    /// decoded, checked and committed in one input transaction, outside
257    /// the application writer (§21.2). The state, near a kilobyte with its
258    /// decoder, lives on the heap: each batch moves it to the blocking pool
259    /// and back, and every future holding the staging carries what is inline.
260    Streaming(Box<StagingState>),
261    /// The stream checked out to its end marker.
262    Staged {
263        header: SnapshotHeader,
264        /// The first record that failed the loader's checks as it staged,
265        /// and its section: the load or file check refuses the body with it
266        /// where it reaches that section.
267        refusal: Option<(usize, DbError)>,
268    },
269    /// A batch or the end of the stream failed; only discarding is left.
270    Failed,
271}
272
273/// What staging keeps between batches beside its tables: the decoder, the
274/// statements that stage records, the records staged so far, the columns the
275/// snapshot names, the source part whose changes are arriving, and the first
276/// record the loader's checks refuse.
277struct 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
286/// The part of section 4 whose changes follow it (D7.3). Its row is staged
287/// with its first change, which names its audience.
288struct 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        // Only an abandoned staging gets here: a cancelled or panicking
300        // caller. Its connection's temporary storage goes when the store
301        // closes, which waits for this removal. Nobody waits for the removal,
302        // so a bug in it goes to the store's bug sink.
303        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    /// Begin staging the snapshot `id`, whose signed prefix is `prefix`
340    /// (§16.2). Nothing outside the loader reads what it stages.
341    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    /// Where the input connection keeps temporary tables (`PRAGMA temp_store`):
366    /// 1 is in files.
367    #[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    /// Change a staged record before the next load or file check reads it,
378    /// as damage to this device's disk would (§19.1).
379    #[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    /// Make `damage` to each of the next `count` stagings as it finishes,
389    /// before finishing checks it, as damage to this device's disk would
390    /// (§19.1). Damage to two stagings of one snapshot alike stands for a
391    /// snapshot whose own records fail.
392    #[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    /// The records staged on the input connection now, for every snapshot:
407    /// the rows of every staging's tables, and their applied writes.
408    #[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
437/// The failure that ends `stagings` before their load or file check runs,
438/// reported once each has removed its records, beside any removal that
439/// failed (§16.2).
440pub(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    /// Decode one batch of opened plaintext and commit its records together.
459    /// After a failure, discard this staging.
460    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    /// Refuse a stream without its header, declared records and end marker,
492    /// then check its applied writes. Nothing more stages after this; after a
493    /// failure, discard this staging.
494    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    /// Remove everything staged so far (§16.2).
535    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    /// Whether `database` staged it: records on another store's input
543    /// connection are not there to read.
544    pub(crate) fn staged_by(&self, database: &Database) -> bool {
545        Arc::ptr_eq(&self.database.inner, &database.inner)
546    }
547
548    /// Authenticate its staged write evidence for the load or file check
549    /// reading it, in that operation's input transaction (D7.5).
550    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    /// Its records are gone, with the transaction that removed them.
565    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        // Once a record is refused the body is, but it is still decoded to
601        // its end, so a damaged body is refused for that.
602        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
613/// Where one staging's records go.
614struct RecordStaging<'a> {
615    db: &'a DatabaseConnection,
616    schema: &'a WriteSchema,
617    inserts: &'a Inserts,
618}
619
620/// A record the loader's checks accept, or the error refusing it.
621type Checked = Result<(), DbError>;
622
623impl RecordStaging<'_> {
624    /// Stage one record. Failing to write it fails the batch; a record the
625    /// loader refuses is the inner error, which refuses the whole body.
626    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    /// A merge row's checks, then its generations, cells and parents.
676    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    /// A source part, or a kept change of the part before it. A change
728    /// holds in full exactly the columns that were references under its
729    /// write's schema, as this device would keep it (D7.3).
730    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
782/// The loader's checks of a merge row on its own.
783fn 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
817/// Stage the parents each column's value carries, by reference pair.
818fn 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;