Skip to main content

sbol_db_postgres/repo/
projections.rs

1//! Persistence for the typed SBOL projections. Each table
2//! shares the IRI-keyed upsert shape. Rows are inserted *after* the
3//! corresponding `sbol_objects` row in the same transaction; the
4//! `object_id` is looked up via the IRI to keep these methods independent
5//! of the upsert order.
6
7use sbol_db_core::{
8    ComponentProjection, ConstraintProjection, DomainError, FeatureProjection,
9    InteractionProjection, IriString, LocationProjection, ParticipationProjection,
10    SequenceProjection, TypedProjections,
11};
12
13use crate::repo::db_err;
14use crate::PgPool;
15
16#[derive(Clone)]
17pub struct TypedProjectionRepository {
18    _pool: PgPool,
19}
20
21impl TypedProjectionRepository {
22    pub fn new(pool: PgPool) -> Self {
23        Self { _pool: pool }
24    }
25
26    /// Upsert every typed projection produced for a single document. Run
27    /// inside the import transaction.
28    pub async fn upsert_all(
29        &self,
30        conn: &mut sqlx::PgConnection,
31        projections: &TypedProjections,
32    ) -> Result<TypedProjectionCounts, DomainError> {
33        let mut counts = TypedProjectionCounts::default();
34        for c in &projections.components {
35            upsert_component(conn, c).await?;
36            counts.components += 1;
37        }
38        for s in &projections.sequences {
39            upsert_sequence(conn, s).await?;
40            counts.sequences += 1;
41        }
42        for f in &projections.features {
43            upsert_feature(conn, f).await?;
44            counts.features += 1;
45        }
46        for l in &projections.locations {
47            upsert_location(conn, l).await?;
48            counts.locations += 1;
49        }
50        for c in &projections.constraints {
51            upsert_constraint(conn, c).await?;
52            counts.constraints += 1;
53        }
54        for i in &projections.interactions {
55            upsert_interaction(conn, i).await?;
56            counts.interactions += 1;
57        }
58        for p in &projections.participations {
59            upsert_participation(conn, p).await?;
60            counts.participations += 1;
61        }
62        Ok(counts)
63    }
64}
65
66#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
67pub struct TypedProjectionCounts {
68    pub components: usize,
69    pub sequences: usize,
70    pub features: usize,
71    pub locations: usize,
72    pub constraints: usize,
73    pub interactions: usize,
74    pub participations: usize,
75}
76
77fn as_str_opt(value: &Option<IriString>) -> Option<&str> {
78    value.as_ref().map(|i| i.as_str())
79}
80
81async fn upsert_component(
82    conn: &mut sqlx::PgConnection,
83    p: &ComponentProjection,
84) -> Result<(), DomainError> {
85    sqlx::query(
86        r#"
87        INSERT INTO sbol_components (
88            object_id, iri, types, roles, sequence_iris, feature_iris,
89            interaction_iris, model_iris
90        )
91        SELECT id, $1,
92               $2::text[]::sbol_ontology_term[],
93               $3::text[]::sbol_ontology_term[],
94               $4::text[]::sbol_iri[],
95               $5::text[]::sbol_iri[],
96               $6::text[]::sbol_iri[],
97               $7::text[]::sbol_iri[]
98        FROM sbol_objects WHERE iri = $1
99        ON CONFLICT (object_id) DO UPDATE SET
100            types = EXCLUDED.types,
101            roles = EXCLUDED.roles,
102            sequence_iris = EXCLUDED.sequence_iris,
103            feature_iris = EXCLUDED.feature_iris,
104            interaction_iris = EXCLUDED.interaction_iris,
105            model_iris = EXCLUDED.model_iris,
106            updated_at = now()
107        "#,
108    )
109    .bind(p.iri.as_str())
110    .bind(&p.types)
111    .bind(&p.roles)
112    .bind(&p.sequence_iris)
113    .bind(&p.feature_iris)
114    .bind(&p.interaction_iris)
115    .bind(&p.model_iris)
116    .execute(&mut *conn)
117    .await
118    .map_err(db_err)?;
119    Ok(())
120}
121
122async fn upsert_sequence(
123    conn: &mut sqlx::PgConnection,
124    p: &SequenceProjection,
125) -> Result<(), DomainError> {
126    let alphabet_str = p.alphabet.map(|a| a.as_db_str());
127    let row = sqlx::query(
128        r#"
129        INSERT INTO sbol_sequences (
130            object_id, iri, encoding_iri, elements, alphabet, content_hash
131        )
132        SELECT id, $1, $2, $3, $4, $5
133        FROM sbol_objects WHERE iri = $1
134        ON CONFLICT (object_id) DO UPDATE SET
135            encoding_iri = EXCLUDED.encoding_iri,
136            elements = EXCLUDED.elements,
137            alphabet = EXCLUDED.alphabet,
138            content_hash = EXCLUDED.content_hash
139        RETURNING object_id
140        "#,
141    )
142    .bind(p.iri.as_str())
143    .bind(as_str_opt(&p.encoding_iri))
144    .bind(p.elements.as_deref())
145    .bind(alphabet_str)
146    .bind(p.content_hash.as_deref())
147    .fetch_optional(&mut *conn)
148    .await
149    .map_err(db_err)?;
150    if let Some(row) = row {
151        use sqlx::Row;
152        let object_id: uuid::Uuid = row.try_get("object_id").map_err(db_err)?;
153        crate::repo::sequence_search::reindex_kmers(
154            conn,
155            object_id,
156            p.elements.as_deref(),
157            alphabet_str,
158        )
159        .await?;
160    }
161    Ok(())
162}
163
164async fn upsert_feature(
165    conn: &mut sqlx::PgConnection,
166    p: &FeatureProjection,
167) -> Result<(), DomainError> {
168    sqlx::query(
169        r#"
170        INSERT INTO sbol_features (
171            object_id, iri, parent_component_iri, feature_kind,
172            instance_of_iri, roles, orientation_iri
173        )
174        SELECT id, $1, $2, $3, $4, $5::text[]::sbol_ontology_term[], $6
175        FROM sbol_objects WHERE iri = $1
176        ON CONFLICT (object_id) DO UPDATE SET
177            parent_component_iri = EXCLUDED.parent_component_iri,
178            feature_kind = EXCLUDED.feature_kind,
179            instance_of_iri = EXCLUDED.instance_of_iri,
180            roles = EXCLUDED.roles,
181            orientation_iri = EXCLUDED.orientation_iri
182        "#,
183    )
184    .bind(p.iri.as_str())
185    .bind(as_str_opt(&p.parent_component_iri))
186    .bind(&p.feature_kind)
187    .bind(as_str_opt(&p.instance_of_iri))
188    .bind(&p.roles)
189    .bind(as_str_opt(&p.orientation_iri))
190    .execute(&mut *conn)
191    .await
192    .map_err(db_err)?;
193    Ok(())
194}
195
196async fn upsert_location(
197    conn: &mut sqlx::PgConnection,
198    p: &LocationProjection,
199) -> Result<(), DomainError> {
200    sqlx::query(
201        r#"
202        INSERT INTO sbol_locations (
203            object_id, iri, feature_iri, sequence_iri, location_kind,
204            start_pos, end_pos, cut_pos, orientation_iri, data
205        )
206        SELECT id, $1, $2, $3, $4, $5, $6, $7, $8, $9
207        FROM sbol_objects WHERE iri = $1
208        ON CONFLICT (object_id) DO UPDATE SET
209            feature_iri = EXCLUDED.feature_iri,
210            sequence_iri = EXCLUDED.sequence_iri,
211            location_kind = EXCLUDED.location_kind,
212            start_pos = EXCLUDED.start_pos,
213            end_pos = EXCLUDED.end_pos,
214            cut_pos = EXCLUDED.cut_pos,
215            orientation_iri = EXCLUDED.orientation_iri,
216            data = EXCLUDED.data
217        "#,
218    )
219    .bind(p.iri.as_str())
220    .bind(as_str_opt(&p.feature_iri))
221    .bind(as_str_opt(&p.sequence_iri))
222    .bind(&p.location_kind)
223    .bind(p.start_pos)
224    .bind(p.end_pos)
225    .bind(p.cut_pos)
226    .bind(as_str_opt(&p.orientation_iri))
227    .bind(&p.data)
228    .execute(&mut *conn)
229    .await
230    .map_err(db_err)?;
231    Ok(())
232}
233
234async fn upsert_constraint(
235    conn: &mut sqlx::PgConnection,
236    p: &ConstraintProjection,
237) -> Result<(), DomainError> {
238    sqlx::query(
239        r#"
240        INSERT INTO sbol_constraints (
241            object_id, iri, parent_component_iri, restriction_iri,
242            subject_iri, object_iri
243        )
244        SELECT id, $1, $2, $3, $4, $5
245        FROM sbol_objects WHERE iri = $1
246        ON CONFLICT (object_id) DO UPDATE SET
247            parent_component_iri = EXCLUDED.parent_component_iri,
248            restriction_iri = EXCLUDED.restriction_iri,
249            subject_iri = EXCLUDED.subject_iri,
250            object_iri = EXCLUDED.object_iri
251        "#,
252    )
253    .bind(p.iri.as_str())
254    .bind(as_str_opt(&p.parent_component_iri))
255    .bind(as_str_opt(&p.restriction_iri))
256    .bind(as_str_opt(&p.subject_iri))
257    .bind(as_str_opt(&p.object_iri))
258    .execute(&mut *conn)
259    .await
260    .map_err(db_err)?;
261    Ok(())
262}
263
264async fn upsert_interaction(
265    conn: &mut sqlx::PgConnection,
266    p: &InteractionProjection,
267) -> Result<(), DomainError> {
268    sqlx::query(
269        r#"
270        INSERT INTO sbol_interactions (
271            object_id, iri, parent_component_iri, interaction_types
272        )
273        SELECT id, $1, $2, $3::text[]::sbol_ontology_term[]
274        FROM sbol_objects WHERE iri = $1
275        ON CONFLICT (object_id) DO UPDATE SET
276            parent_component_iri = EXCLUDED.parent_component_iri,
277            interaction_types = EXCLUDED.interaction_types
278        "#,
279    )
280    .bind(p.iri.as_str())
281    .bind(as_str_opt(&p.parent_component_iri))
282    .bind(&p.interaction_types)
283    .execute(&mut *conn)
284    .await
285    .map_err(db_err)?;
286    Ok(())
287}
288
289async fn upsert_participation(
290    conn: &mut sqlx::PgConnection,
291    p: &ParticipationProjection,
292) -> Result<(), DomainError> {
293    sqlx::query(
294        r#"
295        INSERT INTO sbol_participations (
296            object_id, iri, interaction_iri, participant_iri, roles
297        )
298        SELECT id, $1, $2, $3, $4::text[]::sbol_ontology_term[]
299        FROM sbol_objects WHERE iri = $1
300        ON CONFLICT (object_id) DO UPDATE SET
301            interaction_iri = EXCLUDED.interaction_iri,
302            participant_iri = EXCLUDED.participant_iri,
303            roles = EXCLUDED.roles
304        "#,
305    )
306    .bind(p.iri.as_str())
307    .bind(as_str_opt(&p.interaction_iri))
308    .bind(as_str_opt(&p.participant_iri))
309    .bind(&p.roles)
310    .execute(&mut *conn)
311    .await
312    .map_err(db_err)?;
313    Ok(())
314}