1use std::collections::HashMap;
20use std::sync::Arc;
21
22use panproto_expr::{BuiltinOp, Literal};
23use panproto_gat::Name;
24
25use crate::functor::FInstance;
26use crate::ginstance::GInstance;
27use crate::metadata::Node;
28use crate::value::{FieldPresence, Value};
29use crate::wtype::{
30 WInstance, build_env_with_children, collect_scalar_child_values, value_to_expr_literal,
31};
32
33pub trait ElementOps {
40 fn fiber(&self, vertex: &Name) -> Vec<u32>;
44
45 fn pushforward(&self, elements: &[u32], edge_kind: &Name) -> Vec<u32>;
54
55 fn stalk(&self, element_id: u32) -> panproto_expr::Env;
68
69 fn eval_graph_builtin(
79 &self,
80 op: BuiltinOp,
81 args: &[Literal],
82 context: Option<u32>,
83 ) -> Result<Literal, panproto_expr::ExprError>;
84
85 fn attributes(&self, element_id: u32) -> HashMap<String, Value>;
91
92 fn sort(&self, element_id: u32) -> Option<Name>;
94
95 fn element_value(&self, element_id: u32) -> Option<FieldPresence>;
97}
98
99impl ElementOps for WInstance {
104 fn fiber(&self, vertex: &Name) -> Vec<u32> {
105 self.nodes
106 .iter()
107 .filter(|(_, n)| n.anchor == *vertex)
108 .map(|(id, _)| *id)
109 .collect()
110 }
111
112 fn pushforward(&self, elements: &[u32], edge_kind: &Name) -> Vec<u32> {
113 let mut result = Vec::new();
114 for &node_id in elements {
115 for &(src, tgt, ref edge) in &self.arcs {
116 if src == node_id && edge.kind == *edge_kind {
117 result.push(tgt);
118 }
119 }
120 }
121 result
122 }
123
124 fn stalk(&self, element_id: u32) -> panproto_expr::Env {
125 let Some(node) = self.nodes.get(&element_id) else {
126 return panproto_expr::Env::new();
127 };
128 crate::query::build_node_env(node, self)
129 }
130
131 fn eval_graph_builtin(
132 &self,
133 op: BuiltinOp,
134 args: &[Literal],
135 context: Option<u32>,
136 ) -> Result<Literal, panproto_expr::ExprError> {
137 winstance_graph_builtin(self, op, args, context)
138 }
139
140 fn attributes(&self, element_id: u32) -> HashMap<String, Value> {
141 let Some(node) = self.nodes.get(&element_id) else {
142 return HashMap::new();
143 };
144 let scalars = collect_scalar_child_values(self, element_id);
145 let mut combined = scalars;
146 for (key, val) in &node.extra_fields {
147 combined.insert(key.clone(), val.clone());
148 }
149 combined
150 }
151
152 fn sort(&self, element_id: u32) -> Option<Name> {
153 self.nodes.get(&element_id).map(|n| n.anchor.clone())
154 }
155
156 fn element_value(&self, element_id: u32) -> Option<FieldPresence> {
157 self.nodes.get(&element_id).and_then(|n| n.value.clone())
158 }
159}
160
161impl ElementOps for GInstance {
166 fn fiber(&self, vertex: &Name) -> Vec<u32> {
167 self.nodes
168 .iter()
169 .filter(|(_, n)| n.anchor == *vertex)
170 .map(|(id, _)| *id)
171 .collect()
172 }
173
174 fn pushforward(&self, elements: &[u32], edge_kind: &Name) -> Vec<u32> {
175 let mut result = Vec::new();
176 let mut seen = rustc_hash::FxHashSet::default();
177 for &node_id in elements {
178 for &(src, tgt, ref edge) in &self.edges {
179 if src == node_id && edge.kind == *edge_kind && seen.insert(tgt) {
180 result.push(tgt);
181 }
182 }
183 }
184 result
185 }
186
187 fn stalk(&self, element_id: u32) -> panproto_expr::Env {
188 let Some(node) = self.nodes.get(&element_id) else {
189 return panproto_expr::Env::new();
190 };
191
192 let child_scalars = collect_graph_scalar_children(self, element_id);
196 let mut env = build_env_with_children(&node.extra_fields, &child_scalars);
197
198 env = env.extend(
200 Arc::from("_anchor"),
201 Literal::Str(node.anchor.as_ref().into()),
202 );
203 env = env.extend(Arc::from("_id"), Literal::Int(i64::from(node.id)));
204
205 if let Some(val) = self.values.get(&element_id) {
206 env = env.extend(Arc::from("_value"), value_to_expr_literal(val));
207 }
208
209 let edge_count = self
210 .edges
211 .iter()
212 .filter(|(src, _, _)| *src == element_id)
213 .count();
214 #[allow(clippy::cast_possible_wrap)]
215 {
216 env = env.extend(Arc::from("_edge_count"), Literal::Int(edge_count as i64));
217 }
218
219 env
220 }
221
222 fn eval_graph_builtin(
223 &self,
224 op: BuiltinOp,
225 args: &[Literal],
226 context: Option<u32>,
227 ) -> Result<Literal, panproto_expr::ExprError> {
228 ginstance_graph_builtin(self, op, args, context)
229 }
230
231 fn attributes(&self, element_id: u32) -> HashMap<String, Value> {
232 let Some(node) = self.nodes.get(&element_id) else {
233 return HashMap::new();
234 };
235 let child_scalars = collect_graph_scalar_children(self, element_id);
236 let mut combined = child_scalars;
237 for (key, val) in &node.extra_fields {
238 combined.insert(key.clone(), val.clone());
239 }
240 combined
241 }
242
243 fn sort(&self, element_id: u32) -> Option<Name> {
244 self.nodes.get(&element_id).map(|n| n.anchor.clone())
245 }
246
247 fn element_value(&self, element_id: u32) -> Option<FieldPresence> {
248 self.values
249 .get(&element_id)
250 .map(|v| FieldPresence::Present(v.clone()))
251 }
252}
253
254fn collect_graph_scalar_children(instance: &GInstance, node_id: u32) -> HashMap<String, Value> {
261 let mut result = HashMap::new();
262 for &(src, tgt, ref edge) in &instance.edges {
263 if src != node_id {
264 continue;
265 }
266 if let Some(val) = instance.values.get(&tgt) {
268 let field_name = edge.name.as_deref().unwrap_or(&*edge.tgt);
269 result.insert(field_name.to_string(), val.clone());
270 } else if let Some(tgt_node) = instance.nodes.get(&tgt) {
271 if let Some(FieldPresence::Present(val)) = &tgt_node.value {
273 let field_name = edge.name.as_deref().unwrap_or(&*edge.tgt);
274 result.insert(field_name.to_string(), val.clone());
275 }
276 }
277 }
278 result
279}
280
281#[must_use]
292pub const fn encode_finstance_id(table_ordinal: u16, row_index: u16) -> u32 {
293 (table_ordinal as u32) << 16 | row_index as u32
294}
295
296#[must_use]
298pub const fn decode_finstance_id(id: u32) -> (u16, u16) {
299 #[allow(clippy::cast_possible_truncation)]
300 ((id >> 16) as u16, id as u16)
301}
302
303impl FInstance {
304 fn sorted_table_names(&self) -> Vec<&str> {
306 let mut names: Vec<&str> = self.tables.keys().map(String::as_str).collect();
307 names.sort_unstable();
308 names
309 }
310
311 fn table_ordinal(&self, table_name: &str) -> Option<u16> {
313 let names = self.sorted_table_names();
314 names
315 .binary_search(&table_name)
316 .ok()
317 .and_then(|i| u16::try_from(i).ok())
318 }
319
320 fn ordinal_map(&self) -> Vec<&str> {
325 self.sorted_table_names()
326 }
327
328 fn resolve_element(&self, element_id: u32) -> Option<(&str, usize)> {
332 let (table_ord, row_idx) = decode_finstance_id(element_id);
333 let names = self.sorted_table_names();
334 let table_name = *names.get(usize::from(table_ord))?;
335 let rows = self.tables.get(table_name)?;
336 let row = usize::from(row_idx);
337 if row < rows.len() {
338 Some((table_name, row))
339 } else {
340 None
341 }
342 }
343}
344
345impl ElementOps for FInstance {
346 fn fiber(&self, vertex: &Name) -> Vec<u32> {
347 let vertex_str: &str = vertex.as_ref();
348 let Some(rows) = self.tables.get(vertex_str) else {
349 return Vec::new();
350 };
351 let Some(table_ord) = self.table_ordinal(vertex_str) else {
352 return Vec::new();
353 };
354 (0..rows.len())
355 .filter_map(|i| {
356 u16::try_from(i)
357 .ok()
358 .map(|ri| encode_finstance_id(table_ord, ri))
359 })
360 .collect()
361 }
362
363 fn pushforward(&self, elements: &[u32], edge_kind: &Name) -> Vec<u32> {
364 let element_set: rustc_hash::FxHashSet<u32> = elements.iter().copied().collect();
365 let names = self.ordinal_map();
366 let mut result = Vec::new();
367 let mut seen = rustc_hash::FxHashSet::default();
368
369 for (edge, pairs) in &self.foreign_keys {
370 if edge.kind != *edge_kind {
371 continue;
372 }
373 let Some(src_ord) = names
374 .binary_search(&&*edge.src)
375 .ok()
376 .and_then(|i| u16::try_from(i).ok())
377 else {
378 continue;
379 };
380 let Some(tgt_ord) = names
381 .binary_search(&&*edge.tgt)
382 .ok()
383 .and_then(|i| u16::try_from(i).ok())
384 else {
385 continue;
386 };
387
388 for &(src_row, tgt_row) in pairs {
389 let Ok(src_row_u16) = u16::try_from(src_row) else {
390 continue;
391 };
392 let Ok(tgt_row_u16) = u16::try_from(tgt_row) else {
393 continue;
394 };
395 let src_id = encode_finstance_id(src_ord, src_row_u16);
396 if element_set.contains(&src_id) {
397 let tgt_id = encode_finstance_id(tgt_ord, tgt_row_u16);
398 if seen.insert(tgt_id) {
399 result.push(tgt_id);
400 }
401 }
402 }
403 }
404
405 result
406 }
407
408 fn stalk(&self, element_id: u32) -> panproto_expr::Env {
409 let Some((table_name, row_idx)) = self.resolve_element(element_id) else {
410 return panproto_expr::Env::new();
411 };
412 let Some(rows) = self.tables.get(table_name) else {
413 return panproto_expr::Env::new();
414 };
415 let Some(row) = rows.get(row_idx) else {
416 return panproto_expr::Env::new();
417 };
418
419 let mut env = crate::wtype::build_env_from_extra_fields(row);
422
423 env = env.extend(Arc::from("_anchor"), Literal::Str(table_name.into()));
425 env = env.extend(Arc::from("_id"), Literal::Int(i64::from(element_id)));
426
427 let fk_count = self
429 .foreign_keys
430 .iter()
431 .filter(|(edge, _)| edge.src.as_ref() == table_name)
432 .flat_map(|(_, pairs)| pairs.iter())
433 .filter(|(src_row, _)| *src_row == row_idx)
434 .count();
435 #[allow(clippy::cast_possible_wrap)]
436 {
437 env = env.extend(Arc::from("_edge_count"), Literal::Int(fk_count as i64));
438 }
439
440 env
441 }
442
443 fn eval_graph_builtin(
444 &self,
445 op: BuiltinOp,
446 args: &[Literal],
447 context: Option<u32>,
448 ) -> Result<Literal, panproto_expr::ExprError> {
449 finstance_graph_builtin(self, op, args, context)
450 }
451
452 fn attributes(&self, element_id: u32) -> HashMap<String, Value> {
453 let Some((table_name, row_idx)) = self.resolve_element(element_id) else {
454 return HashMap::new();
455 };
456 self.tables
457 .get(table_name)
458 .and_then(|rows| rows.get(row_idx))
459 .cloned()
460 .unwrap_or_default()
461 }
462
463 fn sort(&self, element_id: u32) -> Option<Name> {
464 self.resolve_element(element_id)
465 .map(|(table_name, _)| Name::from(table_name))
466 }
467
468 fn element_value(&self, _element_id: u32) -> Option<FieldPresence> {
469 None
471 }
472}
473
474fn resolve_node_ref(lit: &Literal, context: Option<u32>) -> Result<u32, panproto_expr::ExprError> {
480 match lit {
481 Literal::Int(id) => u32::try_from(*id).map_err(|_| panproto_expr::ExprError::TypeError {
482 expected: "non-negative int fitting u32".into(),
483 got: format!("{id}"),
484 }),
485 Literal::Str(s) if s == "self" => context.ok_or_else(|| {
486 panproto_expr::ExprError::UnboundVariable("self (no context node)".into())
487 }),
488 _ => Err(panproto_expr::ExprError::TypeError {
489 expected: "int or \"self\"".into(),
490 got: lit.type_name().into(),
491 }),
492 }
493}
494
495fn node_to_literal(node: &Node) -> Literal {
497 let mut fields: Vec<(Arc<str>, Literal)> = Vec::new();
498 fields.push((Arc::from("_id"), Literal::Int(i64::from(node.id))));
499 fields.push((
500 Arc::from("_anchor"),
501 Literal::Str(node.anchor.as_ref().into()),
502 ));
503 for (key, val) in &node.extra_fields {
504 fields.push((Arc::from(key.as_str()), value_to_expr_literal(val)));
505 }
506 Literal::Record(fields)
507}
508
509fn winstance_graph_builtin(
511 instance: &WInstance,
512 op: BuiltinOp,
513 args: &[Literal],
514 context: Option<u32>,
515) -> Result<Literal, panproto_expr::ExprError> {
516 match op {
517 BuiltinOp::Edge => {
518 let node_id = resolve_node_ref(&args[0], context)?;
519 let edge_kind =
520 args[1]
521 .as_str()
522 .ok_or_else(|| panproto_expr::ExprError::TypeError {
523 expected: "string".into(),
524 got: args[1].type_name().into(),
525 })?;
526 let edge_name = Name::from(edge_kind);
527 for &(src, tgt, ref edge) in &instance.arcs {
528 if src == node_id && edge.kind == edge_name {
529 if let Some(node) = instance.nodes.get(&tgt) {
530 return Ok(node_to_literal(node));
531 }
532 return Ok(Literal::Null);
533 }
534 }
535 Ok(Literal::Null)
536 }
537 BuiltinOp::Children => {
538 let node_id = resolve_node_ref(&args[0], context)?;
539 let mut children = Vec::new();
540 for &(src, tgt, _) in &instance.arcs {
541 if src == node_id {
542 if let Some(node) = instance.nodes.get(&tgt) {
543 children.push(node_to_literal(node));
544 }
545 }
546 }
547 Ok(Literal::List(children))
548 }
549 BuiltinOp::HasEdge => {
550 let node_id = resolve_node_ref(&args[0], context)?;
551 let edge_kind =
552 args[1]
553 .as_str()
554 .ok_or_else(|| panproto_expr::ExprError::TypeError {
555 expected: "string".into(),
556 got: args[1].type_name().into(),
557 })?;
558 let edge_name = Name::from(edge_kind);
559 let found = instance
560 .arcs
561 .iter()
562 .any(|(src, _, edge)| *src == node_id && edge.kind == edge_name);
563 Ok(Literal::Bool(found))
564 }
565 BuiltinOp::EdgeCount => {
566 let node_id = resolve_node_ref(&args[0], context)?;
567 let count = instance
568 .arcs
569 .iter()
570 .filter(|(src, _, _)| *src == node_id)
571 .count();
572 #[allow(clippy::cast_possible_wrap)]
573 Ok(Literal::Int(count as i64))
574 }
575 BuiltinOp::Anchor => {
576 let node_id = resolve_node_ref(&args[0], context)?;
577 instance
578 .nodes
579 .get(&node_id)
580 .map_or(Ok(Literal::Null), |node| {
581 Ok(Literal::Str(node.anchor.as_ref().into()))
582 })
583 }
584 _ => Ok(Literal::Null),
585 }
586}
587
588fn ginstance_graph_builtin(
590 instance: &GInstance,
591 op: BuiltinOp,
592 args: &[Literal],
593 context: Option<u32>,
594) -> Result<Literal, panproto_expr::ExprError> {
595 match op {
596 BuiltinOp::Edge => {
597 let node_id = resolve_node_ref(&args[0], context)?;
598 let edge_kind =
599 args[1]
600 .as_str()
601 .ok_or_else(|| panproto_expr::ExprError::TypeError {
602 expected: "string".into(),
603 got: args[1].type_name().into(),
604 })?;
605 let edge_name = Name::from(edge_kind);
606 for &(src, tgt, ref edge) in &instance.edges {
607 if src == node_id && edge.kind == edge_name {
608 if let Some(node) = instance.nodes.get(&tgt) {
609 return Ok(node_to_literal(node));
610 }
611 return Ok(Literal::Null);
612 }
613 }
614 Ok(Literal::Null)
615 }
616 BuiltinOp::Children => {
617 let node_id = resolve_node_ref(&args[0], context)?;
618 let mut children = Vec::new();
619 for &(src, tgt, _) in &instance.edges {
620 if src == node_id {
621 if let Some(node) = instance.nodes.get(&tgt) {
622 children.push(node_to_literal(node));
623 }
624 }
625 }
626 Ok(Literal::List(children))
627 }
628 BuiltinOp::HasEdge => {
629 let node_id = resolve_node_ref(&args[0], context)?;
630 let edge_kind =
631 args[1]
632 .as_str()
633 .ok_or_else(|| panproto_expr::ExprError::TypeError {
634 expected: "string".into(),
635 got: args[1].type_name().into(),
636 })?;
637 let edge_name = Name::from(edge_kind);
638 let found = instance
639 .edges
640 .iter()
641 .any(|(src, _, edge)| *src == node_id && edge.kind == edge_name);
642 Ok(Literal::Bool(found))
643 }
644 BuiltinOp::EdgeCount => {
645 let node_id = resolve_node_ref(&args[0], context)?;
646 let count = instance
647 .edges
648 .iter()
649 .filter(|(src, _, _)| *src == node_id)
650 .count();
651 #[allow(clippy::cast_possible_wrap)]
652 Ok(Literal::Int(count as i64))
653 }
654 BuiltinOp::Anchor => {
655 let node_id = resolve_node_ref(&args[0], context)?;
656 instance
657 .nodes
658 .get(&node_id)
659 .map_or(Ok(Literal::Null), |node| {
660 Ok(Literal::Str(node.anchor.as_ref().into()))
661 })
662 }
663 _ => Ok(Literal::Null),
664 }
665}
666
667fn finstance_row_to_literal(row: &HashMap<String, Value>) -> Literal {
669 let fields: Vec<(Arc<str>, Literal)> = row
670 .iter()
671 .map(|(k, v)| (Arc::from(k.as_str()), value_to_expr_literal(v)))
672 .collect();
673 Literal::Record(fields)
674}
675
676fn finstance_graph_builtin(
678 instance: &FInstance,
679 op: BuiltinOp,
680 args: &[Literal],
681 context: Option<u32>,
682) -> Result<Literal, panproto_expr::ExprError> {
683 match op {
684 BuiltinOp::Edge => {
685 let element_id = resolve_node_ref(&args[0], context)?;
686 let edge_kind =
687 args[1]
688 .as_str()
689 .ok_or_else(|| panproto_expr::ExprError::TypeError {
690 expected: "string".into(),
691 got: args[1].type_name().into(),
692 })?;
693 let Some((src_table, src_row)) = instance.resolve_element(element_id) else {
694 return Ok(Literal::Null);
695 };
696 let edge_name = Name::from(edge_kind);
697 for (edge, pairs) in &instance.foreign_keys {
698 if edge.kind == edge_name && edge.src.as_ref() == src_table {
699 for &(s, t) in pairs {
700 if s == src_row {
701 if let Some(row) =
702 instance.tables.get(&*edge.tgt).and_then(|r| r.get(t))
703 {
704 return Ok(finstance_row_to_literal(row));
705 }
706 }
707 }
708 }
709 }
710 Ok(Literal::Null)
711 }
712 BuiltinOp::Children => {
713 let element_id = resolve_node_ref(&args[0], context)?;
714 let Some((src_table, src_row)) = instance.resolve_element(element_id) else {
715 return Ok(Literal::List(Vec::new()));
716 };
717 let mut children = Vec::new();
718 for (edge, pairs) in &instance.foreign_keys {
719 if edge.src.as_ref() != src_table {
720 continue;
721 }
722 for &(s, t) in pairs {
723 if s == src_row {
724 if let Some(row) = instance.tables.get(&*edge.tgt).and_then(|r| r.get(t)) {
725 children.push(finstance_row_to_literal(row));
726 }
727 }
728 }
729 }
730 Ok(Literal::List(children))
731 }
732 BuiltinOp::HasEdge => {
733 let element_id = resolve_node_ref(&args[0], context)?;
734 let edge_kind =
735 args[1]
736 .as_str()
737 .ok_or_else(|| panproto_expr::ExprError::TypeError {
738 expected: "string".into(),
739 got: args[1].type_name().into(),
740 })?;
741 let edge_name = Name::from(edge_kind);
742
743 let Some((src_table, src_row)) = instance.resolve_element(element_id) else {
744 return Ok(Literal::Bool(false));
745 };
746
747 let found = instance.foreign_keys.iter().any(|(edge, pairs)| {
748 edge.kind == edge_name
749 && edge.src.as_ref() == src_table
750 && pairs.iter().any(|(s, _)| *s == src_row)
751 });
752 Ok(Literal::Bool(found))
753 }
754 BuiltinOp::EdgeCount => {
755 let element_id = resolve_node_ref(&args[0], context)?;
756 let Some((src_table, src_row)) = instance.resolve_element(element_id) else {
757 return Ok(Literal::Int(0));
758 };
759
760 let count: usize = instance
761 .foreign_keys
762 .iter()
763 .filter(|(edge, _)| edge.src.as_ref() == src_table)
764 .flat_map(|(_, pairs)| pairs.iter())
765 .filter(|(s, _)| *s == src_row)
766 .count();
767 #[allow(clippy::cast_possible_wrap)]
768 Ok(Literal::Int(count as i64))
769 }
770 BuiltinOp::Anchor => {
771 let element_id = resolve_node_ref(&args[0], context)?;
772 instance
773 .resolve_element(element_id)
774 .map_or(Ok(Literal::Null), |(table_name, _)| {
775 Ok(Literal::Str(table_name.into()))
776 })
777 }
778 _ => Ok(Literal::Null),
779 }
780}
781
782#[cfg(test)]
787#[allow(clippy::unwrap_used)]
788mod tests {
789 use super::*;
790 use panproto_schema::Edge as SchemaEdge;
791
792 fn make_winstance_with_children() -> WInstance {
795 let mut nodes = HashMap::new();
796
797 nodes.insert(0, Node::new(0, "binding"));
799
800 nodes.insert(
802 1,
803 Node::new(1, "binding.var").with_value(FieldPresence::Present(Value::Str("x0".into()))),
804 );
805
806 nodes.insert(
808 2,
809 Node::new(2, "binding.type")
810 .with_value(FieldPresence::Present(Value::Str("noun".into()))),
811 );
812
813 let arcs = vec![
814 (
815 0,
816 1,
817 SchemaEdge {
818 src: "binding".into(),
819 tgt: "binding.var".into(),
820 kind: "prop".into(),
821 name: Some("var".into()),
822 },
823 ),
824 (
825 0,
826 2,
827 SchemaEdge {
828 src: "binding".into(),
829 tgt: "binding.type".into(),
830 kind: "prop".into(),
831 name: Some("type".into()),
832 },
833 ),
834 ];
835
836 WInstance::new(nodes, arcs, vec![], 0, Name::from("binding"))
837 }
838
839 #[test]
840 fn winstance_fiber_selects_by_anchor() {
841 let inst = make_winstance_with_children();
842 let fibers = inst.fiber(&Name::from("binding"));
843 assert_eq!(fibers.len(), 1);
844 assert_eq!(fibers[0], 0);
845 }
846
847 #[test]
848 fn winstance_pushforward_follows_arcs() {
849 let inst = make_winstance_with_children();
850 let children = inst.pushforward(&[0], &Name::from("prop"));
851 assert_eq!(children.len(), 2);
852 assert!(children.contains(&1));
853 assert!(children.contains(&2));
854 }
855
856 #[test]
857 fn winstance_stalk_includes_child_scalars() {
858 let inst = make_winstance_with_children();
859 let env = inst.stalk(0);
860 let config = panproto_expr::EvalConfig::default();
862 let var_expr = panproto_expr::Expr::Var("var".into());
863 let result = panproto_expr::eval(&var_expr, &env, &config);
864 assert_eq!(result.unwrap(), Literal::Str("x0".into()));
865
866 let type_expr = panproto_expr::Expr::Var("type".into());
867 let result = panproto_expr::eval(&type_expr, &env, &config);
868 assert_eq!(result.unwrap(), Literal::Str("noun".into()));
869 }
870
871 #[test]
872 fn winstance_attributes_includes_child_scalars() {
873 let inst = make_winstance_with_children();
874 let attrs = inst.attributes(0);
875 assert_eq!(attrs.get("var"), Some(&Value::Str("x0".into())));
876 assert_eq!(attrs.get("type"), Some(&Value::Str("noun".into())));
877 }
878
879 #[test]
880 fn winstance_extra_fields_override_child_scalars() {
881 let mut nodes = HashMap::new();
882 let mut parent = Node::new(0, "thing");
883 parent
885 .extra_fields
886 .insert("name".into(), Value::Str("override".into()));
887 nodes.insert(0, parent);
888
889 nodes.insert(
891 1,
892 Node::new(1, "thing.name")
893 .with_value(FieldPresence::Present(Value::Str("original".into()))),
894 );
895
896 let arcs = vec![(
897 0,
898 1,
899 SchemaEdge {
900 src: "thing".into(),
901 tgt: "thing.name".into(),
902 kind: "prop".into(),
903 name: Some("name".into()),
904 },
905 )];
906
907 let inst = WInstance::new(nodes, arcs, vec![], 0, Name::from("thing"));
908 let attrs = inst.attributes(0);
909 assert_eq!(attrs.get("name"), Some(&Value::Str("override".into())));
911 }
912
913 fn make_ginstance_with_values() -> GInstance {
916 let mut person_a = Node::new(0, "person");
917 person_a
918 .extra_fields
919 .insert("role".into(), Value::Str("manager".into()));
920
921 GInstance::new()
922 .with_node(person_a)
923 .with_node(Node::new(1, "person"))
924 .with_node(Node::new(2, "department"))
925 .with_edge(
926 0,
927 1,
928 SchemaEdge {
929 src: "person".into(),
930 tgt: "person".into(),
931 kind: "knows".into(),
932 name: None,
933 },
934 )
935 .with_edge(
936 0,
937 2,
938 SchemaEdge {
939 src: "person".into(),
940 tgt: "department".into(),
941 kind: "works_in".into(),
942 name: Some("department".into()),
943 },
944 )
945 .with_value(0, Value::Str("Alice".into()))
946 .with_value(1, Value::Str("Bob".into()))
947 .with_value(2, Value::Str("Engineering".into()))
948 }
949
950 #[test]
951 fn ginstance_fiber_selects_by_anchor() {
952 let g = make_ginstance_with_values();
953 let persons = g.fiber(&Name::from("person"));
954 assert_eq!(persons.len(), 2);
955 let depts = g.fiber(&Name::from("department"));
956 assert_eq!(depts.len(), 1);
957 }
958
959 #[test]
960 fn ginstance_pushforward_with_dedup() {
961 let g = GInstance::new()
963 .with_node(Node::new(0, "a"))
964 .with_node(Node::new(1, "a"))
965 .with_node(Node::new(2, "b"))
966 .with_edge(
967 0,
968 2,
969 SchemaEdge {
970 src: "a".into(),
971 tgt: "b".into(),
972 kind: "link".into(),
973 name: None,
974 },
975 )
976 .with_edge(
977 1,
978 2,
979 SchemaEdge {
980 src: "a".into(),
981 tgt: "b".into(),
982 kind: "link".into(),
983 name: None,
984 },
985 );
986
987 let result = g.pushforward(&[0, 1], &Name::from("link"));
989 assert_eq!(result.len(), 1);
990 assert_eq!(result[0], 2);
991 }
992
993 #[test]
994 fn ginstance_stalk_includes_edge_target_values() {
995 let g = make_ginstance_with_values();
996 let env = g.stalk(0);
997 let config = panproto_expr::EvalConfig::default();
998
999 let dept_expr = panproto_expr::Expr::Var("department".into());
1001 let result = panproto_expr::eval(&dept_expr, &env, &config);
1002 assert_eq!(result.unwrap(), Literal::Str("Engineering".into()));
1003
1004 let role_expr = panproto_expr::Expr::Var("role".into());
1006 let result = panproto_expr::eval(&role_expr, &env, &config);
1007 assert_eq!(result.unwrap(), Literal::Str("manager".into()));
1008
1009 let val_expr = panproto_expr::Expr::Var("_value".into());
1011 let result = panproto_expr::eval(&val_expr, &env, &config);
1012 assert_eq!(result.unwrap(), Literal::Str("Alice".into()));
1013 }
1014
1015 #[test]
1016 fn ginstance_graph_builtins() {
1017 let g = make_ginstance_with_values();
1018
1019 let count = g
1021 .eval_graph_builtin(BuiltinOp::EdgeCount, &[Literal::Int(0)], Some(0))
1022 .unwrap();
1023 assert_eq!(count, Literal::Int(2));
1024
1025 let has = g
1027 .eval_graph_builtin(
1028 BuiltinOp::HasEdge,
1029 &[Literal::Int(0), Literal::Str("knows".into())],
1030 Some(0),
1031 )
1032 .unwrap();
1033 assert_eq!(has, Literal::Bool(true));
1034
1035 let has_not = g
1037 .eval_graph_builtin(
1038 BuiltinOp::HasEdge,
1039 &[Literal::Int(0), Literal::Str("nonexistent".into())],
1040 Some(0),
1041 )
1042 .unwrap();
1043 assert_eq!(has_not, Literal::Bool(false));
1044
1045 let anchor = g
1047 .eval_graph_builtin(BuiltinOp::Anchor, &[Literal::Int(2)], Some(0))
1048 .unwrap();
1049 assert_eq!(anchor, Literal::Str("department".into()));
1050 }
1051
1052 fn make_finstance_with_fk() -> FInstance {
1055 let mut alice = HashMap::new();
1056 alice.insert("name".to_string(), Value::Str("Alice".into()));
1057 alice.insert("age".to_string(), Value::Int(30));
1058
1059 let mut bob = HashMap::new();
1060 bob.insert("name".to_string(), Value::Str("Bob".into()));
1061 bob.insert("age".to_string(), Value::Int(25));
1062
1063 let mut post1 = HashMap::new();
1064 post1.insert("title".to_string(), Value::Str("Hello".into()));
1065
1066 let fk = SchemaEdge {
1067 src: "posts".into(),
1068 tgt: "users".into(),
1069 kind: "author".into(),
1070 name: Some("author".into()),
1071 };
1072
1073 FInstance::new()
1074 .with_table("users", vec![alice, bob])
1075 .with_table("posts", vec![post1])
1076 .with_foreign_key(fk, vec![(0, 0)]) }
1078
1079 #[test]
1080 fn finstance_fiber_returns_synthetic_ids() {
1081 let f = make_finstance_with_fk();
1082 let users = f.fiber(&Name::from("users"));
1083 assert_eq!(users.len(), 2);
1084
1085 for &id in &users {
1087 assert_eq!(f.sort(id), Some(Name::from("users")));
1088 }
1089
1090 let posts = f.fiber(&Name::from("posts"));
1091 assert_eq!(posts.len(), 1);
1092 for &id in &posts {
1093 assert_eq!(f.sort(id), Some(Name::from("posts")));
1094 }
1095 }
1096
1097 #[test]
1098 fn finstance_pushforward_follows_fk() {
1099 let f = make_finstance_with_fk();
1100 let posts = f.fiber(&Name::from("posts"));
1101 let authors = f.pushforward(&posts, &Name::from("author"));
1102 assert_eq!(authors.len(), 1);
1103 let attrs = f.attributes(authors[0]);
1105 assert_eq!(attrs.get("name"), Some(&Value::Str("Alice".into())));
1106 }
1107
1108 #[test]
1109 fn finstance_stalk_binds_columns() {
1110 let f = make_finstance_with_fk();
1111 let users = f.fiber(&Name::from("users"));
1112 let alice_id = users
1114 .iter()
1115 .find(|&&id| {
1116 let attrs = f.attributes(id);
1117 attrs.get("name") == Some(&Value::Str("Alice".into()))
1118 })
1119 .copied()
1120 .unwrap();
1121
1122 let env = f.stalk(alice_id);
1123 let config = panproto_expr::EvalConfig::default();
1124 let name_expr = panproto_expr::Expr::Var("name".into());
1125 let result = panproto_expr::eval(&name_expr, &env, &config);
1126 assert_eq!(result.unwrap(), Literal::Str("Alice".into()));
1127
1128 let age_expr = panproto_expr::Expr::Var("age".into());
1129 let result = panproto_expr::eval(&age_expr, &env, &config);
1130 assert_eq!(result.unwrap(), Literal::Int(30));
1131 }
1132
1133 #[test]
1134 fn finstance_round_trip_encoding() {
1135 let id = encode_finstance_id(3, 42);
1136 let (table_ord, row_idx) = decode_finstance_id(id);
1137 assert_eq!(table_ord, 3);
1138 assert_eq!(row_idx, 42);
1139 }
1140
1141 #[test]
1142 fn finstance_graph_builtins() {
1143 let f = make_finstance_with_fk();
1144 let posts = f.fiber(&Name::from("posts"));
1145 let post_id = posts[0];
1146
1147 let count = f
1149 .eval_graph_builtin(
1150 BuiltinOp::EdgeCount,
1151 &[Literal::Int(i64::from(post_id))],
1152 Some(post_id),
1153 )
1154 .unwrap();
1155 assert_eq!(count, Literal::Int(1));
1156
1157 let has = f
1159 .eval_graph_builtin(
1160 BuiltinOp::HasEdge,
1161 &[
1162 Literal::Int(i64::from(post_id)),
1163 Literal::Str("author".into()),
1164 ],
1165 Some(post_id),
1166 )
1167 .unwrap();
1168 assert_eq!(has, Literal::Bool(true));
1169
1170 let anchor = f
1172 .eval_graph_builtin(
1173 BuiltinOp::Anchor,
1174 &[Literal::Int(i64::from(post_id))],
1175 Some(post_id),
1176 )
1177 .unwrap();
1178 assert_eq!(anchor, Literal::Str("posts".into()));
1179 }
1180}