1use sbol_db_core::{DomainError, GraphId, GraphRecord, IriString, NewGraph, SerializationFormat};
9use sbol_db_rdf::GRAPH_IRI_PREFIX;
10use sqlx::Row;
11
12use crate::repo::db_err;
13use crate::PgPool;
14
15use sbol_db_storage::ListGraphsFilter;
16
17#[derive(Clone)]
18pub struct GraphRepository {
19 pool: PgPool,
20}
21
22impl GraphRepository {
23 pub fn new(pool: PgPool) -> Self {
24 Self { pool }
25 }
26
27 pub async fn insert(
31 &self,
32 conn: &mut sqlx::PgConnection,
33 id: GraphId,
34 input: NewGraph,
35 ) -> Result<GraphId, DomainError> {
36 let id = id.0;
37 let iri = format!("{GRAPH_IRI_PREFIX}{id}");
38 sqlx::query(
39 r#"
40 INSERT INTO sbol_graphs (
41 iri, id, kind, document_iri, name, description,
42 serialization_format, source_uri, content_hash, created_by
43 ) VALUES ($1, $2, 'sbol3', $3, $4, $5, $6, $7, $8, $9)
44 "#,
45 )
46 .bind(&iri)
47 .bind(id)
48 .bind(input.document_iri.as_ref().map(|i| i.as_str()))
49 .bind(input.name)
50 .bind(input.description)
51 .bind(input.serialization_format.as_db_str())
52 .bind(input.source_uri)
53 .bind(input.content_hash)
54 .bind(input.created_by)
55 .execute(&mut *conn)
56 .await
57 .map_err(db_err)?;
58
59 Ok(GraphId(id))
60 }
61
62 pub async fn exists_by_hash(&self, hash: &[u8]) -> Result<bool, DomainError> {
65 let row = sqlx::query(
66 "SELECT EXISTS(SELECT 1 FROM sbol_graphs \
67 WHERE kind = 'sbol3' AND content_hash = $1) AS hit",
68 )
69 .bind(hash)
70 .fetch_one(&self.pool)
71 .await
72 .map_err(db_err)?;
73 row.try_get::<bool, _>("hit").map_err(db_err)
74 }
75
76 pub async fn get(&self, id: GraphId) -> Result<Option<GraphRecord>, DomainError> {
77 let row = sqlx::query(
78 r#"
79 SELECT id, document_iri, name, description, serialization_format,
80 source_uri, content_hash, created_at, updated_at
81 FROM sbol_graphs
82 WHERE id = $1 AND kind = 'sbol3'
83 "#,
84 )
85 .bind(id.0)
86 .fetch_optional(&self.pool)
87 .await
88 .map_err(db_err)?;
89
90 row.map(row_to_record).transpose()
91 }
92
93 pub async fn list(&self, filter: &ListGraphsFilter) -> Result<Vec<GraphRecord>, DomainError> {
97 let format_value = filter.format.map(|f| f.as_db_str().to_owned());
98 let rows = sqlx::query(
99 r#"
100 SELECT id, document_iri, name, description, serialization_format,
101 source_uri, content_hash, created_at, updated_at
102 FROM sbol_graphs
103 WHERE kind = 'sbol3'
104 AND ($1::text IS NULL OR name ILIKE '%' || $1 || '%')
105 AND ($2::text IS NULL OR serialization_format = $2)
106 ORDER BY created_at DESC
107 LIMIT $3
108 "#,
109 )
110 .bind(filter.name.as_deref())
111 .bind(format_value)
112 .bind(filter.limit as i64)
113 .fetch_all(&self.pool)
114 .await
115 .map_err(db_err)?;
116 rows.into_iter().map(row_to_record).collect()
117 }
118
119 pub async fn id_by_document_iri(
121 &self,
122 document_iri: &str,
123 ) -> Result<Option<GraphId>, DomainError> {
124 let row =
125 sqlx::query("SELECT id FROM sbol_graphs WHERE document_iri = $1 AND kind = 'sbol3'")
126 .bind(document_iri)
127 .fetch_optional(&self.pool)
128 .await
129 .map_err(db_err)?;
130 match row {
131 Some(row) => Ok(Some(GraphId(row.try_get("id").map_err(db_err)?))),
132 None => Ok(None),
133 }
134 }
135
136 pub async fn delete(&self, id: GraphId) -> Result<bool, DomainError> {
142 let mut tx = self.pool.begin().await.map_err(db_err)?;
143 let removed = delete_graph_on(&mut tx, id).await?;
144 tx.commit().await.map_err(db_err)?;
145 Ok(removed)
146 }
147
148 pub async fn id_by_document_iri_in(
152 &self,
153 conn: &mut sqlx::PgConnection,
154 document_iri: &str,
155 ) -> Result<Option<GraphId>, DomainError> {
156 let row =
157 sqlx::query("SELECT id FROM sbol_graphs WHERE document_iri = $1 AND kind = 'sbol3'")
158 .bind(document_iri)
159 .fetch_optional(&mut *conn)
160 .await
161 .map_err(db_err)?;
162 match row {
163 Some(row) => Ok(Some(GraphId(row.try_get("id").map_err(db_err)?))),
164 None => Ok(None),
165 }
166 }
167
168 pub async fn delete_in(
170 &self,
171 conn: &mut sqlx::PgConnection,
172 id: GraphId,
173 ) -> Result<bool, DomainError> {
174 delete_graph_on(conn, id).await
175 }
176}
177
178async fn delete_graph_on(conn: &mut sqlx::PgConnection, id: GraphId) -> Result<bool, DomainError> {
183 sqlx::query("DELETE FROM sbol_objects WHERE graph_id = $1")
184 .bind(id.0)
185 .execute(&mut *conn)
186 .await
187 .map_err(db_err)?;
188 let affected = sqlx::query("DELETE FROM sbol_graphs WHERE id = $1 AND kind = 'sbol3'")
189 .bind(id.0)
190 .execute(&mut *conn)
191 .await
192 .map_err(db_err)?
193 .rows_affected();
194 Ok(affected > 0)
195}
196
197fn row_to_record(row: sqlx::postgres::PgRow) -> Result<GraphRecord, DomainError> {
198 let id: uuid::Uuid = row.try_get("id").map_err(db_err)?;
199 let document_iri: Option<String> = row.try_get("document_iri").map_err(db_err)?;
200 let name: Option<String> = row.try_get("name").map_err(db_err)?;
201 let description: Option<String> = row.try_get("description").map_err(db_err)?;
202 let serialization_format: Option<String> =
203 row.try_get("serialization_format").map_err(db_err)?;
204 let source_uri: Option<String> = row.try_get("source_uri").map_err(db_err)?;
205 let content_hash: Option<Vec<u8>> = row.try_get("content_hash").map_err(db_err)?;
206 let created_at: chrono::DateTime<chrono::Utc> = row.try_get("created_at").map_err(db_err)?;
207 let updated_at: chrono::DateTime<chrono::Utc> = row.try_get("updated_at").map_err(db_err)?;
208 let serialization_format = serialization_format
209 .as_deref()
210 .map(parse_serialization_format)
211 .transpose()?
212 .unwrap_or(SerializationFormat::Json);
213 Ok(GraphRecord {
214 id: GraphId(id),
215 document_iri: document_iri.map(IriString::unchecked),
216 name,
217 description,
218 serialization_format,
219 source_uri,
220 content_hash: content_hash.unwrap_or_default(),
221 created_at,
222 updated_at,
223 })
224}
225
226fn parse_serialization_format(value: &str) -> Result<SerializationFormat, DomainError> {
227 Ok(match value {
228 "json" => SerializationFormat::Json,
229 "jsonld" => SerializationFormat::JsonLd,
230 "rdfxml" => SerializationFormat::RdfXml,
231 "turtle" => SerializationFormat::Turtle,
232 "trig" => SerializationFormat::TriG,
233 "ntriples" => SerializationFormat::NTriples,
234 "nquads" => SerializationFormat::NQuads,
235 "genbank" => SerializationFormat::GenBank,
236 "fasta" => SerializationFormat::Fasta,
237 other => {
238 return Err(DomainError::Database(format!(
239 "unknown serialization_format: {other}"
240 )))
241 }
242 })
243}