use std::collections::{HashMap, HashSet, VecDeque};
use crate::expr::function::{Function, FunctionCall};
use crate::expr::visit::{Visit, Visitor};
use crate::expr::{Expr, Idiom, Literal, Part};
const TARGET: &str = "surrealdb::core::perms";
#[derive(Clone, Debug, Default, Eq, PartialEq, Hash)]
pub struct ComputedDeps {
pub fields: Vec<String>,
pub is_complete: bool,
}
pub(crate) fn extract_computed_deps(expr: &Expr) -> ComputedDeps {
let mut extractor = FieldDependencyExtractor {
deps: HashSet::new(),
is_complete: true,
};
let _ = extractor.visit_expr(expr);
ComputedDeps {
fields: {
let mut fields: Vec<String> = extractor.deps.into_iter().collect();
fields.sort();
fields
},
is_complete: extractor.is_complete,
}
}
fn field_name_from_part(part: &Part) -> Option<String> {
match part {
Part::Field(name) => Some(name.as_str().to_owned()),
Part::Value(Expr::Literal(Literal::String(name))) => Some(name.as_str().to_owned()),
_ => None,
}
}
struct FieldDependencyExtractor {
deps: HashSet<String>,
is_complete: bool,
}
impl Visitor for FieldDependencyExtractor {
type Error = std::convert::Infallible;
fn visit_idiom(&mut self, idiom: &Idiom) -> Result<(), Self::Error> {
let consumed = match idiom.0.as_slice() {
[Part::Field(name), ..] => {
self.deps.insert(name.as_str().to_owned());
1
}
[Part::Start(Expr::Param(p)), second, ..] if matches!(p.as_str(), "this" | "self") => {
match field_name_from_part(second) {
Some(name) => {
self.deps.insert(name);
2
}
None => 0,
}
}
_ => 0,
};
for p in idiom.0.iter().skip(consumed) {
self.visit_part(p)?;
}
Ok(())
}
fn visit_expr(&mut self, expr: &Expr) -> Result<(), Self::Error> {
match expr {
Expr::Literal(_)
| Expr::Idiom(_)
| Expr::Table(_)
| Expr::Mock(_)
| Expr::Constant(_)
| Expr::Break
| Expr::Continue
| Expr::Prefix { .. }
| Expr::Postfix { .. }
| Expr::Binary { .. }
| Expr::Block(_)
| Expr::IfElse(_)
| Expr::Foreach(_)
| Expr::Let(_)
| Expr::Return(_)
| Expr::Throw(_)
| Expr::Explain { .. }
| Expr::Sleep(_)
| Expr::Info(_) => {
expr.visit(self)?;
}
Expr::Select(_)
| Expr::Create(_)
| Expr::Update(_)
| Expr::Upsert(_)
| Expr::Delete(_)
| Expr::Relate(_)
| Expr::Insert(_)
| Expr::Closure(_)
| Expr::Define(_)
| Expr::Remove(_)
| Expr::Rebuild(_)
| Expr::Alter(_) => {
self.is_complete = false;
expr.visit(self)?;
}
#[cfg(feature = "gql")]
Expr::Match(_) => {
self.is_complete = false;
expr.visit(self)?;
}
Expr::Param(_) => {
self.is_complete = false;
}
Expr::FunctionCall(call) => {
self.visit_function_call_with_projection(call)?;
}
}
Ok(())
}
fn visit_part(&mut self, part: &Part) -> Result<(), Self::Error> {
match part {
Part::All
| Part::Flatten
| Part::Last
| Part::First
| Part::Field(_)
| Part::Optional
| Part::Doc
| Part::RepeatRecurse => {}
Part::Where(_) | Part::Value(_) | Part::Destructure(_) | Part::Recurse(_, _, _) => {
part.visit(self)?;
}
Part::Lookup(_) | Part::Start(_) | Part::Method(_, _) => {
self.is_complete = false;
part.visit(self)?;
}
}
Ok(())
}
}
impl FieldDependencyExtractor {
fn visit_function_call_with_projection(
&mut self,
call: &FunctionCall,
) -> Result<(), std::convert::Infallible> {
if let Function::Normal(name) = &call.receiver {
match name.as_str() {
"type::field" => self.analyse_type_field(&call.arguments),
"type::fields" => self.analyse_type_fields(&call.arguments),
_ => {}
}
}
call.visit(self)?;
Ok(())
}
fn analyse_type_field(&mut self, args: &[Expr]) {
match args {
[Expr::Literal(Literal::String(s))] => match parse_idiom_root(s.as_str()) {
Some(root) => {
self.deps.insert(root);
}
None => {
self.is_complete = false;
}
},
_ => {
self.is_complete = false;
}
}
}
fn analyse_type_fields(&mut self, args: &[Expr]) {
let [Expr::Literal(Literal::Array(items))] = args else {
self.is_complete = false;
return;
};
let mut pending = Vec::with_capacity(items.len());
for item in items {
let Expr::Literal(Literal::String(s)) = item else {
self.is_complete = false;
return;
};
let Some(root) = parse_idiom_root(s.as_str()) else {
self.is_complete = false;
return;
};
pending.push(root);
}
self.deps.extend(pending);
}
}
fn parse_idiom_root(s: &str) -> Option<String> {
let idi: Idiom = crate::syn::idiom(s).ok()?.into();
match idi.0.first()? {
Part::Field(name) => Some(name.as_str().to_owned()),
_ => None,
}
}
pub(crate) fn warn_incomplete_perm_deps(table: &str, field: &str) {
tracing::warn!(
target: TARGET,
table = %table,
field = %field,
"Field-permission expression has opaque dependencies; \
all computed fields on the named table will be evaluated for every \
row. Inspect with `INFO FOR TABLE`."
);
}
pub fn topological_sort_computed_fields(fields: &[(String, Vec<String>)]) -> Vec<usize> {
if fields.is_empty() {
return Vec::new();
}
let name_to_idx: HashMap<&str, usize> =
fields.iter().enumerate().map(|(i, (name, _))| (name.as_str(), i)).collect();
let mut in_degree = vec![0usize; fields.len()];
let mut dependents: Vec<Vec<usize>> = vec![Vec::new(); fields.len()];
for (idx, (_, deps)) in fields.iter().enumerate() {
for dep in deps {
if let Some(&dep_idx) = name_to_idx.get(dep.as_str()) {
in_degree[idx] += 1;
dependents[dep_idx].push(idx);
}
}
}
let mut queue: VecDeque<usize> = VecDeque::new();
for (idx, °) in in_degree.iter().enumerate() {
if deg == 0 {
queue.push_back(idx);
}
}
let mut result = Vec::with_capacity(fields.len());
while let Some(idx) = queue.pop_front() {
result.push(idx);
for &dependent in &dependents[idx] {
in_degree[dependent] -= 1;
if in_degree[dependent] == 0 {
queue.push_back(dependent);
}
}
}
if result.len() < fields.len() {
for idx in 0..fields.len() {
if !result.contains(&idx) {
result.push(idx);
}
}
}
result
}
pub fn resolve_required_computed_fields(
needed: &HashSet<String>,
computed_deps: &HashMap<String, ComputedDeps>,
) -> Option<HashSet<String>> {
let mut required: HashSet<String> = HashSet::new();
let mut worklist: Vec<String> = needed.iter().cloned().collect();
while let Some(field) = worklist.pop() {
if !required.insert(field.clone()) {
continue; }
if let Some(deps) = computed_deps.get(&field) {
if !deps.is_complete {
return None;
}
for dep in &deps.fields {
if !required.contains(dep) {
worklist.push(dep.clone());
}
}
}
}
Some(required)
}
#[cfg(test)]
mod tests {
use surrealdb_strand::Strand;
use super::*;
use crate::expr::operator::BinaryOperator;
use crate::expr::{Literal, Part};
fn extract(expr: &Expr) -> ComputedDeps {
extract_computed_deps(expr)
}
fn field_expr(name: &str) -> Expr {
Expr::Idiom(Idiom(vec![Part::Field(name.into())]))
}
fn int_expr(n: i64) -> Expr {
Expr::Literal(Literal::Integer(n))
}
fn str_lit(s: &str) -> Expr {
Expr::Literal(Literal::String(Strand::new(s)))
}
fn fn_call(name: &str, args: Vec<Expr>) -> Expr {
Expr::FunctionCall(Box::new(crate::expr::function::FunctionCall {
receiver: crate::expr::function::Function::Normal(name.to_owned()),
arguments: args,
}))
}
#[test]
fn simple_field_reference() {
let expr = field_expr("b");
let deps = extract(&expr);
assert_eq!(deps.fields, vec!["b"]);
assert!(deps.is_complete);
}
#[test]
fn binary_expression_two_fields() {
let expr = Expr::Binary {
left: Box::new(field_expr("b")),
op: BinaryOperator::Add,
right: Box::new(field_expr("c")),
};
let deps = extract(&expr);
assert_eq!(deps.fields, vec!["b", "c"]);
assert!(deps.is_complete);
}
#[test]
fn field_plus_literal() {
let expr = Expr::Binary {
left: Box::new(field_expr("d")),
op: BinaryOperator::Add,
right: Box::new(int_expr(1)),
};
let deps = extract(&expr);
assert_eq!(deps.fields, vec!["d"]);
assert!(deps.is_complete);
}
#[test]
fn nested_field_access() {
let expr = Expr::Idiom(Idiom(vec![
Part::Field(Strand::new_static("user")),
Part::Field(Strand::new_static("name")),
Part::Field(Strand::new_static("first")),
]));
let deps = extract(&expr);
assert_eq!(deps.fields, vec!["user"]);
assert!(deps.is_complete);
}
#[test]
fn param_marks_incomplete() {
let expr = Expr::Param(crate::expr::Param::from("param".to_string()));
let deps = extract(&expr);
assert!(deps.fields.is_empty());
assert!(!deps.is_complete);
}
#[test]
fn no_deps_literal_only() {
let expr = Expr::Binary {
left: Box::new(int_expr(55)),
op: BinaryOperator::Multiply,
right: Box::new(int_expr(1000)),
};
let deps = extract(&expr);
assert!(deps.fields.is_empty());
assert!(deps.is_complete);
}
#[test]
fn deduplicates_deps() {
let expr = Expr::Binary {
left: Box::new(field_expr("a")),
op: BinaryOperator::Add,
right: Box::new(field_expr("a")),
};
let deps = extract(&expr);
assert_eq!(deps.fields, vec!["a"]);
assert!(deps.is_complete);
}
#[test]
fn self_reference() {
let expr = Expr::Binary {
left: Box::new(field_expr("a")),
op: BinaryOperator::Multiply,
right: Box::new(int_expr(2)),
};
let deps = extract(&expr);
assert_eq!(deps.fields, vec!["a"]);
assert!(deps.is_complete);
}
fn this_field_expr(name: &str) -> Expr {
Expr::Idiom(Idiom(vec![
Part::Start(Expr::Param(crate::expr::Param::from("this".to_string()))),
Part::Field(name.into()),
]))
}
fn self_field_expr(name: &str) -> Expr {
Expr::Idiom(Idiom(vec![
Part::Start(Expr::Param(crate::expr::Param::from("self".to_string()))),
Part::Field(name.into()),
]))
}
#[test]
fn this_dot_field() {
let deps = extract(&this_field_expr("a"));
assert_eq!(deps.fields, vec!["a"]);
assert!(deps.is_complete);
}
#[test]
fn self_dot_field() {
let deps = extract(&self_field_expr("a"));
assert_eq!(deps.fields, vec!["a"]);
assert!(deps.is_complete);
}
#[test]
fn this_dot_nested() {
let expr = Expr::Idiom(Idiom(vec![
Part::Start(Expr::Param(crate::expr::Param::from("this".to_string()))),
Part::Field(Strand::new_static("a")),
Part::Field(Strand::new_static("b")),
Part::Field(Strand::new_static("c")),
]));
let deps = extract(&expr);
assert_eq!(deps.fields, vec!["a"]);
assert!(deps.is_complete);
}
#[test]
fn this_dot_field_in_binary() {
let expr = Expr::Binary {
left: Box::new(this_field_expr("a")),
op: BinaryOperator::Add,
right: Box::new(this_field_expr("b")),
};
let deps = extract(&expr);
assert_eq!(deps.fields, vec!["a", "b"]);
assert!(deps.is_complete);
}
#[test]
fn mixed_bare_and_this() {
let expr = Expr::Binary {
left: Box::new(Expr::Binary {
left: Box::new(field_expr("a")),
op: BinaryOperator::Add,
right: Box::new(this_field_expr("b")),
}),
op: BinaryOperator::Add,
right: Box::new(self_field_expr("c")),
};
let deps = extract(&expr);
assert_eq!(deps.fields, vec!["a", "b", "c"]);
assert!(deps.is_complete);
}
#[test]
fn this_in_function_args() {
use crate::expr::function::{Function, FunctionCall};
let expr = Expr::FunctionCall(Box::new(FunctionCall {
receiver: Function::Normal("math::sum".to_string()),
arguments: vec![Expr::Literal(Literal::Array(vec![
this_field_expr("a"),
field_expr("b"),
self_field_expr("c"),
]))],
}));
let deps = extract(&expr);
assert_eq!(deps.fields, vec!["a", "b", "c"]);
assert!(deps.is_complete);
}
#[test]
fn this_in_nested_parens() {
let expr = Expr::Binary {
left: Box::new(field_expr("a")),
op: BinaryOperator::Add,
right: Box::new(Expr::Binary {
left: Box::new(this_field_expr("b")),
op: BinaryOperator::Add,
right: Box::new(self_field_expr("c")),
}),
};
let deps = extract(&expr);
assert_eq!(deps.fields, vec!["a", "b", "c"]);
assert!(deps.is_complete);
}
#[test]
fn this_alone_marks_incomplete() {
let expr = Expr::Param(crate::expr::Param::from("this".to_string()));
let deps = extract(&expr);
assert!(deps.fields.is_empty());
assert!(!deps.is_complete);
}
#[test]
fn this_dot_wildcard_marks_incomplete() {
let expr = Expr::Idiom(Idiom(vec![
Part::Start(Expr::Param(crate::expr::Param::from("this".to_string()))),
Part::All,
]));
let deps = extract(&expr);
assert!(deps.fields.is_empty());
assert!(!deps.is_complete);
}
#[test]
fn this_bracket_string_field() {
let expr = Expr::Idiom(Idiom(vec![
Part::Start(Expr::Param(crate::expr::Param::from("this".to_string()))),
Part::Value(Expr::Literal(Literal::String(Strand::new_static("a")))),
]));
let deps = extract(&expr);
assert_eq!(deps.fields, vec!["a"]);
assert!(deps.is_complete);
}
#[test]
fn self_bracket_string_field() {
let expr = Expr::Idiom(Idiom(vec![
Part::Start(Expr::Param(crate::expr::Param::from("self".to_string()))),
Part::Value(Expr::Literal(Literal::String(Strand::new_static("c")))),
]));
let deps = extract(&expr);
assert_eq!(deps.fields, vec!["c"]);
assert!(deps.is_complete);
}
#[test]
fn mixed_dot_and_bracket() {
let expr = Expr::Binary {
left: Box::new(field_expr("a")),
op: BinaryOperator::Add,
right: Box::new(Expr::Binary {
left: Box::new(this_field_expr("b")),
op: BinaryOperator::Add,
right: Box::new(Expr::Idiom(Idiom(vec![
Part::Start(Expr::Param(crate::expr::Param::from("self".to_string()))),
Part::Value(Expr::Literal(Literal::String(Strand::new_static("c")))),
]))),
}),
};
let deps = extract(&expr);
assert_eq!(deps.fields, vec!["a", "b", "c"]);
assert!(deps.is_complete);
}
#[test]
fn other_param_dot_field_marks_incomplete() {
let expr = Expr::Idiom(Idiom(vec![
Part::Start(Expr::Param(crate::expr::Param::from("foo".to_string()))),
Part::Field(Strand::new_static("a")),
]));
let deps = extract(&expr);
assert!(!deps.is_complete);
}
fn parse(s: &str) -> Expr {
crate::syn::expr(s).expect("test expression must parse").into()
}
#[test]
fn select_subquery_marks_incomplete() {
let expr = parse("(SELECT * FROM t) + b");
let deps = extract(&expr);
assert!(!deps.is_complete);
assert!(deps.fields.contains(&"b".to_string()));
}
#[test]
fn create_subquery_marks_incomplete() {
let expr = parse("(CREATE t SET x = 1).y");
let deps = extract(&expr);
assert!(!deps.is_complete);
}
#[test]
fn update_subquery_marks_incomplete() {
let expr = parse("(UPDATE t SET x = 1).y");
let deps = extract(&expr);
assert!(!deps.is_complete);
}
#[test]
fn upsert_subquery_marks_incomplete() {
let expr = parse("(UPSERT t SET x = 1).y");
let deps = extract(&expr);
assert!(!deps.is_complete);
}
#[test]
fn delete_subquery_marks_incomplete() {
let expr = parse("(DELETE t).y");
let deps = extract(&expr);
assert!(!deps.is_complete);
}
#[test]
fn relate_subquery_marks_incomplete() {
let expr = parse("(RELATE a:1 -> rel -> b:1).y");
let deps = extract(&expr);
assert!(!deps.is_complete);
}
#[test]
fn insert_subquery_marks_incomplete() {
let expr = parse("(INSERT INTO t { x: 1 }).y");
let deps = extract(&expr);
assert!(!deps.is_complete);
}
#[test]
fn closure_marks_incomplete() {
let expr = parse("|$x: any| $x + a");
let deps = extract(&expr);
assert!(!deps.is_complete);
}
#[test]
fn graph_lookup_marks_incomplete() {
let expr = parse("->friends->person");
let deps = extract(&expr);
assert!(!deps.is_complete);
}
#[test]
fn start_with_arbitrary_expr_marks_incomplete() {
let expr = parse("(a + b).field");
let deps = extract(&expr);
assert!(!deps.is_complete);
}
#[test]
fn subquery_inside_function_args() {
let expr = parse("array::len([a, (SELECT * FROM t)])");
let deps = extract(&expr);
assert!(!deps.is_complete);
assert!(deps.fields.contains(&"a".to_string()));
}
#[test]
fn subquery_inside_ifelse() {
let expr = parse("IF a > 0 { (SELECT * FROM t) } ELSE { b }");
let deps = extract(&expr);
assert!(!deps.is_complete);
assert!(deps.fields.contains(&"a".to_string()));
assert!(deps.fields.contains(&"b".to_string()));
}
#[test]
fn ifelse_extracts_branch_deps() {
let expr = parse("IF a { b } ELSE { c }");
let deps = extract(&expr);
assert!(deps.is_complete);
assert!(deps.fields.contains(&"a".to_string()));
assert!(deps.fields.contains(&"b".to_string()));
assert!(deps.fields.contains(&"c".to_string()));
}
#[test]
fn type_field_literal_extracts_root() {
let expr = fn_call("type::field", vec![str_lit("admin_only")]);
let deps = extract(&expr);
assert!(deps.is_complete);
assert_eq!(deps.fields, vec!["admin_only"]);
}
#[test]
fn type_field_dotted_extracts_root() {
let expr = fn_call("type::field", vec![str_lit("user.name")]);
let deps = extract(&expr);
assert!(deps.is_complete);
assert_eq!(deps.fields, vec!["user"]);
}
#[test]
fn type_field_dollar_this_marks_incomplete() {
let expr = fn_call("type::field", vec![str_lit("$this.x")]);
let deps = extract(&expr);
assert!(!deps.is_complete);
}
#[test]
fn type_field_param_arg_marks_incomplete() {
let expr =
fn_call("type::field", vec![Expr::Param(crate::expr::Param::from("name".to_string()))]);
let deps = extract(&expr);
assert!(!deps.is_complete);
}
#[test]
fn type_field_concat_arg_marks_incomplete() {
let expr = fn_call(
"type::field",
vec![Expr::Binary {
left: Box::new(str_lit("a")),
op: BinaryOperator::Add,
right: Box::new(field_expr("b")),
}],
);
let deps = extract(&expr);
assert!(!deps.is_complete);
assert!(deps.fields.contains(&"b".to_string()));
}
#[test]
fn type_fields_array_of_literals_extracts_roots() {
let expr = fn_call(
"type::fields",
vec![Expr::Literal(Literal::Array(vec![str_lit("a"), str_lit("b.c")]))],
);
let deps = extract(&expr);
assert!(deps.is_complete);
assert_eq!(deps.fields, vec!["a", "b"]);
}
#[test]
fn type_fields_with_dynamic_element_marks_incomplete() {
let expr =
fn_call("type::fields", vec![Expr::Literal(Literal::Array(vec![field_expr("a")]))]);
let deps = extract(&expr);
assert!(!deps.is_complete);
}
#[test]
fn type_fields_non_array_marks_incomplete() {
let expr = fn_call(
"type::fields",
vec![Expr::Param(crate::expr::Param::from("names".to_string()))],
);
let deps = extract(&expr);
assert!(!deps.is_complete);
}
#[test]
fn type_field_combined_with_other_field() {
let expr = Expr::Binary {
left: Box::new(fn_call("type::field", vec![str_lit("a")])),
op: BinaryOperator::Add,
right: Box::new(field_expr("b")),
};
let deps = extract(&expr);
assert!(deps.is_complete);
assert_eq!(deps.fields, vec!["a", "b"]);
}
#[test]
fn topo_sort_empty() {
let result = topological_sort_computed_fields(&[]);
assert!(result.is_empty());
}
#[test]
fn topo_sort_no_deps() {
let fields = vec![("b".to_string(), vec![]), ("a".to_string(), vec![])];
let order = topological_sort_computed_fields(&fields);
assert_eq!(order.len(), 2);
assert!(order.contains(&0));
assert!(order.contains(&1));
}
#[test]
fn topo_sort_linear_chain() {
let fields = vec![
("a".to_string(), vec!["b".to_string()]),
("b".to_string(), vec!["c".to_string()]),
("c".to_string(), vec![]),
];
let order = topological_sort_computed_fields(&fields);
assert_eq!(order.len(), 3);
let pos_a = order.iter().position(|&x| x == 0).unwrap();
let pos_b = order.iter().position(|&x| x == 1).unwrap();
let pos_c = order.iter().position(|&x| x == 2).unwrap();
assert!(pos_c < pos_b);
assert!(pos_b < pos_a);
}
#[test]
fn topo_sort_diamond() {
let fields = vec![
("a".to_string(), vec!["b".to_string(), "c".to_string()]),
("b".to_string(), vec!["d".to_string()]),
("c".to_string(), vec!["d".to_string()]),
("d".to_string(), vec![]),
];
let order = topological_sort_computed_fields(&fields);
assert_eq!(order.len(), 4);
let pos_a = order.iter().position(|&x| x == 0).unwrap();
let pos_b = order.iter().position(|&x| x == 1).unwrap();
let pos_c = order.iter().position(|&x| x == 2).unwrap();
let pos_d = order.iter().position(|&x| x == 3).unwrap();
assert!(pos_d < pos_b);
assert!(pos_d < pos_c);
assert!(pos_b < pos_a);
assert!(pos_c < pos_a);
}
#[test]
fn topo_sort_dep_on_stored_field() {
let fields = vec![("a".to_string(), vec!["stored".to_string()])];
let order = topological_sort_computed_fields(&fields);
assert_eq!(order, vec![0]);
}
#[test]
fn closure_simple() {
let mut computed = HashMap::new();
computed.insert(
"a".to_string(),
ComputedDeps {
fields: vec!["b".to_string(), "c".to_string()],
is_complete: true,
},
);
computed.insert(
"b".to_string(),
ComputedDeps {
fields: vec![],
is_complete: true,
},
);
computed.insert(
"c".to_string(),
ComputedDeps {
fields: vec!["d".to_string()],
is_complete: true,
},
);
let needed: HashSet<String> = ["a".to_string()].into_iter().collect();
let required = resolve_required_computed_fields(&needed, &computed).unwrap();
assert!(required.contains("a"));
assert!(required.contains("b"));
assert!(required.contains("c"));
assert!(required.contains("d")); }
#[test]
fn closure_incomplete_forces_all() {
let mut computed = HashMap::new();
computed.insert(
"a".to_string(),
ComputedDeps {
fields: vec!["b".to_string()],
is_complete: false,
},
);
computed.insert(
"b".to_string(),
ComputedDeps {
fields: vec![],
is_complete: true,
},
);
let needed: HashSet<String> = ["a".to_string()].into_iter().collect();
let result = resolve_required_computed_fields(&needed, &computed);
assert!(result.is_none());
}
#[test]
fn closure_only_needed() {
let mut computed = HashMap::new();
computed.insert(
"a".to_string(),
ComputedDeps {
fields: vec!["b".to_string(), "c".to_string()],
is_complete: true,
},
);
computed.insert(
"b".to_string(),
ComputedDeps {
fields: vec![],
is_complete: true,
},
);
computed.insert(
"c".to_string(),
ComputedDeps {
fields: vec![],
is_complete: true,
},
);
computed.insert(
"x".to_string(),
ComputedDeps {
fields: vec!["y".to_string()],
is_complete: true,
},
);
let needed: HashSet<String> = ["b".to_string()].into_iter().collect();
let required = resolve_required_computed_fields(&needed, &computed).unwrap();
assert!(required.contains("b"));
assert!(!required.contains("a"));
assert!(!required.contains("x"));
}
}