Skip to main content

ahri_tre_pgmeta/
read_models.rs

1//! PostgreSQL read models and query helpers for the metadata schema.
2//!
3//! These structs mirror rows returned from metadata tables and keep the
4//! persistence/domain boundary explicit.
5
6use crate::{MetadataExecutor, PgMetaError, PgMetadataConnection};
7use ahri_tre_types::{
8    AssetId, AssetType, DomainId, EntityId, EntityRelationId, KeyRole, StudyId, StudyTypeId,
9    TransformationId, TransformationType, ValueTypeId, VariableId, VersionId, VocabularyId,
10    VocabularyItemId,
11};
12use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc};
13use postgres::Row;
14use uuid::Uuid;
15
16/// Metadata table names included in schema-bootstrap verification.
17pub const METADATA_TABLES: &[&str] = &[
18    "datastore_identity",
19    "duo_codes",
20    "study_types",
21    "studies",
22    "study_access",
23    "study_access_requests",
24    "study_custodians",
25    "domains",
26    "study_domains",
27    "study_duo_restrictions",
28    "transformations",
29    "value_types",
30    "vocabularies",
31    "vocabulary_items",
32    "variables",
33    "vocabulary_mapping",
34    "assets",
35    "asset_versions",
36    "datasets",
37    "dataset_admission_receipts",
38    "dataset_executor_generation",
39    "dataset_output_reservations",
40    "dataset_output_attempts",
41    "operations",
42    "operation_idempotency",
43    "dataset_version_withdrawals",
44    "datafiles",
45    "asset_version_duo_restrictions",
46    "transformation_inputs",
47    "transformation_outputs",
48    "dataset_variables",
49    "entities",
50    "entityrelations",
51    "entity_instances",
52    "study_entity_instances",
53    "relation_instances",
54    "study_relation_instances",
55    "asset_version_entities",
56    "dataset_version_entities",
57    "asset_version_relation_instances",
58    "dataset_version_relation_instances",
59    "tags",
60    "study_tags",
61    "domain_tags",
62    "variable_tags",
63    "asset_tags",
64    "asset_version_tags",
65    "entity_tags",
66    "entityrelation_tags",
67];
68
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct PgStudyReadModel {
71    pub study_id: StudyId,
72    pub name: String,
73    pub description: Option<String>,
74    pub documentation: Option<String>,
75    pub agent_instructions: Option<String>,
76    pub external_id: Option<String>,
77    pub study_type_id: Option<StudyTypeId>,
78    pub date_created: Option<DateTime<Utc>>,
79    pub created_by: Option<String>,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct PgStudyTypeReadModel {
84    pub study_type_id: StudyTypeId,
85    pub name: String,
86    pub namespace: Option<String>,
87    pub ontology_class: Option<String>,
88    pub description: Option<String>,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct PgDomainReadModel {
93    pub domain_id: DomainId,
94    pub name: String,
95    pub uri: Option<String>,
96    pub description: Option<String>,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct PgStudyDomainReadModel {
101    pub study_id: StudyId,
102    pub domain_id: DomainId,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct PgDuoCodeReadModel {
107    pub duo_id: String,
108    pub duo_code: String,
109    pub label: String,
110    pub definition: String,
111    pub category: String,
112    pub qualifier_type: String,
113    pub qualifier_required: bool,
114    pub qualifier_note: Option<String>,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct PgStudyAccessReadModel {
119    pub study_id: StudyId,
120    pub user_id: String,
121    pub date_granted: Option<DateTime<Utc>>,
122    pub granted_by: Option<String>,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct PgStudyCustodianReadModel {
127    pub study_id: StudyId,
128    pub user_id: String,
129    pub is_primary: bool,
130    pub date_granted: Option<DateTime<Utc>>,
131    pub granted_by: Option<String>,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq)]
135pub struct PgDuoRestrictionReadModel {
136    pub restriction_id: i64,
137    pub study_id: StudyId,
138    pub duo_id: String,
139    pub duo_code: String,
140    pub label: String,
141    pub qualifier_ontology_namespace: Option<String>,
142    pub qualifier_ontology_id: Option<String>,
143    pub qualifier_text: Option<String>,
144    pub qualifier_date: Option<NaiveDate>,
145    pub qualifier_integer: Option<i32>,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct PgAssetReadModel {
150    pub asset_id: AssetId,
151    pub study_id: StudyId,
152    pub name: String,
153    pub description: Option<String>,
154    pub agent_instructions: Option<String>,
155    pub asset_type: AssetType,
156    pub date_created: Option<DateTime<Utc>>,
157    pub created_by: Option<String>,
158}
159
160#[derive(Debug, Clone, PartialEq, Eq)]
161pub struct PgAssetVersionReadModel {
162    pub version_id: VersionId,
163    pub asset_id: AssetId,
164    pub major: i32,
165    pub minor: i32,
166    pub patch: i32,
167    pub version_label: Option<String>,
168    pub version_note: Option<String>,
169    pub is_latest: Option<bool>,
170    pub doi: Option<String>,
171    pub created_at: Option<DateTime<Utc>>,
172    pub created_by: Option<String>,
173}
174
175#[derive(Debug, Clone, PartialEq, Eq)]
176pub struct PgAssetVersionDuoRestrictionReadModel {
177    pub restriction_id: i64,
178    pub version_id: VersionId,
179    pub duo_id: String,
180    pub duo_code: String,
181    pub label: String,
182    pub qualifier_ontology_namespace: Option<String>,
183    pub qualifier_ontology_id: Option<String>,
184    pub qualifier_text: Option<String>,
185    pub qualifier_date: Option<NaiveDate>,
186    pub qualifier_integer: Option<i32>,
187}
188
189#[derive(Debug, Clone, PartialEq, Eq)]
190pub struct PgDatasetReadModel {
191    pub dataset_id: VersionId,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct PgDataFileReadModel {
196    pub datafile_id: VersionId,
197    /// Size in bytes of the managed stored artifact, when known.
198    pub size_bytes: Option<i64>,
199    pub compressed: Option<bool>,
200    pub encrypted: Option<bool>,
201    pub compression_algorithm: Option<String>,
202    pub encryption_algorithm: Option<String>,
203    pub encryption_key: Option<Vec<u8>>,
204    pub storage_uri: String,
205    pub edam_format: String,
206    pub digest: String,
207}
208
209#[derive(Debug, Clone, PartialEq, Eq)]
210pub struct PgValueTypeReadModel {
211    pub value_type_id: ValueTypeId,
212    pub value_type: String,
213    pub description: Option<String>,
214}
215
216#[derive(Debug, Clone, PartialEq, Eq)]
217pub struct PgVocabularyReadModel {
218    pub vocabulary_id: VocabularyId,
219    pub domain_id: DomainId,
220    pub name: String,
221    pub description: Option<String>,
222}
223
224#[derive(Debug, Clone, PartialEq, Eq)]
225pub struct PgVocabularyMappingReadModel {
226    pub vocabulary_mapping_id: i64,
227    pub from_vocabulary_item: VocabularyItemId,
228    pub to_vocabulary_item: VocabularyItemId,
229}
230
231#[derive(Debug, Clone, PartialEq, Eq)]
232pub struct PgVocabularyMappingWithItemsReadModel {
233    pub mapping: PgVocabularyMappingReadModel,
234    pub from_item: PgVocabularyItemReadModel,
235    pub to_item: PgVocabularyItemReadModel,
236}
237
238#[derive(Debug, Clone, PartialEq, Eq)]
239pub struct PgVocabularyItemReadModel {
240    pub vocabulary_item_id: VocabularyItemId,
241    pub vocabulary_id: VocabularyId,
242    pub value: i32,
243    pub code: String,
244    pub description: Option<String>,
245}
246
247#[derive(Debug, Clone, PartialEq, Eq)]
248pub struct PgVariableReadModel {
249    pub variable_id: VariableId,
250    pub domain_id: DomainId,
251    pub name: String,
252    pub value_type_id: ValueTypeId,
253    pub value_type: String,
254    pub value_format: Option<String>,
255    pub vocabulary_id: Option<VocabularyId>,
256    pub key_role: KeyRole,
257    pub description: Option<String>,
258    pub note: Option<String>,
259    pub ontology_namespace: Option<String>,
260    pub ontology_class: Option<String>,
261}
262
263pub mod provisional {
264    use super::VariableId;
265
266    /// Schema-pending read-model stub for a future `public.variable_mapping` table.
267    /// Callers should treat this as provisional until the live metadata schema exists.
268    #[derive(Debug, Clone, PartialEq, Eq)]
269    pub struct PgVariableMappingReadModel {
270        pub mapping_id: i64,
271        pub from_variable_id: VariableId,
272        pub to_variable_id: VariableId,
273        pub operator: String,
274        pub operants: String,
275        pub prerequisite_id: Option<VariableId>,
276    }
277}
278
279#[derive(Debug, Clone, PartialEq, Eq)]
280pub struct PgDatasetVariableReadModel {
281    pub dataset_id: VersionId,
282    pub variable_id: VariableId,
283    pub row_role: KeyRole,
284}
285
286#[derive(Debug, Clone, PartialEq, Eq)]
287pub struct PgEntityReadModel {
288    pub entity_id: EntityId,
289    pub domain_id: DomainId,
290    pub uuid: Option<Uuid>,
291    pub name: String,
292    pub description: Option<String>,
293    pub agent_instructions: Option<String>,
294    pub ontology_namespace: Option<String>,
295    pub ontology_class: Option<String>,
296}
297
298#[derive(Debug, Clone, PartialEq, Eq)]
299pub struct PgEntityInstanceReadModel {
300    pub instance_id: i64,
301    pub uuid: Uuid,
302    pub entity_id: EntityId,
303    pub label: Option<String>,
304    pub note: Option<String>,
305    pub transformation_id: TransformationId,
306}
307
308#[derive(Debug, Clone, PartialEq, Eq)]
309pub struct PgStudyEntityInstanceReadModel {
310    pub study_id: StudyId,
311    pub entity_instance_id: i64,
312    pub entity_id: EntityId,
313    pub external_id: String,
314    pub transformation_id: TransformationId,
315}
316
317#[derive(Debug, Clone, PartialEq, Eq)]
318pub struct PgEntityRelationReadModel {
319    pub entity_relation_id: EntityRelationId,
320    pub subject_entity_id: EntityId,
321    pub object_entity_id: EntityId,
322    pub domain_id: DomainId,
323    pub uuid: Option<Uuid>,
324    pub name: String,
325    pub description: Option<String>,
326    pub agent_instructions: Option<String>,
327    pub ontology_namespace: Option<String>,
328    pub ontology_class: Option<String>,
329}
330
331#[derive(Debug, Clone, PartialEq, Eq)]
332pub struct PgRelationInstanceReadModel {
333    pub relation_instance_id: i64,
334    pub uuid: Uuid,
335    pub entity_relation_id: EntityRelationId,
336    pub entity_instance_id_1: i64,
337    pub entity_instance_id_2: i64,
338    pub valid_from: Option<NaiveDate>,
339    pub valid_to: Option<NaiveDate>,
340    pub note: Option<String>,
341    pub transformation_id: TransformationId,
342}
343
344#[derive(Debug, Clone, PartialEq, Eq)]
345pub struct PgStudyRelationInstanceReadModel {
346    pub study_id: StudyId,
347    pub relation_instance_id: i64,
348    pub entity_relation_id: EntityRelationId,
349    pub external_id: String,
350    pub transformation_id: TransformationId,
351}
352
353#[derive(Debug, Clone, PartialEq, Eq)]
354pub struct PgAssetVersionEntityReadModel {
355    pub version_id: VersionId,
356    pub entity_instance_id: i64,
357    pub transformation_id: TransformationId,
358}
359
360#[derive(Debug, Clone, PartialEq, Eq)]
361pub struct PgDatasetVersionEntityReadModel {
362    pub version_id: VersionId,
363    pub entity_instance_id: i64,
364    pub entity_variable_id: VariableId,
365    pub transformation_id: TransformationId,
366}
367
368#[derive(Debug, Clone, PartialEq, Eq)]
369pub struct PgAssetVersionRelationInstanceReadModel {
370    pub version_id: VersionId,
371    pub relation_instance_id: i64,
372    pub transformation_id: TransformationId,
373}
374
375#[derive(Debug, Clone, PartialEq, Eq)]
376pub struct PgDatasetVersionRelationInstanceReadModel {
377    pub version_id: VersionId,
378    pub relation_instance_id: i64,
379    pub subject_variable_id: VariableId,
380    pub object_variable_id: VariableId,
381    pub relation_variable_id: Option<VariableId>,
382    pub transformation_id: TransformationId,
383}
384
385#[derive(Debug, Clone, PartialEq, Eq)]
386pub struct PgTransformationReadModel {
387    pub transformation_id: TransformationId,
388    pub transformation_type: TransformationType,
389    pub description: String,
390    pub repository_url: Option<String>,
391    pub commit_hash: Option<String>,
392    pub file_path: Option<String>,
393    pub date_created: Option<DateTime<Utc>>,
394    pub created_by: Option<String>,
395}
396
397#[derive(Debug, Clone, PartialEq, Eq)]
398pub struct PgTagReadModel {
399    pub tag_id: i64,
400    pub name: String,
401}
402
403#[derive(Debug, Clone, PartialEq, Eq)]
404pub struct PgStudyTagReadModel {
405    pub study_id: StudyId,
406    pub tag_id: i64,
407}
408
409#[derive(Debug, Clone, PartialEq, Eq)]
410pub struct PgDomainTagReadModel {
411    pub domain_id: DomainId,
412    pub tag_id: i64,
413}
414
415#[derive(Debug, Clone, PartialEq, Eq)]
416pub struct PgVariableTagReadModel {
417    pub variable_id: VariableId,
418    pub tag_id: i64,
419}
420
421#[derive(Debug, Clone, PartialEq, Eq)]
422pub struct PgAssetTagReadModel {
423    pub asset_id: AssetId,
424    pub tag_id: i64,
425}
426
427#[derive(Debug, Clone, PartialEq, Eq)]
428pub struct PgAssetVersionTagReadModel {
429    pub version_id: VersionId,
430    pub tag_id: i64,
431}
432
433#[derive(Debug, Clone, PartialEq, Eq)]
434pub struct PgEntityTagReadModel {
435    pub entity_id: EntityId,
436    pub tag_id: i64,
437}
438
439#[derive(Debug, Clone, PartialEq, Eq)]
440pub struct PgEntityRelationTagReadModel {
441    pub entity_relation_id: EntityRelationId,
442    pub tag_id: i64,
443}
444
445#[derive(Debug, Clone, PartialEq, Eq)]
446pub struct PgTransformationInputReadModel {
447    pub transformation_id: TransformationId,
448    pub version_id: VersionId,
449}
450
451#[derive(Debug, Clone, PartialEq, Eq)]
452pub struct PgTransformationOutputReadModel {
453    pub transformation_id: TransformationId,
454    pub version_id: VersionId,
455}
456
457impl PgMetadataConnection {
458    pub fn missing_metadata_tables(&mut self) -> Result<Vec<&'static str>, PgMetaError> {
459        let mut missing = Vec::new();
460        for table_name in METADATA_TABLES {
461            let exists = MetadataExecutor::query_optional(
462                self,
463                "check metadata table",
464                "
465                    SELECT 1
466                      FROM information_schema.tables
467                     WHERE table_schema = 'public'
468                       AND table_name = $1
469                     LIMIT 1
470                    ",
471                &[table_name],
472            )?
473            .is_some();
474            if !exists {
475                missing.push(*table_name);
476            }
477        }
478        Ok(missing)
479    }
480
481    pub fn list_study_types(&mut self) -> Result<Vec<PgStudyTypeReadModel>, PgMetaError> {
482        let rows = self
483            .client
484            .query(
485                "
486                SELECT study_type_id, name, namespace, ontology_class, description
487                  FROM public.study_types
488                 ORDER BY study_type_id
489                ",
490                &[],
491            )
492            .map_err(PgMetaError::Query)?;
493        rows.iter().map(study_type_from_row).collect()
494    }
495
496    pub fn list_studies(&mut self) -> Result<Vec<PgStudyReadModel>, PgMetaError> {
497        let rows = self
498            .client
499            .query(
500                "
501                SELECT study_id, name, description, documentation, agent_instructions,
502                       external_id, study_type_id, date_created, created_by
503                  FROM public.studies
504                 ORDER BY name
505                ",
506                &[],
507            )
508            .map_err(PgMetaError::Query)?;
509        rows.iter().map(study_from_row).collect()
510    }
511
512    /// Lists Studies explicitly linked to `domain_id` through `study_domains`.
513    /// Dataset and Variable relationships are intentionally absent from this
514    /// authority query.
515    pub fn list_studies_for_domain(
516        &mut self,
517        domain_id: DomainId,
518    ) -> Result<Vec<PgStudyReadModel>, PgMetaError> {
519        let rows = self
520            .client
521            .query(
522                "
523                SELECT s.study_id, s.name, s.description, s.documentation,
524                       s.agent_instructions, s.external_id, s.study_type_id,
525                       s.date_created, s.created_by
526                  FROM public.studies s
527                  JOIN public.study_domains sd ON sd.study_id = s.study_id
528                 WHERE sd.domain_id = $1
529                 ORDER BY lower(s.name), s.study_id
530                ",
531                &[&id_i32(domain_id.0, "domain_id")?],
532            )
533            .map_err(PgMetaError::Query)?;
534        rows.iter().map(study_from_row).collect()
535    }
536
537    pub fn get_study(
538        &mut self,
539        study_id: StudyId,
540    ) -> Result<Option<PgStudyReadModel>, PgMetaError> {
541        let row = self
542            .client
543            .query_opt(
544                "
545                SELECT study_id, name, description, documentation, agent_instructions,
546                       external_id, study_type_id, date_created, created_by
547                  FROM public.studies
548                 WHERE study_id = $1
549                 LIMIT 1
550                ",
551                &[&study_id.0],
552            )
553            .map_err(PgMetaError::Query)?;
554        row.as_ref().map(study_from_row).transpose()
555    }
556
557    pub fn get_study_by_name(
558        &mut self,
559        name: &str,
560    ) -> Result<Option<PgStudyReadModel>, PgMetaError> {
561        let row = self
562            .client
563            .query_opt(
564                "
565                SELECT study_id, name, description, documentation, agent_instructions,
566                       external_id, study_type_id, date_created, created_by
567                  FROM public.studies
568                 WHERE name = $1
569                 LIMIT 1
570                ",
571                &[&name],
572            )
573            .map_err(PgMetaError::Query)?;
574        row.as_ref().map(study_from_row).transpose()
575    }
576
577    pub fn list_domains(&mut self) -> Result<Vec<PgDomainReadModel>, PgMetaError> {
578        let rows = self
579            .client
580            .query(
581                "
582                SELECT domain_id, name, uri, description
583                  FROM public.domains
584                 ORDER BY name
585                ",
586                &[],
587            )
588            .map_err(PgMetaError::Query)?;
589        rows.iter().map(domain_from_row).collect()
590    }
591
592    pub fn list_domains_by_name(
593        &mut self,
594        name: &str,
595    ) -> Result<Vec<PgDomainReadModel>, PgMetaError> {
596        let rows = self.client.query(
597            "SELECT domain_id, name, uri, description FROM public.domains WHERE name = $1 ORDER BY domain_id",
598            &[&name],
599        ).map_err(PgMetaError::Query)?;
600        rows.iter().map(domain_from_row).collect()
601    }
602
603    pub fn get_domain(
604        &mut self,
605        domain_id: DomainId,
606    ) -> Result<Option<PgDomainReadModel>, PgMetaError> {
607        let row = self
608            .client
609            .query_opt(
610                "
611                SELECT domain_id, name, uri, description
612                  FROM public.domains
613                 WHERE domain_id = $1
614                 LIMIT 1
615                ",
616                &[&id_i32(domain_id.0, "domain_id")?],
617            )
618            .map_err(PgMetaError::Query)?;
619        row.as_ref().map(domain_from_row).transpose()
620    }
621
622    pub fn get_domain_by_name(
623        &mut self,
624        name: &str,
625    ) -> Result<Option<PgDomainReadModel>, PgMetaError> {
626        let row = self
627            .client
628            .query_opt(
629                "
630                SELECT domain_id, name, uri, description
631                  FROM public.domains
632                 WHERE name = $1
633                 LIMIT 1
634                ",
635                &[&name],
636            )
637            .map_err(PgMetaError::Query)?;
638        row.as_ref().map(domain_from_row).transpose()
639    }
640
641    pub fn list_duo_codes(&mut self) -> Result<Vec<PgDuoCodeReadModel>, PgMetaError> {
642        let rows = self
643            .client
644            .query(
645                "
646                SELECT duo_id, duo_code, label, definition, category,
647                       qualifier_type, qualifier_required, qualifier_note
648                  FROM public.duo_codes
649                 ORDER BY duo_code
650                ",
651                &[],
652            )
653            .map_err(PgMetaError::Query)?;
654        rows.iter().map(duo_code_from_row).collect()
655    }
656
657    pub fn list_study_domains(
658        &mut self,
659        study_id: StudyId,
660    ) -> Result<Vec<PgDomainReadModel>, PgMetaError> {
661        let rows = self
662            .client
663            .query(
664                "
665                SELECT d.domain_id, d.name, d.uri, d.description
666                  FROM public.domains d
667                  JOIN public.study_domains sd ON sd.domain_id = d.domain_id
668                 WHERE sd.study_id = $1
669                 ORDER BY d.name
670                ",
671                &[&study_id.0],
672            )
673            .map_err(PgMetaError::Query)?;
674        rows.iter().map(domain_from_row).collect()
675    }
676
677    pub fn list_study_domain_links(
678        &mut self,
679        study_id: StudyId,
680    ) -> Result<Vec<PgStudyDomainReadModel>, PgMetaError> {
681        let rows = self
682            .client
683            .query(
684                "
685                SELECT study_id, domain_id
686                  FROM public.study_domains
687                 WHERE study_id = $1
688                 ORDER BY domain_id
689                ",
690                &[&study_id.0],
691            )
692            .map_err(PgMetaError::Query)?;
693        rows.iter().map(study_domain_from_row).collect()
694    }
695
696    pub fn list_study_access(
697        &mut self,
698        study_id: StudyId,
699    ) -> Result<Vec<PgStudyAccessReadModel>, PgMetaError> {
700        let rows = self
701            .client
702            .query(
703                "
704                SELECT study_id, user_id, date_granted, granted_by
705                  FROM public.study_access
706                 WHERE study_id = $1
707                 ORDER BY user_id
708                ",
709                &[&study_id.0],
710            )
711            .map_err(PgMetaError::Query)?;
712        rows.iter().map(study_access_from_row).collect()
713    }
714
715    pub fn list_study_custodians(
716        &mut self,
717        study_id: StudyId,
718    ) -> Result<Vec<PgStudyCustodianReadModel>, PgMetaError> {
719        let rows = self
720            .client
721            .query(
722                "
723                SELECT study_id, user_id, is_primary, date_granted, granted_by
724                  FROM public.study_custodians
725                 WHERE study_id = $1
726                 ORDER BY is_primary DESC, user_id
727                ",
728                &[&study_id.0],
729            )
730            .map_err(PgMetaError::Query)?;
731        rows.iter().map(study_custodian_from_row).collect()
732    }
733
734    pub fn list_study_duo_restrictions(
735        &mut self,
736        study_id: StudyId,
737    ) -> Result<Vec<PgDuoRestrictionReadModel>, PgMetaError> {
738        let rows = self
739            .client
740            .query(
741                "
742                SELECT sdr.study_duo_restriction_id, sdr.study_id, sdr.duo_id,
743                       dc.duo_code, dc.label,
744                       sdr.qualifier_ontology_namespace, sdr.qualifier_ontology_id,
745                       sdr.qualifier_text, sdr.qualifier_date, sdr.qualifier_integer
746                  FROM public.study_duo_restrictions sdr
747                  JOIN public.duo_codes dc ON dc.duo_id = sdr.duo_id
748                 WHERE sdr.study_id = $1
749                 ORDER BY sdr.study_duo_restriction_id
750                ",
751                &[&study_id.0],
752            )
753            .map_err(PgMetaError::Query)?;
754        rows.iter().map(study_duo_from_row).collect()
755    }
756
757    pub fn list_assets_for_study(
758        &mut self,
759        study_id: StudyId,
760    ) -> Result<Vec<PgAssetReadModel>, PgMetaError> {
761        let rows = self
762            .client
763            .query(
764                "
765                SELECT asset_id, study_id, name, description, agent_instructions,
766                       asset_type::text AS asset_type, date_created, created_by
767                  FROM public.assets
768                 WHERE study_id = $1
769                 ORDER BY name
770                ",
771                &[&study_id.0],
772            )
773            .map_err(PgMetaError::Query)?;
774        rows.iter().map(asset_from_row).collect()
775    }
776
777    pub fn get_asset(
778        &mut self,
779        asset_id: AssetId,
780    ) -> Result<Option<PgAssetReadModel>, PgMetaError> {
781        self.client
782            .query_opt(
783                "
784                SELECT asset_id, study_id, name, description, agent_instructions,
785                       asset_type::text AS asset_type, date_created, created_by
786                  FROM public.assets
787                 WHERE asset_id = $1
788                 LIMIT 1
789                ",
790                &[&asset_id.0],
791            )
792            .map_err(PgMetaError::Query)?
793            .as_ref()
794            .map(asset_from_row)
795            .transpose()
796    }
797
798    pub fn get_asset_by_name(
799        &mut self,
800        study_id: StudyId,
801        name: &str,
802    ) -> Result<Option<PgAssetReadModel>, PgMetaError> {
803        self.client
804            .query_opt(
805                "
806                SELECT asset_id, study_id, name, description, agent_instructions,
807                       asset_type::text AS asset_type, date_created, created_by
808                  FROM public.assets
809                 WHERE study_id = $1
810                   AND name = $2
811                 LIMIT 1
812                ",
813                &[&study_id.0, &name],
814            )
815            .map_err(PgMetaError::Query)?
816            .as_ref()
817            .map(asset_from_row)
818            .transpose()
819    }
820
821    pub fn list_asset_versions(
822        &mut self,
823        asset_id: AssetId,
824    ) -> Result<Vec<PgAssetVersionReadModel>, PgMetaError> {
825        let rows = self
826            .client
827            .query(
828                "
829                SELECT version_id, asset_id, major, minor, patch, version_label,
830                       version_note, is_latest, doi, created_at, created_by
831                  FROM public.asset_versions
832                 WHERE asset_id = $1
833                 ORDER BY major, minor, patch
834                ",
835                &[&asset_id.0],
836            )
837            .map_err(PgMetaError::Query)?;
838        rows.iter().map(asset_version_from_row).collect()
839    }
840
841    pub fn get_asset_version(
842        &mut self,
843        version_id: VersionId,
844    ) -> Result<Option<PgAssetVersionReadModel>, PgMetaError> {
845        self.client
846            .query_opt(
847                "
848                SELECT version_id, asset_id, major, minor, patch, version_label,
849                       version_note, is_latest, doi, created_at, created_by
850                  FROM public.asset_versions
851                 WHERE version_id = $1
852                 LIMIT 1
853                ",
854                &[&version_id.0],
855            )
856            .map_err(PgMetaError::Query)?
857            .as_ref()
858            .map(asset_version_from_row)
859            .transpose()
860    }
861
862    pub fn list_asset_version_duo_restrictions(
863        &mut self,
864        version_id: VersionId,
865    ) -> Result<Vec<PgAssetVersionDuoRestrictionReadModel>, PgMetaError> {
866        let rows = self
867            .client
868            .query(
869                "
870                SELECT avdr.asset_version_duo_restriction_id, avdr.version_id, avdr.duo_id,
871                       dc.duo_code, dc.label,
872                       avdr.qualifier_ontology_namespace, avdr.qualifier_ontology_id,
873                       avdr.qualifier_text, avdr.qualifier_date, avdr.qualifier_integer
874                  FROM public.asset_version_duo_restrictions avdr
875                  JOIN public.duo_codes dc ON dc.duo_id = avdr.duo_id
876                 WHERE avdr.version_id = $1
877                 ORDER BY avdr.asset_version_duo_restriction_id
878                ",
879                &[&version_id.0],
880            )
881            .map_err(PgMetaError::Query)?;
882        rows.iter().map(asset_version_duo_from_row).collect()
883    }
884
885    pub fn list_datasets_for_asset(
886        &mut self,
887        asset_id: AssetId,
888    ) -> Result<Vec<PgDatasetReadModel>, PgMetaError> {
889        let rows = self
890            .client
891            .query(
892                "
893                SELECT d.dataset_id
894                  FROM public.datasets d
895                  JOIN public.asset_versions av ON av.version_id = d.dataset_id
896                 WHERE av.asset_id = $1
897                 ORDER BY av.major, av.minor, av.patch
898                ",
899                &[&asset_id.0],
900            )
901            .map_err(PgMetaError::Query)?;
902        rows.iter().map(dataset_from_row).collect()
903    }
904
905    pub fn get_dataset(
906        &mut self,
907        dataset_id: VersionId,
908    ) -> Result<Option<PgDatasetReadModel>, PgMetaError> {
909        self.client
910            .query_opt(
911                "
912                SELECT dataset_id
913                  FROM public.datasets
914                 WHERE dataset_id = $1
915                 LIMIT 1
916                ",
917                &[&dataset_id.0],
918            )
919            .map_err(PgMetaError::Query)?
920            .as_ref()
921            .map(dataset_from_row)
922            .transpose()
923    }
924
925    /// Returns the authoritative withdrawal timestamp for one Dataset version.
926    pub fn get_dataset_version_withdrawn_at(
927        &mut self,
928        dataset_id: VersionId,
929    ) -> Result<Option<DateTime<Utc>>, PgMetaError> {
930        self.client
931            .query_opt(
932                "
933                SELECT withdrawn_at
934                  FROM public.dataset_version_withdrawals
935                 WHERE dataset_id = $1
936                 LIMIT 1
937                ",
938                &[&dataset_id.0],
939            )
940            .map_err(PgMetaError::Query)
941            .map(|row| row.map(|row| utc(row.get::<_, NaiveDateTime>("withdrawn_at"))))
942    }
943
944    pub fn list_datafiles_for_asset(
945        &mut self,
946        asset_id: AssetId,
947    ) -> Result<Vec<PgDataFileReadModel>, PgMetaError> {
948        let rows = self
949            .client
950            .query(
951                "
952                SELECT df.datafile_id, df.size_bytes, df.compressed, df.encrypted,
953                       df.compression_algorithm, df.encryption_algorithm,
954                       df.encryption_key, df.storage_uri, df.edam_format, df.digest
955                  FROM public.datafiles df
956                  JOIN public.asset_versions av ON av.version_id = df.datafile_id
957                 WHERE av.asset_id = $1
958                 ORDER BY av.major, av.minor, av.patch
959                ",
960                &[&asset_id.0],
961            )
962            .map_err(PgMetaError::Query)?;
963        rows.iter().map(datafile_from_row).collect()
964    }
965
966    pub fn get_datafile(
967        &mut self,
968        datafile_id: VersionId,
969    ) -> Result<Option<PgDataFileReadModel>, PgMetaError> {
970        self.client
971            .query_opt(
972                "
973                SELECT datafile_id, size_bytes, compressed, encrypted,
974                       compression_algorithm, encryption_algorithm,
975                       encryption_key, storage_uri, edam_format, digest
976                  FROM public.datafiles
977                 WHERE datafile_id = $1
978                 LIMIT 1
979                ",
980                &[&datafile_id.0],
981            )
982            .map_err(PgMetaError::Query)?
983            .as_ref()
984            .map(datafile_from_row)
985            .transpose()
986    }
987
988    pub fn list_value_types(&mut self) -> Result<Vec<PgValueTypeReadModel>, PgMetaError> {
989        let rows = self
990            .client
991            .query(
992                "
993                SELECT value_type_id, value_type, description
994                  FROM public.value_types
995                 ORDER BY value_type_id
996                ",
997                &[],
998            )
999            .map_err(PgMetaError::Query)?;
1000        rows.iter().map(value_type_from_row).collect()
1001    }
1002
1003    pub fn list_vocabularies_for_domain(
1004        &mut self,
1005        domain_id: DomainId,
1006    ) -> Result<Vec<PgVocabularyReadModel>, PgMetaError> {
1007        let rows = self
1008            .client
1009            .query(
1010                "
1011                SELECT vocabulary_id, domain_id, name, description
1012                  FROM public.vocabularies
1013                 WHERE domain_id = $1
1014                 ORDER BY name
1015                ",
1016                &[&id_i32(domain_id.0, "domain_id")?],
1017            )
1018            .map_err(PgMetaError::Query)?;
1019        rows.iter().map(vocabulary_from_row).collect()
1020    }
1021
1022    pub fn get_vocabulary(
1023        &mut self,
1024        vocabulary_id: VocabularyId,
1025    ) -> Result<Option<PgVocabularyReadModel>, PgMetaError> {
1026        self.client
1027            .query_opt(
1028                "
1029                SELECT vocabulary_id, domain_id, name, description
1030                  FROM public.vocabularies
1031                 WHERE vocabulary_id = $1
1032                 LIMIT 1
1033                ",
1034                &[&id_i32(vocabulary_id.0, "vocabulary_id")?],
1035            )
1036            .map_err(PgMetaError::Query)?
1037            .as_ref()
1038            .map(vocabulary_from_row)
1039            .transpose()
1040    }
1041
1042    pub fn list_vocabulary_items(
1043        &mut self,
1044        vocabulary_id: VocabularyId,
1045    ) -> Result<Vec<PgVocabularyItemReadModel>, PgMetaError> {
1046        let rows = self
1047            .client
1048            .query(
1049                "
1050                SELECT vocabulary_item_id, vocabulary_id, value, code, description
1051                  FROM public.vocabulary_items
1052                 WHERE vocabulary_id = $1
1053                 ORDER BY value, vocabulary_item_id
1054                ",
1055                &[&id_i32(vocabulary_id.0, "vocabulary_id")?],
1056            )
1057            .map_err(PgMetaError::Query)?;
1058        rows.iter().map(vocabulary_item_from_row).collect()
1059    }
1060
1061    pub fn get_vocabulary_item(
1062        &mut self,
1063        vocabulary_item_id: VocabularyItemId,
1064    ) -> Result<Option<PgVocabularyItemReadModel>, PgMetaError> {
1065        self.client
1066            .query_opt(
1067                "
1068                SELECT vocabulary_item_id, vocabulary_id, value, code, description
1069                  FROM public.vocabulary_items
1070                 WHERE vocabulary_item_id = $1
1071                 LIMIT 1
1072                ",
1073                &[&id_i32(vocabulary_item_id.0, "vocabulary_item_id")?],
1074            )
1075            .map_err(PgMetaError::Query)?
1076            .as_ref()
1077            .map(vocabulary_item_from_row)
1078            .transpose()
1079    }
1080
1081    pub fn list_vocabulary_mappings(
1082        &mut self,
1083    ) -> Result<Vec<PgVocabularyMappingReadModel>, PgMetaError> {
1084        let rows = self
1085            .client
1086            .query(
1087                "
1088                SELECT vocabulary_mapping_id, from_vocabulary_item, to_vocabulary_item
1089                  FROM public.vocabulary_mapping
1090                 ORDER BY vocabulary_mapping_id
1091                ",
1092                &[],
1093            )
1094            .map_err(PgMetaError::Query)?;
1095        rows.iter().map(vocabulary_mapping_from_row).collect()
1096    }
1097
1098    pub fn list_vocabulary_mappings_for_vocabulary(
1099        &mut self,
1100        vocabulary_id: VocabularyId,
1101    ) -> Result<Vec<PgVocabularyMappingWithItemsReadModel>, PgMetaError> {
1102        let rows = self
1103            .client
1104            .query(
1105                "
1106                SELECT vm.vocabulary_mapping_id,
1107                       fi.vocabulary_item_id AS from_vocabulary_item_id,
1108                       fi.vocabulary_id AS from_vocabulary_id,
1109                       fi.value AS from_value, fi.code AS from_code,
1110                       fi.description AS from_description,
1111                       ti.vocabulary_item_id AS to_vocabulary_item_id,
1112                       ti.vocabulary_id AS to_vocabulary_id,
1113                       ti.value AS to_value, ti.code AS to_code,
1114                       ti.description AS to_description
1115                  FROM public.vocabulary_mapping vm
1116                  JOIN public.vocabulary_items fi
1117                    ON fi.vocabulary_item_id = vm.from_vocabulary_item
1118                  JOIN public.vocabulary_items ti
1119                    ON ti.vocabulary_item_id = vm.to_vocabulary_item
1120                 WHERE fi.vocabulary_id = $1 OR ti.vocabulary_id = $1
1121                 ORDER BY fi.vocabulary_id, fi.value, fi.vocabulary_item_id,
1122                          ti.vocabulary_id, ti.value, ti.vocabulary_item_id,
1123                          vm.vocabulary_mapping_id
1124                ",
1125                &[&id_i32(vocabulary_id.0, "vocabulary_id")?],
1126            )
1127            .map_err(PgMetaError::Query)?;
1128        rows.iter()
1129            .map(|row| {
1130                let mapping = PgVocabularyMappingReadModel {
1131                    vocabulary_mapping_id: i64::from(row.get::<_, i32>("vocabulary_mapping_id")),
1132                    from_vocabulary_item: VocabularyItemId(i64::from(
1133                        row.get::<_, i32>("from_vocabulary_item_id"),
1134                    )),
1135                    to_vocabulary_item: VocabularyItemId(i64::from(
1136                        row.get::<_, i32>("to_vocabulary_item_id"),
1137                    )),
1138                };
1139                let from_item = PgVocabularyItemReadModel {
1140                    vocabulary_item_id: VocabularyItemId(i64::from(
1141                        row.get::<_, i32>("from_vocabulary_item_id"),
1142                    )),
1143                    vocabulary_id: VocabularyId(i64::from(row.get::<_, i32>("from_vocabulary_id"))),
1144                    value: row.get("from_value"),
1145                    code: row.get("from_code"),
1146                    description: row.get("from_description"),
1147                };
1148                let to_item = PgVocabularyItemReadModel {
1149                    vocabulary_item_id: VocabularyItemId(i64::from(
1150                        row.get::<_, i32>("to_vocabulary_item_id"),
1151                    )),
1152                    vocabulary_id: VocabularyId(i64::from(row.get::<_, i32>("to_vocabulary_id"))),
1153                    value: row.get("to_value"),
1154                    code: row.get("to_code"),
1155                    description: row.get("to_description"),
1156                };
1157                Ok(PgVocabularyMappingWithItemsReadModel {
1158                    mapping,
1159                    from_item,
1160                    to_item,
1161                })
1162            })
1163            .collect()
1164    }
1165
1166    pub fn list_variables_for_domain(
1167        &mut self,
1168        domain_id: DomainId,
1169    ) -> Result<Vec<PgVariableReadModel>, PgMetaError> {
1170        let rows = self
1171            .client
1172            .query(
1173                "
1174                SELECT v.variable_id, v.domain_id, v.name, v.value_type_id,
1175                       vt.value_type, v.value_format, v.vocabulary_id,
1176                       v.keyrole::text AS keyrole, v.description, v.note,
1177                       v.ontology_namespace, v.ontology_class
1178                  FROM public.variables v
1179                  JOIN public.value_types vt ON vt.value_type_id = v.value_type_id
1180                 WHERE v.domain_id = $1
1181                 ORDER BY lower(v.name), v.variable_id
1182                ",
1183                &[&id_i32(domain_id.0, "domain_id")?],
1184            )
1185            .map_err(PgMetaError::Query)?;
1186        rows.iter().map(variable_from_row).collect()
1187    }
1188
1189    pub fn get_variable(
1190        &mut self,
1191        variable_id: VariableId,
1192    ) -> Result<Option<PgVariableReadModel>, PgMetaError> {
1193        self.client
1194            .query_opt(
1195                "
1196                SELECT v.variable_id, v.domain_id, v.name, v.value_type_id,
1197                       vt.value_type, v.value_format, v.vocabulary_id,
1198                       v.keyrole::text AS keyrole, v.description, v.note,
1199                       v.ontology_namespace, v.ontology_class
1200                  FROM public.variables v
1201                  JOIN public.value_types vt ON vt.value_type_id = v.value_type_id
1202                 WHERE v.variable_id = $1
1203                 LIMIT 1
1204                ",
1205                &[&id_i32(variable_id.0, "variable_id")?],
1206            )
1207            .map_err(PgMetaError::Query)?
1208            .as_ref()
1209            .map(variable_from_row)
1210            .transpose()
1211    }
1212
1213    /// Provisional read path for a future `public.variable_mapping` table.
1214    /// Callers should first verify that the target schema actually contains this table.
1215    pub fn list_variable_mappings_provisional(
1216        &mut self,
1217    ) -> Result<Vec<provisional::PgVariableMappingReadModel>, PgMetaError> {
1218        let rows = self
1219            .client
1220            .query(
1221                "
1222                SELECT mapping_id, from_variable_id, to_variable_id,
1223                       operator::text AS operator, operants, prerequisite_id
1224                  FROM public.variable_mapping
1225                 ORDER BY mapping_id
1226                ",
1227                &[],
1228            )
1229            .map_err(PgMetaError::Query)?;
1230        rows.iter().map(variable_mapping_from_row).collect()
1231    }
1232
1233    pub fn list_dataset_variables(
1234        &mut self,
1235        dataset_id: VersionId,
1236    ) -> Result<Vec<PgDatasetVariableReadModel>, PgMetaError> {
1237        let rows = self
1238            .client
1239            .query(
1240                "
1241                SELECT dataset_id, variable_id, row_role::text AS row_role
1242                  FROM public.dataset_variables
1243                 WHERE dataset_id = $1
1244                 ORDER BY variable_id
1245                ",
1246                &[&dataset_id.0],
1247            )
1248            .map_err(PgMetaError::Query)?;
1249        rows.iter().map(dataset_variable_from_row).collect()
1250    }
1251
1252    pub fn list_dataset_variables_for_variable(
1253        &mut self,
1254        variable_id: VariableId,
1255    ) -> Result<Vec<PgDatasetVariableReadModel>, PgMetaError> {
1256        let rows = self
1257            .client
1258            .query(
1259                "
1260                SELECT dataset_id, variable_id, row_role::text AS row_role
1261                  FROM public.dataset_variables
1262                 WHERE variable_id = $1
1263                 ORDER BY dataset_id
1264                ",
1265                &[&id_i32(variable_id.0, "variable_id")?],
1266            )
1267            .map_err(PgMetaError::Query)?;
1268        rows.iter().map(dataset_variable_from_row).collect()
1269    }
1270
1271    pub fn list_entities_for_domain(
1272        &mut self,
1273        domain_id: DomainId,
1274    ) -> Result<Vec<PgEntityReadModel>, PgMetaError> {
1275        let rows = self
1276            .client
1277            .query(
1278                "
1279                SELECT entity_id, domain_id, uuid, name, description,
1280                       agent_instructions, ontology_namespace, ontology_class
1281                  FROM public.entities
1282                 WHERE domain_id = $1
1283                 ORDER BY name
1284                ",
1285                &[&id_i32(domain_id.0, "domain_id")?],
1286            )
1287            .map_err(PgMetaError::Query)?;
1288        rows.iter().map(entity_from_row).collect()
1289    }
1290
1291    pub fn list_entity_relations_for_domain(
1292        &mut self,
1293        domain_id: DomainId,
1294    ) -> Result<Vec<PgEntityRelationReadModel>, PgMetaError> {
1295        let rows = self
1296            .client
1297            .query(
1298                "
1299                SELECT entityrelation_id, subject_entity_id, object_entity_id,
1300                       domain_id, uuid, name, description, agent_instructions,
1301                       ontology_namespace, ontology_class
1302                  FROM public.entityrelations
1303                 WHERE domain_id = $1
1304                 ORDER BY name
1305                ",
1306                &[&id_i32(domain_id.0, "domain_id")?],
1307            )
1308            .map_err(PgMetaError::Query)?;
1309        rows.iter().map(entity_relation_from_row).collect()
1310    }
1311
1312    pub fn list_entity_instances_for_entity(
1313        &mut self,
1314        entity_id: EntityId,
1315    ) -> Result<Vec<PgEntityInstanceReadModel>, PgMetaError> {
1316        let rows = self
1317            .client
1318            .query(
1319                "
1320                SELECT instance_id, uuid, entity_id, label, note, transformation_id
1321                  FROM public.entity_instances
1322                 WHERE entity_id = $1
1323                 ORDER BY instance_id
1324                ",
1325                &[&id_i32(entity_id.0, "entity_id")?],
1326            )
1327            .map_err(PgMetaError::Query)?;
1328        rows.iter().map(entity_instance_from_row).collect()
1329    }
1330
1331    pub fn get_entity_instance(
1332        &mut self,
1333        instance_id: i64,
1334    ) -> Result<Option<PgEntityInstanceReadModel>, PgMetaError> {
1335        self.client
1336            .query_opt(
1337                "
1338                SELECT instance_id, uuid, entity_id, label, note, transformation_id
1339                  FROM public.entity_instances
1340                 WHERE instance_id = $1
1341                 LIMIT 1
1342                ",
1343                &[&instance_id],
1344            )
1345            .map_err(PgMetaError::Query)?
1346            .as_ref()
1347            .map(entity_instance_from_row)
1348            .transpose()
1349    }
1350
1351    pub fn get_entity_instance_by_uuid(
1352        &mut self,
1353        uuid: Uuid,
1354    ) -> Result<Option<PgEntityInstanceReadModel>, PgMetaError> {
1355        self.client
1356            .query_opt(
1357                "
1358                SELECT instance_id, uuid, entity_id, label, note, transformation_id
1359                  FROM public.entity_instances
1360                 WHERE uuid = $1
1361                 LIMIT 1
1362                ",
1363                &[&uuid],
1364            )
1365            .map_err(PgMetaError::Query)?
1366            .as_ref()
1367            .map(entity_instance_from_row)
1368            .transpose()
1369    }
1370
1371    pub fn get_study_entity_instance(
1372        &mut self,
1373        study_id: StudyId,
1374        entity_id: EntityId,
1375        external_id: &str,
1376    ) -> Result<Option<PgStudyEntityInstanceReadModel>, PgMetaError> {
1377        self.client
1378            .query_opt(
1379                "
1380                SELECT study_id, entity_instance_id, entity_id,
1381                       external_id, transformation_id
1382                  FROM public.study_entity_instances
1383                 WHERE study_id = $1 AND entity_id = $2 AND external_id = $3
1384                 LIMIT 1
1385                ",
1386                &[
1387                    &study_id.0,
1388                    &id_i32(entity_id.0, "entity_id")?,
1389                    &external_id,
1390                ],
1391            )
1392            .map_err(PgMetaError::Query)?
1393            .as_ref()
1394            .map(study_entity_instance_from_row)
1395            .transpose()
1396    }
1397
1398    pub fn list_study_entity_instances(
1399        &mut self,
1400        study_id: StudyId,
1401    ) -> Result<Vec<PgStudyEntityInstanceReadModel>, PgMetaError> {
1402        let rows = self
1403            .client
1404            .query(
1405                "
1406                SELECT study_id, entity_instance_id, entity_id,
1407                       external_id, transformation_id
1408                  FROM public.study_entity_instances
1409                 WHERE study_id = $1
1410                 ORDER BY entity_instance_id
1411                ",
1412                &[&study_id.0],
1413            )
1414            .map_err(PgMetaError::Query)?;
1415        rows.iter().map(study_entity_instance_from_row).collect()
1416    }
1417
1418    pub fn list_relation_instances_for_relation(
1419        &mut self,
1420        entity_relation_id: EntityRelationId,
1421    ) -> Result<Vec<PgRelationInstanceReadModel>, PgMetaError> {
1422        let rows = self
1423            .client
1424            .query(
1425                "
1426                SELECT relation_instance_id, uuid, entityrelation_id,
1427                       entity_instance_id_1, entity_instance_id_2,
1428                       valid_from, valid_to, note, transformation_id
1429                  FROM public.relation_instances
1430                 WHERE entityrelation_id = $1
1431                 ORDER BY relation_instance_id
1432                ",
1433                &[&id_i32(entity_relation_id.0, "entityrelation_id")?],
1434            )
1435            .map_err(PgMetaError::Query)?;
1436        rows.iter().map(relation_instance_from_row).collect()
1437    }
1438
1439    pub fn get_relation_instance(
1440        &mut self,
1441        relation_instance_id: i64,
1442    ) -> Result<Option<PgRelationInstanceReadModel>, PgMetaError> {
1443        self.client
1444            .query_opt(
1445                "
1446                SELECT relation_instance_id, uuid, entityrelation_id,
1447                       entity_instance_id_1, entity_instance_id_2,
1448                       valid_from, valid_to, note, transformation_id
1449                  FROM public.relation_instances
1450                 WHERE relation_instance_id = $1
1451                 LIMIT 1
1452                ",
1453                &[&relation_instance_id],
1454            )
1455            .map_err(PgMetaError::Query)?
1456            .as_ref()
1457            .map(relation_instance_from_row)
1458            .transpose()
1459    }
1460
1461    pub fn get_relation_instance_by_uuid(
1462        &mut self,
1463        uuid: Uuid,
1464    ) -> Result<Option<PgRelationInstanceReadModel>, PgMetaError> {
1465        self.client
1466            .query_opt(
1467                "
1468                SELECT relation_instance_id, uuid, entityrelation_id,
1469                       entity_instance_id_1, entity_instance_id_2,
1470                       valid_from, valid_to, note, transformation_id
1471                  FROM public.relation_instances
1472                 WHERE uuid = $1
1473                 LIMIT 1
1474                ",
1475                &[&uuid],
1476            )
1477            .map_err(PgMetaError::Query)?
1478            .as_ref()
1479            .map(relation_instance_from_row)
1480            .transpose()
1481    }
1482
1483    pub fn get_study_relation_instance(
1484        &mut self,
1485        study_id: StudyId,
1486        entity_relation_id: EntityRelationId,
1487        external_id: &str,
1488    ) -> Result<Option<PgStudyRelationInstanceReadModel>, PgMetaError> {
1489        self.client
1490            .query_opt(
1491                "
1492                SELECT study_id, relation_instance_id, entityrelation_id,
1493                       external_id, transformation_id
1494                  FROM public.study_relation_instances
1495                 WHERE study_id = $1 AND entityrelation_id = $2 AND external_id = $3
1496                 LIMIT 1
1497                ",
1498                &[
1499                    &study_id.0,
1500                    &id_i32(entity_relation_id.0, "entityrelation_id")?,
1501                    &external_id,
1502                ],
1503            )
1504            .map_err(PgMetaError::Query)?
1505            .as_ref()
1506            .map(study_relation_instance_from_row)
1507            .transpose()
1508    }
1509
1510    pub fn list_study_relation_instances(
1511        &mut self,
1512        study_id: StudyId,
1513    ) -> Result<Vec<PgStudyRelationInstanceReadModel>, PgMetaError> {
1514        let rows = self
1515            .client
1516            .query(
1517                "
1518                SELECT study_id, relation_instance_id, entityrelation_id,
1519                       external_id, transformation_id
1520                  FROM public.study_relation_instances
1521                 WHERE study_id = $1
1522                 ORDER BY relation_instance_id
1523                ",
1524                &[&study_id.0],
1525            )
1526            .map_err(PgMetaError::Query)?;
1527        rows.iter().map(study_relation_instance_from_row).collect()
1528    }
1529
1530    pub fn list_asset_version_entities(
1531        &mut self,
1532        version_id: VersionId,
1533    ) -> Result<Vec<PgAssetVersionEntityReadModel>, PgMetaError> {
1534        let rows = self
1535            .client
1536            .query(
1537                "
1538                SELECT version_id, entity_instance_id, transformation_id
1539                  FROM public.asset_version_entities
1540                 WHERE version_id = $1
1541                 ORDER BY entity_instance_id
1542                ",
1543                &[&version_id.0],
1544            )
1545            .map_err(PgMetaError::Query)?;
1546        rows.iter().map(asset_version_entity_from_row).collect()
1547    }
1548
1549    pub fn list_asset_version_entities_for_instance(
1550        &mut self,
1551        entity_instance_id: i64,
1552    ) -> Result<Vec<PgAssetVersionEntityReadModel>, PgMetaError> {
1553        let rows = self
1554            .client
1555            .query(
1556                "
1557                SELECT version_id, entity_instance_id, transformation_id
1558                  FROM public.asset_version_entities
1559                 WHERE entity_instance_id = $1
1560                 ORDER BY version_id
1561                ",
1562                &[&entity_instance_id],
1563            )
1564            .map_err(PgMetaError::Query)?;
1565        rows.iter().map(asset_version_entity_from_row).collect()
1566    }
1567
1568    pub fn list_dataset_version_entities(
1569        &mut self,
1570        version_id: VersionId,
1571    ) -> Result<Vec<PgDatasetVersionEntityReadModel>, PgMetaError> {
1572        let rows = self
1573            .client
1574            .query(
1575                "
1576                SELECT version_id, entity_instance_id, entity_variable_id,
1577                       transformation_id
1578                  FROM public.dataset_version_entities
1579                 WHERE version_id = $1
1580                 ORDER BY entity_instance_id
1581                ",
1582                &[&version_id.0],
1583            )
1584            .map_err(PgMetaError::Query)?;
1585        rows.iter().map(dataset_version_entity_from_row).collect()
1586    }
1587
1588    pub fn list_dataset_version_entities_for_instance(
1589        &mut self,
1590        entity_instance_id: i64,
1591    ) -> Result<Vec<PgDatasetVersionEntityReadModel>, PgMetaError> {
1592        let rows = self
1593            .client
1594            .query(
1595                "
1596                SELECT version_id, entity_instance_id, entity_variable_id,
1597                       transformation_id
1598                  FROM public.dataset_version_entities
1599                 WHERE entity_instance_id = $1
1600                 ORDER BY version_id, entity_variable_id
1601                ",
1602                &[&entity_instance_id],
1603            )
1604            .map_err(PgMetaError::Query)?;
1605        rows.iter().map(dataset_version_entity_from_row).collect()
1606    }
1607
1608    pub fn list_asset_version_relation_instances(
1609        &mut self,
1610        version_id: VersionId,
1611    ) -> Result<Vec<PgAssetVersionRelationInstanceReadModel>, PgMetaError> {
1612        let rows = self
1613            .client
1614            .query(
1615                "
1616                SELECT version_id, relation_instance_id, transformation_id
1617                  FROM public.asset_version_relation_instances
1618                 WHERE version_id = $1
1619                 ORDER BY relation_instance_id
1620                ",
1621                &[&version_id.0],
1622            )
1623            .map_err(PgMetaError::Query)?;
1624        rows.iter()
1625            .map(asset_version_relation_instance_from_row)
1626            .collect()
1627    }
1628
1629    pub fn list_asset_version_relation_instances_for_instance(
1630        &mut self,
1631        relation_instance_id: i64,
1632    ) -> Result<Vec<PgAssetVersionRelationInstanceReadModel>, PgMetaError> {
1633        let rows = self
1634            .client
1635            .query(
1636                "
1637                SELECT version_id, relation_instance_id, transformation_id
1638                  FROM public.asset_version_relation_instances
1639                 WHERE relation_instance_id = $1
1640                 ORDER BY version_id
1641                ",
1642                &[&relation_instance_id],
1643            )
1644            .map_err(PgMetaError::Query)?;
1645        rows.iter()
1646            .map(asset_version_relation_instance_from_row)
1647            .collect()
1648    }
1649
1650    pub fn list_dataset_version_relation_instances(
1651        &mut self,
1652        version_id: VersionId,
1653    ) -> Result<Vec<PgDatasetVersionRelationInstanceReadModel>, PgMetaError> {
1654        let rows = self
1655            .client
1656            .query(
1657                "
1658                SELECT version_id, relation_instance_id, subject_variable_id,
1659                       object_variable_id, relation_variable_id, transformation_id
1660                  FROM public.dataset_version_relation_instances
1661                 WHERE version_id = $1
1662                 ORDER BY relation_instance_id
1663                ",
1664                &[&version_id.0],
1665            )
1666            .map_err(PgMetaError::Query)?;
1667        rows.iter()
1668            .map(dataset_version_relation_instance_from_row)
1669            .collect()
1670    }
1671
1672    pub fn list_dataset_version_relation_instances_for_instance(
1673        &mut self,
1674        relation_instance_id: i64,
1675    ) -> Result<Vec<PgDatasetVersionRelationInstanceReadModel>, PgMetaError> {
1676        let rows = self
1677            .client
1678            .query(
1679                "
1680                SELECT version_id, relation_instance_id, subject_variable_id,
1681                       object_variable_id, relation_variable_id, transformation_id
1682                  FROM public.dataset_version_relation_instances
1683                 WHERE relation_instance_id = $1
1684                 ORDER BY version_id
1685                ",
1686                &[&relation_instance_id],
1687            )
1688            .map_err(PgMetaError::Query)?;
1689        rows.iter()
1690            .map(dataset_version_relation_instance_from_row)
1691            .collect()
1692    }
1693
1694    pub fn list_transformations(&mut self) -> Result<Vec<PgTransformationReadModel>, PgMetaError> {
1695        let rows = self
1696            .client
1697            .query(
1698                "
1699                SELECT transformation_id, transformation_type::text AS transformation_type,
1700                       description, repository_url, commit_hash, file_path,
1701                       date_created, created_by
1702                  FROM public.transformations
1703                 ORDER BY transformation_id
1704                ",
1705                &[],
1706            )
1707            .map_err(PgMetaError::Query)?;
1708        rows.iter().map(transformation_from_row).collect()
1709    }
1710
1711    pub fn get_transformation(
1712        &mut self,
1713        transformation_id: TransformationId,
1714    ) -> Result<Option<PgTransformationReadModel>, PgMetaError> {
1715        self.client
1716            .query_opt(
1717                "
1718                SELECT transformation_id, transformation_type::text AS transformation_type,
1719                       description, repository_url, commit_hash, file_path,
1720                       date_created, created_by
1721                  FROM public.transformations
1722                 WHERE transformation_id = $1
1723                ",
1724                &[&id_i32(transformation_id.0, "transformation_id")?],
1725            )
1726            .map_err(PgMetaError::Query)?
1727            .as_ref()
1728            .map(transformation_from_row)
1729            .transpose()
1730    }
1731
1732    pub fn list_transformation_inputs(
1733        &mut self,
1734        transformation_id: TransformationId,
1735    ) -> Result<Vec<PgTransformationInputReadModel>, PgMetaError> {
1736        let rows = self
1737            .client
1738            .query(
1739                "
1740                SELECT transformation_id, version_id
1741                  FROM public.transformation_inputs
1742                 WHERE transformation_id = $1
1743                 ORDER BY version_id
1744                ",
1745                &[&id_i32(transformation_id.0, "transformation_id")?],
1746            )
1747            .map_err(PgMetaError::Query)?;
1748        rows.iter().map(transformation_input_from_row).collect()
1749    }
1750
1751    pub fn list_transformation_inputs_for_version(
1752        &mut self,
1753        version_id: VersionId,
1754    ) -> Result<Vec<PgTransformationInputReadModel>, PgMetaError> {
1755        let rows = self
1756            .client
1757            .query(
1758                "
1759                SELECT transformation_id, version_id
1760                  FROM public.transformation_inputs
1761                 WHERE version_id = $1
1762                 ORDER BY transformation_id
1763                ",
1764                &[&version_id.0],
1765            )
1766            .map_err(PgMetaError::Query)?;
1767        rows.iter().map(transformation_input_from_row).collect()
1768    }
1769
1770    pub fn list_transformation_outputs(
1771        &mut self,
1772        transformation_id: TransformationId,
1773    ) -> Result<Vec<PgTransformationOutputReadModel>, PgMetaError> {
1774        let rows = self
1775            .client
1776            .query(
1777                "
1778                SELECT transformation_id, version_id
1779                  FROM public.transformation_outputs
1780                 WHERE transformation_id = $1
1781                 ORDER BY version_id
1782                ",
1783                &[&id_i32(transformation_id.0, "transformation_id")?],
1784            )
1785            .map_err(PgMetaError::Query)?;
1786        rows.iter().map(transformation_output_from_row).collect()
1787    }
1788
1789    pub fn list_transformation_outputs_for_version(
1790        &mut self,
1791        version_id: VersionId,
1792    ) -> Result<Vec<PgTransformationOutputReadModel>, PgMetaError> {
1793        let rows = self
1794            .client
1795            .query(
1796                "
1797                SELECT transformation_id, version_id
1798                  FROM public.transformation_outputs
1799                 WHERE version_id = $1
1800                 ORDER BY transformation_id
1801                ",
1802                &[&version_id.0],
1803            )
1804            .map_err(PgMetaError::Query)?;
1805        rows.iter().map(transformation_output_from_row).collect()
1806    }
1807
1808    pub fn list_transformations_producing_version(
1809        &mut self,
1810        version_id: VersionId,
1811    ) -> Result<Vec<PgTransformationReadModel>, PgMetaError> {
1812        let rows = self
1813            .client
1814            .query(
1815                "
1816                SELECT t.transformation_id,
1817                       t.transformation_type::text AS transformation_type,
1818                       t.description, t.repository_url, t.commit_hash, t.file_path,
1819                       t.date_created, t.created_by
1820                  FROM public.transformations t
1821                  JOIN public.transformation_outputs o
1822                    ON o.transformation_id = t.transformation_id
1823                 WHERE o.version_id = $1
1824                 ORDER BY t.transformation_id
1825                ",
1826                &[&version_id.0],
1827            )
1828            .map_err(PgMetaError::Query)?;
1829        rows.iter().map(transformation_from_row).collect()
1830    }
1831
1832    pub fn list_tags_for_target(
1833        &mut self,
1834        target: PgTagTarget,
1835    ) -> Result<Vec<String>, PgMetaError> {
1836        let (mapping_table, key_column, key_value) = target.mapping();
1837        let sql = format!(
1838            "
1839            SELECT t.name
1840              FROM public.tags t
1841              JOIN public.{mapping_table} m ON m.tag_id = t.tag_id
1842             WHERE m.{key_column} = $1
1843             ORDER BY t.name
1844            "
1845        );
1846        let rows = match key_value {
1847            PgTagKey::Uuid(value) => self.client.query(&sql, &[&value]),
1848            PgTagKey::Int(value) => self.client.query(&sql, &[&id_i32(value, "tag target id")?]),
1849        }
1850        .map_err(PgMetaError::Query)?;
1851        Ok(rows.iter().map(|row| row.get("name")).collect())
1852    }
1853
1854    pub fn list_tag_records_for_target(
1855        &mut self,
1856        target: PgTagTarget,
1857    ) -> Result<Vec<PgTagReadModel>, PgMetaError> {
1858        let (mapping_table, key_column, key_value) = target.mapping();
1859        let sql = format!(
1860            "
1861            SELECT t.tag_id, t.name
1862              FROM public.tags t
1863              JOIN public.{mapping_table} m ON m.tag_id = t.tag_id
1864             WHERE m.{key_column} = $1
1865             ORDER BY t.name
1866            "
1867        );
1868        let rows = match key_value {
1869            PgTagKey::Uuid(value) => self.client.query(&sql, &[&value]),
1870            PgTagKey::Int(value) => self.client.query(&sql, &[&id_i32(value, "tag target id")?]),
1871        }
1872        .map_err(PgMetaError::Query)?;
1873        rows.iter().map(tag_from_row).collect()
1874    }
1875
1876    pub fn list_study_tag_links(
1877        &mut self,
1878        study_id: StudyId,
1879    ) -> Result<Vec<PgStudyTagReadModel>, PgMetaError> {
1880        let rows = self
1881            .client
1882            .query(
1883                "
1884                SELECT study_id, tag_id
1885                  FROM public.study_tags
1886                 WHERE study_id = $1
1887                 ORDER BY tag_id
1888                ",
1889                &[&study_id.0],
1890            )
1891            .map_err(PgMetaError::Query)?;
1892        rows.iter().map(study_tag_from_row).collect()
1893    }
1894
1895    pub fn list_domain_tag_links(
1896        &mut self,
1897        domain_id: DomainId,
1898    ) -> Result<Vec<PgDomainTagReadModel>, PgMetaError> {
1899        let rows = self
1900            .client
1901            .query(
1902                "
1903                SELECT domain_id, tag_id
1904                  FROM public.domain_tags
1905                 WHERE domain_id = $1
1906                 ORDER BY tag_id
1907                ",
1908                &[&id_i32(domain_id.0, "domain_id")?],
1909            )
1910            .map_err(PgMetaError::Query)?;
1911        rows.iter().map(domain_tag_from_row).collect()
1912    }
1913
1914    pub fn list_variable_tag_links(
1915        &mut self,
1916        variable_id: VariableId,
1917    ) -> Result<Vec<PgVariableTagReadModel>, PgMetaError> {
1918        let rows = self
1919            .client
1920            .query(
1921                "
1922                SELECT variable_id, tag_id
1923                  FROM public.variable_tags
1924                 WHERE variable_id = $1
1925                 ORDER BY tag_id
1926                ",
1927                &[&id_i32(variable_id.0, "variable_id")?],
1928            )
1929            .map_err(PgMetaError::Query)?;
1930        rows.iter().map(variable_tag_from_row).collect()
1931    }
1932
1933    pub fn list_asset_tag_links(
1934        &mut self,
1935        asset_id: AssetId,
1936    ) -> Result<Vec<PgAssetTagReadModel>, PgMetaError> {
1937        let rows = self
1938            .client
1939            .query(
1940                "
1941                SELECT asset_id, tag_id
1942                  FROM public.asset_tags
1943                 WHERE asset_id = $1
1944                 ORDER BY tag_id
1945                ",
1946                &[&asset_id.0],
1947            )
1948            .map_err(PgMetaError::Query)?;
1949        rows.iter().map(asset_tag_from_row).collect()
1950    }
1951
1952    pub fn list_asset_version_tag_links(
1953        &mut self,
1954        version_id: VersionId,
1955    ) -> Result<Vec<PgAssetVersionTagReadModel>, PgMetaError> {
1956        let rows = self
1957            .client
1958            .query(
1959                "
1960                SELECT version_id, tag_id
1961                  FROM public.asset_version_tags
1962                 WHERE version_id = $1
1963                 ORDER BY tag_id
1964                ",
1965                &[&version_id.0],
1966            )
1967            .map_err(PgMetaError::Query)?;
1968        rows.iter().map(asset_version_tag_from_row).collect()
1969    }
1970
1971    pub fn list_entity_tag_links(
1972        &mut self,
1973        entity_id: EntityId,
1974    ) -> Result<Vec<PgEntityTagReadModel>, PgMetaError> {
1975        let rows = self
1976            .client
1977            .query(
1978                "
1979                SELECT entity_id, tag_id
1980                  FROM public.entity_tags
1981                 WHERE entity_id = $1
1982                 ORDER BY tag_id
1983                ",
1984                &[&id_i32(entity_id.0, "entity_id")?],
1985            )
1986            .map_err(PgMetaError::Query)?;
1987        rows.iter().map(entity_tag_from_row).collect()
1988    }
1989
1990    pub fn list_entity_relation_tag_links(
1991        &mut self,
1992        entity_relation_id: EntityRelationId,
1993    ) -> Result<Vec<PgEntityRelationTagReadModel>, PgMetaError> {
1994        let rows = self
1995            .client
1996            .query(
1997                "
1998                SELECT entityrelation_id, tag_id
1999                  FROM public.entityrelation_tags
2000                 WHERE entityrelation_id = $1
2001                 ORDER BY tag_id
2002                ",
2003                &[&id_i32(entity_relation_id.0, "entityrelation_id")?],
2004            )
2005            .map_err(PgMetaError::Query)?;
2006        rows.iter().map(entity_relation_tag_from_row).collect()
2007    }
2008
2009    pub fn list_tags(&mut self) -> Result<Vec<PgTagReadModel>, PgMetaError> {
2010        let rows = self
2011            .client
2012            .query(
2013                "
2014                SELECT tag_id, name
2015                  FROM public.tags
2016                 ORDER BY name
2017                ",
2018                &[],
2019            )
2020            .map_err(PgMetaError::Query)?;
2021        rows.iter().map(tag_from_row).collect()
2022    }
2023}
2024
2025/// Target that can have tags attached through the read-model API.
2026#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2027pub enum PgTagTarget {
2028    Study(StudyId),
2029    Domain(DomainId),
2030    Variable(VariableId),
2031    Asset(AssetId),
2032    AssetVersion(VersionId),
2033    Entity(EntityId),
2034    EntityRelation(EntityRelationId),
2035}
2036
2037impl PgTagTarget {
2038    fn mapping(self) -> (&'static str, &'static str, PgTagKey) {
2039        match self {
2040            Self::Study(id) => ("study_tags", "study_id", PgTagKey::Uuid(id.0)),
2041            Self::Domain(id) => ("domain_tags", "domain_id", PgTagKey::Int(id.0)),
2042            Self::Variable(id) => ("variable_tags", "variable_id", PgTagKey::Int(id.0)),
2043            Self::Asset(id) => ("asset_tags", "asset_id", PgTagKey::Uuid(id.0)),
2044            Self::AssetVersion(id) => ("asset_version_tags", "version_id", PgTagKey::Uuid(id.0)),
2045            Self::Entity(id) => ("entity_tags", "entity_id", PgTagKey::Int(id.0)),
2046            Self::EntityRelation(id) => (
2047                "entityrelation_tags",
2048                "entityrelation_id",
2049                PgTagKey::Int(id.0),
2050            ),
2051        }
2052    }
2053}
2054
2055#[derive(Debug, Clone, Copy)]
2056enum PgTagKey {
2057    Uuid(Uuid),
2058    Int(i64),
2059}
2060
2061fn study_from_row(row: &Row) -> Result<PgStudyReadModel, PgMetaError> {
2062    Ok(PgStudyReadModel {
2063        study_id: StudyId(row.get("study_id")),
2064        name: row.get("name"),
2065        description: row.get("description"),
2066        documentation: row.get("documentation"),
2067        agent_instructions: row.get("agent_instructions"),
2068        external_id: row.get("external_id"),
2069        study_type_id: optional_int_id(row, "study_type_id", StudyTypeId),
2070        date_created: row.get::<_, Option<NaiveDateTime>>("date_created").map(utc),
2071        created_by: row.get("created_by"),
2072    })
2073}
2074
2075fn study_type_from_row(row: &Row) -> Result<PgStudyTypeReadModel, PgMetaError> {
2076    Ok(PgStudyTypeReadModel {
2077        study_type_id: StudyTypeId(i64::from(row.get::<_, i32>("study_type_id"))),
2078        name: row.get("name"),
2079        namespace: row.get("namespace"),
2080        ontology_class: row.get("ontology_class"),
2081        description: row.get("description"),
2082    })
2083}
2084
2085fn domain_from_row(row: &Row) -> Result<PgDomainReadModel, PgMetaError> {
2086    Ok(PgDomainReadModel {
2087        domain_id: DomainId(i64::from(row.get::<_, i32>("domain_id"))),
2088        name: row.get("name"),
2089        uri: row.get("uri"),
2090        description: row.get("description"),
2091    })
2092}
2093
2094fn study_domain_from_row(row: &Row) -> Result<PgStudyDomainReadModel, PgMetaError> {
2095    Ok(PgStudyDomainReadModel {
2096        study_id: StudyId(row.get("study_id")),
2097        domain_id: DomainId(i64::from(row.get::<_, i32>("domain_id"))),
2098    })
2099}
2100
2101fn duo_code_from_row(row: &Row) -> Result<PgDuoCodeReadModel, PgMetaError> {
2102    Ok(PgDuoCodeReadModel {
2103        duo_id: row.get("duo_id"),
2104        duo_code: row.get("duo_code"),
2105        label: row.get("label"),
2106        definition: row.get("definition"),
2107        category: row.get("category"),
2108        qualifier_type: row.get("qualifier_type"),
2109        qualifier_required: row.get("qualifier_required"),
2110        qualifier_note: row.get("qualifier_note"),
2111    })
2112}
2113
2114fn study_access_from_row(row: &Row) -> Result<PgStudyAccessReadModel, PgMetaError> {
2115    Ok(PgStudyAccessReadModel {
2116        study_id: StudyId(row.get("study_id")),
2117        user_id: row.get("user_id"),
2118        date_granted: row.get::<_, Option<NaiveDateTime>>("date_granted").map(utc),
2119        granted_by: row.get("granted_by"),
2120    })
2121}
2122
2123fn study_custodian_from_row(row: &Row) -> Result<PgStudyCustodianReadModel, PgMetaError> {
2124    Ok(PgStudyCustodianReadModel {
2125        study_id: StudyId(row.get("study_id")),
2126        user_id: row.get("user_id"),
2127        is_primary: row.get("is_primary"),
2128        date_granted: row.get::<_, Option<NaiveDateTime>>("date_granted").map(utc),
2129        granted_by: row.get("granted_by"),
2130    })
2131}
2132
2133fn study_duo_from_row(row: &Row) -> Result<PgDuoRestrictionReadModel, PgMetaError> {
2134    Ok(PgDuoRestrictionReadModel {
2135        restriction_id: i64::from(row.get::<_, i32>("study_duo_restriction_id")),
2136        study_id: StudyId(row.get("study_id")),
2137        duo_id: row.get("duo_id"),
2138        duo_code: row.get("duo_code"),
2139        label: row.get("label"),
2140        qualifier_ontology_namespace: row.get("qualifier_ontology_namespace"),
2141        qualifier_ontology_id: row.get("qualifier_ontology_id"),
2142        qualifier_text: row.get("qualifier_text"),
2143        qualifier_date: row.get("qualifier_date"),
2144        qualifier_integer: row.get("qualifier_integer"),
2145    })
2146}
2147
2148fn asset_from_row(row: &Row) -> Result<PgAssetReadModel, PgMetaError> {
2149    Ok(PgAssetReadModel {
2150        asset_id: AssetId(row.get("asset_id")),
2151        study_id: StudyId(row.get("study_id")),
2152        name: row.get("name"),
2153        description: row.get("description"),
2154        agent_instructions: row.get("agent_instructions"),
2155        asset_type: parse_asset_type(row.get("asset_type"))?,
2156        date_created: row.get::<_, Option<NaiveDateTime>>("date_created").map(utc),
2157        created_by: row.get("created_by"),
2158    })
2159}
2160
2161fn asset_version_from_row(row: &Row) -> Result<PgAssetVersionReadModel, PgMetaError> {
2162    Ok(PgAssetVersionReadModel {
2163        version_id: VersionId(row.get("version_id")),
2164        asset_id: AssetId(row.get("asset_id")),
2165        major: row.get("major"),
2166        minor: row.get("minor"),
2167        patch: row.get("patch"),
2168        version_label: row.get("version_label"),
2169        version_note: row.get("version_note"),
2170        is_latest: row.get("is_latest"),
2171        doi: row.get("doi"),
2172        created_at: row.get::<_, Option<NaiveDateTime>>("created_at").map(utc),
2173        created_by: row.get("created_by"),
2174    })
2175}
2176
2177fn asset_version_duo_from_row(
2178    row: &Row,
2179) -> Result<PgAssetVersionDuoRestrictionReadModel, PgMetaError> {
2180    Ok(PgAssetVersionDuoRestrictionReadModel {
2181        restriction_id: i64::from(row.get::<_, i32>("asset_version_duo_restriction_id")),
2182        version_id: VersionId(row.get("version_id")),
2183        duo_id: row.get("duo_id"),
2184        duo_code: row.get("duo_code"),
2185        label: row.get("label"),
2186        qualifier_ontology_namespace: row.get("qualifier_ontology_namespace"),
2187        qualifier_ontology_id: row.get("qualifier_ontology_id"),
2188        qualifier_text: row.get("qualifier_text"),
2189        qualifier_date: row.get("qualifier_date"),
2190        qualifier_integer: row.get("qualifier_integer"),
2191    })
2192}
2193
2194fn dataset_from_row(row: &Row) -> Result<PgDatasetReadModel, PgMetaError> {
2195    Ok(PgDatasetReadModel {
2196        dataset_id: VersionId(row.get("dataset_id")),
2197    })
2198}
2199
2200fn datafile_from_row(row: &Row) -> Result<PgDataFileReadModel, PgMetaError> {
2201    Ok(PgDataFileReadModel {
2202        datafile_id: VersionId(row.get("datafile_id")),
2203        size_bytes: row.get("size_bytes"),
2204        compressed: row.get("compressed"),
2205        encrypted: row.get("encrypted"),
2206        compression_algorithm: row.get("compression_algorithm"),
2207        encryption_algorithm: row.get("encryption_algorithm"),
2208        encryption_key: row.get("encryption_key"),
2209        storage_uri: row.get("storage_uri"),
2210        edam_format: row.get("edam_format"),
2211        digest: row.get("digest"),
2212    })
2213}
2214
2215fn value_type_from_row(row: &Row) -> Result<PgValueTypeReadModel, PgMetaError> {
2216    Ok(PgValueTypeReadModel {
2217        value_type_id: ValueTypeId(i64::from(row.get::<_, i32>("value_type_id"))),
2218        value_type: row.get("value_type"),
2219        description: row.get("description"),
2220    })
2221}
2222
2223fn vocabulary_from_row(row: &Row) -> Result<PgVocabularyReadModel, PgMetaError> {
2224    Ok(PgVocabularyReadModel {
2225        vocabulary_id: VocabularyId(i64::from(row.get::<_, i32>("vocabulary_id"))),
2226        domain_id: DomainId(i64::from(row.get::<_, i32>("domain_id"))),
2227        name: row.get("name"),
2228        description: row.get("description"),
2229    })
2230}
2231
2232fn vocabulary_mapping_from_row(row: &Row) -> Result<PgVocabularyMappingReadModel, PgMetaError> {
2233    Ok(PgVocabularyMappingReadModel {
2234        vocabulary_mapping_id: i64::from(row.get::<_, i32>("vocabulary_mapping_id")),
2235        from_vocabulary_item: VocabularyItemId(i64::from(
2236            row.get::<_, i32>("from_vocabulary_item"),
2237        )),
2238        to_vocabulary_item: VocabularyItemId(i64::from(row.get::<_, i32>("to_vocabulary_item"))),
2239    })
2240}
2241
2242fn vocabulary_item_from_row(row: &Row) -> Result<PgVocabularyItemReadModel, PgMetaError> {
2243    Ok(PgVocabularyItemReadModel {
2244        vocabulary_item_id: VocabularyItemId(i64::from(row.get::<_, i32>("vocabulary_item_id"))),
2245        vocabulary_id: VocabularyId(i64::from(row.get::<_, i32>("vocabulary_id"))),
2246        value: row.get("value"),
2247        code: row.get("code"),
2248        description: row.get("description"),
2249    })
2250}
2251
2252fn variable_from_row(row: &Row) -> Result<PgVariableReadModel, PgMetaError> {
2253    Ok(PgVariableReadModel {
2254        variable_id: VariableId(i64::from(row.get::<_, i32>("variable_id"))),
2255        domain_id: DomainId(i64::from(row.get::<_, i32>("domain_id"))),
2256        name: row.get("name"),
2257        value_type_id: ValueTypeId(i64::from(row.get::<_, i32>("value_type_id"))),
2258        value_type: row.get("value_type"),
2259        value_format: row.get("value_format"),
2260        vocabulary_id: optional_int_id(row, "vocabulary_id", VocabularyId),
2261        key_role: parse_key_role(row.get("keyrole"))?,
2262        description: row.get("description"),
2263        note: row.get("note"),
2264        ontology_namespace: row.get("ontology_namespace"),
2265        ontology_class: row.get("ontology_class"),
2266    })
2267}
2268
2269fn variable_mapping_from_row(
2270    row: &Row,
2271) -> Result<provisional::PgVariableMappingReadModel, PgMetaError> {
2272    Ok(provisional::PgVariableMappingReadModel {
2273        mapping_id: i64::from(row.get::<_, i32>("mapping_id")),
2274        from_variable_id: VariableId(i64::from(row.get::<_, i32>("from_variable_id"))),
2275        to_variable_id: VariableId(i64::from(row.get::<_, i32>("to_variable_id"))),
2276        operator: row.get("operator"),
2277        operants: row.get("operants"),
2278        prerequisite_id: optional_int_id(row, "prerequisite_id", VariableId),
2279    })
2280}
2281
2282fn dataset_variable_from_row(row: &Row) -> Result<PgDatasetVariableReadModel, PgMetaError> {
2283    Ok(PgDatasetVariableReadModel {
2284        dataset_id: VersionId(row.get("dataset_id")),
2285        variable_id: VariableId(i64::from(row.get::<_, i32>("variable_id"))),
2286        row_role: parse_key_role(row.get("row_role"))?,
2287    })
2288}
2289
2290fn entity_from_row(row: &Row) -> Result<PgEntityReadModel, PgMetaError> {
2291    Ok(PgEntityReadModel {
2292        entity_id: EntityId(i64::from(row.get::<_, i32>("entity_id"))),
2293        domain_id: DomainId(i64::from(row.get::<_, i32>("domain_id"))),
2294        uuid: row.get("uuid"),
2295        name: row.get("name"),
2296        description: row.get("description"),
2297        agent_instructions: row.get("agent_instructions"),
2298        ontology_namespace: row.get("ontology_namespace"),
2299        ontology_class: row.get("ontology_class"),
2300    })
2301}
2302
2303fn entity_instance_from_row(row: &Row) -> Result<PgEntityInstanceReadModel, PgMetaError> {
2304    Ok(PgEntityInstanceReadModel {
2305        instance_id: row.get("instance_id"),
2306        uuid: row.get("uuid"),
2307        entity_id: EntityId(i64::from(row.get::<_, i32>("entity_id"))),
2308        label: row.get("label"),
2309        note: row.get("note"),
2310        transformation_id: TransformationId(i64::from(row.get::<_, i32>("transformation_id"))),
2311    })
2312}
2313
2314fn study_entity_instance_from_row(
2315    row: &Row,
2316) -> Result<PgStudyEntityInstanceReadModel, PgMetaError> {
2317    Ok(PgStudyEntityInstanceReadModel {
2318        study_id: StudyId(row.get("study_id")),
2319        entity_instance_id: row.get("entity_instance_id"),
2320        entity_id: EntityId(i64::from(row.get::<_, i32>("entity_id"))),
2321        external_id: row.get("external_id"),
2322        transformation_id: TransformationId(i64::from(row.get::<_, i32>("transformation_id"))),
2323    })
2324}
2325
2326fn entity_relation_from_row(row: &Row) -> Result<PgEntityRelationReadModel, PgMetaError> {
2327    Ok(PgEntityRelationReadModel {
2328        entity_relation_id: EntityRelationId(i64::from(row.get::<_, i32>("entityrelation_id"))),
2329        subject_entity_id: EntityId(i64::from(row.get::<_, i32>("subject_entity_id"))),
2330        object_entity_id: EntityId(i64::from(row.get::<_, i32>("object_entity_id"))),
2331        domain_id: DomainId(i64::from(row.get::<_, i32>("domain_id"))),
2332        uuid: row.get("uuid"),
2333        name: row.get("name"),
2334        description: row.get("description"),
2335        agent_instructions: row.get("agent_instructions"),
2336        ontology_namespace: row.get("ontology_namespace"),
2337        ontology_class: row.get("ontology_class"),
2338    })
2339}
2340
2341fn relation_instance_from_row(row: &Row) -> Result<PgRelationInstanceReadModel, PgMetaError> {
2342    Ok(PgRelationInstanceReadModel {
2343        relation_instance_id: row.get("relation_instance_id"),
2344        uuid: row.get("uuid"),
2345        entity_relation_id: EntityRelationId(i64::from(row.get::<_, i32>("entityrelation_id"))),
2346        entity_instance_id_1: row.get("entity_instance_id_1"),
2347        entity_instance_id_2: row.get("entity_instance_id_2"),
2348        valid_from: row.get("valid_from"),
2349        valid_to: row.get("valid_to"),
2350        note: row.get("note"),
2351        transformation_id: TransformationId(i64::from(row.get::<_, i32>("transformation_id"))),
2352    })
2353}
2354
2355fn study_relation_instance_from_row(
2356    row: &Row,
2357) -> Result<PgStudyRelationInstanceReadModel, PgMetaError> {
2358    Ok(PgStudyRelationInstanceReadModel {
2359        study_id: StudyId(row.get("study_id")),
2360        relation_instance_id: row.get("relation_instance_id"),
2361        entity_relation_id: EntityRelationId(i64::from(row.get::<_, i32>("entityrelation_id"))),
2362        external_id: row.get("external_id"),
2363        transformation_id: TransformationId(i64::from(row.get::<_, i32>("transformation_id"))),
2364    })
2365}
2366
2367fn asset_version_entity_from_row(row: &Row) -> Result<PgAssetVersionEntityReadModel, PgMetaError> {
2368    Ok(PgAssetVersionEntityReadModel {
2369        version_id: VersionId(row.get("version_id")),
2370        entity_instance_id: row.get("entity_instance_id"),
2371        transformation_id: TransformationId(i64::from(row.get::<_, i32>("transformation_id"))),
2372    })
2373}
2374
2375fn dataset_version_entity_from_row(
2376    row: &Row,
2377) -> Result<PgDatasetVersionEntityReadModel, PgMetaError> {
2378    Ok(PgDatasetVersionEntityReadModel {
2379        version_id: VersionId(row.get("version_id")),
2380        entity_instance_id: row.get("entity_instance_id"),
2381        entity_variable_id: VariableId(i64::from(row.get::<_, i32>("entity_variable_id"))),
2382        transformation_id: TransformationId(i64::from(row.get::<_, i32>("transformation_id"))),
2383    })
2384}
2385
2386fn asset_version_relation_instance_from_row(
2387    row: &Row,
2388) -> Result<PgAssetVersionRelationInstanceReadModel, PgMetaError> {
2389    Ok(PgAssetVersionRelationInstanceReadModel {
2390        version_id: VersionId(row.get("version_id")),
2391        relation_instance_id: row.get("relation_instance_id"),
2392        transformation_id: TransformationId(i64::from(row.get::<_, i32>("transformation_id"))),
2393    })
2394}
2395
2396fn dataset_version_relation_instance_from_row(
2397    row: &Row,
2398) -> Result<PgDatasetVersionRelationInstanceReadModel, PgMetaError> {
2399    Ok(PgDatasetVersionRelationInstanceReadModel {
2400        version_id: VersionId(row.get("version_id")),
2401        relation_instance_id: row.get("relation_instance_id"),
2402        subject_variable_id: VariableId(i64::from(row.get::<_, i32>("subject_variable_id"))),
2403        object_variable_id: VariableId(i64::from(row.get::<_, i32>("object_variable_id"))),
2404        relation_variable_id: optional_int_id(row, "relation_variable_id", VariableId),
2405        transformation_id: TransformationId(i64::from(row.get::<_, i32>("transformation_id"))),
2406    })
2407}
2408
2409fn transformation_from_row(row: &Row) -> Result<PgTransformationReadModel, PgMetaError> {
2410    Ok(PgTransformationReadModel {
2411        transformation_id: TransformationId(i64::from(row.get::<_, i32>("transformation_id"))),
2412        transformation_type: parse_transformation_type(row.get("transformation_type"))?,
2413        description: row.get("description"),
2414        repository_url: row.get("repository_url"),
2415        commit_hash: row.get("commit_hash"),
2416        file_path: row.get("file_path"),
2417        date_created: row.get::<_, Option<NaiveDateTime>>("date_created").map(utc),
2418        created_by: row.get("created_by"),
2419    })
2420}
2421
2422fn tag_from_row(row: &Row) -> Result<PgTagReadModel, PgMetaError> {
2423    Ok(PgTagReadModel {
2424        tag_id: i64::from(row.get::<_, i32>("tag_id")),
2425        name: row.get("name"),
2426    })
2427}
2428
2429fn study_tag_from_row(row: &Row) -> Result<PgStudyTagReadModel, PgMetaError> {
2430    Ok(PgStudyTagReadModel {
2431        study_id: StudyId(row.get("study_id")),
2432        tag_id: i64::from(row.get::<_, i32>("tag_id")),
2433    })
2434}
2435
2436fn domain_tag_from_row(row: &Row) -> Result<PgDomainTagReadModel, PgMetaError> {
2437    Ok(PgDomainTagReadModel {
2438        domain_id: DomainId(i64::from(row.get::<_, i32>("domain_id"))),
2439        tag_id: i64::from(row.get::<_, i32>("tag_id")),
2440    })
2441}
2442
2443fn variable_tag_from_row(row: &Row) -> Result<PgVariableTagReadModel, PgMetaError> {
2444    Ok(PgVariableTagReadModel {
2445        variable_id: VariableId(i64::from(row.get::<_, i32>("variable_id"))),
2446        tag_id: i64::from(row.get::<_, i32>("tag_id")),
2447    })
2448}
2449
2450fn asset_tag_from_row(row: &Row) -> Result<PgAssetTagReadModel, PgMetaError> {
2451    Ok(PgAssetTagReadModel {
2452        asset_id: AssetId(row.get("asset_id")),
2453        tag_id: i64::from(row.get::<_, i32>("tag_id")),
2454    })
2455}
2456
2457fn asset_version_tag_from_row(row: &Row) -> Result<PgAssetVersionTagReadModel, PgMetaError> {
2458    Ok(PgAssetVersionTagReadModel {
2459        version_id: VersionId(row.get("version_id")),
2460        tag_id: i64::from(row.get::<_, i32>("tag_id")),
2461    })
2462}
2463
2464fn entity_tag_from_row(row: &Row) -> Result<PgEntityTagReadModel, PgMetaError> {
2465    Ok(PgEntityTagReadModel {
2466        entity_id: EntityId(i64::from(row.get::<_, i32>("entity_id"))),
2467        tag_id: i64::from(row.get::<_, i32>("tag_id")),
2468    })
2469}
2470
2471fn entity_relation_tag_from_row(row: &Row) -> Result<PgEntityRelationTagReadModel, PgMetaError> {
2472    Ok(PgEntityRelationTagReadModel {
2473        entity_relation_id: EntityRelationId(i64::from(row.get::<_, i32>("entityrelation_id"))),
2474        tag_id: i64::from(row.get::<_, i32>("tag_id")),
2475    })
2476}
2477
2478fn transformation_input_from_row(row: &Row) -> Result<PgTransformationInputReadModel, PgMetaError> {
2479    Ok(PgTransformationInputReadModel {
2480        transformation_id: TransformationId(i64::from(row.get::<_, i32>("transformation_id"))),
2481        version_id: VersionId(row.get("version_id")),
2482    })
2483}
2484
2485fn transformation_output_from_row(
2486    row: &Row,
2487) -> Result<PgTransformationOutputReadModel, PgMetaError> {
2488    Ok(PgTransformationOutputReadModel {
2489        transformation_id: TransformationId(i64::from(row.get::<_, i32>("transformation_id"))),
2490        version_id: VersionId(row.get("version_id")),
2491    })
2492}
2493
2494fn parse_asset_type(value: String) -> Result<AssetType, PgMetaError> {
2495    match value.as_str() {
2496        "dataset" => Ok(AssetType::Dataset),
2497        "file" => Ok(AssetType::File),
2498        _ => decode_error("asset_type", value, "expected dataset or file"),
2499    }
2500}
2501
2502fn parse_key_role(value: String) -> Result<KeyRole, PgMetaError> {
2503    match value.as_str() {
2504        "none" => Ok(KeyRole::None),
2505        "record" => Ok(KeyRole::Record),
2506        "external" => Ok(KeyRole::External),
2507        _ => decode_error("keyrole", value, "expected none, record, or external"),
2508    }
2509}
2510
2511fn parse_transformation_type(value: String) -> Result<TransformationType, PgMetaError> {
2512    match value.as_str() {
2513        "ingest" => Ok(TransformationType::Ingest),
2514        "transform" => Ok(TransformationType::Transform),
2515        "entity" => Ok(TransformationType::Entity),
2516        "export" => Ok(TransformationType::Export),
2517        "repository" => Ok(TransformationType::Repository),
2518        _ => decode_error(
2519            "transformation_type",
2520            value,
2521            "expected ingest, transform, entity, export, or repository",
2522        ),
2523    }
2524}
2525
2526fn optional_int_id<T>(row: &Row, field: &'static str, wrap: fn(i64) -> T) -> Option<T> {
2527    row.get::<_, Option<i32>>(field)
2528        .map(|value| wrap(i64::from(value)))
2529}
2530
2531fn id_i32(value: i64, field: &'static str) -> Result<i32, PgMetaError> {
2532    i32::try_from(value).map_err(|_| PgMetaError::Decode {
2533        field,
2534        value: value.to_string(),
2535        message: "integer identifier is outside PostgreSQL INTEGER range".to_string(),
2536    })
2537}
2538
2539fn utc(value: NaiveDateTime) -> DateTime<Utc> {
2540    value.and_utc()
2541}
2542
2543fn decode_error<T>(
2544    field: &'static str,
2545    value: String,
2546    message: impl Into<String>,
2547) -> Result<T, PgMetaError> {
2548    Err(PgMetaError::Decode {
2549        field,
2550        value,
2551        message: message.into(),
2552    })
2553}
2554
2555#[cfg(test)]
2556mod tests {
2557    use super::*;
2558
2559    #[test]
2560    fn parse_key_role_decodes_external() {
2561        assert_eq!(
2562            parse_key_role("external".to_string()).expect("external should decode"),
2563            KeyRole::External
2564        );
2565    }
2566}