1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
//! DB layer normalisation: copy live SQLite DB into CAS via VACUUM.
//!
//! Implements [`db_to_cas`] / [`db_to_cas_conn`] (WS4 Task 8 / VAL-CAS-019),
//! which:
//!
//! 1. **Checkpoints the WAL** into the main DB file
//! (`PRAGMA wal_checkpoint(TRUNCATE)`). This flushes pending WAL frames so
//! the subsequent VACUUM reads a single consistent snapshot.
//! 2. **VACUUM INTO** a normalised temp file. VACUUM rebuilds the file from
//! scratch, producing a deterministic page layout: no freelist, no unused
//! pages, no WAL/SHM sidecars. Two DBs with identical logical content but
//! different physical layouts (B-tree splits, freelist, fragmentation)
//! thus produce byte-identical VACUUM output.
//! 3. **`cas.put`** the normalised bytes to obtain a blake3 content hash.
//!
//! This is the deduplication guarantee for the DB layer (spec §4.4): the
//! observed 4→2 generation duplication collapses to one blob per distinct
//! logical DB state.
//!
//! ## Why VACUUM-normalize?
//!
//! SQLite's file layout is not deterministic across DB lifetimes. Insertion
//! order, B-tree page splits, freelist growth, and WAL checkpoints all
//! influence the on-disk byte representation. Two databases with identical
//! logical content (rows + schema) can therefore differ at the byte level,
//! defeating CAS content-addressing.
//!
//! `VACUUM INTO` produces a byte-deterministic representation by rebuilding
//! the file from a canonical page-by-page copy: no freelist, no fragmented
//! B-trees, no WAL. Given the same logical content, VACUUM always emits the
//! same byte sequence, so `cas.put` returns the same blake3 hash. This is
//! the foundation of cross-generation dedup.
use Path;
use crate;
use crateStorage;
/// Errors returned by [`db_to_cas`] / [`db_to_cas_conn`].
/// Copy a live SQLite DB at `storage` into the CAS via VACUUM-normalisation.
///
/// Convenience wrapper around [`db_to_cas_conn`] that pulls the underlying
/// [`rusqlite::Connection`] out of a [`Storage`] handle.
/// Copy a live SQLite DB at `conn` into the CAS via VACUUM-normalisation.
///
/// Steps:
/// 1. `PRAGMA wal_checkpoint(TRUNCATE)` — flush pending WAL frames into the
/// main DB file and truncate the WAL.
/// 2. `VACUUM INTO '<temp>'` — write a normalised copy to a fresh file.
/// VACUUM rebuilds the file page-by-page, producing a deterministic layout
/// with no freelist and no fragmented B-trees. Errors if the target file
/// already exists, so [`db_to_cas_conn`] always writes to a fresh path
/// inside a private temp directory.
/// 3. Read the temp file into memory and `cas.put` its bytes. The CAS hashes
/// the payload with blake3 and stores it deduplicated.
/// 4. Remove the temp file.
///
/// The live DB is **not** disturbed by step 2: `VACUUM INTO` writes to a
/// separate target file. After step 1 the WAL is truncated (its contents are
/// durably in the main DB file); the live connection remains usable for
/// further reads and writes.
///
/// Returns the blake3 hash of the normalised DB bytes. The same logical DB
/// content always produces the same hash, regardless of B-tree shape, page
/// fragmentation, or WAL state at the time of the call.
/// Format `path` as a string for embedding in a SQL string literal.
///
/// On Unix we forward the path bytes lossily. SQLite accepts both UTF-8 byte
/// sequences and UTF-16 file paths via the VFS; the bundled SQLite on Linux
/// uses the UTF-8 VFS, so a lossy UTF-8 string is correct.
/// Escape a string for inclusion inside SQLite single-quoted string literal.
///
/// SQLite doubles embedded single quotes (`'` → `''`) inside a single-quoted
/// string. We don't need backslash escapes here because the bundled VFS does
/// not interpret backslashes in URI-style paths on Linux.