Skip to main content

dag_ml_data_core/
handle.rs

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    /// Target-major values: `values[target_idx][sample_idx]`.
157    pub values: Vec<Vec<serde_json::Value>>,
158    /// Target-major validity masks aligned with `values`.
159    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/// Typed sibling of [`CoordinatorFeatureBlock`]: the same projection with the
234/// cells flattened row-major into one `Vec<f64>` instead of per-cell JSON
235/// values. Produced by the `*_f64` projection path so a large numeric block
236/// never allocates `rows × cols` boxed [`serde_json::Value`]s. Masked cells
237/// are rejected by that path (it has no `Null` to project them into).
238#[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        // A handle is only "unknown" if it is absent from the data-handle
395        // registry. A live handle materialized without scoped relations is
396        // present in `records` but has no `data_relations` entry — that is a
397        // missing-relations contract error, not an unknown/stale handle.
398        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    // When both `view.source_ids` and `view.branch_view` constrain sources, the
803    // two are composed as intersection: a relation must pass both filters.
804    // `source_ids` is the materialization scope (sources loaded into the parent
805    // handle); the branch selector is the subset this branch cares about.
806    // Intersection prevents a branch from silently widening past what was
807    // actually materialized.
808    let branch_source_filter = match view.branch_view.as_ref() {
809        Some(branch_view) => match branch_view.mode {
810            crate::coordinator::CoordinatorBranchViewMode::BySource => Some(
811                branch_view
812                    .selector
813                    .source_ids
814                    .iter()
815                    .collect::<BTreeSet<_>>(),
816            ),
817            crate::coordinator::CoordinatorBranchViewMode::Separation => None,
818            other_mode => {
819                return Err(DataError::Validation(format!(
820                    "coordinator branch view `{}` mode={:?} requires host-side filtering; \
821                     in-memory arena only natively executes by_source",
822                    branch_view.view_id, other_mode,
823                )));
824            }
825        },
826        None => None,
827    };
828    let mut filtered = relations
829        .iter()
830        .enumerate()
831        .filter(|relation| {
832            let relation = relation.1;
833            sample_filter
834                .as_ref()
835                .map(|samples| samples.contains(&relation.sample_id))
836                .unwrap_or(true)
837        })
838        .filter(|relation| {
839            let relation = relation.1;
840            source_filter
841                .as_ref()
842                .map(|sources| {
843                    relation
844                        .source_id
845                        .as_ref()
846                        .map(|source_id| sources.contains(source_id))
847                        .unwrap_or(false)
848                })
849                .unwrap_or(true)
850        })
851        .filter(|relation| {
852            let relation = relation.1;
853            branch_source_filter
854                .as_ref()
855                .map(|sources| {
856                    relation
857                        .source_id
858                        .as_ref()
859                        .map(|source_id| sources.contains(source_id))
860                        .unwrap_or(false)
861                })
862                .unwrap_or(true)
863        })
864        .filter(|relation| view.include_augmented || !relation.1.is_augmented)
865        .map(|(idx, relation)| (idx, relation.clone()))
866        .collect::<Vec<_>>();
867    if filtered.is_empty() {
868        return Err(DataError::Validation(
869            "data view selected no coordinator relations".to_string(),
870        ));
871    }
872    if let Some(sample_ids) = &view.sample_ids {
873        let sample_order = sample_ids
874            .iter()
875            .enumerate()
876            .map(|(idx, sample_id)| (sample_id, idx))
877            .collect::<BTreeMap<_, _>>();
878        filtered.sort_by_key(|(idx, relation)| {
879            (
880                sample_order
881                    .get(&relation.sample_id)
882                    .copied()
883                    .unwrap_or(usize::MAX),
884                *idx,
885            )
886        });
887    }
888    Ok(filtered.into_iter().map(|(_, relation)| relation).collect())
889}
890
891fn unique_sample_count(relations: &[CoordinatorRelation]) -> usize {
892    relations
893        .iter()
894        .map(|relation| &relation.sample_id)
895        .collect::<BTreeSet<_>>()
896        .len()
897}
898
899fn selected_feature_indices(
900    table: &CoordinatorFeatureTable,
901    view: &DataView,
902) -> Result<Vec<usize>> {
903    let index_by_name = table
904        .feature_names
905        .iter()
906        .enumerate()
907        .map(|(idx, name)| (name, idx))
908        .collect::<BTreeMap<_, _>>();
909    let indices = if let Some(columns) = &view.columns {
910        columns
911            .iter()
912            .map(|column| {
913                index_by_name.get(column).copied().ok_or_else(|| {
914                    DataError::Validation(format!(
915                        "feature table `{}` has no feature column `{}`",
916                        table.feature_set_id, column
917                    ))
918                })
919            })
920            .collect::<Result<Vec<_>>>()?
921    } else {
922        (0..table.feature_names.len()).collect()
923    };
924    if indices.is_empty() {
925        return Err(DataError::Validation(format!(
926            "feature table `{}` selected no feature columns",
927            table.feature_set_id
928        )));
929    }
930    Ok(indices)
931}
932
933#[cfg(test)]
934mod tests {
935    use super::*;
936    use serde_json::json;
937
938    fn envelope() -> CoordinatorDataPlanEnvelope {
939        serde_json::from_str(include_str!(
940            "../../../examples/fixtures/oof_campaign/coordinator_data_plan_envelope_nir.json"
941        ))
942        .unwrap()
943    }
944
945    fn request() -> CoordinatorDataMaterializationRequest {
946        serde_json::from_str(include_str!(
947            "../../../examples/fixtures/oof_campaign/materialization_request_model_base_x.json"
948        ))
949        .unwrap()
950    }
951
952    #[test]
953    fn materializes_validated_coordinator_handle_record() {
954        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
955        let record = arena.materialize(&envelope(), &request()).unwrap();
956
957        assert_eq!(record.handle.handle, 1);
958        assert_eq!(record.handle.kind, CoordinatorHandleKind::Data);
959        assert_eq!(record.input_name, "x");
960        assert_eq!(record.plan_id, "nir-to-tabular");
961        assert_eq!(record.sample_count, Some(2));
962        assert_eq!(record.relation_record_count, Some(4));
963        assert_eq!(arena.handle_record(1), Some(record));
964        assert_eq!(arena.handle_records().len(), 1);
965    }
966
967    #[test]
968    fn materialization_refuses_fingerprint_mismatch() {
969        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
970        let mut request = request();
971        request.plan_fingerprint = "0".repeat(64);
972
973        assert!(arena.materialize(&envelope(), &request).is_err());
974    }
975
976    #[test]
977    fn materialization_scopes_relations_to_requested_sources() {
978        let mut envelope = envelope();
979        let chem = SourceId::new("chem").unwrap();
980        envelope.plan.steps.push(crate::plan::DataPlanStep {
981            kind: crate::plan::DataPlanStepKind::Materialize,
982            source_id: Some(chem.clone()),
983            adapter_id: None,
984            input_representation: None,
985            output_representation: Some(RepresentationId::new("tabular_numeric").unwrap()),
986            fit_scope: crate::plan::FitScope::Stateless,
987            requires_user_choice: false,
988            metadata: BTreeMap::new(),
989        });
990        envelope.plan_fingerprint = crate::data_plan_fingerprint(&envelope.plan).unwrap();
991        envelope.relation_fingerprint = None;
992        envelope
993            .coordinator_relations
994            .as_mut()
995            .unwrap()
996            .records
997            .push(CoordinatorRelation {
998                observation_id: ObservationId::new("chem.S001").unwrap(),
999                sample_id: SampleId::new("S001").unwrap(),
1000                target_id: Some(TargetId::new("y").unwrap()),
1001                group_id: None,
1002                origin_sample_id: None,
1003                source_id: Some(chem.clone()),
1004                is_augmented: false,
1005            });
1006        envelope.validate().unwrap();
1007
1008        let mut request = request();
1009        request.plan_fingerprint = envelope.plan_fingerprint.clone();
1010        request.relation_fingerprint = None;
1011        request.require_relations = false;
1012        request.source_ids = vec![SourceId::new("nir").unwrap()];
1013
1014        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1015        let data = arena.materialize(&envelope, &request).unwrap();
1016        let view = arena
1017            .make_view(data.handle.handle, &DataView::default())
1018            .unwrap();
1019        let identity = arena.view_identity(view.handle.handle).unwrap();
1020
1021        assert_eq!(data.relation_record_count, Some(4));
1022        assert_eq!(
1023            arena
1024                .data_identity(data.handle.handle)
1025                .unwrap()
1026                .records
1027                .len(),
1028            4
1029        );
1030        assert!(identity
1031            .records
1032            .iter()
1033            .all(|record| record.source_id.as_ref() == Some(&SourceId::new("nir").unwrap())));
1034    }
1035
1036    #[test]
1037    fn view_filters_augmented_rows_and_preserves_repetition_identity() {
1038        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1039        let data = arena.materialize(&envelope(), &request()).unwrap();
1040        let view = DataView {
1041            sample_ids: Some(vec![SampleId::new("S001").unwrap()]),
1042            include_augmented: false,
1043            ..Default::default()
1044        };
1045
1046        let view_record = arena.make_view(data.handle.handle, &view).unwrap();
1047        let identity = arena.view_identity(view_record.handle.handle).unwrap();
1048
1049        assert_eq!(view_record.handle.kind, CoordinatorHandleKind::View);
1050        assert_eq!(view_record.sample_count, 1);
1051        assert_eq!(view_record.relation_record_count, 2);
1052        assert_eq!(identity.records.len(), 2);
1053        assert_eq!(identity.records[0].observation_id.as_str(), "obs.S001.base");
1054        assert_eq!(identity.records[1].observation_id.as_str(), "obs.S001.rep1");
1055        assert_eq!(
1056            arena.view_record(view_record.handle.handle),
1057            Some(view_record)
1058        );
1059    }
1060
1061    #[test]
1062    fn branch_view_by_source_filters_relations_to_branch_sources() {
1063        use crate::coordinator::{
1064            CoordinatorBranchView, CoordinatorBranchViewMode, CoordinatorBranchViewSelector,
1065        };
1066
1067        let mut envelope = envelope();
1068        let chem = SourceId::new("chem").unwrap();
1069        envelope.plan.steps.push(crate::plan::DataPlanStep {
1070            kind: crate::plan::DataPlanStepKind::Materialize,
1071            source_id: Some(chem.clone()),
1072            adapter_id: None,
1073            input_representation: None,
1074            output_representation: Some(RepresentationId::new("tabular_numeric").unwrap()),
1075            fit_scope: crate::plan::FitScope::Stateless,
1076            requires_user_choice: false,
1077            metadata: BTreeMap::new(),
1078        });
1079        envelope.plan_fingerprint = crate::data_plan_fingerprint(&envelope.plan).unwrap();
1080        envelope.relation_fingerprint = None;
1081        envelope
1082            .coordinator_relations
1083            .as_mut()
1084            .unwrap()
1085            .records
1086            .push(CoordinatorRelation {
1087                observation_id: ObservationId::new("chem.S001").unwrap(),
1088                sample_id: SampleId::new("S001").unwrap(),
1089                target_id: Some(TargetId::new("y").unwrap()),
1090                group_id: None,
1091                origin_sample_id: None,
1092                source_id: Some(chem.clone()),
1093                is_augmented: false,
1094            });
1095        envelope.validate().unwrap();
1096        let mut request = request();
1097        request.plan_fingerprint = envelope.plan_fingerprint.clone();
1098        request.relation_fingerprint = None;
1099        request.require_relations = false;
1100        request.source_ids = vec![SourceId::new("nir").unwrap(), chem.clone()];
1101
1102        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1103        let data = arena.materialize(&envelope, &request).unwrap();
1104        let view = DataView {
1105            branch_view: Some(CoordinatorBranchView {
1106                view_id: "branch_view:nir".to_string(),
1107                branch_id: "branch:nir_only".to_string(),
1108                mode: CoordinatorBranchViewMode::BySource,
1109                selector: CoordinatorBranchViewSelector {
1110                    source_ids: vec![SourceId::new("nir").unwrap()],
1111                    ..Default::default()
1112                },
1113                allow_overlap: false,
1114                metadata: BTreeMap::new(),
1115            }),
1116            include_augmented: true,
1117            ..Default::default()
1118        };
1119        let view_record = arena.make_view(data.handle.handle, &view).unwrap();
1120        let identity = arena.view_identity(view_record.handle.handle).unwrap();
1121
1122        assert!(identity
1123            .records
1124            .iter()
1125            .all(|record| record.source_id.as_ref() == Some(&SourceId::new("nir").unwrap())));
1126        assert!(identity.records.iter().any(|record| record
1127            .observation_id
1128            .as_str()
1129            .starts_with("obs.S001")
1130            || record.observation_id.as_str().starts_with("obs.S002")));
1131    }
1132
1133    #[test]
1134    fn branch_view_separation_does_not_restrict_relations() {
1135        use crate::coordinator::{
1136            CoordinatorBranchView, CoordinatorBranchViewMode, CoordinatorBranchViewSelector,
1137        };
1138
1139        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1140        let data = arena.materialize(&envelope(), &request()).unwrap();
1141        let view = DataView {
1142            branch_view: Some(CoordinatorBranchView {
1143                view_id: "branch_view:separation".to_string(),
1144                branch_id: "branch:0".to_string(),
1145                mode: CoordinatorBranchViewMode::Separation,
1146                selector: CoordinatorBranchViewSelector {
1147                    tags: vec!["clean".to_string()],
1148                    ..Default::default()
1149                },
1150                allow_overlap: false,
1151                metadata: BTreeMap::new(),
1152            }),
1153            include_augmented: true,
1154            ..Default::default()
1155        };
1156        let view_record = arena.make_view(data.handle.handle, &view).unwrap();
1157        let identity = arena.view_identity(view_record.handle.handle).unwrap();
1158        assert!(!identity.records.is_empty());
1159    }
1160
1161    #[test]
1162    fn branch_view_by_metadata_tag_filter_modes_require_host_filtering() {
1163        use crate::coordinator::{
1164            CoordinatorBranchView, CoordinatorBranchViewMode, CoordinatorBranchViewSelector,
1165        };
1166
1167        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1168        let data = arena.materialize(&envelope(), &request()).unwrap();
1169        for (mode, selector, label) in [
1170            (
1171                CoordinatorBranchViewMode::ByMetadata,
1172                CoordinatorBranchViewSelector {
1173                    metadata: BTreeMap::from([("site".to_string(), serde_json::json!("a"))]),
1174                    ..Default::default()
1175                },
1176                "by_metadata",
1177            ),
1178            (
1179                CoordinatorBranchViewMode::ByTag,
1180                CoordinatorBranchViewSelector {
1181                    tags: vec!["clean".to_string()],
1182                    ..Default::default()
1183                },
1184                "by_tag",
1185            ),
1186            (
1187                CoordinatorBranchViewMode::ByFilter,
1188                CoordinatorBranchViewSelector {
1189                    filter: Some(serde_json::json!({"op": "always"})),
1190                    ..Default::default()
1191                },
1192                "by_filter",
1193            ),
1194        ] {
1195            let view = DataView {
1196                branch_view: Some(CoordinatorBranchView {
1197                    view_id: format!("branch_view:{label}"),
1198                    branch_id: "branch:0".to_string(),
1199                    mode,
1200                    selector,
1201                    allow_overlap: false,
1202                    metadata: BTreeMap::new(),
1203                }),
1204                include_augmented: true,
1205                ..Default::default()
1206            };
1207            let error = arena
1208                .make_view(data.handle.handle, &view)
1209                .expect_err("host-only branch view modes must reject in-memory execution");
1210            let message = format!("{error}");
1211            assert!(
1212                message.contains("requires host-side filtering"),
1213                "expected host-filtering error for {label}, got: {message}"
1214            );
1215        }
1216    }
1217
1218    #[test]
1219    fn target_values_are_sample_level_and_dedup_repetitions() {
1220        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1221        let data = arena.materialize(&envelope(), &request()).unwrap();
1222        let view = DataView {
1223            sample_ids: Some(vec![SampleId::new("S001").unwrap()]),
1224            include_augmented: false,
1225            ..Default::default()
1226        };
1227        let view_record = arena.make_view(data.handle.handle, &view).unwrap();
1228        let target_table = CoordinatorTargetTable {
1229            target_id: TargetId::new("y").unwrap(),
1230            values: vec![
1231                CoordinatorTargetValue {
1232                    sample_id: SampleId::new("S001").unwrap(),
1233                    value: json!(42.0),
1234                },
1235                CoordinatorTargetValue {
1236                    sample_id: SampleId::new("S002").unwrap(),
1237                    value: json!(7.0),
1238                },
1239            ],
1240        };
1241
1242        let target = arena
1243            .target_values(view_record.handle.handle, &target_table)
1244            .unwrap();
1245
1246        assert_eq!(target.target_id.as_str(), "y");
1247        assert_eq!(target.sample_ids, vec![SampleId::new("S001").unwrap()]);
1248        assert_eq!(target.values, vec![json!(42.0)]);
1249    }
1250
1251    #[test]
1252    fn multi_target_values_align_samples_and_emit_validity_masks() {
1253        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1254        let data = arena.materialize(&envelope(), &request()).unwrap();
1255        let view = DataView {
1256            sample_ids: Some(vec![
1257                SampleId::new("S002").unwrap(),
1258                SampleId::new("S001").unwrap(),
1259            ]),
1260            include_augmented: false,
1261            ..Default::default()
1262        };
1263        let view_record = arena.make_view(data.handle.handle, &view).unwrap();
1264        let y = CoordinatorTargetTable {
1265            target_id: TargetId::new("y").unwrap(),
1266            values: vec![
1267                CoordinatorTargetValue {
1268                    sample_id: SampleId::new("S001").unwrap(),
1269                    value: json!(42.0),
1270                },
1271                CoordinatorTargetValue {
1272                    sample_id: SampleId::new("S002").unwrap(),
1273                    value: json!(7.0),
1274                },
1275            ],
1276        };
1277        let protein = CoordinatorTargetTable {
1278            target_id: TargetId::new("protein").unwrap(),
1279            values: vec![CoordinatorTargetValue {
1280                sample_id: SampleId::new("S001").unwrap(),
1281                value: json!(12.5),
1282            }],
1283        };
1284
1285        let block = arena
1286            .multi_target_values(view_record.handle.handle, &[y, protein])
1287            .unwrap();
1288
1289        assert_eq!(
1290            block.target_ids,
1291            vec![
1292                TargetId::new("y").unwrap(),
1293                TargetId::new("protein").unwrap()
1294            ]
1295        );
1296        assert_eq!(
1297            block.sample_ids,
1298            vec![
1299                SampleId::new("S002").unwrap(),
1300                SampleId::new("S001").unwrap()
1301            ]
1302        );
1303        assert_eq!(block.values[0], vec![json!(7.0), json!(42.0)]);
1304        assert_eq!(block.validity_masks[0], vec![true, true]);
1305        assert_eq!(block.values[1], vec![json!(null), json!(12.5)]);
1306        assert_eq!(block.validity_masks[1], vec![false, true]);
1307    }
1308
1309    #[test]
1310    fn feature_values_are_observation_level_and_filter_columns() {
1311        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1312        let data = arena.materialize(&envelope(), &request()).unwrap();
1313        let view = DataView {
1314            sample_ids: Some(vec![SampleId::new("S001").unwrap()]),
1315            columns: Some(vec!["f1".to_string()]),
1316            include_augmented: false,
1317            ..Default::default()
1318        };
1319        let view_record = arena.make_view(data.handle.handle, &view).unwrap();
1320        let feature_table = CoordinatorFeatureTable {
1321            feature_set_id: "x".to_string(),
1322            representation_id: RepresentationId::new("tabular_numeric").unwrap(),
1323            feature_names: vec!["f0".to_string(), "f1".to_string()],
1324            rows: vec![
1325                CoordinatorFeatureRow {
1326                    observation_id: ObservationId::new("obs.S001.base").unwrap(),
1327                    values: vec![json!(1.0), json!(10.0)],
1328                },
1329                CoordinatorFeatureRow {
1330                    observation_id: ObservationId::new("obs.S001.rep1").unwrap(),
1331                    values: vec![json!(2.0), json!(20.0)],
1332                },
1333                CoordinatorFeatureRow {
1334                    observation_id: ObservationId::new("obs.S001.aug0").unwrap(),
1335                    values: vec![json!(3.0), json!(30.0)],
1336                },
1337                CoordinatorFeatureRow {
1338                    observation_id: ObservationId::new("obs.S002.base").unwrap(),
1339                    values: vec![json!(4.0), json!(40.0)],
1340                },
1341            ],
1342        };
1343
1344        let features = arena
1345            .feature_values(view_record.handle.handle, &feature_table)
1346            .unwrap();
1347
1348        assert_eq!(features.feature_set_id, "x");
1349        assert_eq!(features.feature_names, vec!["f1".to_string()]);
1350        assert_eq!(features.representation_id.as_str(), "tabular_numeric");
1351        assert_eq!(
1352            features.observation_ids,
1353            vec![
1354                ObservationId::new("obs.S001.base").unwrap(),
1355                ObservationId::new("obs.S001.rep1").unwrap(),
1356            ]
1357        );
1358        assert_eq!(
1359            features.sample_ids,
1360            vec![
1361                SampleId::new("S001").unwrap(),
1362                SampleId::new("S001").unwrap()
1363            ]
1364        );
1365        assert_eq!(features.values, vec![vec![json!(10.0)], vec![json!(20.0)]]);
1366
1367        let mut wrong_representation = feature_table;
1368        wrong_representation.representation_id = RepresentationId::new("dense_signal").unwrap();
1369        assert!(arena
1370            .feature_values(view_record.handle.handle, &wrong_representation)
1371            .is_err());
1372    }
1373
1374    #[test]
1375    fn view_honors_requested_sample_order_for_identity_targets_and_features() {
1376        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1377        let data = arena.materialize(&envelope(), &request()).unwrap();
1378        let view = DataView {
1379            sample_ids: Some(vec![
1380                SampleId::new("S002").unwrap(),
1381                SampleId::new("S001").unwrap(),
1382            ]),
1383            include_augmented: false,
1384            ..Default::default()
1385        };
1386        let view_record = arena.make_view(data.handle.handle, &view).unwrap();
1387
1388        let identity = arena.view_identity(view_record.handle.handle).unwrap();
1389        assert_eq!(
1390            identity
1391                .records
1392                .iter()
1393                .map(|relation| relation.observation_id.as_str())
1394                .collect::<Vec<_>>(),
1395            vec!["obs.S002.base", "obs.S001.base", "obs.S001.rep1"]
1396        );
1397
1398        let target_table = CoordinatorTargetTable {
1399            target_id: TargetId::new("y").unwrap(),
1400            values: vec![
1401                CoordinatorTargetValue {
1402                    sample_id: SampleId::new("S001").unwrap(),
1403                    value: json!(42.0),
1404                },
1405                CoordinatorTargetValue {
1406                    sample_id: SampleId::new("S002").unwrap(),
1407                    value: json!(7.0),
1408                },
1409            ],
1410        };
1411        let target = arena
1412            .target_values(view_record.handle.handle, &target_table)
1413            .unwrap();
1414        assert_eq!(
1415            target.sample_ids,
1416            vec![
1417                SampleId::new("S002").unwrap(),
1418                SampleId::new("S001").unwrap()
1419            ]
1420        );
1421        assert_eq!(target.values, vec![json!(7.0), json!(42.0)]);
1422
1423        let feature_table = CoordinatorFeatureTable {
1424            feature_set_id: "x".to_string(),
1425            representation_id: RepresentationId::new("tabular_numeric").unwrap(),
1426            feature_names: vec!["f0".to_string(), "f1".to_string()],
1427            rows: vec![
1428                CoordinatorFeatureRow {
1429                    observation_id: ObservationId::new("obs.S001.base").unwrap(),
1430                    values: vec![json!(1.0), json!(10.0)],
1431                },
1432                CoordinatorFeatureRow {
1433                    observation_id: ObservationId::new("obs.S001.rep1").unwrap(),
1434                    values: vec![json!(2.0), json!(20.0)],
1435                },
1436                CoordinatorFeatureRow {
1437                    observation_id: ObservationId::new("obs.S002.base").unwrap(),
1438                    values: vec![json!(4.0), json!(40.0)],
1439                },
1440            ],
1441        };
1442        let features = arena
1443            .feature_values(view_record.handle.handle, &feature_table)
1444            .unwrap();
1445        assert_eq!(
1446            features.observation_ids,
1447            vec![
1448                ObservationId::new("obs.S002.base").unwrap(),
1449                ObservationId::new("obs.S001.base").unwrap(),
1450                ObservationId::new("obs.S001.rep1").unwrap(),
1451            ]
1452        );
1453        assert_eq!(
1454            features.values,
1455            vec![
1456                vec![json!(4.0), json!(40.0)],
1457                vec![json!(1.0), json!(10.0)],
1458                vec![json!(2.0), json!(20.0)],
1459            ]
1460        );
1461    }
1462
1463    #[test]
1464    fn release_data_handle_releases_child_views() {
1465        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1466        let data = arena.materialize(&envelope(), &request()).unwrap();
1467        let view_record = arena
1468            .make_view(data.handle.handle, &DataView::default())
1469            .unwrap();
1470
1471        assert!(arena.release_handle(data.handle.handle));
1472        assert_eq!(arena.handle_record(data.handle.handle), None);
1473        assert_eq!(arena.view_record(view_record.handle.handle), None);
1474        let error = arena.view_identity(view_record.handle.handle).unwrap_err();
1475        assert_eq!(error.category(), "runtime");
1476        assert_eq!(error.code(), "unknown_handle");
1477        assert_eq!(error.error_code(), 0x0001_0001);
1478        assert!(!arena.release_handle(data.handle.handle));
1479    }
1480}