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 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            // A relation matches a `by_metadata` selector iff its own metadata
823            // contains every selector key with an equal value; `by_tag` iff its
824            // tags contain every selector tag.
825            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            // `by_filter` needs a deterministic predicate DSL that the in-memory
833            // arena does not yet evaluate; it stays host-side for now.
834            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        // Mark the sole S002 observation excluded. Under a training policy
1100        // (`include_excluded=false`) it must be filtered out; under a
1101        // validation/predict policy (`include_excluded=true`) it must be kept.
1102        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        // `relation_fingerprint` is a replay key for the *source* relation
1109        // table, not for the derived coordinator_relations we just edited, so
1110        // it stays intact: the embedded coordinator relations are validated
1111        // structurally, and `excluded` does not participate in that fingerprint.
1112        envelope.validate().unwrap();
1113
1114        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1115        let data = arena.materialize(&envelope, &request()).unwrap();
1116
1117        // Training view: excluded S002 row is dropped.
1118        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        // Validation/predict view: excluded S002 row is retained.
1135        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    /// Build the shared envelope, tagging S001 observations with
1256    /// `metadata={"group":"A"}` + `tags=["clean"]` and the S002 observation
1257    /// with `metadata={"group":"B"}` + `tags=["dirty"]`, so `by_metadata` /
1258    /// `by_tag` branch views have something to discriminate on. Editing the
1259    /// derived `coordinator_relations` does not invalidate the source
1260    /// `relation_fingerprint` (it is a replay key for the source table, not the
1261    /// derived view).
1262    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        // group=A INCLUDES the S001 relations and EXCLUDES S002.
1301        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        // group=B INCLUDES S002 and EXCLUDES S001.
1324        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        // tag=clean INCLUDES S001 and EXCLUDES S002.
1362        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        // tag=dirty INCLUDES S002 and EXCLUDES S001.
1378        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        // A branch_view scoped to group=B, intersected with a fold restriction
1397        // (sample_ids) that contains ONLY S001 samples, selects no relations —
1398        // the empty partition ∩ fold case. The arena must surface this explicitly
1399        // rather than returning an empty view (no silent mis-coverage).
1400        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        // The S001 sample id only (group=A); group=B (S002) is disjoint from this.
1409        // `DataView.sample_ids` rejects duplicates, so collect the distinct id.
1410        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}