pub struct SourceFingerprint {
pub main_len: u64,
pub change_counter: Option<u32>,
pub wal: Option<WalFileHeader>,
pub wal_len: u64,
}Expand description
R858-B18 — everything about a source database’s files that must hold still for a copy of them to be a point-in-time image, sampled with no engine open and no lock taken.
This is the other half of reading a foreign-written database. A ReadOnly
open (CoreWalSeam::open_reader) gets us in the door without stealing the
whole-file lock, but it buys no shared locking protocol with upstream C
SQLite: turso locks whole files with fcntl, C SQLite uses byte-range locks
plus the -shm WAL index, and neither engine observes the other’s. So a
foreign checkpoint landing in the middle of our copy can fold WAL frames
into the main file we have already half-read, and the result is a torn image
that still passes PRAGMA integrity_check.
Rather than reimplement SQLite’s reader protocol (registering a read-mark in
the -shm WAL index — a research project and a permanent compatibility
liability against an engine we do not control), raw_consistent_copy_live
uses textbook optimistic validation: sample this before the copy, sample
it again after, and accept the copy only if nothing moved. That converts
“may silently read torn state” into “detects torn state and refuses”, needs
no cooperation from the foreign engine, and costs two stats and a 132-byte
read per attempt.
§Why these fields
wal(salt + checkpoint_seq) moves on every WAL reset, in both fold regimes — fresh randomness on a writer restart,salt1incrementing on an in-process autocheckpoint (both measured; seeWalSalt).main_lenmoves when a checkpoint grows the main database.change_counter(main header bytes 24..28) moves on every write to the main file — i.e. on every checkpoint — even one that leaves its length alone. It is meaningful here only because the foreign writer is C SQLite: turso does not maintain this field (it stays1in every journal mode, verified — seesnapshot.rs’s two-gate rationale), which is exactly why the WAL salt carries the weight and this one is corroboration.wal_lenis recorded for the report but deliberately not part of the accept/reject test — seeSelf::stable_across, which is the comparison to use. There is noPartialEqon this type on purpose: a bare==would silently includewal_lenand refuse every copy taken while the application was merely writing.
Fields§
§main_len: u64Length of the main database file.
change_counter: Option<u32>The main header’s change counter, or None when the file is too short
to carry a SQLite header at all (a database whose page 1 still lives
only in the WAL). Unknown-and-unknown compares equal, which is safe
because main_len participates in the same comparison.
wal: Option<WalFileHeader>The -wal header, or None when there is no WAL sidecar.
wal_len: u64Length of the -wal file (0 when absent).
Implementations§
Source§impl SourceFingerprint
impl SourceFingerprint
Sourcepub fn read(db_path: &str) -> Result<Self>
pub fn read(db_path: &str) -> Result<Self>
Sample the fingerprint of the database at db_path. Touches nothing:
two metadata calls plus a 28-byte and a 32-byte read.
Sourcepub fn stable_across(&self, after: &Self) -> bool
pub fn stable_across(&self, after: &Self) -> bool
True when nothing that can tear a copy moved between self (sampled
before) and after (sampled after). This is the accept test in
raw_consistent_copy_live, and it is narrower than field equality on
purpose.
§What can tear the copy, and what cannot
The copy reads the main file, then replays WAL frames 1..=max_frame
captured when the seam opened. Against that algorithm:
- A checkpoint tears it. It rewrites pages of the main file and
resets the WAL, so our already-read main bytes and our frame reads can
straddle the fold — replaying pre-fold frames over post-fold pages
rolls pages backwards. Caught: a checkpoint bumps
change_counterand/ormain_len, and a WAL restart re-rolls the salt and the sequence (measured in both regimes,examples/foreign_checkpoint_probe.rsprobes A and E). - A plain append does NOT tear it. SQLite only ever appends frames
within a generation, and only a reset (which re-rolls
salt1) lets it overwrite an existing frame. So frames1..=max_frameare immutable for as long as the salt holds, and a writer that commits during our copy just means our image is a slightly earlier point in time — which is what a point-in-time copy is.
Which is why wal_len and the frame count are excluded. Including them
buys no additional safety and costs a refusal on every copy taken while
the application is writing at all — turning a working backup into one
that only succeeds against an idle database. R858-B18 measured that
difference rather than assuming it; see probe H.
Trait Implementations§
Source§impl Clone for SourceFingerprint
impl Clone for SourceFingerprint
Source§fn clone(&self) -> SourceFingerprint
fn clone(&self) -> SourceFingerprint
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreimpl Copy for SourceFingerprint
Auto Trait Implementations§
impl Freeze for SourceFingerprint
impl RefUnwindSafe for SourceFingerprint
impl Send for SourceFingerprint
impl Sync for SourceFingerprint
impl Unpin for SourceFingerprint
impl UnsafeUnpin for SourceFingerprint
impl UnwindSafe for SourceFingerprint
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>, which can then be
downcast into Box<dyn ConcreteType> where ConcreteType implements Trait.Source§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Rc<Trait> (where Trait: Downcast) to Rc<Any>, which can then be further
downcast into Rc<ConcreteType> where ConcreteType implements Trait.Source§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.Source§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.Source§impl<T> DowncastSend for T
impl<T> DowncastSend for T
Source§impl<T> DowncastSync for T
impl<T> DowncastSync for T
impl<T> ErasedDestructor for Twhere
T: 'static,
impl<T> Fruit for T
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more