Skip to main content

sbol_db_sqlite/
store.rs

1//! The SQLite-backed SBOL store: ingest plus the derived-view read surface,
2//! the SPARQL read/write adapters, and the storage-trait implementations
3//! (objects, graphs, ontology, neighborhood, sequence search, and the lab
4//! dashboard). The job queue lives in [`crate::repo::job`].
5
6use std::collections::HashSet;
7use std::sync::Arc;
8
9use async_trait::async_trait;
10use sbol_db_core::{
11    DomainError, GraphId, GraphRecord, ImportReport, IriString, NeighborhoodQuery,
12    NeighborhoodResult, ObjectId, SbolObjectRecord, SerializationFormat, Triple,
13};
14use sbol_db_derive::{build_import_plan, to_rdf_format};
15use sbol_db_rdf::rdf_graph_to_triples;
16use sbol_db_storage::{
17    AccelSolutions, AcceleratedQuery, BatchSequenceMatch, ClassCount, CorpusCounts, GraphFilter,
18    GraphOverview, GraphStore, GraphTriplesPage, GraphWriteMode, ImportInput, LabStore,
19    ListGraphsFilter, ListObjectsFilter, NeighborhoodStore, ObjectStore, OntologyLoadReport,
20    OntologyRecord, OntologyStore, OntologyTermRecord, PatternObject, PatternSubject, SbolStore,
21    SequenceMatch, SequenceSearchOptions, SequenceSearchStore, TripleChange, TripleSource,
22    TripleWriter, UpdateOutcome,
23};
24use tokio::runtime::Handle;
25
26use crate::pool::db_err;
27use crate::repo::{
28    AccelRepository, GraphRepository, LabRepository, OntologyRepository, SbolObjectRepository,
29    SequenceSearchRepository, TripleRepository,
30};
31use crate::SqlitePool;
32
33/// Per-call cap on a Graph Store `GET`, matching the Postgres backend.
34const GRAPH_READ_LIMIT: i64 = 5_000_000;
35const UPDATE_SOURCE: &str = "sparql-update";
36
37/// The SQLite SBOL store. Cloneable; all clones share one connection pool.
38#[derive(Clone)]
39pub struct SqliteStore {
40    pool: SqlitePool,
41    graphs: GraphRepository,
42    objects: SbolObjectRepository,
43    triples: TripleRepository,
44    accel: AccelRepository,
45    ontology: OntologyRepository,
46    sequences: SequenceSearchRepository,
47    lab: LabRepository,
48}
49
50impl SqliteStore {
51    pub fn new(pool: SqlitePool) -> Self {
52        Self {
53            graphs: GraphRepository::new(pool.clone()),
54            objects: SbolObjectRepository::new(pool.clone()),
55            triples: TripleRepository::new(pool.clone()),
56            accel: AccelRepository::new(pool.clone(), TripleRepository::new(pool.clone())),
57            ontology: OntologyRepository::new(pool.clone()),
58            sequences: SequenceSearchRepository::new(pool.clone()),
59            lab: LabRepository::new(pool.clone()),
60            pool,
61        }
62    }
63
64    pub fn pool(&self) -> &SqlitePool {
65        &self.pool
66    }
67
68    pub async fn ping(&self) -> Result<(), DomainError> {
69        sqlx::query("SELECT 1")
70            .execute(&self.pool)
71            .await
72            .map(|_| ())
73            .map_err(db_err)
74    }
75
76    pub fn triple_source(&self) -> Arc<dyn TripleSource> {
77        Arc::new(SqliteTripleSource {
78            triples: Arc::new(self.triples.clone()),
79            accel: self.accel.clone(),
80        })
81    }
82
83    pub fn triple_writer(&self) -> Arc<dyn TripleWriter> {
84        Arc::new(SqliteTripleWriter {
85            triples: Arc::new(self.triples.clone()),
86            accel: self.accel.clone(),
87            pool: self.pool.clone(),
88        })
89    }
90
91    pub async fn import_document(&self, input: ImportInput) -> Result<ImportReport, DomainError> {
92        let mut tx = self.pool.begin().await.map_err(db_err)?;
93        let report = self.import_into_conn(&mut tx, input).await?;
94        tx.commit().await.map_err(db_err)?;
95        Ok(report)
96    }
97
98    pub async fn import_documents(
99        &self,
100        inputs: Vec<ImportInput>,
101    ) -> Result<Vec<ImportReport>, DomainError> {
102        if inputs.is_empty() {
103            return Ok(Vec::new());
104        }
105        let mut tx = self.pool.begin().await.map_err(db_err)?;
106        let mut reports = Vec::with_capacity(inputs.len());
107        for input in inputs {
108            reports.push(self.import_into_conn(&mut tx, input).await?);
109        }
110        tx.commit().await.map_err(db_err)?;
111        Ok(reports)
112    }
113
114    async fn import_into_conn(
115        &self,
116        conn: &mut sqlx::SqliteConnection,
117        input: ImportInput,
118    ) -> Result<ImportReport, DomainError> {
119        let plan = build_import_plan(&input)?;
120
121        self.graphs
122            .insert(&mut *conn, plan.graph_id, plan.new_graph)
123            .await?;
124        let triple_count = self
125            .triples
126            .insert_triples(&mut *conn, &plan.triples, "sbol")
127            .await?;
128        self.accel
129            .refresh_graph(&mut *conn, plan.graph_iri.as_str())
130            .await?;
131        let object_count = plan.summaries.len();
132        for summary in &plan.summaries {
133            self.objects
134                .upsert(&mut *conn, summary, Some(plan.graph_id))
135                .await?;
136        }
137
138        // Persist sequence projections so sequence search has data + a k-mer
139        // index. (Other typed projections are not yet materialized in SQLite.)
140        for sequence in &plan.projections.sequences {
141            self.sequences.upsert_sequence(&mut *conn, sequence).await?;
142        }
143
144        Ok(ImportReport {
145            graph_id: plan.graph_id,
146            object_count,
147            triple_count,
148            validation_status: plan.validation_status,
149            validation_issue_count: plan.validation_issue_count,
150        })
151    }
152
153    pub async fn graph_store_write(
154        &self,
155        graph: &str,
156        body: &str,
157        format: SerializationFormat,
158        mode: GraphWriteMode,
159    ) -> Result<usize, DomainError> {
160        let rdf_format = to_rdf_format(format)?;
161        let parsed = sbol_rdf::Graph::parse(body, rdf_format)
162            .map_err(|e| DomainError::Parse(e.to_string()))?;
163        let triples = rdf_graph_to_triples(&parsed, &IriString::unchecked(graph));
164
165        let mut tx = self.pool.begin().await.map_err(db_err)?;
166        self.triples
167            .ensure_graph(&mut tx, graph, "verbatim")
168            .await?;
169        if mode == GraphWriteMode::Replace {
170            self.triples.clear_graph(&mut tx, Some(graph)).await?;
171        }
172        let inserted = self
173            .triples
174            .insert_triples(&mut tx, &triples, "graph-store")
175            .await?;
176        self.accel.refresh_graph(&mut tx, graph).await?;
177        tx.commit().await.map_err(db_err)?;
178        Ok(inserted)
179    }
180
181    pub async fn graph_store_clear(&self, graph: &str) -> Result<usize, DomainError> {
182        let mut tx = self.pool.begin().await.map_err(db_err)?;
183        let deleted = self.triples.clear_graph(&mut tx, Some(graph)).await?;
184        self.triples.delete_graph(&mut tx, graph).await?;
185        self.accel.refresh_graph(&mut tx, graph).await?;
186        tx.commit().await.map_err(db_err)?;
187        Ok(deleted)
188    }
189
190    pub async fn graph_store_read(&self, graph: &str) -> Result<Vec<Triple>, DomainError> {
191        self.triples
192            .triples_for_graph(Some(graph), GRAPH_READ_LIMIT)
193            .await
194    }
195}
196
197/// Synchronous [`TripleSource`] over the async triplestore, mirroring the
198/// Postgres adapter: each call blocks the current async task to completion, so
199/// it is only valid inside `tokio::task::spawn_blocking` (how the SPARQL
200/// evaluator drives it).
201#[derive(Clone)]
202struct SqliteTripleSource {
203    triples: Arc<TripleRepository>,
204    accel: AccelRepository,
205}
206
207impl TripleSource for SqliteTripleSource {
208    fn scan_pattern(
209        &self,
210        subject: Option<&PatternSubject>,
211        predicate: Option<&str>,
212        object: Option<&PatternObject>,
213        graph: Option<&GraphFilter>,
214        limit: i64,
215    ) -> Result<Vec<Triple>, DomainError> {
216        Handle::current().block_on(
217            self.triples
218                .scan_pattern(subject, predicate, object, graph, limit),
219        )
220    }
221
222    fn distinct_named_graphs(&self) -> Result<Vec<String>, DomainError> {
223        Handle::current().block_on(self.triples.distinct_named_graphs())
224    }
225
226    fn triples_for_graph(
227        &self,
228        graph: Option<&str>,
229        limit: i64,
230    ) -> Result<Vec<Triple>, DomainError> {
231        Handle::current().block_on(self.triples.triples_for_graph(graph, limit))
232    }
233
234    fn triples_for_subject(&self, subject_iri: &str) -> Result<Vec<Triple>, DomainError> {
235        Handle::current().block_on(self.triples.triples_for_subject(subject_iri))
236    }
237
238    fn run_accelerated(
239        &self,
240        query: &AcceleratedQuery,
241    ) -> Result<Option<AccelSolutions>, DomainError> {
242        Handle::current().block_on(self.accel.run(query)).map(Some)
243    }
244}
245
246/// Transactional [`TripleWriter`] for SPARQL Update: the whole batch commits or
247/// none of it does.
248#[derive(Clone)]
249struct SqliteTripleWriter {
250    triples: Arc<TripleRepository>,
251    accel: AccelRepository,
252    pool: SqlitePool,
253}
254
255/// Every named graph an update touches (insert, delete, or clear), whose
256/// accelerator indexes must be marked stale. The default (graphless) partition
257/// has no named graph and is never accelerated, so it is skipped.
258fn touched_named_graphs(changes: &[TripleChange]) -> HashSet<String> {
259    let mut graphs = HashSet::new();
260    for change in changes {
261        match change {
262            TripleChange::Change { deletes, inserts } => {
263                for triple in deletes.iter().chain(inserts) {
264                    if let Some(graph) = &triple.graph_iri {
265                        graphs.insert(graph.as_str().to_owned());
266                    }
267                }
268            }
269            TripleChange::Clear(graph) => {
270                if let Some(graph) = graph {
271                    graphs.insert(graph.as_str().to_owned());
272                }
273            }
274        }
275    }
276    graphs
277}
278
279#[async_trait]
280impl TripleWriter for SqliteTripleWriter {
281    async fn apply_update(&self, changes: Vec<TripleChange>) -> Result<UpdateOutcome, DomainError> {
282        let mut outcome = UpdateOutcome::default();
283        let mut tx = self.pool.begin().await.map_err(db_err)?;
284        for change in &changes {
285            match change {
286                TripleChange::Change { deletes, inserts } => {
287                    outcome.deleted += self.triples.delete_triples(&mut tx, deletes).await?;
288                    let mut ensured = HashSet::new();
289                    for triple in inserts {
290                        if let Some(graph) = &triple.graph_iri {
291                            if ensured.insert(graph.as_str().to_owned()) {
292                                self.triples
293                                    .ensure_graph(&mut tx, graph.as_str(), "verbatim")
294                                    .await?;
295                            }
296                        }
297                    }
298                    outcome.inserted += self
299                        .triples
300                        .insert_triples(&mut tx, inserts, UPDATE_SOURCE)
301                        .await?;
302                }
303                TripleChange::Clear(graph) => {
304                    outcome.deleted += self
305                        .triples
306                        .clear_graph(&mut tx, graph.as_ref().map(|i| i.as_str()))
307                        .await?;
308                }
309            }
310        }
311        for graph in touched_named_graphs(&changes) {
312            self.accel.refresh_graph(&mut tx, &graph).await?;
313        }
314        tx.commit().await.map_err(db_err)?;
315        Ok(outcome)
316    }
317}
318
319#[async_trait]
320impl ObjectStore for SqliteStore {
321    async fn get_object_by_iri(&self, iri: &str) -> Result<Option<SbolObjectRecord>, DomainError> {
322        self.objects.get_by_iri(iri).await
323    }
324
325    async fn get_objects_by_iris(
326        &self,
327        iris: &[&str],
328    ) -> Result<Vec<SbolObjectRecord>, DomainError> {
329        self.objects.get_by_iris(iris).await
330    }
331
332    async fn list_objects(
333        &self,
334        filter: &ListObjectsFilter,
335    ) -> Result<Vec<SbolObjectRecord>, DomainError> {
336        self.objects.list(filter).await
337    }
338
339    async fn get_object_iri_by_id(&self, id: ObjectId) -> Result<Option<String>, DomainError> {
340        self.objects.get_iri_by_id(id).await
341    }
342}
343
344#[async_trait]
345impl GraphStore for SqliteStore {
346    async fn get_graph(&self, id: GraphId) -> Result<Option<GraphRecord>, DomainError> {
347        self.graphs.get(id).await
348    }
349
350    async fn list_graphs(
351        &self,
352        filter: &ListGraphsFilter,
353    ) -> Result<Vec<GraphRecord>, DomainError> {
354        self.graphs.list(filter).await
355    }
356
357    async fn delete_graph(&self, id: GraphId) -> Result<bool, DomainError> {
358        self.graphs.delete(id).await
359    }
360
361    async fn graph_exists_by_hash(&self, hash: &[u8]) -> Result<bool, DomainError> {
362        self.graphs.exists_by_hash(hash).await
363    }
364}
365
366#[async_trait]
367impl OntologyStore for SqliteStore {
368    async fn load_ontology_from_url(
369        &self,
370        prefix: &str,
371        name: &str,
372        source_url: &str,
373    ) -> Result<OntologyLoadReport, DomainError> {
374        let client = reqwest::Client::builder()
375            .user_agent("sbol-db/0.1 (+https://github.com/marpaia/sbol-db)")
376            .build()
377            .map_err(|e| DomainError::InvalidInput(format!("reqwest client: {e}")))?;
378        let body = client
379            .get(source_url)
380            .send()
381            .await
382            .map_err(|e| DomainError::InvalidInput(format!("fetch {source_url}: {e}")))?
383            .error_for_status()
384            .map_err(|e| DomainError::InvalidInput(format!("HTTP {source_url}: {e}")))?
385            .text()
386            .await
387            .map_err(|e| DomainError::InvalidInput(format!("decode {source_url}: {e}")))?;
388        self.ontology
389            .load_from_text(prefix, name, Some(source_url), &body)
390            .await
391    }
392
393    async fn load_ontology_from_text(
394        &self,
395        prefix: &str,
396        name: &str,
397        source_url: Option<&str>,
398        text: &str,
399    ) -> Result<OntologyLoadReport, DomainError> {
400        self.ontology
401            .load_from_text(prefix, name, source_url, text)
402            .await
403    }
404
405    async fn list_ontologies(&self) -> Result<Vec<OntologyRecord>, DomainError> {
406        self.ontology.list_ontologies().await
407    }
408
409    async fn canonicalize(&self, iri: &str) -> Result<Option<String>, DomainError> {
410        self.ontology.canonicalize(iri).await
411    }
412
413    async fn descendants(&self, iri: &str) -> Result<Vec<(String, i16)>, DomainError> {
414        self.ontology.descendants(iri).await
415    }
416
417    async fn list_ontology_terms(
418        &self,
419        prefix: &str,
420        limit: i64,
421        offset: i64,
422        search: Option<&str>,
423    ) -> Result<(Vec<OntologyTermRecord>, i64), DomainError> {
424        self.ontology
425            .list_terms(prefix, limit, offset, search)
426            .await
427    }
428
429    async fn get_ontology_term(
430        &self,
431        iri: &str,
432    ) -> Result<Option<OntologyTermRecord>, DomainError> {
433        self.ontology.get_term(iri).await
434    }
435}
436
437#[async_trait]
438impl NeighborhoodStore for SqliteStore {
439    async fn walk(&self, query: &NeighborhoodQuery) -> Result<NeighborhoodResult, DomainError> {
440        crate::repo::neighborhood::walk(&self.triples, &self.objects, query).await
441    }
442}
443
444#[async_trait]
445impl SequenceSearchStore for SqliteStore {
446    async fn search(
447        &self,
448        pattern: &str,
449        options: SequenceSearchOptions,
450    ) -> Result<Vec<SequenceMatch>, DomainError> {
451        self.sequences.search(pattern, options).await
452    }
453
454    async fn search_many(
455        &self,
456        patterns: &[String],
457        options: SequenceSearchOptions,
458    ) -> Result<Vec<BatchSequenceMatch>, DomainError> {
459        self.sequences.search_many(patterns, options).await
460    }
461}
462
463#[async_trait]
464impl LabStore for SqliteStore {
465    async fn corpus_counts(&self) -> Result<CorpusCounts, DomainError> {
466        self.lab.corpus_counts().await
467    }
468
469    async fn recent_graphs(&self, limit: i64) -> Result<Vec<GraphOverview>, DomainError> {
470        self.lab.list_graph_overviews(None, limit, 0).await
471    }
472
473    async fn top_classes(&self, limit: i64) -> Result<Vec<ClassCount>, DomainError> {
474        self.lab.top_classes(limit).await
475    }
476
477    async fn count_graphs(&self, kind: Option<&str>) -> Result<i64, DomainError> {
478        self.lab.count_graphs(kind).await
479    }
480
481    async fn list_graph_overviews(
482        &self,
483        kind: Option<&str>,
484        limit: i64,
485        offset: i64,
486    ) -> Result<Vec<GraphOverview>, DomainError> {
487        self.lab.list_graph_overviews(kind, limit, offset).await
488    }
489
490    async fn get_graph_overview(&self, id: GraphId) -> Result<Option<GraphOverview>, DomainError> {
491        self.lab.get_graph_overview(id).await
492    }
493
494    async fn graph_triples(
495        &self,
496        id: GraphId,
497        limit: i64,
498        offset: i64,
499    ) -> Result<Option<GraphTriplesPage>, DomainError> {
500        self.lab.graph_triples(id, limit, offset).await
501    }
502}
503
504#[async_trait]
505impl SbolStore for SqliteStore {
506    async fn import_document(&self, input: ImportInput) -> Result<ImportReport, DomainError> {
507        self.import_document(input).await
508    }
509
510    async fn import_documents(
511        &self,
512        inputs: Vec<ImportInput>,
513    ) -> Result<Vec<ImportReport>, DomainError> {
514        self.import_documents(inputs).await
515    }
516
517    async fn graph_store_write(
518        &self,
519        graph: &str,
520        body: &str,
521        format: SerializationFormat,
522        mode: GraphWriteMode,
523    ) -> Result<usize, DomainError> {
524        self.graph_store_write(graph, body, format, mode).await
525    }
526
527    async fn graph_store_clear(&self, graph: &str) -> Result<usize, DomainError> {
528        self.graph_store_clear(graph).await
529    }
530
531    async fn graph_store_read(&self, graph: &str) -> Result<Vec<Triple>, DomainError> {
532        self.graph_store_read(graph).await
533    }
534
535    async fn triples_for_subject(&self, subject_iri: &str) -> Result<Vec<Triple>, DomainError> {
536        self.triples.triples_for_subject(subject_iri).await
537    }
538
539    async fn ping(&self) -> Result<(), DomainError> {
540        self.ping().await
541    }
542}