1use crate::{
2 ProtocolVersion, PublicName,
3 asset::{AssetCatalog, SafeAssetCatalog},
4 domain::DomainSelector,
5 pagination::PageRequest,
6 public_error::{ProtocolError, ValidationIssue, ValidationIssueCode},
7 refs::{DatasetRef, SessionSelector, StudyRef, TagSummary, VersionRef},
8 search::{PredicateSet, SearchCursorBinding, TagPredicate, TextPredicate},
9 session::SessionStatusPayload,
10 study::StudySelector,
11 tabular::TabularDataRef,
12};
13use ahri_tre_types::DatasetRecord;
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
18pub enum DatasetSelector {
19 Id {
20 dataset: DatasetRef,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
22 study: Option<StudySelector>,
23 },
24 Name {
25 study: StudySelector,
26 name: PublicName,
27 },
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub struct DatasetVersionRef {
32 pub dataset: DatasetRef,
33 pub version: VersionRef,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
37pub struct VersionLabel(pub String);
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct DatasetSummary {
41 pub dataset: DatasetRef,
42 pub study: StudyRef,
43 pub name: String,
44 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub latest_version: Option<DatasetVersionRef>,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub version_label: Option<VersionLabel>,
48 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub description: Option<String>,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub row_count: Option<u64>,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub variable_count: Option<u64>,
54 #[serde(default)]
55 pub tags: Vec<TagSummary>,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59pub struct DatasetDetail {
60 pub summary: DatasetSummary,
61 #[serde(default, skip_serializing_if = "Vec::is_empty")]
62 pub tabular_refs: Vec<TabularDataRef>,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66pub struct ListDatasetsRequest {
67 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub session: Option<SessionSelector>,
69 pub study: StudySelector,
70 pub page: PageRequest,
71 #[serde(default)]
72 pub include_versions: bool,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
76pub struct GetDatasetMetadataRequest {
77 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub session: Option<SessionSelector>,
79 pub dataset: DatasetSelector,
80 #[serde(default)]
81 pub with_variables: bool,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85pub struct PreviewDatasetRequest {
86 #[serde(default, skip_serializing_if = "Option::is_none")]
87 pub session: Option<SessionSelector>,
88 pub dataset: DatasetSelector,
89 pub limit: usize,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub width: Option<usize>,
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
95#[serde(rename_all = "snake_case")]
96pub enum DatasetDataFormat {
97 Arrow,
98 Parquet,
99 Csv,
100 Json,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104pub struct DatasetDataRequest {
105 #[serde(default, skip_serializing_if = "Option::is_none")]
106 pub session: Option<SessionSelector>,
107 pub dataset: DatasetSelector,
108 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub limit: Option<usize>,
110 #[serde(default, skip_serializing_if = "Option::is_none")]
111 pub to: Option<String>,
112 pub format: DatasetDataFormat,
113 #[serde(default)]
114 pub compress: bool,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
118pub struct ExportDatasetRequest {
119 #[serde(default, skip_serializing_if = "Option::is_none")]
120 pub session: Option<SessionSelector>,
121 pub dataset: DatasetSelector,
122 #[serde(default, skip_serializing_if = "Option::is_none")]
123 pub limit: Option<usize>,
124 pub target: DatasetExportTarget,
125 pub format: DatasetDataFormat,
126 #[serde(default)]
127 pub compress: bool,
128 #[serde(default)]
129 pub overwrite: bool,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133pub struct DatasetExportTarget {
134 pub path: String,
135 #[serde(default)]
136 pub request_only: bool,
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140pub struct SearchDatasetsRequest {
141 #[serde(default, skip_serializing_if = "Option::is_none")]
142 pub session: Option<SessionSelector>,
143 pub predicates: PredicateSet<DatasetPredicate>,
144 pub page: PageRequest,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
151pub struct BrowserDatasetSearchRequest {
152 pub datastore_id: crate::PublicUuid,
153 pub search: SearchDatasetsRequest,
154}
155
156#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
157pub struct DatasetSearchCursorAnchor {
158 pub study: StudyRef,
159 pub dataset: DatasetRef,
160 pub name: String,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164pub struct DatasetSearchCursorQuery {
165 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub session: Option<SessionSelector>,
167 pub predicates: PredicateSet<DatasetPredicate>,
168}
169
170#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
171pub struct DatasetSearchCursor {
172 pub binding: SearchCursorBinding<DatasetSearchCursorQuery>,
173 pub after: DatasetSearchCursorAnchor,
174}
175
176#[allow(clippy::result_large_err)]
177impl SearchDatasetsRequest {
178 pub fn validate(&mut self, protocol_version: &ProtocolVersion) -> Result<(), ProtocolError> {
179 let mut issues = Vec::new();
180 collect_predicate_set_issues("body.predicates", &self.predicates, &mut issues);
181 collect_dataset_predicate_issues(&mut self.predicates, &mut issues);
182 collect_page_issues("body.page.limit", self.page.limit, &mut issues);
183 collect_cursor_issues(self, protocol_version, &mut issues);
184
185 if issues.is_empty() {
186 Ok(())
187 } else {
188 Err(ProtocolError::validation(
189 "dataset search request is invalid",
190 issues,
191 ))
192 }
193 }
194
195 pub fn decoded_cursor(
196 &self,
197 protocol_version: &ProtocolVersion,
198 ) -> Result<Option<DatasetSearchCursorAnchor>, ProtocolError> {
199 match self.page.cursor.as_deref() {
200 None => Ok(None),
201 Some(raw_cursor) => {
202 let cursor: DatasetSearchCursor =
203 serde_json::from_str(raw_cursor).map_err(|error| {
204 ProtocolError::validation(
205 "dataset search request is invalid",
206 vec![ValidationIssue::new(
207 "body.page.cursor",
208 ValidationIssueCode::InvalidFormat,
209 format!("dataset search cursor is invalid: {error}"),
210 )],
211 )
212 })?;
213 let expected = self.cursor_binding(protocol_version);
214 if cursor.binding != expected {
215 return Err(ProtocolError::validation(
216 "dataset search request is invalid",
217 vec![ValidationIssue::new(
218 "body.page.cursor",
219 ValidationIssueCode::Conflict,
220 "dataset search cursor does not match the request predicates, page limit, or protocol version",
221 )],
222 ));
223 }
224 Ok(Some(cursor.after))
225 }
226 }
227 }
228
229 pub fn encode_cursor(
230 &self,
231 protocol_version: &ProtocolVersion,
232 after: DatasetSearchCursorAnchor,
233 ) -> Result<String, ProtocolError> {
234 serde_json::to_string(&DatasetSearchCursor {
235 binding: self.cursor_binding(protocol_version),
236 after,
237 })
238 .map_err(|error| {
239 ProtocolError::new(
240 crate::public_error::ProtocolErrorCode::Internal,
241 format!("failed to encode dataset search cursor: {error}"),
242 )
243 })
244 }
245
246 fn cursor_binding(
247 &self,
248 protocol_version: &ProtocolVersion,
249 ) -> SearchCursorBinding<DatasetSearchCursorQuery> {
250 SearchCursorBinding {
251 query: DatasetSearchCursorQuery {
252 session: self.session.clone(),
253 predicates: self.predicates.clone(),
254 },
255 page_limit: self.page.limit,
256 protocol_version: protocol_version.to_string(),
257 }
258 }
259
260 #[allow(clippy::result_large_err)]
264 pub fn validate_for_browser(
265 &mut self,
266 datastore_id: crate::PublicUuid,
267 protocol_version: &ProtocolVersion,
268 ) -> Result<(), ProtocolError> {
269 let mut issues = Vec::new();
270 if self.session.is_some() {
271 issues.push(ValidationIssue::new(
272 "body.session",
273 ValidationIssueCode::Unsupported,
274 "daemon session is not accepted for browser dataset search",
275 ));
276 }
277 collect_predicate_set_issues("body.predicates", &self.predicates, &mut issues);
278 collect_dataset_predicate_issues(&mut self.predicates, &mut issues);
279 collect_page_issues("body.page.limit", self.page.limit, &mut issues);
280 if let Some(raw_cursor) = self.page.cursor.as_deref() {
281 match serde_json::from_str::<BrowserDatasetSearchCursor>(raw_cursor) {
282 Ok(cursor)
283 if cursor.binding
284 == self.browser_cursor_binding(datastore_id, protocol_version) => {}
285 Ok(_) => issues.push(ValidationIssue::new(
286 "body.page.cursor",
287 ValidationIssueCode::Conflict,
288 "browser dataset search cursor does not match the datastore, search family, predicates, page limit, or protocol version",
289 )),
290 Err(_) => issues.push(ValidationIssue::new(
291 "body.page.cursor",
292 ValidationIssueCode::InvalidFormat,
293 "browser dataset search cursor is invalid",
294 )),
295 }
296 }
297 if issues.is_empty() {
298 Ok(())
299 } else {
300 Err(ProtocolError::validation(
301 "browser dataset search request is invalid",
302 issues,
303 ))
304 }
305 }
306
307 #[allow(clippy::result_large_err)]
308 pub fn browser_decoded_cursor(
309 &self,
310 datastore_id: crate::PublicUuid,
311 protocol_version: &ProtocolVersion,
312 ) -> Result<Option<BrowserDatasetSearchCursorAnchor>, ProtocolError> {
313 let Some(raw_cursor) = self.page.cursor.as_deref() else {
314 return Ok(None);
315 };
316 let cursor: BrowserDatasetSearchCursor =
317 serde_json::from_str(raw_cursor).map_err(|_| {
318 ProtocolError::validation(
319 "browser dataset search request is invalid",
320 vec![ValidationIssue::new(
321 "body.page.cursor",
322 ValidationIssueCode::InvalidFormat,
323 "browser dataset search cursor is invalid",
324 )],
325 )
326 })?;
327 if cursor.binding != self.browser_cursor_binding(datastore_id, protocol_version) {
328 return Err(ProtocolError::validation(
329 "browser dataset search request is invalid",
330 vec![ValidationIssue::new(
331 "body.page.cursor",
332 ValidationIssueCode::Conflict,
333 "browser dataset search cursor does not match the datastore, search family, predicates, page limit, or protocol version",
334 )],
335 ));
336 }
337 Ok(Some(cursor.after))
338 }
339
340 #[allow(clippy::result_large_err)]
341 pub fn encode_browser_cursor(
342 &self,
343 datastore_id: crate::PublicUuid,
344 protocol_version: &ProtocolVersion,
345 after: BrowserDatasetSearchCursorAnchor,
346 ) -> Result<String, ProtocolError> {
347 serde_json::to_string(&BrowserDatasetSearchCursor {
348 binding: self.browser_cursor_binding(datastore_id, protocol_version),
349 after,
350 })
351 .map_err(|_| {
352 ProtocolError::new(
353 crate::public_error::ProtocolErrorCode::Internal,
354 "failed to encode browser dataset search cursor",
355 )
356 })
357 }
358
359 fn browser_cursor_binding(
360 &self,
361 datastore_id: crate::PublicUuid,
362 protocol_version: &ProtocolVersion,
363 ) -> SearchCursorBinding<BrowserDatasetSearchCursorQuery> {
364 SearchCursorBinding {
365 query: BrowserDatasetSearchCursorQuery {
366 datastore_id,
367 family: "datasets".to_string(),
368 predicates: self.predicates.clone(),
369 },
370 page_limit: self.page.limit,
371 protocol_version: protocol_version.to_string(),
372 }
373 }
374}
375
376#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
380pub struct BrowserDatasetSearchResponse {
381 pub protocol_version: ProtocolVersion,
382 pub datastore_id: crate::PublicUuid,
383 pub family: String,
384 pub predicates: PredicateSet<DatasetPredicate>,
385 #[serde(default)]
386 pub datasets: Vec<crate::resource_discovery::BrowserDatasetDiscoveryRecord>,
387 pub returned_count: u16,
388 pub requested_limit: u16,
389 pub ordering: crate::study::BrowserSearchOrdering,
390 pub has_more: bool,
391 #[serde(default, skip_serializing_if = "Option::is_none")]
392 pub next_cursor: Option<String>,
393 #[serde(default, skip_serializing_if = "Option::is_none")]
394 pub empty_state: Option<BrowserDatasetSearchEmptyState>,
395}
396
397#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
398pub struct BrowserDatasetSearchEmptyState {
399 pub code: String,
400 pub message: String,
401}
402
403#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
404pub struct BrowserDatasetSearchCursorQuery {
405 pub datastore_id: crate::PublicUuid,
406 pub family: String,
407 pub predicates: PredicateSet<DatasetPredicate>,
408}
409
410#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
411pub struct BrowserDatasetSearchCursor {
412 pub binding: SearchCursorBinding<BrowserDatasetSearchCursorQuery>,
413 pub after: BrowserDatasetSearchCursorAnchor,
414}
415
416#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
417pub struct BrowserDatasetSearchCursorAnchor {
418 pub study: StudyRef,
419 pub dataset: DatasetRef,
420 pub asset: crate::refs::AssetRef,
421 pub name: String,
422}
423
424#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
425#[serde(tag = "kind", rename_all = "snake_case")]
426pub enum DatasetPredicate {
427 Text(TextPredicate<DatasetTextField>),
428 Study { study: StudySelector },
429 Domain { domain: DomainSelector },
430 VersionLabel { label: VersionLabel },
431 Tag(TagPredicate),
432}
433
434#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
435#[serde(rename_all = "snake_case")]
436pub enum DatasetTextField {
437 Name,
438 Description,
439 VersionLabel,
440}
441
442#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
443pub struct RegisteredDatasetVariablePayload {
444 pub variable: crate::catalogue::VariableSummary,
445 pub vocabulary: Option<crate::catalogue::VocabularySummary>,
446 pub vocabulary_items: Vec<crate::catalogue::VocabularyItemSummary>,
447 #[serde(default = "withheld_by_default")]
449 pub vocabulary_items_withheld: bool,
450 pub link: crate::catalogue::DatasetVariableLinkSummary,
451}
452
453#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
454pub struct DatasetVariableMetadataPayload {
455 pub link: crate::catalogue::DatasetVariableLinkSummary,
456 pub variable: crate::catalogue::VariableSummary,
457 pub value_type: crate::catalogue::ValueTypeSummary,
458 pub vocabulary: Option<crate::catalogue::VocabularySummary>,
459 pub vocabulary_items: Vec<crate::catalogue::VocabularyItemSummary>,
460 #[serde(default = "withheld_by_default")]
462 pub vocabulary_items_withheld: bool,
463}
464
465#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
466pub struct DatasetListEntryPayload {
467 pub catalog: SafeAssetCatalog,
468 pub latest_dataset: Option<crate::catalogue::DatasetSummary>,
469}
470
471#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
472pub struct DatasetListResponse {
473 pub session: SessionStatusPayload,
474 pub study: String,
475 pub study_id: StudyRef,
476 pub datasets: Vec<DatasetListEntryPayload>,
477}
478
479#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
480pub struct DatasetSearchResponse {
481 pub session: SessionStatusPayload,
482 pub datasets: Vec<DatasetSummary>,
483 #[serde(default, skip_serializing_if = "Option::is_none")]
484 pub next_cursor: Option<String>,
485}
486
487#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
488pub struct DatasetMetadataPayload {
489 pub catalog: SafeAssetCatalog,
490 pub dataset: crate::catalogue::DatasetSummary,
491 #[serde(default)]
492 pub variables_included: bool,
493 #[serde(default)]
494 pub variable_count: usize,
495 pub variables: Vec<DatasetVariableMetadataPayload>,
496}
497
498#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
499pub struct DatasetMetadataResponse {
500 pub session: SessionStatusPayload,
501 pub study: String,
502 pub metadata: DatasetMetadataPayload,
503}
504
505#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
506pub struct DatasetPreviewColumnPayload {
507 pub name: String,
508 pub value_type: String,
509}
510
511#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
512pub struct DatasetPreviewPayload {
513 pub catalog: AssetCatalog,
514 pub dataset: DatasetRecord,
515 pub columns: Vec<DatasetPreviewColumnPayload>,
516 pub rows: Vec<Vec<Option<String>>>,
517 pub displayed_row_count: usize,
518 pub total_row_count: usize,
519 pub truncated: bool,
520 pub limit: usize,
521}
522
523#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
524pub struct DatasetPreviewResponse {
525 pub session: SessionStatusPayload,
526 pub study: String,
527 pub preview: DatasetPreviewPayload,
528}
529
530#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
531pub struct DatasetDataResponse {
532 pub session: SessionStatusPayload,
533 pub study: String,
534 pub catalog: AssetCatalog,
535 pub dataset: DatasetRecord,
536 pub lineage: crate::transformation::TransformationSummary,
537 pub format: String,
538 pub target_path: String,
539 pub compressed: bool,
540 pub row_count: usize,
541 pub column_count: usize,
542 pub exported_size_bytes: u64,
543 pub warnings: Vec<String>,
544}
545
546#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
547pub struct DatasetExportResponse {
548 pub session: SessionStatusPayload,
549 pub study: String,
550 pub catalog: AssetCatalog,
551 pub dataset: DatasetRecord,
552 pub lineage: crate::transformation::TransformationSummary,
553 pub artifact: DatasetExportArtifactSummary,
554 pub row_count: usize,
555 pub column_count: usize,
556 #[serde(default, skip_serializing_if = "Vec::is_empty")]
557 pub warnings: Vec<String>,
558}
559
560#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
561pub struct DatasetExportArtifactSummary {
562 pub kind: DatasetExportArtifactKind,
563 pub format: DatasetDataFormat,
564 pub compressed: bool,
565 pub exported_size_bytes: u64,
566 #[serde(default)]
567 pub target_request_only: bool,
568}
569
570#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
571#[serde(rename_all = "snake_case")]
572pub enum DatasetExportArtifactKind {
573 Dataset,
574}
575
576fn collect_predicate_set_issues(
577 path: &str,
578 predicates: &PredicateSet<DatasetPredicate>,
579 issues: &mut Vec<ValidationIssue>,
580) {
581 if let Err(message) = predicates.validate() {
582 let code = if predicates.predicates().is_empty() {
583 ValidationIssueCode::TooShort
584 } else {
585 ValidationIssueCode::TooLong
586 };
587 issues.push(ValidationIssue::new(path, code, message));
588 }
589}
590
591fn collect_dataset_predicate_issues(
592 predicates: &mut PredicateSet<DatasetPredicate>,
593 issues: &mut Vec<ValidationIssue>,
594) {
595 for (index, predicate) in predicates.predicates_mut().iter_mut().enumerate() {
596 match predicate {
597 DatasetPredicate::Text(text) => {
598 if let Err(message) = text.validate() {
599 let code = if text.value.trim().is_empty() {
600 ValidationIssueCode::TooShort
601 } else {
602 ValidationIssueCode::TooLong
603 };
604 issues.push(ValidationIssue::new(
605 format!("body.predicates.predicates[{index}].value"),
606 code,
607 message,
608 ));
609 }
610 }
611 DatasetPredicate::VersionLabel { label } => {
612 label.0 = label.0.trim().to_string();
613 if label.0.is_empty() {
614 issues.push(ValidationIssue::new(
615 format!("body.predicates.predicates[{index}].label"),
616 ValidationIssueCode::TooShort,
617 "dataset version label must not be empty",
618 ));
619 } else if label.0.len() > crate::search::MAX_TEXT_VALUE_LENGTH {
620 issues.push(ValidationIssue::new(
621 format!("body.predicates.predicates[{index}].label"),
622 ValidationIssueCode::TooLong,
623 format!(
624 "dataset version label must be at most {} characters",
625 crate::search::MAX_TEXT_VALUE_LENGTH
626 ),
627 ));
628 }
629 }
630 DatasetPredicate::Study { .. }
631 | DatasetPredicate::Domain { .. }
632 | DatasetPredicate::Tag(_) => {}
633 }
634 }
635}
636
637fn collect_page_issues(path: &str, limit: u16, issues: &mut Vec<ValidationIssue>) {
638 if limit == 0 || limit > crate::search::MAX_PAGE_LIMIT {
639 issues.push(ValidationIssue::new(
640 path,
641 ValidationIssueCode::OutOfRange,
642 if limit == 0 {
643 "page limit must be greater than zero".to_string()
644 } else {
645 format!(
646 "page limit must be at most {}",
647 crate::search::MAX_PAGE_LIMIT
648 )
649 },
650 ));
651 }
652}
653
654fn collect_cursor_issues(
655 request: &SearchDatasetsRequest,
656 protocol_version: &ProtocolVersion,
657 issues: &mut Vec<ValidationIssue>,
658) {
659 if let Some(raw_cursor) = request.page.cursor.as_deref() {
660 let cursor: DatasetSearchCursor = match serde_json::from_str(raw_cursor) {
661 Ok(cursor) => cursor,
662 Err(error) => {
663 issues.push(ValidationIssue::new(
664 "body.page.cursor",
665 ValidationIssueCode::InvalidFormat,
666 format!("dataset search cursor is invalid: {error}"),
667 ));
668 return;
669 }
670 };
671
672 if cursor.binding != request.cursor_binding(protocol_version) {
673 issues.push(ValidationIssue::new(
674 "body.page.cursor",
675 ValidationIssueCode::Conflict,
676 "dataset search cursor does not match the request predicates, page limit, or protocol version",
677 ));
678 }
679 }
680}
681
682fn withheld_by_default() -> bool {
683 true
684}
685
686#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
688#[serde(deny_unknown_fields)]
689pub struct DatasetTransformRequest {
690 pub session: Option<crate::refs::SessionSelector>,
691 pub study: StudySelector,
692 pub dataset: crate::PublicName,
693 pub inputs: Vec<crate::content::QueryInput>,
694 #[serde(default)]
695 pub views: std::collections::BTreeMap<String, String>,
696 pub sql: String,
697 pub description: String,
698 pub version_note: Option<String>,
699 #[serde(default)]
700 pub budgets: ahri_tre_types::DisclosureBudgetOverrides,
701}
702
703#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
705pub struct DatasetTransformResponse {
706 pub session: SessionStatusPayload,
707 pub catalog: SafeAssetCatalog,
708 pub lineage: crate::transformation::TransformationSummary,
709}