1use sbol_db_core::{DomainError, ObjectTerm, SubjectTerm, Triple};
2use sqlx::{QueryBuilder, Row};
3
4use crate::repo::db_err;
5use crate::PgPool;
6
7use sbol_db_storage::{GraphFilter, PatternObject, PatternSubject};
8
9#[derive(Clone)]
10pub struct TripleRepository {
11 pool: PgPool,
12}
13
14impl TripleRepository {
15 pub fn new(pool: PgPool) -> Self {
16 Self { pool }
17 }
18
19 pub async fn scan_all_page(
23 &self,
24 after_id: i64,
25 limit: i64,
26 ) -> Result<Vec<(i64, Triple)>, DomainError> {
27 let rows = sqlx::query(
28 r#"
29 SELECT id, graph_iri, subject_iri, subject_blank, predicate_iri,
30 object_iri, object_blank, object_literal, datatype_iri, language
31 FROM sbol_triples
32 WHERE id > $1
33 ORDER BY id
34 LIMIT $2
35 "#,
36 )
37 .bind(after_id)
38 .bind(limit)
39 .fetch_all(&self.pool)
40 .await
41 .map_err(db_err)?;
42 rows.into_iter()
43 .map(|row| {
44 let id: i64 = row.try_get("id").map_err(db_err)?;
45 Ok((id, row_to_triple(row)?))
46 })
47 .collect()
48 }
49
50 pub async fn scan_graph_page(
52 &self,
53 graph: &str,
54 after_id: i64,
55 limit: i64,
56 ) -> Result<Vec<(i64, Triple)>, DomainError> {
57 let rows = sqlx::query(
58 r#"
59 SELECT id, graph_iri, subject_iri, subject_blank, predicate_iri,
60 object_iri, object_blank, object_literal, datatype_iri, language
61 FROM sbol_triples
62 WHERE graph_iri = $1 AND id > $2
63 ORDER BY id
64 LIMIT $3
65 "#,
66 )
67 .bind(graph)
68 .bind(after_id)
69 .bind(limit)
70 .fetch_all(&self.pool)
71 .await
72 .map_err(db_err)?;
73 rows.into_iter()
74 .map(|row| {
75 let id: i64 = row.try_get("id").map_err(db_err)?;
76 Ok((id, row_to_triple(row)?))
77 })
78 .collect()
79 }
80
81 pub async fn insert_triples(
92 &self,
93 conn: &mut sqlx::PgConnection,
94 triples: &[Triple],
95 source: &str,
96 ) -> Result<usize, DomainError> {
97 if triples.is_empty() {
98 return Ok(0);
99 }
100
101 let cols = TripleColumns::from_triples(triples);
102 let inserted = sqlx::query(
103 r#"
104 INSERT INTO sbol_triples (
105 graph_iri,
106 subject_iri,
107 subject_blank,
108 predicate_iri,
109 object_iri,
110 object_blank,
111 object_literal,
112 datatype_iri,
113 language,
114 source
115 )
116 SELECT
117 graph_iri,
118 subject_iri,
119 subject_blank,
120 predicate_iri,
121 object_iri,
122 object_blank,
123 object_literal,
124 datatype_iri,
125 language,
126 $10
127 FROM UNNEST(
128 $1::text[],
129 $2::text[],
130 $3::text[],
131 $4::text[],
132 $5::text[],
133 $6::text[],
134 $7::text[],
135 $8::text[],
136 $9::text[]
137 ) AS u(
138 graph_iri,
139 subject_iri,
140 subject_blank,
141 predicate_iri,
142 object_iri,
143 object_blank,
144 object_literal,
145 datatype_iri,
146 language
147 )
148 ON CONFLICT DO NOTHING
149 "#,
150 )
151 .bind(&cols.graph_iri)
152 .bind(&cols.subject_iri)
153 .bind(&cols.subject_blank)
154 .bind(&cols.predicate_iri)
155 .bind(&cols.object_iri)
156 .bind(&cols.object_blank)
157 .bind(&cols.object_literal)
158 .bind(&cols.datatype_iri)
159 .bind(&cols.language)
160 .bind(source)
161 .execute(&mut *conn)
162 .await
163 .map_err(db_err)?;
164
165 Ok(inserted.rows_affected() as usize)
166 }
167
168 pub async fn delete_triples(
176 &self,
177 conn: &mut sqlx::PgConnection,
178 triples: &[Triple],
179 ) -> Result<usize, DomainError> {
180 if triples.is_empty() {
181 return Ok(0);
182 }
183
184 let cols = TripleColumns::from_triples(triples);
185 let deleted = sqlx::query(
186 r#"
187 DELETE FROM sbol_triples q
188 USING UNNEST(
189 $1::text[],
190 $2::text[],
191 $3::text[],
192 $4::text[],
193 $5::text[],
194 $6::text[],
195 $7::text[],
196 $8::text[],
197 $9::text[]
198 ) AS d(
199 graph_iri,
200 subject_iri,
201 subject_blank,
202 predicate_iri,
203 object_iri,
204 object_blank,
205 object_literal,
206 datatype_iri,
207 language
208 )
209 WHERE q.graph_iri IS NOT DISTINCT FROM d.graph_iri
210 AND q.subject_iri IS NOT DISTINCT FROM d.subject_iri
211 AND q.subject_blank IS NOT DISTINCT FROM d.subject_blank
212 AND q.predicate_iri = d.predicate_iri
213 AND q.object_iri IS NOT DISTINCT FROM d.object_iri
214 AND q.object_blank IS NOT DISTINCT FROM d.object_blank
215 AND q.object_literal IS NOT DISTINCT FROM d.object_literal
216 AND q.datatype_iri IS NOT DISTINCT FROM d.datatype_iri
217 AND q.language IS NOT DISTINCT FROM d.language
218 "#,
219 )
220 .bind(&cols.graph_iri)
221 .bind(&cols.subject_iri)
222 .bind(&cols.subject_blank)
223 .bind(&cols.predicate_iri)
224 .bind(&cols.object_iri)
225 .bind(&cols.object_blank)
226 .bind(&cols.object_literal)
227 .bind(&cols.datatype_iri)
228 .bind(&cols.language)
229 .execute(&mut *conn)
230 .await
231 .map_err(db_err)?;
232
233 Ok(deleted.rows_affected() as usize)
234 }
235
236 pub async fn clear_graph(
240 &self,
241 conn: &mut sqlx::PgConnection,
242 graph: Option<&str>,
243 ) -> Result<usize, DomainError> {
244 let deleted = match graph {
245 Some(g) => sqlx::query("DELETE FROM sbol_triples WHERE graph_iri = $1")
246 .bind(g)
247 .execute(&mut *conn)
248 .await
249 .map_err(db_err)?,
250 None => sqlx::query("DELETE FROM sbol_triples WHERE graph_iri IS NULL")
251 .execute(&mut *conn)
252 .await
253 .map_err(db_err)?,
254 };
255 Ok(deleted.rows_affected() as usize)
256 }
257
258 pub async fn ensure_graph(
265 &self,
266 conn: &mut sqlx::PgConnection,
267 iri: &str,
268 kind: &str,
269 ) -> Result<(), DomainError> {
270 sqlx::query(
271 "INSERT INTO sbol_graphs (iri, kind) VALUES ($1, $2) ON CONFLICT (iri) DO NOTHING",
272 )
273 .bind(iri)
274 .bind(kind)
275 .execute(&mut *conn)
276 .await
277 .map_err(db_err)?;
278 Ok(())
279 }
280
281 pub async fn delete_graph(
287 &self,
288 conn: &mut sqlx::PgConnection,
289 iri: &str,
290 ) -> Result<bool, DomainError> {
291 let affected = sqlx::query("DELETE FROM sbol_graphs WHERE iri = $1")
292 .bind(iri)
293 .execute(&mut *conn)
294 .await
295 .map_err(db_err)?
296 .rows_affected();
297 Ok(affected > 0)
298 }
299
300 pub async fn triples_for_subject(&self, subject_iri: &str) -> Result<Vec<Triple>, DomainError> {
301 let rows = sqlx::query(
302 r#"
303 SELECT graph_iri, subject_iri, subject_blank, predicate_iri,
304 object_iri, object_blank, object_literal, datatype_iri, language
305 FROM sbol_triples
306 WHERE subject_iri = $1
307 "#,
308 )
309 .bind(subject_iri)
310 .fetch_all(&self.pool)
311 .await
312 .map_err(db_err)?;
313
314 rows.into_iter().map(row_to_triple).collect()
315 }
316
317 pub async fn scan_pattern(
327 &self,
328 subject: Option<&PatternSubject>,
329 predicate: Option<&str>,
330 object: Option<&PatternObject>,
331 graph: Option<&GraphFilter>,
332 limit: i64,
333 ) -> Result<Vec<Triple>, DomainError> {
334 let mut qb: QueryBuilder<sqlx::Postgres> = QueryBuilder::new(
335 "SELECT graph_iri, subject_iri, subject_blank, predicate_iri, \
336 object_iri, object_blank, object_literal, datatype_iri, language \
337 FROM sbol_triples WHERE 1=1",
338 );
339 match subject {
340 Some(PatternSubject::Iri(s)) => {
341 qb.push(" AND subject_iri = ").push_bind(s.clone());
342 }
343 Some(PatternSubject::Blank(b)) => {
344 qb.push(" AND subject_blank = ").push_bind(b.clone());
345 }
346 None => {}
347 }
348 if let Some(p) = predicate {
349 qb.push(" AND predicate_iri = ").push_bind(p.to_owned());
350 }
351 match object {
352 Some(PatternObject::Iri(o)) => {
353 qb.push(" AND object_iri = ").push_bind(o.clone());
354 }
355 Some(PatternObject::Blank(b)) => {
356 qb.push(" AND object_blank = ").push_bind(b.clone());
357 }
358 Some(PatternObject::Literal {
359 value,
360 datatype,
361 language,
362 }) => {
363 qb.push(" AND object_literal = ").push_bind(value.clone());
364 qb.push(" AND datatype_iri = ").push_bind(datatype.clone());
365 match language {
366 Some(lang) => {
367 qb.push(" AND language = ").push_bind(lang.clone());
368 }
369 None => {
370 qb.push(" AND language IS NULL");
371 }
372 }
373 }
374 None => {}
375 }
376 match graph {
377 Some(GraphFilter::AnyNamed) => {
378 qb.push(" AND graph_iri IS NOT NULL");
379 }
380 Some(GraphFilter::DefaultOnly) => {
381 qb.push(" AND graph_iri IS NULL");
382 }
383 Some(GraphFilter::Iri(g)) => {
384 qb.push(" AND graph_iri = ").push_bind(g.clone());
385 }
386 None => {}
387 }
388 qb.push(" LIMIT ").push_bind(limit);
389
390 let rows = qb.build().fetch_all(&self.pool).await.map_err(db_err)?;
391 rows.into_iter().map(row_to_triple).collect()
392 }
393
394 pub async fn scan_graph_in_conn(
399 &self,
400 conn: &mut sqlx::PgConnection,
401 graph: &str,
402 ) -> Result<Vec<Triple>, DomainError> {
403 let rows = sqlx::query(
404 r#"
405 SELECT graph_iri, subject_iri, subject_blank, predicate_iri,
406 object_iri, object_blank, object_literal, datatype_iri, language
407 FROM sbol_triples
408 WHERE graph_iri = $1
409 "#,
410 )
411 .bind(graph)
412 .fetch_all(&mut *conn)
413 .await
414 .map_err(db_err)?;
415 rows.into_iter().map(row_to_triple).collect()
416 }
417
418 pub async fn distinct_named_graphs(&self) -> Result<Vec<String>, DomainError> {
421 let rows = sqlx::query(
422 "SELECT DISTINCT graph_iri::text AS graph_iri \
423 FROM sbol_triples WHERE graph_iri IS NOT NULL",
424 )
425 .fetch_all(&self.pool)
426 .await
427 .map_err(db_err)?;
428 rows.into_iter()
429 .map(|r| {
430 let g: String = r.try_get("graph_iri").map_err(db_err)?;
431 Ok(g)
432 })
433 .collect()
434 }
435
436 pub async fn triples_for_graph(
440 &self,
441 graph: Option<&str>,
442 limit: i64,
443 ) -> Result<Vec<Triple>, DomainError> {
444 let rows = match graph {
445 Some(g) => sqlx::query(
446 r#"
447 SELECT graph_iri, subject_iri, subject_blank, predicate_iri,
448 object_iri, object_blank, object_literal, datatype_iri, language
449 FROM sbol_triples
450 WHERE graph_iri = $1
451 LIMIT $2
452 "#,
453 )
454 .bind(g)
455 .bind(limit)
456 .fetch_all(&self.pool)
457 .await
458 .map_err(db_err)?,
459 None => sqlx::query(
460 r#"
461 SELECT graph_iri, subject_iri, subject_blank, predicate_iri,
462 object_iri, object_blank, object_literal, datatype_iri, language
463 FROM sbol_triples
464 WHERE graph_iri IS NULL
465 LIMIT $1
466 "#,
467 )
468 .bind(limit)
469 .fetch_all(&self.pool)
470 .await
471 .map_err(db_err)?,
472 };
473 rows.into_iter().map(row_to_triple).collect()
474 }
475}
476
477struct TripleColumns {
483 graph_iri: Vec<Option<String>>,
484 subject_iri: Vec<Option<String>>,
485 subject_blank: Vec<Option<String>>,
486 predicate_iri: Vec<String>,
487 object_iri: Vec<Option<String>>,
488 object_blank: Vec<Option<String>>,
489 object_literal: Vec<Option<String>>,
490 datatype_iri: Vec<Option<String>>,
491 language: Vec<Option<String>>,
492}
493
494impl TripleColumns {
495 fn from_triples(triples: &[Triple]) -> Self {
496 let mut cols = TripleColumns {
497 graph_iri: Vec::with_capacity(triples.len()),
498 subject_iri: Vec::with_capacity(triples.len()),
499 subject_blank: Vec::with_capacity(triples.len()),
500 predicate_iri: Vec::with_capacity(triples.len()),
501 object_iri: Vec::with_capacity(triples.len()),
502 object_blank: Vec::with_capacity(triples.len()),
503 object_literal: Vec::with_capacity(triples.len()),
504 datatype_iri: Vec::with_capacity(triples.len()),
505 language: Vec::with_capacity(triples.len()),
506 };
507 for q in triples {
508 cols.graph_iri
509 .push(q.graph_iri.as_ref().map(|i| i.as_str().to_owned()));
510 match &q.subject {
511 SubjectTerm::Iri(iri) => {
512 cols.subject_iri.push(Some(iri.as_str().to_owned()));
513 cols.subject_blank.push(None);
514 }
515 SubjectTerm::BlankNode(node) => {
516 cols.subject_iri.push(None);
517 cols.subject_blank.push(Some(node.clone()));
518 }
519 }
520 cols.predicate_iri.push(q.predicate.as_str().to_owned());
521 match &q.object {
522 ObjectTerm::Iri(iri) => {
523 cols.object_iri.push(Some(iri.as_str().to_owned()));
524 cols.object_blank.push(None);
525 cols.object_literal.push(None);
526 cols.datatype_iri.push(None);
527 cols.language.push(None);
528 }
529 ObjectTerm::BlankNode(node) => {
530 cols.object_iri.push(None);
531 cols.object_blank.push(Some(node.clone()));
532 cols.object_literal.push(None);
533 cols.datatype_iri.push(None);
534 cols.language.push(None);
535 }
536 ObjectTerm::Literal {
537 value,
538 datatype,
539 language: lang,
540 } => {
541 cols.object_iri.push(None);
542 cols.object_blank.push(None);
543 cols.object_literal.push(Some(value.clone()));
544 cols.datatype_iri.push(Some(datatype.as_str().to_owned()));
545 cols.language.push(lang.clone());
546 }
547 }
548 }
549 cols
550 }
551}
552
553fn row_to_triple(row: sqlx::postgres::PgRow) -> Result<Triple, DomainError> {
554 use sbol_db_core::IriString;
555 let graph_iri: Option<String> = row.try_get("graph_iri").map_err(db_err)?;
556 let subject_iri: Option<String> = row.try_get("subject_iri").map_err(db_err)?;
557 let subject_blank: Option<String> = row.try_get("subject_blank").map_err(db_err)?;
558 let predicate_iri: String = row.try_get("predicate_iri").map_err(db_err)?;
559 let object_iri: Option<String> = row.try_get("object_iri").map_err(db_err)?;
560 let object_blank: Option<String> = row.try_get("object_blank").map_err(db_err)?;
561 let object_literal: Option<String> = row.try_get("object_literal").map_err(db_err)?;
562 let datatype_iri: Option<String> = row.try_get("datatype_iri").map_err(db_err)?;
563 let language: Option<String> = row.try_get("language").map_err(db_err)?;
564
565 let subject = match (subject_iri, subject_blank) {
566 (Some(iri), _) => SubjectTerm::Iri(IriString::unchecked(iri)),
567 (None, Some(blank)) => SubjectTerm::BlankNode(blank),
568 (None, None) => return Err(DomainError::Database("triple missing subject".into())),
569 };
570
571 let object = match (object_iri, object_blank, object_literal) {
572 (Some(iri), _, _) => ObjectTerm::Iri(IriString::unchecked(iri)),
573 (None, Some(blank), _) => ObjectTerm::BlankNode(blank),
574 (None, None, Some(value)) => ObjectTerm::Literal {
575 value,
576 datatype: IriString::unchecked(
577 datatype_iri
578 .unwrap_or_else(|| "http://www.w3.org/2001/XMLSchema#string".to_owned()),
579 ),
580 language,
581 },
582 (None, None, None) => return Err(DomainError::Database("triple missing object".into())),
583 };
584
585 Ok(Triple {
586 graph_iri: graph_iri.map(IriString::unchecked),
587 subject,
588 predicate: IriString::unchecked(predicate_iri),
589 object,
590 })
591}