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