Skip to main content

sim_platform_sqlite/
driver.rs

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/// Capsule-owned map from stable preopened names to private host paths.
30#[derive(Clone, Default)]
31pub struct PreopenedStores(Arc<BTreeMap<Symbol, PathBuf>>);
32impl PreopenedStores {
33    /// Builds the capsule authority map. Paths never enter the relation locator.
34    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
42/// The sole `SQLite` driver, configured with admitted domains and preopened storage.
43pub struct SqliteDriver {
44    domains: Arc<DomainCatalog>,
45    stores: PreopenedStores,
46    busy_ms: u32,
47}
48impl SqliteDriver {
49    /// Constructs the capsule driver.
50    #[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    /// Constructs the canonical loadable site library.
59    ///
60    /// # Errors
61    ///
62    /// Returns a registration refusal when the canonical driver manifest is
63    /// invalid.
64    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/// Canonical exported site symbol.
75#[must_use]
76pub fn site_symbol() -> Symbol {
77    Symbol::qualified("relation/site", "sqlite")
78}
79/// Canonical provider identity.
80#[must_use]
81pub fn provider_symbol() -> Symbol {
82    Symbol::qualified("relation/provider", "sqlite")
83}
84/// Verifies that a manifest declares exactly the `SQLite` kernel site export.
85///
86/// # Errors
87///
88/// Returns [`SiteError::Registration`] unless exactly one canonical site export
89/// is declared.
90pub 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
551/// Normalizes the main `SQLite` catalog, excluding capsule metadata.
552///
553/// # Errors
554///
555/// Returns a typed site refusal when catalog queries fail or observed names,
556/// domains, columns, indexes, or revisions cannot be normalized.
557mod introspection;
558
559pub use introspection::{introspect_connection, verify_or_adopt};
560
561#[cfg(test)]
562#[path = "tests.rs"]
563mod tests;