1use std::cell::RefCell;
2use std::collections::{BTreeMap, BTreeSet};
3
4use serde::{Deserialize, Serialize};
5
6use crate::coordinator::{
7 parse_native_branch_view_filter, validate_fingerprint, CoordinatorDataPlanEnvelope,
8 CoordinatorRelation, CoordinatorRelationSet,
9};
10use crate::error::{DataError, Result};
11use crate::ids::{ObservationId, RepresentationId, SampleId, SourceId, TargetId};
12use crate::model::DataView;
13
14#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum CoordinatorHandleKind {
17 Data,
18 View,
19}
20
21#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
22pub struct CoordinatorHandleRef {
23 pub handle: u64,
24 pub kind: CoordinatorHandleKind,
25 pub owner_controller: String,
26}
27
28#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
29pub struct CoordinatorDataMaterializationRequest {
30 pub run_id: String,
31 pub node_id: String,
32 pub input_name: String,
33 pub phase: String,
34 #[serde(default)]
35 pub variant_id: Option<String>,
36 #[serde(default)]
37 pub fold_id: Option<String>,
38 pub request_id: String,
39 pub schema_fingerprint: String,
40 pub plan_fingerprint: String,
41 #[serde(default)]
42 pub relation_fingerprint: Option<String>,
43 pub output_representation: RepresentationId,
44 #[serde(default)]
45 pub source_ids: Vec<SourceId>,
46 #[serde(default)]
47 pub require_relations: bool,
48 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub predict_cohort: Option<crate::PredictCohort>,
51}
52
53impl CoordinatorDataMaterializationRequest {
54 pub fn validate(&self) -> Result<()> {
55 validate_non_empty("run_id", &self.run_id)?;
56 validate_non_empty("node_id", &self.node_id)?;
57 validate_non_empty("input_name", &self.input_name)?;
58 validate_non_empty("phase", &self.phase)?;
59 validate_non_empty("request_id", &self.request_id)?;
60 validate_fingerprint("schema", &self.schema_fingerprint)?;
61 validate_fingerprint("plan", &self.plan_fingerprint)?;
62 if let Some(relation_fingerprint) = &self.relation_fingerprint {
63 validate_fingerprint("relation", relation_fingerprint)?;
64 } else if self.require_relations {
65 return Err(DataError::Validation(format!(
66 "materialization request `{}` on `{}` requires relations but has no relation_fingerprint",
67 self.input_name, self.node_id
68 )));
69 }
70 let unique_sources = self.source_ids.iter().collect::<BTreeSet<_>>();
71 if unique_sources.len() != self.source_ids.len() {
72 return Err(DataError::Validation(format!(
73 "materialization request `{}` on `{}` contains duplicate source ids",
74 self.input_name, self.node_id
75 )));
76 }
77 Ok(())
78 }
79}
80
81#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
82pub struct CoordinatorDataHandleRecord {
83 pub handle: CoordinatorHandleRef,
84 pub run_id: String,
85 pub node_id: String,
86 pub input_name: String,
87 pub phase: String,
88 #[serde(default)]
89 pub variant_id: Option<String>,
90 #[serde(default)]
91 pub fold_id: Option<String>,
92 pub request_id: String,
93 pub schema_fingerprint: String,
94 pub plan_fingerprint: String,
95 #[serde(default)]
96 pub relation_fingerprint: Option<String>,
97 pub plan_id: String,
98 pub output_representation: RepresentationId,
99 #[serde(default)]
100 pub source_ids: Vec<SourceId>,
101 #[serde(default)]
102 pub sample_count: Option<usize>,
103 #[serde(default)]
104 pub relation_record_count: Option<usize>,
105}
106
107#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
108pub struct CoordinatorDataViewRecord {
109 pub handle: CoordinatorHandleRef,
110 pub parent_handle: CoordinatorHandleRef,
111 pub view: DataView,
112 pub sample_count: usize,
113 pub relation_record_count: usize,
114}
115
116#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
117pub struct CoordinatorTargetValue {
118 pub sample_id: SampleId,
119 pub value: serde_json::Value,
120}
121
122#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
123pub struct CoordinatorTargetTable {
124 pub target_id: TargetId,
125 pub values: Vec<CoordinatorTargetValue>,
126}
127
128impl CoordinatorTargetTable {
129 pub fn validate(&self) -> Result<()> {
130 if self.values.is_empty() {
131 return Err(DataError::Validation(format!(
132 "target table `{}` contains no values",
133 self.target_id
134 )));
135 }
136 let mut seen = BTreeSet::new();
137 for value in &self.values {
138 if !seen.insert(&value.sample_id) {
139 return Err(DataError::Validation(format!(
140 "target table `{}` contains duplicate sample `{}`",
141 self.target_id, value.sample_id
142 )));
143 }
144 }
145 Ok(())
146 }
147}
148
149#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
150pub struct CoordinatorTargetBlock {
151 pub target_id: TargetId,
152 pub sample_ids: Vec<SampleId>,
153 pub values: Vec<serde_json::Value>,
154}
155
156#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
157pub struct CoordinatorMultiTargetBlock {
158 pub target_ids: Vec<TargetId>,
159 pub sample_ids: Vec<SampleId>,
160 pub values: Vec<Vec<serde_json::Value>>,
162 pub validity_masks: Vec<Vec<bool>>,
164}
165
166#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
167pub struct CoordinatorFeatureRow {
168 pub observation_id: ObservationId,
169 pub values: Vec<serde_json::Value>,
170}
171
172#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
173pub struct CoordinatorFeatureTable {
174 pub feature_set_id: String,
175 pub representation_id: RepresentationId,
176 pub feature_names: Vec<String>,
177 pub rows: Vec<CoordinatorFeatureRow>,
178}
179
180impl CoordinatorFeatureTable {
181 pub fn validate(&self) -> Result<()> {
182 validate_non_empty("feature_set_id", &self.feature_set_id)?;
183 if self.feature_names.is_empty() {
184 return Err(DataError::Validation(format!(
185 "feature table `{}` contains no features",
186 self.feature_set_id
187 )));
188 }
189 let mut seen_features = BTreeSet::new();
190 for feature_name in &self.feature_names {
191 validate_non_empty("feature_name", feature_name)?;
192 if !seen_features.insert(feature_name) {
193 return Err(DataError::Validation(format!(
194 "feature table `{}` contains duplicate feature `{}`",
195 self.feature_set_id, feature_name
196 )));
197 }
198 }
199 if self.rows.is_empty() {
200 return Err(DataError::Validation(format!(
201 "feature table `{}` contains no rows",
202 self.feature_set_id
203 )));
204 }
205 let mut seen_observations = BTreeSet::new();
206 for row in &self.rows {
207 if !seen_observations.insert(&row.observation_id) {
208 return Err(DataError::Validation(format!(
209 "feature table `{}` contains duplicate observation `{}`",
210 self.feature_set_id, row.observation_id
211 )));
212 }
213 if row.values.len() != self.feature_names.len() {
214 return Err(DataError::Validation(format!(
215 "feature table `{}` row `{}` has {} values for {} features",
216 self.feature_set_id,
217 row.observation_id,
218 row.values.len(),
219 self.feature_names.len()
220 )));
221 }
222 }
223 Ok(())
224 }
225}
226
227#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
228pub struct CoordinatorFeatureBlock {
229 pub feature_set_id: String,
230 pub representation_id: RepresentationId,
231 pub feature_names: Vec<String>,
232 pub observation_ids: Vec<ObservationId>,
233 pub sample_ids: Vec<SampleId>,
234 pub values: Vec<Vec<serde_json::Value>>,
235}
236
237#[derive(Clone, Debug, PartialEq)]
243pub struct CoordinatorFeatureBlockF64 {
244 pub feature_set_id: String,
245 pub representation_id: RepresentationId,
246 pub feature_names: Vec<String>,
247 pub observation_ids: Vec<ObservationId>,
248 pub sample_ids: Vec<SampleId>,
249 pub values: Vec<f64>,
250}
251
252#[derive(Debug)]
253pub struct CoordinatorHandleArena {
254 owner_controller: String,
255 next_handle: RefCell<u64>,
256 records: RefCell<BTreeMap<u64, CoordinatorDataHandleRecord>>,
257 data_relations: RefCell<BTreeMap<u64, CoordinatorRelationSet>>,
258 view_records: RefCell<BTreeMap<u64, CoordinatorDataViewRecord>>,
259 view_relations: RefCell<BTreeMap<u64, CoordinatorRelationSet>>,
260}
261
262impl CoordinatorHandleArena {
263 pub fn new(owner_controller: impl Into<String>) -> Result<Self> {
264 let owner_controller = owner_controller.into();
265 validate_non_empty("owner_controller", &owner_controller)?;
266 Ok(Self {
267 owner_controller,
268 next_handle: RefCell::new(1),
269 records: RefCell::new(BTreeMap::new()),
270 data_relations: RefCell::new(BTreeMap::new()),
271 view_records: RefCell::new(BTreeMap::new()),
272 view_relations: RefCell::new(BTreeMap::new()),
273 })
274 }
275
276 pub fn materialize(
277 &self,
278 envelope: &CoordinatorDataPlanEnvelope,
279 request: &CoordinatorDataMaterializationRequest,
280 ) -> Result<CoordinatorDataHandleRecord> {
281 envelope.validate()?;
282 request.validate()?;
283 validate_request_against_envelope(envelope, request)?;
284 let scoped_relations = materialization_relations(envelope, request)?
285 .map(|relations| scoped_relations_for_materialization(relations, request))
286 .transpose()?;
287
288 let handle = CoordinatorHandleRef {
289 handle: self.next_handle(),
290 kind: CoordinatorHandleKind::Data,
291 owner_controller: self.owner_controller.clone(),
292 };
293 let record = CoordinatorDataHandleRecord {
294 handle: handle.clone(),
295 run_id: request.run_id.clone(),
296 node_id: request.node_id.clone(),
297 input_name: request.input_name.clone(),
298 phase: request.phase.clone(),
299 variant_id: request.variant_id.clone(),
300 fold_id: request.fold_id.clone(),
301 request_id: request.request_id.clone(),
302 schema_fingerprint: request.schema_fingerprint.clone(),
303 plan_fingerprint: request.plan_fingerprint.clone(),
304 relation_fingerprint: request.relation_fingerprint.clone(),
305 plan_id: envelope.plan.id.clone(),
306 output_representation: request.output_representation.clone(),
307 source_ids: request.source_ids.clone(),
308 sample_count: scoped_relations.as_ref().map(|relations| {
309 relations
310 .records
311 .iter()
312 .map(|record| &record.sample_id)
313 .collect::<BTreeSet<_>>()
314 .len()
315 }),
316 relation_record_count: scoped_relations
317 .as_ref()
318 .map(|relations| relations.records.len()),
319 };
320 self.records
321 .borrow_mut()
322 .insert(handle.handle, record.clone());
323 if let Some(relations) = scoped_relations {
324 self.data_relations
325 .borrow_mut()
326 .insert(handle.handle, relations);
327 }
328 Ok(record)
329 }
330
331 pub fn make_view(
332 &self,
333 data_handle: u64,
334 view: &DataView,
335 ) -> Result<CoordinatorDataViewRecord> {
336 validate_view(view)?;
337 let parent =
338 self.records
339 .borrow()
340 .get(&data_handle)
341 .cloned()
342 .ok_or(DataError::UnknownHandle {
343 kind: "data",
344 handle: data_handle,
345 })?;
346 let relations = self
347 .data_relations
348 .borrow()
349 .get(&data_handle)
350 .cloned()
351 .ok_or_else(|| {
352 DataError::Validation(format!(
353 "data handle `{data_handle}` has no coordinator relations"
354 ))
355 })?;
356 let selectors = resolved_view_labels(view, &parent)?;
357 let scoped = relations
358 .records
359 .into_iter()
360 .filter(|relation| {
361 selectors.iter().all(|(key, label)| {
362 relation
363 .metadata
364 .get(*key)
365 .and_then(serde_json::Value::as_str)
366 == Some(*label)
367 })
368 })
369 .collect::<Vec<_>>();
370 let filtered = filter_relations(&scoped, view)?;
371 let sample_count = unique_sample_count(&filtered);
372 let relation_record_count = filtered.len();
373 let handle = CoordinatorHandleRef {
374 handle: self.next_handle(),
375 kind: CoordinatorHandleKind::View,
376 owner_controller: self.owner_controller.clone(),
377 };
378 let record = CoordinatorDataViewRecord {
379 handle: handle.clone(),
380 parent_handle: parent.handle,
381 view: view.clone(),
382 sample_count,
383 relation_record_count,
384 };
385 self.view_records
386 .borrow_mut()
387 .insert(handle.handle, record.clone());
388 self.view_relations
389 .borrow_mut()
390 .insert(handle.handle, CoordinatorRelationSet { records: filtered });
391 Ok(record)
392 }
393
394 pub fn view_record(&self, handle: u64) -> Option<CoordinatorDataViewRecord> {
395 self.view_records.borrow().get(&handle).cloned()
396 }
397
398 pub fn view_identity(&self, handle: u64) -> Result<CoordinatorRelationSet> {
399 self.view_relations
400 .borrow()
401 .get(&handle)
402 .cloned()
403 .ok_or(DataError::UnknownHandle {
404 kind: "view",
405 handle,
406 })
407 }
408
409 pub fn data_identity(&self, handle: u64) -> Result<CoordinatorRelationSet> {
410 if !self.records.borrow().contains_key(&handle) {
415 return Err(DataError::UnknownHandle {
416 kind: "data",
417 handle,
418 });
419 }
420 self.data_relations
421 .borrow()
422 .get(&handle)
423 .cloned()
424 .ok_or_else(|| {
425 DataError::Validation(format!(
426 "data handle `{handle}` has no coordinator relations"
427 ))
428 })
429 }
430
431 pub fn release_handle(&self, handle: u64) -> bool {
432 if self.view_records.borrow_mut().remove(&handle).is_some() {
433 self.view_relations.borrow_mut().remove(&handle);
434 return true;
435 }
436 if let Some(record) = self.records.borrow_mut().remove(&handle) {
437 self.data_relations.borrow_mut().remove(&handle);
438 let child_views = self
439 .view_records
440 .borrow()
441 .iter()
442 .filter_map(|(view_handle, view_record)| {
443 (view_record.parent_handle == record.handle).then_some(*view_handle)
444 })
445 .collect::<Vec<_>>();
446 for view_handle in child_views {
447 self.view_records.borrow_mut().remove(&view_handle);
448 self.view_relations.borrow_mut().remove(&view_handle);
449 }
450 return true;
451 }
452 false
453 }
454
455 pub fn target_values(
456 &self,
457 view_handle: u64,
458 target_table: &CoordinatorTargetTable,
459 ) -> Result<CoordinatorTargetBlock> {
460 target_table.validate()?;
461 let relations = self.view_identity(view_handle)?;
462 let values_by_sample = target_table
463 .values
464 .iter()
465 .map(|value| (&value.sample_id, &value.value))
466 .collect::<BTreeMap<_, _>>();
467 let mut seen_samples = BTreeSet::new();
468 let mut sample_ids = Vec::new();
469 let mut values = Vec::new();
470 for relation in relations.records.iter().filter(|relation| {
471 relation
472 .target_id
473 .as_ref()
474 .map(|target_id| target_id == &target_table.target_id)
475 .unwrap_or(true)
476 }) {
477 if !seen_samples.insert(&relation.sample_id) {
478 continue;
479 }
480 let value = values_by_sample.get(&relation.sample_id).ok_or_else(|| {
481 DataError::Validation(format!(
482 "target table `{}` has no value for sample `{}`",
483 target_table.target_id, relation.sample_id
484 ))
485 })?;
486 sample_ids.push(relation.sample_id.clone());
487 values.push((*value).clone());
488 }
489 if sample_ids.is_empty() {
490 return Err(DataError::Validation(format!(
491 "view `{view_handle}` contains no samples for target `{}`",
492 target_table.target_id
493 )));
494 }
495 Ok(CoordinatorTargetBlock {
496 target_id: target_table.target_id.clone(),
497 sample_ids,
498 values,
499 })
500 }
501
502 pub fn multi_target_values(
503 &self,
504 view_handle: u64,
505 target_tables: &[CoordinatorTargetTable],
506 ) -> Result<CoordinatorMultiTargetBlock> {
507 if target_tables.is_empty() {
508 return Err(DataError::Validation(
509 "multi-target materialization requires at least one target table".to_string(),
510 ));
511 }
512 let mut seen_targets = BTreeSet::new();
513 for table in target_tables {
514 table.validate()?;
515 if !seen_targets.insert(table.target_id.clone()) {
516 return Err(DataError::Validation(format!(
517 "multi-target materialization contains duplicate target `{}`",
518 table.target_id
519 )));
520 }
521 }
522
523 let target_ids = target_tables
524 .iter()
525 .map(|table| table.target_id.clone())
526 .collect::<Vec<_>>();
527 let target_universe = target_ids.iter().collect::<BTreeSet<_>>();
528 let relations = self.view_identity(view_handle)?;
529 let mut seen_samples = BTreeSet::new();
530 let mut sample_ids = Vec::new();
531 for relation in relations.records.iter().filter(|relation| {
532 relation
533 .target_id
534 .as_ref()
535 .map(|target_id| target_universe.contains(target_id))
536 .unwrap_or(true)
537 }) {
538 if seen_samples.insert(relation.sample_id.clone()) {
539 sample_ids.push(relation.sample_id.clone());
540 }
541 }
542 if sample_ids.is_empty() {
543 return Err(DataError::Validation(format!(
544 "view `{view_handle}` contains no samples for requested targets"
545 )));
546 }
547
548 let mut values = Vec::with_capacity(target_tables.len());
549 let mut validity_masks = Vec::with_capacity(target_tables.len());
550 for table in target_tables {
551 let values_by_sample = table
552 .values
553 .iter()
554 .map(|value| (&value.sample_id, &value.value))
555 .collect::<BTreeMap<_, _>>();
556 let mut target_values = Vec::with_capacity(sample_ids.len());
557 let mut validity = Vec::with_capacity(sample_ids.len());
558 for sample_id in &sample_ids {
559 match values_by_sample.get(sample_id) {
560 Some(value) if !value.is_null() => {
561 target_values.push((*value).clone());
562 validity.push(true);
563 }
564 Some(_) | None => {
565 target_values.push(serde_json::Value::Null);
566 validity.push(false);
567 }
568 }
569 }
570 values.push(target_values);
571 validity_masks.push(validity);
572 }
573
574 Ok(CoordinatorMultiTargetBlock {
575 target_ids,
576 sample_ids,
577 values,
578 validity_masks,
579 })
580 }
581
582 pub fn feature_values(
583 &self,
584 view_handle: u64,
585 feature_table: &CoordinatorFeatureTable,
586 ) -> Result<CoordinatorFeatureBlock> {
587 feature_table.validate()?;
588 let view_record = self
589 .view_records
590 .borrow()
591 .get(&view_handle)
592 .cloned()
593 .ok_or(DataError::UnknownHandle {
594 kind: "view",
595 handle: view_handle,
596 })?;
597 let parent_record = self
598 .records
599 .borrow()
600 .get(&view_record.parent_handle.handle)
601 .cloned()
602 .ok_or(DataError::UnknownHandle {
603 kind: "data",
604 handle: view_record.parent_handle.handle,
605 })?;
606 if feature_table.representation_id != parent_record.output_representation {
607 return Err(DataError::Validation(format!(
608 "feature table `{}` representation `{}` does not match materialized output representation `{}`",
609 feature_table.feature_set_id,
610 feature_table.representation_id,
611 parent_record.output_representation
612 )));
613 }
614 let relations = self.view_identity(view_handle)?;
615 let selected_indices = selected_feature_indices(feature_table, &view_record.view)?;
616 let rows_by_observation = feature_table
617 .rows
618 .iter()
619 .map(|row| (&row.observation_id, row))
620 .collect::<BTreeMap<_, _>>();
621 let mut observation_ids = Vec::with_capacity(relations.records.len());
622 let mut sample_ids = Vec::with_capacity(relations.records.len());
623 let mut values = Vec::with_capacity(relations.records.len());
624 for relation in &relations.records {
625 let row = rows_by_observation
626 .get(&relation.observation_id)
627 .ok_or_else(|| {
628 DataError::Validation(format!(
629 "feature table `{}` has no row for observation `{}`",
630 feature_table.feature_set_id, relation.observation_id
631 ))
632 })?;
633 observation_ids.push(relation.observation_id.clone());
634 sample_ids.push(relation.sample_id.clone());
635 values.push(
636 selected_indices
637 .iter()
638 .map(|idx| row.values[*idx].clone())
639 .collect(),
640 );
641 }
642 Ok(CoordinatorFeatureBlock {
643 feature_set_id: feature_table.feature_set_id.clone(),
644 representation_id: feature_table.representation_id.clone(),
645 feature_names: selected_indices
646 .iter()
647 .map(|idx| feature_table.feature_names[*idx].clone())
648 .collect(),
649 observation_ids,
650 sample_ids,
651 values,
652 })
653 }
654
655 pub fn handle_record(&self, handle: u64) -> Option<CoordinatorDataHandleRecord> {
656 self.records.borrow().get(&handle).cloned()
657 }
658
659 pub fn handle_records(&self) -> Vec<CoordinatorDataHandleRecord> {
660 self.records.borrow().values().cloned().collect()
661 }
662
663 fn next_handle(&self) -> u64 {
664 let mut next = self.next_handle.borrow_mut();
665 let handle = *next;
666 *next += 1;
667 handle
668 }
669}
670
671fn validate_request_against_envelope(
672 envelope: &CoordinatorDataPlanEnvelope,
673 request: &CoordinatorDataMaterializationRequest,
674) -> Result<()> {
675 if request.schema_fingerprint != envelope.schema_fingerprint {
676 return Err(DataError::FingerprintMismatch {
677 kind: "schema",
678 expected: envelope.schema_fingerprint.clone(),
679 actual: request.schema_fingerprint.clone(),
680 });
681 }
682 if request.plan_fingerprint != envelope.plan_fingerprint {
683 return Err(DataError::FingerprintMismatch {
684 kind: "plan",
685 expected: envelope.plan_fingerprint.clone(),
686 actual: request.plan_fingerprint.clone(),
687 });
688 }
689 if request.relation_fingerprint != envelope.relation_fingerprint {
690 let none = || "<none>".to_string();
691 return Err(DataError::FingerprintMismatch {
692 kind: "relation",
693 expected: envelope.relation_fingerprint.clone().unwrap_or_else(none),
694 actual: request.relation_fingerprint.clone().unwrap_or_else(none),
695 });
696 }
697 if request.require_relations && materialization_relations(envelope, request)?.is_none() {
698 return Err(DataError::Validation(format!(
699 "materialization request `{}` on `{}` requires coordinator relations",
700 request.input_name, request.node_id
701 )));
702 }
703 if request.output_representation != envelope.plan.output_representation {
704 return Err(DataError::Validation(format!(
705 "materialization request `{}` on `{}` output representation `{}` does not match plan output `{}`",
706 request.input_name,
707 request.node_id,
708 request.output_representation,
709 envelope.plan.output_representation
710 )));
711 }
712 if !request.source_ids.is_empty() {
713 let plan_sources = envelope
714 .plan
715 .steps
716 .iter()
717 .filter_map(|step| step.source_id.as_ref())
718 .collect::<BTreeSet<_>>();
719 for source_id in &request.source_ids {
720 if !plan_sources.contains(source_id) {
721 return Err(DataError::Validation(format!(
722 "materialization request `{}` on `{}` source `{}` is not present in data plan `{}`",
723 request.input_name, request.node_id, source_id, envelope.plan.id
724 )));
725 }
726 }
727 }
728 Ok(())
729}
730
731fn materialization_relations<'a>(
732 envelope: &'a CoordinatorDataPlanEnvelope,
733 request: &CoordinatorDataMaterializationRequest,
734) -> Result<Option<&'a CoordinatorRelationSet>> {
735 let prediction = request.phase.eq_ignore_ascii_case("predict")
736 || request.phase.eq_ignore_ascii_case("explain");
737 if let Some(cohort) = &request.predict_cohort {
738 cohort.validate()?;
739 if envelope.predict_cohort.as_ref() != Some(cohort) {
740 return Err(DataError::Validation(
741 "request predict_cohort does not match envelope authority".into(),
742 ));
743 }
744 let companion = request.phase.eq_ignore_ascii_case("fit_cv")
745 || request.phase.eq_ignore_ascii_case("refit");
746 if !(prediction || companion && cohort.role == crate::PredictCohortRole::ExternalTest) {
747 return Err(DataError::Validation(
748 "request phase cannot read this predict cohort".into(),
749 ));
750 }
751 return Ok(Some(
752 &envelope
753 .predict_cohort
754 .as_ref()
755 .expect("authority checked")
756 .relations,
757 ));
758 }
759 if prediction {
760 if let Some(cohort) = &envelope.predict_cohort {
761 return Ok(Some(&cohort.relations));
762 }
763 }
764 Ok(envelope.coordinator_relations.as_ref())
765}
766
767fn scoped_relations_for_materialization(
768 relations: &CoordinatorRelationSet,
769 request: &CoordinatorDataMaterializationRequest,
770) -> Result<CoordinatorRelationSet> {
771 relations.validate()?;
772 if request.source_ids.is_empty() {
773 return Ok(relations.clone());
774 }
775 let source_filter = request.source_ids.iter().collect::<BTreeSet<_>>();
776 let scoped = relations
777 .records
778 .iter()
779 .filter(|relation| {
780 relation
781 .source_id
782 .as_ref()
783 .map(|source_id| source_filter.contains(source_id))
784 .unwrap_or(false)
785 })
786 .cloned()
787 .collect::<Vec<_>>();
788 if scoped.is_empty() {
789 return Err(DataError::Validation(format!(
790 "materialization request `{}` on `{}` selected no coordinator relations for requested source ids",
791 request.input_name, request.node_id
792 )));
793 }
794 let scoped = CoordinatorRelationSet { records: scoped };
795 scoped.validate()?;
796 Ok(scoped)
797}
798
799fn validate_non_empty(label: &str, value: &str) -> Result<()> {
800 if value.trim().is_empty() {
801 return Err(DataError::Validation(format!("{label} must not be empty")));
802 }
803 Ok(())
804}
805
806fn validate_view(view: &DataView) -> Result<()> {
807 for (key, value) in [("partition", &view.partition), ("fold_id", &view.fold_id)] {
808 if let Some(value) = value {
809 validate_non_empty(key, value)?;
810 }
811 }
812 if let Some(samples) = &view.sample_ids {
813 let unique = samples.iter().collect::<BTreeSet<_>>();
814 if unique.len() != samples.len() {
815 return Err(DataError::Validation(
816 "data view contains duplicate sample ids".to_string(),
817 ));
818 }
819 }
820 if let Some(sources) = &view.source_ids {
821 let unique = sources.iter().collect::<BTreeSet<_>>();
822 if unique.len() != sources.len() {
823 return Err(DataError::Validation(
824 "data view contains duplicate source ids".to_string(),
825 ));
826 }
827 }
828 if let Some(columns) = &view.columns {
829 let unique = columns.iter().collect::<BTreeSet<_>>();
830 if unique.len() != columns.len() {
831 return Err(DataError::Validation(
832 "data view contains duplicate columns".to_string(),
833 ));
834 }
835 if columns.iter().any(|column| column.trim().is_empty()) {
836 return Err(DataError::Validation(
837 "data view contains an empty column".to_string(),
838 ));
839 }
840 }
841 if let Some(branch_view) = &view.branch_view {
842 branch_view.validate()?;
843 }
844 Ok(())
845}
846
847fn resolved_view_labels<'a>(
852 view: &'a DataView,
853 parent: &CoordinatorDataHandleRecord,
854) -> Result<Vec<(&'static str, &'a str)>> {
855 if view.sample_ids.is_some() {
856 return Ok(Vec::new());
857 }
858 let whole_handle = match view.partition.as_deref() {
859 Some("full_train") => true,
860 Some("predict") => {
861 if !parent.phase.eq_ignore_ascii_case("predict") {
862 return Err(DataError::Validation("partition=predict requires a PREDICT materialized handle or explicit sample_ids".into()));
863 }
864 true
865 }
866 _ => false,
867 };
868 let mut selectors = Vec::new();
869 if !whole_handle {
870 if let Some(partition) = &view.partition {
871 selectors.push(("partition", partition.as_str()));
872 }
873 }
874 if let Some(fold) = &view.fold_id {
875 if !whole_handle || parent.fold_id.as_ref() != Some(fold) {
876 selectors.push(("fold_id", fold.as_str()));
877 }
878 }
879 Ok(selectors)
880}
881
882fn filter_relations(
883 relations: &[CoordinatorRelation],
884 view: &DataView,
885) -> Result<Vec<CoordinatorRelation>> {
886 let sample_filter = view
887 .sample_ids
888 .as_ref()
889 .map(|sample_ids| sample_ids.iter().collect::<BTreeSet<_>>());
890 let source_filter = view
891 .source_ids
892 .as_ref()
893 .map(|source_ids| source_ids.iter().collect::<BTreeSet<_>>());
894 let mut branch_source_filter: Option<BTreeSet<&SourceId>> = None;
901 let mut branch_metadata_filter: Option<&BTreeMap<String, serde_json::Value>> = None;
902 let mut branch_tag_filter: Option<BTreeSet<&String>> = None;
903 let mut branch_native_filter = None;
904 if let Some(branch_view) = view.branch_view.as_ref() {
905 match branch_view.mode {
906 crate::coordinator::CoordinatorBranchViewMode::BySource => {
907 branch_source_filter = Some(
908 branch_view
909 .selector
910 .source_ids
911 .iter()
912 .collect::<BTreeSet<_>>(),
913 );
914 }
915 crate::coordinator::CoordinatorBranchViewMode::ByMetadata => {
919 branch_metadata_filter = Some(&branch_view.selector.metadata);
920 }
921 crate::coordinator::CoordinatorBranchViewMode::ByTag => {
922 branch_tag_filter = Some(branch_view.selector.tags.iter().collect::<BTreeSet<_>>());
923 }
924 crate::coordinator::CoordinatorBranchViewMode::Separation => {}
925 crate::coordinator::CoordinatorBranchViewMode::ByFilter => {
926 let filter = branch_view.selector.filter.as_ref().ok_or_else(|| {
927 DataError::Validation(format!(
928 "coordinator branch view `{}` mode=by_filter requires filter",
929 branch_view.view_id
930 ))
931 })?;
932 branch_native_filter = Some(parse_native_branch_view_filter(
933 filter,
934 &format!("coordinator branch view `{}`", branch_view.view_id),
935 )?);
936 }
937 }
938 }
939 let mut filtered = relations
940 .iter()
941 .enumerate()
942 .filter(|relation| {
943 let relation = relation.1;
944 sample_filter
945 .as_ref()
946 .map(|samples| samples.contains(&relation.sample_id))
947 .unwrap_or(true)
948 })
949 .filter(|relation| {
950 let relation = relation.1;
951 source_filter
952 .as_ref()
953 .map(|sources| {
954 relation
955 .source_id
956 .as_ref()
957 .map(|source_id| sources.contains(source_id))
958 .unwrap_or(false)
959 })
960 .unwrap_or(true)
961 })
962 .filter(|relation| {
963 let relation = relation.1;
964 branch_source_filter
965 .as_ref()
966 .map(|sources| {
967 relation
968 .source_id
969 .as_ref()
970 .map(|source_id| sources.contains(source_id))
971 .unwrap_or(false)
972 })
973 .unwrap_or(true)
974 })
975 .filter(|relation| {
976 let relation = relation.1;
977 branch_metadata_filter
978 .map(|selector| {
979 selector
980 .iter()
981 .all(|(key, value)| relation.metadata.get(key) == Some(value))
982 })
983 .unwrap_or(true)
984 })
985 .filter(|relation| {
986 let relation = relation.1;
987 branch_tag_filter
988 .as_ref()
989 .map(|tags| tags.iter().all(|tag| relation.tags.contains(tag)))
990 .unwrap_or(true)
991 })
992 .filter(|relation| {
993 let relation = relation.1;
994 branch_native_filter
995 .as_ref()
996 .map(|filter| {
997 filter
998 .metadata_equals
999 .iter()
1000 .all(|(key, value)| relation.metadata.get(key) == Some(value))
1001 && filter
1002 .tags_all
1003 .iter()
1004 .all(|tag| relation.tags.contains(tag))
1005 })
1006 .unwrap_or(true)
1007 })
1008 .filter(|relation| view.include_augmented || !relation.1.is_augmented)
1009 .filter(|relation| view.include_excluded || !relation.1.excluded)
1010 .map(|(idx, relation)| (idx, relation.clone()))
1011 .collect::<Vec<_>>();
1012 if filtered.is_empty() {
1013 return Err(DataError::Validation(
1014 "data view selected no coordinator relations".to_string(),
1015 ));
1016 }
1017 if let Some(sample_ids) = &view.sample_ids {
1018 let sample_order = sample_ids
1019 .iter()
1020 .enumerate()
1021 .map(|(idx, sample_id)| (sample_id, idx))
1022 .collect::<BTreeMap<_, _>>();
1023 filtered.sort_by_key(|(idx, relation)| {
1024 (
1025 sample_order
1026 .get(&relation.sample_id)
1027 .copied()
1028 .unwrap_or(usize::MAX),
1029 *idx,
1030 )
1031 });
1032 }
1033 Ok(filtered.into_iter().map(|(_, relation)| relation).collect())
1034}
1035
1036fn unique_sample_count(relations: &[CoordinatorRelation]) -> usize {
1037 relations
1038 .iter()
1039 .map(|relation| &relation.sample_id)
1040 .collect::<BTreeSet<_>>()
1041 .len()
1042}
1043
1044fn selected_feature_indices(
1045 table: &CoordinatorFeatureTable,
1046 view: &DataView,
1047) -> Result<Vec<usize>> {
1048 let index_by_name = table
1049 .feature_names
1050 .iter()
1051 .enumerate()
1052 .map(|(idx, name)| (name, idx))
1053 .collect::<BTreeMap<_, _>>();
1054 let indices = if let Some(columns) = &view.columns {
1055 columns
1056 .iter()
1057 .map(|column| {
1058 index_by_name.get(column).copied().ok_or_else(|| {
1059 DataError::Validation(format!(
1060 "feature table `{}` has no feature column `{}`",
1061 table.feature_set_id, column
1062 ))
1063 })
1064 })
1065 .collect::<Result<Vec<_>>>()?
1066 } else {
1067 (0..table.feature_names.len()).collect()
1068 };
1069 if indices.is_empty() {
1070 return Err(DataError::Validation(format!(
1071 "feature table `{}` selected no feature columns",
1072 table.feature_set_id
1073 )));
1074 }
1075 Ok(indices)
1076}
1077
1078#[cfg(test)]
1079mod tests {
1080 use super::*;
1081 use serde_json::json;
1082
1083 fn envelope() -> CoordinatorDataPlanEnvelope {
1084 serde_json::from_str(include_str!(
1085 "../../../examples/fixtures/oof_campaign/coordinator_data_plan_envelope_nir.json"
1086 ))
1087 .unwrap()
1088 }
1089
1090 fn request() -> CoordinatorDataMaterializationRequest {
1091 serde_json::from_str(include_str!(
1092 "../../../examples/fixtures/oof_campaign/materialization_request_model_base_x.json"
1093 ))
1094 .unwrap()
1095 }
1096
1097 #[test]
1098 fn materializes_validated_coordinator_handle_record() {
1099 let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1100 let record = arena.materialize(&envelope(), &request()).unwrap();
1101
1102 assert_eq!(record.handle.handle, 1);
1103 assert_eq!(record.handle.kind, CoordinatorHandleKind::Data);
1104 assert_eq!(record.input_name, "x");
1105 assert_eq!(record.plan_id, "nir-to-tabular");
1106 assert_eq!(record.sample_count, Some(2));
1107 assert_eq!(record.relation_record_count, Some(4));
1108 assert_eq!(arena.handle_record(1), Some(record));
1109 assert_eq!(arena.handle_records().len(), 1);
1110 }
1111
1112 #[test]
1113 fn view_labels_require_membership_but_preserve_host_resolved_views() {
1114 let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1115 let record = arena.materialize(&envelope(), &request()).unwrap();
1116 for value in [
1117 json!({"partition":"does-not-exist"}),
1118 json!({"fold_id":"does-not-exist"}),
1119 json!({"partition":"fold_train"}),
1120 json!({"partition":"predict"}),
1121 ] {
1122 let view: DataView = serde_json::from_value(value).unwrap();
1123 assert!(arena.make_view(record.handle.handle, &view).is_err());
1124 }
1125 assert!(serde_json::from_value::<DataView>(json!({"sampl_ids":["S001"]})).is_err());
1126 let explicit: DataView = serde_json::from_value(
1127 json!({"sample_ids":["S001"],"partition":"fold_train","fold_id":"fold0"}),
1128 )
1129 .unwrap();
1130 assert_eq!(
1131 arena
1132 .make_view(record.handle.handle, &explicit)
1133 .unwrap()
1134 .sample_count,
1135 1
1136 );
1137 let all: DataView = serde_json::from_value(json!({"partition":"full_train"})).unwrap();
1138 assert_eq!(
1139 arena
1140 .make_view(record.handle.handle, &all)
1141 .unwrap()
1142 .sample_count,
1143 2
1144 );
1145 let mut predict_request = request();
1146 predict_request.phase = "PREDICT".into();
1147 let predict = arena.materialize(&envelope(), &predict_request).unwrap();
1148 let view: DataView = serde_json::from_value(json!({"partition":"predict"})).unwrap();
1149 assert_eq!(
1150 arena
1151 .make_view(predict.handle.handle, &view)
1152 .unwrap()
1153 .sample_count,
1154 2
1155 );
1156 }
1157
1158 #[test]
1159 fn view_resolves_partition_and_fold_from_supplied_relation_metadata() {
1160 let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1161 let mut envelope = envelope();
1162 for relation in &mut envelope.coordinator_relations.as_mut().unwrap().records {
1163 relation.metadata.insert(
1164 "partition".into(),
1165 json!(if relation.sample_id.as_str() == "S001" {
1166 "train"
1167 } else {
1168 "validation"
1169 }),
1170 );
1171 relation.metadata.insert("fold_id".into(), json!("fold0"));
1172 }
1173 let record = arena.materialize(&envelope, &request()).unwrap();
1174 let view: DataView =
1175 serde_json::from_value(json!({"partition":"validation","fold_id":"fold0"})).unwrap();
1176 let selected = arena.make_view(record.handle.handle, &view).unwrap();
1177 assert_eq!(selected.sample_count, 1);
1178 assert!(arena
1179 .view_identity(selected.handle.handle)
1180 .unwrap()
1181 .records
1182 .iter()
1183 .all(|row| row.sample_id.as_str() == "S002"));
1184 }
1185
1186 #[test]
1187 fn materialization_refuses_fingerprint_mismatch() {
1188 let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1189 let mut request = request();
1190 request.plan_fingerprint = "0".repeat(64);
1191
1192 assert!(arena.materialize(&envelope(), &request).is_err());
1193 }
1194
1195 #[test]
1196 fn materialization_scopes_relations_to_requested_sources() {
1197 let mut envelope = envelope();
1198 let chem = SourceId::new("chem").unwrap();
1199 envelope.plan.steps.push(crate::plan::DataPlanStep {
1200 kind: crate::plan::DataPlanStepKind::Materialize,
1201 source_id: Some(chem.clone()),
1202 adapter_id: None,
1203 input_representation: None,
1204 output_representation: Some(RepresentationId::new("tabular_numeric").unwrap()),
1205 fit_scope: crate::plan::FitScope::Stateless,
1206 requires_user_choice: false,
1207 metadata: BTreeMap::new(),
1208 });
1209 envelope.plan_fingerprint = crate::data_plan_fingerprint(&envelope.plan).unwrap();
1210 envelope.relation_fingerprint = None;
1211 envelope
1212 .coordinator_relations
1213 .as_mut()
1214 .unwrap()
1215 .records
1216 .push(CoordinatorRelation {
1217 unit_level: crate::CoordinatorEntityUnitLevel::Observation,
1218 unit_id: None,
1219 rep_id: None,
1220 derived_unit_id: None,
1221 component_observation_ids: Vec::new(),
1222 sample_influence_weight: None,
1223 quality_flag: None,
1224 observation_id: ObservationId::new("chem.S001").unwrap(),
1225 sample_id: SampleId::new("S001").unwrap(),
1226 target_id: Some(TargetId::new("y").unwrap()),
1227 group_id: None,
1228 origin_sample_id: None,
1229 source_id: Some(chem.clone()),
1230 is_augmented: false,
1231 excluded: false,
1232 metadata: BTreeMap::new(),
1233 tags: Vec::new(),
1234 });
1235 envelope.validate().unwrap();
1236
1237 let mut request = request();
1238 request.plan_fingerprint = envelope.plan_fingerprint.clone();
1239 request.relation_fingerprint = None;
1240 request.require_relations = false;
1241 request.source_ids = vec![SourceId::new("nir").unwrap()];
1242
1243 let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1244 let data = arena.materialize(&envelope, &request).unwrap();
1245 let view = arena
1246 .make_view(data.handle.handle, &DataView::default())
1247 .unwrap();
1248 let identity = arena.view_identity(view.handle.handle).unwrap();
1249
1250 assert_eq!(data.relation_record_count, Some(4));
1251 assert_eq!(
1252 arena
1253 .data_identity(data.handle.handle)
1254 .unwrap()
1255 .records
1256 .len(),
1257 4
1258 );
1259 assert!(identity
1260 .records
1261 .iter()
1262 .all(|record| record.source_id.as_ref() == Some(&SourceId::new("nir").unwrap())));
1263 }
1264
1265 #[test]
1266 fn view_filters_augmented_rows_and_preserves_repetition_identity() {
1267 let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1268 let data = arena.materialize(&envelope(), &request()).unwrap();
1269 let view = DataView {
1270 sample_ids: Some(vec![SampleId::new("S001").unwrap()]),
1271 include_augmented: false,
1272 ..Default::default()
1273 };
1274
1275 let view_record = arena.make_view(data.handle.handle, &view).unwrap();
1276 let identity = arena.view_identity(view_record.handle.handle).unwrap();
1277
1278 assert_eq!(view_record.handle.kind, CoordinatorHandleKind::View);
1279 assert_eq!(view_record.sample_count, 1);
1280 assert_eq!(view_record.relation_record_count, 2);
1281 assert_eq!(identity.records.len(), 2);
1282 assert_eq!(identity.records[0].observation_id.as_str(), "obs.S001.base");
1283 assert_eq!(identity.records[1].observation_id.as_str(), "obs.S001.rep1");
1284 assert_eq!(
1285 arena.view_record(view_record.handle.handle),
1286 Some(view_record)
1287 );
1288 }
1289
1290 #[test]
1291 fn view_drops_excluded_rows_for_training_and_keeps_them_otherwise() {
1292 let mut envelope = envelope();
1296 for record in &mut envelope.coordinator_relations.as_mut().unwrap().records {
1297 if record.observation_id.as_str() == "obs.S002.base" {
1298 record.excluded = true;
1299 }
1300 }
1301 envelope.validate().unwrap();
1306
1307 let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1308 let data = arena.materialize(&envelope, &request()).unwrap();
1309
1310 let train_view = DataView {
1312 include_augmented: true,
1313 include_excluded: false,
1314 ..Default::default()
1315 };
1316 let train_record = arena.make_view(data.handle.handle, &train_view).unwrap();
1317 let train_identity = arena.view_identity(train_record.handle.handle).unwrap();
1318 assert!(
1319 train_identity
1320 .records
1321 .iter()
1322 .all(|record| record.sample_id.as_str() != "S002"),
1323 "excluded sample S002 must be absent from a training view"
1324 );
1325 assert_eq!(train_record.relation_record_count, 3);
1326
1327 let predict_view = DataView {
1329 include_augmented: true,
1330 include_excluded: true,
1331 ..Default::default()
1332 };
1333 let predict_record = arena.make_view(data.handle.handle, &predict_view).unwrap();
1334 let predict_identity = arena.view_identity(predict_record.handle.handle).unwrap();
1335 assert!(
1336 predict_identity
1337 .records
1338 .iter()
1339 .any(|record| record.sample_id.as_str() == "S002"),
1340 "excluded sample S002 must be present in a validation/predict view"
1341 );
1342 assert_eq!(predict_record.relation_record_count, 4);
1343 }
1344
1345 #[test]
1346 fn branch_view_by_source_filters_relations_to_branch_sources() {
1347 use crate::coordinator::{
1348 CoordinatorBranchView, CoordinatorBranchViewMode, CoordinatorBranchViewSelector,
1349 };
1350
1351 let mut envelope = envelope();
1352 let chem = SourceId::new("chem").unwrap();
1353 envelope.plan.steps.push(crate::plan::DataPlanStep {
1354 kind: crate::plan::DataPlanStepKind::Materialize,
1355 source_id: Some(chem.clone()),
1356 adapter_id: None,
1357 input_representation: None,
1358 output_representation: Some(RepresentationId::new("tabular_numeric").unwrap()),
1359 fit_scope: crate::plan::FitScope::Stateless,
1360 requires_user_choice: false,
1361 metadata: BTreeMap::new(),
1362 });
1363 envelope.plan_fingerprint = crate::data_plan_fingerprint(&envelope.plan).unwrap();
1364 envelope.relation_fingerprint = None;
1365 envelope
1366 .coordinator_relations
1367 .as_mut()
1368 .unwrap()
1369 .records
1370 .push(CoordinatorRelation {
1371 unit_level: crate::CoordinatorEntityUnitLevel::Observation,
1372 unit_id: None,
1373 rep_id: None,
1374 derived_unit_id: None,
1375 component_observation_ids: Vec::new(),
1376 sample_influence_weight: None,
1377 quality_flag: None,
1378 observation_id: ObservationId::new("chem.S001").unwrap(),
1379 sample_id: SampleId::new("S001").unwrap(),
1380 target_id: Some(TargetId::new("y").unwrap()),
1381 group_id: None,
1382 origin_sample_id: None,
1383 source_id: Some(chem.clone()),
1384 is_augmented: false,
1385 excluded: false,
1386 metadata: BTreeMap::new(),
1387 tags: Vec::new(),
1388 });
1389 envelope.validate().unwrap();
1390 let mut request = request();
1391 request.plan_fingerprint = envelope.plan_fingerprint.clone();
1392 request.relation_fingerprint = None;
1393 request.require_relations = false;
1394 request.source_ids = vec![SourceId::new("nir").unwrap(), chem.clone()];
1395
1396 let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1397 let data = arena.materialize(&envelope, &request).unwrap();
1398 let view = DataView {
1399 branch_view: Some(CoordinatorBranchView {
1400 view_id: "branch_view:nir".to_string(),
1401 branch_id: "branch:nir_only".to_string(),
1402 mode: CoordinatorBranchViewMode::BySource,
1403 selector: CoordinatorBranchViewSelector {
1404 source_ids: vec![SourceId::new("nir").unwrap()],
1405 ..Default::default()
1406 },
1407 allow_overlap: false,
1408 metadata: BTreeMap::new(),
1409 }),
1410 include_augmented: true,
1411 ..Default::default()
1412 };
1413 let view_record = arena.make_view(data.handle.handle, &view).unwrap();
1414 let identity = arena.view_identity(view_record.handle.handle).unwrap();
1415
1416 assert!(identity
1417 .records
1418 .iter()
1419 .all(|record| record.source_id.as_ref() == Some(&SourceId::new("nir").unwrap())));
1420 assert!(identity.records.iter().any(|record| record
1421 .observation_id
1422 .as_str()
1423 .starts_with("obs.S001")
1424 || record.observation_id.as_str().starts_with("obs.S002")));
1425 }
1426
1427 #[test]
1428 fn branch_view_separation_does_not_restrict_relations() {
1429 use crate::coordinator::{
1430 CoordinatorBranchView, CoordinatorBranchViewMode, CoordinatorBranchViewSelector,
1431 };
1432
1433 let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1434 let data = arena.materialize(&envelope(), &request()).unwrap();
1435 let view = DataView {
1436 branch_view: Some(CoordinatorBranchView {
1437 view_id: "branch_view:separation".to_string(),
1438 branch_id: "branch:0".to_string(),
1439 mode: CoordinatorBranchViewMode::Separation,
1440 selector: CoordinatorBranchViewSelector {
1441 tags: vec!["clean".to_string()],
1442 ..Default::default()
1443 },
1444 allow_overlap: false,
1445 metadata: BTreeMap::new(),
1446 }),
1447 include_augmented: true,
1448 ..Default::default()
1449 };
1450 let view_record = arena.make_view(data.handle.handle, &view).unwrap();
1451 let identity = arena.view_identity(view_record.handle.handle).unwrap();
1452 assert!(!identity.records.is_empty());
1453 }
1454
1455 fn tagged_envelope() -> CoordinatorDataPlanEnvelope {
1463 let mut envelope = envelope();
1464 for record in &mut envelope.coordinator_relations.as_mut().unwrap().records {
1465 let (group, tag) = if record.sample_id.as_str() == "S002" {
1466 ("B", "dirty")
1467 } else {
1468 ("A", "clean")
1469 };
1470 record
1471 .metadata
1472 .insert("group".to_string(), serde_json::json!(group));
1473 record.tags = vec![tag.to_string()];
1474 }
1475 envelope.validate().unwrap();
1476 envelope
1477 }
1478
1479 #[test]
1480 fn branch_view_by_metadata_filters_relations_natively() {
1481 use crate::coordinator::{
1482 CoordinatorBranchView, CoordinatorBranchViewMode, CoordinatorBranchViewSelector,
1483 };
1484
1485 let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1486 let data = arena.materialize(&tagged_envelope(), &request()).unwrap();
1487
1488 let by_metadata = |group: &str| CoordinatorBranchView {
1489 view_id: format!("branch_view:group_{group}"),
1490 branch_id: format!("branch:group_{group}"),
1491 mode: CoordinatorBranchViewMode::ByMetadata,
1492 selector: CoordinatorBranchViewSelector {
1493 metadata: BTreeMap::from([("group".to_string(), serde_json::json!(group))]),
1494 ..Default::default()
1495 },
1496 allow_overlap: false,
1497 metadata: BTreeMap::new(),
1498 };
1499
1500 let view_a = DataView {
1502 branch_view: Some(by_metadata("A")),
1503 include_augmented: true,
1504 ..Default::default()
1505 };
1506 let record_a = arena.make_view(data.handle.handle, &view_a).unwrap();
1507 let identity_a = arena.view_identity(record_a.handle.handle).unwrap();
1508 assert!(
1509 identity_a
1510 .records
1511 .iter()
1512 .all(|record| record.sample_id.as_str() == "S001"),
1513 "by_metadata group=A must include only S001 relations"
1514 );
1515 assert!(
1516 identity_a
1517 .records
1518 .iter()
1519 .any(|record| record.sample_id.as_str() == "S001"),
1520 "by_metadata group=A must keep the matching S001 relations"
1521 );
1522
1523 let view_b = DataView {
1525 branch_view: Some(by_metadata("B")),
1526 include_augmented: true,
1527 ..Default::default()
1528 };
1529 let record_b = arena.make_view(data.handle.handle, &view_b).unwrap();
1530 let identity_b = arena.view_identity(record_b.handle.handle).unwrap();
1531 assert!(
1532 identity_b
1533 .records
1534 .iter()
1535 .all(|record| record.sample_id.as_str() == "S002"),
1536 "by_metadata group=B must exclude S001 relations"
1537 );
1538 }
1539
1540 #[test]
1541 fn branch_view_by_tag_filters_relations_natively() {
1542 use crate::coordinator::{
1543 CoordinatorBranchView, CoordinatorBranchViewMode, CoordinatorBranchViewSelector,
1544 };
1545
1546 let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1547 let data = arena.materialize(&tagged_envelope(), &request()).unwrap();
1548
1549 let by_tag = |tag: &str| CoordinatorBranchView {
1550 view_id: format!("branch_view:tag_{tag}"),
1551 branch_id: format!("branch:tag_{tag}"),
1552 mode: CoordinatorBranchViewMode::ByTag,
1553 selector: CoordinatorBranchViewSelector {
1554 tags: vec![tag.to_string()],
1555 ..Default::default()
1556 },
1557 allow_overlap: false,
1558 metadata: BTreeMap::new(),
1559 };
1560
1561 let view_clean = DataView {
1563 branch_view: Some(by_tag("clean")),
1564 include_augmented: true,
1565 ..Default::default()
1566 };
1567 let record_clean = arena.make_view(data.handle.handle, &view_clean).unwrap();
1568 let identity_clean = arena.view_identity(record_clean.handle.handle).unwrap();
1569 assert!(
1570 identity_clean
1571 .records
1572 .iter()
1573 .all(|record| record.sample_id.as_str() == "S001"),
1574 "by_tag clean must include only S001 relations"
1575 );
1576
1577 let view_dirty = DataView {
1579 branch_view: Some(by_tag("dirty")),
1580 include_augmented: true,
1581 ..Default::default()
1582 };
1583 let record_dirty = arena.make_view(data.handle.handle, &view_dirty).unwrap();
1584 let identity_dirty = arena.view_identity(record_dirty.handle.handle).unwrap();
1585 assert!(
1586 identity_dirty
1587 .records
1588 .iter()
1589 .all(|record| record.sample_id.as_str() == "S002"),
1590 "by_tag dirty must exclude S001 relations"
1591 );
1592 }
1593
1594 #[test]
1595 fn branch_view_by_filter_filters_closed_metadata_and_tag_predicates() {
1596 use crate::coordinator::{
1597 CoordinatorBranchView, CoordinatorBranchViewMode, CoordinatorBranchViewSelector,
1598 };
1599
1600 let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1601 let data = arena.materialize(&tagged_envelope(), &request()).unwrap();
1602 let view = DataView {
1603 branch_view: Some(CoordinatorBranchView {
1604 view_id: "branch_view:filter_A_clean".to_string(),
1605 branch_id: "branch:filter_A_clean".to_string(),
1606 mode: CoordinatorBranchViewMode::ByFilter,
1607 selector: CoordinatorBranchViewSelector {
1608 filter: Some(serde_json::json!({
1609 "metadata_equals": {"group": "A"},
1610 "tags_all": ["clean"]
1611 })),
1612 ..Default::default()
1613 },
1614 allow_overlap: false,
1615 metadata: BTreeMap::new(),
1616 }),
1617 include_augmented: true,
1618 ..Default::default()
1619 };
1620 let record = arena.make_view(data.handle.handle, &view).unwrap();
1621 let identity = arena.view_identity(record.handle.handle).unwrap();
1622 assert!(
1623 identity
1624 .records
1625 .iter()
1626 .all(|relation| relation.sample_id.as_str() == "S001"),
1627 "closed by_filter must retain exactly the matching relations"
1628 );
1629 }
1630
1631 #[test]
1632 fn branch_view_empty_partition_intersection_raises_a_clear_error() {
1633 use crate::coordinator::{
1638 CoordinatorBranchView, CoordinatorBranchViewMode, CoordinatorBranchViewSelector,
1639 };
1640
1641 let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1642 let envelope = tagged_envelope();
1643 let data = arena.materialize(&envelope, &request()).unwrap();
1644
1645 let s001_samples: Vec<SampleId> = envelope
1648 .coordinator_relations
1649 .as_ref()
1650 .unwrap()
1651 .records
1652 .iter()
1653 .filter(|record| record.sample_id.as_str() == "S001")
1654 .map(|record| record.sample_id.clone())
1655 .collect::<BTreeSet<_>>()
1656 .into_iter()
1657 .collect();
1658 assert!(!s001_samples.is_empty(), "fixture must carry S001 samples");
1659
1660 let view = DataView {
1661 branch_view: Some(CoordinatorBranchView {
1662 view_id: "branch_view:group_B".to_string(),
1663 branch_id: "branch:group_B".to_string(),
1664 mode: CoordinatorBranchViewMode::ByMetadata,
1665 selector: CoordinatorBranchViewSelector {
1666 metadata: BTreeMap::from([("group".to_string(), serde_json::json!("B"))]),
1667 ..Default::default()
1668 },
1669 allow_overlap: false,
1670 metadata: BTreeMap::new(),
1671 }),
1672 sample_ids: Some(s001_samples),
1673 include_augmented: true,
1674 ..Default::default()
1675 };
1676 let error = arena
1677 .make_view(data.handle.handle, &view)
1678 .unwrap_err()
1679 .to_string();
1680 assert!(
1681 error.contains("selected no coordinator relations"),
1682 "empty branch ∩ fold must raise a clear error: {error}"
1683 );
1684 }
1685
1686 #[test]
1687 fn branch_view_by_filter_refuses_unknown_predicates_before_selection() {
1688 use crate::coordinator::{
1689 CoordinatorBranchView, CoordinatorBranchViewMode, CoordinatorBranchViewSelector,
1690 };
1691
1692 let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1693 let data = arena.materialize(&envelope(), &request()).unwrap();
1694 let view = DataView {
1695 branch_view: Some(CoordinatorBranchView {
1696 view_id: "branch_view:by_filter".to_string(),
1697 branch_id: "branch:0".to_string(),
1698 mode: CoordinatorBranchViewMode::ByFilter,
1699 selector: CoordinatorBranchViewSelector {
1700 filter: Some(serde_json::json!({"op": "always"})),
1701 ..Default::default()
1702 },
1703 allow_overlap: false,
1704 metadata: BTreeMap::new(),
1705 }),
1706 include_augmented: true,
1707 ..Default::default()
1708 };
1709 let error = arena
1710 .make_view(data.handle.handle, &view)
1711 .expect_err("unknown by_filter predicates must fail closed");
1712 let message = format!("{error}");
1713 assert!(
1714 message.contains("native predicate"),
1715 "expected native-predicate validation error, got: {message}"
1716 );
1717 }
1718
1719 #[test]
1720 fn target_values_are_sample_level_and_dedup_repetitions() {
1721 let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1722 let data = arena.materialize(&envelope(), &request()).unwrap();
1723 let view = DataView {
1724 sample_ids: Some(vec![SampleId::new("S001").unwrap()]),
1725 include_augmented: false,
1726 ..Default::default()
1727 };
1728 let view_record = arena.make_view(data.handle.handle, &view).unwrap();
1729 let target_table = CoordinatorTargetTable {
1730 target_id: TargetId::new("y").unwrap(),
1731 values: vec![
1732 CoordinatorTargetValue {
1733 sample_id: SampleId::new("S001").unwrap(),
1734 value: json!(42.0),
1735 },
1736 CoordinatorTargetValue {
1737 sample_id: SampleId::new("S002").unwrap(),
1738 value: json!(7.0),
1739 },
1740 ],
1741 };
1742
1743 let target = arena
1744 .target_values(view_record.handle.handle, &target_table)
1745 .unwrap();
1746
1747 assert_eq!(target.target_id.as_str(), "y");
1748 assert_eq!(target.sample_ids, vec![SampleId::new("S001").unwrap()]);
1749 assert_eq!(target.values, vec![json!(42.0)]);
1750 }
1751
1752 #[test]
1753 fn multi_target_values_align_samples_and_emit_validity_masks() {
1754 let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1755 let data = arena.materialize(&envelope(), &request()).unwrap();
1756 let view = DataView {
1757 sample_ids: Some(vec![
1758 SampleId::new("S002").unwrap(),
1759 SampleId::new("S001").unwrap(),
1760 ]),
1761 include_augmented: false,
1762 ..Default::default()
1763 };
1764 let view_record = arena.make_view(data.handle.handle, &view).unwrap();
1765 let y = CoordinatorTargetTable {
1766 target_id: TargetId::new("y").unwrap(),
1767 values: vec![
1768 CoordinatorTargetValue {
1769 sample_id: SampleId::new("S001").unwrap(),
1770 value: json!(42.0),
1771 },
1772 CoordinatorTargetValue {
1773 sample_id: SampleId::new("S002").unwrap(),
1774 value: json!(7.0),
1775 },
1776 ],
1777 };
1778 let protein = CoordinatorTargetTable {
1779 target_id: TargetId::new("protein").unwrap(),
1780 values: vec![CoordinatorTargetValue {
1781 sample_id: SampleId::new("S001").unwrap(),
1782 value: json!(12.5),
1783 }],
1784 };
1785
1786 let block = arena
1787 .multi_target_values(view_record.handle.handle, &[y, protein])
1788 .unwrap();
1789
1790 assert_eq!(
1791 block.target_ids,
1792 vec![
1793 TargetId::new("y").unwrap(),
1794 TargetId::new("protein").unwrap()
1795 ]
1796 );
1797 assert_eq!(
1798 block.sample_ids,
1799 vec![
1800 SampleId::new("S002").unwrap(),
1801 SampleId::new("S001").unwrap()
1802 ]
1803 );
1804 assert_eq!(block.values[0], vec![json!(7.0), json!(42.0)]);
1805 assert_eq!(block.validity_masks[0], vec![true, true]);
1806 assert_eq!(block.values[1], vec![json!(null), json!(12.5)]);
1807 assert_eq!(block.validity_masks[1], vec![false, true]);
1808 }
1809
1810 #[test]
1811 fn feature_values_are_observation_level_and_filter_columns() {
1812 let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1813 let data = arena.materialize(&envelope(), &request()).unwrap();
1814 let view = DataView {
1815 sample_ids: Some(vec![SampleId::new("S001").unwrap()]),
1816 columns: Some(vec!["f1".to_string()]),
1817 include_augmented: false,
1818 ..Default::default()
1819 };
1820 let view_record = arena.make_view(data.handle.handle, &view).unwrap();
1821 let feature_table = CoordinatorFeatureTable {
1822 feature_set_id: "x".to_string(),
1823 representation_id: RepresentationId::new("tabular_numeric").unwrap(),
1824 feature_names: vec!["f0".to_string(), "f1".to_string()],
1825 rows: vec![
1826 CoordinatorFeatureRow {
1827 observation_id: ObservationId::new("obs.S001.base").unwrap(),
1828 values: vec![json!(1.0), json!(10.0)],
1829 },
1830 CoordinatorFeatureRow {
1831 observation_id: ObservationId::new("obs.S001.rep1").unwrap(),
1832 values: vec![json!(2.0), json!(20.0)],
1833 },
1834 CoordinatorFeatureRow {
1835 observation_id: ObservationId::new("obs.S001.aug0").unwrap(),
1836 values: vec![json!(3.0), json!(30.0)],
1837 },
1838 CoordinatorFeatureRow {
1839 observation_id: ObservationId::new("obs.S002.base").unwrap(),
1840 values: vec![json!(4.0), json!(40.0)],
1841 },
1842 ],
1843 };
1844
1845 let features = arena
1846 .feature_values(view_record.handle.handle, &feature_table)
1847 .unwrap();
1848
1849 assert_eq!(features.feature_set_id, "x");
1850 assert_eq!(features.feature_names, vec!["f1".to_string()]);
1851 assert_eq!(features.representation_id.as_str(), "tabular_numeric");
1852 assert_eq!(
1853 features.observation_ids,
1854 vec![
1855 ObservationId::new("obs.S001.base").unwrap(),
1856 ObservationId::new("obs.S001.rep1").unwrap(),
1857 ]
1858 );
1859 assert_eq!(
1860 features.sample_ids,
1861 vec![
1862 SampleId::new("S001").unwrap(),
1863 SampleId::new("S001").unwrap()
1864 ]
1865 );
1866 assert_eq!(features.values, vec![vec![json!(10.0)], vec![json!(20.0)]]);
1867
1868 let mut wrong_representation = feature_table;
1869 wrong_representation.representation_id = RepresentationId::new("dense_signal").unwrap();
1870 assert!(arena
1871 .feature_values(view_record.handle.handle, &wrong_representation)
1872 .is_err());
1873 }
1874
1875 #[test]
1876 fn view_honors_requested_sample_order_for_identity_targets_and_features() {
1877 let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1878 let data = arena.materialize(&envelope(), &request()).unwrap();
1879 let view = DataView {
1880 sample_ids: Some(vec![
1881 SampleId::new("S002").unwrap(),
1882 SampleId::new("S001").unwrap(),
1883 ]),
1884 include_augmented: false,
1885 ..Default::default()
1886 };
1887 let view_record = arena.make_view(data.handle.handle, &view).unwrap();
1888
1889 let identity = arena.view_identity(view_record.handle.handle).unwrap();
1890 assert_eq!(
1891 identity
1892 .records
1893 .iter()
1894 .map(|relation| relation.observation_id.as_str())
1895 .collect::<Vec<_>>(),
1896 vec!["obs.S002.base", "obs.S001.base", "obs.S001.rep1"]
1897 );
1898
1899 let target_table = CoordinatorTargetTable {
1900 target_id: TargetId::new("y").unwrap(),
1901 values: vec![
1902 CoordinatorTargetValue {
1903 sample_id: SampleId::new("S001").unwrap(),
1904 value: json!(42.0),
1905 },
1906 CoordinatorTargetValue {
1907 sample_id: SampleId::new("S002").unwrap(),
1908 value: json!(7.0),
1909 },
1910 ],
1911 };
1912 let target = arena
1913 .target_values(view_record.handle.handle, &target_table)
1914 .unwrap();
1915 assert_eq!(
1916 target.sample_ids,
1917 vec![
1918 SampleId::new("S002").unwrap(),
1919 SampleId::new("S001").unwrap()
1920 ]
1921 );
1922 assert_eq!(target.values, vec![json!(7.0), json!(42.0)]);
1923
1924 let feature_table = CoordinatorFeatureTable {
1925 feature_set_id: "x".to_string(),
1926 representation_id: RepresentationId::new("tabular_numeric").unwrap(),
1927 feature_names: vec!["f0".to_string(), "f1".to_string()],
1928 rows: vec![
1929 CoordinatorFeatureRow {
1930 observation_id: ObservationId::new("obs.S001.base").unwrap(),
1931 values: vec![json!(1.0), json!(10.0)],
1932 },
1933 CoordinatorFeatureRow {
1934 observation_id: ObservationId::new("obs.S001.rep1").unwrap(),
1935 values: vec![json!(2.0), json!(20.0)],
1936 },
1937 CoordinatorFeatureRow {
1938 observation_id: ObservationId::new("obs.S002.base").unwrap(),
1939 values: vec![json!(4.0), json!(40.0)],
1940 },
1941 ],
1942 };
1943 let features = arena
1944 .feature_values(view_record.handle.handle, &feature_table)
1945 .unwrap();
1946 assert_eq!(
1947 features.observation_ids,
1948 vec![
1949 ObservationId::new("obs.S002.base").unwrap(),
1950 ObservationId::new("obs.S001.base").unwrap(),
1951 ObservationId::new("obs.S001.rep1").unwrap(),
1952 ]
1953 );
1954 assert_eq!(
1955 features.values,
1956 vec![
1957 vec![json!(4.0), json!(40.0)],
1958 vec![json!(1.0), json!(10.0)],
1959 vec![json!(2.0), json!(20.0)],
1960 ]
1961 );
1962 }
1963
1964 #[test]
1965 fn release_data_handle_releases_child_views() {
1966 let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1967 let data = arena.materialize(&envelope(), &request()).unwrap();
1968 let view_record = arena
1969 .make_view(data.handle.handle, &DataView::default())
1970 .unwrap();
1971
1972 assert!(arena.release_handle(data.handle.handle));
1973 assert_eq!(arena.handle_record(data.handle.handle), None);
1974 assert_eq!(arena.view_record(view_record.handle.handle), None);
1975 let error = arena.view_identity(view_record.handle.handle).unwrap_err();
1976 assert_eq!(error.category(), "runtime");
1977 assert_eq!(error.code(), "unknown_handle");
1978 assert_eq!(error.error_code(), 0x0001_0001);
1979 assert!(!arena.release_handle(data.handle.handle));
1980 }
1981}