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