1use crate::{Codec, Error, Hash32, ObjectKind, ObjectRecord, Result, VerifiedObject, compress, decompress};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
4pub enum MerkleMode {
5 Disabled,
6 #[default]
7 Enabled,
8}
9
10#[derive(Debug, Clone, Default)]
11pub struct SqliteStoreOptions {
12 pub namespace: Option<String>,
13 pub merkle: MerkleMode,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct MerkleProof {
18 pub leaf_pos: u64,
19 pub siblings: Vec<Hash32>,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct MerkleSummary {
24 pub root: Hash32,
25 pub leaf_count: u64,
26}
27
28#[derive(Debug, Clone)]
29pub(in crate::sqlite) struct SqliteLayout {
30 pub namespace: Option<String>,
31 pub objects: String,
32 pub file_recipe_cache: String,
33 pub merkle_leaves: String,
34 pub merkle_nodes: String,
35 pub merkle_meta: String,
36 pub merkle_leaves_pos_index: String,
37}
38
39impl SqliteLayout {
40 pub fn new(namespace: Option<&str>) -> Result<Self> {
41 let namespace = normalize_namespace(namespace)?;
42 let prefix = namespace.as_ref().map(|value| format!("{value}_")).unwrap_or_default();
43 let index_prefix = namespace
44 .as_ref()
45 .map(|value| format!("idx_{value}_"))
46 .unwrap_or_else(|| "idx_".into());
47 Ok(Self {
48 namespace,
49 objects: format!("{prefix}objects"),
50 file_recipe_cache: format!("{prefix}file_recipe_cache"),
51 merkle_leaves: format!("{prefix}merkle_leaves"),
52 merkle_nodes: format!("{prefix}merkle_nodes"),
53 merkle_meta: format!("{prefix}merkle_meta"),
54 merkle_leaves_pos_index: format!("{index_prefix}merkle_leaves_pos"),
55 })
56 }
57}
58
59fn normalize_namespace(namespace: Option<&str>) -> Result<Option<String>> {
60 let Some(namespace) = namespace else {
61 return Ok(None);
62 };
63 if namespace.is_empty() {
64 return Err(Error::InvalidConfig("store namespace cannot be empty".into()));
65 }
66 if namespace.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '_') {
67 Ok(Some(namespace.to_string()))
68 } else {
69 Err(Error::InvalidConfig(format!(
70 "store namespace must contain only ASCII letters, digits, or underscores: {namespace}"
71 )))
72 }
73}
74
75pub(in crate::sqlite) fn schema_statements(layout: &SqliteLayout, merkle: MerkleMode) -> Vec<String> {
76 let mut statements = vec![
77 format!(
78 "CREATE TABLE IF NOT EXISTS {} (hash BLOB PRIMARY KEY, kind INTEGER NOT NULL, size INTEGER NOT NULL, stored_size INTEGER NOT NULL, \
79 codec TEXT NOT NULL, content BLOB NOT NULL, created_at INTEGER NOT NULL);",
80 layout.objects
81 ),
82 format!(
83 "CREATE TABLE IF NOT EXISTS {} (file_hash BLOB PRIMARY KEY, recipe_hash BLOB NOT NULL, updated_at INTEGER NOT NULL);",
84 layout.file_recipe_cache
85 ),
86 ];
87 if merkle == MerkleMode::Enabled {
88 statements.extend([
89 format!(
90 "CREATE TABLE IF NOT EXISTS {} (hash BLOB PRIMARY KEY, pos INTEGER NOT NULL);",
91 layout.merkle_leaves
92 ),
93 format!(
94 "CREATE UNIQUE INDEX IF NOT EXISTS {} ON {}(pos);",
95 layout.merkle_leaves_pos_index, layout.merkle_leaves
96 ),
97 format!(
98 "CREATE TABLE IF NOT EXISTS {} (level INTEGER NOT NULL, pos INTEGER NOT NULL, hash BLOB NOT NULL, PRIMARY KEY(level, pos));",
99 layout.merkle_nodes
100 ),
101 format!(
102 "CREATE TABLE IF NOT EXISTS {} (key TEXT PRIMARY KEY, value TEXT NOT NULL);",
103 layout.merkle_meta
104 ),
105 ]);
106 }
107 statements
108}
109
110pub(in crate::sqlite) struct StoredObjectRow {
111 pub hash: Hash32,
112 pub kind: i64,
113 pub decoded_len: i64,
114 pub codec: String,
115 pub stored_bytes: Vec<u8>,
116}
117
118pub(in crate::sqlite) fn prepare_object(hash: Hash32, kind: ObjectKind, content: &[u8], codec: Codec) -> Result<ObjectRecord> {
119 if Hash32::sha3_256(content) != hash {
120 return Err(Error::HashMismatch);
121 }
122 let mut selected = codec;
123 let stored_bytes = if codec == Codec::Raw {
124 content.to_vec()
125 } else {
126 let candidate = compress(codec, content)?;
127 if candidate.len() as f64 / content.len().max(1) as f64 >= 0.98 {
128 selected = Codec::Raw;
129 content.to_vec()
130 } else {
131 candidate
132 }
133 };
134 Ok(ObjectRecord {
135 hash,
136 kind,
137 decoded_len: content.len() as u64,
138 codec: selected,
139 stored_bytes,
140 })
141}
142
143pub(in crate::sqlite) fn verified_object_from_row(row: StoredObjectRow) -> Result<VerifiedObject> {
144 let kind = ObjectKind::from_i64(row.kind).ok_or_else(|| Error::Integrity("invalid kind".into()))?;
145 let expected = u64::try_from(row.decoded_len).map_err(|_| Error::Integrity("negative object size".into()))?;
146 let bytes = decompress(codec_from_str(&row.codec)?, &row.stored_bytes)?;
147 let actual = bytes.len() as u64;
148 if actual != expected {
149 return Err(Error::ObjectLengthMismatch { expected, actual });
150 }
151 let actual_hash = Hash32::sha3_256(&bytes);
152 if actual_hash != row.hash {
153 return Err(Error::ObjectHashMismatch {
154 expected: row.hash,
155 actual: actual_hash,
156 });
157 }
158 Ok(VerifiedObject {
159 hash: row.hash,
160 kind,
161 bytes,
162 })
163}
164
165pub(in crate::sqlite) fn object_record_from_row(row: StoredObjectRow) -> Result<ObjectRecord> {
166 Ok(ObjectRecord {
167 hash: row.hash,
168 kind: ObjectKind::from_i64(row.kind).ok_or_else(|| Error::Integrity("invalid kind".into()))?,
169 decoded_len: u64::try_from(row.decoded_len).map_err(|_| Error::Integrity("negative object size".into()))?,
170 codec: codec_from_str(&row.codec)?,
171 stored_bytes: row.stored_bytes,
172 })
173}
174
175pub(in crate::sqlite) fn codec_to_str(codec: Codec) -> &'static str {
176 match codec {
177 Codec::Raw => "raw",
178 Codec::Zstd => "zstd",
179 Codec::Brotli => "brotli",
180 }
181}
182
183pub(in crate::sqlite) fn codec_from_str(value: &str) -> Result<Codec> {
184 match value {
185 "raw" => Ok(Codec::Raw),
186 "zstd" => Ok(Codec::Zstd),
187 "brotli" => Ok(Codec::Brotli),
188 other => Err(Error::InvalidCodec(other.to_string())),
189 }
190}
191
192pub(in crate::sqlite) fn hash_pair(left: &Hash32, right: &Hash32) -> Hash32 {
193 let mut bytes = Vec::with_capacity(64);
194 bytes.extend_from_slice(left.as_bytes());
195 bytes.extend_from_slice(right.as_bytes());
196 Hash32::sha3_256(bytes)
197}
198
199pub(in crate::sqlite) fn merkle_root_from_hashes(hashes: &[Hash32]) -> MerkleSummary {
200 if hashes.is_empty() {
201 return MerkleSummary {
202 root: Hash32::sha3_256([]),
203 leaf_count: 0,
204 };
205 }
206 let mut current = hashes.to_vec();
207 while current.len() > 1 {
208 current = current
209 .chunks(2)
210 .map(|pair| hash_pair(&pair[0], pair.get(1).unwrap_or(&pair[0])))
211 .collect();
212 }
213 MerkleSummary {
214 root: current[0],
215 leaf_count: hashes.len() as u64,
216 }
217}