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