1use std::cell::RefCell;
2use std::collections::{BTreeMap, BTreeSet};
3
4use serde::{Deserialize, Serialize};
5
6use crate::coordinator::{
7 validate_fingerprint, CoordinatorDataPlanEnvelope, CoordinatorRelation, CoordinatorRelationSet,
8};
9use crate::error::{DataError, Result};
10use crate::ids::{ObservationId, RepresentationId, SampleId, SourceId, TargetId};
11use crate::model::DataView;
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum CoordinatorHandleKind {
16 Data,
17 View,
18}
19
20#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
21pub struct CoordinatorHandleRef {
22 pub handle: u64,
23 pub kind: CoordinatorHandleKind,
24 pub owner_controller: String,
25}
26
27#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
28pub struct CoordinatorDataMaterializationRequest {
29 pub run_id: String,
30 pub node_id: String,
31 pub input_name: String,
32 pub phase: String,
33 #[serde(default)]
34 pub variant_id: Option<String>,
35 #[serde(default)]
36 pub fold_id: Option<String>,
37 pub request_id: String,
38 pub schema_fingerprint: String,
39 pub plan_fingerprint: String,
40 #[serde(default)]
41 pub relation_fingerprint: Option<String>,
42 pub output_representation: RepresentationId,
43 #[serde(default)]
44 pub source_ids: Vec<SourceId>,
45 #[serde(default)]
46 pub require_relations: bool,
47}
48
49impl CoordinatorDataMaterializationRequest {
50 pub fn validate(&self) -> Result<()> {
51 validate_non_empty("run_id", &self.run_id)?;
52 validate_non_empty("node_id", &self.node_id)?;
53 validate_non_empty("input_name", &self.input_name)?;
54 validate_non_empty("phase", &self.phase)?;
55 validate_non_empty("request_id", &self.request_id)?;
56 validate_fingerprint("schema", &self.schema_fingerprint)?;
57 validate_fingerprint("plan", &self.plan_fingerprint)?;
58 if let Some(relation_fingerprint) = &self.relation_fingerprint {
59 validate_fingerprint("relation", relation_fingerprint)?;
60 } else if self.require_relations {
61 return Err(DataError::Validation(format!(
62 "materialization request `{}` on `{}` requires relations but has no relation_fingerprint",
63 self.input_name, self.node_id
64 )));
65 }
66 let unique_sources = self.source_ids.iter().collect::<BTreeSet<_>>();
67 if unique_sources.len() != self.source_ids.len() {
68 return Err(DataError::Validation(format!(
69 "materialization request `{}` on `{}` contains duplicate source ids",
70 self.input_name, self.node_id
71 )));
72 }
73 Ok(())
74 }
75}
76
77#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
78pub struct CoordinatorDataHandleRecord {
79 pub handle: CoordinatorHandleRef,
80 pub run_id: String,
81 pub node_id: String,
82 pub input_name: String,
83 pub phase: String,
84 #[serde(default)]
85 pub variant_id: Option<String>,
86 #[serde(default)]
87 pub fold_id: Option<String>,
88 pub request_id: String,
89 pub schema_fingerprint: String,
90 pub plan_fingerprint: String,
91 #[serde(default)]
92 pub relation_fingerprint: Option<String>,
93 pub plan_id: String,
94 pub output_representation: RepresentationId,
95 #[serde(default)]
96 pub source_ids: Vec<SourceId>,
97 #[serde(default)]
98 pub sample_count: Option<usize>,
99 #[serde(default)]
100 pub relation_record_count: Option<usize>,
101}
102
103#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
104pub struct CoordinatorDataViewRecord {
105 pub handle: CoordinatorHandleRef,
106 pub parent_handle: CoordinatorHandleRef,
107 pub view: DataView,
108 pub sample_count: usize,
109 pub relation_record_count: usize,
110}
111
112#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
113pub struct CoordinatorTargetValue {
114 pub sample_id: SampleId,
115 pub value: serde_json::Value,
116}
117
118#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
119pub struct CoordinatorTargetTable {
120 pub target_id: TargetId,
121 pub values: Vec<CoordinatorTargetValue>,
122}
123
124impl CoordinatorTargetTable {
125 pub fn validate(&self) -> Result<()> {
126 if self.values.is_empty() {
127 return Err(DataError::Validation(format!(
128 "target table `{}` contains no values",
129 self.target_id
130 )));
131 }
132 let mut seen = BTreeSet::new();
133 for value in &self.values {
134 if !seen.insert(&value.sample_id) {
135 return Err(DataError::Validation(format!(
136 "target table `{}` contains duplicate sample `{}`",
137 self.target_id, value.sample_id
138 )));
139 }
140 }
141 Ok(())
142 }
143}
144
145#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
146pub struct CoordinatorTargetBlock {
147 pub target_id: TargetId,
148 pub sample_ids: Vec<SampleId>,
149 pub values: Vec<serde_json::Value>,
150}
151
152#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
153pub struct CoordinatorMultiTargetBlock {
154 pub target_ids: Vec<TargetId>,
155 pub sample_ids: Vec<SampleId>,
156 pub values: Vec<Vec<serde_json::Value>>,
158 pub validity_masks: Vec<Vec<bool>>,
160}
161
162#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
163pub struct CoordinatorFeatureRow {
164 pub observation_id: ObservationId,
165 pub values: Vec<serde_json::Value>,
166}
167
168#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
169pub struct CoordinatorFeatureTable {
170 pub feature_set_id: String,
171 pub representation_id: RepresentationId,
172 pub feature_names: Vec<String>,
173 pub rows: Vec<CoordinatorFeatureRow>,
174}
175
176impl CoordinatorFeatureTable {
177 pub fn validate(&self) -> Result<()> {
178 validate_non_empty("feature_set_id", &self.feature_set_id)?;
179 if self.feature_names.is_empty() {
180 return Err(DataError::Validation(format!(
181 "feature table `{}` contains no features",
182 self.feature_set_id
183 )));
184 }
185 let mut seen_features = BTreeSet::new();
186 for feature_name in &self.feature_names {
187 validate_non_empty("feature_name", feature_name)?;
188 if !seen_features.insert(feature_name) {
189 return Err(DataError::Validation(format!(
190 "feature table `{}` contains duplicate feature `{}`",
191 self.feature_set_id, feature_name
192 )));
193 }
194 }
195 if self.rows.is_empty() {
196 return Err(DataError::Validation(format!(
197 "feature table `{}` contains no rows",
198 self.feature_set_id
199 )));
200 }
201 let mut seen_observations = BTreeSet::new();
202 for row in &self.rows {
203 if !seen_observations.insert(&row.observation_id) {
204 return Err(DataError::Validation(format!(
205 "feature table `{}` contains duplicate observation `{}`",
206 self.feature_set_id, row.observation_id
207 )));
208 }
209 if row.values.len() != self.feature_names.len() {
210 return Err(DataError::Validation(format!(
211 "feature table `{}` row `{}` has {} values for {} features",
212 self.feature_set_id,
213 row.observation_id,
214 row.values.len(),
215 self.feature_names.len()
216 )));
217 }
218 }
219 Ok(())
220 }
221}
222
223#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
224pub struct CoordinatorFeatureBlock {
225 pub feature_set_id: String,
226 pub representation_id: RepresentationId,
227 pub feature_names: Vec<String>,
228 pub observation_ids: Vec<ObservationId>,
229 pub sample_ids: Vec<SampleId>,
230 pub values: Vec<Vec<serde_json::Value>>,
231}
232
233#[derive(Clone, Debug, PartialEq)]
239pub struct CoordinatorFeatureBlockF64 {
240 pub feature_set_id: String,
241 pub representation_id: RepresentationId,
242 pub feature_names: Vec<String>,
243 pub observation_ids: Vec<ObservationId>,
244 pub sample_ids: Vec<SampleId>,
245 pub values: Vec<f64>,
246}
247
248#[derive(Debug)]
249pub struct CoordinatorHandleArena {
250 owner_controller: String,
251 next_handle: RefCell<u64>,
252 records: RefCell<BTreeMap<u64, CoordinatorDataHandleRecord>>,
253 data_relations: RefCell<BTreeMap<u64, CoordinatorRelationSet>>,
254 view_records: RefCell<BTreeMap<u64, CoordinatorDataViewRecord>>,
255 view_relations: RefCell<BTreeMap<u64, CoordinatorRelationSet>>,
256}
257
258impl CoordinatorHandleArena {
259 pub fn new(owner_controller: impl Into<String>) -> Result<Self> {
260 let owner_controller = owner_controller.into();
261 validate_non_empty("owner_controller", &owner_controller)?;
262 Ok(Self {
263 owner_controller,
264 next_handle: RefCell::new(1),
265 records: RefCell::new(BTreeMap::new()),
266 data_relations: RefCell::new(BTreeMap::new()),
267 view_records: RefCell::new(BTreeMap::new()),
268 view_relations: RefCell::new(BTreeMap::new()),
269 })
270 }
271
272 pub fn materialize(
273 &self,
274 envelope: &CoordinatorDataPlanEnvelope,
275 request: &CoordinatorDataMaterializationRequest,
276 ) -> Result<CoordinatorDataHandleRecord> {
277 envelope.validate()?;
278 request.validate()?;
279 validate_request_against_envelope(envelope, request)?;
280 let scoped_relations = envelope
281 .coordinator_relations
282 .as_ref()
283 .map(|relations| scoped_relations_for_materialization(relations, request))
284 .transpose()?;
285
286 let handle = CoordinatorHandleRef {
287 handle: self.next_handle(),
288 kind: CoordinatorHandleKind::Data,
289 owner_controller: self.owner_controller.clone(),
290 };
291 let record = CoordinatorDataHandleRecord {
292 handle: handle.clone(),
293 run_id: request.run_id.clone(),
294 node_id: request.node_id.clone(),
295 input_name: request.input_name.clone(),
296 phase: request.phase.clone(),
297 variant_id: request.variant_id.clone(),
298 fold_id: request.fold_id.clone(),
299 request_id: request.request_id.clone(),
300 schema_fingerprint: request.schema_fingerprint.clone(),
301 plan_fingerprint: request.plan_fingerprint.clone(),
302 relation_fingerprint: request.relation_fingerprint.clone(),
303 plan_id: envelope.plan.id.clone(),
304 output_representation: request.output_representation.clone(),
305 source_ids: request.source_ids.clone(),
306 sample_count: scoped_relations.as_ref().map(|relations| {
307 relations
308 .records
309 .iter()
310 .map(|record| &record.sample_id)
311 .collect::<BTreeSet<_>>()
312 .len()
313 }),
314 relation_record_count: scoped_relations
315 .as_ref()
316 .map(|relations| relations.records.len()),
317 };
318 self.records
319 .borrow_mut()
320 .insert(handle.handle, record.clone());
321 if let Some(relations) = scoped_relations {
322 self.data_relations
323 .borrow_mut()
324 .insert(handle.handle, relations);
325 }
326 Ok(record)
327 }
328
329 pub fn make_view(
330 &self,
331 data_handle: u64,
332 view: &DataView,
333 ) -> Result<CoordinatorDataViewRecord> {
334 validate_view(view)?;
335 let parent =
336 self.records
337 .borrow()
338 .get(&data_handle)
339 .cloned()
340 .ok_or(DataError::UnknownHandle {
341 kind: "data",
342 handle: data_handle,
343 })?;
344 let relations = self
345 .data_relations
346 .borrow()
347 .get(&data_handle)
348 .cloned()
349 .ok_or_else(|| {
350 DataError::Validation(format!(
351 "data handle `{data_handle}` has no coordinator relations"
352 ))
353 })?;
354 let filtered = filter_relations(&relations.records, view)?;
355 let sample_count = unique_sample_count(&filtered);
356 let relation_record_count = filtered.len();
357 let handle = CoordinatorHandleRef {
358 handle: self.next_handle(),
359 kind: CoordinatorHandleKind::View,
360 owner_controller: self.owner_controller.clone(),
361 };
362 let record = CoordinatorDataViewRecord {
363 handle: handle.clone(),
364 parent_handle: parent.handle,
365 view: view.clone(),
366 sample_count,
367 relation_record_count,
368 };
369 self.view_records
370 .borrow_mut()
371 .insert(handle.handle, record.clone());
372 self.view_relations
373 .borrow_mut()
374 .insert(handle.handle, CoordinatorRelationSet { records: filtered });
375 Ok(record)
376 }
377
378 pub fn view_record(&self, handle: u64) -> Option<CoordinatorDataViewRecord> {
379 self.view_records.borrow().get(&handle).cloned()
380 }
381
382 pub fn view_identity(&self, handle: u64) -> Result<CoordinatorRelationSet> {
383 self.view_relations
384 .borrow()
385 .get(&handle)
386 .cloned()
387 .ok_or(DataError::UnknownHandle {
388 kind: "view",
389 handle,
390 })
391 }
392
393 pub fn data_identity(&self, handle: u64) -> Result<CoordinatorRelationSet> {
394 if !self.records.borrow().contains_key(&handle) {
399 return Err(DataError::UnknownHandle {
400 kind: "data",
401 handle,
402 });
403 }
404 self.data_relations
405 .borrow()
406 .get(&handle)
407 .cloned()
408 .ok_or_else(|| {
409 DataError::Validation(format!(
410 "data handle `{handle}` has no coordinator relations"
411 ))
412 })
413 }
414
415 pub fn release_handle(&self, handle: u64) -> bool {
416 if self.view_records.borrow_mut().remove(&handle).is_some() {
417 self.view_relations.borrow_mut().remove(&handle);
418 return true;
419 }
420 if let Some(record) = self.records.borrow_mut().remove(&handle) {
421 self.data_relations.borrow_mut().remove(&handle);
422 let child_views = self
423 .view_records
424 .borrow()
425 .iter()
426 .filter_map(|(view_handle, view_record)| {
427 (view_record.parent_handle == record.handle).then_some(*view_handle)
428 })
429 .collect::<Vec<_>>();
430 for view_handle in child_views {
431 self.view_records.borrow_mut().remove(&view_handle);
432 self.view_relations.borrow_mut().remove(&view_handle);
433 }
434 return true;
435 }
436 false
437 }
438
439 pub fn target_values(
440 &self,
441 view_handle: u64,
442 target_table: &CoordinatorTargetTable,
443 ) -> Result<CoordinatorTargetBlock> {
444 target_table.validate()?;
445 let relations = self.view_identity(view_handle)?;
446 let values_by_sample = target_table
447 .values
448 .iter()
449 .map(|value| (&value.sample_id, &value.value))
450 .collect::<BTreeMap<_, _>>();
451 let mut seen_samples = BTreeSet::new();
452 let mut sample_ids = Vec::new();
453 let mut values = Vec::new();
454 for relation in relations.records.iter().filter(|relation| {
455 relation
456 .target_id
457 .as_ref()
458 .map(|target_id| target_id == &target_table.target_id)
459 .unwrap_or(true)
460 }) {
461 if !seen_samples.insert(&relation.sample_id) {
462 continue;
463 }
464 let value = values_by_sample.get(&relation.sample_id).ok_or_else(|| {
465 DataError::Validation(format!(
466 "target table `{}` has no value for sample `{}`",
467 target_table.target_id, relation.sample_id
468 ))
469 })?;
470 sample_ids.push(relation.sample_id.clone());
471 values.push((*value).clone());
472 }
473 if sample_ids.is_empty() {
474 return Err(DataError::Validation(format!(
475 "view `{view_handle}` contains no samples for target `{}`",
476 target_table.target_id
477 )));
478 }
479 Ok(CoordinatorTargetBlock {
480 target_id: target_table.target_id.clone(),
481 sample_ids,
482 values,
483 })
484 }
485
486 pub fn multi_target_values(
487 &self,
488 view_handle: u64,
489 target_tables: &[CoordinatorTargetTable],
490 ) -> Result<CoordinatorMultiTargetBlock> {
491 if target_tables.is_empty() {
492 return Err(DataError::Validation(
493 "multi-target materialization requires at least one target table".to_string(),
494 ));
495 }
496 let mut seen_targets = BTreeSet::new();
497 for table in target_tables {
498 table.validate()?;
499 if !seen_targets.insert(table.target_id.clone()) {
500 return Err(DataError::Validation(format!(
501 "multi-target materialization contains duplicate target `{}`",
502 table.target_id
503 )));
504 }
505 }
506
507 let target_ids = target_tables
508 .iter()
509 .map(|table| table.target_id.clone())
510 .collect::<Vec<_>>();
511 let target_universe = target_ids.iter().collect::<BTreeSet<_>>();
512 let relations = self.view_identity(view_handle)?;
513 let mut seen_samples = BTreeSet::new();
514 let mut sample_ids = Vec::new();
515 for relation in relations.records.iter().filter(|relation| {
516 relation
517 .target_id
518 .as_ref()
519 .map(|target_id| target_universe.contains(target_id))
520 .unwrap_or(true)
521 }) {
522 if seen_samples.insert(relation.sample_id.clone()) {
523 sample_ids.push(relation.sample_id.clone());
524 }
525 }
526 if sample_ids.is_empty() {
527 return Err(DataError::Validation(format!(
528 "view `{view_handle}` contains no samples for requested targets"
529 )));
530 }
531
532 let mut values = Vec::with_capacity(target_tables.len());
533 let mut validity_masks = Vec::with_capacity(target_tables.len());
534 for table in target_tables {
535 let values_by_sample = table
536 .values
537 .iter()
538 .map(|value| (&value.sample_id, &value.value))
539 .collect::<BTreeMap<_, _>>();
540 let mut target_values = Vec::with_capacity(sample_ids.len());
541 let mut validity = Vec::with_capacity(sample_ids.len());
542 for sample_id in &sample_ids {
543 match values_by_sample.get(sample_id) {
544 Some(value) if !value.is_null() => {
545 target_values.push((*value).clone());
546 validity.push(true);
547 }
548 Some(_) | None => {
549 target_values.push(serde_json::Value::Null);
550 validity.push(false);
551 }
552 }
553 }
554 values.push(target_values);
555 validity_masks.push(validity);
556 }
557
558 Ok(CoordinatorMultiTargetBlock {
559 target_ids,
560 sample_ids,
561 values,
562 validity_masks,
563 })
564 }
565
566 pub fn feature_values(
567 &self,
568 view_handle: u64,
569 feature_table: &CoordinatorFeatureTable,
570 ) -> Result<CoordinatorFeatureBlock> {
571 feature_table.validate()?;
572 let view_record = self
573 .view_records
574 .borrow()
575 .get(&view_handle)
576 .cloned()
577 .ok_or(DataError::UnknownHandle {
578 kind: "view",
579 handle: view_handle,
580 })?;
581 let parent_record = self
582 .records
583 .borrow()
584 .get(&view_record.parent_handle.handle)
585 .cloned()
586 .ok_or(DataError::UnknownHandle {
587 kind: "data",
588 handle: view_record.parent_handle.handle,
589 })?;
590 if feature_table.representation_id != parent_record.output_representation {
591 return Err(DataError::Validation(format!(
592 "feature table `{}` representation `{}` does not match materialized output representation `{}`",
593 feature_table.feature_set_id,
594 feature_table.representation_id,
595 parent_record.output_representation
596 )));
597 }
598 let relations = self.view_identity(view_handle)?;
599 let selected_indices = selected_feature_indices(feature_table, &view_record.view)?;
600 let rows_by_observation = feature_table
601 .rows
602 .iter()
603 .map(|row| (&row.observation_id, row))
604 .collect::<BTreeMap<_, _>>();
605 let mut observation_ids = Vec::with_capacity(relations.records.len());
606 let mut sample_ids = Vec::with_capacity(relations.records.len());
607 let mut values = Vec::with_capacity(relations.records.len());
608 for relation in &relations.records {
609 let row = rows_by_observation
610 .get(&relation.observation_id)
611 .ok_or_else(|| {
612 DataError::Validation(format!(
613 "feature table `{}` has no row for observation `{}`",
614 feature_table.feature_set_id, relation.observation_id
615 ))
616 })?;
617 observation_ids.push(relation.observation_id.clone());
618 sample_ids.push(relation.sample_id.clone());
619 values.push(
620 selected_indices
621 .iter()
622 .map(|idx| row.values[*idx].clone())
623 .collect(),
624 );
625 }
626 Ok(CoordinatorFeatureBlock {
627 feature_set_id: feature_table.feature_set_id.clone(),
628 representation_id: feature_table.representation_id.clone(),
629 feature_names: selected_indices
630 .iter()
631 .map(|idx| feature_table.feature_names[*idx].clone())
632 .collect(),
633 observation_ids,
634 sample_ids,
635 values,
636 })
637 }
638
639 pub fn handle_record(&self, handle: u64) -> Option<CoordinatorDataHandleRecord> {
640 self.records.borrow().get(&handle).cloned()
641 }
642
643 pub fn handle_records(&self) -> Vec<CoordinatorDataHandleRecord> {
644 self.records.borrow().values().cloned().collect()
645 }
646
647 fn next_handle(&self) -> u64 {
648 let mut next = self.next_handle.borrow_mut();
649 let handle = *next;
650 *next += 1;
651 handle
652 }
653}
654
655fn validate_request_against_envelope(
656 envelope: &CoordinatorDataPlanEnvelope,
657 request: &CoordinatorDataMaterializationRequest,
658) -> Result<()> {
659 if request.schema_fingerprint != envelope.schema_fingerprint {
660 return Err(DataError::FingerprintMismatch {
661 kind: "schema",
662 expected: envelope.schema_fingerprint.clone(),
663 actual: request.schema_fingerprint.clone(),
664 });
665 }
666 if request.plan_fingerprint != envelope.plan_fingerprint {
667 return Err(DataError::FingerprintMismatch {
668 kind: "plan",
669 expected: envelope.plan_fingerprint.clone(),
670 actual: request.plan_fingerprint.clone(),
671 });
672 }
673 if request.relation_fingerprint != envelope.relation_fingerprint {
674 let none = || "<none>".to_string();
675 return Err(DataError::FingerprintMismatch {
676 kind: "relation",
677 expected: envelope.relation_fingerprint.clone().unwrap_or_else(none),
678 actual: request.relation_fingerprint.clone().unwrap_or_else(none),
679 });
680 }
681 if request.require_relations && envelope.coordinator_relations.is_none() {
682 return Err(DataError::Validation(format!(
683 "materialization request `{}` on `{}` requires coordinator relations",
684 request.input_name, request.node_id
685 )));
686 }
687 if request.output_representation != envelope.plan.output_representation {
688 return Err(DataError::Validation(format!(
689 "materialization request `{}` on `{}` output representation `{}` does not match plan output `{}`",
690 request.input_name,
691 request.node_id,
692 request.output_representation,
693 envelope.plan.output_representation
694 )));
695 }
696 if !request.source_ids.is_empty() {
697 let plan_sources = envelope
698 .plan
699 .steps
700 .iter()
701 .filter_map(|step| step.source_id.as_ref())
702 .collect::<BTreeSet<_>>();
703 for source_id in &request.source_ids {
704 if !plan_sources.contains(source_id) {
705 return Err(DataError::Validation(format!(
706 "materialization request `{}` on `{}` source `{}` is not present in data plan `{}`",
707 request.input_name, request.node_id, source_id, envelope.plan.id
708 )));
709 }
710 }
711 }
712 Ok(())
713}
714
715fn scoped_relations_for_materialization(
716 relations: &CoordinatorRelationSet,
717 request: &CoordinatorDataMaterializationRequest,
718) -> Result<CoordinatorRelationSet> {
719 relations.validate()?;
720 if request.source_ids.is_empty() {
721 return Ok(relations.clone());
722 }
723 let source_filter = request.source_ids.iter().collect::<BTreeSet<_>>();
724 let scoped = relations
725 .records
726 .iter()
727 .filter(|relation| {
728 relation
729 .source_id
730 .as_ref()
731 .map(|source_id| source_filter.contains(source_id))
732 .unwrap_or(false)
733 })
734 .cloned()
735 .collect::<Vec<_>>();
736 if scoped.is_empty() {
737 return Err(DataError::Validation(format!(
738 "materialization request `{}` on `{}` selected no coordinator relations for requested source ids",
739 request.input_name, request.node_id
740 )));
741 }
742 let scoped = CoordinatorRelationSet { records: scoped };
743 scoped.validate()?;
744 Ok(scoped)
745}
746
747fn validate_non_empty(label: &str, value: &str) -> Result<()> {
748 if value.trim().is_empty() {
749 return Err(DataError::Validation(format!("{label} must not be empty")));
750 }
751 Ok(())
752}
753
754fn validate_view(view: &DataView) -> Result<()> {
755 if let Some(samples) = &view.sample_ids {
756 let unique = samples.iter().collect::<BTreeSet<_>>();
757 if unique.len() != samples.len() {
758 return Err(DataError::Validation(
759 "data view contains duplicate sample ids".to_string(),
760 ));
761 }
762 }
763 if let Some(sources) = &view.source_ids {
764 let unique = sources.iter().collect::<BTreeSet<_>>();
765 if unique.len() != sources.len() {
766 return Err(DataError::Validation(
767 "data view contains duplicate source ids".to_string(),
768 ));
769 }
770 }
771 if let Some(columns) = &view.columns {
772 let unique = columns.iter().collect::<BTreeSet<_>>();
773 if unique.len() != columns.len() {
774 return Err(DataError::Validation(
775 "data view contains duplicate columns".to_string(),
776 ));
777 }
778 if columns.iter().any(|column| column.trim().is_empty()) {
779 return Err(DataError::Validation(
780 "data view contains an empty column".to_string(),
781 ));
782 }
783 }
784 if let Some(branch_view) = &view.branch_view {
785 branch_view.validate()?;
786 }
787 Ok(())
788}
789
790fn filter_relations(
791 relations: &[CoordinatorRelation],
792 view: &DataView,
793) -> Result<Vec<CoordinatorRelation>> {
794 let sample_filter = view
795 .sample_ids
796 .as_ref()
797 .map(|sample_ids| sample_ids.iter().collect::<BTreeSet<_>>());
798 let source_filter = view
799 .source_ids
800 .as_ref()
801 .map(|source_ids| source_ids.iter().collect::<BTreeSet<_>>());
802 let 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}