1use rusqlite::{
2 Connection, OpenFlags, ToSql,
3 types::{FromSql, Value, ValueRef},
4};
5use sim_codec_sql::{
6 PreparedSql, SqlBinding, SqliteDialect, prepare_migration, prepare_mutation, prepare_query,
7};
8use sim_kernel::{Datum, LibManifest, Symbol};
9use sim_relation_core::{
10 BaseDomain, Cell, ColumnName, DomainCatalog, DomainId, IndexName, ProviderName, RevisionName,
11 Row, SchemaName, StorageValue, TableName,
12};
13use sim_relation_migrate::{AdoptionManifest, CheckedProgram, SchemaAttestation};
14use sim_relation_plan::{CheckedMutation, CheckedQuery};
15use sim_relation_schema::{PhysicalColumn, PhysicalIndex, PhysicalSchema, PhysicalTable};
16use sim_relation_site::{
17 Bindings, Driver, DriverManifest, Limits, ProviderStats, RelationPlacement, RelationSite,
18 RelationSiteLib, RowSink, Session, SiteError, StorageAccess, StorageLocator, Transaction,
19};
20use std::{
21 collections::{BTreeMap, HashMap},
22 path::{Path, PathBuf},
23 sync::Arc,
24 time::Instant,
25};
26
27const HEX: &[u8; 16] = b"0123456789abcdef";
28
29#[derive(Clone, Default)]
31pub struct PreopenedStores(Arc<BTreeMap<Symbol, PathBuf>>);
32impl PreopenedStores {
33 pub fn new(values: impl IntoIterator<Item = (Symbol, PathBuf)>) -> Self {
35 Self(Arc::new(values.into_iter().collect()))
36 }
37 fn resolve(&self, name: &Symbol) -> Option<&Path> {
38 self.0.get(name).map(PathBuf::as_path)
39 }
40}
41
42pub struct SqliteDriver {
44 domains: Arc<DomainCatalog>,
45 stores: PreopenedStores,
46 busy_ms: u32,
47}
48impl SqliteDriver {
49 #[must_use]
51 pub fn new(domains: DomainCatalog, stores: PreopenedStores) -> Self {
52 Self {
53 domains: Arc::new(domains),
54 stores,
55 busy_ms: 2_000,
56 }
57 }
58 pub fn library(self, locator: &StorageLocator) -> Result<RelationSiteLib, SiteError> {
65 let manifest = DriverManifest::sqlite(site_symbol(), provider_symbol())?;
66 let datum = locator_datum(locator);
67 Ok(RelationSiteLib::new(RelationSite::new(
68 RelationPlacement::new(manifest.site, datum),
69 Arc::new(self),
70 )))
71 }
72}
73
74#[must_use]
76pub fn site_symbol() -> Symbol {
77 Symbol::qualified("relation/site", "sqlite")
78}
79#[must_use]
81pub fn provider_symbol() -> Symbol {
82 Symbol::qualified("relation/provider", "sqlite")
83}
84pub fn verify_manifest(manifest: &LibManifest) -> Result<(), SiteError> {
91 let count = manifest.exports.iter().filter(|export| matches!(export, sim_kernel::Export::Site { symbol, .. } if symbol == &site_symbol())).count();
92 if count == 1 {
93 Ok(())
94 } else {
95 Err(SiteError::Registration)
96 }
97}
98
99fn locator_datum(value: &StorageLocator) -> Datum {
100 match value {
101 StorageLocator::Memory => Datum::Node {
102 tag: Symbol::qualified("relation", "memory"),
103 fields: vec![],
104 },
105 StorageLocator::Preopened { reference, access } => Datum::Node {
106 tag: Symbol::qualified("relation", "preopened"),
107 fields: vec![
108 (Symbol::new("ref"), Datum::Symbol(reference.clone())),
109 (
110 Symbol::new("access"),
111 Datum::Symbol(Symbol::new(match access {
112 StorageAccess::ReadOnly => "read-only",
113 StorageAccess::ReadWrite => "read-write",
114 })),
115 ),
116 ],
117 },
118 }
119}
120
121impl Driver for SqliteDriver {
122 fn connect(&self, locator: &Datum, limits: &Limits) -> Result<Box<dyn Session>, SiteError> {
123 let locator = StorageLocator::from_datum(locator)?;
124 let connection = match locator {
125 StorageLocator::Memory => Connection::open_in_memory(),
126 StorageLocator::Preopened { reference, access } => {
127 let path = self.stores.resolve(&reference).ok_or(SiteError::Locator)?;
128 let flags = match access {
129 StorageAccess::ReadOnly => OpenFlags::SQLITE_OPEN_READ_ONLY,
130 StorageAccess::ReadWrite => {
131 OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE
132 }
133 };
134 Connection::open_with_flags(path, flags)
135 }
136 }
137 .map_err(|error| map_error(&error))?;
138 configure(&connection, self.busy_ms, limits)?;
139 Ok(Box::new(SqliteSession {
140 connection,
141 domains: self.domains.clone(),
142 stores: self.stores.clone(),
143 cache: HashMap::new(),
144 generation: 0,
145 savepoint: 0,
146 }))
147 }
148}
149
150fn configure(connection: &Connection, busy_ms: u32, limits: &Limits) -> Result<(), SiteError> {
151 connection
152 .pragma_update(None, "foreign_keys", "ON")
153 .map_err(|error| map_error(&error))?;
154 connection
155 .busy_timeout(std::time::Duration::from_millis(u64::from(busy_ms)))
156 .map_err(|error| map_error(&error))?;
157 let started = Instant::now();
158 let deadline = limits.deadline;
159 connection.progress_handler(
160 100,
161 Some(move || deadline.is_some_and(|limit| started.elapsed() >= limit)),
162 );
163 Ok(())
164}
165
166struct SqliteSession {
167 connection: Connection,
168 domains: Arc<DomainCatalog>,
169 stores: PreopenedStores,
170 cache: HashMap<String, String>,
171 generation: u64,
172 savepoint: u64,
173}
174impl SqliteSession {
175 fn execute_rows(
176 &mut self,
177 prepared: &PreparedSql,
178 bindings: &Bindings,
179 limits: &Limits,
180 sink: &mut dyn RowSink,
181 ) -> Result<ProviderStats, SiteError> {
182 let key = format!("{:?}:{}", prepared.cache_key(), self.generation);
183 self.cache
184 .entry(key)
185 .or_insert_with(|| prepared.text().to_owned());
186 let values = bind_values(prepared, bindings, &self.domains)?;
187 let refs: Vec<&dyn ToSql> = values.iter().map(|v| v as &dyn ToSql).collect();
188 let mut statement = self
189 .connection
190 .prepare_cached(prepared.text())
191 .map_err(|error| map_error(&error))?;
192 if prepared.cache_key().output_row_type.fields().is_empty() {
193 let affected = statement
194 .execute(refs.as_slice())
195 .map_err(|error| map_error(&error))? as u64;
196 return Ok(ProviderStats {
197 work: affected.max(1),
198 affected,
199 });
200 }
201 let mut rows = statement
202 .query(refs.as_slice())
203 .map_err(|error| map_error(&error))?;
204 let mut work = 0u64;
205 while let Some(row) = rows.next().map_err(|error| map_error(&error))? {
206 work = work
207 .checked_add(1)
208 .ok_or(SiteError::Limit(sim_relation_site::LimitKind::Work))?;
209 if work > limits.work {
210 return Err(SiteError::Limit(sim_relation_site::LimitKind::Work));
211 }
212 let cells = prepared
213 .cache_key()
214 .output_row_type
215 .fields()
216 .iter()
217 .enumerate()
218 .map(|(index, field)| {
219 decode_cell(
220 row.get_ref(index).map_err(|error| map_error(&error))?,
221 &field.domain,
222 &self.domains,
223 )
224 })
225 .collect::<Result<Vec<_>, _>>()?;
226 let admitted = Row::new(prepared.cache_key().output_row_type.clone(), cells)
227 .map_err(|_| SiteError::Conversion)?;
228 sink.push(admitted)?;
229 }
230 Ok(ProviderStats { work, affected: 0 })
231 }
232 fn migrate_inner(
233 &mut self,
234 program: &CheckedProgram,
235 limits: &Limits,
236 ) -> Result<ProviderStats, SiteError> {
237 let catalog = program
238 .program()
239 .base_schema
240 .id()
241 .map_err(|_| SiteError::Provider)?;
242 let statements = prepare_migration(program, &catalog, &SqliteDialect)
243 .map_err(|_| SiteError::Provider)?;
244 self.connection
245 .execute_batch("BEGIN IMMEDIATE")
246 .map_err(|error| map_error(&error))?;
247 let result = (|| {
248 let empty = sim_relation_core::RowType::new([]).map_err(|_| SiteError::Provider)?;
249 let bindings = Bindings::new(&empty, []).map_err(|_| SiteError::Provider)?;
250 let mut sink = NullSink;
251 let mut stats = ProviderStats::default();
252 for statement in statements.statements() {
253 let got = self.execute_rows(statement, &bindings, limits, &mut sink)?;
254 stats.work += got.work;
255 stats.affected += got.affected;
256 }
257 self.write_attestation(program)?;
258 Ok(stats)
259 })();
260 match result {
261 Ok(stats) => {
262 self.connection
263 .execute_batch("COMMIT")
264 .map_err(|error| map_error(&error))?;
265 self.invalidate();
266 Ok(stats)
267 }
268 Err(error) => {
269 let _ = self.connection.execute_batch("ROLLBACK");
270 Err(error)
271 }
272 }
273 }
274 fn write_attestation(&self, program: &CheckedProgram) -> Result<(), SiteError> {
275 self.connection.execute_batch("CREATE TABLE IF NOT EXISTS __sim_relation_attestation (singleton INTEGER PRIMARY KEY CHECK(singleton=1), logical_schema TEXT NOT NULL, physical_schema TEXT NOT NULL, revision TEXT NOT NULL)").map_err(|error| map_error(&error))?;
276 let physical = self.introspect(
277 RevisionName::new(Symbol::new("current")).map_err(|_| SiteError::Conversion)?,
278 )?;
279 let logical = relation_id_text(&program.program().target_schema);
280 let revision_id = program
281 .program()
282 .revisions
283 .last()
284 .map_or(&program.program().base_revision, |r| r.id());
285 let revision = relation_id_text(revision_id);
286 let physical_id = physical.id().map_err(|_| SiteError::Provider)?;
287 self.connection
288 .execute(
289 "INSERT OR REPLACE INTO __sim_relation_attestation VALUES (1, ?1, ?2, ?3)",
290 (&logical, relation_id_text(&physical_id), revision),
291 )
292 .map_err(|error| map_error(&error))?;
293 Ok(())
294 }
295 fn invalidate(&mut self) {
296 self.connection.flush_prepared_statement_cache();
297 self.cache.clear();
298 self.generation = self.generation.wrapping_add(1);
299 }
300 fn introspect(&self, revision: RevisionName) -> Result<PhysicalSchema, SiteError> {
301 introspect_connection(&self.connection, revision)
302 }
303}
304
305impl Session for SqliteSession {
306 fn query(
307 &mut self,
308 plan: &CheckedQuery,
309 bindings: &Bindings,
310 limits: &Limits,
311 sink: &mut dyn RowSink,
312 ) -> Result<ProviderStats, SiteError> {
313 let prepared = prepare_query(plan, &SqliteDialect).map_err(|_| SiteError::Provider)?;
314 self.execute_rows(&prepared, bindings, limits, sink)
315 }
316 fn mutate(
317 &mut self,
318 plan: &CheckedMutation,
319 bindings: &Bindings,
320 limits: &Limits,
321 sink: &mut dyn RowSink,
322 ) -> Result<ProviderStats, SiteError> {
323 let prepared = prepare_mutation(plan, &SqliteDialect).map_err(|_| SiteError::Provider)?;
324 self.execute_rows(&prepared, bindings, limits, sink)
325 }
326 fn migrate(
327 &mut self,
328 program: &CheckedProgram,
329 limits: &Limits,
330 ) -> Result<ProviderStats, SiteError> {
331 self.migrate_inner(program, limits)
332 }
333 fn schema(
334 &mut self,
335 program: &CheckedProgram,
336 limits: &Limits,
337 ) -> Result<ProviderStats, SiteError> {
338 self.migrate_inner(program, limits)
339 }
340 fn transaction(
341 &mut self,
342 body: &mut dyn FnMut(&mut dyn Transaction) -> Result<(), SiteError>,
343 ) -> Result<(), SiteError> {
344 self.connection
345 .execute_batch("BEGIN IMMEDIATE")
346 .map_err(|error| map_error(&error))?;
347 match body(self) {
348 Ok(()) => self
349 .connection
350 .execute_batch("COMMIT")
351 .map_err(|error| map_error(&error)),
352 Err(error) => {
353 let _ = self.connection.execute_batch("ROLLBACK");
354 Err(error)
355 }
356 }
357 }
358 fn attach(&mut self, locator: &Datum, _: &Limits) -> Result<ProviderStats, SiteError> {
359 let Datum::Node { tag, fields } = locator else {
360 return Err(SiteError::Locator);
361 };
362 if tag != &Symbol::qualified("relation", "attach") || fields.len() != 3 {
363 return Err(SiteError::Locator);
364 }
365 let get = |key: &str| {
366 fields
367 .iter()
368 .find(|(name, _)| name == &Symbol::new(key))
369 .map(|(_, value)| value)
370 };
371 let (
372 Some(Datum::Symbol(name)),
373 Some(Datum::Symbol(reference)),
374 Some(Datum::Symbol(access)),
375 ) = (get("name"), get("ref"), get("access"))
376 else {
377 return Err(SiteError::Locator);
378 };
379 if !valid_source(name) {
380 return Err(SiteError::Locator);
381 }
382 let path = self.stores.resolve(reference).ok_or(SiteError::Locator)?;
383 if access.namespace.is_some() {
384 return Err(SiteError::Locator);
385 }
386 let uri = match access.name.as_ref() {
387 "read-only" => format!("file:{}?mode=ro", path.display()),
388 "read-write" => path.display().to_string(),
389 _ => return Err(SiteError::Locator),
390 };
391 self.connection
392 .execute("ATTACH DATABASE ?1 AS ?2", (&uri, name.name.as_ref()))
393 .map_err(|error| map_error(&error))?;
394 self.invalidate();
395 Ok(ProviderStats {
396 work: 1,
397 affected: 0,
398 })
399 }
400}
401impl Transaction for SqliteSession {
402 fn savepoint(
403 &mut self,
404 body: &mut dyn FnMut(&mut dyn Transaction) -> Result<(), SiteError>,
405 ) -> Result<(), SiteError> {
406 self.savepoint += 1;
407 let name = format!("sim_savepoint_{}", self.savepoint);
408 self.connection
409 .execute_batch(&format!("SAVEPOINT {name}"))
410 .map_err(|error| map_error(&error))?;
411 match body(self) {
412 Ok(()) => self
413 .connection
414 .execute_batch(&format!("RELEASE {name}"))
415 .map_err(|error| map_error(&error)),
416 Err(error) => {
417 let _ = self
418 .connection
419 .execute_batch(&format!("ROLLBACK TO {name}; RELEASE {name}"));
420 Err(error)
421 }
422 }
423 }
424}
425
426struct NullSink;
427impl RowSink for NullSink {
428 fn push(&mut self, _: Row) -> Result<(), SiteError> {
429 Ok(())
430 }
431}
432
433fn bind_values(
434 prepared: &PreparedSql,
435 supplied: &Bindings,
436 domains: &DomainCatalog,
437) -> Result<Vec<Value>, SiteError> {
438 prepared
439 .bindings()
440 .iter()
441 .map(|binding| match binding {
442 SqlBinding::Literal(cell) => encode_cell(cell, domains),
443 SqlBinding::Parameter(name) => supplied
444 .row()
445 .row_type()
446 .fields()
447 .iter()
448 .position(|field| field.name.symbol() == name.symbol())
449 .map(|index| encode_cell(&supplied.row().cells()[index], domains))
450 .ok_or(SiteError::Conversion)?,
451 })
452 .collect()
453}
454fn base(domain: &DomainId, domains: &DomainCatalog) -> Result<BaseDomain, SiteError> {
455 let storage = domains.get(domain).ok_or(SiteError::Conversion)?.storage();
456 Ok(match storage {
457 sim_relation_core::StorageRepr::Bool => BaseDomain::Bool,
458 sim_relation_core::StorageRepr::I64 => BaseDomain::I64,
459 sim_relation_core::StorageRepr::F64 => BaseDomain::F64,
460 sim_relation_core::StorageRepr::Text => BaseDomain::Text,
461 sim_relation_core::StorageRepr::Bytes => BaseDomain::Bytes,
462 })
463}
464fn encode_cell(cell: &Cell, domains: &DomainCatalog) -> Result<Value, SiteError> {
465 let Some(value) = cell.value() else {
466 return Ok(Value::Null);
467 };
468 Ok(
469 match base(cell.domain(), domains)?
470 .from_datum(value)
471 .map_err(|_| SiteError::Conversion)?
472 {
473 StorageValue::Bool(v) => Value::Integer(i64::from(v)),
474 StorageValue::I64(v) => Value::Integer(v),
475 StorageValue::F64(v) => Value::Real(v),
476 StorageValue::Text(v) => Value::Text(v),
477 StorageValue::Bytes(v) => Value::Blob(v),
478 },
479 )
480}
481fn decode_cell(
482 value: ValueRef<'_>,
483 domain: &DomainId,
484 domains: &DomainCatalog,
485) -> Result<Cell, SiteError> {
486 if value == ValueRef::Null {
487 return Ok(Cell::null(domain.clone()));
488 }
489 let base = base(domain, domains)?;
490 let storage = match base {
491 BaseDomain::Bool => {
492 StorageValue::Bool(i64::column_result(value).map_err(|_| SiteError::Conversion)? != 0)
493 }
494 BaseDomain::I64 => {
495 StorageValue::I64(i64::column_result(value).map_err(|_| SiteError::Conversion)?)
496 }
497 BaseDomain::F64 => {
498 StorageValue::F64(f64::column_result(value).map_err(|_| SiteError::Conversion)?)
499 }
500 BaseDomain::Text => {
501 StorageValue::Text(String::column_result(value).map_err(|_| SiteError::Conversion)?)
502 }
503 BaseDomain::Bytes => {
504 StorageValue::Bytes(Vec::<u8>::column_result(value).map_err(|_| SiteError::Conversion)?)
505 }
506 };
507 Ok(Cell::new(
508 domain.clone(),
509 Some(base.to_datum(storage).map_err(|_| SiteError::Conversion)?),
510 ))
511}
512fn valid_source(value: &Symbol) -> bool {
513 if value.namespace.is_some() {
514 return false;
515 }
516 let text = value.name.as_ref();
517 !text.is_empty() && text.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_')
518}
519fn relation_id_text(value: &sim_relation_core::RelationId) -> String {
520 let content = value.content_id();
521 let mut digest = String::with_capacity(content.bytes.len() * 2);
522 for byte in content.bytes {
523 digest.push(char::from(HEX[usize::from(byte >> 4)]));
524 digest.push(char::from(HEX[usize::from(byte & 0x0f)]));
525 }
526 format!("{}:{digest}", content.algorithm)
527}
528fn map_error(error: &rusqlite::Error) -> SiteError {
529 use rusqlite::{
530 Error::SqliteFailure,
531 ffi::ErrorCode::{
532 ConstraintViolation, DatabaseBusy, DatabaseCorrupt, DatabaseLocked, NotADatabase,
533 OperationInterrupted, ReadOnly,
534 },
535 };
536 match error {
537 SqliteFailure(inner, _) => match inner.code {
538 ConstraintViolation => SiteError::Constraint,
539 DatabaseBusy | DatabaseLocked => SiteError::Locked,
540 ReadOnly => SiteError::ReadOnly,
541 OperationInterrupted => SiteError::Interrupted,
542 DatabaseCorrupt | NotADatabase => SiteError::Corruption,
543 _ => SiteError::Provider,
544 },
545 rusqlite::Error::FromSqlConversionFailure(..)
546 | rusqlite::Error::IntegralValueOutOfRange(..) => SiteError::Conversion,
547 _ => SiteError::Provider,
548 }
549}
550
551mod introspection;
558
559pub use introspection::{introspect_connection, verify_or_adopt};
560
561#[cfg(test)]
562#[path = "tests.rs"]
563mod tests;