1use crate::Gates;
2use crate::{CloudOutboxRecords, Database, DbError, ExternalBlobRecords, PreparedExternalBlob};
3use coven_protocol::blob::Provenance;
4
5use coven_protocol::hlc::UpdatedAtStamper;
6use coven_protocol::synced_schema::SyncedTable;
7
8const EXTERNAL_BLOB_HASH_PARAMETER: &str = ":coven_external_blob_hash";
9
10pub struct SqlReadContext<'connection> {
22 connection: &'connection rusqlite::Connection,
23 dependencies: Option<crate::live_query::ReadDependencyCapture>,
24}
25
26impl<'connection> SqlReadContext<'connection> {
27 pub(crate) fn new(connection: &'connection rusqlite::Connection) -> Self {
28 Self {
29 connection,
30 dependencies: None,
31 }
32 }
33
34 pub(crate) fn tracking(
35 connection: &'connection rusqlite::Connection,
36 dependencies: crate::live_query::ReadDependencyCapture,
37 ) -> Self {
38 Self {
39 connection,
40 dependencies: Some(dependencies),
41 }
42 }
43
44 pub fn query_row<T, P, F>(&self, sql: &str, params: P, map: F) -> rusqlite::Result<T>
45 where
46 P: rusqlite::Params,
47 F: FnOnce(&rusqlite::Row<'_>) -> rusqlite::Result<T>,
48 {
49 let mut statement = self.prepare_tracked(sql)?;
50 if let Err(error) = params.__bind_in(&mut statement) {
51 self.finish_statement(None);
52 return Err(error);
53 }
54 self.finish_statement(statement.expanded_sql());
55 let mut rows = statement.raw_query();
56 match rows.next()? {
57 Some(row) => map(row),
58 None => Err(rusqlite::Error::QueryReturnedNoRows),
59 }
60 }
61
62 pub fn query<T, P, F>(&self, sql: &str, params: P, map: F) -> rusqlite::Result<Vec<T>>
63 where
64 P: rusqlite::Params,
65 F: FnMut(&rusqlite::Row<'_>) -> rusqlite::Result<T>,
66 {
67 let mut statement = self.prepare_tracked(sql)?;
68 if let Err(error) = params.__bind_in(&mut statement) {
69 self.finish_statement(None);
70 return Err(error);
71 }
72 self.finish_statement(statement.expanded_sql());
73 let mut rows = statement.raw_query();
74 let mut map = map;
75 let mut values = Vec::new();
76 while let Some(row) = rows.next()? {
77 values.push(map(row)?);
78 }
79 Ok(values)
80 }
81
82 fn prepare_tracked(&self, sql: &str) -> rusqlite::Result<rusqlite::Statement<'connection>> {
83 if let Some(dependencies) = &self.dependencies {
84 dependencies.begin_statement();
85 }
86 match self.connection.prepare(sql) {
87 Ok(statement) => Ok(statement),
88 Err(error) => {
89 self.finish_statement(None);
90 Err(error)
91 }
92 }
93 }
94
95 fn finish_statement(&self, expanded_sql: Option<String>) {
96 if let Some(dependencies) = &self.dependencies {
97 dependencies.finish_statement(expanded_sql);
98 }
99 }
100}
101
102pub struct SqlContext<'context, 'connection> {
107 transaction: &'context rusqlite::Transaction<'connection>,
108 stamper: UpdatedAtStamper,
109 tables: &'context [SyncedTable],
110 gates: &'context Gates,
111}
112
113impl<'context, 'connection> SqlContext<'context, 'connection> {
114 pub(crate) fn new(
115 transaction: &'context rusqlite::Transaction<'connection>,
116 stamper: UpdatedAtStamper,
117 tables: &'context [SyncedTable],
118 gates: &'context Gates,
119 ) -> Self {
120 Self {
121 transaction,
122 stamper,
123 tables,
124 gates,
125 }
126 }
127
128 fn blob_table(&self, table: &str) -> Result<&SyncedTable, DbError> {
129 let declared = self
130 .tables
131 .iter()
132 .find(|candidate| candidate.name() == table)
133 .ok_or_else(|| DbError::Message(format!("undeclared synced table {table:?}")))?;
134 if declared.blob().is_none() {
135 return Err(DbError::Message(format!(
136 "synced table {table:?} has no blob declaration"
137 )));
138 }
139 Ok(declared)
140 }
141
142 fn user_provided_blob_table(&self, table: &str) -> Result<&SyncedTable, DbError> {
143 let declared = self.blob_table(table)?;
144 let blob = declared.blob().expect("blob_table requires a declaration");
145 if blob.provenance != Provenance::UserProvided {
146 return Err(DbError::Message(format!(
147 "table {table:?} declares host-provided blobs, which Coven copies; \
148 an external file registration on it would never be read"
149 )));
150 }
151 Ok(declared)
152 }
153
154 fn register_prepared_external_blob(
155 &self,
156 declared: &SyncedTable,
157 table: &str,
158 row_id: &str,
159 prepared: PreparedExternalBlob,
160 ) -> Result<(), DbError> {
161 prepared.validate_current()?;
162 let blob = declared.blob().expect("blob table requires a declaration");
163 let table_ident = crate::quote_ident(table);
164 let size_ident = crate::quote_ident(&blob.size_column);
165 let hash_ident = crate::quote_ident(&blob.hash_column);
166 let select = format!("SELECT {size_ident}, {hash_ident} FROM {table_ident} WHERE id = ?1");
167 let (declared_size, declared_hash) =
168 self.transaction.query_row(&select, [row_id], |row| {
169 Ok((row.get::<_, i64>(0)?, row.get::<_, Option<String>>(1)?))
170 })?;
171 let declared_size = u64::try_from(declared_size).map_err(|_| {
172 DbError::Message(format!(
173 "external blob row {table:?}/{row_id:?} has negative size"
174 ))
175 })?;
176 if declared_size != prepared.size() {
177 return Err(DbError::Message(format!(
178 "external blob row {table:?}/{row_id:?} declares {declared_size} bytes, but Coven read {}",
179 prepared.size()
180 )));
181 }
182 match declared_hash {
183 Some(hash) if hash != prepared.hash() => {
184 return Err(DbError::Message(format!(
185 "external blob row {table:?}/{row_id:?} already declares different content"
186 )));
187 }
188 Some(_) => {}
189 None => {
190 let update = format!(
191 "UPDATE {table_ident} SET {hash_ident} = ?1 \
192 WHERE id = ?2 AND {hash_ident} IS NULL"
193 );
194 let updated = self
195 .transaction
196 .execute(&update, rusqlite::params![prepared.hash(), row_id])?;
197 if updated != 1 {
198 return Err(DbError::Message(format!(
199 "external blob row {table:?}/{row_id:?} changed before registration"
200 )));
201 }
202 }
203 }
204 let reference = Database::row_blob_ref_on(self.transaction, self.gates, declared, row_id)?;
205 ExternalBlobRecords::new(self.transaction).register(&reference, prepared.path())
206 }
207
208 pub fn execute<P>(&self, sql: &str, params: P) -> rusqlite::Result<usize>
209 where
210 P: rusqlite::Params,
211 {
212 self.transaction.execute(sql, params)
213 }
214
215 pub fn execute_batch(&self, sql: &str) -> rusqlite::Result<()> {
216 self.transaction.execute_batch(sql)
217 }
218
219 pub fn query_row<T, P, F>(&self, sql: &str, params: P, map: F) -> rusqlite::Result<T>
220 where
221 P: rusqlite::Params,
222 F: FnOnce(&rusqlite::Row<'_>) -> rusqlite::Result<T>,
223 {
224 self.transaction.query_row(sql, params, map)
225 }
226
227 pub fn query<T, P, F>(&self, sql: &str, params: P, map: F) -> rusqlite::Result<Vec<T>>
228 where
229 P: rusqlite::Params,
230 F: FnMut(&rusqlite::Row<'_>) -> rusqlite::Result<T>,
231 {
232 let mut statement = self.transaction.prepare(sql)?;
233 let values = statement.query_map(params, map)?.collect();
234 values
235 }
236
237 pub fn stamp(&self) -> String {
238 self.stamper.stamp()
239 }
240
241 pub fn insert_external_blob(
247 &self,
248 table: &str,
249 row_id: &str,
250 prepared: PreparedExternalBlob,
251 insert_sql: &str,
252 params: &[(&str, &dyn rusqlite::ToSql)],
253 ) -> Result<(), DbError> {
254 crate::observe_host_sql_write();
255 crate::with_coven_sql_authority(|| {
256 let declared = self.user_provided_blob_table(table)?;
257 if params
258 .iter()
259 .any(|(name, _)| *name == EXTERNAL_BLOB_HASH_PARAMETER)
260 {
261 return Err(DbError::Message(format!(
262 "{EXTERNAL_BLOB_HASH_PARAMETER} is reserved for Coven"
263 )));
264 }
265 let mut names = std::collections::HashSet::with_capacity(params.len());
266 if params.iter().any(|(name, _)| !names.insert(*name)) {
267 return Err(DbError::Message(
268 "external blob insert parameters contain a duplicate name".to_string(),
269 ));
270 }
271
272 let mut statement = self.transaction.prepare(insert_sql)?;
273 let hash_index = statement
274 .parameter_index(EXTERNAL_BLOB_HASH_PARAMETER)?
275 .ok_or_else(|| {
276 DbError::Message(format!(
277 "external blob insert is missing {EXTERNAL_BLOB_HASH_PARAMETER}"
278 ))
279 })?;
280 let expected_parameters = params.len() + 1;
281 if statement.parameter_count() != expected_parameters {
282 return Err(DbError::Message(format!(
283 "external blob insert declares {} parameters, expected {expected_parameters}",
284 statement.parameter_count()
285 )));
286 }
287 rusqlite::Params::__bind_in(params, &mut statement)?;
288 statement.raw_bind_parameter(hash_index, prepared.hash())?;
289 let inserted = statement.raw_execute()?;
290 if inserted != 1 {
291 return Err(DbError::Message(format!(
292 "external blob insert changed {inserted} rows, expected 1"
293 )));
294 }
295 drop(statement);
296 self.register_prepared_external_blob(declared, table, row_id, prepared)
297 })
298 }
299
300 pub fn register_external_blob(
301 &self,
302 table: &str,
303 row_id: &str,
304 prepared: PreparedExternalBlob,
305 ) -> Result<(), DbError> {
306 crate::observe_host_sql_write();
307 crate::with_coven_sql_authority(|| {
308 let declared = self.user_provided_blob_table(table)?;
309 self.register_prepared_external_blob(declared, table, row_id, prepared)
310 })
311 }
312
313 pub fn validate_row_blob_ref(
317 &self,
318 reference: &coven_protocol::blob::RowBlobRef,
319 ) -> Result<(), DbError> {
320 crate::with_coven_sql_authority(|| {
321 let table = self.blob_table(reference.table())?;
322 Database::validate_row_blob_ref_on(self.transaction, self.gates, table, reference)
323 })
324 }
325
326 pub fn enqueue_blob_delete(
327 &self,
328 blob: &coven_protocol::blob::RowBlobRef,
329 ) -> Result<(), DbError> {
330 crate::observe_host_sql_write();
331 let stored = blob.stored().ok_or_else(|| {
332 DbError::Message(format!(
333 "blob {:?} in {:?} has no cloud object to remove",
334 blob.blob().id,
335 blob.blob().namespace
336 ))
337 })?;
338 crate::with_coven_sql_authority(|| {
339 CloudOutboxRecords::new(self.transaction).enqueue_delete(stored, &self.stamp())
340 })
341 }
342
343 pub fn clear_external_blob(&self, table: &str, row_id: &str) -> Result<(), DbError> {
344 crate::observe_host_sql_write();
345 crate::with_coven_sql_authority(|| {
346 let declared = self.blob_table(table)?;
347 let reference =
348 Database::row_blob_ref_on(self.transaction, self.gates, declared, row_id)?;
349 ExternalBlobRecords::new(self.transaction).clear(&reference)
350 })
351 }
352
353 #[cfg(any(test, feature = "test-utils"))]
354 pub fn materialized_sequence(&self, stream_id: &str) -> Result<Option<u64>, DbError> {
355 use rusqlite::OptionalExtension;
356
357 crate::with_coven_sql_authority(|| {
358 self.transaction
359 .query_row(
360 "SELECT seq FROM materialized_commits WHERE device_id = ?1",
361 [stream_id],
362 |row| row.get::<_, i64>(0),
363 )
364 .optional()
365 .map_err(DbError::from)?
366 .map(|sequence| {
367 u64::try_from(sequence)
368 .map_err(|error| DbError::context("invalid sequence", error))
369 })
370 .transpose()
371 })
372 }
373}