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 insert_triples(
30 &self,
31 conn: &mut sqlx::PgConnection,
32 triples: &[Triple],
33 source: &str,
34 ) -> Result<usize, DomainError> {
35 if triples.is_empty() {
36 return Ok(0);
37 }
38
39 let cols = TripleColumns::from_triples(triples);
40 let inserted = sqlx::query(
41 r#"
42 INSERT INTO sbol_triples (
43 graph_iri,
44 subject_iri,
45 subject_blank,
46 predicate_iri,
47 object_iri,
48 object_blank,
49 object_literal,
50 datatype_iri,
51 language,
52 source
53 )
54 SELECT
55 graph_iri,
56 subject_iri,
57 subject_blank,
58 predicate_iri,
59 object_iri,
60 object_blank,
61 object_literal,
62 datatype_iri,
63 language,
64 $10
65 FROM UNNEST(
66 $1::text[],
67 $2::text[],
68 $3::text[],
69 $4::text[],
70 $5::text[],
71 $6::text[],
72 $7::text[],
73 $8::text[],
74 $9::text[]
75 ) AS u(
76 graph_iri,
77 subject_iri,
78 subject_blank,
79 predicate_iri,
80 object_iri,
81 object_blank,
82 object_literal,
83 datatype_iri,
84 language
85 )
86 ON CONFLICT DO NOTHING
87 "#,
88 )
89 .bind(&cols.graph_iri)
90 .bind(&cols.subject_iri)
91 .bind(&cols.subject_blank)
92 .bind(&cols.predicate_iri)
93 .bind(&cols.object_iri)
94 .bind(&cols.object_blank)
95 .bind(&cols.object_literal)
96 .bind(&cols.datatype_iri)
97 .bind(&cols.language)
98 .bind(source)
99 .execute(&mut *conn)
100 .await
101 .map_err(db_err)?;
102
103 Ok(inserted.rows_affected() as usize)
104 }
105
106 pub async fn delete_triples(
114 &self,
115 conn: &mut sqlx::PgConnection,
116 triples: &[Triple],
117 ) -> Result<usize, DomainError> {
118 if triples.is_empty() {
119 return Ok(0);
120 }
121
122 let cols = TripleColumns::from_triples(triples);
123 let deleted = sqlx::query(
124 r#"
125 DELETE FROM sbol_triples q
126 USING UNNEST(
127 $1::text[],
128 $2::text[],
129 $3::text[],
130 $4::text[],
131 $5::text[],
132 $6::text[],
133 $7::text[],
134 $8::text[],
135 $9::text[]
136 ) AS d(
137 graph_iri,
138 subject_iri,
139 subject_blank,
140 predicate_iri,
141 object_iri,
142 object_blank,
143 object_literal,
144 datatype_iri,
145 language
146 )
147 WHERE q.graph_iri IS NOT DISTINCT FROM d.graph_iri
148 AND q.subject_iri IS NOT DISTINCT FROM d.subject_iri
149 AND q.subject_blank IS NOT DISTINCT FROM d.subject_blank
150 AND q.predicate_iri = d.predicate_iri
151 AND q.object_iri IS NOT DISTINCT FROM d.object_iri
152 AND q.object_blank IS NOT DISTINCT FROM d.object_blank
153 AND q.object_literal IS NOT DISTINCT FROM d.object_literal
154 AND q.datatype_iri IS NOT DISTINCT FROM d.datatype_iri
155 AND q.language IS NOT DISTINCT FROM d.language
156 "#,
157 )
158 .bind(&cols.graph_iri)
159 .bind(&cols.subject_iri)
160 .bind(&cols.subject_blank)
161 .bind(&cols.predicate_iri)
162 .bind(&cols.object_iri)
163 .bind(&cols.object_blank)
164 .bind(&cols.object_literal)
165 .bind(&cols.datatype_iri)
166 .bind(&cols.language)
167 .execute(&mut *conn)
168 .await
169 .map_err(db_err)?;
170
171 Ok(deleted.rows_affected() as usize)
172 }
173
174 pub async fn clear_graph(
178 &self,
179 conn: &mut sqlx::PgConnection,
180 graph: Option<&str>,
181 ) -> Result<usize, DomainError> {
182 let deleted = match graph {
183 Some(g) => sqlx::query("DELETE FROM sbol_triples WHERE graph_iri = $1")
184 .bind(g)
185 .execute(&mut *conn)
186 .await
187 .map_err(db_err)?,
188 None => sqlx::query("DELETE FROM sbol_triples WHERE graph_iri IS NULL")
189 .execute(&mut *conn)
190 .await
191 .map_err(db_err)?,
192 };
193 Ok(deleted.rows_affected() as usize)
194 }
195
196 pub async fn ensure_graph(
203 &self,
204 conn: &mut sqlx::PgConnection,
205 iri: &str,
206 kind: &str,
207 ) -> Result<(), DomainError> {
208 sqlx::query(
209 "INSERT INTO sbol_graphs (iri, kind) VALUES ($1, $2) ON CONFLICT (iri) DO NOTHING",
210 )
211 .bind(iri)
212 .bind(kind)
213 .execute(&mut *conn)
214 .await
215 .map_err(db_err)?;
216 Ok(())
217 }
218
219 pub async fn delete_graph(
225 &self,
226 conn: &mut sqlx::PgConnection,
227 iri: &str,
228 ) -> Result<bool, DomainError> {
229 let affected = sqlx::query("DELETE FROM sbol_graphs WHERE iri = $1")
230 .bind(iri)
231 .execute(&mut *conn)
232 .await
233 .map_err(db_err)?
234 .rows_affected();
235 Ok(affected > 0)
236 }
237
238 pub async fn triples_for_subject(&self, subject_iri: &str) -> Result<Vec<Triple>, DomainError> {
239 let rows = sqlx::query(
240 r#"
241 SELECT graph_iri, subject_iri, subject_blank, predicate_iri,
242 object_iri, object_blank, object_literal, datatype_iri, language
243 FROM sbol_triples
244 WHERE subject_iri = $1
245 "#,
246 )
247 .bind(subject_iri)
248 .fetch_all(&self.pool)
249 .await
250 .map_err(db_err)?;
251
252 rows.into_iter().map(row_to_triple).collect()
253 }
254
255 pub async fn scan_pattern(
265 &self,
266 subject: Option<&PatternSubject>,
267 predicate: Option<&str>,
268 object: Option<&PatternObject>,
269 graph: Option<&GraphFilter>,
270 limit: i64,
271 ) -> Result<Vec<Triple>, DomainError> {
272 let mut qb: QueryBuilder<sqlx::Postgres> = QueryBuilder::new(
273 "SELECT graph_iri, subject_iri, subject_blank, predicate_iri, \
274 object_iri, object_blank, object_literal, datatype_iri, language \
275 FROM sbol_triples WHERE 1=1",
276 );
277 match subject {
278 Some(PatternSubject::Iri(s)) => {
279 qb.push(" AND subject_iri = ").push_bind(s.clone());
280 }
281 Some(PatternSubject::Blank(b)) => {
282 qb.push(" AND subject_blank = ").push_bind(b.clone());
283 }
284 None => {}
285 }
286 if let Some(p) = predicate {
287 qb.push(" AND predicate_iri = ").push_bind(p.to_owned());
288 }
289 match object {
290 Some(PatternObject::Iri(o)) => {
291 qb.push(" AND object_iri = ").push_bind(o.clone());
292 }
293 Some(PatternObject::Blank(b)) => {
294 qb.push(" AND object_blank = ").push_bind(b.clone());
295 }
296 Some(PatternObject::Literal {
297 value,
298 datatype,
299 language,
300 }) => {
301 qb.push(" AND object_literal = ").push_bind(value.clone());
302 qb.push(" AND datatype_iri = ").push_bind(datatype.clone());
303 match language {
304 Some(lang) => {
305 qb.push(" AND language = ").push_bind(lang.clone());
306 }
307 None => {
308 qb.push(" AND language IS NULL");
309 }
310 }
311 }
312 None => {}
313 }
314 match graph {
315 Some(GraphFilter::AnyNamed) => {
316 qb.push(" AND graph_iri IS NOT NULL");
317 }
318 Some(GraphFilter::DefaultOnly) => {
319 qb.push(" AND graph_iri IS NULL");
320 }
321 Some(GraphFilter::Iri(g)) => {
322 qb.push(" AND graph_iri = ").push_bind(g.clone());
323 }
324 None => {}
325 }
326 qb.push(" LIMIT ").push_bind(limit);
327
328 let rows = qb.build().fetch_all(&self.pool).await.map_err(db_err)?;
329 rows.into_iter().map(row_to_triple).collect()
330 }
331
332 pub async fn scan_graph_in_conn(
337 &self,
338 conn: &mut sqlx::PgConnection,
339 graph: &str,
340 ) -> Result<Vec<Triple>, DomainError> {
341 let rows = sqlx::query(
342 r#"
343 SELECT graph_iri, subject_iri, subject_blank, predicate_iri,
344 object_iri, object_blank, object_literal, datatype_iri, language
345 FROM sbol_triples
346 WHERE graph_iri = $1
347 "#,
348 )
349 .bind(graph)
350 .fetch_all(&mut *conn)
351 .await
352 .map_err(db_err)?;
353 rows.into_iter().map(row_to_triple).collect()
354 }
355
356 pub async fn distinct_named_graphs(&self) -> Result<Vec<String>, DomainError> {
359 let rows = sqlx::query(
360 "SELECT DISTINCT graph_iri::text AS graph_iri \
361 FROM sbol_triples WHERE graph_iri IS NOT NULL",
362 )
363 .fetch_all(&self.pool)
364 .await
365 .map_err(db_err)?;
366 rows.into_iter()
367 .map(|r| {
368 let g: String = r.try_get("graph_iri").map_err(db_err)?;
369 Ok(g)
370 })
371 .collect()
372 }
373
374 pub async fn triples_for_graph(
378 &self,
379 graph: Option<&str>,
380 limit: i64,
381 ) -> Result<Vec<Triple>, DomainError> {
382 let rows = match graph {
383 Some(g) => sqlx::query(
384 r#"
385 SELECT graph_iri, subject_iri, subject_blank, predicate_iri,
386 object_iri, object_blank, object_literal, datatype_iri, language
387 FROM sbol_triples
388 WHERE graph_iri = $1
389 LIMIT $2
390 "#,
391 )
392 .bind(g)
393 .bind(limit)
394 .fetch_all(&self.pool)
395 .await
396 .map_err(db_err)?,
397 None => sqlx::query(
398 r#"
399 SELECT graph_iri, subject_iri, subject_blank, predicate_iri,
400 object_iri, object_blank, object_literal, datatype_iri, language
401 FROM sbol_triples
402 WHERE graph_iri IS NULL
403 LIMIT $1
404 "#,
405 )
406 .bind(limit)
407 .fetch_all(&self.pool)
408 .await
409 .map_err(db_err)?,
410 };
411 rows.into_iter().map(row_to_triple).collect()
412 }
413}
414
415struct TripleColumns {
421 graph_iri: Vec<Option<String>>,
422 subject_iri: Vec<Option<String>>,
423 subject_blank: Vec<Option<String>>,
424 predicate_iri: Vec<String>,
425 object_iri: Vec<Option<String>>,
426 object_blank: Vec<Option<String>>,
427 object_literal: Vec<Option<String>>,
428 datatype_iri: Vec<Option<String>>,
429 language: Vec<Option<String>>,
430}
431
432impl TripleColumns {
433 fn from_triples(triples: &[Triple]) -> Self {
434 let mut cols = TripleColumns {
435 graph_iri: Vec::with_capacity(triples.len()),
436 subject_iri: Vec::with_capacity(triples.len()),
437 subject_blank: Vec::with_capacity(triples.len()),
438 predicate_iri: Vec::with_capacity(triples.len()),
439 object_iri: Vec::with_capacity(triples.len()),
440 object_blank: Vec::with_capacity(triples.len()),
441 object_literal: Vec::with_capacity(triples.len()),
442 datatype_iri: Vec::with_capacity(triples.len()),
443 language: Vec::with_capacity(triples.len()),
444 };
445 for q in triples {
446 cols.graph_iri
447 .push(q.graph_iri.as_ref().map(|i| i.as_str().to_owned()));
448 match &q.subject {
449 SubjectTerm::Iri(iri) => {
450 cols.subject_iri.push(Some(iri.as_str().to_owned()));
451 cols.subject_blank.push(None);
452 }
453 SubjectTerm::BlankNode(node) => {
454 cols.subject_iri.push(None);
455 cols.subject_blank.push(Some(node.clone()));
456 }
457 }
458 cols.predicate_iri.push(q.predicate.as_str().to_owned());
459 match &q.object {
460 ObjectTerm::Iri(iri) => {
461 cols.object_iri.push(Some(iri.as_str().to_owned()));
462 cols.object_blank.push(None);
463 cols.object_literal.push(None);
464 cols.datatype_iri.push(None);
465 cols.language.push(None);
466 }
467 ObjectTerm::BlankNode(node) => {
468 cols.object_iri.push(None);
469 cols.object_blank.push(Some(node.clone()));
470 cols.object_literal.push(None);
471 cols.datatype_iri.push(None);
472 cols.language.push(None);
473 }
474 ObjectTerm::Literal {
475 value,
476 datatype,
477 language: lang,
478 } => {
479 cols.object_iri.push(None);
480 cols.object_blank.push(None);
481 cols.object_literal.push(Some(value.clone()));
482 cols.datatype_iri.push(Some(datatype.as_str().to_owned()));
483 cols.language.push(lang.clone());
484 }
485 }
486 }
487 cols
488 }
489}
490
491fn row_to_triple(row: sqlx::postgres::PgRow) -> Result<Triple, DomainError> {
492 use sbol_db_core::IriString;
493 let graph_iri: Option<String> = row.try_get("graph_iri").map_err(db_err)?;
494 let subject_iri: Option<String> = row.try_get("subject_iri").map_err(db_err)?;
495 let subject_blank: Option<String> = row.try_get("subject_blank").map_err(db_err)?;
496 let predicate_iri: String = row.try_get("predicate_iri").map_err(db_err)?;
497 let object_iri: Option<String> = row.try_get("object_iri").map_err(db_err)?;
498 let object_blank: Option<String> = row.try_get("object_blank").map_err(db_err)?;
499 let object_literal: Option<String> = row.try_get("object_literal").map_err(db_err)?;
500 let datatype_iri: Option<String> = row.try_get("datatype_iri").map_err(db_err)?;
501 let language: Option<String> = row.try_get("language").map_err(db_err)?;
502
503 let subject = match (subject_iri, subject_blank) {
504 (Some(iri), _) => SubjectTerm::Iri(IriString::unchecked(iri)),
505 (None, Some(blank)) => SubjectTerm::BlankNode(blank),
506 (None, None) => return Err(DomainError::Database("triple missing subject".into())),
507 };
508
509 let object = match (object_iri, object_blank, object_literal) {
510 (Some(iri), _, _) => ObjectTerm::Iri(IriString::unchecked(iri)),
511 (None, Some(blank), _) => ObjectTerm::BlankNode(blank),
512 (None, None, Some(value)) => ObjectTerm::Literal {
513 value,
514 datatype: IriString::unchecked(
515 datatype_iri
516 .unwrap_or_else(|| "http://www.w3.org/2001/XMLSchema#string".to_owned()),
517 ),
518 language,
519 },
520 (None, None, None) => return Err(DomainError::Database("triple missing object".into())),
521 };
522
523 Ok(Triple {
524 graph_iri: graph_iri.map(IriString::unchecked),
525 subject,
526 predicate: IriString::unchecked(predicate_iri),
527 object,
528 })
529}