Skip to main content

ahri_tre_pgmeta/
repository.rs

1use crate::{
2    MetadataExecutor, MetadataTransaction, PgAssetReadModel, PgAssetVersionDuoRestrictionReadModel,
3    PgAssetVersionEntityReadModel, PgAssetVersionReadModel,
4    PgAssetVersionRelationInstanceReadModel, PgCreateAsset, PgCreateAssetVersion,
5    PgCreateAssetVersionDuoRestriction, PgCreateDataFile, PgCreateDomain, PgCreateEntity,
6    PgCreateEntityRelation, PgCreateStudy, PgCreateStudyDuoRestriction, PgCreateTransformation,
7    PgCreateVariable, PgCreateVocabulary, PgCreateVocabularyItem, PgCreateVocabularyMapping,
8    PgDataFileReadModel, PgDatasetReadModel, PgDatasetVariableReadModel,
9    PgDatasetVersionEntityReadModel, PgDatasetVersionRelationInstanceReadModel, PgDomainReadModel,
10    PgDuoQualifiers, PgDuoRestrictionReadModel, PgEntityInstanceReadModel, PgEntityReadModel,
11    PgEntityRelationReadModel, PgMetaError, PgMetadataConnection, PgRelationInstanceReadModel,
12    PgStudyAccessReadModel, PgStudyCustodianReadModel, PgStudyEntityInstanceReadModel,
13    PgStudyReadModel, PgStudyRelationInstanceReadModel, PgTagReadModel, PgTagTarget,
14    PgTransformationInputReadModel, PgTransformationOutputReadModel, PgTransformationReadModel,
15    PgValueTypeReadModel, PgVariableReadModel, PgVocabularyItemReadModel, PgVocabularyReadModel,
16    PgWriteEntityInstance, PgWriteRelationInstance, PgWriteStudyEntityInstance,
17    PgWriteStudyRelationInstance, PgWriteTagTarget,
18};
19use ahri_tre_core::{
20    AssetRepository, CatalogueRegistrationRepository, CoreError, DataFileSearchCursorAnchor,
21    DataFileSearchPredicate, DataFileSearchQuery, DataFileSearchRecord,
22    DataFileSearchStudySelector, DataFileSearchTextField, DatasetSearchCursorAnchor,
23    DatasetSearchDomainSelector, DatasetSearchPredicate, DatasetSearchQuery, DatasetSearchRecord,
24    DatasetSearchStudySelector, DatasetSearchTextField, DomainRepository, EntityRepository,
25    EntitySearchCursorAnchor, EntitySearchDomainSelector, EntitySearchPredicate, EntitySearchQuery,
26    EntitySearchRecord, EntitySearchTextField, RelationSearchCursorAnchor,
27    RelationSearchDomainSelector, RelationSearchEntitySelector, RelationSearchPredicate,
28    RelationSearchQuery, RelationSearchRecord, RelationSearchTextField, SearchPage,
29    StudyDomainRepository, StudyGovernanceRepository, StudyRepository, StudySearchCursorAnchor,
30    StudySearchDomainSelector, StudySearchPredicate, StudySearchQuery, StudySearchTextField,
31    TagAttachmentTarget, TagRepository, TransformationRepository, VariableRepository,
32    VariableSearchCursorAnchor, VariableSearchDomainSelector, VariableSearchPredicate,
33    VariableSearchQuery, VariableSearchRecord, VariableSearchTextField,
34    VariableSearchVocabularySelector, VocabularyRepository, predicate_set_matches,
35    search_tags_match_selector, search_text_matches,
36};
37use ahri_tre_libpq_oauth::{LibpqOAuthConnection, LibpqOAuthRow};
38use ahri_tre_types::{
39    AssetId, AssetRecord, AssetType, AssetVersionDuoRestrictionId,
40    AssetVersionDuoRestrictionRecord, AssetVersionEntityRecord, AssetVersionRecord,
41    AssetVersionRelationInstanceRecord, DataFileRecord, DatasetRecord, DatasetVersionEntityRecord,
42    DatasetVersionRelationInstanceRecord, DatasetVersionWithdrawalRecord, DomainId, DomainRecord,
43    EntityId, EntityInstanceRecord, EntityInstanceWriteRecord, EntityRecord, EntityRelationId,
44    EntityRelationRecord, KeyRole, NcName, NewAssetRecord, NewAssetVersionDuoRestrictionRecord,
45    NewAssetVersionRecord, NewDatasetVersionWithdrawalRecord, NewDomainRecord,
46    NewEntityInstanceRecord, NewEntityRecord, NewEntityRelationRecord, NewRelationInstanceRecord,
47    NewStudyDuoRestrictionRecord, NewStudyRecord, NewTransformationRecord, NewVariableRecord,
48    NewVocabularyItemRecord, NewVocabularyMappingRecord, NewVocabularyRecord,
49    RelationInstanceRecord, RelationInstanceWriteRecord, Sha256Hex, StudyAccessGrantRecord,
50    StudyCustodianLinkRecord, StudyDomainLinkRecord, StudyDuoRestrictionId,
51    StudyDuoRestrictionRecord, StudyEntityInstanceRecord, StudyEntityInstanceWriteRecord, StudyId,
52    StudyRecord, StudyRelationInstanceRecord, StudyRelationInstanceWriteRecord, StudyTypeId, TagId,
53    TagRecord, TransformationId, TransformationInputLinkRecord, TransformationOutputLinkRecord,
54    TransformationRecord, TransformationType, ValueTypeId, ValueTypeRecord, VariableId,
55    VariableRecord, VersionId, VocabularyId, VocabularyItemId, VocabularyItemRecord,
56    VocabularyMappingRecord, VocabularyRecord,
57};
58use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc};
59use std::collections::BTreeMap;
60use std::sync::{Arc, Mutex};
61
62const OAUTH_ADD_STUDY_DELEGATE_CUSTODIAN_SQL: &str = "
63        SELECT public.tre_add_study_delegate_custodian($1, $2)
64        ";
65const OAUTH_TRANSFER_PRIMARY_CUSTODIANSHIP_SQL: &str = "
66        SELECT public.tre_transfer_study_primary_custodianship($1, $2)
67        ";
68const OAUTH_REMOVE_STUDY_DELEGATE_CUSTODIAN_SQL: &str = "
69        SELECT public.tre_remove_study_delegate_custodian($1, $2)
70        ";
71const OAUTH_LIST_DATASET_VARIABLES_SQL: &str = "
72        SELECT dv.dataset_id::text, dv.variable_id::text, dv.row_role::text
73          FROM public.dataset_variables AS dv
74         WHERE dv.dataset_id = $1
75         ORDER BY dv.variable_id
76        ";
77const DATASET_VERSION_WITHDRAWALS_RELATION: &str = "public.dataset_version_withdrawals";
78const DATASET_VERSION_WITHDRAWALS_EXISTS_FIELD: &str = "dataset_version_withdrawals_exists";
79
80/// Core repository implementation backed by PostgreSQL metadata tables.
81#[derive(Debug, Clone)]
82pub struct PgMetadataRepository<'connection> {
83    pub(super) observation: Option<ahri_tre_observability::CorrelationContext>,
84    pub(super) connection: PgMetadataRepositoryConnection<'connection>,
85}
86
87#[derive(Debug, Clone)]
88pub(super) enum PgMetadataRepositoryConnection<'connection> {
89    Direct(Arc<Mutex<PgMetadataConnection>>),
90    OAuth(Arc<Mutex<LibpqOAuthConnection>>),
91    ScopedDirect(Arc<Mutex<&'connection mut PgMetadataConnection>>),
92    ScopedOAuth(Arc<Mutex<&'connection mut LibpqOAuthConnection>>),
93}
94
95impl PgMetadataRepository<'_> {
96    /// Creates a repository that owns a direct PostgreSQL metadata connection.
97    pub fn new(connection: PgMetadataConnection) -> Self {
98        Self {
99            observation: None,
100            connection: PgMetadataRepositoryConnection::Direct(Arc::new(Mutex::new(connection))),
101        }
102    }
103
104    /// Creates a repository from a shared direct PostgreSQL metadata connection.
105    pub fn from_shared(connection: Arc<Mutex<PgMetadataConnection>>) -> Self {
106        Self {
107            observation: None,
108            connection: PgMetadataRepositoryConnection::Direct(connection),
109        }
110    }
111
112    /// Creates a repository from a shared OAuth-authenticated metadata connection.
113    pub fn from_oauth_shared(connection: Arc<Mutex<LibpqOAuthConnection>>) -> Self {
114        Self {
115            observation: None,
116            connection: PgMetadataRepositoryConnection::OAuth(connection),
117        }
118    }
119
120    /// Scope observation to a newly constructed repository, never the retained connection.
121    pub fn with_correlation(mut self, context: ahri_tre_observability::CorrelationContext) -> Self {
122        self.observation = Some(context);
123        self
124    }
125
126    pub(super) fn with_session_connection<T>(
127        &self,
128        operation: &'static str,
129        direct: impl FnOnce(&mut PgMetadataConnection) -> Result<T, PgMetaError>,
130        oauth: impl FnOnce(&mut LibpqOAuthConnection) -> Result<T, PgMetaError>,
131    ) -> Result<T, CoreError> {
132        match &self.connection {
133            PgMetadataRepositoryConnection::Direct(connection) => {
134                let mut connection = connection.lock().map_err(|_| {
135                    CoreError::Infrastructure(format!(
136                        "metadata connection lock was poisoned while trying to {operation}"
137                    ))
138                })?;
139                direct(&mut connection).map_err(|error| pgmeta_error(operation, error))
140            }
141            PgMetadataRepositoryConnection::OAuth(connection) => {
142                let mut connection = connection.lock().map_err(|_| {
143                    CoreError::Infrastructure(format!(
144                        "OAuth metadata connection lock was poisoned while trying to {operation}"
145                    ))
146                })?;
147                oauth(&mut connection).map_err(|error| pgmeta_error(operation, error))
148            }
149            PgMetadataRepositoryConnection::ScopedDirect(connection) => {
150                let mut connection = connection.lock().map_err(|_| {
151                    CoreError::Infrastructure(
152                        "Dataset admission connection lock was poisoned".into(),
153                    )
154                })?;
155                direct(&mut connection).map_err(|error| pgmeta_error(operation, error))
156            }
157            PgMetadataRepositoryConnection::ScopedOAuth(connection) => {
158                let mut connection = connection.lock().map_err(|_| {
159                    CoreError::Infrastructure(
160                        "Dataset admission connection lock was poisoned".into(),
161                    )
162                })?;
163                oauth(&mut connection).map_err(|error| pgmeta_error(operation, error))
164            }
165        }
166    }
167
168    async fn create_transformation_from_input(
169        &self,
170        input: PgCreateTransformation,
171    ) -> Result<TransformationRecord, CoreError> {
172        let transformation_id = self.with_session_connection(
173            "create transformation",
174            |connection| connection.create_transformation(input.clone()),
175            |connection| oauth_create_transformation(connection, input.clone()),
176        )?;
177
178        self.with_session_connection(
179            "read created transformation",
180            |connection| connection.get_transformation(transformation_id),
181            |connection| oauth_get_transformation(connection, transformation_id),
182        )?
183        .map(transformation_record)
184        .transpose()?
185        .ok_or_else(|| {
186            CoreError::NotFound(format!(
187                "created transformation {:?} was not readable",
188                transformation_id
189            ))
190        })
191    }
192}
193
194#[async_trait::async_trait]
195impl DomainRepository for PgMetadataRepository<'_> {
196    async fn list_domains(&self) -> Result<Vec<DomainRecord>, CoreError> {
197        use ahri_tre_observability::{FailureCategory, Measurement, Outcome, Stage};
198        let span = self
199            .observation
200            .map(|context| context.span(Stage::MetadataQuery));
201        let category = std::cell::Cell::new(None);
202        let observe = |error: PgMetaError| {
203            category.set(Some(error.operational_category()));
204            error
205        };
206        let result: Result<Vec<_>, CoreError> = self
207            .with_session_connection(
208                "list domains",
209                |connection| connection.list_domains().map_err(&observe),
210                |connection| oauth_list_domains(connection).map_err(&observe),
211            )
212            .and_then(|rows| rows.into_iter().map(domain_record).collect());
213        if let Some(span) = span {
214            let (outcome, failure) = match (&result, category.get()) {
215                (Ok(_), _) => (Outcome::Success, None),
216                (
217                    Err(_),
218                    Some(
219                        category @ (FailureCategory::Authentication
220                        | FailureCategory::Authorization),
221                    ),
222                ) => (Outcome::Rejected, Some(category)),
223                (Err(_), Some(category)) => (Outcome::Unavailable, Some(category)),
224                (Err(_), None) => (
225                    Outcome::InternalFailure,
226                    Some(FailureCategory::Compatibility),
227                ),
228            };
229            let evidence = result
230                .as_ref()
231                .ok()
232                .map(|rows| Measurement::ReturnedRecords(rows.len() as u64));
233            span.finish_with_evidence(outcome, failure, evidence.as_slice());
234        }
235        result
236    }
237
238    async fn list_domains_by_name(&self, name: &str) -> Result<Vec<DomainRecord>, CoreError> {
239        self.with_session_connection(
240            "list domains by name",
241            |connection| connection.list_domains_by_name(name),
242            |connection| oauth_list_domains_by_name(connection, name),
243        )?
244        .into_iter()
245        .map(domain_record)
246        .collect()
247    }
248
249    async fn get_domain_by_id(
250        &self,
251        domain_id: DomainId,
252    ) -> Result<Option<DomainRecord>, CoreError> {
253        self.with_session_connection(
254            "get domain by id",
255            |connection| connection.get_domain(domain_id),
256            |connection| oauth_get_domain(connection, domain_id),
257        )?
258        .map(domain_record)
259        .transpose()
260    }
261
262    async fn get_domain_by_name(&self, name: &str) -> Result<Option<DomainRecord>, CoreError> {
263        self.with_session_connection(
264            "get domain by name",
265            |connection| connection.get_domain_by_name(name),
266            |connection| oauth_get_domain_by_name(connection, name),
267        )?
268        .map(domain_record)
269        .transpose()
270    }
271
272    async fn create_domain(&self, domain: &NewDomainRecord) -> Result<DomainRecord, CoreError> {
273        let input = PgCreateDomain {
274            domain_id: None,
275            name: domain.name.as_str().to_string(),
276            uri: domain.uri.clone(),
277            description: domain.description.clone(),
278        };
279        let domain_id = self.with_session_connection(
280            "create domain",
281            |connection| connection.create_domain(input.clone()),
282            |connection| oauth_create_domain(connection, input.clone()),
283        )?;
284
285        self.with_session_connection(
286            "read created domain",
287            |connection| connection.get_domain(domain_id),
288            |connection| oauth_get_domain(connection, domain_id),
289        )?
290        .map(domain_record)
291        .transpose()?
292        .ok_or_else(|| {
293            CoreError::NotFound(format!("created domain {:?} was not readable", domain_id))
294        })
295    }
296
297    async fn save_domain(&self, domain: &DomainRecord) -> Result<DomainRecord, CoreError> {
298        let input = PgCreateDomain {
299            domain_id: Some(domain.domain_id),
300            name: domain.name.as_str().to_string(),
301            uri: domain.uri.clone(),
302            description: domain.description.clone(),
303        };
304        let domain_id = self.with_session_connection(
305            "create domain",
306            |connection| connection.create_domain(input.clone()),
307            |connection| oauth_create_domain(connection, input.clone()),
308        )?;
309
310        self.with_session_connection(
311            "read created domain",
312            |connection| connection.get_domain(domain_id),
313            |connection| oauth_get_domain(connection, domain_id),
314        )?
315        .map(domain_record)
316        .transpose()?
317        .ok_or_else(|| {
318            CoreError::NotFound(format!("created domain {:?} was not readable", domain_id))
319        })
320    }
321
322    async fn delete_domain_record(&self, domain_id: DomainId) -> Result<bool, CoreError> {
323        self.with_session_connection(
324            "delete domain record",
325            |connection| delete_semantic_record(connection, "domains", "domain_id", domain_id.0),
326            |connection| delete_semantic_record(connection, "domains", "domain_id", domain_id.0),
327        )
328    }
329}
330
331#[async_trait::async_trait]
332impl CatalogueRegistrationRepository for PgMetadataRepository<'_> {
333    async fn create_domain_registration(
334        &self,
335        domain: &NewDomainRecord,
336        normalized_tags: &[String],
337    ) -> Result<DomainRecord, CoreError> {
338        let domain = domain.clone();
339        let tags = normalized_tags.to_vec();
340        self.with_session_connection(
341            "create Domain registration",
342            |connection| direct_create_domain_registration(connection, &domain, &tags),
343            |connection| oauth_create_domain_registration(connection, &domain, &tags),
344        )
345    }
346
347    async fn create_study_registration(
348        &self,
349        study: &NewStudyRecord,
350        domain_ids: &[DomainId],
351        normalized_tags: &[String],
352    ) -> Result<StudyRecord, CoreError> {
353        let study = study.clone();
354        let domain_ids = domain_ids.to_vec();
355        let tags = normalized_tags.to_vec();
356        self.with_session_connection(
357            "create Study registration",
358            |connection| direct_create_study_registration(connection, &study, &domain_ids, &tags),
359            |connection| oauth_create_study_registration(connection, &study, &domain_ids, &tags),
360        )
361    }
362
363    async fn save_domain_registration(
364        &self,
365        domain: &DomainRecord,
366        normalized_tags: &[String],
367    ) -> Result<DomainRecord, CoreError> {
368        let domain = domain.clone();
369        let tags = normalized_tags.to_vec();
370        self.with_session_connection(
371            "save Domain registration",
372            |connection| direct_save_domain_registration(connection, &domain, &tags),
373            |connection| oauth_save_domain_registration(connection, &domain, &tags),
374        )
375    }
376
377    async fn save_study_registration(
378        &self,
379        study: &StudyRecord,
380        domain_ids: &[DomainId],
381        normalized_tags: &[String],
382    ) -> Result<StudyRecord, CoreError> {
383        let study = study.clone();
384        let domain_ids = domain_ids.to_vec();
385        let tags = normalized_tags.to_vec();
386        self.with_session_connection(
387            "save Study registration",
388            |connection| direct_save_study_registration(connection, &study, &domain_ids, &tags),
389            |connection| oauth_save_study_registration(connection, &study, &domain_ids, &tags),
390        )
391    }
392}
393
394#[async_trait::async_trait]
395impl StudyRepository for PgMetadataRepository<'_> {
396    async fn study_type_exists(&self, id: StudyTypeId) -> Result<bool, CoreError> {
397        self.with_session_connection(
398            "lookup Study type",
399            |connection| {
400                Ok(connection
401                    .list_study_types()?
402                    .iter()
403                    .any(|row| row.study_type_id == id))
404            },
405            |connection| {
406                Ok(MetadataExecutor::query_optional(
407                    connection,
408                    "lookup Study type",
409                    "SELECT study_type_id::text FROM public.study_types WHERE study_type_id = $1",
410                    &[&metadata_i32(id.0, "study_type_id")?],
411                )?
412                .is_some())
413            },
414        )
415    }
416
417    async fn list_studies(&self) -> Result<Vec<StudyRecord>, CoreError> {
418        self.with_session_connection(
419            "list studies",
420            |connection| connection.list_studies(),
421            oauth_list_studies,
422        )?
423        .into_iter()
424        .map(study_record)
425        .collect()
426    }
427
428    async fn get_study_by_id(&self, study_id: StudyId) -> Result<Option<StudyRecord>, CoreError> {
429        self.with_session_connection(
430            "get study by id",
431            |connection| connection.get_study(study_id),
432            |connection| oauth_get_study(connection, study_id),
433        )?
434        .map(study_record)
435        .transpose()
436    }
437
438    async fn get_study_by_name(&self, name: &str) -> Result<Option<StudyRecord>, CoreError> {
439        self.with_session_connection(
440            "get study by name",
441            |connection| connection.get_study_by_name(name),
442            |connection| oauth_get_study_by_name(connection, name),
443        )?
444        .map(study_record)
445        .transpose()
446    }
447
448    async fn search_studies(
449        &self,
450        query: &StudySearchQuery,
451    ) -> Result<SearchPage<StudyRecord, StudySearchCursorAnchor>, CoreError> {
452        let studies = <Self as StudyRepository>::list_studies(self).await?;
453        let mut matched = Vec::new();
454        for study in studies {
455            let domains =
456                <Self as StudyDomainRepository>::list_domains_for_study(self, study.study_id)
457                    .await?;
458            let tags = <Self as TagRepository>::list_tags_for_target(
459                self,
460                TagAttachmentTarget::Study(study.study_id),
461            )
462            .await?;
463            if study_matches_search_query(&study, &domains, &tags, query) {
464                matched.push(study);
465            }
466        }
467
468        matched.sort_by(study_search_sort_key);
469        if let Some(after) = query.after.as_ref() {
470            matched.retain(|study| study_sort_tuple(study) > study_cursor_tuple(after));
471        }
472
473        let limit = usize::from(query.limit);
474        let has_more = matched.len() > limit;
475        if has_more {
476            matched.truncate(limit);
477        }
478        let next_cursor = if has_more {
479            matched.last().map(|study| StudySearchCursorAnchor {
480                name: study.name.as_str().to_string(),
481                study_id: study.study_id,
482            })
483        } else {
484            None
485        };
486
487        Ok(SearchPage {
488            items: matched,
489            next_cursor,
490        })
491    }
492
493    async fn create_study(&self, study: &NewStudyRecord) -> Result<StudyRecord, CoreError> {
494        let input = PgCreateStudy {
495            study_id: None,
496            name: study.name.as_str().to_string(),
497            description: study.description.clone(),
498            documentation: study.documentation.clone(),
499            agent_instructions: study.agent_instructions.clone(),
500            external_id: study.external_id.clone(),
501            study_type_id: study.study_type_id,
502            date_created: study.date_created,
503            created_by: study.created_by.clone(),
504        };
505        let study_id = self.with_session_connection(
506            "create study",
507            |connection| connection.create_study(input.clone()),
508            |connection| oauth_create_study(connection, input.clone()),
509        )?;
510
511        self.with_session_connection(
512            "read created study",
513            |connection| connection.get_study(study_id),
514            |connection| oauth_get_study(connection, study_id),
515        )?
516        .map(study_record)
517        .transpose()?
518        .ok_or_else(|| {
519            CoreError::NotFound(format!("created study {:?} was not readable", study_id))
520        })
521    }
522
523    async fn save_study(&self, study: &StudyRecord) -> Result<StudyRecord, CoreError> {
524        let input = PgCreateStudy {
525            study_id: Some(study.study_id),
526            name: study.name.as_str().to_string(),
527            description: study.description.clone(),
528            documentation: study.documentation.clone(),
529            agent_instructions: study.agent_instructions.clone(),
530            external_id: study.external_id.clone(),
531            study_type_id: study.study_type_id,
532            date_created: study.date_created,
533            created_by: study.created_by.clone(),
534        };
535        let study_id = self.with_session_connection(
536            "create study",
537            |connection| connection.create_study(input.clone()),
538            |connection| oauth_create_study(connection, input.clone()),
539        )?;
540
541        self.with_session_connection(
542            "read created study",
543            |connection| connection.get_study(study_id),
544            |connection| oauth_get_study(connection, study_id),
545        )?
546        .map(study_record)
547        .transpose()?
548        .ok_or_else(|| {
549            CoreError::NotFound(format!("created study {:?} was not readable", study_id))
550        })
551    }
552
553    async fn delete_study_record(&self, study_id: StudyId) -> Result<bool, CoreError> {
554        self.with_session_connection(
555            "delete study record",
556            |connection| delete_study_metadata_record(connection, study_id),
557            |connection| delete_study_metadata_record(connection, study_id),
558        )
559    }
560}
561
562#[async_trait::async_trait]
563impl StudyDomainRepository for PgMetadataRepository<'_> {
564    async fn link_study_domain(
565        &self,
566        study_id: StudyId,
567        domain_id: DomainId,
568    ) -> Result<StudyDomainLinkRecord, CoreError> {
569        self.with_session_connection(
570            "link study domain",
571            |connection| connection.link_study_domain(study_id, domain_id),
572            |connection| oauth_link_study_domain(connection, study_id, domain_id),
573        )?;
574        Ok(StudyDomainLinkRecord {
575            study_id,
576            domain_id,
577        })
578    }
579
580    async fn list_domains_for_study(
581        &self,
582        study_id: StudyId,
583    ) -> Result<Vec<DomainRecord>, CoreError> {
584        self.with_session_connection(
585            "list study domains",
586            |connection| connection.list_study_domains(study_id),
587            |connection| oauth_list_study_domains(connection, study_id),
588        )?
589        .into_iter()
590        .map(domain_record)
591        .collect()
592    }
593}
594
595#[async_trait::async_trait]
596impl StudyGovernanceRepository for PgMetadataRepository<'_> {
597    async fn list_access_grants(
598        &self,
599        study_id: StudyId,
600    ) -> Result<Vec<StudyAccessGrantRecord>, CoreError> {
601        self.with_session_connection(
602            "list study access",
603            |connection| connection.list_study_access(study_id),
604            |connection| oauth_list_study_access(connection, study_id),
605        )?
606        .into_iter()
607        .map(study_access_record)
608        .collect()
609    }
610
611    async fn grant_study_access(
612        &self,
613        study_id: StudyId,
614        target_user_id: &str,
615    ) -> Result<StudyAccessGrantRecord, CoreError> {
616        let normalized_target_user_id = target_user_id.trim().to_string();
617        self.with_session_connection(
618            "grant study access",
619            |connection| connection.grant_study_access(study_id, &normalized_target_user_id),
620            |connection| oauth_grant_study_access(connection, study_id, &normalized_target_user_id),
621        )?;
622        Ok(StudyAccessGrantRecord {
623            study_id,
624            user_id: normalized_target_user_id,
625            date_granted: None,
626            granted_by: None,
627        })
628    }
629
630    async fn save_access_grant(
631        &self,
632        grant: &StudyAccessGrantRecord,
633    ) -> Result<StudyAccessGrantRecord, CoreError> {
634        self.grant_study_access(grant.study_id, &grant.user_id)
635            .await
636    }
637
638    async fn revoke_access_grant(
639        &self,
640        study_id: StudyId,
641        principal: &str,
642    ) -> Result<(), CoreError> {
643        self.revoke_study_access(study_id, principal).await
644    }
645
646    async fn revoke_study_access(
647        &self,
648        study_id: StudyId,
649        target_user_id: &str,
650    ) -> Result<(), CoreError> {
651        let normalized_target_user_id = target_user_id.trim().to_string();
652        self.with_session_connection(
653            "revoke study access",
654            |connection| connection.revoke_study_access(study_id, &normalized_target_user_id),
655            |connection| {
656                oauth_revoke_study_access(connection, study_id, &normalized_target_user_id)
657            },
658        )
659    }
660
661    async fn list_custodians(
662        &self,
663        study_id: StudyId,
664    ) -> Result<Vec<StudyCustodianLinkRecord>, CoreError> {
665        self.with_session_connection(
666            "list study custodians",
667            |connection| connection.list_study_custodians(study_id),
668            |connection| oauth_list_study_custodians(connection, study_id),
669        )?
670        .into_iter()
671        .map(study_custodian_record)
672        .collect()
673    }
674
675    async fn add_delegate_custodian(
676        &self,
677        study_id: StudyId,
678        target_user_id: &str,
679    ) -> Result<StudyCustodianLinkRecord, CoreError> {
680        let normalized_target_user_id = target_user_id.trim().to_string();
681        self.with_session_connection(
682            "add study delegate custodian",
683            |connection| {
684                connection.add_study_delegate_custodian(study_id, &normalized_target_user_id)
685            },
686            |connection| {
687                oauth_add_study_delegate_custodian(connection, study_id, &normalized_target_user_id)
688            },
689        )?;
690        Ok(StudyCustodianLinkRecord {
691            study_id,
692            user_id: normalized_target_user_id,
693            is_primary: false,
694            date_granted: None,
695            granted_by: None,
696        })
697    }
698
699    async fn transfer_primary_custodianship(
700        &self,
701        study_id: StudyId,
702        target_user_id: &str,
703    ) -> Result<StudyCustodianLinkRecord, CoreError> {
704        let normalized_target_user_id = target_user_id.trim().to_string();
705        self.with_session_connection(
706            "transfer study primary custodianship",
707            |connection| {
708                connection
709                    .transfer_study_primary_custodianship(study_id, &normalized_target_user_id)
710            },
711            |connection| {
712                oauth_transfer_study_primary_custodianship(
713                    connection,
714                    study_id,
715                    &normalized_target_user_id,
716                )
717            },
718        )?;
719        Ok(StudyCustodianLinkRecord {
720            study_id,
721            user_id: normalized_target_user_id,
722            is_primary: true,
723            date_granted: None,
724            granted_by: None,
725        })
726    }
727
728    async fn remove_delegate_custodian(
729        &self,
730        study_id: StudyId,
731        target_user_id: &str,
732    ) -> Result<(), CoreError> {
733        let normalized_target_user_id = target_user_id.trim().to_string();
734        self.with_session_connection(
735            "remove study delegate custodian",
736            |connection| {
737                connection.remove_study_delegate_custodian(study_id, &normalized_target_user_id)
738            },
739            |connection| {
740                oauth_remove_study_delegate_custodian(
741                    connection,
742                    study_id,
743                    &normalized_target_user_id,
744                )
745            },
746        )
747    }
748
749    async fn save_custodian(
750        &self,
751        custodian: &StudyCustodianLinkRecord,
752    ) -> Result<StudyCustodianLinkRecord, CoreError> {
753        if !custodian.is_primary
754            && custodian.date_granted.is_none()
755            && custodian.granted_by.is_none()
756        {
757            return self
758                .add_delegate_custodian(custodian.study_id, &custodian.user_id)
759                .await;
760        }
761
762        self.with_session_connection(
763            "save study custodian",
764            |connection| {
765                connection.add_study_custodian_with_metadata(
766                    custodian.study_id,
767                    &custodian.user_id,
768                    custodian.is_primary,
769                    custodian.date_granted,
770                    custodian.granted_by.as_deref(),
771                )
772            },
773            |connection| oauth_save_study_custodian(connection, custodian),
774        )?;
775        Ok(custodian.clone())
776    }
777
778    async fn list_study_duo_restrictions(
779        &self,
780        study_id: StudyId,
781    ) -> Result<Vec<StudyDuoRestrictionRecord>, CoreError> {
782        self.with_session_connection(
783            "list study DUO restrictions",
784            |connection| connection.list_study_duo_restrictions(study_id),
785            |connection| oauth_list_study_duo_restrictions(connection, study_id),
786        )?
787        .into_iter()
788        .map(study_duo_restriction_record)
789        .collect()
790    }
791
792    async fn create_study_duo_restriction(
793        &self,
794        restriction: &NewStudyDuoRestrictionRecord,
795    ) -> Result<StudyDuoRestrictionRecord, CoreError> {
796        let input = pg_create_study_duo_restriction(None, restriction);
797        let restriction_id = self.with_session_connection(
798            "create study DUO restriction",
799            |connection| connection.create_study_duo_restriction(input.clone()),
800            |connection| oauth_create_study_duo_restriction(connection, input.clone()),
801        )?;
802        Ok(StudyDuoRestrictionRecord {
803            study_duo_restriction_id: StudyDuoRestrictionId(restriction_id),
804            study_id: restriction.study_id,
805            duo_id: restriction.duo_id.clone(),
806            qualifier_ontology_namespace: restriction.qualifier_ontology_namespace.clone(),
807            qualifier_ontology_id: restriction.qualifier_ontology_id.clone(),
808            qualifier_text: restriction.qualifier_text.clone(),
809            qualifier_date: restriction.qualifier_date,
810            qualifier_integer: restriction.qualifier_integer,
811        })
812    }
813
814    async fn save_study_duo_restriction(
815        &self,
816        restriction: &StudyDuoRestrictionRecord,
817    ) -> Result<StudyDuoRestrictionRecord, CoreError> {
818        let input = pg_create_study_duo_restriction(
819            Some(restriction.study_duo_restriction_id),
820            &NewStudyDuoRestrictionRecord {
821                study_id: restriction.study_id,
822                duo_id: restriction.duo_id.clone(),
823                qualifier_ontology_namespace: restriction.qualifier_ontology_namespace.clone(),
824                qualifier_ontology_id: restriction.qualifier_ontology_id.clone(),
825                qualifier_text: restriction.qualifier_text.clone(),
826                qualifier_date: restriction.qualifier_date,
827                qualifier_integer: restriction.qualifier_integer,
828            },
829        );
830        self.with_session_connection(
831            "save study DUO restriction",
832            |connection| connection.create_study_duo_restriction(input.clone()),
833            |connection| oauth_create_study_duo_restriction(connection, input.clone()),
834        )?;
835        Ok(restriction.clone())
836    }
837
838    async fn replace_study_duo_restrictions(
839        &self,
840        study_id: StudyId,
841        restrictions: &[NewStudyDuoRestrictionRecord],
842    ) -> Result<Vec<StudyDuoRestrictionRecord>, CoreError> {
843        let restrictions = self.with_session_connection(
844            "replace study DUO restrictions",
845            |connection| {
846                replace_study_duo_restrictions_with_executor(connection, study_id, restrictions)?;
847                connection.list_study_duo_restrictions(study_id)
848            },
849            |connection| {
850                replace_study_duo_restrictions_with_executor(connection, study_id, restrictions)?;
851                oauth_list_study_duo_restrictions(connection, study_id)
852            },
853        )?;
854        restrictions
855            .into_iter()
856            .map(study_duo_restriction_record)
857            .collect()
858    }
859}
860
861#[async_trait::async_trait]
862impl AssetRepository for PgMetadataRepository<'_> {
863    async fn asset_classification(
864        &self,
865        asset: AssetId,
866    ) -> Result<Option<ahri_tre_types::AssetClassification>, CoreError> {
867        self.with_session_connection(
868            "read Asset classification",
869            |db| crate::governance::classification(db, asset.0),
870            |db| crate::governance::classification(db, asset.0),
871        )
872    }
873    async fn validate_dataset_derivation(
874        &self,
875        inputs: &[VersionId],
876        risk: ahri_tre_types::AssetRisk,
877        inherit: bool,
878    ) -> Result<(), CoreError> {
879        let inputs = inputs.iter().map(|id| id.0).collect::<Vec<_>>();
880        self.with_session_connection(
881            "validate Dataset derivation",
882            |db| crate::governance::validate_derivation(db, &inputs, risk, inherit),
883            |db| crate::governance::validate_derivation(db, &inputs, risk, inherit),
884        )
885    }
886    async fn list_assets_for_study(
887        &self,
888        study_id: StudyId,
889    ) -> Result<Vec<AssetRecord>, CoreError> {
890        self.with_session_connection(
891            "list assets for study",
892            |connection| connection.list_assets_for_study(study_id),
893            |connection| oauth_list_assets_for_study(connection, study_id),
894        )?
895        .into_iter()
896        .map(asset_record)
897        .collect()
898    }
899
900    async fn get_asset_by_id(&self, asset_id: AssetId) -> Result<Option<AssetRecord>, CoreError> {
901        self.with_session_connection(
902            "get asset by id",
903            |connection| connection.get_asset(asset_id),
904            |connection| oauth_get_asset(connection, asset_id),
905        )?
906        .map(asset_record)
907        .transpose()
908    }
909
910    async fn get_asset_by_name(
911        &self,
912        study_id: StudyId,
913        name: &str,
914    ) -> Result<Option<AssetRecord>, CoreError> {
915        self.with_session_connection(
916            "get asset by name",
917            |connection| connection.get_asset_by_name(study_id, name),
918            |connection| oauth_get_asset_by_name(connection, study_id, name),
919        )?
920        .map(asset_record)
921        .transpose()
922    }
923
924    async fn create_asset(&self, asset: &NewAssetRecord) -> Result<AssetRecord, CoreError> {
925        let input = PgCreateAsset {
926            asset_id: None,
927            study_id: asset.study_id,
928            name: asset.name.as_str().to_string(),
929            description: asset.description.clone(),
930            agent_instructions: asset.agent_instructions.clone(),
931            asset_type: asset.asset_type,
932            date_created: asset.date_created,
933            created_by: asset.created_by.clone(),
934        };
935        let asset_id = self.with_session_connection(
936            "create asset",
937            |connection| connection.create_asset(input.clone()),
938            |connection| oauth_create_asset(connection, input.clone()),
939        )?;
940
941        self.with_session_connection(
942            "read created asset",
943            |connection| connection.get_asset(asset_id),
944            |connection| oauth_get_asset(connection, asset_id),
945        )?
946        .map(asset_record)
947        .transpose()?
948        .ok_or_else(|| {
949            CoreError::NotFound(format!("created asset {:?} was not readable", asset_id))
950        })
951    }
952
953    async fn save_asset(&self, asset: &AssetRecord) -> Result<AssetRecord, CoreError> {
954        let input = PgCreateAsset {
955            asset_id: Some(asset.asset_id),
956            study_id: asset.study_id,
957            name: asset.name.as_str().to_string(),
958            description: asset.description.clone(),
959            agent_instructions: asset.agent_instructions.clone(),
960            asset_type: asset.asset_type,
961            date_created: asset.date_created,
962            created_by: asset.created_by.clone(),
963        };
964        let asset_id = self.with_session_connection(
965            "create asset",
966            |connection| connection.create_asset(input.clone()),
967            |connection| oauth_create_asset(connection, input.clone()),
968        )?;
969
970        self.with_session_connection(
971            "read created asset",
972            |connection| connection.get_asset(asset_id),
973            |connection| oauth_get_asset(connection, asset_id),
974        )?
975        .map(asset_record)
976        .transpose()?
977        .ok_or_else(|| {
978            CoreError::NotFound(format!("created asset {:?} was not readable", asset_id))
979        })
980    }
981
982    async fn delete_asset_record(&self, asset_id: AssetId) -> Result<bool, CoreError> {
983        self.with_session_connection(
984            "delete asset record",
985            |connection| delete_asset_metadata_record(connection, asset_id),
986            |connection| delete_asset_metadata_record(connection, asset_id),
987        )
988    }
989
990    async fn list_asset_versions(
991        &self,
992        asset_id: AssetId,
993    ) -> Result<Vec<AssetVersionRecord>, CoreError> {
994        Ok(self
995            .with_session_connection(
996                "list asset versions",
997                |connection| connection.list_asset_versions(asset_id),
998                |connection| oauth_list_asset_versions(connection, asset_id),
999            )?
1000            .into_iter()
1001            .map(asset_version_record)
1002            .collect())
1003    }
1004
1005    async fn get_asset_version(
1006        &self,
1007        version_id: VersionId,
1008    ) -> Result<Option<AssetVersionRecord>, CoreError> {
1009        Ok(self
1010            .with_session_connection(
1011                "get asset version",
1012                |connection| connection.get_asset_version(version_id),
1013                |connection| oauth_get_asset_version(connection, version_id),
1014            )?
1015            .map(asset_version_record))
1016    }
1017
1018    async fn delete_datafile_version_metadata(
1019        &self,
1020        version_id: VersionId,
1021        latest_promotion: Option<(AssetId, Option<VersionId>)>,
1022    ) -> Result<bool, CoreError> {
1023        self.with_session_connection(
1024            "delete Datafile version metadata",
1025            |connection| delete_version_metadata(connection, version_id, latest_promotion),
1026            |connection| delete_version_metadata(connection, version_id, latest_promotion),
1027        )
1028    }
1029
1030    async fn delete_asset_version_record(&self, version_id: VersionId) -> Result<bool, CoreError> {
1031        self.with_session_connection(
1032            "delete asset version record",
1033            |connection| delete_asset_version_metadata_record(connection, version_id),
1034            |connection| delete_asset_version_metadata_record(connection, version_id),
1035        )
1036    }
1037
1038    async fn create_asset_version(
1039        &self,
1040        version: &NewAssetVersionRecord,
1041    ) -> Result<AssetVersionRecord, CoreError> {
1042        let input = PgCreateAssetVersion {
1043            classification: version.classification.clone(),
1044            version_id: None,
1045            asset_id: version.asset_id,
1046            major: Some(version.major),
1047            minor: Some(version.minor),
1048            patch: Some(version.patch),
1049            version_note: version.version_note.clone(),
1050            is_latest: version.is_latest,
1051            doi: version.doi.clone(),
1052            created_at: version.created_at,
1053            created_by: version.created_by.clone(),
1054        };
1055        let version_id = self.with_session_connection(
1056            "create asset version",
1057            |connection| connection.create_asset_version(input.clone()),
1058            |connection| oauth_create_asset_version(connection, input.clone()),
1059        )?;
1060
1061        self.with_session_connection(
1062            "read created asset version",
1063            |connection| connection.get_asset_version(version_id),
1064            |connection| oauth_get_asset_version(connection, version_id),
1065        )?
1066        .map(asset_version_record)
1067        .ok_or_else(|| {
1068            CoreError::NotFound(format!(
1069                "created asset version {:?} was not readable",
1070                version_id
1071            ))
1072        })
1073    }
1074
1075    async fn save_asset_version(
1076        &self,
1077        _version: &AssetVersionRecord,
1078    ) -> Result<AssetVersionRecord, CoreError> {
1079        Err(CoreError::Validation(
1080            "Explicit ingest classification is required".into(),
1081        ))
1082    }
1083
1084    async fn admit_asset_version(
1085        &self,
1086        version: &AssetVersionRecord,
1087        classification: &ahri_tre_types::IngestClassification,
1088    ) -> Result<AssetVersionRecord, CoreError> {
1089        let input = PgCreateAssetVersion {
1090            classification: classification.clone(),
1091            version_id: Some(version.version_id),
1092            asset_id: version.asset_id,
1093            major: Some(version.major),
1094            minor: Some(version.minor),
1095            patch: Some(version.patch),
1096            version_note: version.version_note.clone(),
1097            is_latest: version.is_latest,
1098            doi: version.doi.clone(),
1099            created_at: version.created_at,
1100            created_by: version.created_by.clone(),
1101        };
1102        let version_id = self.with_session_connection(
1103            "create asset version",
1104            |connection| connection.create_asset_version(input.clone()),
1105            |connection| oauth_create_asset_version(connection, input.clone()),
1106        )?;
1107
1108        self.with_session_connection(
1109            "read created asset version",
1110            |connection| connection.get_asset_version(version_id),
1111            |connection| oauth_get_asset_version(connection, version_id),
1112        )?
1113        .map(asset_version_record)
1114        .ok_or_else(|| {
1115            CoreError::NotFound(format!(
1116                "created asset version {:?} was not readable",
1117                version_id
1118            ))
1119        })
1120    }
1121
1122    async fn mark_asset_version_latest(
1123        &self,
1124        asset_id: AssetId,
1125        version_id: VersionId,
1126    ) -> Result<AssetVersionRecord, CoreError> {
1127        self.with_session_connection(
1128            "mark asset version latest",
1129            |connection| connection.with_transaction("promote admitted Dataset", |transaction| {
1130                transaction.execute_command(
1131                    "clear latest asset version",
1132                    "UPDATE public.asset_versions SET is_latest = FALSE WHERE asset_id = $1 AND is_latest = TRUE",
1133                    &[&asset_id.0],
1134                )?;
1135                transaction.query_one(
1136                    "mark asset version latest",
1137                    "UPDATE public.asset_versions SET is_latest = TRUE WHERE asset_id = $1 AND version_id = $2 RETURNING version_id",
1138                    &[&asset_id.0, &version_id.0],
1139                )?;
1140                Ok(())
1141            }),
1142            |connection| oauth_mark_asset_version_latest(connection, asset_id, version_id),
1143        )?;
1144
1145        self.get_asset_version(version_id).await?.ok_or_else(|| {
1146            CoreError::NotFound(format!(
1147                "marked asset version {:?} was not readable",
1148                version_id
1149            ))
1150        })
1151    }
1152
1153    async fn list_asset_version_duo_restrictions(
1154        &self,
1155        version_id: VersionId,
1156    ) -> Result<Vec<AssetVersionDuoRestrictionRecord>, CoreError> {
1157        self.with_session_connection(
1158            "list asset version DUO restrictions",
1159            |connection| connection.list_asset_version_duo_restrictions(version_id),
1160            |connection| oauth_list_asset_version_duo_restrictions(connection, version_id),
1161        )?
1162        .into_iter()
1163        .map(asset_version_duo_restriction_record)
1164        .collect()
1165    }
1166
1167    async fn create_asset_version_duo_restriction(
1168        &self,
1169        restriction: &NewAssetVersionDuoRestrictionRecord,
1170    ) -> Result<AssetVersionDuoRestrictionRecord, CoreError> {
1171        let input = pg_create_asset_version_duo_restriction(None, restriction);
1172        let restriction_id = self.with_session_connection(
1173            "create asset version DUO restriction",
1174            |connection| connection.create_asset_version_duo_restriction(input.clone()),
1175            |connection| oauth_create_asset_version_duo_restriction(connection, input.clone()),
1176        )?;
1177        Ok(AssetVersionDuoRestrictionRecord {
1178            asset_version_duo_restriction_id: AssetVersionDuoRestrictionId(restriction_id),
1179            version_id: restriction.version_id,
1180            duo_id: restriction.duo_id.clone(),
1181            qualifier_ontology_namespace: restriction.qualifier_ontology_namespace.clone(),
1182            qualifier_ontology_id: restriction.qualifier_ontology_id.clone(),
1183            qualifier_text: restriction.qualifier_text.clone(),
1184            qualifier_date: restriction.qualifier_date,
1185            qualifier_integer: restriction.qualifier_integer,
1186        })
1187    }
1188
1189    async fn save_asset_version_duo_restriction(
1190        &self,
1191        restriction: &AssetVersionDuoRestrictionRecord,
1192    ) -> Result<AssetVersionDuoRestrictionRecord, CoreError> {
1193        let input = pg_create_asset_version_duo_restriction(
1194            Some(restriction.asset_version_duo_restriction_id),
1195            &NewAssetVersionDuoRestrictionRecord {
1196                version_id: restriction.version_id,
1197                duo_id: restriction.duo_id.clone(),
1198                qualifier_ontology_namespace: restriction.qualifier_ontology_namespace.clone(),
1199                qualifier_ontology_id: restriction.qualifier_ontology_id.clone(),
1200                qualifier_text: restriction.qualifier_text.clone(),
1201                qualifier_date: restriction.qualifier_date,
1202                qualifier_integer: restriction.qualifier_integer,
1203            },
1204        );
1205        self.with_session_connection(
1206            "save asset version DUO restriction",
1207            |connection| connection.create_asset_version_duo_restriction(input.clone()),
1208            |connection| oauth_create_asset_version_duo_restriction(connection, input.clone()),
1209        )?;
1210        Ok(restriction.clone())
1211    }
1212
1213    async fn replace_asset_version_duo_restrictions(
1214        &self,
1215        version_id: VersionId,
1216        restrictions: &[NewAssetVersionDuoRestrictionRecord],
1217    ) -> Result<Vec<AssetVersionDuoRestrictionRecord>, CoreError> {
1218        let restrictions = self.with_session_connection(
1219            "replace asset version DUO restrictions",
1220            |connection| {
1221                replace_asset_version_duo_restrictions_with_executor(
1222                    connection,
1223                    version_id,
1224                    restrictions,
1225                )?;
1226                connection.list_asset_version_duo_restrictions(version_id)
1227            },
1228            |connection| {
1229                replace_asset_version_duo_restrictions_with_executor(
1230                    connection,
1231                    version_id,
1232                    restrictions,
1233                )?;
1234                oauth_list_asset_version_duo_restrictions(connection, version_id)
1235            },
1236        )?;
1237        restrictions
1238            .into_iter()
1239            .map(asset_version_duo_restriction_record)
1240            .collect()
1241    }
1242
1243    async fn clear_asset_version_duo_restrictions(
1244        &self,
1245        version_id: VersionId,
1246    ) -> Result<Vec<AssetVersionDuoRestrictionRecord>, CoreError> {
1247        let restrictions = self.with_session_connection(
1248            "clear asset version DUO restrictions",
1249            |connection| {
1250                clear_asset_version_duo_restrictions_with_executor(connection, version_id)?;
1251                connection.list_asset_version_duo_restrictions(version_id)
1252            },
1253            |connection| {
1254                clear_asset_version_duo_restrictions_with_executor(connection, version_id)?;
1255                oauth_list_asset_version_duo_restrictions(connection, version_id)
1256            },
1257        )?;
1258        restrictions
1259            .into_iter()
1260            .map(asset_version_duo_restriction_record)
1261            .collect()
1262    }
1263
1264    async fn list_datasets_for_asset(
1265        &self,
1266        asset_id: AssetId,
1267    ) -> Result<Vec<DatasetRecord>, CoreError> {
1268        self.with_session_connection(
1269            "list datasets for asset",
1270            |connection| connection.list_datasets_for_asset(asset_id),
1271            |connection| oauth_list_datasets_for_asset(connection, asset_id),
1272        )?
1273        .into_iter()
1274        .map(dataset_record)
1275        .collect()
1276    }
1277
1278    async fn search_datasets(
1279        &self,
1280        query: &DatasetSearchQuery,
1281    ) -> Result<SearchPage<DatasetSearchRecord, DatasetSearchCursorAnchor>, CoreError> {
1282        let studies = <Self as StudyRepository>::list_studies(self).await?;
1283        let mut matched = Vec::new();
1284        for study in studies {
1285            let domains =
1286                <Self as StudyDomainRepository>::list_domains_for_study(self, study.study_id)
1287                    .await?;
1288            for asset in <Self as AssetRepository>::list_assets_for_study(self, study.study_id)
1289                .await?
1290                .into_iter()
1291                .filter(|asset| asset.asset_type == AssetType::Dataset)
1292            {
1293                let versions =
1294                    <Self as AssetRepository>::list_asset_versions(self, asset.asset_id).await?;
1295                let dataset_ids =
1296                    <Self as AssetRepository>::list_datasets_for_asset(self, asset.asset_id)
1297                        .await?
1298                        .into_iter()
1299                        .map(|dataset| dataset.dataset_id.0)
1300                        .collect::<Vec<_>>();
1301                let withdrawn_ids = <Self as AssetRepository>::list_dataset_version_withdrawals(
1302                    self,
1303                    asset.asset_id,
1304                )
1305                .await?
1306                .into_iter()
1307                .map(|withdrawal| withdrawal.dataset_id.0)
1308                .collect::<Vec<_>>();
1309                let Some(latest_version) =
1310                    latest_searchable_dataset_version(&versions, &dataset_ids, &withdrawn_ids)
1311                else {
1312                    continue;
1313                };
1314                let tags = <Self as TagRepository>::list_tags_for_target(
1315                    self,
1316                    TagAttachmentTarget::AssetVersion(latest_version.version_id),
1317                )
1318                .await?;
1319                if dataset_matches_search_query(
1320                    &study,
1321                    &asset,
1322                    &latest_version,
1323                    &domains,
1324                    &tags,
1325                    query,
1326                ) {
1327                    matched.push(DatasetSearchRecord {
1328                        study_id: study.study_id,
1329                        asset,
1330                        latest_version,
1331                    });
1332                }
1333            }
1334        }
1335
1336        matched.sort_by(dataset_search_sort_key);
1337        if let Some(after) = query.after.as_ref() {
1338            matched.retain(|dataset| dataset_sort_tuple(dataset) > dataset_cursor_tuple(after));
1339        }
1340
1341        let limit = usize::from(query.limit);
1342        let has_more = matched.len() > limit;
1343        if has_more {
1344            matched.truncate(limit);
1345        }
1346        let next_cursor = if has_more {
1347            matched.last().map(|dataset| DatasetSearchCursorAnchor {
1348                name: dataset.asset.name.as_str().to_string(),
1349                study_id: dataset.study_id,
1350                asset_id: dataset.asset.asset_id,
1351            })
1352        } else {
1353            None
1354        };
1355
1356        Ok(SearchPage {
1357            items: matched,
1358            next_cursor,
1359        })
1360    }
1361
1362    async fn search_datafiles(
1363        &self,
1364        query: &DataFileSearchQuery,
1365    ) -> Result<SearchPage<DataFileSearchRecord, DataFileSearchCursorAnchor>, CoreError> {
1366        let studies = <Self as StudyRepository>::list_studies(self).await?;
1367        let mut matched = Vec::new();
1368        for study in studies {
1369            for asset in <Self as AssetRepository>::list_assets_for_study(self, study.study_id)
1370                .await?
1371                .into_iter()
1372                .filter(|asset| asset.asset_type == AssetType::File)
1373            {
1374                let versions =
1375                    <Self as AssetRepository>::list_asset_versions(self, asset.asset_id).await?;
1376                let datafiles =
1377                    <Self as AssetRepository>::list_datafiles_for_asset(self, asset.asset_id)
1378                        .await?;
1379                let Some((latest_version, datafile)) =
1380                    latest_searchable_datafile_version(&versions, &datafiles)
1381                else {
1382                    continue;
1383                };
1384                let tags = <Self as TagRepository>::list_tags_for_target(
1385                    self,
1386                    TagAttachmentTarget::AssetVersion(latest_version.version_id),
1387                )
1388                .await?;
1389                if datafile_matches_search_query(&study, &asset, &datafile, &tags, query) {
1390                    matched.push(DataFileSearchRecord {
1391                        study_id: study.study_id,
1392                        asset,
1393                        latest_version,
1394                        datafile,
1395                    });
1396                }
1397            }
1398        }
1399
1400        matched.sort_by(datafile_search_sort_key);
1401        if let Some(after) = query.after.as_ref() {
1402            matched.retain(|datafile| datafile_sort_tuple(datafile) > datafile_cursor_tuple(after));
1403        }
1404
1405        let limit = usize::from(query.limit);
1406        let has_more = matched.len() > limit;
1407        if has_more {
1408            matched.truncate(limit);
1409        }
1410        let next_cursor = if has_more {
1411            matched.last().map(|datafile| DataFileSearchCursorAnchor {
1412                name: datafile.asset.name.as_str().to_string(),
1413                study_id: datafile.study_id,
1414                asset_id: datafile.asset.asset_id,
1415            })
1416        } else {
1417            None
1418        };
1419
1420        Ok(SearchPage {
1421            items: matched,
1422            next_cursor,
1423        })
1424    }
1425
1426    async fn get_dataset(&self, dataset_id: VersionId) -> Result<Option<DatasetRecord>, CoreError> {
1427        self.with_session_connection(
1428            "get dataset",
1429            |connection| connection.get_dataset(dataset_id),
1430            |connection| oauth_get_dataset(connection, dataset_id),
1431        )?
1432        .map(dataset_record)
1433        .transpose()
1434    }
1435
1436    async fn save_dataset(&self, dataset: &DatasetRecord) -> Result<DatasetRecord, CoreError> {
1437        self.with_session_connection(
1438            "create dataset",
1439            |connection| connection.create_dataset(dataset.dataset_id),
1440            |connection| oauth_create_dataset(connection, dataset.dataset_id),
1441        )?;
1442
1443        self.with_session_connection(
1444            "read created dataset",
1445            |connection| connection.get_dataset(dataset.dataset_id),
1446            |connection| oauth_get_dataset(connection, dataset.dataset_id),
1447        )?
1448        .map(dataset_record)
1449        .transpose()?
1450        .ok_or_else(|| {
1451            CoreError::NotFound(format!(
1452                "created dataset {:?} was not readable",
1453                dataset.dataset_id
1454            ))
1455        })
1456    }
1457
1458    async fn get_dataset_version_withdrawal(
1459        &self,
1460        dataset_id: VersionId,
1461    ) -> Result<Option<DatasetVersionWithdrawalRecord>, CoreError> {
1462        self.with_session_connection(
1463            "get dataset version withdrawal",
1464            |connection| {
1465                if !pg_dataset_version_withdrawals_table_exists(connection)? {
1466                    return Ok(None);
1467                }
1468
1469                let row = connection
1470                    .client()
1471                    .query_opt(
1472                        "
1473                        SELECT dataset_id, asset_id, withdrawn_at, withdrawn_by, reason,
1474                               tombstone_state, provenance, drop_lake_table
1475                          FROM public.dataset_version_withdrawals
1476                         WHERE dataset_id = $1
1477                         LIMIT 1
1478                        ",
1479                        &[&dataset_id.0],
1480                    )
1481                    .map_err(PgMetaError::Query)?;
1482                Ok(row.map(|row| DatasetVersionWithdrawalRecord {
1483                    dataset_id: VersionId(row.get("dataset_id")),
1484                    asset_id: AssetId(row.get("asset_id")),
1485                    withdrawn_at: Some(DateTime::from_naive_utc_and_offset(
1486                        row.get::<_, NaiveDateTime>("withdrawn_at"),
1487                        Utc,
1488                    )),
1489                    withdrawn_by: row.get("withdrawn_by"),
1490                    reason: row.get("reason"),
1491                    tombstone_state: row.get("tombstone_state"),
1492                    provenance: row.get("provenance"),
1493                    drop_lake_table: row.get("drop_lake_table"),
1494                }))
1495            },
1496            |connection| oauth_get_dataset_version_withdrawal(connection, dataset_id),
1497        )
1498    }
1499
1500    async fn list_dataset_version_withdrawals(
1501        &self,
1502        asset_id: AssetId,
1503    ) -> Result<Vec<DatasetVersionWithdrawalRecord>, CoreError> {
1504        self.with_session_connection(
1505            "list dataset version withdrawals",
1506            |connection| {
1507                if !pg_dataset_version_withdrawals_table_exists(connection)? {
1508                    return Ok(Vec::new());
1509                }
1510
1511                let rows = connection
1512                    .client()
1513                    .query(
1514                        "
1515                        SELECT dataset_id, asset_id, withdrawn_at, withdrawn_by, reason,
1516                               tombstone_state, provenance, drop_lake_table
1517                          FROM public.dataset_version_withdrawals
1518                         WHERE asset_id = $1
1519                         ORDER BY withdrawn_at, dataset_id
1520                        ",
1521                        &[&asset_id.0],
1522                    )
1523                    .map_err(PgMetaError::Query)?;
1524                Ok(rows
1525                    .into_iter()
1526                    .map(|row| DatasetVersionWithdrawalRecord {
1527                        dataset_id: VersionId(row.get("dataset_id")),
1528                        asset_id: AssetId(row.get("asset_id")),
1529                        withdrawn_at: Some(DateTime::from_naive_utc_and_offset(
1530                            row.get::<_, NaiveDateTime>("withdrawn_at"),
1531                            Utc,
1532                        )),
1533                        withdrawn_by: row.get("withdrawn_by"),
1534                        reason: row.get("reason"),
1535                        tombstone_state: row.get("tombstone_state"),
1536                        provenance: row.get("provenance"),
1537                        drop_lake_table: row.get("drop_lake_table"),
1538                    })
1539                    .collect())
1540            },
1541            |connection| oauth_list_dataset_version_withdrawals(connection, asset_id),
1542        )
1543    }
1544
1545    async fn save_dataset_version_withdrawal(
1546        &self,
1547        withdrawal: &NewDatasetVersionWithdrawalRecord,
1548    ) -> Result<DatasetVersionWithdrawalRecord, CoreError> {
1549        self.with_session_connection(
1550            "save dataset version withdrawal",
1551            |connection| {
1552                connection
1553                    .client()
1554                    .execute(
1555                        "
1556                        INSERT INTO public.dataset_version_withdrawals (
1557                            dataset_id, asset_id, withdrawn_by, reason, tombstone_state,
1558                            provenance, drop_lake_table
1559                        )
1560                        VALUES ($1, $2, COALESCE($3, CURRENT_USER), $4, $5, $6, $7)
1561                        ON CONFLICT (dataset_id) DO NOTHING
1562                        ",
1563                        &[
1564                            &withdrawal.dataset_id.0,
1565                            &withdrawal.asset_id.0,
1566                            &withdrawal.withdrawn_by,
1567                            &withdrawal.reason,
1568                            &withdrawal.tombstone_state,
1569                            &withdrawal.provenance,
1570                            &withdrawal.drop_lake_table,
1571                        ],
1572                    )
1573                    .map_err(|source| PgMetaError::Write {
1574                        operation: "save dataset version withdrawal",
1575                        source,
1576                    })?;
1577                Ok(())
1578            },
1579            |connection| oauth_save_dataset_version_withdrawal(connection, withdrawal),
1580        )?;
1581
1582        self.get_dataset_version_withdrawal(withdrawal.dataset_id)
1583            .await?
1584            .ok_or_else(|| {
1585                CoreError::NotFound(format!(
1586                    "dataset version withdrawal was not readable: {}",
1587                    withdrawal.dataset_id.0
1588                ))
1589            })
1590    }
1591
1592    async fn deactivate_dataset_version_links(
1593        &self,
1594        dataset_id: VersionId,
1595    ) -> Result<(), CoreError> {
1596        self.with_session_connection(
1597            "deactivate dataset version links",
1598            |connection| {
1599                let mut transaction = connection.client().transaction().map_err(PgMetaError::Transaction)?;
1600                for (operation, sql) in [
1601                    (
1602                        "delete dataset version relation links",
1603                        "DELETE FROM public.dataset_version_relation_instances WHERE version_id = $1",
1604                    ),
1605                    (
1606                        "delete dataset version entity links",
1607                        "DELETE FROM public.dataset_version_entities WHERE version_id = $1",
1608                    ),
1609                    (
1610                        "delete asset version relation links",
1611                        "DELETE FROM public.asset_version_relation_instances WHERE version_id = $1",
1612                    ),
1613                    (
1614                        "delete asset version entity links",
1615                        "DELETE FROM public.asset_version_entities WHERE version_id = $1",
1616                    ),
1617                    (
1618                        "delete dataset variable links",
1619                        "DELETE FROM public.dataset_variables WHERE dataset_id = $1",
1620                    ),
1621                ] {
1622                    transaction.execute(sql, &[&dataset_id.0]).map_err(|source| PgMetaError::Write {
1623                        operation,
1624                        source,
1625                    })?;
1626                }
1627                transaction.commit().map_err(PgMetaError::Transaction)?;
1628                Ok(())
1629            },
1630            |connection| oauth_deactivate_dataset_version_links(connection, dataset_id),
1631        )
1632    }
1633
1634    async fn clear_asset_version_latest(
1635        &self,
1636        asset_id: AssetId,
1637        version_id: VersionId,
1638    ) -> Result<AssetVersionRecord, CoreError> {
1639        self.with_session_connection(
1640            "clear asset version latest",
1641            |connection| {
1642                connection
1643                    .client()
1644                    .query_one(
1645                        "UPDATE public.asset_versions SET is_latest = FALSE WHERE asset_id = $1 AND version_id = $2 RETURNING version_id",
1646                        &[&asset_id.0, &version_id.0],
1647                    )
1648                    .map_err(|source| PgMetaError::Write {
1649                        operation: "clear asset version latest",
1650                        source,
1651                    })?;
1652                Ok(())
1653            },
1654            |connection| oauth_clear_asset_version_latest(connection, asset_id, version_id),
1655        )?;
1656
1657        self.get_asset_version(version_id).await?.ok_or_else(|| {
1658            CoreError::NotFound(format!(
1659                "cleared asset version {:?} was not readable",
1660                version_id
1661            ))
1662        })
1663    }
1664
1665    async fn list_datafiles_for_asset(
1666        &self,
1667        asset_id: AssetId,
1668    ) -> Result<Vec<DataFileRecord>, CoreError> {
1669        self.with_session_connection(
1670            "list datafiles for asset",
1671            |connection| connection.list_datafiles_for_asset(asset_id),
1672            |connection| oauth_list_datafiles_for_asset(connection, asset_id),
1673        )?
1674        .into_iter()
1675        .map(datafile_record)
1676        .collect()
1677    }
1678
1679    async fn get_datafile(
1680        &self,
1681        datafile_id: VersionId,
1682    ) -> Result<Option<DataFileRecord>, CoreError> {
1683        self.with_session_connection(
1684            "get datafile",
1685            |connection| connection.get_datafile(datafile_id),
1686            |connection| oauth_get_datafile(connection, datafile_id),
1687        )?
1688        .map(datafile_record)
1689        .transpose()
1690    }
1691
1692    async fn save_datafile(&self, datafile: &DataFileRecord) -> Result<DataFileRecord, CoreError> {
1693        ahri_tre_core::validate_datafile_record(datafile)?;
1694        let input = PgCreateDataFile {
1695            datafile_id: datafile.datafile_id,
1696            size_bytes: datafile
1697                .size_bytes
1698                .map(i64::try_from)
1699                .transpose()
1700                .map_err(|_| {
1701                    CoreError::Validation("datafile size exceeds PostgreSQL BIGINT".to_string())
1702                })?,
1703            compressed: datafile.compressed,
1704            encrypted: datafile.encrypted,
1705            compression_algorithm: datafile.compression_algorithm.clone(),
1706            encryption_algorithm: datafile.encryption_algorithm.clone(),
1707            encryption_key: datafile.encryption_key.clone(),
1708            storage_uri: datafile.storage_uri.clone(),
1709            edam_format: datafile.edam_format.clone(),
1710            digest: datafile.digest.as_str().to_string(),
1711        };
1712        self.with_session_connection(
1713            "create datafile",
1714            |connection| connection.create_datafile(input.clone()),
1715            |connection| oauth_create_datafile(connection, input.clone()),
1716        )?;
1717
1718        self.with_session_connection(
1719            "read created datafile",
1720            |connection| connection.get_datafile(datafile.datafile_id),
1721            |connection| oauth_get_datafile(connection, datafile.datafile_id),
1722        )?
1723        .map(datafile_record)
1724        .transpose()?
1725        .ok_or_else(|| {
1726            CoreError::NotFound(format!(
1727                "created datafile {:?} was not readable",
1728                datafile.datafile_id
1729            ))
1730        })
1731    }
1732
1733    async fn delete_datafile_record(&self, datafile_id: VersionId) -> Result<bool, CoreError> {
1734        self.with_session_connection(
1735            "delete datafile record",
1736            |connection| delete_datafile_metadata_record(connection, datafile_id),
1737            |connection| delete_datafile_metadata_record(connection, datafile_id),
1738        )
1739    }
1740}
1741
1742#[async_trait::async_trait]
1743impl VariableRepository for PgMetadataRepository<'_> {
1744    async fn list_value_types(&self) -> Result<Vec<ValueTypeRecord>, CoreError> {
1745        self.with_session_connection(
1746            "list value types",
1747            |connection| connection.list_value_types(),
1748            oauth_list_value_types,
1749        )?
1750        .into_iter()
1751        .map(value_type_record)
1752        .collect()
1753    }
1754
1755    async fn list_domain_variables(
1756        &self,
1757        domain_id: DomainId,
1758    ) -> Result<Vec<VariableRecord>, CoreError> {
1759        self.with_session_connection(
1760            "list domain variables",
1761            |connection| connection.list_variables_for_domain(domain_id),
1762            |connection| oauth_list_variables_for_domain(connection, domain_id),
1763        )?
1764        .into_iter()
1765        .map(variable_record)
1766        .collect()
1767    }
1768
1769    async fn get_variable(
1770        &self,
1771        variable_id: VariableId,
1772    ) -> Result<Option<VariableRecord>, CoreError> {
1773        self.with_session_connection(
1774            "get variable",
1775            |connection| connection.get_variable(variable_id),
1776            |connection| oauth_get_variable(connection, variable_id),
1777        )?
1778        .map(variable_record)
1779        .transpose()
1780    }
1781
1782    async fn create_variable(
1783        &self,
1784        variable: &NewVariableRecord,
1785    ) -> Result<VariableRecord, CoreError> {
1786        let input = PgCreateVariable {
1787            variable_id: None,
1788            domain_id: variable.domain_id,
1789            name: variable.name.as_str().to_string(),
1790            value_type_id: variable.value_type_id,
1791            value_format: variable.value_format.clone(),
1792            vocabulary_id: variable.vocabulary_id,
1793            key_role: variable.key_role,
1794            description: variable.description.clone(),
1795            note: variable.note.clone(),
1796            ontology_namespace: variable.ontology_namespace.clone(),
1797            ontology_class: variable.ontology_class.clone(),
1798        };
1799        let variable_id = self.with_session_connection(
1800            "create variable",
1801            |connection| connection.create_variable(input.clone()),
1802            |connection| oauth_create_variable(connection, input.clone()),
1803        )?;
1804
1805        self.with_session_connection(
1806            "read created variable",
1807            |connection| connection.list_variables_for_domain(variable.domain_id),
1808            |connection| oauth_list_variables_for_domain(connection, variable.domain_id),
1809        )?
1810        .into_iter()
1811        .find(|read_model| read_model.variable_id == variable_id)
1812        .map(variable_record)
1813        .transpose()?
1814        .ok_or_else(|| {
1815            CoreError::NotFound(format!(
1816                "created variable {:?} was not readable",
1817                variable_id
1818            ))
1819        })
1820    }
1821
1822    async fn save_variable(&self, variable: &VariableRecord) -> Result<VariableRecord, CoreError> {
1823        let input = PgCreateVariable {
1824            variable_id: Some(variable.variable_id),
1825            domain_id: variable.domain_id,
1826            name: variable.name.as_str().to_string(),
1827            value_type_id: variable.value_type_id,
1828            value_format: variable.value_format.clone(),
1829            vocabulary_id: variable.vocabulary_id,
1830            key_role: variable.key_role,
1831            description: variable.description.clone(),
1832            note: variable.note.clone(),
1833            ontology_namespace: variable.ontology_namespace.clone(),
1834            ontology_class: variable.ontology_class.clone(),
1835        };
1836        let variable_id = self.with_session_connection(
1837            "create variable",
1838            |connection| connection.create_variable(input.clone()),
1839            |connection| oauth_create_variable(connection, input.clone()),
1840        )?;
1841
1842        self.with_session_connection(
1843            "read created variable",
1844            |connection| connection.list_variables_for_domain(variable.domain_id),
1845            |connection| oauth_list_variables_for_domain(connection, variable.domain_id),
1846        )?
1847        .into_iter()
1848        .find(|read_model| read_model.variable_id == variable_id)
1849        .map(variable_record)
1850        .transpose()?
1851        .ok_or_else(|| {
1852            CoreError::NotFound(format!(
1853                "created variable {:?} was not readable",
1854                variable_id
1855            ))
1856        })
1857    }
1858
1859    async fn update_variable_metadata(
1860        &self,
1861        variable: &VariableRecord,
1862    ) -> Result<VariableRecord, CoreError> {
1863        let input = PgCreateVariable {
1864            variable_id: Some(variable.variable_id),
1865            domain_id: variable.domain_id,
1866            name: variable.name.as_str().to_string(),
1867            value_type_id: variable.value_type_id,
1868            value_format: variable.value_format.clone(),
1869            vocabulary_id: variable.vocabulary_id,
1870            key_role: variable.key_role,
1871            description: variable.description.clone(),
1872            note: variable.note.clone(),
1873            ontology_namespace: variable.ontology_namespace.clone(),
1874            ontology_class: variable.ontology_class.clone(),
1875        };
1876        self.with_session_connection(
1877            "update variable",
1878            |connection| connection.update_variable(input.clone()),
1879            |connection| oauth_update_variable(connection, input.clone()),
1880        )?;
1881
1882        self.with_session_connection(
1883            "read updated variable",
1884            |connection| connection.list_variables_for_domain(variable.domain_id),
1885            |connection| oauth_list_variables_for_domain(connection, variable.domain_id),
1886        )?
1887        .into_iter()
1888        .find(|read_model| read_model.variable_id == variable.variable_id)
1889        .map(variable_record)
1890        .transpose()?
1891        .ok_or_else(|| {
1892            CoreError::NotFound(format!(
1893                "updated variable {:?} was not readable",
1894                variable.variable_id
1895            ))
1896        })
1897    }
1898
1899    async fn link_dataset_variable(
1900        &self,
1901        dataset_id: VersionId,
1902        variable_id: VariableId,
1903        row_role: KeyRole,
1904    ) -> Result<ahri_tre_types::DatasetVariableLinkRecord, CoreError> {
1905        self.with_session_connection(
1906            "link dataset variable",
1907            |connection| connection.link_dataset_variable(dataset_id, variable_id, row_role),
1908            |connection| oauth_link_dataset_variable(connection, dataset_id, variable_id, row_role),
1909        )?;
1910        Ok(ahri_tre_types::DatasetVariableLinkRecord {
1911            dataset_id,
1912            variable_id,
1913            row_role,
1914        })
1915    }
1916
1917    async fn list_dataset_variables(
1918        &self,
1919        dataset_id: VersionId,
1920    ) -> Result<Vec<ahri_tre_types::DatasetVariableLinkRecord>, CoreError> {
1921        Ok(self
1922            .with_session_connection(
1923                "list dataset variables",
1924                |connection| connection.list_dataset_variables(dataset_id),
1925                |connection| oauth_list_dataset_variables(connection, dataset_id),
1926            )?
1927            .into_iter()
1928            .map(dataset_variable_link_record)
1929            .collect())
1930    }
1931
1932    async fn list_dataset_variables_for_variable(
1933        &self,
1934        variable_id: VariableId,
1935    ) -> Result<Vec<ahri_tre_types::DatasetVariableLinkRecord>, CoreError> {
1936        Ok(self
1937            .with_session_connection(
1938                "list dataset variables for variable",
1939                |connection| connection.list_dataset_variables_for_variable(variable_id),
1940                |connection| oauth_list_dataset_variables_for_variable(connection, variable_id),
1941            )?
1942            .into_iter()
1943            .map(dataset_variable_link_record)
1944            .collect())
1945    }
1946
1947    async fn delete_variable_record(&self, variable_id: VariableId) -> Result<bool, CoreError> {
1948        self.with_session_connection(
1949            "delete variable record",
1950            |connection| {
1951                delete_semantic_record(connection, "variables", "variable_id", variable_id.0)
1952            },
1953            |connection| {
1954                delete_semantic_record(connection, "variables", "variable_id", variable_id.0)
1955            },
1956        )
1957    }
1958
1959    async fn search_variables(
1960        &self,
1961        query: &VariableSearchQuery,
1962    ) -> Result<SearchPage<VariableSearchRecord, VariableSearchCursorAnchor>, CoreError> {
1963        let domains = self.list_domains().await?;
1964        let value_types = self.list_value_types().await?;
1965        let mut matched = Vec::new();
1966        for domain in domains {
1967            let vocabularies = self.list_vocabularies(domain.domain_id).await?;
1968            for variable in self.list_domain_variables(domain.domain_id).await? {
1969                let value_type = value_types
1970                    .iter()
1971                    .find(|value_type| value_type.value_type_id == variable.value_type_id);
1972                let vocabulary = variable.vocabulary_id.and_then(|vocabulary_id| {
1973                    vocabularies
1974                        .iter()
1975                        .find(|vocabulary| vocabulary.vocabulary_id == vocabulary_id)
1976                });
1977                let tags = self
1978                    .list_tags_for_target(TagAttachmentTarget::Variable(variable.variable_id))
1979                    .await?;
1980                if variable_matches_search_query(
1981                    query, &domain, &variable, value_type, vocabulary, &tags,
1982                ) {
1983                    matched.push(VariableSearchRecord { variable });
1984                }
1985            }
1986        }
1987
1988        matched.sort_by(variable_search_sort_key);
1989        let start = query
1990            .after
1991            .as_ref()
1992            .and_then(|anchor| {
1993                matched.iter().position(|variable| {
1994                    variable_sort_tuple(variable) > variable_cursor_tuple(anchor)
1995                })
1996            })
1997            .unwrap_or(0);
1998        let limit = usize::from(query.limit);
1999        let mut items = matched
2000            .into_iter()
2001            .skip(start)
2002            .take(limit + 1)
2003            .collect::<Vec<_>>();
2004        let next_cursor = if items.len() > limit {
2005            items.truncate(limit);
2006            items.last().map(|variable| VariableSearchCursorAnchor {
2007                name: variable.variable.name.as_str().to_string(),
2008                domain_id: variable.variable.domain_id,
2009                variable_id: variable.variable.variable_id,
2010            })
2011        } else {
2012            None
2013        };
2014        Ok(SearchPage { items, next_cursor })
2015    }
2016}
2017
2018#[async_trait::async_trait]
2019impl VocabularyRepository for PgMetadataRepository<'_> {
2020    async fn list_vocabularies(
2021        &self,
2022        domain_id: DomainId,
2023    ) -> Result<Vec<VocabularyRecord>, CoreError> {
2024        self.with_session_connection(
2025            "list vocabularies",
2026            |connection| connection.list_vocabularies_for_domain(domain_id),
2027            |connection| oauth_list_vocabularies_for_domain(connection, domain_id),
2028        )?
2029        .into_iter()
2030        .map(vocabulary_record)
2031        .collect()
2032    }
2033
2034    async fn get_vocabulary(
2035        &self,
2036        vocabulary_id: VocabularyId,
2037    ) -> Result<Option<VocabularyRecord>, CoreError> {
2038        self.with_session_connection(
2039            "get vocabulary",
2040            |connection| connection.get_vocabulary(vocabulary_id),
2041            |connection| oauth_get_vocabulary(connection, vocabulary_id),
2042        )?
2043        .map(vocabulary_record)
2044        .transpose()
2045    }
2046
2047    async fn list_vocabulary_items(
2048        &self,
2049        vocabulary_id: VocabularyId,
2050    ) -> Result<Vec<VocabularyItemRecord>, CoreError> {
2051        Ok(self
2052            .with_session_connection(
2053                "list vocabulary items",
2054                |connection| connection.list_vocabulary_items(vocabulary_id),
2055                |connection| oauth_list_vocabulary_items(connection, vocabulary_id),
2056            )?
2057            .into_iter()
2058            .map(vocabulary_item_record)
2059            .collect())
2060    }
2061
2062    async fn get_vocabulary_item(
2063        &self,
2064        vocabulary_item_id: VocabularyItemId,
2065    ) -> Result<Option<VocabularyItemRecord>, CoreError> {
2066        Ok(self
2067            .with_session_connection(
2068                "get vocabulary item",
2069                |connection| connection.get_vocabulary_item(vocabulary_item_id),
2070                |connection| oauth_get_vocabulary_item(connection, vocabulary_item_id),
2071            )?
2072            .map(vocabulary_item_record))
2073    }
2074
2075    async fn create_vocabulary(
2076        &self,
2077        vocabulary: &NewVocabularyRecord,
2078    ) -> Result<VocabularyRecord, CoreError> {
2079        let input = PgCreateVocabulary {
2080            vocabulary_id: None,
2081            domain_id: vocabulary.domain_id,
2082            name: vocabulary.name.as_str().to_string(),
2083            description: vocabulary.description.clone(),
2084        };
2085        let vocabulary_id = self.with_session_connection(
2086            "create vocabulary",
2087            |connection| connection.create_vocabulary(input.clone()),
2088            |connection| oauth_create_vocabulary(connection, input.clone()),
2089        )?;
2090
2091        self.with_session_connection(
2092            "read created vocabulary",
2093            |connection| connection.list_vocabularies_for_domain(vocabulary.domain_id),
2094            |connection| oauth_list_vocabularies_for_domain(connection, vocabulary.domain_id),
2095        )?
2096        .into_iter()
2097        .find(|read_model| read_model.vocabulary_id == vocabulary_id)
2098        .map(vocabulary_record)
2099        .transpose()?
2100        .ok_or_else(|| {
2101            CoreError::NotFound(format!(
2102                "created vocabulary {:?} was not readable",
2103                vocabulary_id
2104            ))
2105        })
2106    }
2107
2108    async fn save_vocabulary(
2109        &self,
2110        vocabulary: &VocabularyRecord,
2111    ) -> Result<VocabularyRecord, CoreError> {
2112        let input = PgCreateVocabulary {
2113            vocabulary_id: Some(vocabulary.vocabulary_id),
2114            domain_id: vocabulary.domain_id,
2115            name: vocabulary.name.as_str().to_string(),
2116            description: vocabulary.description.clone(),
2117        };
2118        let vocabulary_id = self.with_session_connection(
2119            "create vocabulary",
2120            |connection| connection.create_vocabulary(input.clone()),
2121            |connection| oauth_create_vocabulary(connection, input.clone()),
2122        )?;
2123
2124        self.with_session_connection(
2125            "read created vocabulary",
2126            |connection| connection.list_vocabularies_for_domain(vocabulary.domain_id),
2127            |connection| oauth_list_vocabularies_for_domain(connection, vocabulary.domain_id),
2128        )?
2129        .into_iter()
2130        .find(|read_model| read_model.vocabulary_id == vocabulary_id)
2131        .map(vocabulary_record)
2132        .transpose()?
2133        .ok_or_else(|| {
2134            CoreError::NotFound(format!(
2135                "created vocabulary {:?} was not readable",
2136                vocabulary_id
2137            ))
2138        })
2139    }
2140
2141    async fn update_vocabulary(
2142        &self,
2143        vocabulary: &VocabularyRecord,
2144    ) -> Result<VocabularyRecord, CoreError> {
2145        let input = PgCreateVocabulary {
2146            vocabulary_id: Some(vocabulary.vocabulary_id),
2147            domain_id: vocabulary.domain_id,
2148            name: vocabulary.name.as_str().to_string(),
2149            description: vocabulary.description.clone(),
2150        };
2151        self.with_session_connection(
2152            "update vocabulary",
2153            |connection| connection.update_vocabulary(input.clone()),
2154            |connection| oauth_update_vocabulary(connection, input.clone()),
2155        )?;
2156
2157        self.with_session_connection(
2158            "read updated vocabulary",
2159            |connection| connection.list_vocabularies_for_domain(vocabulary.domain_id),
2160            |connection| oauth_list_vocabularies_for_domain(connection, vocabulary.domain_id),
2161        )?
2162        .into_iter()
2163        .find(|read_model| read_model.vocabulary_id == vocabulary.vocabulary_id)
2164        .map(vocabulary_record)
2165        .transpose()?
2166        .ok_or_else(|| {
2167            CoreError::NotFound(format!(
2168                "updated vocabulary {:?} was not readable",
2169                vocabulary.vocabulary_id
2170            ))
2171        })
2172    }
2173
2174    async fn create_vocabulary_items(
2175        &self,
2176        vocabulary_id: VocabularyId,
2177        items: &[NewVocabularyItemRecord],
2178    ) -> Result<Vec<VocabularyItemRecord>, CoreError> {
2179        self.with_session_connection(
2180            "create vocabulary items",
2181            |connection| {
2182                let mut created = Vec::with_capacity(items.len());
2183                for item in items {
2184                    let vocabulary_item_id =
2185                        connection.create_vocabulary_item(PgCreateVocabularyItem {
2186                            vocabulary_item_id: None,
2187                            vocabulary_id,
2188                            value: item.value,
2189                            code: item.code.clone(),
2190                            description: item.description.clone(),
2191                        })?;
2192                    created.push(VocabularyItemRecord {
2193                        vocabulary_item_id,
2194                        vocabulary_id,
2195                        value: item.value,
2196                        code: item.code.clone(),
2197                        description: item.description.clone(),
2198                    });
2199                }
2200                Ok(created)
2201            },
2202            |connection| oauth_create_vocabulary_items(connection, vocabulary_id, items),
2203        )
2204    }
2205
2206    async fn save_vocabulary_items(&self, items: &[VocabularyItemRecord]) -> Result<(), CoreError> {
2207        self.with_session_connection(
2208            "create vocabulary items",
2209            |connection| {
2210                for item in items {
2211                    connection.create_vocabulary_item(PgCreateVocabularyItem {
2212                        vocabulary_item_id: Some(item.vocabulary_item_id),
2213                        vocabulary_id: item.vocabulary_id,
2214                        value: item.value,
2215                        code: item.code.clone(),
2216                        description: item.description.clone(),
2217                    })?;
2218                }
2219                Ok(())
2220            },
2221            |connection| oauth_save_vocabulary_items(connection, items),
2222        )
2223    }
2224
2225    async fn update_vocabulary_items(
2226        &self,
2227        items: &[VocabularyItemRecord],
2228    ) -> Result<(), CoreError> {
2229        self.with_session_connection(
2230            "update vocabulary items",
2231            |connection| {
2232                for item in items {
2233                    connection.update_vocabulary_item(PgCreateVocabularyItem {
2234                        vocabulary_item_id: Some(item.vocabulary_item_id),
2235                        vocabulary_id: item.vocabulary_id,
2236                        value: item.value,
2237                        code: item.code.clone(),
2238                        description: item.description.clone(),
2239                    })?;
2240                }
2241                Ok(())
2242            },
2243            |connection| oauth_update_vocabulary_items(connection, items),
2244        )
2245    }
2246
2247    async fn list_vocabulary_mappings(&self) -> Result<Vec<VocabularyMappingRecord>, CoreError> {
2248        Ok(self
2249            .with_session_connection(
2250                "list vocabulary mappings",
2251                |connection| connection.list_vocabulary_mappings(),
2252                oauth_list_vocabulary_mappings,
2253            )?
2254            .into_iter()
2255            .map(vocabulary_mapping_record)
2256            .collect())
2257    }
2258
2259    async fn save_vocabulary_mapping(
2260        &self,
2261        mapping: &VocabularyMappingRecord,
2262    ) -> Result<VocabularyMappingRecord, CoreError> {
2263        let input = PgCreateVocabularyMapping {
2264            vocabulary_mapping_id: Some(mapping.vocabulary_mapping_id),
2265            from_vocabulary_item: mapping.from_vocabulary_item,
2266            to_vocabulary_item: mapping.to_vocabulary_item,
2267        };
2268        let mapping_id = self.with_session_connection(
2269            "create vocabulary mapping",
2270            |connection| connection.create_vocabulary_mapping(input.clone()),
2271            |connection| oauth_create_vocabulary_mapping(connection, input.clone()),
2272        )?;
2273        Ok(VocabularyMappingRecord {
2274            vocabulary_mapping_id: mapping_id,
2275            from_vocabulary_item: mapping.from_vocabulary_item,
2276            to_vocabulary_item: mapping.to_vocabulary_item,
2277        })
2278    }
2279
2280    async fn create_vocabulary_mapping(
2281        &self,
2282        mapping: &NewVocabularyMappingRecord,
2283    ) -> Result<VocabularyMappingRecord, CoreError> {
2284        let input = PgCreateVocabularyMapping {
2285            vocabulary_mapping_id: None,
2286            from_vocabulary_item: mapping.from_vocabulary_item,
2287            to_vocabulary_item: mapping.to_vocabulary_item,
2288        };
2289        let mapping_id = self.with_session_connection(
2290            "create vocabulary mapping",
2291            |connection| connection.create_vocabulary_mapping(input.clone()),
2292            |connection| oauth_create_vocabulary_mapping(connection, input.clone()),
2293        )?;
2294        Ok(VocabularyMappingRecord {
2295            vocabulary_mapping_id: mapping_id,
2296            from_vocabulary_item: mapping.from_vocabulary_item,
2297            to_vocabulary_item: mapping.to_vocabulary_item,
2298        })
2299    }
2300
2301    async fn delete_vocabulary_record(
2302        &self,
2303        vocabulary_id: VocabularyId,
2304    ) -> Result<bool, CoreError> {
2305        self.with_session_connection(
2306            "delete vocabulary record",
2307            |connection| {
2308                delete_semantic_record(connection, "vocabularies", "vocabulary_id", vocabulary_id.0)
2309            },
2310            |connection| {
2311                delete_semantic_record(connection, "vocabularies", "vocabulary_id", vocabulary_id.0)
2312            },
2313        )
2314    }
2315}
2316
2317#[async_trait::async_trait]
2318impl EntityRepository for PgMetadataRepository<'_> {
2319    async fn list_entities(&self, domain_id: DomainId) -> Result<Vec<EntityRecord>, CoreError> {
2320        self.with_session_connection(
2321            "list entities",
2322            |connection| connection.list_entities_for_domain(domain_id),
2323            |connection| oauth_list_entities_for_domain(connection, domain_id),
2324        )?
2325        .into_iter()
2326        .map(entity_record)
2327        .collect()
2328    }
2329
2330    async fn list_entity_relations(
2331        &self,
2332        domain_id: DomainId,
2333    ) -> Result<Vec<EntityRelationRecord>, CoreError> {
2334        self.with_session_connection(
2335            "list entity relations",
2336            |connection| connection.list_entity_relations_for_domain(domain_id),
2337            |connection| oauth_list_entity_relations_for_domain(connection, domain_id),
2338        )?
2339        .into_iter()
2340        .map(entity_relation_record)
2341        .collect()
2342    }
2343
2344    async fn search_entities(
2345        &self,
2346        query: &EntitySearchQuery,
2347    ) -> Result<SearchPage<EntitySearchRecord, EntitySearchCursorAnchor>, CoreError> {
2348        let domains = self.list_domains().await?;
2349        let mut matched = Vec::new();
2350        for domain in domains {
2351            for entity in self.list_entities(domain.domain_id).await? {
2352                let tags = self
2353                    .list_tags_for_target(TagAttachmentTarget::Entity(entity.entity_id))
2354                    .await?;
2355                if entity_matches_search_query(query, &domain, &entity, &tags) {
2356                    matched.push(EntitySearchRecord { entity });
2357                }
2358            }
2359        }
2360
2361        matched.sort_by(entity_search_sort_key);
2362        let start = query
2363            .after
2364            .as_ref()
2365            .and_then(|anchor| {
2366                matched
2367                    .iter()
2368                    .position(|entity| entity_sort_tuple(entity) > entity_cursor_tuple(anchor))
2369            })
2370            .unwrap_or(0);
2371        let limit = usize::from(query.limit);
2372        let mut items = matched
2373            .into_iter()
2374            .skip(start)
2375            .take(limit + 1)
2376            .collect::<Vec<_>>();
2377        let next_cursor = if items.len() > limit {
2378            items.truncate(limit);
2379            items.last().map(|entity| EntitySearchCursorAnchor {
2380                name: entity.entity.name.as_str().to_string(),
2381                domain_id: entity.entity.domain_id,
2382                entity_id: entity.entity.entity_id,
2383            })
2384        } else {
2385            None
2386        };
2387        Ok(SearchPage { items, next_cursor })
2388    }
2389
2390    async fn search_entity_relations(
2391        &self,
2392        query: &RelationSearchQuery,
2393    ) -> Result<SearchPage<RelationSearchRecord, RelationSearchCursorAnchor>, CoreError> {
2394        let domains = self.list_domains().await?;
2395        let mut matched = Vec::new();
2396        for domain in domains {
2397            let entities = self.list_entities(domain.domain_id).await?;
2398            let entities_by_id = entities
2399                .iter()
2400                .map(|entity| (entity.entity_id.0, entity.clone()))
2401                .collect::<BTreeMap<_, _>>();
2402            for relation in self.list_entity_relations(domain.domain_id).await? {
2403                let Some(source_entity) =
2404                    entities_by_id.get(&relation.subject_entity_id.0).cloned()
2405                else {
2406                    continue;
2407                };
2408                let Some(target_entity) = entities_by_id.get(&relation.object_entity_id.0).cloned()
2409                else {
2410                    continue;
2411                };
2412                let tags = self
2413                    .list_tags_for_target(TagAttachmentTarget::EntityRelation(
2414                        relation.entity_relation_id,
2415                    ))
2416                    .await?;
2417                if relation_matches_search_query(
2418                    query,
2419                    &domain,
2420                    &relation,
2421                    &source_entity,
2422                    &target_entity,
2423                    &tags,
2424                ) {
2425                    matched.push(RelationSearchRecord {
2426                        relation,
2427                        source_entity,
2428                        target_entity,
2429                    });
2430                }
2431            }
2432        }
2433
2434        matched.sort_by(relation_search_sort_key);
2435        let start = query
2436            .after
2437            .as_ref()
2438            .and_then(|anchor| {
2439                matched.iter().position(|relation| {
2440                    relation_sort_tuple(relation) > relation_cursor_tuple(anchor)
2441                })
2442            })
2443            .unwrap_or(0);
2444        let limit = usize::from(query.limit);
2445        let mut items = matched
2446            .into_iter()
2447            .skip(start)
2448            .take(limit + 1)
2449            .collect::<Vec<_>>();
2450        let next_cursor = if items.len() > limit {
2451            items.truncate(limit);
2452            items.last().map(|relation| RelationSearchCursorAnchor {
2453                name: relation.relation.name.as_str().to_string(),
2454                domain_id: relation.relation.domain_id,
2455                relation_id: relation.relation.entity_relation_id,
2456            })
2457        } else {
2458            None
2459        };
2460        Ok(SearchPage { items, next_cursor })
2461    }
2462
2463    async fn create_entity(&self, entity: &NewEntityRecord) -> Result<EntityRecord, CoreError> {
2464        let input = PgCreateEntity {
2465            entity_id: None,
2466            domain_id: entity.domain_id,
2467            uuid: None,
2468            name: entity.name.as_str().to_string(),
2469            description: entity.description.clone(),
2470            agent_instructions: entity.agent_instructions.clone(),
2471            ontology_namespace: entity.ontology_namespace.clone(),
2472            ontology_class: entity.ontology_class.clone(),
2473        };
2474        let entity_id = self.with_session_connection(
2475            "create entity",
2476            |connection| connection.create_entity(input.clone()),
2477            |connection| oauth_create_entity(connection, input.clone()),
2478        )?;
2479        self.with_session_connection(
2480            "read created entity",
2481            |connection| connection.list_entities_for_domain(entity.domain_id),
2482            |connection| oauth_list_entities_for_domain(connection, entity.domain_id),
2483        )?
2484        .into_iter()
2485        .find(|read_model| read_model.entity_id == entity_id)
2486        .map(entity_record)
2487        .transpose()?
2488        .ok_or_else(|| {
2489            CoreError::NotFound(format!("created entity {:?} was not readable", entity_id))
2490        })
2491    }
2492
2493    async fn save_entity(&self, entity: &EntityRecord) -> Result<EntityRecord, CoreError> {
2494        let input = PgCreateEntity {
2495            entity_id: Some(entity.entity_id),
2496            domain_id: entity.domain_id,
2497            uuid: entity.uuid,
2498            name: entity.name.as_str().to_string(),
2499            description: entity.description.clone(),
2500            agent_instructions: entity.agent_instructions.clone(),
2501            ontology_namespace: entity.ontology_namespace.clone(),
2502            ontology_class: entity.ontology_class.clone(),
2503        };
2504        let entity_id = self.with_session_connection(
2505            "save entity",
2506            |connection| connection.create_entity(input.clone()),
2507            |connection| oauth_create_entity(connection, input.clone()),
2508        )?;
2509        self.with_session_connection(
2510            "read saved entity",
2511            |connection| connection.list_entities_for_domain(entity.domain_id),
2512            |connection| oauth_list_entities_for_domain(connection, entity.domain_id),
2513        )?
2514        .into_iter()
2515        .find(|read_model| read_model.entity_id == entity_id)
2516        .map(entity_record)
2517        .transpose()?
2518        .ok_or_else(|| {
2519            CoreError::NotFound(format!("saved entity {:?} was not readable", entity_id))
2520        })
2521    }
2522
2523    async fn create_entity_relation(
2524        &self,
2525        relation: &NewEntityRelationRecord,
2526    ) -> Result<EntityRelationRecord, CoreError> {
2527        let input = PgCreateEntityRelation {
2528            entity_relation_id: None,
2529            subject_entity_id: relation.subject_entity_id,
2530            object_entity_id: relation.object_entity_id,
2531            domain_id: relation.domain_id,
2532            uuid: None,
2533            name: relation.name.as_str().to_string(),
2534            description: relation.description.clone(),
2535            agent_instructions: relation.agent_instructions.clone(),
2536            ontology_namespace: relation.ontology_namespace.clone(),
2537            ontology_class: relation.ontology_class.clone(),
2538        };
2539        let relation_id = self.with_session_connection(
2540            "create entity relation",
2541            |connection| connection.create_entity_relation(input.clone()),
2542            |connection| oauth_create_entity_relation(connection, input.clone()),
2543        )?;
2544        self.with_session_connection(
2545            "read created entity relation",
2546            |connection| connection.list_entity_relations_for_domain(relation.domain_id),
2547            |connection| oauth_list_entity_relations_for_domain(connection, relation.domain_id),
2548        )?
2549        .into_iter()
2550        .find(|read_model| read_model.entity_relation_id == relation_id)
2551        .map(entity_relation_record)
2552        .transpose()?
2553        .ok_or_else(|| {
2554            CoreError::NotFound(format!(
2555                "created entity relation {:?} was not readable",
2556                relation_id
2557            ))
2558        })
2559    }
2560
2561    async fn save_entity_relation(
2562        &self,
2563        relation: &EntityRelationRecord,
2564    ) -> Result<EntityRelationRecord, CoreError> {
2565        let input = PgCreateEntityRelation {
2566            entity_relation_id: Some(relation.entity_relation_id),
2567            subject_entity_id: relation.subject_entity_id,
2568            object_entity_id: relation.object_entity_id,
2569            domain_id: relation.domain_id,
2570            uuid: relation.uuid,
2571            name: relation.name.as_str().to_string(),
2572            description: relation.description.clone(),
2573            agent_instructions: relation.agent_instructions.clone(),
2574            ontology_namespace: relation.ontology_namespace.clone(),
2575            ontology_class: relation.ontology_class.clone(),
2576        };
2577        let relation_id = self.with_session_connection(
2578            "save entity relation",
2579            |connection| connection.create_entity_relation(input.clone()),
2580            |connection| oauth_create_entity_relation(connection, input.clone()),
2581        )?;
2582        self.with_session_connection(
2583            "read saved entity relation",
2584            |connection| connection.list_entity_relations_for_domain(relation.domain_id),
2585            |connection| oauth_list_entity_relations_for_domain(connection, relation.domain_id),
2586        )?
2587        .into_iter()
2588        .find(|read_model| read_model.entity_relation_id == relation_id)
2589        .map(entity_relation_record)
2590        .transpose()?
2591        .ok_or_else(|| {
2592            CoreError::NotFound(format!(
2593                "saved entity relation {:?} was not readable",
2594                relation_id
2595            ))
2596        })
2597    }
2598
2599    async fn delete_entity_record(&self, entity_id: EntityId) -> Result<bool, CoreError> {
2600        self.with_session_connection(
2601            "delete entity record",
2602            |connection| delete_semantic_record(connection, "entities", "entity_id", entity_id.0),
2603            |connection| delete_semantic_record(connection, "entities", "entity_id", entity_id.0),
2604        )
2605    }
2606
2607    async fn delete_entity_relation_record(
2608        &self,
2609        relation_id: EntityRelationId,
2610    ) -> Result<bool, CoreError> {
2611        self.with_session_connection(
2612            "delete entity relation record",
2613            |connection| {
2614                delete_semantic_record(
2615                    connection,
2616                    "entityrelations",
2617                    "entityrelation_id",
2618                    relation_id.0,
2619                )
2620            },
2621            |connection| {
2622                delete_semantic_record(
2623                    connection,
2624                    "entityrelations",
2625                    "entityrelation_id",
2626                    relation_id.0,
2627                )
2628            },
2629        )
2630    }
2631
2632    async fn get_entity_instance(
2633        &self,
2634        instance_id: i64,
2635    ) -> Result<Option<EntityInstanceRecord>, CoreError> {
2636        Ok(self
2637            .with_session_connection(
2638                "get entity instance",
2639                |connection| connection.get_entity_instance(instance_id),
2640                |connection| oauth_get_entity_instance(connection, instance_id),
2641            )?
2642            .map(entity_instance_record))
2643    }
2644
2645    async fn get_entity_instance_by_uuid(
2646        &self,
2647        uuid: uuid::Uuid,
2648    ) -> Result<Option<EntityInstanceRecord>, CoreError> {
2649        Ok(self
2650            .with_session_connection(
2651                "get entity instance by uuid",
2652                |connection| connection.get_entity_instance_by_uuid(uuid),
2653                |connection| oauth_get_entity_instance_by_uuid(connection, uuid),
2654            )?
2655            .map(entity_instance_record))
2656    }
2657
2658    async fn list_entity_instances(
2659        &self,
2660        entity_id: EntityId,
2661    ) -> Result<Vec<EntityInstanceRecord>, CoreError> {
2662        Ok(self
2663            .with_session_connection(
2664                "list entity instances",
2665                |connection| connection.list_entity_instances_for_entity(entity_id),
2666                |connection| oauth_list_entity_instances_for_entity(connection, entity_id),
2667            )?
2668            .into_iter()
2669            .map(entity_instance_record)
2670            .collect())
2671    }
2672
2673    async fn save_entity_instance(
2674        &self,
2675        instance: &EntityInstanceWriteRecord,
2676    ) -> Result<EntityInstanceRecord, CoreError> {
2677        let input = PgWriteEntityInstance {
2678            instance_id: instance.instance_id,
2679            uuid: instance.uuid,
2680            entity_id: instance.entity_id,
2681            label: instance.label.clone(),
2682            note: instance.note.clone(),
2683            transformation_id: instance.transformation_id,
2684        };
2685        let instance_id = self.with_session_connection(
2686            "save entity instance",
2687            |connection| connection.save_entity_instance(input.clone()),
2688            |connection| oauth_save_entity_instance(connection, input.clone()),
2689        )?;
2690        self.get_entity_instance(instance_id).await?.ok_or_else(|| {
2691            CoreError::NotFound(format!(
2692                "saved entity instance {instance_id} was not readable"
2693            ))
2694        })
2695    }
2696
2697    async fn create_entity_instance(
2698        &self,
2699        instance: &NewEntityInstanceRecord,
2700    ) -> Result<EntityInstanceRecord, CoreError> {
2701        let input = PgWriteEntityInstance {
2702            instance_id: None,
2703            uuid: None,
2704            entity_id: instance.entity_id,
2705            label: instance.label.clone(),
2706            note: instance.note.clone(),
2707            transformation_id: instance.transformation_id,
2708        };
2709        let instance_id = self.with_session_connection(
2710            "create entity instance",
2711            |connection| connection.save_entity_instance(input.clone()),
2712            |connection| oauth_save_entity_instance(connection, input.clone()),
2713        )?;
2714        self.get_entity_instance(instance_id).await?.ok_or_else(|| {
2715            CoreError::NotFound(format!(
2716                "created entity instance {instance_id} was not readable"
2717            ))
2718        })
2719    }
2720
2721    async fn get_study_entity_instance(
2722        &self,
2723        study_id: StudyId,
2724        entity_id: EntityId,
2725        external_id: &str,
2726    ) -> Result<Option<StudyEntityInstanceRecord>, CoreError> {
2727        Ok(self
2728            .with_session_connection(
2729                "get study entity instance",
2730                |connection| connection.get_study_entity_instance(study_id, entity_id, external_id),
2731                |connection| {
2732                    oauth_get_study_entity_instance(connection, study_id, entity_id, external_id)
2733                },
2734            )?
2735            .map(study_entity_instance_record))
2736    }
2737
2738    async fn save_study_entity_instance(
2739        &self,
2740        mapping: &StudyEntityInstanceWriteRecord,
2741    ) -> Result<StudyEntityInstanceRecord, CoreError> {
2742        let input = PgWriteStudyEntityInstance {
2743            study_id: mapping.study_id,
2744            entity_instance_id: mapping.entity_instance_id,
2745            entity_id: mapping.entity_id,
2746            external_id: mapping.external_id.clone(),
2747            transformation_id: mapping.transformation_id,
2748        };
2749        self.with_session_connection(
2750            "save study entity instance",
2751            |connection| connection.save_study_entity_instance(input.clone()),
2752            |connection| oauth_save_study_entity_instance(connection, input.clone()),
2753        )?;
2754        self.get_study_entity_instance(mapping.study_id, mapping.entity_id, &mapping.external_id)
2755            .await?
2756            .ok_or_else(|| {
2757                CoreError::NotFound(format!(
2758                    "saved study entity mapping {:?}/{} was not readable",
2759                    mapping.study_id, mapping.external_id
2760                ))
2761            })
2762    }
2763
2764    async fn list_study_entity_instances(
2765        &self,
2766        study_id: StudyId,
2767    ) -> Result<Vec<StudyEntityInstanceRecord>, CoreError> {
2768        Ok(self
2769            .with_session_connection(
2770                "list study entity instances",
2771                |connection| connection.list_study_entity_instances(study_id),
2772                |connection| oauth_list_study_entity_instances(connection, study_id),
2773            )?
2774            .into_iter()
2775            .map(study_entity_instance_record)
2776            .collect())
2777    }
2778
2779    async fn get_relation_instance(
2780        &self,
2781        relation_instance_id: i64,
2782    ) -> Result<Option<RelationInstanceRecord>, CoreError> {
2783        Ok(self
2784            .with_session_connection(
2785                "get relation instance",
2786                |connection| connection.get_relation_instance(relation_instance_id),
2787                |connection| oauth_get_relation_instance(connection, relation_instance_id),
2788            )?
2789            .map(relation_instance_record))
2790    }
2791
2792    async fn get_relation_instance_by_uuid(
2793        &self,
2794        uuid: uuid::Uuid,
2795    ) -> Result<Option<RelationInstanceRecord>, CoreError> {
2796        Ok(self
2797            .with_session_connection(
2798                "get relation instance by uuid",
2799                |connection| connection.get_relation_instance_by_uuid(uuid),
2800                |connection| oauth_get_relation_instance_by_uuid(connection, uuid),
2801            )?
2802            .map(relation_instance_record))
2803    }
2804
2805    async fn list_relation_instances(
2806        &self,
2807        relation_id: EntityRelationId,
2808    ) -> Result<Vec<RelationInstanceRecord>, CoreError> {
2809        Ok(self
2810            .with_session_connection(
2811                "list relation instances",
2812                |connection| connection.list_relation_instances_for_relation(relation_id),
2813                |connection| oauth_list_relation_instances_for_relation(connection, relation_id),
2814            )?
2815            .into_iter()
2816            .map(relation_instance_record)
2817            .collect())
2818    }
2819
2820    async fn save_relation_instance(
2821        &self,
2822        relation: &RelationInstanceWriteRecord,
2823    ) -> Result<RelationInstanceRecord, CoreError> {
2824        let input = PgWriteRelationInstance {
2825            relation_instance_id: relation.relation_instance_id,
2826            uuid: relation.uuid,
2827            entity_relation_id: relation.entity_relation_id,
2828            entity_instance_id_1: relation.entity_instance_id_1,
2829            entity_instance_id_2: relation.entity_instance_id_2,
2830            valid_from: relation.valid_from,
2831            valid_to: relation.valid_to,
2832            note: relation.note.clone(),
2833            transformation_id: relation.transformation_id,
2834        };
2835        let relation_instance_id = self.with_session_connection(
2836            "save relation instance",
2837            |connection| connection.save_relation_instance(input.clone()),
2838            |connection| oauth_save_relation_instance(connection, input.clone()),
2839        )?;
2840        self.get_relation_instance(relation_instance_id)
2841            .await?
2842            .ok_or_else(|| {
2843                CoreError::NotFound(format!(
2844                    "saved relation instance {relation_instance_id} was not readable"
2845                ))
2846            })
2847    }
2848
2849    async fn create_relation_instance(
2850        &self,
2851        relation: &NewRelationInstanceRecord,
2852    ) -> Result<RelationInstanceRecord, CoreError> {
2853        let input = PgWriteRelationInstance {
2854            relation_instance_id: None,
2855            uuid: None,
2856            entity_relation_id: relation.entity_relation_id,
2857            entity_instance_id_1: relation.entity_instance_id_1,
2858            entity_instance_id_2: relation.entity_instance_id_2,
2859            valid_from: relation.valid_from,
2860            valid_to: relation.valid_to,
2861            note: relation.note.clone(),
2862            transformation_id: relation.transformation_id,
2863        };
2864        let relation_instance_id = self.with_session_connection(
2865            "create relation instance",
2866            |connection| connection.save_relation_instance(input.clone()),
2867            |connection| oauth_save_relation_instance(connection, input.clone()),
2868        )?;
2869        self.get_relation_instance(relation_instance_id)
2870            .await?
2871            .ok_or_else(|| {
2872                CoreError::NotFound(format!(
2873                    "created relation instance {relation_instance_id} was not readable"
2874                ))
2875            })
2876    }
2877
2878    async fn get_study_relation_instance(
2879        &self,
2880        study_id: StudyId,
2881        relation_id: EntityRelationId,
2882        external_id: &str,
2883    ) -> Result<Option<StudyRelationInstanceRecord>, CoreError> {
2884        Ok(self
2885            .with_session_connection(
2886                "get study relation instance",
2887                |connection| {
2888                    connection.get_study_relation_instance(study_id, relation_id, external_id)
2889                },
2890                |connection| {
2891                    oauth_get_study_relation_instance(
2892                        connection,
2893                        study_id,
2894                        relation_id,
2895                        external_id,
2896                    )
2897                },
2898            )?
2899            .map(study_relation_instance_record))
2900    }
2901
2902    async fn save_study_relation_instance(
2903        &self,
2904        mapping: &StudyRelationInstanceWriteRecord,
2905    ) -> Result<StudyRelationInstanceRecord, CoreError> {
2906        let input = PgWriteStudyRelationInstance {
2907            study_id: mapping.study_id,
2908            relation_instance_id: mapping.relation_instance_id,
2909            entity_relation_id: mapping.entity_relation_id,
2910            external_id: mapping.external_id.clone(),
2911            transformation_id: mapping.transformation_id,
2912        };
2913        self.with_session_connection(
2914            "save study relation instance",
2915            |connection| connection.save_study_relation_instance(input.clone()),
2916            |connection| oauth_save_study_relation_instance(connection, input.clone()),
2917        )?;
2918        self.get_study_relation_instance(
2919            mapping.study_id,
2920            mapping.entity_relation_id,
2921            &mapping.external_id,
2922        )
2923        .await?
2924        .ok_or_else(|| {
2925            CoreError::NotFound(format!(
2926                "saved study relation mapping {:?}/{} was not readable",
2927                mapping.study_id, mapping.external_id
2928            ))
2929        })
2930    }
2931
2932    async fn list_study_relation_instances(
2933        &self,
2934        study_id: StudyId,
2935    ) -> Result<Vec<StudyRelationInstanceRecord>, CoreError> {
2936        Ok(self
2937            .with_session_connection(
2938                "list study relation instances",
2939                |connection| connection.list_study_relation_instances(study_id),
2940                |connection| oauth_list_study_relation_instances(connection, study_id),
2941            )?
2942            .into_iter()
2943            .map(study_relation_instance_record)
2944            .collect())
2945    }
2946
2947    async fn link_asset_version_entity(
2948        &self,
2949        link: &AssetVersionEntityRecord,
2950    ) -> Result<AssetVersionEntityRecord, CoreError> {
2951        self.with_session_connection(
2952            "link asset version entity",
2953            |connection| {
2954                connection.link_asset_version_entity(
2955                    link.version_id,
2956                    link.entity_instance_id,
2957                    link.transformation_id,
2958                )
2959            },
2960            |connection| oauth_link_asset_version_entity(connection, link),
2961        )?;
2962        Ok(link.clone())
2963    }
2964
2965    async fn list_asset_version_entities(
2966        &self,
2967        version_id: VersionId,
2968    ) -> Result<Vec<AssetVersionEntityRecord>, CoreError> {
2969        let links = self.with_session_connection(
2970            "list asset version entities",
2971            |connection| connection.list_asset_version_entities(version_id),
2972            |connection| oauth_list_asset_version_entities(connection, version_id),
2973        )?;
2974        Ok(links.into_iter().map(asset_version_entity_record).collect())
2975    }
2976
2977    async fn list_asset_version_entities_for_instance(
2978        &self,
2979        entity_instance_id: i64,
2980    ) -> Result<Vec<AssetVersionEntityRecord>, CoreError> {
2981        let links = self.with_session_connection(
2982            "list asset version entities for instance",
2983            |connection| connection.list_asset_version_entities_for_instance(entity_instance_id),
2984            |connection| {
2985                oauth_list_asset_version_entities_for_instance(connection, entity_instance_id)
2986            },
2987        )?;
2988        Ok(links.into_iter().map(asset_version_entity_record).collect())
2989    }
2990
2991    async fn link_dataset_version_entity(
2992        &self,
2993        link: &DatasetVersionEntityRecord,
2994    ) -> Result<DatasetVersionEntityRecord, CoreError> {
2995        self.with_session_connection(
2996            "link dataset version entity",
2997            |connection| {
2998                connection.add_dataset_version_entity(
2999                    link.version_id,
3000                    link.entity_instance_id,
3001                    link.entity_variable_id,
3002                    link.transformation_id,
3003                )
3004            },
3005            |connection| oauth_link_dataset_version_entity(connection, link),
3006        )?;
3007        Ok(link.clone())
3008    }
3009
3010    async fn list_dataset_version_entities(
3011        &self,
3012        version_id: VersionId,
3013    ) -> Result<Vec<DatasetVersionEntityRecord>, CoreError> {
3014        let links = self.with_session_connection(
3015            "list dataset version entities",
3016            |connection| connection.list_dataset_version_entities(version_id),
3017            |connection| oauth_list_dataset_version_entities(connection, version_id),
3018        )?;
3019        Ok(links
3020            .into_iter()
3021            .map(dataset_version_entity_record)
3022            .collect())
3023    }
3024
3025    async fn list_dataset_version_entities_for_instance(
3026        &self,
3027        entity_instance_id: i64,
3028    ) -> Result<Vec<DatasetVersionEntityRecord>, CoreError> {
3029        let links = self.with_session_connection(
3030            "list dataset version entities for instance",
3031            |connection| connection.list_dataset_version_entities_for_instance(entity_instance_id),
3032            |connection| {
3033                oauth_list_dataset_version_entities_for_instance(connection, entity_instance_id)
3034            },
3035        )?;
3036        Ok(links
3037            .into_iter()
3038            .map(dataset_version_entity_record)
3039            .collect())
3040    }
3041
3042    async fn link_asset_version_relation_instance(
3043        &self,
3044        link: &AssetVersionRelationInstanceRecord,
3045    ) -> Result<AssetVersionRelationInstanceRecord, CoreError> {
3046        self.with_session_connection(
3047            "link asset version relation instance",
3048            |connection| {
3049                connection.link_asset_version_relation_instance(
3050                    link.version_id,
3051                    link.relation_instance_id,
3052                    link.transformation_id,
3053                )
3054            },
3055            |connection| oauth_link_asset_version_relation_instance(connection, link),
3056        )?;
3057        Ok(link.clone())
3058    }
3059
3060    async fn list_asset_version_relation_instances(
3061        &self,
3062        version_id: VersionId,
3063    ) -> Result<Vec<AssetVersionRelationInstanceRecord>, CoreError> {
3064        let links = self.with_session_connection(
3065            "list asset version relation instances",
3066            |connection| connection.list_asset_version_relation_instances(version_id),
3067            |connection| oauth_list_asset_version_relation_instances(connection, version_id),
3068        )?;
3069        Ok(links
3070            .into_iter()
3071            .map(asset_version_relation_instance_record)
3072            .collect())
3073    }
3074
3075    async fn list_asset_version_relation_instances_for_instance(
3076        &self,
3077        relation_instance_id: i64,
3078    ) -> Result<Vec<AssetVersionRelationInstanceRecord>, CoreError> {
3079        let links = self.with_session_connection(
3080            "list asset version relation instances for instance",
3081            |connection| {
3082                connection.list_asset_version_relation_instances_for_instance(relation_instance_id)
3083            },
3084            |connection| {
3085                oauth_list_asset_version_relation_instances_for_instance(
3086                    connection,
3087                    relation_instance_id,
3088                )
3089            },
3090        )?;
3091        Ok(links
3092            .into_iter()
3093            .map(asset_version_relation_instance_record)
3094            .collect())
3095    }
3096
3097    async fn link_dataset_version_relation_instance(
3098        &self,
3099        link: &DatasetVersionRelationInstanceRecord,
3100    ) -> Result<DatasetVersionRelationInstanceRecord, CoreError> {
3101        self.with_session_connection(
3102            "link dataset version relation instance",
3103            |connection| {
3104                connection.add_dataset_version_relation(
3105                    link.version_id,
3106                    link.relation_instance_id,
3107                    link.subject_variable_id,
3108                    link.object_variable_id,
3109                    link.relation_variable_id,
3110                    link.transformation_id,
3111                )
3112            },
3113            |connection| oauth_link_dataset_version_relation_instance(connection, link),
3114        )?;
3115        Ok(link.clone())
3116    }
3117
3118    async fn list_dataset_version_relation_instances(
3119        &self,
3120        version_id: VersionId,
3121    ) -> Result<Vec<DatasetVersionRelationInstanceRecord>, CoreError> {
3122        let links = self.with_session_connection(
3123            "list dataset version relation instances",
3124            |connection| connection.list_dataset_version_relation_instances(version_id),
3125            |connection| oauth_list_dataset_version_relation_instances(connection, version_id),
3126        )?;
3127        Ok(links
3128            .into_iter()
3129            .map(dataset_version_relation_instance_record)
3130            .collect())
3131    }
3132
3133    async fn list_dataset_version_relation_instances_for_instance(
3134        &self,
3135        relation_instance_id: i64,
3136    ) -> Result<Vec<DatasetVersionRelationInstanceRecord>, CoreError> {
3137        let links = self.with_session_connection(
3138            "list dataset version relation instances for instance",
3139            |connection| {
3140                connection
3141                    .list_dataset_version_relation_instances_for_instance(relation_instance_id)
3142            },
3143            |connection| {
3144                oauth_list_dataset_version_relation_instances_for_instance(
3145                    connection,
3146                    relation_instance_id,
3147                )
3148            },
3149        )?;
3150        Ok(links
3151            .into_iter()
3152            .map(dataset_version_relation_instance_record)
3153            .collect())
3154    }
3155
3156    async fn link_dataset_version_entities(
3157        &self,
3158        links: &[DatasetVersionEntityRecord],
3159    ) -> Result<(), CoreError> {
3160        let link_inputs: Vec<_> = links
3161            .iter()
3162            .map(|link| {
3163                (
3164                    link.version_id,
3165                    link.entity_instance_id,
3166                    link.entity_variable_id,
3167                    link.transformation_id,
3168                )
3169            })
3170            .collect();
3171        self.with_session_connection(
3172            "bulk link dataset version entities",
3173            |connection| connection.link_dataset_version_entities(&link_inputs),
3174            |connection| oauth_link_dataset_version_entities(connection, links),
3175        )
3176    }
3177
3178    async fn link_dataset_version_relation_instances(
3179        &self,
3180        links: &[DatasetVersionRelationInstanceRecord],
3181    ) -> Result<(), CoreError> {
3182        let link_inputs: Vec<_> = links
3183            .iter()
3184            .map(|link| {
3185                (
3186                    link.version_id,
3187                    link.relation_instance_id,
3188                    link.subject_variable_id,
3189                    link.object_variable_id,
3190                    link.relation_variable_id,
3191                    link.transformation_id,
3192                )
3193            })
3194            .collect();
3195        self.with_session_connection(
3196            "bulk link dataset version relation instances",
3197            |connection| connection.link_dataset_version_relation_instances(&link_inputs),
3198            |connection| oauth_link_dataset_version_relation_instances(connection, links),
3199        )
3200    }
3201}
3202
3203#[async_trait::async_trait]
3204impl TransformationRepository for PgMetadataRepository<'_> {
3205    async fn create_transformation(
3206        &self,
3207        transformation: &NewTransformationRecord,
3208    ) -> Result<TransformationRecord, CoreError> {
3209        let input = PgCreateTransformation {
3210            transformation_id: None,
3211            transformation_type: transformation.transformation_type,
3212            description: transformation.description.clone(),
3213            repository_url: transformation.repository_url.clone(),
3214            commit_hash: transformation.commit_hash.clone(),
3215            file_path: transformation.file_path.clone(),
3216            date_created: transformation.date_created,
3217            created_by: transformation.created_by.clone(),
3218        };
3219        self.create_transformation_from_input(input).await
3220    }
3221
3222    async fn save_transformation(
3223        &self,
3224        transformation: &TransformationRecord,
3225    ) -> Result<TransformationRecord, CoreError> {
3226        let input = PgCreateTransformation {
3227            transformation_id: Some(transformation.transformation_id),
3228            transformation_type: transformation.transformation_type,
3229            description: transformation.description.clone(),
3230            repository_url: transformation.repository_url.clone(),
3231            commit_hash: transformation.commit_hash.clone(),
3232            file_path: transformation.file_path.clone(),
3233            date_created: transformation.date_created,
3234            created_by: transformation.created_by.clone(),
3235        };
3236        self.create_transformation_from_input(input).await
3237    }
3238
3239    async fn add_inputs(
3240        &self,
3241        transformation_id: TransformationId,
3242        input_ids: &[VersionId],
3243    ) -> Result<(), CoreError> {
3244        self.with_session_connection(
3245            "link transformation inputs",
3246            |connection| {
3247                for version_id in input_ids {
3248                    connection.link_transformation_input(transformation_id, *version_id)?;
3249                }
3250                Ok(())
3251            },
3252            |connection| oauth_add_transformation_inputs(connection, transformation_id, input_ids),
3253        )
3254    }
3255
3256    async fn add_outputs(
3257        &self,
3258        transformation_id: TransformationId,
3259        output_ids: &[VersionId],
3260    ) -> Result<(), CoreError> {
3261        self.with_session_connection(
3262            "link transformation outputs",
3263            |connection| {
3264                for version_id in output_ids {
3265                    connection.link_transformation_output(transformation_id, *version_id)?;
3266                }
3267                Ok(())
3268            },
3269            |connection| {
3270                oauth_add_transformation_outputs(connection, transformation_id, output_ids)
3271            },
3272        )
3273    }
3274
3275    async fn delete_transformation(
3276        &self,
3277        transformation_id: TransformationId,
3278    ) -> Result<bool, CoreError> {
3279        self.with_session_connection(
3280            "delete transformation",
3281            |connection| connection.delete_transformation(transformation_id),
3282            |connection| oauth_delete_transformation(connection, transformation_id),
3283        )
3284    }
3285
3286    async fn list_inputs(
3287        &self,
3288        transformation_id: TransformationId,
3289    ) -> Result<Vec<TransformationInputLinkRecord>, CoreError> {
3290        Ok(self
3291            .with_session_connection(
3292                "list transformation inputs",
3293                |connection| connection.list_transformation_inputs(transformation_id),
3294                |connection| oauth_list_transformation_inputs(connection, transformation_id),
3295            )?
3296            .into_iter()
3297            .map(transformation_input_record)
3298            .collect())
3299    }
3300
3301    async fn list_outputs(
3302        &self,
3303        transformation_id: TransformationId,
3304    ) -> Result<Vec<TransformationOutputLinkRecord>, CoreError> {
3305        Ok(self
3306            .with_session_connection(
3307                "list transformation outputs",
3308                |connection| connection.list_transformation_outputs(transformation_id),
3309                |connection| oauth_list_transformation_outputs(connection, transformation_id),
3310            )?
3311            .into_iter()
3312            .map(transformation_output_record)
3313            .collect())
3314    }
3315
3316    async fn list_transformations_producing(
3317        &self,
3318        version_id: VersionId,
3319    ) -> Result<Vec<TransformationRecord>, CoreError> {
3320        self.with_session_connection(
3321            "list transformations producing version",
3322            |connection| connection.list_transformations_producing_version(version_id),
3323            |connection| oauth_list_transformations_producing_version(connection, version_id),
3324        )?
3325        .into_iter()
3326        .map(transformation_record)
3327        .collect()
3328    }
3329
3330    async fn list_transformations(&self) -> Result<Vec<TransformationRecord>, CoreError> {
3331        self.with_session_connection(
3332            "list transformations",
3333            |connection| connection.list_transformations(),
3334            oauth_list_transformations,
3335        )?
3336        .into_iter()
3337        .map(transformation_record)
3338        .collect()
3339    }
3340}
3341
3342#[async_trait::async_trait]
3343impl TagRepository for PgMetadataRepository<'_> {
3344    async fn list_tags(&self) -> Result<Vec<TagRecord>, CoreError> {
3345        Ok(self
3346            .with_session_connection(
3347                "list tags",
3348                |connection| connection.list_tags(),
3349                oauth_list_tags,
3350            )?
3351            .into_iter()
3352            .map(tag_record)
3353            .collect())
3354    }
3355
3356    async fn upsert_tag(&self, normalized_name: &str) -> Result<TagRecord, CoreError> {
3357        let tag_id = self.with_session_connection(
3358            "upsert tag",
3359            |connection| connection.upsert_tag(normalized_name),
3360            |connection| oauth_upsert_tag(connection, normalized_name),
3361        )?;
3362        Ok(TagRecord {
3363            tag_id: TagId(tag_id),
3364            name: normalized_name.to_string(),
3365        })
3366    }
3367
3368    async fn attach_tag(
3369        &self,
3370        tag_id: TagId,
3371        target: TagAttachmentTarget,
3372    ) -> Result<(), CoreError> {
3373        let direct_target = pg_write_tag_target(target.clone());
3374        let oauth_target = pg_tag_target(target);
3375        self.with_session_connection(
3376            "attach tag",
3377            |connection| connection.attach_tag(direct_target, tag_id.0),
3378            |connection| oauth_attach_tag(connection, oauth_target, tag_id.0),
3379        )
3380    }
3381
3382    async fn detach_tag(
3383        &self,
3384        tag_id: TagId,
3385        target: TagAttachmentTarget,
3386    ) -> Result<(), CoreError> {
3387        let direct_target = pg_write_tag_target(target.clone());
3388        let oauth_target = pg_tag_target(target);
3389        self.with_session_connection(
3390            "detach tag",
3391            |connection| connection.detach_tag(direct_target, tag_id.0),
3392            |connection| oauth_detach_tag(connection, oauth_target, tag_id.0),
3393        )
3394    }
3395
3396    async fn delete_tag_if_unused(&self, tag_id: TagId) -> Result<bool, CoreError> {
3397        self.with_session_connection(
3398            "delete unused tag",
3399            |connection| connection.delete_tag_if_unused(tag_id.0),
3400            |connection| oauth_delete_tag_if_unused(connection, tag_id.0),
3401        )
3402    }
3403
3404    async fn list_tags_for_target(
3405        &self,
3406        target: TagAttachmentTarget,
3407    ) -> Result<Vec<TagRecord>, CoreError> {
3408        let target = pg_tag_target(target);
3409        Ok(self
3410            .with_session_connection(
3411                "list tags for target",
3412                |connection| connection.list_tag_records_for_target(target),
3413                |connection| oauth_list_tags_for_target(connection, target),
3414            )?
3415            .into_iter()
3416            .map(tag_record)
3417            .collect())
3418    }
3419}
3420
3421fn oauth_list_assets_for_study(
3422    connection: &mut LibpqOAuthConnection,
3423    study_id: StudyId,
3424) -> Result<Vec<PgAssetReadModel>, PgMetaError> {
3425    let rows = MetadataExecutor::query_many(
3426        connection,
3427        "list assets for study",
3428        "
3429        SELECT asset_id::text, study_id::text, name, description, agent_instructions,
3430               asset_type::text, date_created::text, created_by
3431          FROM public.assets
3432         WHERE study_id = $1
3433         ORDER BY name
3434        ",
3435        &[&study_id.0],
3436    )?;
3437    rows.iter().map(oauth_asset_from_row).collect()
3438}
3439
3440fn oauth_get_asset(
3441    connection: &mut LibpqOAuthConnection,
3442    asset_id: AssetId,
3443) -> Result<Option<PgAssetReadModel>, PgMetaError> {
3444    let row = MetadataExecutor::query_optional(
3445        connection,
3446        "get asset by id",
3447        "
3448        SELECT asset_id::text, study_id::text, name, description, agent_instructions,
3449               asset_type::text, date_created::text, created_by
3450          FROM public.assets
3451         WHERE asset_id = $1
3452         LIMIT 1
3453        ",
3454        &[&asset_id.0],
3455    )?;
3456    row.as_ref().map(oauth_asset_from_row).transpose()
3457}
3458
3459fn oauth_get_asset_by_name(
3460    connection: &mut LibpqOAuthConnection,
3461    study_id: StudyId,
3462    name: &str,
3463) -> Result<Option<PgAssetReadModel>, PgMetaError> {
3464    let row = MetadataExecutor::query_optional(
3465        connection,
3466        "get asset by name",
3467        "
3468        SELECT asset_id::text, study_id::text, name, description, agent_instructions,
3469               asset_type::text, date_created::text, created_by
3470          FROM public.assets
3471         WHERE study_id = $1 AND name = $2
3472         LIMIT 1
3473        ",
3474        &[&study_id.0, &name],
3475    )?;
3476    row.as_ref().map(oauth_asset_from_row).transpose()
3477}
3478
3479fn oauth_create_asset(
3480    connection: &mut LibpqOAuthConnection,
3481    input: PgCreateAsset,
3482) -> Result<AssetId, PgMetaError> {
3483    let asset_type = asset_type_sql(input.asset_type);
3484    let row = match input.asset_id {
3485        Some(asset_id) => MetadataExecutor::query_one(
3486            connection,
3487            "create asset",
3488            "
3489            INSERT INTO public.assets (
3490                asset_id, study_id, name, description, agent_instructions, asset_type,
3491                date_created, created_by
3492            )
3493            VALUES ($1, $2, $3, $4, $5, CAST($6 AS text)::public.asset_type_enum,
3494                    COALESCE($7, CURRENT_TIMESTAMP), COALESCE($8, CURRENT_USER))
3495            RETURNING asset_id::text
3496            ",
3497            &[
3498                &asset_id.0,
3499                &input.study_id.0,
3500                &input.name,
3501                &input.description,
3502                &input.agent_instructions,
3503                &asset_type,
3504                &input.date_created,
3505                &input.created_by,
3506            ],
3507        )?,
3508        None => MetadataExecutor::query_one(
3509            connection,
3510            "create asset",
3511            "
3512            INSERT INTO public.assets (
3513                study_id, name, description, agent_instructions, asset_type,
3514                date_created, created_by
3515            )
3516            VALUES ($1, $2, $3, $4, CAST($5 AS text)::public.asset_type_enum,
3517                    COALESCE($6, CURRENT_TIMESTAMP), COALESCE($7, CURRENT_USER))
3518            RETURNING asset_id::text
3519            ",
3520            &[
3521                &input.study_id.0,
3522                &input.name,
3523                &input.description,
3524                &input.agent_instructions,
3525                &asset_type,
3526                &input.date_created,
3527                &input.created_by,
3528            ],
3529        )?,
3530    };
3531    Ok(AssetId(oauth_uuid(&row, 0, "asset_id")?))
3532}
3533
3534fn oauth_list_asset_versions(
3535    connection: &mut LibpqOAuthConnection,
3536    asset_id: AssetId,
3537) -> Result<Vec<PgAssetVersionReadModel>, PgMetaError> {
3538    let rows = MetadataExecutor::query_many(
3539        connection,
3540        "list asset versions",
3541        "
3542        SELECT version_id::text, asset_id::text, major::text, minor::text, patch::text,
3543               version_label, version_note, is_latest::text, doi, created_at::text, created_by
3544          FROM public.asset_versions
3545         WHERE asset_id = $1
3546         ORDER BY major, minor, patch
3547        ",
3548        &[&asset_id.0],
3549    )?;
3550    rows.iter().map(oauth_asset_version_from_row).collect()
3551}
3552
3553fn oauth_get_asset_version(
3554    connection: &mut LibpqOAuthConnection,
3555    version_id: VersionId,
3556) -> Result<Option<PgAssetVersionReadModel>, PgMetaError> {
3557    let row = MetadataExecutor::query_optional(
3558        connection,
3559        "get asset version",
3560        "
3561        SELECT version_id::text, asset_id::text, major::text, minor::text, patch::text,
3562               version_label, version_note, is_latest::text, doi, created_at::text, created_by
3563          FROM public.asset_versions
3564         WHERE version_id = $1
3565         LIMIT 1
3566        ",
3567        &[&version_id.0],
3568    )?;
3569    row.as_ref().map(oauth_asset_version_from_row).transpose()
3570}
3571
3572fn oauth_create_asset_version(
3573    connection: &mut LibpqOAuthConnection,
3574    input: PgCreateAssetVersion,
3575) -> Result<VersionId, PgMetaError> {
3576    MetadataExecutor::with_transaction(connection, "create asset version", |transaction| {
3577        oauth_insert_asset_version(transaction, input)
3578    })
3579}
3580
3581fn oauth_insert_asset_version(
3582    transaction: &mut dyn MetadataTransaction<Row = LibpqOAuthRow>,
3583    input: PgCreateAssetVersion,
3584) -> Result<VersionId, PgMetaError> {
3585    transaction.query_one(
3586        "validate ingest classification",
3587        "SELECT public.tre_validate_ingest($1,$2,$3)",
3588        &[
3589            &input.asset_id.0,
3590            &input.classification.risk.as_str(),
3591            &input.classification.justification,
3592        ],
3593    )?;
3594    if input.is_latest.unwrap_or(true) {
3595        transaction.execute_command(
3596            "clear latest asset version",
3597            "UPDATE public.asset_versions SET is_latest = FALSE WHERE asset_id = $1 AND is_latest = TRUE",
3598            &[&input.asset_id.0],
3599        )?;
3600    }
3601
3602    let row = match input.version_id {
3603        Some(version_id) => transaction.query_one(
3604            "create asset version",
3605            "
3606                INSERT INTO public.asset_versions (
3607                    version_id, asset_id, major, minor, patch, version_note, is_latest, doi,
3608                    created_at, created_by
3609                )
3610                VALUES ($1, $2, COALESCE($3, 1), COALESCE($4, 0), COALESCE($5, 0),
3611                        COALESCE($6, 'Original version'), COALESCE($7, TRUE), $8,
3612                        COALESCE($9, CURRENT_TIMESTAMP), COALESCE($10, CURRENT_USER))
3613                RETURNING version_id::text
3614                ",
3615            &[
3616                &version_id.0,
3617                &input.asset_id.0,
3618                &input.major,
3619                &input.minor,
3620                &input.patch,
3621                &input.version_note,
3622                &input.is_latest,
3623                &input.doi,
3624                &input.created_at,
3625                &input.created_by,
3626            ],
3627        )?,
3628        None => transaction.query_one(
3629            "create asset version",
3630            "
3631                INSERT INTO public.asset_versions (
3632                    asset_id, major, minor, patch, version_note, is_latest, doi,
3633                    created_at, created_by
3634                )
3635                VALUES ($1, COALESCE($2, 1), COALESCE($3, 0), COALESCE($4, 0),
3636                        COALESCE($5, 'Original version'), COALESCE($6, TRUE), $7,
3637                        COALESCE($8, CURRENT_TIMESTAMP), COALESCE($9, CURRENT_USER))
3638                RETURNING version_id::text
3639                ",
3640            &[
3641                &input.asset_id.0,
3642                &input.major,
3643                &input.minor,
3644                &input.patch,
3645                &input.version_note,
3646                &input.is_latest,
3647                &input.doi,
3648                &input.created_at,
3649                &input.created_by,
3650            ],
3651        )?,
3652    };
3653    Ok(VersionId(oauth_uuid(&row, 0, "version_id")?))
3654}
3655
3656fn oauth_mark_asset_version_latest(
3657    connection: &mut LibpqOAuthConnection,
3658    asset_id: AssetId,
3659    version_id: VersionId,
3660) -> Result<(), PgMetaError> {
3661    MetadataExecutor::with_transaction(connection, "mark asset version latest", |transaction| {
3662        transaction.execute_command(
3663            "clear latest asset version",
3664            "UPDATE public.asset_versions SET is_latest = FALSE WHERE asset_id = $1 AND is_latest = TRUE",
3665            &[&asset_id.0],
3666        )?;
3667        transaction.query_one(
3668            "mark asset version latest",
3669            "UPDATE public.asset_versions SET is_latest = TRUE WHERE asset_id = $1 AND version_id = $2 RETURNING version_id::text",
3670            &[&asset_id.0, &version_id.0],
3671        )?;
3672        Ok(())
3673    })
3674}
3675
3676fn oauth_list_asset_version_duo_restrictions(
3677    connection: &mut LibpqOAuthConnection,
3678    version_id: VersionId,
3679) -> Result<Vec<PgAssetVersionDuoRestrictionReadModel>, PgMetaError> {
3680    let rows = MetadataExecutor::query_many(
3681        connection,
3682        "list asset version DUO restrictions",
3683        "
3684        SELECT avdr.asset_version_duo_restriction_id::text, avdr.version_id::text, avdr.duo_id,
3685               dc.duo_code, dc.label,
3686               avdr.qualifier_ontology_namespace, avdr.qualifier_ontology_id,
3687               avdr.qualifier_text, avdr.qualifier_date::text, avdr.qualifier_integer::text
3688          FROM public.asset_version_duo_restrictions avdr
3689          JOIN public.duo_codes dc ON dc.duo_id = avdr.duo_id
3690         WHERE avdr.version_id = $1
3691         ORDER BY avdr.asset_version_duo_restriction_id
3692        ",
3693        &[&version_id.0],
3694    )?;
3695    rows.iter().map(oauth_asset_version_duo_from_row).collect()
3696}
3697
3698fn oauth_create_asset_version_duo_restriction(
3699    connection: &mut LibpqOAuthConnection,
3700    input: PgCreateAssetVersionDuoRestriction,
3701) -> Result<i64, PgMetaError> {
3702    let restriction_id = optional_metadata_i32(
3703        input.asset_version_duo_restriction_id.map(|value| value.0),
3704        "asset_version_duo_restriction_id",
3705    )?;
3706    let row = match restriction_id {
3707        Some(restriction_id) => MetadataExecutor::query_one(
3708            connection,
3709            "create asset version DUO restriction",
3710            "
3711            INSERT INTO public.asset_version_duo_restrictions (
3712                asset_version_duo_restriction_id, version_id, duo_id,
3713                qualifier_ontology_namespace, qualifier_ontology_id,
3714                qualifier_text, qualifier_date, qualifier_integer
3715            )
3716            VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
3717            RETURNING asset_version_duo_restriction_id::text
3718            ",
3719            &[
3720                &restriction_id,
3721                &input.version_id.0,
3722                &input.duo_id,
3723                &input.qualifiers.qualifier_ontology_namespace,
3724                &input.qualifiers.qualifier_ontology_id,
3725                &input.qualifiers.qualifier_text,
3726                &input.qualifiers.qualifier_date,
3727                &input.qualifiers.qualifier_integer,
3728            ],
3729        )?,
3730        None => MetadataExecutor::query_one(
3731            connection,
3732            "create asset version DUO restriction",
3733            "
3734            INSERT INTO public.asset_version_duo_restrictions (
3735                version_id, duo_id, qualifier_ontology_namespace, qualifier_ontology_id,
3736                qualifier_text, qualifier_date, qualifier_integer
3737            )
3738            VALUES ($1, $2, $3, $4, $5, $6, $7)
3739            RETURNING asset_version_duo_restriction_id::text
3740            ",
3741            &[
3742                &input.version_id.0,
3743                &input.duo_id,
3744                &input.qualifiers.qualifier_ontology_namespace,
3745                &input.qualifiers.qualifier_ontology_id,
3746                &input.qualifiers.qualifier_text,
3747                &input.qualifiers.qualifier_date,
3748                &input.qualifiers.qualifier_integer,
3749            ],
3750        )?,
3751    };
3752    oauth_i64(&row, 0, "asset_version_duo_restriction_id")
3753}
3754
3755fn oauth_list_datasets_for_asset(
3756    connection: &mut LibpqOAuthConnection,
3757    asset_id: AssetId,
3758) -> Result<Vec<PgDatasetReadModel>, PgMetaError> {
3759    let rows = MetadataExecutor::query_many(
3760        connection,
3761        "list datasets for asset",
3762        "
3763        SELECT d.dataset_id::text
3764          FROM public.datasets d
3765          JOIN public.asset_versions av ON av.version_id = d.dataset_id
3766         WHERE av.asset_id = $1
3767         ORDER BY av.major, av.minor, av.patch
3768        ",
3769        &[&asset_id.0],
3770    )?;
3771    rows.iter().map(oauth_dataset_from_row).collect()
3772}
3773
3774fn oauth_get_dataset(
3775    connection: &mut LibpqOAuthConnection,
3776    dataset_id: VersionId,
3777) -> Result<Option<PgDatasetReadModel>, PgMetaError> {
3778    let row = MetadataExecutor::query_optional(
3779        connection,
3780        "get dataset",
3781        "
3782        SELECT dataset_id::text
3783          FROM public.datasets
3784         WHERE dataset_id = $1
3785         LIMIT 1
3786        ",
3787        &[&dataset_id.0],
3788    )?;
3789    row.as_ref().map(oauth_dataset_from_row).transpose()
3790}
3791
3792fn oauth_create_dataset(
3793    connection: &mut LibpqOAuthConnection,
3794    dataset_id: VersionId,
3795) -> Result<(), PgMetaError> {
3796    MetadataExecutor::execute_command(
3797        connection,
3798        "create dataset",
3799        "INSERT INTO public.datasets (dataset_id) VALUES ($1)",
3800        &[&dataset_id.0],
3801    )?;
3802    Ok(())
3803}
3804
3805fn oauth_get_dataset_version_withdrawal(
3806    connection: &mut LibpqOAuthConnection,
3807    dataset_id: VersionId,
3808) -> Result<Option<DatasetVersionWithdrawalRecord>, PgMetaError> {
3809    if !oauth_dataset_version_withdrawals_table_exists(connection)? {
3810        return Ok(None);
3811    }
3812
3813    let row = MetadataExecutor::query_optional(
3814        connection,
3815        "get dataset version withdrawal",
3816        "
3817        SELECT dataset_id::text, asset_id::text, withdrawn_at::text, withdrawn_by, reason,
3818               tombstone_state, provenance, drop_lake_table::text
3819          FROM public.dataset_version_withdrawals
3820         WHERE dataset_id = $1
3821         LIMIT 1
3822        ",
3823        &[&dataset_id.0],
3824    )?;
3825    row.as_ref()
3826        .map(oauth_dataset_version_withdrawal_from_row)
3827        .transpose()
3828}
3829
3830fn oauth_list_dataset_version_withdrawals(
3831    connection: &mut LibpqOAuthConnection,
3832    asset_id: AssetId,
3833) -> Result<Vec<DatasetVersionWithdrawalRecord>, PgMetaError> {
3834    if !oauth_dataset_version_withdrawals_table_exists(connection)? {
3835        return Ok(Vec::new());
3836    }
3837
3838    let rows = MetadataExecutor::query_many(
3839        connection,
3840        "list dataset version withdrawals",
3841        "
3842        SELECT dataset_id::text, asset_id::text, withdrawn_at::text, withdrawn_by, reason,
3843               tombstone_state, provenance, drop_lake_table::text
3844          FROM public.dataset_version_withdrawals
3845         WHERE asset_id = $1
3846         ORDER BY withdrawn_at, dataset_id
3847        ",
3848        &[&asset_id.0],
3849    )?;
3850    rows.iter()
3851        .map(oauth_dataset_version_withdrawal_from_row)
3852        .collect()
3853}
3854
3855fn pg_dataset_version_withdrawals_table_exists(
3856    connection: &mut PgMetadataConnection,
3857) -> Result<bool, PgMetaError> {
3858    let row = MetadataExecutor::query_one(
3859        connection,
3860        "check dataset version withdrawals table",
3861        "SELECT to_regclass($1) IS NOT NULL",
3862        &[&DATASET_VERSION_WITHDRAWALS_RELATION],
3863    )?;
3864    Ok(row.get(0))
3865}
3866
3867fn oauth_dataset_version_withdrawals_table_exists(
3868    connection: &mut LibpqOAuthConnection,
3869) -> Result<bool, PgMetaError> {
3870    let row = MetadataExecutor::query_one(
3871        connection,
3872        "check dataset version withdrawals table",
3873        "SELECT to_regclass($1) IS NOT NULL",
3874        &[&DATASET_VERSION_WITHDRAWALS_RELATION],
3875    )?;
3876    oauth_bool(&row, 0, DATASET_VERSION_WITHDRAWALS_EXISTS_FIELD)
3877}
3878
3879fn oauth_save_dataset_version_withdrawal(
3880    connection: &mut LibpqOAuthConnection,
3881    withdrawal: &NewDatasetVersionWithdrawalRecord,
3882) -> Result<(), PgMetaError> {
3883    MetadataExecutor::execute_command(
3884        connection,
3885        "save dataset version withdrawal",
3886        "
3887        INSERT INTO public.dataset_version_withdrawals (
3888            dataset_id, asset_id, withdrawn_by, reason, tombstone_state, provenance,
3889            drop_lake_table
3890        )
3891        VALUES ($1, $2, COALESCE($3, CURRENT_USER), $4, $5, $6, $7)
3892        ON CONFLICT (dataset_id) DO NOTHING
3893        ",
3894        &[
3895            &withdrawal.dataset_id.0,
3896            &withdrawal.asset_id.0,
3897            &withdrawal.withdrawn_by,
3898            &withdrawal.reason,
3899            &withdrawal.tombstone_state,
3900            &withdrawal.provenance,
3901            &withdrawal.drop_lake_table,
3902        ],
3903    )?;
3904    Ok(())
3905}
3906
3907fn oauth_deactivate_dataset_version_links(
3908    connection: &mut LibpqOAuthConnection,
3909    dataset_id: VersionId,
3910) -> Result<(), PgMetaError> {
3911    MetadataExecutor::with_transaction(
3912        connection,
3913        "deactivate dataset version links",
3914        |transaction| {
3915            for (operation, sql) in [
3916                (
3917                    "delete dataset version relation links",
3918                    "DELETE FROM public.dataset_version_relation_instances WHERE version_id = $1",
3919                ),
3920                (
3921                    "delete dataset version entity links",
3922                    "DELETE FROM public.dataset_version_entities WHERE version_id = $1",
3923                ),
3924                (
3925                    "delete asset version relation links",
3926                    "DELETE FROM public.asset_version_relation_instances WHERE version_id = $1",
3927                ),
3928                (
3929                    "delete asset version entity links",
3930                    "DELETE FROM public.asset_version_entities WHERE version_id = $1",
3931                ),
3932                (
3933                    "delete dataset variable links",
3934                    "DELETE FROM public.dataset_variables WHERE dataset_id = $1",
3935                ),
3936            ] {
3937                transaction.execute_command(operation, sql, &[&dataset_id.0])?;
3938            }
3939            Ok(())
3940        },
3941    )
3942}
3943
3944fn delete_asset_version_metadata_record<E>(
3945    connection: &mut E,
3946    version_id: VersionId,
3947) -> Result<bool, PgMetaError>
3948where
3949    E: MetadataExecutor,
3950{
3951    delete_version_metadata(connection, version_id, None)
3952}
3953
3954fn delete_version_metadata<E: MetadataExecutor>(
3955    connection: &mut E,
3956    version_id: VersionId,
3957    latest_promotion: Option<(AssetId, Option<VersionId>)>,
3958) -> Result<bool, PgMetaError> {
3959    connection.with_transaction("delete asset version record", |transaction| {
3960        if let Some((asset_id, promoted)) = latest_promotion {
3961            transaction.execute_command(
3962                "clear deleted latest Datafile version",
3963                "UPDATE public.asset_versions SET is_latest = FALSE WHERE asset_id = $1 AND is_latest = TRUE",
3964                &[&asset_id.0],
3965            )?;
3966            if let Some(promoted) = promoted {
3967                transaction.query_one(
3968                    "promote previous Datafile version",
3969                    "UPDATE public.asset_versions SET is_latest = TRUE WHERE asset_id = $1 AND version_id = $2 RETURNING version_id::text",
3970                    &[&asset_id.0, &promoted.0],
3971                )?;
3972            }
3973        }
3974        for (operation, sql) in [
3975            (
3976                "delete dataset version relation links",
3977                "DELETE FROM public.dataset_version_relation_instances WHERE version_id = $1",
3978            ),
3979            (
3980                "delete dataset version entity links",
3981                "DELETE FROM public.dataset_version_entities WHERE version_id = $1",
3982            ),
3983            (
3984                "delete asset version relation links",
3985                "DELETE FROM public.asset_version_relation_instances WHERE version_id = $1",
3986            ),
3987            (
3988                "delete asset version entity links",
3989                "DELETE FROM public.asset_version_entities WHERE version_id = $1",
3990            ),
3991            (
3992                "delete dataset variable links",
3993                "DELETE FROM public.dataset_variables WHERE dataset_id = $1",
3994            ),
3995        ] {
3996            transaction.execute_command(operation, sql, &[&version_id.0])?;
3997        }
3998        let deleted = transaction.execute_command(
3999            "delete asset version record",
4000            "DELETE FROM public.asset_versions WHERE version_id = $1",
4001            &[&version_id.0],
4002        )?;
4003        Ok(deleted > 0)
4004    })
4005}
4006
4007fn delete_datafile_metadata_record<E>(
4008    connection: &mut E,
4009    datafile_id: VersionId,
4010) -> Result<bool, PgMetaError>
4011where
4012    E: MetadataExecutor,
4013{
4014    let deleted = connection.execute_command(
4015        "delete datafile record",
4016        "DELETE FROM public.datafiles WHERE datafile_id = $1",
4017        &[&datafile_id.0],
4018    )?;
4019    Ok(deleted > 0)
4020}
4021
4022fn delete_asset_metadata_record<E>(
4023    connection: &mut E,
4024    asset_id: AssetId,
4025) -> Result<bool, PgMetaError>
4026where
4027    E: MetadataExecutor,
4028{
4029    connection.with_transaction("delete asset record", |transaction| {
4030        for (operation, sql) in [
4031            (
4032                "delete dataset version relation links for asset",
4033                "DELETE FROM public.dataset_version_relation_instances WHERE version_id IN (SELECT version_id FROM public.asset_versions WHERE asset_id = $1)",
4034            ),
4035            (
4036                "delete dataset version entity links for asset",
4037                "DELETE FROM public.dataset_version_entities WHERE version_id IN (SELECT version_id FROM public.asset_versions WHERE asset_id = $1)",
4038            ),
4039            (
4040                "delete asset version relation links for asset",
4041                "DELETE FROM public.asset_version_relation_instances WHERE version_id IN (SELECT version_id FROM public.asset_versions WHERE asset_id = $1)",
4042            ),
4043            (
4044                "delete asset version entity links for asset",
4045                "DELETE FROM public.asset_version_entities WHERE version_id IN (SELECT version_id FROM public.asset_versions WHERE asset_id = $1)",
4046            ),
4047            (
4048                "delete dataset variable links for asset",
4049                "DELETE FROM public.dataset_variables WHERE dataset_id IN (SELECT version_id FROM public.asset_versions WHERE asset_id = $1)",
4050            ),
4051        ] {
4052            transaction.execute_command(operation, sql, &[&asset_id.0])?;
4053        }
4054        let deleted = transaction.execute_command(
4055            "delete asset record",
4056            "DELETE FROM public.assets WHERE asset_id = $1",
4057            &[&asset_id.0],
4058        )?;
4059        Ok(deleted > 0)
4060    })
4061}
4062
4063fn delete_study_metadata_record<E>(
4064    connection: &mut E,
4065    study_id: StudyId,
4066) -> Result<bool, PgMetaError>
4067where
4068    E: MetadataExecutor,
4069{
4070    connection.with_transaction("delete study record", |transaction| {
4071        for (operation, sql) in [
4072            (
4073                "delete dataset version relation links for study",
4074                "DELETE FROM public.dataset_version_relation_instances WHERE version_id IN (SELECT av.version_id FROM public.asset_versions av JOIN public.assets a ON a.asset_id = av.asset_id WHERE a.study_id = $1)",
4075            ),
4076            (
4077                "delete dataset version entity links for study",
4078                "DELETE FROM public.dataset_version_entities WHERE version_id IN (SELECT av.version_id FROM public.asset_versions av JOIN public.assets a ON a.asset_id = av.asset_id WHERE a.study_id = $1)",
4079            ),
4080            (
4081                "delete asset version relation links for study",
4082                "DELETE FROM public.asset_version_relation_instances WHERE version_id IN (SELECT av.version_id FROM public.asset_versions av JOIN public.assets a ON a.asset_id = av.asset_id WHERE a.study_id = $1)",
4083            ),
4084            (
4085                "delete asset version entity links for study",
4086                "DELETE FROM public.asset_version_entities WHERE version_id IN (SELECT av.version_id FROM public.asset_versions av JOIN public.assets a ON a.asset_id = av.asset_id WHERE a.study_id = $1)",
4087            ),
4088            (
4089                "delete dataset variable links for study",
4090                "DELETE FROM public.dataset_variables WHERE dataset_id IN (SELECT av.version_id FROM public.asset_versions av JOIN public.assets a ON a.asset_id = av.asset_id WHERE a.study_id = $1)",
4091            ),
4092            (
4093                "delete assets for study",
4094                "DELETE FROM public.assets WHERE study_id = $1",
4095            ),
4096        ] {
4097            transaction.execute_command(operation, sql, &[&study_id.0])?;
4098        }
4099        let deleted = transaction.execute_command(
4100            "delete study record",
4101            "DELETE FROM public.studies WHERE study_id = $1",
4102            &[&study_id.0],
4103        )?;
4104        Ok(deleted > 0)
4105    })
4106}
4107
4108fn delete_semantic_record<E>(
4109    connection: &mut E,
4110    table: &'static str,
4111    id_column: &'static str,
4112    id: i64,
4113) -> Result<bool, PgMetaError>
4114where
4115    E: MetadataExecutor,
4116{
4117    let id = i32::try_from(id).map_err(|_| PgMetaError::Decode {
4118        field: id_column,
4119        value: id.to_string(),
4120        message: "identifier does not fit into PostgreSQL INTEGER".to_string(),
4121    })?;
4122    let sql = format!("DELETE FROM public.{table} WHERE {id_column} = $1");
4123    let deleted = connection.execute_command("delete semantic catalog row", &sql, &[&id])?;
4124    Ok(deleted > 0)
4125}
4126
4127fn oauth_clear_asset_version_latest(
4128    connection: &mut LibpqOAuthConnection,
4129    asset_id: AssetId,
4130    version_id: VersionId,
4131) -> Result<(), PgMetaError> {
4132    MetadataExecutor::query_one(
4133        connection,
4134        "clear asset version latest",
4135        "UPDATE public.asset_versions SET is_latest = FALSE WHERE asset_id = $1 AND version_id = $2 RETURNING version_id::text",
4136        &[&asset_id.0, &version_id.0],
4137    )?;
4138    Ok(())
4139}
4140
4141fn oauth_list_datafiles_for_asset(
4142    connection: &mut LibpqOAuthConnection,
4143    asset_id: AssetId,
4144) -> Result<Vec<PgDataFileReadModel>, PgMetaError> {
4145    let rows = MetadataExecutor::query_many(
4146        connection,
4147        "list datafiles for asset",
4148        "
4149        SELECT df.datafile_id::text, df.size_bytes::text, df.compressed::text, df.encrypted::text,
4150               df.compression_algorithm, df.encryption_algorithm,
4151               encode(df.encryption_key, 'hex'), df.storage_uri, df.edam_format, df.digest
4152          FROM public.datafiles df
4153          JOIN public.asset_versions av ON av.version_id = df.datafile_id
4154         WHERE av.asset_id = $1
4155         ORDER BY av.major, av.minor, av.patch
4156        ",
4157        &[&asset_id.0],
4158    )?;
4159    rows.iter().map(oauth_datafile_from_row).collect()
4160}
4161
4162fn oauth_get_datafile(
4163    connection: &mut LibpqOAuthConnection,
4164    datafile_id: VersionId,
4165) -> Result<Option<PgDataFileReadModel>, PgMetaError> {
4166    let row = MetadataExecutor::query_optional(
4167        connection,
4168        "get datafile",
4169        "
4170        SELECT datafile_id::text, size_bytes::text, compressed::text, encrypted::text,
4171               compression_algorithm, encryption_algorithm,
4172               encode(encryption_key, 'hex'), storage_uri, edam_format, digest
4173          FROM public.datafiles
4174         WHERE datafile_id = $1
4175         LIMIT 1
4176        ",
4177        &[&datafile_id.0],
4178    )?;
4179    row.as_ref().map(oauth_datafile_from_row).transpose()
4180}
4181
4182fn oauth_create_datafile(
4183    connection: &mut LibpqOAuthConnection,
4184    input: PgCreateDataFile,
4185) -> Result<(), PgMetaError> {
4186    MetadataExecutor::execute_command(
4187        connection,
4188        "create datafile",
4189        "
4190        INSERT INTO public.datafiles (
4191            datafile_id, size_bytes, compressed, encrypted, compression_algorithm,
4192            encryption_algorithm, encryption_key, storage_uri, edam_format, digest
4193        )
4194        VALUES (
4195            $1, $2, COALESCE($3, FALSE), COALESCE($4, FALSE),
4196            COALESCE($5, 'zstd'), COALESCE($6, 'AES-256-CBC with PBKDF2'),
4197            $7, $8, $9, $10
4198        )
4199        ",
4200        &[
4201            &input.datafile_id.0,
4202            &input.size_bytes,
4203            &input.compressed,
4204            &input.encrypted,
4205            &input.compression_algorithm,
4206            &input.encryption_algorithm,
4207            &input.encryption_key,
4208            &input.storage_uri,
4209            &input.edam_format,
4210            &input.digest,
4211        ],
4212    )?;
4213    Ok(())
4214}
4215
4216fn oauth_list_value_types(
4217    connection: &mut LibpqOAuthConnection,
4218) -> Result<Vec<PgValueTypeReadModel>, PgMetaError> {
4219    let rows = MetadataExecutor::query_many(
4220        connection,
4221        "list value types",
4222        "
4223        SELECT value_type_id::text, value_type, description
4224          FROM public.value_types
4225         ORDER BY value_type_id
4226        ",
4227        &[],
4228    )?;
4229    rows.iter().map(oauth_value_type_from_row).collect()
4230}
4231
4232fn oauth_list_variables_for_domain(
4233    connection: &mut LibpqOAuthConnection,
4234    domain_id: DomainId,
4235) -> Result<Vec<PgVariableReadModel>, PgMetaError> {
4236    let domain_id = metadata_i32(domain_id.0, "domain_id")?;
4237    let rows = MetadataExecutor::query_many(
4238        connection,
4239        "list domain variables",
4240        "
4241        SELECT v.variable_id::text, v.domain_id::text, v.name, v.value_type_id::text,
4242               vt.value_type, v.value_format, v.vocabulary_id::text,
4243               v.keyrole::text, v.description, v.note,
4244               v.ontology_namespace, v.ontology_class
4245          FROM public.variables v
4246          JOIN public.value_types vt ON vt.value_type_id = v.value_type_id
4247         WHERE v.domain_id = $1
4248         ORDER BY v.name
4249        ",
4250        &[&domain_id],
4251    )?;
4252    rows.iter().map(oauth_variable_from_row).collect()
4253}
4254
4255fn oauth_get_variable(
4256    connection: &mut LibpqOAuthConnection,
4257    variable_id: VariableId,
4258) -> Result<Option<PgVariableReadModel>, PgMetaError> {
4259    let variable_id = metadata_i32(variable_id.0, "variable_id")?;
4260    let row = MetadataExecutor::query_optional(
4261        connection,
4262        "get variable",
4263        "
4264        SELECT v.variable_id::text, v.domain_id::text, v.name, v.value_type_id::text,
4265               vt.value_type, v.value_format, v.vocabulary_id::text,
4266               v.keyrole::text, v.description, v.note,
4267               v.ontology_namespace, v.ontology_class
4268          FROM public.variables v
4269          JOIN public.value_types vt ON vt.value_type_id = v.value_type_id
4270         WHERE v.variable_id = $1
4271         LIMIT 1
4272        ",
4273        &[&variable_id],
4274    )?;
4275    row.as_ref().map(oauth_variable_from_row).transpose()
4276}
4277
4278fn oauth_create_variable(
4279    connection: &mut LibpqOAuthConnection,
4280    input: PgCreateVariable,
4281) -> Result<VariableId, PgMetaError> {
4282    let key_role = key_role_sql(input.key_role)?;
4283    let domain_id = metadata_i32(input.domain_id.0, "domain_id")?;
4284    let value_type_id = metadata_i32(input.value_type_id.0, "value_type_id")?;
4285    let vocabulary_id = optional_metadata_i32(input.vocabulary_id.map(|id| id.0), "vocabulary_id")?;
4286    let row = match input.variable_id {
4287        Some(variable_id) => {
4288            let variable_id = metadata_i32(variable_id.0, "variable_id")?;
4289            MetadataExecutor::query_one(
4290                connection,
4291                "create variable",
4292                "
4293                INSERT INTO public.variables (
4294                    variable_id, domain_id, name, value_type_id, value_format, vocabulary_id,
4295                    keyrole, description, note, ontology_namespace, ontology_class
4296                )
4297                VALUES ($1, $2, $3, $4, $5, $6, CAST($7 AS text)::public.variable_keyrole_enum,
4298                        $8, $9, $10, $11)
4299                RETURNING variable_id::text
4300                ",
4301                &[
4302                    &variable_id,
4303                    &domain_id,
4304                    &input.name,
4305                    &value_type_id,
4306                    &input.value_format,
4307                    &vocabulary_id,
4308                    &key_role,
4309                    &input.description,
4310                    &input.note,
4311                    &input.ontology_namespace,
4312                    &input.ontology_class,
4313                ],
4314            )?
4315        }
4316        None => MetadataExecutor::query_one(
4317            connection,
4318            "create variable",
4319            "
4320            INSERT INTO public.variables (
4321                domain_id, name, value_type_id, value_format, vocabulary_id,
4322                keyrole, description, note, ontology_namespace, ontology_class
4323            )
4324            VALUES ($1, $2, $3, $4, $5, CAST($6 AS text)::public.variable_keyrole_enum,
4325                    $7, $8, $9, $10)
4326            RETURNING variable_id::text
4327            ",
4328            &[
4329                &domain_id,
4330                &input.name,
4331                &value_type_id,
4332                &input.value_format,
4333                &vocabulary_id,
4334                &key_role,
4335                &input.description,
4336                &input.note,
4337                &input.ontology_namespace,
4338                &input.ontology_class,
4339            ],
4340        )?,
4341    };
4342    Ok(VariableId(oauth_i64(&row, 0, "variable_id")?))
4343}
4344
4345fn oauth_update_variable(
4346    connection: &mut LibpqOAuthConnection,
4347    input: PgCreateVariable,
4348) -> Result<(), PgMetaError> {
4349    let variable_id = input.variable_id.ok_or_else(|| PgMetaError::Decode {
4350        field: "variable_id",
4351        value: "NULL".to_string(),
4352        message: "variable update requires a variable_id".to_string(),
4353    })?;
4354    let variable_id = metadata_i32(variable_id.0, "variable_id")?;
4355    let domain_id = metadata_i32(input.domain_id.0, "domain_id")?;
4356    let value_type_id = metadata_i32(input.value_type_id.0, "value_type_id")?;
4357    let vocabulary_id = optional_metadata_i32(input.vocabulary_id.map(|id| id.0), "vocabulary_id")?;
4358    let key_role = key_role_sql(input.key_role)?;
4359    MetadataExecutor::execute_command(
4360        connection,
4361        "update variable",
4362        "
4363        UPDATE public.variables
4364           SET domain_id = $2,
4365               name = $3,
4366               value_type_id = $4,
4367               value_format = $5,
4368               vocabulary_id = $6,
4369               keyrole = CAST($7 AS text)::public.variable_keyrole_enum,
4370               description = $8,
4371               note = $9,
4372               ontology_namespace = $10,
4373               ontology_class = $11
4374         WHERE variable_id = $1
4375        ",
4376        &[
4377            &variable_id,
4378            &domain_id,
4379            &input.name,
4380            &value_type_id,
4381            &input.value_format,
4382            &vocabulary_id,
4383            &key_role,
4384            &input.description,
4385            &input.note,
4386            &input.ontology_namespace,
4387            &input.ontology_class,
4388        ],
4389    )?;
4390    Ok(())
4391}
4392
4393fn oauth_link_dataset_variable(
4394    connection: &mut LibpqOAuthConnection,
4395    dataset_id: VersionId,
4396    variable_id: VariableId,
4397    row_role: KeyRole,
4398) -> Result<(), PgMetaError> {
4399    let variable_id = metadata_i32(variable_id.0, "variable_id")?;
4400    let row_role = key_role_sql(row_role)?;
4401    MetadataExecutor::execute_command(
4402        connection,
4403        "link dataset variable",
4404        "
4405        INSERT INTO public.dataset_variables (dataset_id, variable_id, row_role)
4406        VALUES ($1, $2, CAST($3 AS text)::public.variable_keyrole_enum)
4407        ON CONFLICT (dataset_id, variable_id) DO UPDATE
4408          SET row_role = EXCLUDED.row_role
4409        ",
4410        &[&dataset_id.0, &variable_id, &row_role],
4411    )?;
4412    Ok(())
4413}
4414
4415fn oauth_list_dataset_variables(
4416    connection: &mut LibpqOAuthConnection,
4417    dataset_id: VersionId,
4418) -> Result<Vec<PgDatasetVariableReadModel>, PgMetaError> {
4419    let rows = MetadataExecutor::query_many(
4420        connection,
4421        "list dataset variables",
4422        OAUTH_LIST_DATASET_VARIABLES_SQL,
4423        &[&dataset_id.0],
4424    )?;
4425    rows.iter().map(oauth_dataset_variable_from_row).collect()
4426}
4427
4428fn oauth_list_dataset_variables_for_variable(
4429    connection: &mut LibpqOAuthConnection,
4430    variable_id: VariableId,
4431) -> Result<Vec<PgDatasetVariableReadModel>, PgMetaError> {
4432    let variable_id = metadata_i32(variable_id.0, "variable_id")?;
4433    let rows = MetadataExecutor::query_many(
4434        connection,
4435        "list dataset variables for variable",
4436        "
4437        SELECT dataset_id::text, variable_id::text, row_role::text
4438          FROM public.dataset_variables
4439         WHERE variable_id = $1
4440         ORDER BY dataset_id
4441        ",
4442        &[&variable_id],
4443    )?;
4444    rows.iter().map(oauth_dataset_variable_from_row).collect()
4445}
4446
4447fn oauth_list_dataset_version_entities(
4448    connection: &mut LibpqOAuthConnection,
4449    version_id: VersionId,
4450) -> Result<Vec<PgDatasetVersionEntityReadModel>, PgMetaError> {
4451    let rows = MetadataExecutor::query_many(
4452        connection,
4453        "list dataset version entities",
4454        "
4455        SELECT version_id::text, entity_instance_id::text, entity_variable_id::text,
4456               transformation_id::text
4457          FROM public.dataset_version_entities
4458         WHERE version_id = $1
4459         ORDER BY entity_instance_id
4460        ",
4461        &[&version_id.0],
4462    )?;
4463    rows.iter()
4464        .map(oauth_dataset_version_entity_from_row)
4465        .collect()
4466}
4467
4468fn oauth_list_dataset_version_entities_for_instance(
4469    connection: &mut LibpqOAuthConnection,
4470    entity_instance_id: i64,
4471) -> Result<Vec<PgDatasetVersionEntityReadModel>, PgMetaError> {
4472    let rows = MetadataExecutor::query_many(
4473        connection,
4474        "list dataset version entities for instance",
4475        "
4476        SELECT version_id::text, entity_instance_id::text, entity_variable_id::text,
4477               transformation_id::text
4478          FROM public.dataset_version_entities
4479         WHERE entity_instance_id = $1
4480         ORDER BY version_id, entity_variable_id
4481        ",
4482        &[&entity_instance_id],
4483    )?;
4484    rows.iter()
4485        .map(oauth_dataset_version_entity_from_row)
4486        .collect()
4487}
4488
4489fn oauth_list_dataset_version_relation_instances(
4490    connection: &mut LibpqOAuthConnection,
4491    version_id: VersionId,
4492) -> Result<Vec<PgDatasetVersionRelationInstanceReadModel>, PgMetaError> {
4493    let rows = MetadataExecutor::query_many(
4494        connection,
4495        "list dataset version relation instances",
4496        "
4497        SELECT version_id::text, relation_instance_id::text, subject_variable_id::text,
4498               object_variable_id::text, relation_variable_id::text, transformation_id::text
4499          FROM public.dataset_version_relation_instances
4500         WHERE version_id = $1
4501         ORDER BY relation_instance_id
4502        ",
4503        &[&version_id.0],
4504    )?;
4505    rows.iter()
4506        .map(oauth_dataset_version_relation_instance_from_row)
4507        .collect()
4508}
4509
4510fn oauth_list_dataset_version_relation_instances_for_instance(
4511    connection: &mut LibpqOAuthConnection,
4512    relation_instance_id: i64,
4513) -> Result<Vec<PgDatasetVersionRelationInstanceReadModel>, PgMetaError> {
4514    let rows = MetadataExecutor::query_many(
4515        connection,
4516        "list dataset version relation instances for instance",
4517        "
4518        SELECT version_id::text, relation_instance_id::text, subject_variable_id::text,
4519               object_variable_id::text, relation_variable_id::text, transformation_id::text
4520          FROM public.dataset_version_relation_instances
4521         WHERE relation_instance_id = $1
4522         ORDER BY version_id
4523        ",
4524        &[&relation_instance_id],
4525    )?;
4526    rows.iter()
4527        .map(oauth_dataset_version_relation_instance_from_row)
4528        .collect()
4529}
4530
4531fn oauth_list_vocabularies_for_domain(
4532    connection: &mut LibpqOAuthConnection,
4533    domain_id: DomainId,
4534) -> Result<Vec<PgVocabularyReadModel>, PgMetaError> {
4535    let domain_id = metadata_i32(domain_id.0, "domain_id")?;
4536    let rows = MetadataExecutor::query_many(
4537        connection,
4538        "list vocabularies",
4539        "
4540        SELECT vocabulary_id::text, domain_id::text, name, description
4541          FROM public.vocabularies
4542         WHERE domain_id = $1
4543         ORDER BY name
4544        ",
4545        &[&domain_id],
4546    )?;
4547    rows.iter().map(oauth_vocabulary_from_row).collect()
4548}
4549
4550fn oauth_get_vocabulary(
4551    connection: &mut LibpqOAuthConnection,
4552    vocabulary_id: VocabularyId,
4553) -> Result<Option<PgVocabularyReadModel>, PgMetaError> {
4554    let vocabulary_id = metadata_i32(vocabulary_id.0, "vocabulary_id")?;
4555    let row = MetadataExecutor::query_optional(
4556        connection,
4557        "get vocabulary",
4558        "
4559        SELECT vocabulary_id::text, domain_id::text, name, description
4560          FROM public.vocabularies
4561         WHERE vocabulary_id = $1
4562         LIMIT 1
4563        ",
4564        &[&vocabulary_id],
4565    )?;
4566    row.as_ref().map(oauth_vocabulary_from_row).transpose()
4567}
4568
4569fn oauth_create_vocabulary(
4570    connection: &mut LibpqOAuthConnection,
4571    input: PgCreateVocabulary,
4572) -> Result<VocabularyId, PgMetaError> {
4573    let domain_id = metadata_i32(input.domain_id.0, "domain_id")?;
4574    let row = match input.vocabulary_id {
4575        Some(vocabulary_id) => {
4576            let vocabulary_id = metadata_i32(vocabulary_id.0, "vocabulary_id")?;
4577            MetadataExecutor::query_one(
4578                connection,
4579                "create vocabulary",
4580                "
4581                INSERT INTO public.vocabularies (vocabulary_id, domain_id, name, description)
4582                VALUES ($1, $2, $3, $4)
4583                RETURNING vocabulary_id::text
4584                ",
4585                &[&vocabulary_id, &domain_id, &input.name, &input.description],
4586            )?
4587        }
4588        None => MetadataExecutor::query_one(
4589            connection,
4590            "create vocabulary",
4591            "
4592            INSERT INTO public.vocabularies (domain_id, name, description)
4593            VALUES ($1, $2, $3)
4594            RETURNING vocabulary_id::text
4595            ",
4596            &[&domain_id, &input.name, &input.description],
4597        )?,
4598    };
4599    Ok(VocabularyId(oauth_i64(&row, 0, "vocabulary_id")?))
4600}
4601
4602fn oauth_update_vocabulary(
4603    connection: &mut LibpqOAuthConnection,
4604    input: PgCreateVocabulary,
4605) -> Result<(), PgMetaError> {
4606    let vocabulary_id = input.vocabulary_id.ok_or_else(|| PgMetaError::Decode {
4607        field: "vocabulary_id",
4608        value: "NULL".to_string(),
4609        message: "vocabulary update requires a vocabulary_id".to_string(),
4610    })?;
4611    let vocabulary_id = metadata_i32(vocabulary_id.0, "vocabulary_id")?;
4612    let domain_id = metadata_i32(input.domain_id.0, "domain_id")?;
4613    MetadataExecutor::execute_command(
4614        connection,
4615        "update vocabulary",
4616        "
4617        UPDATE public.vocabularies
4618           SET domain_id = $2, name = $3, description = $4
4619         WHERE vocabulary_id = $1
4620        ",
4621        &[&vocabulary_id, &domain_id, &input.name, &input.description],
4622    )?;
4623    Ok(())
4624}
4625
4626fn oauth_list_vocabulary_items(
4627    connection: &mut LibpqOAuthConnection,
4628    vocabulary_id: VocabularyId,
4629) -> Result<Vec<PgVocabularyItemReadModel>, PgMetaError> {
4630    let vocabulary_id = metadata_i32(vocabulary_id.0, "vocabulary_id")?;
4631    let rows = MetadataExecutor::query_many(
4632        connection,
4633        "list vocabulary items",
4634        "
4635        SELECT vocabulary_item_id::text, vocabulary_id::text, value::text, code, description
4636          FROM public.vocabulary_items
4637         WHERE vocabulary_id = $1
4638         ORDER BY value, code
4639        ",
4640        &[&vocabulary_id],
4641    )?;
4642    rows.iter().map(oauth_vocabulary_item_from_row).collect()
4643}
4644
4645fn oauth_get_vocabulary_item(
4646    connection: &mut LibpqOAuthConnection,
4647    vocabulary_item_id: VocabularyItemId,
4648) -> Result<Option<PgVocabularyItemReadModel>, PgMetaError> {
4649    let vocabulary_item_id = metadata_i32(vocabulary_item_id.0, "vocabulary_item_id")?;
4650    let row = MetadataExecutor::query_optional(
4651        connection,
4652        "get vocabulary item",
4653        "
4654        SELECT vocabulary_item_id::text, vocabulary_id::text, value::text, code, description
4655          FROM public.vocabulary_items
4656         WHERE vocabulary_item_id = $1
4657         LIMIT 1
4658        ",
4659        &[&vocabulary_item_id],
4660    )?;
4661    row.as_ref().map(oauth_vocabulary_item_from_row).transpose()
4662}
4663
4664fn oauth_save_vocabulary_items(
4665    connection: &mut LibpqOAuthConnection,
4666    items: &[VocabularyItemRecord],
4667) -> Result<(), PgMetaError> {
4668    connection.with_transaction("create vocabulary items", |transaction| {
4669        for item in items {
4670            let vocabulary_item_id = metadata_i32(item.vocabulary_item_id.0, "vocabulary_item_id")?;
4671            let vocabulary_id = metadata_i32(item.vocabulary_id.0, "vocabulary_id")?;
4672            transaction.execute_command(
4673                "create vocabulary item",
4674                "
4675                INSERT INTO public.vocabulary_items (
4676                    vocabulary_item_id, vocabulary_id, value, code, description
4677                )
4678                VALUES ($1, $2, $3, $4, $5)
4679                ",
4680                &[
4681                    &vocabulary_item_id,
4682                    &vocabulary_id,
4683                    &item.value,
4684                    &item.code,
4685                    &item.description,
4686                ],
4687            )?;
4688        }
4689        Ok(())
4690    })
4691}
4692
4693fn oauth_create_vocabulary_items(
4694    connection: &mut LibpqOAuthConnection,
4695    vocabulary_id: VocabularyId,
4696    items: &[NewVocabularyItemRecord],
4697) -> Result<Vec<VocabularyItemRecord>, PgMetaError> {
4698    connection.with_transaction("create vocabulary items", |transaction| {
4699        let vocabulary_id_i32 = metadata_i32(vocabulary_id.0, "vocabulary_id")?;
4700        let mut created = Vec::with_capacity(items.len());
4701        for item in items {
4702            let row = transaction.query_one(
4703                "create vocabulary item",
4704                "
4705                INSERT INTO public.vocabulary_items (vocabulary_id, value, code, description)
4706                VALUES ($1, $2, $3, $4)
4707                RETURNING vocabulary_item_id::text
4708                ",
4709                &[
4710                    &vocabulary_id_i32,
4711                    &item.value,
4712                    &item.code,
4713                    &item.description,
4714                ],
4715            )?;
4716            created.push(VocabularyItemRecord {
4717                vocabulary_item_id: VocabularyItemId(oauth_i64(&row, 0, "vocabulary_item_id")?),
4718                vocabulary_id,
4719                value: item.value,
4720                code: item.code.clone(),
4721                description: item.description.clone(),
4722            });
4723        }
4724        Ok(created)
4725    })
4726}
4727
4728fn oauth_update_vocabulary_items(
4729    connection: &mut LibpqOAuthConnection,
4730    items: &[VocabularyItemRecord],
4731) -> Result<(), PgMetaError> {
4732    connection.with_transaction("update vocabulary items", |transaction| {
4733        for item in items {
4734            let vocabulary_item_id = metadata_i32(item.vocabulary_item_id.0, "vocabulary_item_id")?;
4735            let vocabulary_id = metadata_i32(item.vocabulary_id.0, "vocabulary_id")?;
4736            transaction.execute_command(
4737                "update vocabulary item",
4738                "
4739                UPDATE public.vocabulary_items
4740                   SET vocabulary_id = $2, value = $3, code = $4, description = $5
4741                 WHERE vocabulary_item_id = $1
4742                ",
4743                &[
4744                    &vocabulary_item_id,
4745                    &vocabulary_id,
4746                    &item.value,
4747                    &item.code,
4748                    &item.description,
4749                ],
4750            )?;
4751        }
4752        Ok(())
4753    })
4754}
4755
4756fn oauth_list_vocabulary_mappings(
4757    connection: &mut LibpqOAuthConnection,
4758) -> Result<Vec<crate::PgVocabularyMappingReadModel>, PgMetaError> {
4759    let rows = MetadataExecutor::query_many(
4760        connection,
4761        "list vocabulary mappings",
4762        "
4763        SELECT vocabulary_mapping_id::text, from_vocabulary_item::text, to_vocabulary_item::text
4764          FROM public.vocabulary_mapping
4765         ORDER BY vocabulary_mapping_id
4766        ",
4767        &[],
4768    )?;
4769    rows.iter().map(oauth_vocabulary_mapping_from_row).collect()
4770}
4771
4772fn oauth_create_vocabulary_mapping(
4773    connection: &mut LibpqOAuthConnection,
4774    input: PgCreateVocabularyMapping,
4775) -> Result<i64, PgMetaError> {
4776    let from_vocabulary_item = metadata_i32(input.from_vocabulary_item.0, "from_vocabulary_item")?;
4777    let to_vocabulary_item = metadata_i32(input.to_vocabulary_item.0, "to_vocabulary_item")?;
4778    let row = match input.vocabulary_mapping_id {
4779        Some(vocabulary_mapping_id) => {
4780            let vocabulary_mapping_id =
4781                metadata_i32(vocabulary_mapping_id, "vocabulary_mapping_id")?;
4782            MetadataExecutor::query_one(
4783                connection,
4784                "create vocabulary mapping",
4785                "
4786                INSERT INTO public.vocabulary_mapping (
4787                    vocabulary_mapping_id, from_vocabulary_item, to_vocabulary_item
4788                )
4789                VALUES ($1, $2, $3)
4790                RETURNING vocabulary_mapping_id::text
4791                ",
4792                &[
4793                    &vocabulary_mapping_id,
4794                    &from_vocabulary_item,
4795                    &to_vocabulary_item,
4796                ],
4797            )?
4798        }
4799        None => MetadataExecutor::query_one(
4800            connection,
4801            "create vocabulary mapping",
4802            "
4803            INSERT INTO public.vocabulary_mapping (from_vocabulary_item, to_vocabulary_item)
4804            VALUES ($1, $2)
4805            RETURNING vocabulary_mapping_id::text
4806            ",
4807            &[&from_vocabulary_item, &to_vocabulary_item],
4808        )?,
4809    };
4810    oauth_i64(&row, 0, "vocabulary_mapping_id")
4811}
4812
4813fn oauth_list_entities_for_domain(
4814    connection: &mut LibpqOAuthConnection,
4815    domain_id: DomainId,
4816) -> Result<Vec<PgEntityReadModel>, PgMetaError> {
4817    let domain_id = metadata_i32(domain_id.0, "domain_id")?;
4818    let rows = MetadataExecutor::query_many(
4819        connection,
4820        "list entities",
4821        "
4822        SELECT entity_id::text, domain_id::text, uuid::text, name, description,
4823               agent_instructions, ontology_namespace, ontology_class
4824          FROM public.entities
4825         WHERE domain_id = $1
4826         ORDER BY name
4827        ",
4828        &[&domain_id],
4829    )?;
4830    rows.iter().map(oauth_entity_from_row).collect()
4831}
4832
4833fn oauth_create_entity(
4834    connection: &mut LibpqOAuthConnection,
4835    input: PgCreateEntity,
4836) -> Result<EntityId, PgMetaError> {
4837    let domain_id = metadata_i32(input.domain_id.0, "domain_id")?;
4838    let row = match input.entity_id {
4839        Some(entity_id) => {
4840            let entity_id = metadata_i32(entity_id.0, "entity_id")?;
4841            MetadataExecutor::query_one(
4842                connection,
4843                "create entity",
4844                "
4845                INSERT INTO public.entities (
4846                    entity_id, domain_id, uuid, name, description,
4847                    agent_instructions, ontology_namespace, ontology_class
4848                )
4849                VALUES ($1, $2, COALESCE($3, uuidv7()), $4, $5, $6, $7, $8)
4850                RETURNING entity_id::text
4851                ",
4852                &[
4853                    &entity_id,
4854                    &domain_id,
4855                    &input.uuid,
4856                    &input.name,
4857                    &input.description,
4858                    &input.agent_instructions,
4859                    &input.ontology_namespace,
4860                    &input.ontology_class,
4861                ],
4862            )?
4863        }
4864        None => MetadataExecutor::query_one(
4865            connection,
4866            "create entity",
4867            "
4868            INSERT INTO public.entities (
4869                domain_id, uuid, name, description, agent_instructions,
4870                ontology_namespace, ontology_class
4871            )
4872            VALUES ($1, COALESCE($2, uuidv7()), $3, $4, $5, $6, $7)
4873            RETURNING entity_id::text
4874            ",
4875            &[
4876                &domain_id,
4877                &input.uuid,
4878                &input.name,
4879                &input.description,
4880                &input.agent_instructions,
4881                &input.ontology_namespace,
4882                &input.ontology_class,
4883            ],
4884        )?,
4885    };
4886    Ok(EntityId(oauth_i64(&row, 0, "entity_id")?))
4887}
4888
4889fn oauth_list_entity_relations_for_domain(
4890    connection: &mut LibpqOAuthConnection,
4891    domain_id: DomainId,
4892) -> Result<Vec<PgEntityRelationReadModel>, PgMetaError> {
4893    let domain_id = metadata_i32(domain_id.0, "domain_id")?;
4894    let rows = MetadataExecutor::query_many(
4895        connection,
4896        "list entity relations",
4897        "
4898        SELECT entityrelation_id::text, subject_entity_id::text, object_entity_id::text,
4899               domain_id::text, uuid::text, name, description, agent_instructions,
4900               ontology_namespace, ontology_class
4901          FROM public.entityrelations
4902         WHERE domain_id = $1
4903         ORDER BY name
4904        ",
4905        &[&domain_id],
4906    )?;
4907    rows.iter().map(oauth_entity_relation_from_row).collect()
4908}
4909
4910fn oauth_create_entity_relation(
4911    connection: &mut LibpqOAuthConnection,
4912    input: PgCreateEntityRelation,
4913) -> Result<EntityRelationId, PgMetaError> {
4914    let subject_entity_id = metadata_i32(input.subject_entity_id.0, "subject_entity_id")?;
4915    let object_entity_id = metadata_i32(input.object_entity_id.0, "object_entity_id")?;
4916    let domain_id = metadata_i32(input.domain_id.0, "domain_id")?;
4917    let row = match input.entity_relation_id {
4918        Some(entity_relation_id) => {
4919            let entity_relation_id = metadata_i32(entity_relation_id.0, "entityrelation_id")?;
4920            MetadataExecutor::query_one(
4921                connection,
4922                "create entity relation",
4923                "
4924                INSERT INTO public.entityrelations (
4925                    entityrelation_id, subject_entity_id, object_entity_id,
4926                    domain_id, uuid, name, description, agent_instructions,
4927                    ontology_namespace, ontology_class
4928                )
4929                VALUES ($1, $2, $3, $4, COALESCE($5, uuidv7()), $6, $7, $8, $9, $10)
4930                RETURNING entityrelation_id::text
4931                ",
4932                &[
4933                    &entity_relation_id,
4934                    &subject_entity_id,
4935                    &object_entity_id,
4936                    &domain_id,
4937                    &input.uuid,
4938                    &input.name,
4939                    &input.description,
4940                    &input.agent_instructions,
4941                    &input.ontology_namespace,
4942                    &input.ontology_class,
4943                ],
4944            )?
4945        }
4946        None => MetadataExecutor::query_one(
4947            connection,
4948            "create entity relation",
4949            "
4950            INSERT INTO public.entityrelations (
4951                subject_entity_id, object_entity_id, domain_id, uuid, name, description,
4952                agent_instructions, ontology_namespace, ontology_class
4953            )
4954            VALUES ($1, $2, $3, COALESCE($4, uuidv7()), $5, $6, $7, $8, $9)
4955            RETURNING entityrelation_id::text
4956            ",
4957            &[
4958                &subject_entity_id,
4959                &object_entity_id,
4960                &domain_id,
4961                &input.uuid,
4962                &input.name,
4963                &input.description,
4964                &input.agent_instructions,
4965                &input.ontology_namespace,
4966                &input.ontology_class,
4967            ],
4968        )?,
4969    };
4970    Ok(EntityRelationId(oauth_i64(&row, 0, "entityrelation_id")?))
4971}
4972
4973fn oauth_get_entity_instance(
4974    connection: &mut LibpqOAuthConnection,
4975    instance_id: i64,
4976) -> Result<Option<PgEntityInstanceReadModel>, PgMetaError> {
4977    let row = MetadataExecutor::query_optional(
4978        connection,
4979        "get entity instance",
4980        "
4981        SELECT instance_id::text, uuid::text, entity_id::text, label, note, transformation_id::text
4982          FROM public.entity_instances
4983         WHERE instance_id = $1
4984         LIMIT 1
4985        ",
4986        &[&instance_id],
4987    )?;
4988    row.as_ref().map(oauth_entity_instance_from_row).transpose()
4989}
4990
4991fn oauth_get_entity_instance_by_uuid(
4992    connection: &mut LibpqOAuthConnection,
4993    uuid: uuid::Uuid,
4994) -> Result<Option<PgEntityInstanceReadModel>, PgMetaError> {
4995    let row = MetadataExecutor::query_optional(
4996        connection,
4997        "get entity instance by uuid",
4998        "
4999        SELECT instance_id::text, uuid::text, entity_id::text, label, note, transformation_id::text
5000          FROM public.entity_instances
5001         WHERE uuid = $1
5002         LIMIT 1
5003        ",
5004        &[&uuid],
5005    )?;
5006    row.as_ref().map(oauth_entity_instance_from_row).transpose()
5007}
5008
5009fn oauth_list_entity_instances_for_entity(
5010    connection: &mut LibpqOAuthConnection,
5011    entity_id: EntityId,
5012) -> Result<Vec<PgEntityInstanceReadModel>, PgMetaError> {
5013    let entity_id = metadata_i32(entity_id.0, "entity_id")?;
5014    let rows = MetadataExecutor::query_many(
5015        connection,
5016        "list entity instances",
5017        "
5018        SELECT instance_id::text, uuid::text, entity_id::text, label, note, transformation_id::text
5019          FROM public.entity_instances
5020         WHERE entity_id = $1
5021         ORDER BY instance_id
5022        ",
5023        &[&entity_id],
5024    )?;
5025    rows.iter().map(oauth_entity_instance_from_row).collect()
5026}
5027
5028fn oauth_save_entity_instance(
5029    connection: &mut LibpqOAuthConnection,
5030    input: PgWriteEntityInstance,
5031) -> Result<i64, PgMetaError> {
5032    let entity_id = metadata_i32(input.entity_id.0, "entity_id")?;
5033    let transformation_id = metadata_i32(input.transformation_id.0, "transformation_id")?;
5034    let row = match input.instance_id {
5035        Some(instance_id) => MetadataExecutor::query_one(
5036            connection,
5037            "save entity instance",
5038            "
5039            UPDATE public.entity_instances
5040               SET entity_id = $2,
5041                   label = $3,
5042                   note = $4,
5043                   transformation_id = $5
5044             WHERE instance_id = $1
5045               AND ($6::uuid IS NULL OR uuid = $6)
5046            RETURNING instance_id::text
5047            ",
5048            &[
5049                &instance_id,
5050                &entity_id,
5051                &input.label,
5052                &input.note,
5053                &transformation_id,
5054                &input.uuid,
5055            ],
5056        )?,
5057        None => MetadataExecutor::query_one(
5058            connection,
5059            "save entity instance",
5060            "
5061            INSERT INTO public.entity_instances (uuid, entity_id, label, note, transformation_id)
5062            VALUES (COALESCE($1, uuidv7()), $2, $3, $4, $5)
5063            RETURNING instance_id::text
5064            ",
5065            &[
5066                &input.uuid,
5067                &entity_id,
5068                &input.label,
5069                &input.note,
5070                &transformation_id,
5071            ],
5072        )?,
5073    };
5074    oauth_i64(&row, 0, "instance_id")
5075}
5076
5077fn oauth_get_study_entity_instance(
5078    connection: &mut LibpqOAuthConnection,
5079    study_id: StudyId,
5080    entity_id: EntityId,
5081    external_id: &str,
5082) -> Result<Option<PgStudyEntityInstanceReadModel>, PgMetaError> {
5083    let entity_id = metadata_i32(entity_id.0, "entity_id")?;
5084    let row = MetadataExecutor::query_optional(
5085        connection,
5086        "get study entity instance",
5087        "
5088        SELECT study_id::text, entity_instance_id::text, entity_id::text,
5089               external_id, transformation_id::text
5090          FROM public.study_entity_instances
5091         WHERE study_id = $1 AND entity_id = $2 AND external_id = $3
5092         LIMIT 1
5093        ",
5094        &[&study_id.0, &entity_id, &external_id],
5095    )?;
5096    row.as_ref()
5097        .map(oauth_study_entity_instance_from_row)
5098        .transpose()
5099}
5100
5101fn oauth_list_study_entity_instances(
5102    connection: &mut LibpqOAuthConnection,
5103    study_id: StudyId,
5104) -> Result<Vec<PgStudyEntityInstanceReadModel>, PgMetaError> {
5105    let rows = MetadataExecutor::query_many(
5106        connection,
5107        "list study entity instances",
5108        "
5109        SELECT study_id::text, entity_instance_id::text, entity_id::text,
5110               external_id, transformation_id::text
5111          FROM public.study_entity_instances
5112         WHERE study_id = $1
5113         ORDER BY entity_instance_id
5114        ",
5115        &[&study_id.0],
5116    )?;
5117    rows.iter()
5118        .map(oauth_study_entity_instance_from_row)
5119        .collect()
5120}
5121
5122fn oauth_save_study_entity_instance(
5123    connection: &mut LibpqOAuthConnection,
5124    input: PgWriteStudyEntityInstance,
5125) -> Result<(), PgMetaError> {
5126    let entity_id = metadata_i32(input.entity_id.0, "entity_id")?;
5127    let transformation_id = metadata_i32(input.transformation_id.0, "transformation_id")?;
5128    MetadataExecutor::execute_command(
5129        connection,
5130        "save study entity instance",
5131        "
5132        INSERT INTO public.study_entity_instances (
5133            study_id, entity_instance_id, entity_id, external_id, transformation_id
5134        )
5135        VALUES ($1, $2, $3, $4, $5)
5136        ON CONFLICT (study_id, entity_instance_id) DO UPDATE
5137          SET entity_id = EXCLUDED.entity_id,
5138              external_id = EXCLUDED.external_id,
5139              transformation_id = EXCLUDED.transformation_id
5140        ",
5141        &[
5142            &input.study_id.0,
5143            &input.entity_instance_id,
5144            &entity_id,
5145            &input.external_id,
5146            &transformation_id,
5147        ],
5148    )?;
5149    Ok(())
5150}
5151
5152fn oauth_get_relation_instance(
5153    connection: &mut LibpqOAuthConnection,
5154    relation_instance_id: i64,
5155) -> Result<Option<PgRelationInstanceReadModel>, PgMetaError> {
5156    let row = MetadataExecutor::query_optional(
5157        connection,
5158        "get relation instance",
5159        "
5160        SELECT relation_instance_id::text, uuid::text, entityrelation_id::text,
5161               entity_instance_id_1::text, entity_instance_id_2::text,
5162               valid_from::text, valid_to::text, note, transformation_id::text
5163          FROM public.relation_instances
5164         WHERE relation_instance_id = $1
5165         LIMIT 1
5166        ",
5167        &[&relation_instance_id],
5168    )?;
5169    row.as_ref()
5170        .map(oauth_relation_instance_from_row)
5171        .transpose()
5172}
5173
5174fn oauth_get_relation_instance_by_uuid(
5175    connection: &mut LibpqOAuthConnection,
5176    uuid: uuid::Uuid,
5177) -> Result<Option<PgRelationInstanceReadModel>, PgMetaError> {
5178    let row = MetadataExecutor::query_optional(
5179        connection,
5180        "get relation instance by uuid",
5181        "
5182        SELECT relation_instance_id::text, uuid::text, entityrelation_id::text,
5183               entity_instance_id_1::text, entity_instance_id_2::text,
5184               valid_from::text, valid_to::text, note, transformation_id::text
5185          FROM public.relation_instances
5186         WHERE uuid = $1
5187         LIMIT 1
5188        ",
5189        &[&uuid],
5190    )?;
5191    row.as_ref()
5192        .map(oauth_relation_instance_from_row)
5193        .transpose()
5194}
5195
5196fn oauth_list_relation_instances_for_relation(
5197    connection: &mut LibpqOAuthConnection,
5198    relation_id: EntityRelationId,
5199) -> Result<Vec<PgRelationInstanceReadModel>, PgMetaError> {
5200    let relation_id = metadata_i32(relation_id.0, "entityrelation_id")?;
5201    let rows = MetadataExecutor::query_many(
5202        connection,
5203        "list relation instances",
5204        "
5205        SELECT relation_instance_id::text, uuid::text, entityrelation_id::text,
5206               entity_instance_id_1::text, entity_instance_id_2::text,
5207               valid_from::text, valid_to::text, note, transformation_id::text
5208          FROM public.relation_instances
5209         WHERE entityrelation_id = $1
5210         ORDER BY relation_instance_id
5211        ",
5212        &[&relation_id],
5213    )?;
5214    rows.iter().map(oauth_relation_instance_from_row).collect()
5215}
5216
5217fn oauth_save_relation_instance(
5218    connection: &mut LibpqOAuthConnection,
5219    input: PgWriteRelationInstance,
5220) -> Result<i64, PgMetaError> {
5221    let entity_relation_id = metadata_i32(input.entity_relation_id.0, "entityrelation_id")?;
5222    let transformation_id = metadata_i32(input.transformation_id.0, "transformation_id")?;
5223    let row = match input.relation_instance_id {
5224        Some(relation_instance_id) => MetadataExecutor::query_one(
5225            connection,
5226            "save relation instance",
5227            "
5228            UPDATE public.relation_instances
5229               SET entityrelation_id = $2,
5230                   entity_instance_id_1 = $3,
5231                   entity_instance_id_2 = $4,
5232                   valid_from = $5,
5233                   valid_to = $6,
5234                   note = $7,
5235                   transformation_id = $8
5236             WHERE relation_instance_id = $1
5237               AND ($9::uuid IS NULL OR uuid = $9)
5238            RETURNING relation_instance_id::text
5239            ",
5240            &[
5241                &relation_instance_id,
5242                &entity_relation_id,
5243                &input.entity_instance_id_1,
5244                &input.entity_instance_id_2,
5245                &input.valid_from,
5246                &input.valid_to,
5247                &input.note,
5248                &transformation_id,
5249                &input.uuid,
5250            ],
5251        )?,
5252        None => MetadataExecutor::query_one(
5253            connection,
5254            "save relation instance",
5255            "
5256            INSERT INTO public.relation_instances (
5257                uuid, entityrelation_id, entity_instance_id_1, entity_instance_id_2,
5258                valid_from, valid_to, note, transformation_id
5259            )
5260            VALUES (COALESCE($1, uuidv7()), $2, $3, $4, $5, $6, $7, $8)
5261            RETURNING relation_instance_id::text
5262            ",
5263            &[
5264                &input.uuid,
5265                &entity_relation_id,
5266                &input.entity_instance_id_1,
5267                &input.entity_instance_id_2,
5268                &input.valid_from,
5269                &input.valid_to,
5270                &input.note,
5271                &transformation_id,
5272            ],
5273        )?,
5274    };
5275    oauth_i64(&row, 0, "relation_instance_id")
5276}
5277
5278fn oauth_get_study_relation_instance(
5279    connection: &mut LibpqOAuthConnection,
5280    study_id: StudyId,
5281    relation_id: EntityRelationId,
5282    external_id: &str,
5283) -> Result<Option<PgStudyRelationInstanceReadModel>, PgMetaError> {
5284    let relation_id = metadata_i32(relation_id.0, "entityrelation_id")?;
5285    let row = MetadataExecutor::query_optional(
5286        connection,
5287        "get study relation instance",
5288        "
5289        SELECT study_id::text, relation_instance_id::text, entityrelation_id::text,
5290               external_id, transformation_id::text
5291          FROM public.study_relation_instances
5292         WHERE study_id = $1 AND entityrelation_id = $2 AND external_id = $3
5293         LIMIT 1
5294        ",
5295        &[&study_id.0, &relation_id, &external_id],
5296    )?;
5297    row.as_ref()
5298        .map(oauth_study_relation_instance_from_row)
5299        .transpose()
5300}
5301
5302fn oauth_list_study_relation_instances(
5303    connection: &mut LibpqOAuthConnection,
5304    study_id: StudyId,
5305) -> Result<Vec<PgStudyRelationInstanceReadModel>, PgMetaError> {
5306    let rows = MetadataExecutor::query_many(
5307        connection,
5308        "list study relation instances",
5309        "
5310        SELECT study_id::text, relation_instance_id::text, entityrelation_id::text,
5311               external_id, transformation_id::text
5312          FROM public.study_relation_instances
5313         WHERE study_id = $1
5314         ORDER BY relation_instance_id
5315        ",
5316        &[&study_id.0],
5317    )?;
5318    rows.iter()
5319        .map(oauth_study_relation_instance_from_row)
5320        .collect()
5321}
5322
5323fn oauth_save_study_relation_instance(
5324    connection: &mut LibpqOAuthConnection,
5325    input: PgWriteStudyRelationInstance,
5326) -> Result<(), PgMetaError> {
5327    let entity_relation_id = metadata_i32(input.entity_relation_id.0, "entityrelation_id")?;
5328    let transformation_id = metadata_i32(input.transformation_id.0, "transformation_id")?;
5329    MetadataExecutor::execute_command(
5330        connection,
5331        "save study relation instance",
5332        "
5333        INSERT INTO public.study_relation_instances (
5334            study_id, relation_instance_id, entityrelation_id,
5335            external_id, transformation_id
5336        )
5337        VALUES ($1, $2, $3, $4, $5)
5338        ON CONFLICT (study_id, relation_instance_id) DO UPDATE
5339          SET entityrelation_id = EXCLUDED.entityrelation_id,
5340              external_id = EXCLUDED.external_id,
5341              transformation_id = EXCLUDED.transformation_id
5342        ",
5343        &[
5344            &input.study_id.0,
5345            &input.relation_instance_id,
5346            &entity_relation_id,
5347            &input.external_id,
5348            &transformation_id,
5349        ],
5350    )?;
5351    Ok(())
5352}
5353
5354fn oauth_link_asset_version_entity(
5355    connection: &mut LibpqOAuthConnection,
5356    link: &AssetVersionEntityRecord,
5357) -> Result<(), PgMetaError> {
5358    let transformation_id = metadata_i32(link.transformation_id.0, "transformation_id")?;
5359    MetadataExecutor::execute_command(
5360        connection,
5361        "link asset version entity",
5362        "
5363        INSERT INTO public.asset_version_entities (
5364            version_id, entity_instance_id, transformation_id
5365        )
5366        VALUES ($1, $2, $3)
5367        ON CONFLICT (version_id, entity_instance_id) DO UPDATE
5368          SET transformation_id = EXCLUDED.transformation_id
5369        ",
5370        &[
5371            &link.version_id.0,
5372            &link.entity_instance_id,
5373            &transformation_id,
5374        ],
5375    )?;
5376    Ok(())
5377}
5378
5379fn oauth_list_asset_version_entities(
5380    connection: &mut LibpqOAuthConnection,
5381    version_id: VersionId,
5382) -> Result<Vec<PgAssetVersionEntityReadModel>, PgMetaError> {
5383    let rows = MetadataExecutor::query_many(
5384        connection,
5385        "list asset version entities",
5386        "
5387        SELECT version_id::text, entity_instance_id::text, transformation_id::text
5388          FROM public.asset_version_entities
5389         WHERE version_id = $1
5390         ORDER BY entity_instance_id
5391        ",
5392        &[&version_id.0],
5393    )?;
5394    rows.iter()
5395        .map(oauth_asset_version_entity_from_row)
5396        .collect()
5397}
5398
5399fn oauth_list_asset_version_entities_for_instance(
5400    connection: &mut LibpqOAuthConnection,
5401    entity_instance_id: i64,
5402) -> Result<Vec<PgAssetVersionEntityReadModel>, PgMetaError> {
5403    let rows = MetadataExecutor::query_many(
5404        connection,
5405        "list asset version entities for instance",
5406        "
5407        SELECT version_id::text, entity_instance_id::text, transformation_id::text
5408          FROM public.asset_version_entities
5409         WHERE entity_instance_id = $1
5410         ORDER BY version_id
5411        ",
5412        &[&entity_instance_id],
5413    )?;
5414    rows.iter()
5415        .map(oauth_asset_version_entity_from_row)
5416        .collect()
5417}
5418
5419fn oauth_link_dataset_version_entity(
5420    connection: &mut LibpqOAuthConnection,
5421    link: &DatasetVersionEntityRecord,
5422) -> Result<(), PgMetaError> {
5423    connection.with_transaction("link dataset version entity", |transaction| {
5424        oauth_link_dataset_version_entity_in_transaction(transaction, link)
5425    })
5426}
5427
5428fn oauth_link_dataset_version_entities(
5429    connection: &mut LibpqOAuthConnection,
5430    links: &[DatasetVersionEntityRecord],
5431) -> Result<(), PgMetaError> {
5432    connection.with_transaction("bulk link dataset version entities", |transaction| {
5433        for link in links {
5434            oauth_link_dataset_version_entity_in_transaction(transaction, link)?;
5435        }
5436        Ok(())
5437    })
5438}
5439
5440fn oauth_link_dataset_version_entity_in_transaction(
5441    transaction: &mut dyn MetadataTransaction<Row = LibpqOAuthRow>,
5442    link: &DatasetVersionEntityRecord,
5443) -> Result<(), PgMetaError> {
5444    let entity_variable_id = metadata_i32(link.entity_variable_id.0, "entity_variable_id")?;
5445    let transformation_id = metadata_i32(link.transformation_id.0, "transformation_id")?;
5446    transaction.execute_command(
5447        "link asset version entity",
5448        "
5449        INSERT INTO public.asset_version_entities (
5450            version_id, entity_instance_id, transformation_id
5451        )
5452        VALUES ($1, $2, $3)
5453        ON CONFLICT (version_id, entity_instance_id) DO UPDATE
5454          SET transformation_id = EXCLUDED.transformation_id
5455        ",
5456        &[
5457            &link.version_id.0,
5458            &link.entity_instance_id,
5459            &transformation_id,
5460        ],
5461    )?;
5462    transaction.execute_command(
5463        "link dataset version entity",
5464        "
5465        INSERT INTO public.dataset_version_entities (
5466            version_id, entity_instance_id, entity_variable_id, transformation_id
5467        )
5468        VALUES ($1, $2, $3, $4)
5469        ON CONFLICT (version_id, entity_instance_id) DO UPDATE
5470          SET entity_variable_id = EXCLUDED.entity_variable_id,
5471              transformation_id = EXCLUDED.transformation_id
5472        ",
5473        &[
5474            &link.version_id.0,
5475            &link.entity_instance_id,
5476            &entity_variable_id,
5477            &transformation_id,
5478        ],
5479    )?;
5480    Ok(())
5481}
5482
5483fn oauth_link_asset_version_relation_instance(
5484    connection: &mut LibpqOAuthConnection,
5485    link: &AssetVersionRelationInstanceRecord,
5486) -> Result<(), PgMetaError> {
5487    let transformation_id = metadata_i32(link.transformation_id.0, "transformation_id")?;
5488    MetadataExecutor::execute_command(
5489        connection,
5490        "link asset version relation instance",
5491        "
5492        INSERT INTO public.asset_version_relation_instances (
5493            version_id, relation_instance_id, transformation_id
5494        )
5495        VALUES ($1, $2, $3)
5496        ON CONFLICT (version_id, relation_instance_id) DO UPDATE
5497          SET transformation_id = EXCLUDED.transformation_id
5498        ",
5499        &[
5500            &link.version_id.0,
5501            &link.relation_instance_id,
5502            &transformation_id,
5503        ],
5504    )?;
5505    Ok(())
5506}
5507
5508fn oauth_list_asset_version_relation_instances(
5509    connection: &mut LibpqOAuthConnection,
5510    version_id: VersionId,
5511) -> Result<Vec<PgAssetVersionRelationInstanceReadModel>, PgMetaError> {
5512    let rows = MetadataExecutor::query_many(
5513        connection,
5514        "list asset version relation instances",
5515        "
5516        SELECT version_id::text, relation_instance_id::text, transformation_id::text
5517          FROM public.asset_version_relation_instances
5518         WHERE version_id = $1
5519         ORDER BY relation_instance_id
5520        ",
5521        &[&version_id.0],
5522    )?;
5523    rows.iter()
5524        .map(oauth_asset_version_relation_instance_from_row)
5525        .collect()
5526}
5527
5528fn oauth_list_asset_version_relation_instances_for_instance(
5529    connection: &mut LibpqOAuthConnection,
5530    relation_instance_id: i64,
5531) -> Result<Vec<PgAssetVersionRelationInstanceReadModel>, PgMetaError> {
5532    let rows = MetadataExecutor::query_many(
5533        connection,
5534        "list asset version relation instances for instance",
5535        "
5536        SELECT version_id::text, relation_instance_id::text, transformation_id::text
5537          FROM public.asset_version_relation_instances
5538         WHERE relation_instance_id = $1
5539         ORDER BY version_id
5540        ",
5541        &[&relation_instance_id],
5542    )?;
5543    rows.iter()
5544        .map(oauth_asset_version_relation_instance_from_row)
5545        .collect()
5546}
5547
5548fn oauth_link_dataset_version_relation_instance(
5549    connection: &mut LibpqOAuthConnection,
5550    link: &DatasetVersionRelationInstanceRecord,
5551) -> Result<(), PgMetaError> {
5552    connection.with_transaction("link dataset version relation instance", |transaction| {
5553        oauth_link_dataset_version_relation_in_transaction(transaction, link)
5554    })
5555}
5556
5557fn oauth_link_dataset_version_relation_instances(
5558    connection: &mut LibpqOAuthConnection,
5559    links: &[DatasetVersionRelationInstanceRecord],
5560) -> Result<(), PgMetaError> {
5561    connection.with_transaction(
5562        "bulk link dataset version relation instances",
5563        |transaction| {
5564            for link in links {
5565                oauth_link_dataset_version_relation_in_transaction(transaction, link)?;
5566            }
5567            Ok(())
5568        },
5569    )
5570}
5571
5572fn oauth_link_dataset_version_relation_in_transaction(
5573    transaction: &mut dyn MetadataTransaction<Row = LibpqOAuthRow>,
5574    link: &DatasetVersionRelationInstanceRecord,
5575) -> Result<(), PgMetaError> {
5576    let subject_variable_id = metadata_i32(link.subject_variable_id.0, "subject_variable_id")?;
5577    let object_variable_id = metadata_i32(link.object_variable_id.0, "object_variable_id")?;
5578    let relation_variable_id = optional_metadata_i32(
5579        link.relation_variable_id.map(|id| id.0),
5580        "relation_variable_id",
5581    )?;
5582    let transformation_id = metadata_i32(link.transformation_id.0, "transformation_id")?;
5583    transaction.execute_command(
5584        "link asset version relation",
5585        "
5586        INSERT INTO public.asset_version_relation_instances (
5587            version_id, relation_instance_id, transformation_id
5588        )
5589        VALUES ($1, $2, $3)
5590        ON CONFLICT (version_id, relation_instance_id) DO UPDATE
5591          SET transformation_id = EXCLUDED.transformation_id
5592        ",
5593        &[
5594            &link.version_id.0,
5595            &link.relation_instance_id,
5596            &transformation_id,
5597        ],
5598    )?;
5599    transaction.execute_command(
5600        "link dataset version relation",
5601        "
5602        INSERT INTO public.dataset_version_relation_instances (
5603            version_id, relation_instance_id, subject_variable_id,
5604            object_variable_id, relation_variable_id, transformation_id
5605        )
5606        VALUES ($1, $2, $3, $4, $5, $6)
5607        ON CONFLICT (version_id, relation_instance_id) DO UPDATE
5608          SET subject_variable_id = EXCLUDED.subject_variable_id,
5609              object_variable_id = EXCLUDED.object_variable_id,
5610              relation_variable_id = EXCLUDED.relation_variable_id,
5611              transformation_id = EXCLUDED.transformation_id
5612        ",
5613        &[
5614            &link.version_id.0,
5615            &link.relation_instance_id,
5616            &subject_variable_id,
5617            &object_variable_id,
5618            &relation_variable_id,
5619            &transformation_id,
5620        ],
5621    )?;
5622    Ok(())
5623}
5624
5625fn oauth_create_transformation(
5626    connection: &mut LibpqOAuthConnection,
5627    input: PgCreateTransformation,
5628) -> Result<TransformationId, PgMetaError> {
5629    let transformation_type = transformation_type_sql(input.transformation_type);
5630    let row = match input.transformation_id {
5631        Some(transformation_id) => {
5632            let transformation_id = metadata_i32(transformation_id.0, "transformation_id")?;
5633            MetadataExecutor::query_one(
5634                connection,
5635                "create transformation",
5636                "
5637                INSERT INTO public.transformations (
5638                    transformation_id, transformation_type, description,
5639                    repository_url, commit_hash, file_path, date_created, created_by
5640                )
5641                VALUES ($1, CAST($2 AS text)::public.transformation_type_enum, $3, $4, $5, $6,
5642                        COALESCE($7, CURRENT_TIMESTAMP), COALESCE($8, CURRENT_USER))
5643                RETURNING transformation_id::text
5644                ",
5645                &[
5646                    &transformation_id,
5647                    &transformation_type,
5648                    &input.description,
5649                    &input.repository_url,
5650                    &input.commit_hash,
5651                    &input.file_path,
5652                    &input.date_created,
5653                    &input.created_by,
5654                ],
5655            )?
5656        }
5657        None => MetadataExecutor::query_one(
5658            connection,
5659            "create transformation",
5660            "
5661            INSERT INTO public.transformations (
5662                transformation_type, description, repository_url, commit_hash, file_path,
5663                date_created, created_by
5664            )
5665            VALUES (CAST($1 AS text)::public.transformation_type_enum, $2, $3, $4, $5,
5666                    COALESCE($6, CURRENT_TIMESTAMP), COALESCE($7, CURRENT_USER))
5667            RETURNING transformation_id::text
5668            ",
5669            &[
5670                &transformation_type,
5671                &input.description,
5672                &input.repository_url,
5673                &input.commit_hash,
5674                &input.file_path,
5675                &input.date_created,
5676                &input.created_by,
5677            ],
5678        )?,
5679    };
5680    Ok(TransformationId(oauth_i64(&row, 0, "transformation_id")?))
5681}
5682
5683fn oauth_get_transformation(
5684    connection: &mut LibpqOAuthConnection,
5685    transformation_id: TransformationId,
5686) -> Result<Option<PgTransformationReadModel>, PgMetaError> {
5687    let transformation_id = metadata_i32(transformation_id.0, "transformation_id")?;
5688    let row = MetadataExecutor::query_optional(
5689        connection,
5690        "get transformation",
5691        "
5692        SELECT transformation_id::text, transformation_type::text,
5693               description, repository_url, commit_hash, file_path,
5694               date_created::text, created_by
5695          FROM public.transformations
5696         WHERE transformation_id = $1
5697         LIMIT 1
5698        ",
5699        &[&transformation_id],
5700    )?;
5701    row.as_ref().map(oauth_transformation_from_row).transpose()
5702}
5703
5704fn oauth_add_transformation_inputs(
5705    connection: &mut LibpqOAuthConnection,
5706    transformation_id: TransformationId,
5707    input_ids: &[VersionId],
5708) -> Result<(), PgMetaError> {
5709    let transformation_id = metadata_i32(transformation_id.0, "transformation_id")?;
5710    connection.with_transaction("link transformation inputs", |transaction| {
5711        for version_id in input_ids {
5712            transaction.execute_command(
5713                "link transformation input",
5714                "
5715                INSERT INTO public.transformation_inputs (transformation_id, version_id)
5716                VALUES ($1, $2)
5717                ON CONFLICT DO NOTHING
5718                ",
5719                &[&transformation_id, &version_id.0],
5720            )?;
5721        }
5722        Ok(())
5723    })
5724}
5725
5726fn oauth_delete_transformation(
5727    connection: &mut LibpqOAuthConnection,
5728    transformation_id: TransformationId,
5729) -> Result<bool, PgMetaError> {
5730    let transformation_id = metadata_i32(transformation_id.0, "transformation_id")?;
5731    let deleted = MetadataExecutor::execute_command(
5732        connection,
5733        "delete transformation",
5734        "DELETE FROM public.transformations WHERE transformation_id = $1",
5735        &[&transformation_id],
5736    )?;
5737    Ok(deleted > 0)
5738}
5739
5740fn oauth_add_transformation_outputs(
5741    connection: &mut LibpqOAuthConnection,
5742    transformation_id: TransformationId,
5743    output_ids: &[VersionId],
5744) -> Result<(), PgMetaError> {
5745    let transformation_id = metadata_i32(transformation_id.0, "transformation_id")?;
5746    connection.with_transaction("link transformation outputs", |transaction| {
5747        for version_id in output_ids {
5748            transaction.execute_command(
5749                "link transformation output",
5750                "
5751                INSERT INTO public.transformation_outputs (transformation_id, version_id)
5752                VALUES ($1, $2)
5753                ON CONFLICT DO NOTHING
5754                ",
5755                &[&transformation_id, &version_id.0],
5756            )?;
5757        }
5758        Ok(())
5759    })
5760}
5761
5762fn oauth_list_transformation_inputs(
5763    connection: &mut LibpqOAuthConnection,
5764    transformation_id: TransformationId,
5765) -> Result<Vec<PgTransformationInputReadModel>, PgMetaError> {
5766    let transformation_id = metadata_i32(transformation_id.0, "transformation_id")?;
5767    let rows = MetadataExecutor::query_many(
5768        connection,
5769        "list transformation inputs",
5770        "
5771        SELECT transformation_id::text, version_id::text
5772          FROM public.transformation_inputs
5773         WHERE transformation_id = $1
5774         ORDER BY version_id
5775        ",
5776        &[&transformation_id],
5777    )?;
5778    rows.iter()
5779        .map(oauth_transformation_input_from_row)
5780        .collect()
5781}
5782
5783fn oauth_list_transformation_outputs(
5784    connection: &mut LibpqOAuthConnection,
5785    transformation_id: TransformationId,
5786) -> Result<Vec<PgTransformationOutputReadModel>, PgMetaError> {
5787    let transformation_id = metadata_i32(transformation_id.0, "transformation_id")?;
5788    let rows = MetadataExecutor::query_many(
5789        connection,
5790        "list transformation outputs",
5791        "
5792        SELECT transformation_id::text, version_id::text
5793          FROM public.transformation_outputs
5794         WHERE transformation_id = $1
5795         ORDER BY version_id
5796        ",
5797        &[&transformation_id],
5798    )?;
5799    rows.iter()
5800        .map(oauth_transformation_output_from_row)
5801        .collect()
5802}
5803
5804fn oauth_list_transformations_producing_version(
5805    connection: &mut LibpqOAuthConnection,
5806    version_id: VersionId,
5807) -> Result<Vec<PgTransformationReadModel>, PgMetaError> {
5808    let rows = MetadataExecutor::query_many(
5809        connection,
5810        "list transformations producing version",
5811        "
5812        SELECT t.transformation_id::text, t.transformation_type::text,
5813               t.description, t.repository_url, t.commit_hash, t.file_path,
5814               t.date_created::text, t.created_by
5815          FROM public.transformations t
5816          JOIN public.transformation_outputs o
5817            ON o.transformation_id = t.transformation_id
5818         WHERE o.version_id = $1
5819         ORDER BY t.transformation_id
5820        ",
5821        &[&version_id.0],
5822    )?;
5823    rows.iter().map(oauth_transformation_from_row).collect()
5824}
5825
5826fn oauth_list_transformations(
5827    connection: &mut LibpqOAuthConnection,
5828) -> Result<Vec<PgTransformationReadModel>, PgMetaError> {
5829    let rows = MetadataExecutor::query_many(
5830        connection,
5831        "list transformations",
5832        "
5833        SELECT transformation_id::text, transformation_type::text,
5834               description, repository_url, commit_hash, file_path,
5835               date_created::text, created_by
5836          FROM public.transformations
5837         ORDER BY transformation_id
5838        ",
5839        &[],
5840    )?;
5841    rows.iter().map(oauth_transformation_from_row).collect()
5842}
5843
5844fn oauth_list_tags(
5845    connection: &mut LibpqOAuthConnection,
5846) -> Result<Vec<PgTagReadModel>, PgMetaError> {
5847    let rows = MetadataExecutor::query_many(
5848        connection,
5849        "list tags",
5850        "
5851        SELECT tag_id::text, name
5852          FROM public.tags
5853         ORDER BY name
5854        ",
5855        &[],
5856    )?;
5857    rows.iter().map(oauth_tag_from_row).collect()
5858}
5859
5860fn oauth_upsert_tag(
5861    connection: &mut LibpqOAuthConnection,
5862    normalized_name: &str,
5863) -> Result<i64, PgMetaError> {
5864    let row = MetadataExecutor::query_one(
5865        connection,
5866        "upsert tag",
5867        "
5868        INSERT INTO public.tags (name)
5869        VALUES ($1)
5870        ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name
5871        RETURNING tag_id::text
5872        ",
5873        &[&normalized_name],
5874    )?;
5875    oauth_i64(&row, 0, "tag_id")
5876}
5877
5878fn oauth_attach_tag(
5879    connection: &mut LibpqOAuthConnection,
5880    target: PgTagTarget,
5881    tag_id: i64,
5882) -> Result<(), PgMetaError> {
5883    let tag_id = metadata_i32(tag_id, "tag_id")?;
5884    let (mapping_table, key_column, key_value) = oauth_tag_target_mapping(target);
5885    let sql = format!(
5886        "
5887        INSERT INTO public.{mapping_table} ({key_column}, tag_id)
5888        VALUES ($1, $2)
5889        ON CONFLICT DO NOTHING
5890        "
5891    );
5892    oauth_execute_tag_link(connection, "attach tag", &sql, key_value, tag_id)
5893}
5894
5895fn oauth_detach_tag(
5896    connection: &mut LibpqOAuthConnection,
5897    target: PgTagTarget,
5898    tag_id: i64,
5899) -> Result<(), PgMetaError> {
5900    let tag_id = metadata_i32(tag_id, "tag_id")?;
5901    let (mapping_table, key_column, key_value) = oauth_tag_target_mapping(target);
5902    let sql = format!(
5903        "
5904        DELETE FROM public.{mapping_table}
5905         WHERE {key_column} = $1
5906           AND tag_id = $2
5907        "
5908    );
5909    oauth_execute_tag_link(connection, "detach tag", &sql, key_value, tag_id)
5910}
5911
5912fn oauth_delete_tag_if_unused(
5913    connection: &mut LibpqOAuthConnection,
5914    tag_id: i64,
5915) -> Result<bool, PgMetaError> {
5916    let tag_id = metadata_i32(tag_id, "tag_id")?;
5917    let deleted = MetadataExecutor::execute_command(
5918        connection,
5919        "delete unused tag",
5920        "
5921        DELETE FROM public.tags t
5922         WHERE t.tag_id = $1
5923           AND NOT EXISTS (SELECT 1 FROM public.study_tags WHERE tag_id = t.tag_id)
5924           AND NOT EXISTS (SELECT 1 FROM public.domain_tags WHERE tag_id = t.tag_id)
5925           AND NOT EXISTS (SELECT 1 FROM public.variable_tags WHERE tag_id = t.tag_id)
5926           AND NOT EXISTS (SELECT 1 FROM public.asset_tags WHERE tag_id = t.tag_id)
5927           AND NOT EXISTS (SELECT 1 FROM public.asset_version_tags WHERE tag_id = t.tag_id)
5928           AND NOT EXISTS (SELECT 1 FROM public.entity_tags WHERE tag_id = t.tag_id)
5929           AND NOT EXISTS (SELECT 1 FROM public.entityrelation_tags WHERE tag_id = t.tag_id)
5930        ",
5931        &[&tag_id],
5932    )?;
5933    Ok(deleted == 1)
5934}
5935
5936fn oauth_execute_tag_link(
5937    connection: &mut LibpqOAuthConnection,
5938    operation: &'static str,
5939    sql: &str,
5940    key_value: OauthTagKey,
5941    tag_id: i32,
5942) -> Result<(), PgMetaError> {
5943    match key_value {
5944        OauthTagKey::Uuid(value) => {
5945            MetadataExecutor::execute_command(connection, operation, sql, &[&value, &tag_id])?;
5946        }
5947        OauthTagKey::Int(value) => {
5948            let value = metadata_i32(value, "tag target id")?;
5949            MetadataExecutor::execute_command(connection, operation, sql, &[&value, &tag_id])?;
5950        }
5951    }
5952    Ok(())
5953}
5954
5955fn oauth_list_tags_for_target(
5956    connection: &mut LibpqOAuthConnection,
5957    target: PgTagTarget,
5958) -> Result<Vec<PgTagReadModel>, PgMetaError> {
5959    let (mapping_table, key_column, key_value) = oauth_tag_target_mapping(target);
5960    let sql = format!(
5961        "
5962        SELECT t.tag_id::text, t.name
5963          FROM public.tags t
5964          JOIN public.{mapping_table} m ON m.tag_id = t.tag_id
5965         WHERE m.{key_column} = $1
5966         ORDER BY t.name
5967        "
5968    );
5969    let rows = match key_value {
5970        OauthTagKey::Uuid(value) => {
5971            MetadataExecutor::query_many(connection, "list tags for target", &sql, &[&value])?
5972        }
5973        OauthTagKey::Int(value) => {
5974            let value = metadata_i32(value, "tag target id")?;
5975            MetadataExecutor::query_many(connection, "list tags for target", &sql, &[&value])?
5976        }
5977    };
5978    rows.iter().map(oauth_tag_from_row).collect()
5979}
5980
5981#[derive(Debug, Clone, Copy)]
5982enum OauthTagKey {
5983    Uuid(uuid::Uuid),
5984    Int(i64),
5985}
5986
5987fn oauth_tag_target_mapping(target: PgTagTarget) -> (&'static str, &'static str, OauthTagKey) {
5988    match target {
5989        PgTagTarget::Study(id) => ("study_tags", "study_id", OauthTagKey::Uuid(id.0)),
5990        PgTagTarget::Domain(id) => ("domain_tags", "domain_id", OauthTagKey::Int(id.0)),
5991        PgTagTarget::Variable(id) => ("variable_tags", "variable_id", OauthTagKey::Int(id.0)),
5992        PgTagTarget::Asset(id) => ("asset_tags", "asset_id", OauthTagKey::Uuid(id.0)),
5993        PgTagTarget::AssetVersion(id) => {
5994            ("asset_version_tags", "version_id", OauthTagKey::Uuid(id.0))
5995        }
5996        PgTagTarget::Entity(id) => ("entity_tags", "entity_id", OauthTagKey::Int(id.0)),
5997        PgTagTarget::EntityRelation(id) => (
5998            "entityrelation_tags",
5999            "entityrelation_id",
6000            OauthTagKey::Int(id.0),
6001        ),
6002    }
6003}
6004
6005fn oauth_list_domains(
6006    connection: &mut LibpqOAuthConnection,
6007) -> Result<Vec<PgDomainReadModel>, PgMetaError> {
6008    let rows = MetadataExecutor::query_many(
6009        connection,
6010        "list domains",
6011        "
6012        SELECT domain_id::text, name, uri, description
6013          FROM public.domains
6014         ORDER BY name
6015        ",
6016        &[],
6017    )?;
6018    rows.iter().map(oauth_domain_from_row).collect()
6019}
6020
6021fn oauth_list_domains_by_name(
6022    connection: &mut LibpqOAuthConnection,
6023    name: &str,
6024) -> Result<Vec<PgDomainReadModel>, PgMetaError> {
6025    let rows = MetadataExecutor::query_many(
6026        connection,
6027        "list domains by name",
6028        "SELECT domain_id::text, name, uri, description FROM public.domains WHERE name = $1 ORDER BY domain_id",
6029        &[&name],
6030    )?;
6031    rows.iter().map(oauth_domain_from_row).collect()
6032}
6033
6034fn oauth_get_domain(
6035    connection: &mut LibpqOAuthConnection,
6036    domain_id: DomainId,
6037) -> Result<Option<PgDomainReadModel>, PgMetaError> {
6038    let domain_id = metadata_i32(domain_id.0, "domain_id")?;
6039    let row = MetadataExecutor::query_optional(
6040        connection,
6041        "get domain by id",
6042        "
6043        SELECT domain_id::text, name, uri, description
6044          FROM public.domains
6045         WHERE domain_id = $1
6046         LIMIT 1
6047        ",
6048        &[&domain_id],
6049    )?;
6050    row.as_ref().map(oauth_domain_from_row).transpose()
6051}
6052
6053fn oauth_get_domain_by_name(
6054    connection: &mut LibpqOAuthConnection,
6055    name: &str,
6056) -> Result<Option<PgDomainReadModel>, PgMetaError> {
6057    let row = MetadataExecutor::query_optional(
6058        connection,
6059        "get domain by name",
6060        "
6061        SELECT domain_id::text, name, uri, description
6062          FROM public.domains
6063         WHERE name = $1
6064         LIMIT 1
6065        ",
6066        &[&name],
6067    )?;
6068    row.as_ref().map(oauth_domain_from_row).transpose()
6069}
6070
6071fn attach_registered_domain_tags<Row>(
6072    transaction: &mut dyn MetadataTransaction<Row = Row>,
6073    domain_id: i32,
6074    normalized_tags: &[String],
6075) -> Result<(), PgMetaError> {
6076    for tag in normalized_tags {
6077        transaction.execute_command(
6078            "attach registered Domain tag",
6079            "
6080            WITH registered_tag AS (
6081                INSERT INTO public.tags (name)
6082                VALUES ($1)
6083                ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name
6084                RETURNING tag_id
6085            )
6086            INSERT INTO public.domain_tags (domain_id, tag_id)
6087            SELECT $2, tag_id FROM registered_tag
6088            ON CONFLICT DO NOTHING
6089            ",
6090            &[&tag, &domain_id],
6091        )?;
6092    }
6093    Ok(())
6094}
6095
6096fn attach_registered_study_metadata<Row>(
6097    transaction: &mut dyn MetadataTransaction<Row = Row>,
6098    study_id: StudyId,
6099    domain_ids: &[DomainId],
6100    normalized_tags: &[String],
6101) -> Result<(), PgMetaError> {
6102    for tag in normalized_tags {
6103        transaction.execute_command(
6104            "attach registered Study tag",
6105            "
6106            WITH registered_tag AS (
6107                INSERT INTO public.tags (name)
6108                VALUES ($1)
6109                ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name
6110                RETURNING tag_id
6111            )
6112            INSERT INTO public.study_tags (study_id, tag_id)
6113            SELECT $2, tag_id FROM registered_tag
6114            ON CONFLICT DO NOTHING
6115            ",
6116            &[&tag, &study_id.0],
6117        )?;
6118    }
6119    for domain_id in domain_ids {
6120        let domain_id = metadata_i32(domain_id.0, "domain_id")?;
6121        transaction.execute_command(
6122            "link registered Study Domain",
6123            "
6124            INSERT INTO public.study_domains (study_id, domain_id)
6125            VALUES ($1, $2)
6126            ON CONFLICT DO NOTHING
6127            ",
6128            &[&study_id.0, &domain_id],
6129        )?;
6130    }
6131    Ok(())
6132}
6133
6134fn direct_create_domain_registration(
6135    connection: &mut PgMetadataConnection,
6136    input: &NewDomainRecord,
6137    normalized_tags: &[String],
6138) -> Result<DomainRecord, PgMetaError> {
6139    MetadataExecutor::with_transaction(connection, "create Domain registration", |transaction| {
6140        let row = transaction.query_one(
6141            "create registered Domain",
6142            "
6143            INSERT INTO public.domains (name, uri, description)
6144            VALUES ($1, $2, $3)
6145            RETURNING domain_id
6146            ",
6147            &[&input.name.as_str(), &input.uri, &input.description],
6148        )?;
6149        let domain_id = DomainId(i64::from(row.get::<_, i32>("domain_id")));
6150        let domain_id_i32 = metadata_i32(domain_id.0, "domain_id")?;
6151        attach_registered_domain_tags(transaction, domain_id_i32, normalized_tags)?;
6152        Ok(DomainRecord {
6153            domain_id,
6154            name: input.name.clone(),
6155            description: input.description.clone(),
6156            uri: input.uri.clone(),
6157        })
6158    })
6159}
6160
6161fn oauth_create_domain_registration(
6162    connection: &mut LibpqOAuthConnection,
6163    input: &NewDomainRecord,
6164    normalized_tags: &[String],
6165) -> Result<DomainRecord, PgMetaError> {
6166    MetadataExecutor::with_transaction(connection, "create Domain registration", |transaction| {
6167        let row = transaction.query_one(
6168            "create registered Domain",
6169            "
6170            INSERT INTO public.domains (name, uri, description)
6171            VALUES ($1, $2, $3)
6172            RETURNING domain_id::text
6173            ",
6174            &[&input.name.as_str(), &input.uri, &input.description],
6175        )?;
6176        let domain_id = DomainId(oauth_i64(&row, 0, "domain_id")?);
6177        let domain_id_i32 = metadata_i32(domain_id.0, "domain_id")?;
6178        attach_registered_domain_tags(transaction, domain_id_i32, normalized_tags)?;
6179        Ok(DomainRecord {
6180            domain_id,
6181            name: input.name.clone(),
6182            description: input.description.clone(),
6183            uri: input.uri.clone(),
6184        })
6185    })
6186}
6187
6188fn direct_save_domain_registration(
6189    connection: &mut PgMetadataConnection,
6190    input: &DomainRecord,
6191    normalized_tags: &[String],
6192) -> Result<DomainRecord, PgMetaError> {
6193    let domain_id = metadata_i32(input.domain_id.0, "domain_id")?;
6194    MetadataExecutor::with_transaction(connection, "save Domain registration", |transaction| {
6195        transaction.query_one(
6196            "save registered Domain",
6197            "
6198            INSERT INTO public.domains (domain_id, name, uri, description)
6199            VALUES ($1, $2, $3, $4)
6200            RETURNING domain_id
6201            ",
6202            &[
6203                &domain_id,
6204                &input.name.as_str(),
6205                &input.uri,
6206                &input.description,
6207            ],
6208        )?;
6209        attach_registered_domain_tags(transaction, domain_id, normalized_tags)?;
6210        Ok(input.clone())
6211    })
6212}
6213
6214fn oauth_save_domain_registration(
6215    connection: &mut LibpqOAuthConnection,
6216    input: &DomainRecord,
6217    normalized_tags: &[String],
6218) -> Result<DomainRecord, PgMetaError> {
6219    let domain_id = metadata_i32(input.domain_id.0, "domain_id")?;
6220    MetadataExecutor::with_transaction(connection, "save Domain registration", |transaction| {
6221        transaction.query_one(
6222            "save registered Domain",
6223            "
6224            INSERT INTO public.domains (domain_id, name, uri, description)
6225            VALUES ($1, $2, $3, $4)
6226            RETURNING domain_id::text
6227            ",
6228            &[
6229                &domain_id,
6230                &input.name.as_str(),
6231                &input.uri,
6232                &input.description,
6233            ],
6234        )?;
6235        attach_registered_domain_tags(transaction, domain_id, normalized_tags)?;
6236        Ok(input.clone())
6237    })
6238}
6239
6240fn oauth_create_domain(
6241    connection: &mut LibpqOAuthConnection,
6242    input: PgCreateDomain,
6243) -> Result<DomainId, PgMetaError> {
6244    let row = match input.domain_id {
6245        Some(domain_id) => {
6246            let domain_id = metadata_i32(domain_id.0, "domain_id")?;
6247            MetadataExecutor::query_one(
6248                connection,
6249                "create domain",
6250                "
6251                INSERT INTO public.domains (domain_id, name, uri, description)
6252                VALUES ($1, $2, $3, $4)
6253                RETURNING domain_id::text
6254                ",
6255                &[&domain_id, &input.name, &input.uri, &input.description],
6256            )?
6257        }
6258        None => MetadataExecutor::query_one(
6259            connection,
6260            "create domain",
6261            "
6262            INSERT INTO public.domains (name, uri, description)
6263            VALUES ($1, $2, $3)
6264            RETURNING domain_id::text
6265            ",
6266            &[&input.name, &input.uri, &input.description],
6267        )?,
6268    };
6269    Ok(DomainId(oauth_i64(&row, 0, "domain_id")?))
6270}
6271
6272fn direct_create_study_registration(
6273    connection: &mut PgMetadataConnection,
6274    input: &NewStudyRecord,
6275    domain_ids: &[DomainId],
6276    normalized_tags: &[String],
6277) -> Result<StudyRecord, PgMetaError> {
6278    let study_type_id = optional_metadata_i32(input.study_type_id.map(|id| id.0), "study_type_id")?;
6279    let date_created = input.date_created.map(|value| value.naive_utc());
6280    MetadataExecutor::with_transaction(connection, "create Study registration", |transaction| {
6281        let row = transaction.query_one(
6282            "create registered Study",
6283            "
6284            INSERT INTO public.studies (
6285                name, description, documentation, agent_instructions,
6286                external_id, study_type_id, date_created, created_by
6287            )
6288            VALUES ($1, $2, $3, $4, $5, $6,
6289                    COALESCE($7, CURRENT_TIMESTAMP AT TIME ZONE 'UTC'),
6290                    COALESCE($8, CURRENT_USER))
6291            RETURNING study_id, date_created, created_by
6292            ",
6293            &[
6294                &input.name.as_str(),
6295                &input.description,
6296                &input.documentation,
6297                &input.agent_instructions,
6298                &input.external_id,
6299                &study_type_id,
6300                &date_created,
6301                &input.created_by,
6302            ],
6303        )?;
6304        let study_id = StudyId(row.get("study_id"));
6305        attach_registered_study_metadata(transaction, study_id, domain_ids, normalized_tags)?;
6306        Ok(StudyRecord {
6307            study_id,
6308            name: input.name.clone(),
6309            description: input.description.clone(),
6310            documentation: input.documentation.clone(),
6311            agent_instructions: input.agent_instructions.clone(),
6312            external_id: input.external_id.clone(),
6313            study_type_id: input.study_type_id,
6314            date_created: row
6315                .get::<_, Option<NaiveDateTime>>("date_created")
6316                .map(|value| DateTime::from_naive_utc_and_offset(value, Utc)),
6317            created_by: row.get("created_by"),
6318        })
6319    })
6320}
6321
6322fn oauth_create_study_registration(
6323    connection: &mut LibpqOAuthConnection,
6324    input: &NewStudyRecord,
6325    domain_ids: &[DomainId],
6326    normalized_tags: &[String],
6327) -> Result<StudyRecord, PgMetaError> {
6328    let study_type_id = optional_metadata_i32(input.study_type_id.map(|id| id.0), "study_type_id")?;
6329    let date_created = input.date_created.map(|value| value.naive_utc());
6330    MetadataExecutor::with_transaction(connection, "create Study registration", |transaction| {
6331        let row = transaction.query_one(
6332            "create registered Study",
6333            "
6334            INSERT INTO public.studies (
6335                name, description, documentation, agent_instructions,
6336                external_id, study_type_id, date_created, created_by
6337            )
6338            VALUES ($1, $2, $3, $4, $5, $6,
6339                    COALESCE($7, CURRENT_TIMESTAMP AT TIME ZONE 'UTC'),
6340                    COALESCE($8, CURRENT_USER))
6341            RETURNING study_id::text, date_created::text, created_by
6342            ",
6343            &[
6344                &input.name.as_str(),
6345                &input.description,
6346                &input.documentation,
6347                &input.agent_instructions,
6348                &input.external_id,
6349                &study_type_id,
6350                &date_created,
6351                &input.created_by,
6352            ],
6353        )?;
6354        let study_id = StudyId(oauth_uuid(&row, 0, "study_id")?);
6355        attach_registered_study_metadata(transaction, study_id, domain_ids, normalized_tags)?;
6356        Ok(StudyRecord {
6357            study_id,
6358            name: input.name.clone(),
6359            description: input.description.clone(),
6360            documentation: input.documentation.clone(),
6361            agent_instructions: input.agent_instructions.clone(),
6362            external_id: input.external_id.clone(),
6363            study_type_id: input.study_type_id,
6364            date_created: oauth_optional_timestamp(&row, 1, "date_created")?,
6365            created_by: oauth_optional_string(&row, 2),
6366        })
6367    })
6368}
6369
6370fn direct_save_study_registration(
6371    connection: &mut PgMetadataConnection,
6372    input: &StudyRecord,
6373    domain_ids: &[DomainId],
6374    normalized_tags: &[String],
6375) -> Result<StudyRecord, PgMetaError> {
6376    let study_type_id = optional_metadata_i32(input.study_type_id.map(|id| id.0), "study_type_id")?;
6377    let date_created = input.date_created.map(|value| value.naive_utc());
6378    MetadataExecutor::with_transaction(connection, "save Study registration", |transaction| {
6379        let row = transaction.query_one(
6380            "save registered Study",
6381            "
6382            INSERT INTO public.studies (
6383                study_id, name, description, documentation, agent_instructions,
6384                external_id, study_type_id, date_created, created_by
6385            )
6386            VALUES ($1, $2, $3, $4, $5, $6, $7,
6387                    COALESCE($8, CURRENT_TIMESTAMP AT TIME ZONE 'UTC'),
6388                    COALESCE($9, CURRENT_USER))
6389            RETURNING date_created, created_by
6390            ",
6391            &[
6392                &input.study_id.0,
6393                &input.name.as_str(),
6394                &input.description,
6395                &input.documentation,
6396                &input.agent_instructions,
6397                &input.external_id,
6398                &study_type_id,
6399                &date_created,
6400                &input.created_by,
6401            ],
6402        )?;
6403        attach_registered_study_metadata(transaction, input.study_id, domain_ids, normalized_tags)?;
6404        let mut study = input.clone();
6405        study.date_created = row
6406            .get::<_, Option<NaiveDateTime>>("date_created")
6407            .map(|value| DateTime::from_naive_utc_and_offset(value, Utc));
6408        study.created_by = row.get("created_by");
6409        Ok(study)
6410    })
6411}
6412
6413fn oauth_save_study_registration(
6414    connection: &mut LibpqOAuthConnection,
6415    input: &StudyRecord,
6416    domain_ids: &[DomainId],
6417    normalized_tags: &[String],
6418) -> Result<StudyRecord, PgMetaError> {
6419    let study_type_id = optional_metadata_i32(input.study_type_id.map(|id| id.0), "study_type_id")?;
6420    let date_created = input.date_created.map(|value| value.naive_utc());
6421    MetadataExecutor::with_transaction(connection, "save Study registration", |transaction| {
6422        let row = transaction.query_one(
6423            "save registered Study",
6424            "
6425            INSERT INTO public.studies (
6426                study_id, name, description, documentation, agent_instructions,
6427                external_id, study_type_id, date_created, created_by
6428            )
6429            VALUES ($1, $2, $3, $4, $5, $6, $7,
6430                    COALESCE($8, CURRENT_TIMESTAMP AT TIME ZONE 'UTC'),
6431                    COALESCE($9, CURRENT_USER))
6432            RETURNING date_created::text, created_by
6433            ",
6434            &[
6435                &input.study_id.0,
6436                &input.name.as_str(),
6437                &input.description,
6438                &input.documentation,
6439                &input.agent_instructions,
6440                &input.external_id,
6441                &study_type_id,
6442                &date_created,
6443                &input.created_by,
6444            ],
6445        )?;
6446        attach_registered_study_metadata(transaction, input.study_id, domain_ids, normalized_tags)?;
6447        let mut study = input.clone();
6448        study.date_created = oauth_optional_timestamp(&row, 0, "date_created")?;
6449        study.created_by = oauth_optional_string(&row, 1);
6450        Ok(study)
6451    })
6452}
6453
6454fn oauth_list_studies(
6455    connection: &mut LibpqOAuthConnection,
6456) -> Result<Vec<PgStudyReadModel>, PgMetaError> {
6457    let rows = MetadataExecutor::query_many(
6458        connection,
6459        "list studies",
6460        "
6461        SELECT study_id::text, name, description, documentation, agent_instructions,
6462               external_id, study_type_id::text, date_created::text, created_by
6463          FROM public.studies
6464         ORDER BY name
6465        ",
6466        &[],
6467    )?;
6468    rows.iter().map(oauth_study_from_row).collect()
6469}
6470
6471fn oauth_get_study(
6472    connection: &mut LibpqOAuthConnection,
6473    study_id: StudyId,
6474) -> Result<Option<PgStudyReadModel>, PgMetaError> {
6475    let study_id = study_id.0;
6476    let row = MetadataExecutor::query_optional(
6477        connection,
6478        "get study by id",
6479        "
6480        SELECT study_id::text, name, description, documentation, agent_instructions,
6481               external_id, study_type_id::text, date_created::text, created_by
6482          FROM public.studies
6483         WHERE study_id = $1
6484         LIMIT 1
6485        ",
6486        &[&study_id],
6487    )?;
6488    row.as_ref().map(oauth_study_from_row).transpose()
6489}
6490
6491fn oauth_get_study_by_name(
6492    connection: &mut LibpqOAuthConnection,
6493    name: &str,
6494) -> Result<Option<PgStudyReadModel>, PgMetaError> {
6495    let row = MetadataExecutor::query_optional(
6496        connection,
6497        "get study by name",
6498        "
6499        SELECT study_id::text, name, description, documentation, agent_instructions,
6500               external_id, study_type_id::text, date_created::text, created_by
6501          FROM public.studies
6502         WHERE name = $1
6503         LIMIT 1
6504        ",
6505        &[&name],
6506    )?;
6507    row.as_ref().map(oauth_study_from_row).transpose()
6508}
6509
6510fn oauth_create_study(
6511    connection: &mut LibpqOAuthConnection,
6512    input: PgCreateStudy,
6513) -> Result<StudyId, PgMetaError> {
6514    let study_type_id = optional_metadata_i32(input.study_type_id.map(|id| id.0), "study_type_id")?;
6515    let date_created = input.date_created.map(|value| value.naive_utc());
6516    let row = match input.study_id {
6517        Some(study_id) => {
6518            let study_id = study_id.0;
6519            MetadataExecutor::query_one(
6520                connection,
6521                "create study",
6522                "
6523                INSERT INTO public.studies (
6524                    study_id, name, description, documentation, agent_instructions,
6525                    external_id, study_type_id, date_created, created_by
6526                )
6527                VALUES ($1, $2, $3, $4, $5, $6, $7, COALESCE($8, CURRENT_TIMESTAMP),
6528                        COALESCE($9, CURRENT_USER))
6529                RETURNING study_id::text
6530                ",
6531                &[
6532                    &study_id,
6533                    &input.name,
6534                    &input.description,
6535                    &input.documentation,
6536                    &input.agent_instructions,
6537                    &input.external_id,
6538                    &study_type_id,
6539                    &date_created,
6540                    &input.created_by,
6541                ],
6542            )?
6543        }
6544        None => MetadataExecutor::query_one(
6545            connection,
6546            "create study",
6547            "
6548            INSERT INTO public.studies (
6549                name, description, documentation, agent_instructions,
6550                external_id, study_type_id, date_created, created_by
6551            )
6552            VALUES ($1, $2, $3, $4, $5, $6, COALESCE($7, CURRENT_TIMESTAMP),
6553                    COALESCE($8, CURRENT_USER))
6554            RETURNING study_id::text
6555            ",
6556            &[
6557                &input.name,
6558                &input.description,
6559                &input.documentation,
6560                &input.agent_instructions,
6561                &input.external_id,
6562                &study_type_id,
6563                &date_created,
6564                &input.created_by,
6565            ],
6566        )?,
6567    };
6568    Ok(StudyId(oauth_uuid(&row, 0, "study_id")?))
6569}
6570
6571fn oauth_link_study_domain(
6572    connection: &mut LibpqOAuthConnection,
6573    study_id: StudyId,
6574    domain_id: DomainId,
6575) -> Result<(), PgMetaError> {
6576    let study_id = study_id.0;
6577    let domain_id = metadata_i32(domain_id.0, "domain_id")?;
6578    MetadataExecutor::execute_command(
6579        connection,
6580        "link study domain",
6581        "
6582        INSERT INTO public.study_domains (study_id, domain_id)
6583        VALUES ($1, $2)
6584        ON CONFLICT DO NOTHING
6585        ",
6586        &[&study_id, &domain_id],
6587    )?;
6588    Ok(())
6589}
6590
6591fn oauth_list_study_domains(
6592    connection: &mut LibpqOAuthConnection,
6593    study_id: StudyId,
6594) -> Result<Vec<PgDomainReadModel>, PgMetaError> {
6595    let study_id = study_id.0;
6596    let rows = MetadataExecutor::query_many(
6597        connection,
6598        "list study domains",
6599        "
6600        SELECT d.domain_id::text, d.name, d.uri, d.description
6601          FROM public.domains d
6602          JOIN public.study_domains sd ON sd.domain_id = d.domain_id
6603         WHERE sd.study_id = $1
6604         ORDER BY d.name
6605        ",
6606        &[&study_id],
6607    )?;
6608    rows.iter().map(oauth_domain_from_row).collect()
6609}
6610
6611fn oauth_list_study_access(
6612    connection: &mut LibpqOAuthConnection,
6613    study_id: StudyId,
6614) -> Result<Vec<PgStudyAccessReadModel>, PgMetaError> {
6615    let study_id = study_id.0;
6616    let rows = MetadataExecutor::query_many(
6617        connection,
6618        "list study access",
6619        "
6620        SELECT study_id::text, user_id, date_granted::text, granted_by
6621          FROM public.study_access
6622         WHERE study_id = $1
6623         ORDER BY user_id
6624        ",
6625        &[&study_id],
6626    )?;
6627    rows.iter().map(oauth_study_access_from_row).collect()
6628}
6629
6630fn oauth_grant_study_access(
6631    connection: &mut LibpqOAuthConnection,
6632    study_id: StudyId,
6633    target_user_id: &str,
6634) -> Result<(), PgMetaError> {
6635    let study_id = study_id.0;
6636    MetadataExecutor::query_many(
6637        connection,
6638        "grant study access",
6639        "
6640        SELECT public.tre_grant_study_access($1, $2)
6641        ",
6642        &[&study_id, &target_user_id],
6643    )?;
6644    Ok(())
6645}
6646
6647fn oauth_add_study_delegate_custodian(
6648    connection: &mut LibpqOAuthConnection,
6649    study_id: StudyId,
6650    target_user_id: &str,
6651) -> Result<(), PgMetaError> {
6652    let study_id = study_id.0;
6653    MetadataExecutor::query_many(
6654        connection,
6655        "add study delegate custodian",
6656        OAUTH_ADD_STUDY_DELEGATE_CUSTODIAN_SQL,
6657        &[&study_id, &target_user_id],
6658    )?;
6659    Ok(())
6660}
6661
6662fn oauth_transfer_study_primary_custodianship(
6663    connection: &mut LibpqOAuthConnection,
6664    study_id: StudyId,
6665    target_user_id: &str,
6666) -> Result<(), PgMetaError> {
6667    let study_id = study_id.0;
6668    MetadataExecutor::query_many(
6669        connection,
6670        "transfer study primary custodianship",
6671        OAUTH_TRANSFER_PRIMARY_CUSTODIANSHIP_SQL,
6672        &[&study_id, &target_user_id],
6673    )?;
6674    Ok(())
6675}
6676
6677fn oauth_remove_study_delegate_custodian(
6678    connection: &mut LibpqOAuthConnection,
6679    study_id: StudyId,
6680    target_user_id: &str,
6681) -> Result<(), PgMetaError> {
6682    let study_id = study_id.0;
6683    MetadataExecutor::query_many(
6684        connection,
6685        "remove study delegate custodian",
6686        OAUTH_REMOVE_STUDY_DELEGATE_CUSTODIAN_SQL,
6687        &[&study_id, &target_user_id],
6688    )?;
6689    Ok(())
6690}
6691
6692fn oauth_revoke_study_access(
6693    connection: &mut LibpqOAuthConnection,
6694    study_id: StudyId,
6695    target_user_id: &str,
6696) -> Result<(), PgMetaError> {
6697    let study_id = study_id.0;
6698    MetadataExecutor::query_many(
6699        connection,
6700        "revoke study access",
6701        "
6702        SELECT public.tre_revoke_study_access($1, $2)
6703        ",
6704        &[&study_id, &target_user_id],
6705    )?;
6706    Ok(())
6707}
6708
6709fn oauth_list_study_custodians(
6710    connection: &mut LibpqOAuthConnection,
6711    study_id: StudyId,
6712) -> Result<Vec<PgStudyCustodianReadModel>, PgMetaError> {
6713    let study_id = study_id.0;
6714    let rows = MetadataExecutor::query_many(
6715        connection,
6716        "list study custodians",
6717        "
6718        SELECT study_id::text, user_id, is_primary::text, date_granted::text, granted_by
6719          FROM public.study_custodians
6720         WHERE study_id = $1
6721         ORDER BY is_primary DESC, user_id
6722        ",
6723        &[&study_id],
6724    )?;
6725    rows.iter().map(oauth_study_custodian_from_row).collect()
6726}
6727
6728fn oauth_save_study_custodian(
6729    connection: &mut LibpqOAuthConnection,
6730    custodian: &StudyCustodianLinkRecord,
6731) -> Result<(), PgMetaError> {
6732    let study_id = custodian.study_id.0;
6733    let date_granted = custodian.date_granted.map(|value| value.naive_utc());
6734    let granted_by = custodian.granted_by.as_deref();
6735    MetadataExecutor::with_transaction(connection, "save study custodian", |transaction| {
6736        if custodian.is_primary {
6737            transaction.execute_command(
6738                "unset primary study custodian",
6739                "UPDATE public.study_custodians SET is_primary = FALSE WHERE study_id = $1",
6740                &[&study_id],
6741            )?;
6742        }
6743        transaction.execute_command(
6744            "add study custodian",
6745            "
6746            INSERT INTO public.study_custodians (
6747                study_id, user_id, is_primary, date_granted, granted_by
6748            )
6749            VALUES ($1, $2, $3, COALESCE($4, CURRENT_TIMESTAMP), COALESCE($5, CURRENT_USER))
6750            ON CONFLICT (study_id, user_id) DO UPDATE SET
6751                is_primary = EXCLUDED.is_primary
6752            ",
6753            &[
6754                &study_id,
6755                &custodian.user_id,
6756                &custodian.is_primary,
6757                &date_granted,
6758                &granted_by,
6759            ],
6760        )?;
6761        Ok(())
6762    })
6763}
6764
6765fn replace_study_duo_restrictions_with_executor<E>(
6766    executor: &mut E,
6767    study_id: StudyId,
6768    restrictions: &[NewStudyDuoRestrictionRecord],
6769) -> Result<(), PgMetaError>
6770where
6771    E: MetadataExecutor,
6772{
6773    executor.with_transaction("replace study DUO restrictions", |transaction| {
6774        transaction.query_one(
6775            "authorize study DUO replacement",
6776            "SELECT public.tre_assert_study_duo_management($1)",
6777            &[&study_id.0],
6778        )?;
6779        transaction.execute_command(
6780            "delete existing study DUO restrictions",
6781            "DELETE FROM public.study_duo_restrictions WHERE study_id = $1",
6782            &[&study_id.0],
6783        )?;
6784
6785        for restriction in restrictions {
6786            let inserted = transaction.query_optional(
6787                "insert replacement study DUO restriction",
6788                "
6789                INSERT INTO public.study_duo_restrictions (
6790                    study_id, duo_id, qualifier_ontology_namespace, qualifier_ontology_id,
6791                    qualifier_text, qualifier_date, qualifier_integer
6792                )
6793                SELECT $1, dc.duo_id, $3, $4, $5, $6, $7
6794                  FROM public.duo_codes dc
6795                 WHERE dc.duo_id = $2 OR dc.duo_code = $2
6796                RETURNING study_duo_restriction_id
6797                ",
6798                &[
6799                    &study_id.0,
6800                    &restriction.duo_id,
6801                    &restriction.qualifier_ontology_namespace,
6802                    &restriction.qualifier_ontology_id,
6803                    &restriction.qualifier_text,
6804                    &restriction.qualifier_date,
6805                    &restriction.qualifier_integer,
6806                ],
6807            )?;
6808            if inserted.is_none() {
6809                return Err(PgMetaError::Decode {
6810                    field: "duo_id",
6811                    value: restriction.duo_id.clone(),
6812                    message: "DUO code was not found".to_string(),
6813                });
6814            }
6815        }
6816
6817        Ok(())
6818    })
6819}
6820
6821fn replace_asset_version_duo_restrictions_with_executor<E>(
6822    executor: &mut E,
6823    version_id: VersionId,
6824    restrictions: &[NewAssetVersionDuoRestrictionRecord],
6825) -> Result<(), PgMetaError>
6826where
6827    E: MetadataExecutor,
6828{
6829    executor.with_transaction("replace asset version DUO restrictions", |transaction| {
6830        transaction.execute_command(
6831            "delete existing asset version DUO restrictions",
6832            "DELETE FROM public.asset_version_duo_restrictions WHERE version_id = $1",
6833            &[&version_id.0],
6834        )?;
6835
6836        for restriction in restrictions {
6837            let inserted = transaction.query_optional(
6838                "insert replacement asset version DUO restriction",
6839                "
6840                INSERT INTO public.asset_version_duo_restrictions (
6841                    version_id, duo_id, qualifier_ontology_namespace, qualifier_ontology_id,
6842                    qualifier_text, qualifier_date, qualifier_integer
6843                )
6844                SELECT $1, dc.duo_id, $3, $4, $5, $6, $7
6845                  FROM public.duo_codes dc
6846                 WHERE dc.duo_id = $2 OR dc.duo_code = $2
6847                RETURNING asset_version_duo_restriction_id
6848                ",
6849                &[
6850                    &version_id.0,
6851                    &restriction.duo_id,
6852                    &restriction.qualifier_ontology_namespace,
6853                    &restriction.qualifier_ontology_id,
6854                    &restriction.qualifier_text,
6855                    &restriction.qualifier_date,
6856                    &restriction.qualifier_integer,
6857                ],
6858            )?;
6859            if inserted.is_none() {
6860                return Err(PgMetaError::Decode {
6861                    field: "duo_id",
6862                    value: restriction.duo_id.clone(),
6863                    message: "DUO code was not found".to_string(),
6864                });
6865            }
6866        }
6867
6868        Ok(())
6869    })
6870}
6871
6872fn clear_asset_version_duo_restrictions_with_executor<E>(
6873    executor: &mut E,
6874    version_id: VersionId,
6875) -> Result<(), PgMetaError>
6876where
6877    E: MetadataExecutor,
6878{
6879    executor.with_transaction("clear asset version DUO restrictions", |transaction| {
6880        transaction.execute_command(
6881            "delete existing asset version DUO restrictions",
6882            "DELETE FROM public.asset_version_duo_restrictions WHERE version_id = $1",
6883            &[&version_id.0],
6884        )?;
6885        Ok(())
6886    })
6887}
6888
6889fn oauth_list_study_duo_restrictions(
6890    connection: &mut LibpqOAuthConnection,
6891    study_id: StudyId,
6892) -> Result<Vec<PgDuoRestrictionReadModel>, PgMetaError> {
6893    let rows = MetadataExecutor::query_many(
6894        connection,
6895        "list study DUO restrictions",
6896        "
6897        SELECT sdr.study_duo_restriction_id::text, sdr.study_id::text, sdr.duo_id,
6898               dc.duo_code, dc.label,
6899               sdr.qualifier_ontology_namespace, sdr.qualifier_ontology_id,
6900               sdr.qualifier_text, sdr.qualifier_date::text, sdr.qualifier_integer::text
6901          FROM public.study_duo_restrictions sdr
6902          JOIN public.duo_codes dc ON dc.duo_id = sdr.duo_id
6903         WHERE sdr.study_id = $1
6904         ORDER BY sdr.study_duo_restriction_id
6905        ",
6906        &[&study_id.0],
6907    )?;
6908    rows.iter().map(oauth_study_duo_from_row).collect()
6909}
6910
6911fn oauth_create_study_duo_restriction(
6912    connection: &mut LibpqOAuthConnection,
6913    input: PgCreateStudyDuoRestriction,
6914) -> Result<i64, PgMetaError> {
6915    let restriction_id = optional_metadata_i32(
6916        input.study_duo_restriction_id.map(|value| value.0),
6917        "study_duo_restriction_id",
6918    )?;
6919    let row = match restriction_id {
6920        Some(restriction_id) => MetadataExecutor::query_one(
6921            connection,
6922            "create study DUO restriction",
6923            "
6924            INSERT INTO public.study_duo_restrictions (
6925                study_duo_restriction_id, study_id, duo_id,
6926                qualifier_ontology_namespace, qualifier_ontology_id,
6927                qualifier_text, qualifier_date, qualifier_integer
6928            )
6929            VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
6930            RETURNING study_duo_restriction_id::text
6931            ",
6932            &[
6933                &restriction_id,
6934                &input.study_id.0,
6935                &input.duo_id,
6936                &input.qualifiers.qualifier_ontology_namespace,
6937                &input.qualifiers.qualifier_ontology_id,
6938                &input.qualifiers.qualifier_text,
6939                &input.qualifiers.qualifier_date,
6940                &input.qualifiers.qualifier_integer,
6941            ],
6942        )?,
6943        None => MetadataExecutor::query_one(
6944            connection,
6945            "create study DUO restriction",
6946            "
6947            INSERT INTO public.study_duo_restrictions (
6948                study_id, duo_id, qualifier_ontology_namespace, qualifier_ontology_id,
6949                qualifier_text, qualifier_date, qualifier_integer
6950            )
6951            VALUES ($1, $2, $3, $4, $5, $6, $7)
6952            RETURNING study_duo_restriction_id::text
6953            ",
6954            &[
6955                &input.study_id.0,
6956                &input.duo_id,
6957                &input.qualifiers.qualifier_ontology_namespace,
6958                &input.qualifiers.qualifier_ontology_id,
6959                &input.qualifiers.qualifier_text,
6960                &input.qualifiers.qualifier_date,
6961                &input.qualifiers.qualifier_integer,
6962            ],
6963        )?,
6964    };
6965    oauth_i64(&row, 0, "study_duo_restriction_id")
6966}
6967
6968fn oauth_domain_from_row(row: &LibpqOAuthRow) -> Result<PgDomainReadModel, PgMetaError> {
6969    Ok(PgDomainReadModel {
6970        domain_id: DomainId(oauth_i64(row, 0, "domain_id")?),
6971        name: oauth_string(row, 1, "name")?,
6972        uri: oauth_optional_string(row, 2),
6973        description: oauth_optional_string(row, 3),
6974    })
6975}
6976
6977fn oauth_study_from_row(row: &LibpqOAuthRow) -> Result<PgStudyReadModel, PgMetaError> {
6978    Ok(PgStudyReadModel {
6979        study_id: StudyId(oauth_uuid(row, 0, "study_id")?),
6980        name: oauth_string(row, 1, "name")?,
6981        description: oauth_optional_string(row, 2),
6982        documentation: oauth_optional_string(row, 3),
6983        agent_instructions: oauth_optional_string(row, 4),
6984        external_id: oauth_optional_string(row, 5),
6985        study_type_id: oauth_optional_i64(row, 6, "study_type_id")?.map(StudyTypeId),
6986        date_created: oauth_optional_timestamp(row, 7, "date_created")?,
6987        created_by: oauth_optional_string(row, 8),
6988    })
6989}
6990
6991fn oauth_study_access_from_row(row: &LibpqOAuthRow) -> Result<PgStudyAccessReadModel, PgMetaError> {
6992    Ok(PgStudyAccessReadModel {
6993        study_id: StudyId(oauth_uuid(row, 0, "study_id")?),
6994        user_id: oauth_string(row, 1, "user_id")?,
6995        date_granted: oauth_optional_timestamp(row, 2, "date_granted")?,
6996        granted_by: oauth_optional_string(row, 3),
6997    })
6998}
6999
7000fn oauth_study_custodian_from_row(
7001    row: &LibpqOAuthRow,
7002) -> Result<PgStudyCustodianReadModel, PgMetaError> {
7003    Ok(PgStudyCustodianReadModel {
7004        study_id: StudyId(oauth_uuid(row, 0, "study_id")?),
7005        user_id: oauth_string(row, 1, "user_id")?,
7006        is_primary: oauth_bool(row, 2, "is_primary")?,
7007        date_granted: oauth_optional_timestamp(row, 3, "date_granted")?,
7008        granted_by: oauth_optional_string(row, 4),
7009    })
7010}
7011
7012fn oauth_study_duo_from_row(row: &LibpqOAuthRow) -> Result<PgDuoRestrictionReadModel, PgMetaError> {
7013    Ok(PgDuoRestrictionReadModel {
7014        restriction_id: oauth_i64(row, 0, "study_duo_restriction_id")?,
7015        study_id: StudyId(oauth_uuid(row, 1, "study_id")?),
7016        duo_id: oauth_string(row, 2, "duo_id")?,
7017        duo_code: oauth_string(row, 3, "duo_code")?,
7018        label: oauth_string(row, 4, "label")?,
7019        qualifier_ontology_namespace: oauth_optional_string(row, 5),
7020        qualifier_ontology_id: oauth_optional_string(row, 6),
7021        qualifier_text: oauth_optional_string(row, 7),
7022        qualifier_date: oauth_optional_date(row, 8, "qualifier_date")?,
7023        qualifier_integer: oauth_optional_i64(row, 9, "qualifier_integer")?
7024            .map(|value| metadata_i32(value, "qualifier_integer"))
7025            .transpose()?,
7026    })
7027}
7028
7029fn oauth_asset_from_row(row: &LibpqOAuthRow) -> Result<PgAssetReadModel, PgMetaError> {
7030    Ok(PgAssetReadModel {
7031        asset_id: AssetId(oauth_uuid(row, 0, "asset_id")?),
7032        study_id: StudyId(oauth_uuid(row, 1, "study_id")?),
7033        name: oauth_string(row, 2, "name")?,
7034        description: oauth_optional_string(row, 3),
7035        agent_instructions: oauth_optional_string(row, 4),
7036        asset_type: parse_asset_type_text(oauth_string(row, 5, "asset_type")?)?,
7037        date_created: oauth_optional_timestamp(row, 6, "date_created")?,
7038        created_by: oauth_optional_string(row, 7),
7039    })
7040}
7041
7042fn oauth_asset_version_from_row(
7043    row: &LibpqOAuthRow,
7044) -> Result<PgAssetVersionReadModel, PgMetaError> {
7045    Ok(PgAssetVersionReadModel {
7046        version_id: VersionId(oauth_uuid(row, 0, "version_id")?),
7047        asset_id: AssetId(oauth_uuid(row, 1, "asset_id")?),
7048        major: oauth_i32(row, 2, "major")?,
7049        minor: oauth_i32(row, 3, "minor")?,
7050        patch: oauth_i32(row, 4, "patch")?,
7051        version_label: oauth_optional_string(row, 5),
7052        version_note: oauth_optional_string(row, 6),
7053        is_latest: oauth_optional_bool(row, 7, "is_latest")?,
7054        doi: oauth_optional_string(row, 8),
7055        created_at: oauth_optional_timestamp(row, 9, "created_at")?,
7056        created_by: oauth_optional_string(row, 10),
7057    })
7058}
7059
7060fn oauth_asset_version_duo_from_row(
7061    row: &LibpqOAuthRow,
7062) -> Result<PgAssetVersionDuoRestrictionReadModel, PgMetaError> {
7063    Ok(PgAssetVersionDuoRestrictionReadModel {
7064        restriction_id: oauth_i64(row, 0, "asset_version_duo_restriction_id")?,
7065        version_id: VersionId(oauth_uuid(row, 1, "version_id")?),
7066        duo_id: oauth_string(row, 2, "duo_id")?,
7067        duo_code: oauth_string(row, 3, "duo_code")?,
7068        label: oauth_string(row, 4, "label")?,
7069        qualifier_ontology_namespace: oauth_optional_string(row, 5),
7070        qualifier_ontology_id: oauth_optional_string(row, 6),
7071        qualifier_text: oauth_optional_string(row, 7),
7072        qualifier_date: oauth_optional_date(row, 8, "qualifier_date")?,
7073        qualifier_integer: oauth_optional_i64(row, 9, "qualifier_integer")?
7074            .map(|value| metadata_i32(value, "qualifier_integer"))
7075            .transpose()?,
7076    })
7077}
7078
7079fn oauth_dataset_from_row(row: &LibpqOAuthRow) -> Result<PgDatasetReadModel, PgMetaError> {
7080    Ok(PgDatasetReadModel {
7081        dataset_id: VersionId(oauth_uuid(row, 0, "dataset_id")?),
7082    })
7083}
7084
7085fn oauth_dataset_version_withdrawal_from_row(
7086    row: &LibpqOAuthRow,
7087) -> Result<DatasetVersionWithdrawalRecord, PgMetaError> {
7088    Ok(DatasetVersionWithdrawalRecord {
7089        dataset_id: VersionId(oauth_uuid(row, 0, "dataset_id")?),
7090        asset_id: AssetId(oauth_uuid(row, 1, "asset_id")?),
7091        withdrawn_at: oauth_optional_timestamp(row, 2, "withdrawn_at")?,
7092        withdrawn_by: oauth_optional_string(row, 3),
7093        reason: oauth_string(row, 4, "reason")?,
7094        tombstone_state: oauth_string(row, 5, "tombstone_state")?,
7095        provenance: oauth_optional_string(row, 6),
7096        drop_lake_table: oauth_optional_bool(row, 7, "drop_lake_table")?.unwrap_or(false),
7097    })
7098}
7099
7100fn oauth_datafile_from_row(row: &LibpqOAuthRow) -> Result<PgDataFileReadModel, PgMetaError> {
7101    let size_bytes = oauth_optional_string(row, 1);
7102    Ok(PgDataFileReadModel {
7103        datafile_id: VersionId(oauth_uuid(row, 0, "datafile_id")?),
7104        size_bytes: size_bytes
7105            .as_deref()
7106            .map(|value| value.parse::<i64>())
7107            .transpose()
7108            .map_err(|error| PgMetaError::Decode {
7109                field: "size_bytes",
7110                value: size_bytes.unwrap_or_default(),
7111                message: error.to_string(),
7112            })?,
7113        compressed: oauth_optional_bool(row, 2, "compressed")?,
7114        encrypted: oauth_optional_bool(row, 3, "encrypted")?,
7115        compression_algorithm: oauth_optional_string(row, 4),
7116        encryption_algorithm: oauth_optional_string(row, 5),
7117        encryption_key: oauth_optional_hex(row, 6, "encryption_key")?,
7118        storage_uri: oauth_string(row, 7, "storage_uri")?,
7119        edam_format: oauth_string(row, 8, "edam_format")?,
7120        digest: oauth_string(row, 9, "digest")?,
7121    })
7122}
7123
7124fn oauth_value_type_from_row(row: &LibpqOAuthRow) -> Result<PgValueTypeReadModel, PgMetaError> {
7125    Ok(PgValueTypeReadModel {
7126        value_type_id: ValueTypeId(oauth_i64(row, 0, "value_type_id")?),
7127        value_type: oauth_string(row, 1, "value_type")?,
7128        description: oauth_optional_string(row, 2),
7129    })
7130}
7131
7132fn oauth_variable_from_row(row: &LibpqOAuthRow) -> Result<PgVariableReadModel, PgMetaError> {
7133    Ok(PgVariableReadModel {
7134        variable_id: VariableId(oauth_i64(row, 0, "variable_id")?),
7135        domain_id: DomainId(oauth_i64(row, 1, "domain_id")?),
7136        name: oauth_string(row, 2, "name")?,
7137        value_type_id: ValueTypeId(oauth_i64(row, 3, "value_type_id")?),
7138        value_type: oauth_string(row, 4, "value_type")?,
7139        value_format: oauth_optional_string(row, 5),
7140        vocabulary_id: oauth_optional_i64(row, 6, "vocabulary_id")?.map(VocabularyId),
7141        key_role: parse_key_role_text(oauth_string(row, 7, "keyrole")?)?,
7142        description: oauth_optional_string(row, 8),
7143        note: oauth_optional_string(row, 9),
7144        ontology_namespace: oauth_optional_string(row, 10),
7145        ontology_class: oauth_optional_string(row, 11),
7146    })
7147}
7148
7149fn oauth_dataset_variable_from_row(
7150    row: &LibpqOAuthRow,
7151) -> Result<PgDatasetVariableReadModel, PgMetaError> {
7152    Ok(PgDatasetVariableReadModel {
7153        dataset_id: VersionId(oauth_uuid(row, 0, "dataset_id")?),
7154        variable_id: VariableId(oauth_i64(row, 1, "variable_id")?),
7155        row_role: parse_key_role_text(oauth_string(row, 2, "row_role")?)?,
7156    })
7157}
7158
7159fn oauth_dataset_version_entity_from_row(
7160    row: &LibpqOAuthRow,
7161) -> Result<PgDatasetVersionEntityReadModel, PgMetaError> {
7162    Ok(PgDatasetVersionEntityReadModel {
7163        version_id: VersionId(oauth_uuid(row, 0, "version_id")?),
7164        entity_instance_id: oauth_i64(row, 1, "entity_instance_id")?,
7165        entity_variable_id: VariableId(oauth_i64(row, 2, "entity_variable_id")?),
7166        transformation_id: TransformationId(oauth_i64(row, 3, "transformation_id")?),
7167    })
7168}
7169
7170fn oauth_asset_version_entity_from_row(
7171    row: &LibpqOAuthRow,
7172) -> Result<PgAssetVersionEntityReadModel, PgMetaError> {
7173    Ok(PgAssetVersionEntityReadModel {
7174        version_id: VersionId(oauth_uuid(row, 0, "version_id")?),
7175        entity_instance_id: oauth_i64(row, 1, "entity_instance_id")?,
7176        transformation_id: TransformationId(oauth_i64(row, 2, "transformation_id")?),
7177    })
7178}
7179
7180fn oauth_dataset_version_relation_instance_from_row(
7181    row: &LibpqOAuthRow,
7182) -> Result<PgDatasetVersionRelationInstanceReadModel, PgMetaError> {
7183    Ok(PgDatasetVersionRelationInstanceReadModel {
7184        version_id: VersionId(oauth_uuid(row, 0, "version_id")?),
7185        relation_instance_id: oauth_i64(row, 1, "relation_instance_id")?,
7186        subject_variable_id: VariableId(oauth_i64(row, 2, "subject_variable_id")?),
7187        object_variable_id: VariableId(oauth_i64(row, 3, "object_variable_id")?),
7188        relation_variable_id: oauth_optional_i64(row, 4, "relation_variable_id")?.map(VariableId),
7189        transformation_id: TransformationId(oauth_i64(row, 5, "transformation_id")?),
7190    })
7191}
7192
7193fn oauth_asset_version_relation_instance_from_row(
7194    row: &LibpqOAuthRow,
7195) -> Result<PgAssetVersionRelationInstanceReadModel, PgMetaError> {
7196    Ok(PgAssetVersionRelationInstanceReadModel {
7197        version_id: VersionId(oauth_uuid(row, 0, "version_id")?),
7198        relation_instance_id: oauth_i64(row, 1, "relation_instance_id")?,
7199        transformation_id: TransformationId(oauth_i64(row, 2, "transformation_id")?),
7200    })
7201}
7202
7203fn oauth_vocabulary_from_row(row: &LibpqOAuthRow) -> Result<PgVocabularyReadModel, PgMetaError> {
7204    Ok(PgVocabularyReadModel {
7205        vocabulary_id: VocabularyId(oauth_i64(row, 0, "vocabulary_id")?),
7206        domain_id: DomainId(oauth_i64(row, 1, "domain_id")?),
7207        name: oauth_string(row, 2, "name")?,
7208        description: oauth_optional_string(row, 3),
7209    })
7210}
7211
7212fn oauth_vocabulary_item_from_row(
7213    row: &LibpqOAuthRow,
7214) -> Result<PgVocabularyItemReadModel, PgMetaError> {
7215    Ok(PgVocabularyItemReadModel {
7216        vocabulary_item_id: VocabularyItemId(oauth_i64(row, 0, "vocabulary_item_id")?),
7217        vocabulary_id: VocabularyId(oauth_i64(row, 1, "vocabulary_id")?),
7218        value: oauth_i32(row, 2, "value")?,
7219        code: oauth_string(row, 3, "code")?,
7220        description: oauth_optional_string(row, 4),
7221    })
7222}
7223
7224fn oauth_vocabulary_mapping_from_row(
7225    row: &LibpqOAuthRow,
7226) -> Result<crate::PgVocabularyMappingReadModel, PgMetaError> {
7227    Ok(crate::PgVocabularyMappingReadModel {
7228        vocabulary_mapping_id: oauth_i64(row, 0, "vocabulary_mapping_id")?,
7229        from_vocabulary_item: VocabularyItemId(oauth_i64(row, 1, "from_vocabulary_item")?),
7230        to_vocabulary_item: VocabularyItemId(oauth_i64(row, 2, "to_vocabulary_item")?),
7231    })
7232}
7233
7234fn oauth_entity_from_row(row: &LibpqOAuthRow) -> Result<PgEntityReadModel, PgMetaError> {
7235    Ok(PgEntityReadModel {
7236        entity_id: EntityId(oauth_i64(row, 0, "entity_id")?),
7237        domain_id: DomainId(oauth_i64(row, 1, "domain_id")?),
7238        uuid: oauth_optional_uuid(row, 2, "uuid")?,
7239        name: oauth_string(row, 3, "name")?,
7240        description: oauth_optional_string(row, 4),
7241        agent_instructions: oauth_optional_string(row, 5),
7242        ontology_namespace: oauth_optional_string(row, 6),
7243        ontology_class: oauth_optional_string(row, 7),
7244    })
7245}
7246
7247fn oauth_entity_relation_from_row(
7248    row: &LibpqOAuthRow,
7249) -> Result<PgEntityRelationReadModel, PgMetaError> {
7250    Ok(PgEntityRelationReadModel {
7251        entity_relation_id: EntityRelationId(oauth_i64(row, 0, "entityrelation_id")?),
7252        subject_entity_id: EntityId(oauth_i64(row, 1, "subject_entity_id")?),
7253        object_entity_id: EntityId(oauth_i64(row, 2, "object_entity_id")?),
7254        domain_id: DomainId(oauth_i64(row, 3, "domain_id")?),
7255        uuid: oauth_optional_uuid(row, 4, "uuid")?,
7256        name: oauth_string(row, 5, "name")?,
7257        description: oauth_optional_string(row, 6),
7258        agent_instructions: oauth_optional_string(row, 7),
7259        ontology_namespace: oauth_optional_string(row, 8),
7260        ontology_class: oauth_optional_string(row, 9),
7261    })
7262}
7263
7264fn oauth_entity_instance_from_row(
7265    row: &LibpqOAuthRow,
7266) -> Result<PgEntityInstanceReadModel, PgMetaError> {
7267    Ok(PgEntityInstanceReadModel {
7268        instance_id: oauth_i64(row, 0, "instance_id")?,
7269        uuid: oauth_uuid(row, 1, "uuid")?,
7270        entity_id: EntityId(oauth_i64(row, 2, "entity_id")?),
7271        label: oauth_optional_string(row, 3),
7272        note: oauth_optional_string(row, 4),
7273        transformation_id: TransformationId(oauth_i64(row, 5, "transformation_id")?),
7274    })
7275}
7276
7277fn oauth_study_entity_instance_from_row(
7278    row: &LibpqOAuthRow,
7279) -> Result<PgStudyEntityInstanceReadModel, PgMetaError> {
7280    Ok(PgStudyEntityInstanceReadModel {
7281        study_id: StudyId(oauth_uuid(row, 0, "study_id")?),
7282        entity_instance_id: oauth_i64(row, 1, "entity_instance_id")?,
7283        entity_id: EntityId(oauth_i64(row, 2, "entity_id")?),
7284        external_id: oauth_string(row, 3, "external_id")?,
7285        transformation_id: TransformationId(oauth_i64(row, 4, "transformation_id")?),
7286    })
7287}
7288
7289fn oauth_relation_instance_from_row(
7290    row: &LibpqOAuthRow,
7291) -> Result<PgRelationInstanceReadModel, PgMetaError> {
7292    Ok(PgRelationInstanceReadModel {
7293        relation_instance_id: oauth_i64(row, 0, "relation_instance_id")?,
7294        uuid: oauth_uuid(row, 1, "uuid")?,
7295        entity_relation_id: EntityRelationId(oauth_i64(row, 2, "entityrelation_id")?),
7296        entity_instance_id_1: oauth_i64(row, 3, "entity_instance_id_1")?,
7297        entity_instance_id_2: oauth_i64(row, 4, "entity_instance_id_2")?,
7298        valid_from: oauth_optional_date(row, 5, "valid_from")?,
7299        valid_to: oauth_optional_date(row, 6, "valid_to")?,
7300        note: oauth_optional_string(row, 7),
7301        transformation_id: TransformationId(oauth_i64(row, 8, "transformation_id")?),
7302    })
7303}
7304
7305fn oauth_study_relation_instance_from_row(
7306    row: &LibpqOAuthRow,
7307) -> Result<PgStudyRelationInstanceReadModel, PgMetaError> {
7308    Ok(PgStudyRelationInstanceReadModel {
7309        study_id: StudyId(oauth_uuid(row, 0, "study_id")?),
7310        relation_instance_id: oauth_i64(row, 1, "relation_instance_id")?,
7311        entity_relation_id: EntityRelationId(oauth_i64(row, 2, "entityrelation_id")?),
7312        external_id: oauth_string(row, 3, "external_id")?,
7313        transformation_id: TransformationId(oauth_i64(row, 4, "transformation_id")?),
7314    })
7315}
7316
7317fn oauth_transformation_from_row(
7318    row: &LibpqOAuthRow,
7319) -> Result<PgTransformationReadModel, PgMetaError> {
7320    Ok(PgTransformationReadModel {
7321        transformation_id: TransformationId(oauth_i64(row, 0, "transformation_id")?),
7322        transformation_type: parse_transformation_type_text(oauth_string(
7323            row,
7324            1,
7325            "transformation_type",
7326        )?)?,
7327        description: oauth_string(row, 2, "description")?,
7328        repository_url: oauth_optional_string(row, 3),
7329        commit_hash: oauth_optional_string(row, 4),
7330        file_path: oauth_optional_string(row, 5),
7331        date_created: oauth_optional_timestamp(row, 6, "date_created")?,
7332        created_by: oauth_optional_string(row, 7),
7333    })
7334}
7335
7336fn oauth_transformation_input_from_row(
7337    row: &LibpqOAuthRow,
7338) -> Result<PgTransformationInputReadModel, PgMetaError> {
7339    Ok(PgTransformationInputReadModel {
7340        transformation_id: TransformationId(oauth_i64(row, 0, "transformation_id")?),
7341        version_id: VersionId(oauth_uuid(row, 1, "version_id")?),
7342    })
7343}
7344
7345fn oauth_transformation_output_from_row(
7346    row: &LibpqOAuthRow,
7347) -> Result<PgTransformationOutputReadModel, PgMetaError> {
7348    Ok(PgTransformationOutputReadModel {
7349        transformation_id: TransformationId(oauth_i64(row, 0, "transformation_id")?),
7350        version_id: VersionId(oauth_uuid(row, 1, "version_id")?),
7351    })
7352}
7353
7354fn oauth_tag_from_row(row: &LibpqOAuthRow) -> Result<PgTagReadModel, PgMetaError> {
7355    Ok(PgTagReadModel {
7356        tag_id: oauth_i64(row, 0, "tag_id")?,
7357        name: oauth_string(row, 1, "name")?,
7358    })
7359}
7360
7361fn metadata_i32(value: i64, field: &'static str) -> Result<i32, PgMetaError> {
7362    i32::try_from(value).map_err(|_| PgMetaError::Decode {
7363        field,
7364        value: value.to_string(),
7365        message: "value does not fit into PostgreSQL int4".to_string(),
7366    })
7367}
7368
7369fn optional_metadata_i32(
7370    value: Option<i64>,
7371    field: &'static str,
7372) -> Result<Option<i32>, PgMetaError> {
7373    value.map(|value| metadata_i32(value, field)).transpose()
7374}
7375
7376fn oauth_string(
7377    row: &LibpqOAuthRow,
7378    index: usize,
7379    field: &'static str,
7380) -> Result<String, PgMetaError> {
7381    oauth_required(row, index, field).map(ToOwned::to_owned)
7382}
7383
7384fn oauth_optional_string(row: &LibpqOAuthRow, index: usize) -> Option<String> {
7385    row.get(index).map(ToOwned::to_owned)
7386}
7387
7388fn oauth_i64(row: &LibpqOAuthRow, index: usize, field: &'static str) -> Result<i64, PgMetaError> {
7389    let value = oauth_required(row, index, field)?;
7390    value.parse::<i64>().map_err(|error| PgMetaError::Decode {
7391        field,
7392        value: value.to_string(),
7393        message: error.to_string(),
7394    })
7395}
7396
7397fn oauth_i32(row: &LibpqOAuthRow, index: usize, field: &'static str) -> Result<i32, PgMetaError> {
7398    let value = oauth_i64(row, index, field)?;
7399    metadata_i32(value, field)
7400}
7401
7402fn oauth_optional_i64(
7403    row: &LibpqOAuthRow,
7404    index: usize,
7405    field: &'static str,
7406) -> Result<Option<i64>, PgMetaError> {
7407    row.get(index)
7408        .map(|value| {
7409            value.parse::<i64>().map_err(|error| PgMetaError::Decode {
7410                field,
7411                value: value.to_string(),
7412                message: error.to_string(),
7413            })
7414        })
7415        .transpose()
7416}
7417
7418fn oauth_uuid(
7419    row: &LibpqOAuthRow,
7420    index: usize,
7421    field: &'static str,
7422) -> Result<uuid::Uuid, PgMetaError> {
7423    let value = oauth_required(row, index, field)?;
7424    uuid::Uuid::parse_str(value).map_err(|error| PgMetaError::Decode {
7425        field,
7426        value: value.to_string(),
7427        message: error.to_string(),
7428    })
7429}
7430
7431fn oauth_optional_uuid(
7432    row: &LibpqOAuthRow,
7433    index: usize,
7434    field: &'static str,
7435) -> Result<Option<uuid::Uuid>, PgMetaError> {
7436    row.get(index)
7437        .map(|value| {
7438            uuid::Uuid::parse_str(value).map_err(|error| PgMetaError::Decode {
7439                field,
7440                value: value.to_string(),
7441                message: error.to_string(),
7442            })
7443        })
7444        .transpose()
7445}
7446
7447fn oauth_bool(row: &LibpqOAuthRow, index: usize, field: &'static str) -> Result<bool, PgMetaError> {
7448    let value = oauth_required(row, index, field)?;
7449    match value.trim().to_ascii_lowercase().as_str() {
7450        "t" | "true" | "1" => Ok(true),
7451        "f" | "false" | "0" => Ok(false),
7452        _ => Err(PgMetaError::Decode {
7453            field,
7454            value: value.to_string(),
7455            message: "expected PostgreSQL boolean text".to_string(),
7456        }),
7457    }
7458}
7459
7460fn oauth_optional_bool(
7461    row: &LibpqOAuthRow,
7462    index: usize,
7463    field: &'static str,
7464) -> Result<Option<bool>, PgMetaError> {
7465    row.get(index)
7466        .map(|value| parse_oauth_bool(value, field))
7467        .transpose()
7468}
7469
7470fn parse_oauth_bool(value: &str, field: &'static str) -> Result<bool, PgMetaError> {
7471    match value.trim().to_ascii_lowercase().as_str() {
7472        "t" | "true" | "1" => Ok(true),
7473        "f" | "false" | "0" => Ok(false),
7474        _ => Err(PgMetaError::Decode {
7475            field,
7476            value: value.to_string(),
7477            message: "expected PostgreSQL boolean text".to_string(),
7478        }),
7479    }
7480}
7481
7482fn oauth_optional_timestamp(
7483    row: &LibpqOAuthRow,
7484    index: usize,
7485    field: &'static str,
7486) -> Result<Option<DateTime<Utc>>, PgMetaError> {
7487    row.get(index)
7488        .map(|value| {
7489            NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S%.f")
7490                .map(|value| DateTime::from_naive_utc_and_offset(value, Utc))
7491                .map_err(|error| PgMetaError::Decode {
7492                    field,
7493                    value: value.to_string(),
7494                    message: error.to_string(),
7495                })
7496        })
7497        .transpose()
7498}
7499
7500fn oauth_optional_date(
7501    row: &LibpqOAuthRow,
7502    index: usize,
7503    field: &'static str,
7504) -> Result<Option<NaiveDate>, PgMetaError> {
7505    row.get(index)
7506        .map(|value| {
7507            NaiveDate::parse_from_str(value, "%Y-%m-%d").map_err(|error| PgMetaError::Decode {
7508                field,
7509                value: value.to_string(),
7510                message: error.to_string(),
7511            })
7512        })
7513        .transpose()
7514}
7515
7516fn oauth_optional_hex(
7517    row: &LibpqOAuthRow,
7518    index: usize,
7519    field: &'static str,
7520) -> Result<Option<Vec<u8>>, PgMetaError> {
7521    row.get(index)
7522        .map(|value| parse_hex(value, field))
7523        .transpose()
7524}
7525
7526fn parse_hex(value: &str, field: &'static str) -> Result<Vec<u8>, PgMetaError> {
7527    if !value.len().is_multiple_of(2) {
7528        return Err(PgMetaError::Decode {
7529            field,
7530            value: value.to_string(),
7531            message: "hex bytea output had odd length".to_string(),
7532        });
7533    }
7534    let mut bytes = Vec::with_capacity(value.len() / 2);
7535    for chunk in value.as_bytes().chunks_exact(2) {
7536        let chunk = std::str::from_utf8(chunk).map_err(|error| PgMetaError::Decode {
7537            field,
7538            value: value.to_string(),
7539            message: error.to_string(),
7540        })?;
7541        let byte = u8::from_str_radix(chunk, 16).map_err(|error| PgMetaError::Decode {
7542            field,
7543            value: value.to_string(),
7544            message: error.to_string(),
7545        })?;
7546        bytes.push(byte);
7547    }
7548    Ok(bytes)
7549}
7550
7551fn asset_type_sql(value: AssetType) -> &'static str {
7552    match value {
7553        AssetType::Dataset => "dataset",
7554        AssetType::File => "file",
7555    }
7556}
7557
7558fn key_role_sql(value: KeyRole) -> Result<&'static str, PgMetaError> {
7559    match value {
7560        KeyRole::None => Ok("none"),
7561        KeyRole::Record => Ok("record"),
7562        KeyRole::External => Ok("external"),
7563    }
7564}
7565
7566fn transformation_type_sql(value: TransformationType) -> &'static str {
7567    match value {
7568        TransformationType::Ingest => "ingest",
7569        TransformationType::Transform => "transform",
7570        TransformationType::Entity => "entity",
7571        TransformationType::Export => "export",
7572        TransformationType::Repository => "repository",
7573    }
7574}
7575
7576fn parse_asset_type_text(value: String) -> Result<AssetType, PgMetaError> {
7577    match value.as_str() {
7578        "dataset" => Ok(AssetType::Dataset),
7579        "file" => Ok(AssetType::File),
7580        _ => Err(PgMetaError::Decode {
7581            field: "asset_type",
7582            value,
7583            message: "expected dataset or file".to_string(),
7584        }),
7585    }
7586}
7587
7588fn parse_key_role_text(value: String) -> Result<KeyRole, PgMetaError> {
7589    match value.as_str() {
7590        "none" => Ok(KeyRole::None),
7591        "record" => Ok(KeyRole::Record),
7592        "external" => Ok(KeyRole::External),
7593        _ => Err(PgMetaError::Decode {
7594            field: "keyrole",
7595            value,
7596            message: "expected none, record, or external".to_string(),
7597        }),
7598    }
7599}
7600
7601fn parse_transformation_type_text(value: String) -> Result<TransformationType, PgMetaError> {
7602    match value.as_str() {
7603        "ingest" => Ok(TransformationType::Ingest),
7604        "transform" => Ok(TransformationType::Transform),
7605        "entity" => Ok(TransformationType::Entity),
7606        "export" => Ok(TransformationType::Export),
7607        "repository" => Ok(TransformationType::Repository),
7608        _ => Err(PgMetaError::Decode {
7609            field: "transformation_type",
7610            value,
7611            message: "expected ingest, transform, entity, export, or repository".to_string(),
7612        }),
7613    }
7614}
7615
7616fn oauth_required<'a>(
7617    row: &'a LibpqOAuthRow,
7618    index: usize,
7619    field: &'static str,
7620) -> Result<&'a str, PgMetaError> {
7621    row.get(index).ok_or_else(|| PgMetaError::Decode {
7622        field,
7623        value: "NULL".to_string(),
7624        message: "required metadata field was null or missing".to_string(),
7625    })
7626}
7627
7628fn study_matches_search_query(
7629    study: &StudyRecord,
7630    domains: &[DomainRecord],
7631    tags: &[TagRecord],
7632    query: &StudySearchQuery,
7633) -> bool {
7634    let matches = |predicate: &StudySearchPredicate| -> bool {
7635        match predicate {
7636            StudySearchPredicate::Text { field, mode, value } => {
7637                let candidate = match field {
7638                    StudySearchTextField::Name => Some(study.name.as_str()),
7639                    StudySearchTextField::Title => study.documentation.as_deref(),
7640                    StudySearchTextField::Description => study.description.as_deref(),
7641                };
7642                candidate.is_some_and(|candidate| search_text_matches(candidate, *mode, value))
7643            }
7644            StudySearchPredicate::Domain(selector) => domains.iter().any(|domain| match selector {
7645                StudySearchDomainSelector::Id(domain_id) => domain.domain_id == *domain_id,
7646                StudySearchDomainSelector::Name(name) => domain.name.as_str() == name,
7647            }),
7648            StudySearchPredicate::Tag(selector) => search_tags_match_selector(tags, selector),
7649        }
7650    };
7651
7652    predicate_set_matches(query.kind, &query.predicates, matches)
7653}
7654
7655fn study_search_sort_key(left: &StudyRecord, right: &StudyRecord) -> std::cmp::Ordering {
7656    study_sort_tuple(left).cmp(&study_sort_tuple(right))
7657}
7658
7659fn study_sort_tuple(study: &StudyRecord) -> (String, String, uuid::Uuid) {
7660    (
7661        study.name.as_str().to_ascii_lowercase(),
7662        study.name.as_str().to_string(),
7663        study.study_id.0,
7664    )
7665}
7666
7667fn study_cursor_tuple(anchor: &StudySearchCursorAnchor) -> (String, String, uuid::Uuid) {
7668    (
7669        anchor.name.to_ascii_lowercase(),
7670        anchor.name.clone(),
7671        anchor.study_id.0,
7672    )
7673}
7674
7675fn latest_searchable_dataset_version(
7676    versions: &[AssetVersionRecord],
7677    dataset_ids: &[uuid::Uuid],
7678    withdrawn_ids: &[uuid::Uuid],
7679) -> Option<AssetVersionRecord> {
7680    versions
7681        .iter()
7682        .filter(|version| {
7683            dataset_ids.contains(&version.version_id.0)
7684                && version.is_latest == Some(true)
7685                && !withdrawn_ids.contains(&version.version_id.0)
7686        })
7687        .max_by_key(|version| (version.major, version.minor, version.patch))
7688        .cloned()
7689        .map(|mut version| {
7690            version.is_latest = Some(true);
7691            version
7692        })
7693}
7694
7695fn latest_searchable_datafile_version(
7696    versions: &[AssetVersionRecord],
7697    datafiles: &[DataFileRecord],
7698) -> Option<(AssetVersionRecord, DataFileRecord)> {
7699    versions
7700        .iter()
7701        .filter(|version| {
7702            version.is_latest == Some(true)
7703                && datafiles
7704                    .iter()
7705                    .any(|datafile| datafile.datafile_id == version.version_id)
7706        })
7707        .max_by_key(|version| (version.major, version.minor, version.patch))
7708        .and_then(|version| {
7709            datafiles
7710                .iter()
7711                .find(|datafile| datafile.datafile_id == version.version_id)
7712                .map(|datafile| {
7713                    let mut version = version.clone();
7714                    version.is_latest = Some(true);
7715                    (version, datafile.clone())
7716                })
7717        })
7718}
7719
7720fn dataset_matches_search_query(
7721    study: &StudyRecord,
7722    asset: &AssetRecord,
7723    latest_version: &AssetVersionRecord,
7724    domains: &[DomainRecord],
7725    tags: &[TagRecord],
7726    query: &DatasetSearchQuery,
7727) -> bool {
7728    let matches = |predicate: &DatasetSearchPredicate| -> bool {
7729        match predicate {
7730            DatasetSearchPredicate::Text { field, mode, value } => {
7731                let candidate = match field {
7732                    DatasetSearchTextField::Name => Some(asset.name.as_str()),
7733                    DatasetSearchTextField::Description => asset.description.as_deref(),
7734                    DatasetSearchTextField::VersionLabel => latest_version.version_label.as_deref(),
7735                };
7736                candidate.is_some_and(|candidate| search_text_matches(candidate, *mode, value))
7737            }
7738            DatasetSearchPredicate::Study(selector) => match selector {
7739                DatasetSearchStudySelector::Id(study_id) => study.study_id == *study_id,
7740                DatasetSearchStudySelector::Name { name, domain } => {
7741                    study.name.as_str() == name
7742                        && domain.as_ref().is_none_or(|domain_selector| {
7743                            domains.iter().any(|domain| match domain_selector {
7744                                DatasetSearchDomainSelector::Id(domain_id) => {
7745                                    domain.domain_id == *domain_id
7746                                }
7747                                DatasetSearchDomainSelector::Name(domain_name) => {
7748                                    domain.name.as_str() == domain_name
7749                                }
7750                            })
7751                        })
7752                }
7753            },
7754            DatasetSearchPredicate::Domain(selector) => {
7755                domains.iter().any(|domain| match selector {
7756                    DatasetSearchDomainSelector::Id(domain_id) => domain.domain_id == *domain_id,
7757                    DatasetSearchDomainSelector::Name(name) => domain.name.as_str() == name,
7758                })
7759            }
7760            DatasetSearchPredicate::VersionLabel(label) => latest_version
7761                .version_label
7762                .as_deref()
7763                .is_some_and(|version_label| version_label == label),
7764            DatasetSearchPredicate::Tag(selector) => search_tags_match_selector(tags, selector),
7765        }
7766    };
7767
7768    predicate_set_matches(query.kind, &query.predicates, matches)
7769}
7770
7771fn datafile_matches_search_query(
7772    study: &StudyRecord,
7773    asset: &AssetRecord,
7774    datafile: &DataFileRecord,
7775    tags: &[TagRecord],
7776    query: &DataFileSearchQuery,
7777) -> bool {
7778    let matches = |predicate: &DataFileSearchPredicate| -> bool {
7779        match predicate {
7780            DataFileSearchPredicate::Text { field, mode, value } => {
7781                let candidate = match field {
7782                    DataFileSearchTextField::Name => Some(asset.name.as_str()),
7783                    DataFileSearchTextField::Description => asset.description.as_deref(),
7784                    DataFileSearchTextField::Format => Some(datafile.edam_format.as_str()),
7785                };
7786                candidate.is_some_and(|candidate| search_text_matches(candidate, *mode, value))
7787            }
7788            DataFileSearchPredicate::Study(selector) => match selector {
7789                DataFileSearchStudySelector::Id(study_id) => study.study_id == *study_id,
7790                DataFileSearchStudySelector::Name { name } => study.name.as_str() == name,
7791            },
7792            DataFileSearchPredicate::Format(format) => datafile.edam_format == *format,
7793            DataFileSearchPredicate::Tag(selector) => search_tags_match_selector(tags, selector),
7794        }
7795    };
7796
7797    predicate_set_matches(query.kind, &query.predicates, matches)
7798}
7799
7800fn dataset_search_sort_key(
7801    left: &DatasetSearchRecord,
7802    right: &DatasetSearchRecord,
7803) -> std::cmp::Ordering {
7804    dataset_sort_tuple(left).cmp(&dataset_sort_tuple(right))
7805}
7806
7807fn dataset_sort_tuple(dataset: &DatasetSearchRecord) -> (String, String, uuid::Uuid, uuid::Uuid) {
7808    (
7809        dataset.asset.name.as_str().to_ascii_lowercase(),
7810        dataset.asset.name.as_str().to_string(),
7811        dataset.study_id.0,
7812        dataset.asset.asset_id.0,
7813    )
7814}
7815
7816fn dataset_cursor_tuple(
7817    anchor: &DatasetSearchCursorAnchor,
7818) -> (String, String, uuid::Uuid, uuid::Uuid) {
7819    (
7820        anchor.name.to_ascii_lowercase(),
7821        anchor.name.clone(),
7822        anchor.study_id.0,
7823        anchor.asset_id.0,
7824    )
7825}
7826
7827fn datafile_search_sort_key(
7828    left: &DataFileSearchRecord,
7829    right: &DataFileSearchRecord,
7830) -> std::cmp::Ordering {
7831    datafile_sort_tuple(left).cmp(&datafile_sort_tuple(right))
7832}
7833
7834fn datafile_sort_tuple(
7835    datafile: &DataFileSearchRecord,
7836) -> (String, String, uuid::Uuid, uuid::Uuid) {
7837    (
7838        datafile.asset.name.as_str().to_ascii_lowercase(),
7839        datafile.asset.name.as_str().to_string(),
7840        datafile.study_id.0,
7841        datafile.asset.asset_id.0,
7842    )
7843}
7844
7845fn datafile_cursor_tuple(
7846    anchor: &DataFileSearchCursorAnchor,
7847) -> (String, String, uuid::Uuid, uuid::Uuid) {
7848    (
7849        anchor.name.to_ascii_lowercase(),
7850        anchor.name.clone(),
7851        anchor.study_id.0,
7852        anchor.asset_id.0,
7853    )
7854}
7855
7856fn variable_matches_search_query(
7857    query: &VariableSearchQuery,
7858    domain: &DomainRecord,
7859    variable: &VariableRecord,
7860    value_type: Option<&ValueTypeRecord>,
7861    vocabulary: Option<&VocabularyRecord>,
7862    tags: &[TagRecord],
7863) -> bool {
7864    let matches = |predicate: &VariableSearchPredicate| -> bool {
7865        match predicate {
7866            VariableSearchPredicate::Text { field, mode, value } => {
7867                let candidate = match field {
7868                    VariableSearchTextField::Name => Some(variable.name.as_str()),
7869                    VariableSearchTextField::Description => variable.description.as_deref(),
7870                    VariableSearchTextField::ValueType => {
7871                        value_type.map(|value_type| value_type.value_type.as_str())
7872                    }
7873                    VariableSearchTextField::VocabularyName => {
7874                        vocabulary.map(|vocabulary| vocabulary.name.as_str())
7875                    }
7876                    VariableSearchTextField::OntologyClass => variable.ontology_class.as_deref(),
7877                };
7878                candidate.is_some_and(|candidate| search_text_matches(candidate, *mode, value))
7879            }
7880            VariableSearchPredicate::Domain(selector) => match selector {
7881                VariableSearchDomainSelector::Id(domain_id) => domain.domain_id == *domain_id,
7882                VariableSearchDomainSelector::Name(name) => domain.name.as_str() == name,
7883            },
7884            VariableSearchPredicate::ValueType(expected) => value_type
7885                .is_some_and(|value_type| value_type.value_type.eq_ignore_ascii_case(expected)),
7886            VariableSearchPredicate::Vocabulary(selector) => match selector {
7887                VariableSearchVocabularySelector::Id(vocabulary_id) => {
7888                    variable.vocabulary_id == Some(*vocabulary_id)
7889                }
7890                VariableSearchVocabularySelector::Name {
7891                    domain: vocabulary_domain,
7892                    name,
7893                } => {
7894                    let Some(vocabulary) = vocabulary else {
7895                        return false;
7896                    };
7897                    let domain_matches = match vocabulary_domain {
7898                        None => true,
7899                        Some(VariableSearchDomainSelector::Id(domain_id)) => {
7900                            vocabulary.domain_id == *domain_id
7901                        }
7902                        Some(VariableSearchDomainSelector::Name(domain_name)) => {
7903                            domain.name.as_str() == domain_name
7904                        }
7905                    };
7906                    domain_matches && vocabulary.name.as_str() == name
7907                }
7908            },
7909            VariableSearchPredicate::KeyRole(key_role) => variable.key_role == *key_role,
7910            VariableSearchPredicate::Tag(selector) => search_tags_match_selector(tags, selector),
7911        }
7912    };
7913    predicate_set_matches(query.kind, &query.predicates, matches)
7914}
7915
7916fn variable_search_sort_key(
7917    left: &VariableSearchRecord,
7918    right: &VariableSearchRecord,
7919) -> std::cmp::Ordering {
7920    variable_sort_tuple(left).cmp(&variable_sort_tuple(right))
7921}
7922
7923fn variable_sort_tuple(variable: &VariableSearchRecord) -> (String, String, i64, i64) {
7924    (
7925        variable.variable.name.as_str().to_ascii_lowercase(),
7926        variable.variable.name.as_str().to_string(),
7927        variable.variable.domain_id.0,
7928        variable.variable.variable_id.0,
7929    )
7930}
7931
7932fn variable_cursor_tuple(anchor: &VariableSearchCursorAnchor) -> (String, String, i64, i64) {
7933    (
7934        anchor.name.to_ascii_lowercase(),
7935        anchor.name.clone(),
7936        anchor.domain_id.0,
7937        anchor.variable_id.0,
7938    )
7939}
7940
7941fn entity_matches_search_query(
7942    query: &EntitySearchQuery,
7943    domain: &DomainRecord,
7944    entity: &EntityRecord,
7945    tags: &[TagRecord],
7946) -> bool {
7947    let matches = |predicate: &EntitySearchPredicate| -> bool {
7948        match predicate {
7949            EntitySearchPredicate::Text { field, mode, value } => {
7950                let candidate = match field {
7951                    EntitySearchTextField::Name => Some(entity.name.as_str()),
7952                    EntitySearchTextField::Description => entity.description.as_deref(),
7953                    EntitySearchTextField::OntologyClass => entity.ontology_class.as_deref(),
7954                };
7955                candidate.is_some_and(|candidate| search_text_matches(candidate, *mode, value))
7956            }
7957            EntitySearchPredicate::Domain(selector) => match selector {
7958                EntitySearchDomainSelector::Id(domain_id) => domain.domain_id == *domain_id,
7959                EntitySearchDomainSelector::Name(name) => domain.name.as_str() == name,
7960            },
7961            EntitySearchPredicate::Tag(selector) => search_tags_match_selector(tags, selector),
7962        }
7963    };
7964    predicate_set_matches(query.kind, &query.predicates, matches)
7965}
7966
7967fn entity_search_sort_key(
7968    left: &EntitySearchRecord,
7969    right: &EntitySearchRecord,
7970) -> std::cmp::Ordering {
7971    entity_sort_tuple(left).cmp(&entity_sort_tuple(right))
7972}
7973
7974fn entity_sort_tuple(entity: &EntitySearchRecord) -> (String, String, i64, i64) {
7975    (
7976        entity.entity.name.as_str().to_ascii_lowercase(),
7977        entity.entity.name.as_str().to_string(),
7978        entity.entity.domain_id.0,
7979        entity.entity.entity_id.0,
7980    )
7981}
7982
7983fn entity_cursor_tuple(anchor: &EntitySearchCursorAnchor) -> (String, String, i64, i64) {
7984    (
7985        anchor.name.to_ascii_lowercase(),
7986        anchor.name.clone(),
7987        anchor.domain_id.0,
7988        anchor.entity_id.0,
7989    )
7990}
7991
7992fn relation_matches_search_query(
7993    query: &RelationSearchQuery,
7994    domain: &DomainRecord,
7995    relation: &EntityRelationRecord,
7996    source_entity: &EntityRecord,
7997    target_entity: &EntityRecord,
7998    tags: &[TagRecord],
7999) -> bool {
8000    let matches = |predicate: &RelationSearchPredicate| -> bool {
8001        match predicate {
8002            RelationSearchPredicate::Text { field, mode, value } => {
8003                let candidate = match field {
8004                    RelationSearchTextField::Name => Some(relation.name.as_str()),
8005                    RelationSearchTextField::Description => relation.description.as_deref(),
8006                    RelationSearchTextField::SourceEntityName => Some(source_entity.name.as_str()),
8007                    RelationSearchTextField::TargetEntityName => Some(target_entity.name.as_str()),
8008                    RelationSearchTextField::OntologyClass => relation.ontology_class.as_deref(),
8009                };
8010                candidate.is_some_and(|candidate| search_text_matches(candidate, *mode, value))
8011            }
8012            RelationSearchPredicate::Domain(selector) => {
8013                relation_search_domain_matches(domain, selector)
8014            }
8015            RelationSearchPredicate::SourceEntity(selector) => {
8016                relation_search_entity_matches(domain, source_entity, selector)
8017            }
8018            RelationSearchPredicate::TargetEntity(selector) => {
8019                relation_search_entity_matches(domain, target_entity, selector)
8020            }
8021            RelationSearchPredicate::Tag(selector) => search_tags_match_selector(tags, selector),
8022        }
8023    };
8024    predicate_set_matches(query.kind, &query.predicates, matches)
8025}
8026
8027fn relation_search_domain_matches(
8028    domain: &DomainRecord,
8029    selector: &RelationSearchDomainSelector,
8030) -> bool {
8031    match selector {
8032        RelationSearchDomainSelector::Id(domain_id) => domain.domain_id == *domain_id,
8033        RelationSearchDomainSelector::Name(name) => domain.name.as_str() == name,
8034    }
8035}
8036
8037fn relation_search_entity_matches(
8038    domain: &DomainRecord,
8039    entity: &EntityRecord,
8040    selector: &RelationSearchEntitySelector,
8041) -> bool {
8042    match selector {
8043        RelationSearchEntitySelector::Id(entity_id) => entity.entity_id == *entity_id,
8044        RelationSearchEntitySelector::Name {
8045            domain: selector_domain,
8046            name,
8047        } => {
8048            entity.name.as_str() == name
8049                && selector_domain
8050                    .as_ref()
8051                    .is_none_or(|selector| relation_search_domain_matches(domain, selector))
8052        }
8053    }
8054}
8055
8056fn relation_search_sort_key(
8057    left: &RelationSearchRecord,
8058    right: &RelationSearchRecord,
8059) -> std::cmp::Ordering {
8060    relation_sort_tuple(left).cmp(&relation_sort_tuple(right))
8061}
8062
8063fn relation_sort_tuple(relation: &RelationSearchRecord) -> (String, String, i64, i64) {
8064    (
8065        relation.relation.name.as_str().to_ascii_lowercase(),
8066        relation.relation.name.as_str().to_string(),
8067        relation.relation.domain_id.0,
8068        relation.relation.entity_relation_id.0,
8069    )
8070}
8071
8072fn relation_cursor_tuple(anchor: &RelationSearchCursorAnchor) -> (String, String, i64, i64) {
8073    (
8074        anchor.name.to_ascii_lowercase(),
8075        anchor.name.clone(),
8076        anchor.domain_id.0,
8077        anchor.relation_id.0,
8078    )
8079}
8080
8081fn domain_record(read_model: PgDomainReadModel) -> Result<DomainRecord, CoreError> {
8082    Ok(DomainRecord {
8083        domain_id: read_model.domain_id,
8084        name: nc_name("domains.name", read_model.name)?,
8085        description: read_model.description,
8086        uri: read_model.uri,
8087    })
8088}
8089
8090fn study_record(read_model: PgStudyReadModel) -> Result<StudyRecord, CoreError> {
8091    Ok(StudyRecord {
8092        study_id: read_model.study_id,
8093        name: nc_name("studies.name", read_model.name)?,
8094        description: read_model.description,
8095        documentation: read_model.documentation,
8096        agent_instructions: read_model.agent_instructions,
8097        external_id: read_model.external_id,
8098        study_type_id: read_model.study_type_id,
8099        date_created: read_model.date_created,
8100        created_by: read_model.created_by,
8101    })
8102}
8103
8104fn study_access_record(
8105    read_model: PgStudyAccessReadModel,
8106) -> Result<StudyAccessGrantRecord, CoreError> {
8107    Ok(StudyAccessGrantRecord {
8108        study_id: read_model.study_id,
8109        user_id: read_model.user_id,
8110        date_granted: read_model.date_granted,
8111        granted_by: read_model.granted_by,
8112    })
8113}
8114
8115fn study_custodian_record(
8116    read_model: PgStudyCustodianReadModel,
8117) -> Result<StudyCustodianLinkRecord, CoreError> {
8118    Ok(StudyCustodianLinkRecord {
8119        study_id: read_model.study_id,
8120        user_id: read_model.user_id,
8121        is_primary: read_model.is_primary,
8122        date_granted: read_model.date_granted,
8123        granted_by: read_model.granted_by,
8124    })
8125}
8126
8127fn study_duo_restriction_record(
8128    read_model: PgDuoRestrictionReadModel,
8129) -> Result<StudyDuoRestrictionRecord, CoreError> {
8130    Ok(StudyDuoRestrictionRecord {
8131        study_duo_restriction_id: StudyDuoRestrictionId(read_model.restriction_id),
8132        study_id: read_model.study_id,
8133        duo_id: read_model.duo_id,
8134        qualifier_ontology_namespace: read_model.qualifier_ontology_namespace,
8135        qualifier_ontology_id: read_model.qualifier_ontology_id,
8136        qualifier_text: read_model.qualifier_text,
8137        qualifier_date: read_model.qualifier_date,
8138        qualifier_integer: read_model.qualifier_integer,
8139    })
8140}
8141
8142fn asset_record(read_model: PgAssetReadModel) -> Result<AssetRecord, CoreError> {
8143    Ok(AssetRecord {
8144        asset_id: read_model.asset_id,
8145        study_id: read_model.study_id,
8146        name: nc_name("assets.name", read_model.name)?,
8147        description: read_model.description,
8148        agent_instructions: read_model.agent_instructions,
8149        asset_type: read_model.asset_type,
8150        date_created: read_model.date_created,
8151        created_by: read_model.created_by,
8152    })
8153}
8154
8155fn asset_version_record(read_model: PgAssetVersionReadModel) -> AssetVersionRecord {
8156    AssetVersionRecord {
8157        version_id: read_model.version_id,
8158        asset_id: read_model.asset_id,
8159        major: read_model.major,
8160        minor: read_model.minor,
8161        patch: read_model.patch,
8162        version_label: read_model.version_label,
8163        version_note: read_model.version_note,
8164        is_latest: read_model.is_latest,
8165        doi: read_model.doi,
8166        created_at: read_model.created_at,
8167        created_by: read_model.created_by,
8168    }
8169}
8170
8171fn asset_version_duo_restriction_record(
8172    read_model: PgAssetVersionDuoRestrictionReadModel,
8173) -> Result<AssetVersionDuoRestrictionRecord, CoreError> {
8174    Ok(AssetVersionDuoRestrictionRecord {
8175        asset_version_duo_restriction_id: AssetVersionDuoRestrictionId(read_model.restriction_id),
8176        version_id: read_model.version_id,
8177        duo_id: read_model.duo_id,
8178        qualifier_ontology_namespace: read_model.qualifier_ontology_namespace,
8179        qualifier_ontology_id: read_model.qualifier_ontology_id,
8180        qualifier_text: read_model.qualifier_text,
8181        qualifier_date: read_model.qualifier_date,
8182        qualifier_integer: read_model.qualifier_integer,
8183    })
8184}
8185
8186fn datafile_record(read_model: PgDataFileReadModel) -> Result<DataFileRecord, CoreError> {
8187    Ok(DataFileRecord {
8188        datafile_id: read_model.datafile_id,
8189        size_bytes: read_model
8190            .size_bytes
8191            .map(u64::try_from)
8192            .transpose()
8193            .map_err(|_| CoreError::Infrastructure("datafile size is negative".to_string()))?,
8194        compressed: read_model.compressed,
8195        encrypted: read_model.encrypted,
8196        compression_algorithm: read_model.compression_algorithm,
8197        encryption_algorithm: read_model.encryption_algorithm,
8198        encryption_key: read_model.encryption_key,
8199        storage_uri: read_model.storage_uri,
8200        edam_format: read_model.edam_format,
8201        digest: Sha256Hex::parse(read_model.digest)
8202            .map_err(|error| CoreError::Infrastructure(error.to_string()))?,
8203    })
8204}
8205
8206fn dataset_record(read_model: PgDatasetReadModel) -> Result<DatasetRecord, CoreError> {
8207    Ok(DatasetRecord {
8208        dataset_id: read_model.dataset_id,
8209    })
8210}
8211
8212fn variable_record(read_model: PgVariableReadModel) -> Result<VariableRecord, CoreError> {
8213    Ok(VariableRecord {
8214        variable_id: read_model.variable_id,
8215        domain_id: read_model.domain_id,
8216        name: nc_name("variables.name", read_model.name)?,
8217        value_type_id: read_model.value_type_id,
8218        value_format: read_model.value_format,
8219        vocabulary_id: read_model.vocabulary_id,
8220        key_role: read_model.key_role,
8221        description: read_model.description,
8222        note: read_model.note,
8223        ontology_namespace: read_model.ontology_namespace,
8224        ontology_class: read_model.ontology_class,
8225    })
8226}
8227
8228fn value_type_record(read_model: PgValueTypeReadModel) -> Result<ValueTypeRecord, CoreError> {
8229    Ok(ValueTypeRecord {
8230        value_type_id: read_model.value_type_id,
8231        value_type: read_model.value_type,
8232        description: read_model.description,
8233    })
8234}
8235
8236fn vocabulary_record(read_model: PgVocabularyReadModel) -> Result<VocabularyRecord, CoreError> {
8237    Ok(VocabularyRecord {
8238        vocabulary_id: read_model.vocabulary_id,
8239        domain_id: read_model.domain_id,
8240        name: nc_name("vocabularies.name", read_model.name)?,
8241        description: read_model.description,
8242    })
8243}
8244
8245fn vocabulary_item_record(read_model: PgVocabularyItemReadModel) -> VocabularyItemRecord {
8246    VocabularyItemRecord {
8247        vocabulary_item_id: read_model.vocabulary_item_id,
8248        vocabulary_id: read_model.vocabulary_id,
8249        value: read_model.value,
8250        code: read_model.code,
8251        description: read_model.description,
8252    }
8253}
8254
8255fn vocabulary_mapping_record(
8256    read_model: crate::PgVocabularyMappingReadModel,
8257) -> VocabularyMappingRecord {
8258    VocabularyMappingRecord {
8259        vocabulary_mapping_id: read_model.vocabulary_mapping_id,
8260        from_vocabulary_item: read_model.from_vocabulary_item,
8261        to_vocabulary_item: read_model.to_vocabulary_item,
8262    }
8263}
8264
8265fn dataset_variable_link_record(
8266    read_model: PgDatasetVariableReadModel,
8267) -> ahri_tre_types::DatasetVariableLinkRecord {
8268    ahri_tre_types::DatasetVariableLinkRecord {
8269        dataset_id: read_model.dataset_id,
8270        variable_id: read_model.variable_id,
8271        row_role: read_model.row_role,
8272    }
8273}
8274
8275fn entity_record(read_model: PgEntityReadModel) -> Result<EntityRecord, CoreError> {
8276    Ok(EntityRecord {
8277        entity_id: read_model.entity_id,
8278        domain_id: read_model.domain_id,
8279        uuid: read_model.uuid,
8280        name: nc_name("entities.name", read_model.name)?,
8281        description: read_model.description,
8282        agent_instructions: read_model.agent_instructions,
8283        ontology_namespace: read_model.ontology_namespace,
8284        ontology_class: read_model.ontology_class,
8285    })
8286}
8287
8288fn entity_relation_record(
8289    read_model: PgEntityRelationReadModel,
8290) -> Result<EntityRelationRecord, CoreError> {
8291    Ok(EntityRelationRecord {
8292        entity_relation_id: read_model.entity_relation_id,
8293        subject_entity_id: read_model.subject_entity_id,
8294        object_entity_id: read_model.object_entity_id,
8295        domain_id: read_model.domain_id,
8296        uuid: read_model.uuid,
8297        name: nc_name("entityrelations.name", read_model.name)?,
8298        description: read_model.description,
8299        agent_instructions: read_model.agent_instructions,
8300        ontology_namespace: read_model.ontology_namespace,
8301        ontology_class: read_model.ontology_class,
8302    })
8303}
8304
8305fn entity_instance_record(read_model: PgEntityInstanceReadModel) -> EntityInstanceRecord {
8306    EntityInstanceRecord {
8307        instance_id: read_model.instance_id,
8308        uuid: read_model.uuid,
8309        entity_id: read_model.entity_id,
8310        label: read_model.label,
8311        note: read_model.note,
8312        transformation_id: read_model.transformation_id,
8313    }
8314}
8315
8316fn study_entity_instance_record(
8317    read_model: PgStudyEntityInstanceReadModel,
8318) -> StudyEntityInstanceRecord {
8319    StudyEntityInstanceRecord {
8320        study_id: read_model.study_id,
8321        entity_instance_id: read_model.entity_instance_id,
8322        entity_id: read_model.entity_id,
8323        external_id: read_model.external_id,
8324        transformation_id: read_model.transformation_id,
8325    }
8326}
8327
8328fn relation_instance_record(read_model: PgRelationInstanceReadModel) -> RelationInstanceRecord {
8329    RelationInstanceRecord {
8330        relation_instance_id: read_model.relation_instance_id,
8331        uuid: read_model.uuid,
8332        entity_relation_id: read_model.entity_relation_id,
8333        entity_instance_id_1: read_model.entity_instance_id_1,
8334        entity_instance_id_2: read_model.entity_instance_id_2,
8335        valid_from: read_model.valid_from,
8336        valid_to: read_model.valid_to,
8337        note: read_model.note,
8338        transformation_id: read_model.transformation_id,
8339    }
8340}
8341
8342fn study_relation_instance_record(
8343    read_model: PgStudyRelationInstanceReadModel,
8344) -> StudyRelationInstanceRecord {
8345    StudyRelationInstanceRecord {
8346        study_id: read_model.study_id,
8347        relation_instance_id: read_model.relation_instance_id,
8348        entity_relation_id: read_model.entity_relation_id,
8349        external_id: read_model.external_id,
8350        transformation_id: read_model.transformation_id,
8351    }
8352}
8353
8354#[allow(dead_code)]
8355fn asset_version_entity_record(
8356    read_model: PgAssetVersionEntityReadModel,
8357) -> AssetVersionEntityRecord {
8358    AssetVersionEntityRecord {
8359        version_id: read_model.version_id,
8360        entity_instance_id: read_model.entity_instance_id,
8361        transformation_id: read_model.transformation_id,
8362    }
8363}
8364
8365#[allow(dead_code)]
8366fn dataset_version_entity_record(
8367    read_model: PgDatasetVersionEntityReadModel,
8368) -> DatasetVersionEntityRecord {
8369    DatasetVersionEntityRecord {
8370        version_id: read_model.version_id,
8371        entity_instance_id: read_model.entity_instance_id,
8372        entity_variable_id: read_model.entity_variable_id,
8373        transformation_id: read_model.transformation_id,
8374    }
8375}
8376
8377#[allow(dead_code)]
8378fn asset_version_relation_instance_record(
8379    read_model: PgAssetVersionRelationInstanceReadModel,
8380) -> AssetVersionRelationInstanceRecord {
8381    AssetVersionRelationInstanceRecord {
8382        version_id: read_model.version_id,
8383        relation_instance_id: read_model.relation_instance_id,
8384        transformation_id: read_model.transformation_id,
8385    }
8386}
8387
8388#[allow(dead_code)]
8389fn dataset_version_relation_instance_record(
8390    read_model: PgDatasetVersionRelationInstanceReadModel,
8391) -> DatasetVersionRelationInstanceRecord {
8392    DatasetVersionRelationInstanceRecord {
8393        version_id: read_model.version_id,
8394        relation_instance_id: read_model.relation_instance_id,
8395        subject_variable_id: read_model.subject_variable_id,
8396        object_variable_id: read_model.object_variable_id,
8397        relation_variable_id: read_model.relation_variable_id,
8398        transformation_id: read_model.transformation_id,
8399    }
8400}
8401
8402fn transformation_record(
8403    read_model: PgTransformationReadModel,
8404) -> Result<TransformationRecord, CoreError> {
8405    Ok(TransformationRecord {
8406        transformation_id: read_model.transformation_id,
8407        transformation_type: read_model.transformation_type,
8408        description: read_model.description,
8409        repository_url: read_model.repository_url,
8410        commit_hash: read_model.commit_hash,
8411        file_path: read_model.file_path,
8412        date_created: read_model.date_created,
8413        created_by: read_model.created_by,
8414    })
8415}
8416
8417fn transformation_input_record(
8418    read_model: PgTransformationInputReadModel,
8419) -> TransformationInputLinkRecord {
8420    TransformationInputLinkRecord {
8421        transformation_id: read_model.transformation_id,
8422        version_id: read_model.version_id,
8423    }
8424}
8425
8426fn transformation_output_record(
8427    read_model: PgTransformationOutputReadModel,
8428) -> TransformationOutputLinkRecord {
8429    TransformationOutputLinkRecord {
8430        transformation_id: read_model.transformation_id,
8431        version_id: read_model.version_id,
8432    }
8433}
8434
8435fn tag_record(read_model: PgTagReadModel) -> TagRecord {
8436    TagRecord {
8437        tag_id: TagId(read_model.tag_id),
8438        name: read_model.name,
8439    }
8440}
8441
8442fn pg_create_study_duo_restriction(
8443    study_duo_restriction_id: Option<StudyDuoRestrictionId>,
8444    restriction: &NewStudyDuoRestrictionRecord,
8445) -> PgCreateStudyDuoRestriction {
8446    PgCreateStudyDuoRestriction {
8447        study_duo_restriction_id,
8448        study_id: restriction.study_id,
8449        duo_id: restriction.duo_id.clone(),
8450        qualifiers: PgDuoQualifiers {
8451            qualifier_ontology_namespace: restriction.qualifier_ontology_namespace.clone(),
8452            qualifier_ontology_id: restriction.qualifier_ontology_id.clone(),
8453            qualifier_text: restriction.qualifier_text.clone(),
8454            qualifier_date: restriction.qualifier_date,
8455            qualifier_integer: restriction.qualifier_integer,
8456        },
8457    }
8458}
8459
8460fn pg_create_asset_version_duo_restriction(
8461    asset_version_duo_restriction_id: Option<AssetVersionDuoRestrictionId>,
8462    restriction: &NewAssetVersionDuoRestrictionRecord,
8463) -> PgCreateAssetVersionDuoRestriction {
8464    PgCreateAssetVersionDuoRestriction {
8465        asset_version_duo_restriction_id,
8466        version_id: restriction.version_id,
8467        duo_id: restriction.duo_id.clone(),
8468        qualifiers: PgDuoQualifiers {
8469            qualifier_ontology_namespace: restriction.qualifier_ontology_namespace.clone(),
8470            qualifier_ontology_id: restriction.qualifier_ontology_id.clone(),
8471            qualifier_text: restriction.qualifier_text.clone(),
8472            qualifier_date: restriction.qualifier_date,
8473            qualifier_integer: restriction.qualifier_integer,
8474        },
8475    }
8476}
8477
8478fn pg_tag_target(target: TagAttachmentTarget) -> PgTagTarget {
8479    match target {
8480        TagAttachmentTarget::Study(id) => PgTagTarget::Study(id),
8481        TagAttachmentTarget::Domain(id) => PgTagTarget::Domain(id),
8482        TagAttachmentTarget::Variable(id) => PgTagTarget::Variable(id),
8483        TagAttachmentTarget::Asset(id) => PgTagTarget::Asset(id),
8484        TagAttachmentTarget::AssetVersion(id) => PgTagTarget::AssetVersion(id),
8485        TagAttachmentTarget::Entity(id) => PgTagTarget::Entity(id),
8486        TagAttachmentTarget::EntityRelation(id) => PgTagTarget::EntityRelation(id),
8487    }
8488}
8489
8490fn pg_write_tag_target(target: TagAttachmentTarget) -> PgWriteTagTarget {
8491    match target {
8492        TagAttachmentTarget::Study(id) => PgWriteTagTarget::Study(id),
8493        TagAttachmentTarget::Domain(id) => PgWriteTagTarget::Domain(id),
8494        TagAttachmentTarget::Variable(id) => PgWriteTagTarget::Variable(id),
8495        TagAttachmentTarget::Asset(id) => PgWriteTagTarget::Asset(id),
8496        TagAttachmentTarget::AssetVersion(id) => PgWriteTagTarget::AssetVersion(id),
8497        TagAttachmentTarget::Entity(id) => PgWriteTagTarget::Entity(id),
8498        TagAttachmentTarget::EntityRelation(id) => PgWriteTagTarget::EntityRelation(id),
8499    }
8500}
8501
8502fn nc_name(field: &'static str, value: String) -> Result<NcName, CoreError> {
8503    NcName::parse(value.clone()).map_err(|error| {
8504        CoreError::Infrastructure(format!(
8505            "could not decode PostgreSQL metadata field {field}: {error}"
8506        ))
8507    })
8508}
8509
8510fn pgmeta_error(operation: &'static str, error: PgMetaError) -> CoreError {
8511    CoreError::Infrastructure(format!("PostgreSQL metadata {operation} failed: {error}"))
8512}
8513
8514/// Retains the connection's upload deadline until work or compensation starts.
8515pub struct UploadMetadataDeadline {
8516    repository: PgMetadataRepository<'static>,
8517    stop: std::sync::mpsc::Sender<()>,
8518    worker: Option<std::thread::JoinHandle<()>>,
8519}
8520impl PgMetadataRepository<'static> {
8521    pub fn upload_deadline(
8522        &self,
8523        deadline: std::time::Instant,
8524        cancelled: std::sync::Arc<std::sync::atomic::AtomicBool>,
8525    ) -> Result<UploadMetadataDeadline, CoreError> {
8526        let millis = deadline
8527            .saturating_duration_since(std::time::Instant::now())
8528            .as_millis()
8529            .max(1)
8530            .to_string();
8531        let cancellation = self.with_session_connection(
8532            "bound upload metadata",
8533            |connection| {
8534                connection
8535                    .client
8536                    .query_one(
8537                        "SELECT set_config('statement_timeout',$1,FALSE)",
8538                        &[&millis],
8539                    )
8540                    .map_err(PgMetaError::Query)?;
8541                Ok(None)
8542            },
8543            |connection| {
8544                let token = connection
8545                    .cancellation()
8546                    .map_err(|source| PgMetaError::OAuth {
8547                        operation: "prepare cancellation",
8548                        source,
8549                    })?;
8550                connection.set_query_deadline(Some(deadline), Some(cancelled.clone()));
8551                Ok(Some(token))
8552            },
8553        )?;
8554        let (stop, stopped) = std::sync::mpsc::channel();
8555        let worker = cancellation.map(|mut token| {
8556            std::thread::spawn(move || {
8557                while stopped
8558                    .recv_timeout(std::time::Duration::from_millis(25))
8559                    .is_err()
8560                {
8561                    if std::time::Instant::now() >= deadline
8562                        || cancelled.load(std::sync::atomic::Ordering::Acquire)
8563                    {
8564                        token.cancel();
8565                        break;
8566                    }
8567                }
8568            })
8569        });
8570        Ok(UploadMetadataDeadline {
8571            repository: self.clone(),
8572            stop,
8573            worker,
8574        })
8575    }
8576}
8577impl Drop for UploadMetadataDeadline {
8578    fn drop(&mut self) {
8579        let _ = self.stop.send(());
8580        if let Some(worker) = self.worker.take() {
8581            let _ = worker.join();
8582        }
8583        let _ = self.repository.with_session_connection(
8584            "clear upload deadline",
8585            |connection| {
8586                connection
8587                    .client
8588                    .batch_execute("RESET statement_timeout")
8589                    .map_err(PgMetaError::Query)
8590            },
8591            |connection| {
8592                connection.set_query_deadline(None, None);
8593                connection
8594                    .execute_raw("RESET statement_timeout", &[])
8595                    .map(|_| ())
8596                    .map_err(|source| PgMetaError::OAuth {
8597                        operation: "clear upload deadline",
8598                        source,
8599                    })
8600            },
8601        );
8602    }
8603}
8604
8605#[cfg(test)]
8606mod tests {
8607    use super::*;
8608
8609    #[test]
8610    fn oauth_repository_key_role_contract_round_trips_external() {
8611        assert_eq!(
8612            key_role_sql(KeyRole::External).expect("external should encode"),
8613            "external"
8614        );
8615        assert_eq!(
8616            parse_key_role_text("external".to_string()).expect("external should decode"),
8617            KeyRole::External
8618        );
8619    }
8620
8621    #[test]
8622    fn oauth_delegate_custodian_path_uses_named_function() {
8623        assert!(
8624            OAUTH_ADD_STUDY_DELEGATE_CUSTODIAN_SQL
8625                .contains("public.tre_add_study_delegate_custodian($1, $2)")
8626        );
8627    }
8628
8629    #[test]
8630    fn oauth_primary_custodianship_transfer_path_uses_named_function() {
8631        assert!(
8632            OAUTH_TRANSFER_PRIMARY_CUSTODIANSHIP_SQL
8633                .contains("public.tre_transfer_study_primary_custodianship($1, $2)")
8634        );
8635    }
8636
8637    #[test]
8638    fn oauth_remove_delegate_custodian_path_uses_named_function() {
8639        assert!(
8640            OAUTH_REMOVE_STUDY_DELEGATE_CUSTODIAN_SQL
8641                .contains("public.tre_remove_study_delegate_custodian($1, $2)")
8642        );
8643    }
8644
8645    #[test]
8646    fn oauth_dataset_variable_query_orders_the_numeric_source_column() {
8647        assert_eq!(
8648            OAUTH_LIST_DATASET_VARIABLES_SQL
8649                .split_whitespace()
8650                .collect::<Vec<_>>()
8651                .join(" "),
8652            "SELECT dv.dataset_id::text, dv.variable_id::text, dv.row_role::text FROM \
8653             public.dataset_variables AS dv WHERE dv.dataset_id = $1 ORDER BY dv.variable_id"
8654        );
8655    }
8656}