diff --git a/lib/dal-test/src/helpers.rs b/lib/dal-test/src/helpers.rs index 35ec3f1bdc..80cf405344 100644 --- a/lib/dal-test/src/helpers.rs +++ b/lib/dal-test/src/helpers.rs @@ -4,8 +4,8 @@ use color_eyre::eyre::eyre; use color_eyre::Result; use dal::key_pair::KeyPairPk; use dal::{ - AttributeValue, Component, ComponentId, DalContext, InputSocket, KeyPair, OutputSocket, Schema, - SchemaVariant, SchemaVariantId, User, UserPk, + AttributeValue, Component, ComponentId, ComponentType, DalContext, InputSocket, KeyPair, + OutputSocket, Schema, SchemaVariant, SchemaVariantId, User, UserPk, }; use names::{Generator, Name}; @@ -70,6 +70,23 @@ pub async fn create_component_for_schema_name( Ok(Component::new(ctx, name.as_ref().to_string(), schema_variant_id).await?) } +/// Creates a [`Component`] from the default [`SchemaVariant`] corresponding to a provided +/// [`Schema`] name. +pub async fn create_component_for_schema_name_with_type( + ctx: &DalContext, + schema_name: impl AsRef, + name: impl AsRef, + component_type: ComponentType, +) -> Result { + let schema = Schema::find_by_name(ctx, schema_name) + .await? + .ok_or(eyre!("schema not found"))?; + let schema_variant_id = SchemaVariant::get_default_id_for_schema(ctx, schema.id()).await?; + let component = Component::new(ctx, name.as_ref().to_string(), schema_variant_id).await?; + component.set_type(ctx, component_type).await?; + Ok(component) +} + /// Creates a [`Component`] for a given [`SchemaVariantId`](SchemaVariant). pub async fn create_component_for_schema_variant( ctx: &DalContext, diff --git a/lib/dal/src/component.rs b/lib/dal/src/component.rs index 8b736ba387..0378842f06 100644 --- a/lib/dal/src/component.rs +++ b/lib/dal/src/component.rs @@ -2332,7 +2332,7 @@ impl Component { /// if the input socket has arity many and the matches are all siblings /// /// Note: this does not check for whether data should actually flow between components - #[instrument(level = "debug", skip(ctx))] + #[instrument(level = "info", skip(ctx))] pub async fn find_available_inferred_connections_to_input_socket( ctx: &DalContext, input_socket_match: InputSocketMatch, @@ -2362,22 +2362,42 @@ impl Component { } } ComponentType::ConfigurationFrameUp => { - // An up frame's input sockets are sourced from its children's output sockets - // For now, we won't let down frames send outputs to parents and children - // This might need to change, but we can change it when we've got a use case. - let mut matches = Self::find_available_output_socket_match_in_descendants( - ctx, - input_socket_match, - vec![ - ComponentType::ConfigurationFrameUp, - ComponentType::Component, - ], - ) - .await?; - // if there is more than one match, sort by component Ulid so they're - // consistently ordered - matches.sort_by_key(|output_socket| output_socket.component_id); - matches + // An up frame's input sockets are sourced from either its children's output sockets + // or an ancestor. Based on the input socket's arity, we match many (sorted by component ulid) + // or if the arity is single, we return none + let mut matches = vec![]; + let descendant_matches = + Self::find_available_output_socket_match_in_descendants( + ctx, + input_socket_match, + vec![ + ComponentType::ConfigurationFrameUp, + ComponentType::Component, + ], + ) + .await?; + matches.extend(descendant_matches); + if let Some(ascendant_match) = + Self::find_first_output_socket_match_in_ancestors( + ctx, + input_socket_match, + vec![ComponentType::ConfigurationFrameDown], + ) + .await? + { + matches.push(ascendant_match); + } + + let input_socket = + InputSocket::get_by_id(ctx, input_socket_match.input_socket_id).await?; + if input_socket.arity() == SocketArity::One && matches.len() > 1 { + vec![] + } else { + // if there is more than one match, sort by component Ulid so they're + // consistently ordered + matches.sort_by_key(|output_socket| output_socket.component_id); + matches + } } ComponentType::AggregationFrame => vec![], }; @@ -2425,9 +2445,10 @@ impl Component { } } } - for child in Self::get_children_for_id(ctx, component_id).await? { - work_queue.push_back(child); - } + } + // regardless whether the component type matches, we need to continue to descend + for child in Self::get_children_for_id(ctx, component_id).await? { + work_queue.push_back(child); } } @@ -2876,7 +2897,7 @@ impl Component { /// Up Frame. /// /// Down Frames can drive Input Sockets of their children if the child is a Down Frame - /// or a Component. + /// or a Component or an Up Frame. #[instrument(level = "debug", skip(ctx))] pub async fn find_inferred_values_using_this_output_socket( ctx: &DalContext, @@ -2908,7 +2929,7 @@ impl Component { } ComponentType::ConfigurationFrameDown => { // if the type is a down frame, find all descendants - // who have a matching input socket AND are a Down Frame or Component + // who have a matching input socket AND are a Down Frame, Component, or Up Frame Component::find_all_potential_inferred_input_socket_matches_in_descendants( ctx, output_socket_id, @@ -2916,13 +2937,13 @@ impl Component { vec![ ComponentType::ConfigurationFrameDown, ComponentType::Component, + ComponentType::ConfigurationFrameUp, ], ) .await? } - // we are not supporting aggregation frames right now - _ => vec![], + ComponentType::AggregationFrame => vec![], }; Ok(maybe_target_sockets) diff --git a/lib/dal/src/component/frame.rs b/lib/dal/src/component/frame.rs index 9f3d721eae..5d7185e45a 100644 --- a/lib/dal/src/component/frame.rs +++ b/lib/dal/src/component/frame.rs @@ -88,6 +88,9 @@ impl Frame { Ok(()) } + /// Removes the existing parent connection if it exists and adds the new one. + /// Also, determines what needs to be rerun due to the change, based on which + /// input sockets have new/removed/different output sockets driving them #[instrument(level = "info", skip(ctx))] async fn attach_child_to_parent_inner( ctx: &DalContext, @@ -96,23 +99,35 @@ impl Frame { ) -> FrameResult<()> { let total_start = std::time::Instant::now(); + // cache current map of input <-> output sockets based on what the parent knows about right now!!!! + let initial_impacted_values: HashSet = + Self::get_all_inferred_connections_for_component_tree(ctx, parent_id, child_id).await?; + // is the current child already connected to a parent? - let mut cached_impacted_values: HashSet = HashSet::new(); + let mut post_edge_removal_impacted_values: HashSet = + HashSet::new(); if let Some(current_parent_id) = Component::get_parent_by_id(ctx, child_id).await? { - // cache input sockets impacted by the child component - let before_remove_edge_input_sockets = - Self::get_impacted_connections(ctx, child_id).await?; - - cached_impacted_values.extend(before_remove_edge_input_sockets); //remove the edge Component::remove_edge_from_frame(ctx, current_parent_id, child_id).await?; info!( "Remove existing edge from frame took: {:?}", total_start.elapsed() ); + // get the map of input <-> output sockets after the edge was removed. so we can determine if more + // updates need to be made due to the upsert + // note we need to see what the current parent's tree looked like, as there could be nested impacts + post_edge_removal_impacted_values.extend( + Self::get_all_inferred_connections_for_component_tree( + ctx, + current_parent_id, + child_id, + ) + .await?, + ); } let cycle_check_guard = ctx.workspace_snapshot()?.enable_cycle_check().await; + // add the new edge Component::add_edge_to_frame(ctx, parent_id, child_id, EdgeWeightKind::FrameContains) .await?; drop(cycle_check_guard); @@ -120,15 +135,28 @@ impl Frame { "Cycle Check Guard dropped, add edge took {:?}", total_start.elapsed() ); + + // now figure out what needs to rerun! let mut values_to_run: HashSet = HashSet::new(); - let current_impacted_values = Self::get_impacted_connections(ctx, child_id).await?; - // if an input socket + output socket is in both sets, we don't need to rerun it + // get the latest state of the component tree + let current_impacted_values = + Self::get_all_inferred_connections_for_component_tree(ctx, parent_id, child_id).await?; + + // an edge has been removed if it exists in the state after we've detached the component, and it's not in current state values_to_run.extend( - cached_impacted_values + post_edge_removal_impacted_values .difference(¤t_impacted_values) .copied(), ); + // an edge has been removed if it exists before we added the new edge, and not in current + values_to_run.extend( + initial_impacted_values + .difference(¤t_impacted_values) + .copied(), + ); + + // let the front end know if we've removed some inferred edges let mut inferred_edges: Vec = vec![]; for pair in &values_to_run { inferred_edges.push(SummaryDiagramInferredEdge { @@ -144,11 +172,24 @@ impl Frame { .publish_on_commit(ctx) .await?; + // an input socket needs to rerun if: + // the input socket has a new/different output socket driving it values_to_run.extend( current_impacted_values - .difference(&cached_impacted_values) + .difference(&initial_impacted_values) .copied(), ); + + // if we removed an edge, let's also see if there are input sockets that need to rerun + if !post_edge_removal_impacted_values.is_empty() { + values_to_run.extend( + current_impacted_values + .difference(&post_edge_removal_impacted_values) + .copied(), + ); + } + + // enqueue those values that we now know need to run ctx.add_dependent_values_and_enqueue( values_to_run .into_iter() @@ -160,18 +201,35 @@ impl Frame { Ok(()) } + #[instrument( + level = "info", + skip(ctx), + name = "frame.detach_child_from_parent_inner" + )] async fn detach_child_from_parent_inner( ctx: &DalContext, parent_id: ComponentId, child_id: ComponentId, ) -> FrameResult<()> { + // cache current state of the tree let before_change_impacted_input_sockets: HashSet = - Self::get_impacted_connections(ctx, child_id).await?; - //when detaching a child, need to re-run any attribute value functions for those impacted input sockets then queue up dvu! + Self::get_all_inferred_connections_for_component_tree(ctx, parent_id, child_id).await?; + // remove the edge Component::remove_edge_from_frame(ctx, parent_id, child_id).await?; - + // get the new state of the tree (from the perspective of both components, now in disjoint trees because they were detached!) + let current_impacted_sockets = + Self::get_all_inferred_connections_for_component_tree(ctx, parent_id, child_id).await?; + // find the edges that have been removed due to the detachment + // note: there should not be any other changes as this is a pure detachment, not an upsert (where something has moved from + // one frame to another) + let mut diff: HashSet = HashSet::new(); + diff.extend( + before_change_impacted_input_sockets + .difference(¤t_impacted_sockets) + .cloned(), + ); let mut inferred_edges: Vec = vec![]; - for pair in &before_change_impacted_input_sockets { + for pair in &diff { inferred_edges.push(SummaryDiagramInferredEdge { to_socket_id: pair.input_socket_match.input_socket_id, to_component_id: pair.input_socket_match.component_id, @@ -180,14 +238,21 @@ impl Frame { to_delete: false, // irrelevant }) } + // let the front end know what's been removed WsEvent::remove_inferred_edges(ctx, inferred_edges) .await? .publish_on_commit(ctx) .await?; + // also get what's in current that's not in before (because these have also changed!) + diff.extend( + current_impacted_sockets + .difference(&before_change_impacted_input_sockets) + .cloned(), + ); + // enqueue dvu for those values that no longer have an output socket driving them! ctx.add_dependent_values_and_enqueue( - before_change_impacted_input_sockets - .into_iter() + diff.into_iter() .map(|values| values.input_socket_match.attribute_value_id) .collect_vec(), ) @@ -195,84 +260,53 @@ impl Frame { Ok(()) } - /// For a given [`Component`], find all input sockets that have an inferred connection. For - /// every output socket, get all downstream input sockets that have an inferred connection to - /// the provided [`Component`]. - async fn get_impacted_connections( + /// For a pair of Components, find the top most parent of the tree (or each tree if they're not related to each other, for + /// example, if they've been detached). + /// Then, traverse the tree, collecting all inferred connections for all components + /// We need the whole tree because nested components/frames might be indirectly affected by whatever the user is doing + #[instrument(level = "info", skip(ctx), name = "frame.get_impacted_connections")] + async fn get_all_inferred_connections_for_component_tree( ctx: &DalContext, + parent_id: ComponentId, child_id: ComponentId, ) -> FrameResult> { let mut input_map: HashMap> = HashMap::new(); let mut output_map: HashMap> = HashMap::new(); let mut impacted_connections = HashSet::new(); - // Determine whether we should check descendants and/or ascendants based on the component - // type. - let (check_descendants, check_ascendants) = - match Component::get_type_by_id(ctx, child_id).await? { - ComponentType::AggregationFrame => (false, false), - ComponentType::Component => (false, true), - ComponentType::ConfigurationFrameDown => (true, true), - ComponentType::ConfigurationFrameUp => (true, true), - }; - - // Grab all descendants and see who is impacted. - if check_descendants { - let mut work_queue = VecDeque::new(); - let children = Component::get_children_for_id(ctx, child_id).await?; - work_queue.extend(children); + // find the top most parent of each tree (might be the same, but that's fine) + let mut first_top_parent = child_id; + while let Some(parent) = Component::get_parent_by_id(ctx, first_top_parent).await? { + first_top_parent = parent; + } + let mut second_top_parent = parent_id; + while let Some(parent) = Component::get_parent_by_id(ctx, second_top_parent).await? { + second_top_parent = parent; + } - while let Some(child) = work_queue.pop_front() { - let input = - Component::build_map_for_component_id_inferred_incoming_connections(ctx, child) - .await?; - input_map.extend(input); - let output = - Component::build_map_for_component_id_inferred_outgoing_connections(ctx, child) - .await?; - output_map.extend(output); + // Walk down the tree of descendants and accumulate connections for every input/output socket. + let mut work_queue = VecDeque::new(); + work_queue.push_back(first_top_parent); - let _ = Component::get_children_for_id(ctx, child) - .await? - .into_iter() - .map(|comp| work_queue.push_back(comp)); - } + // if we're dealing with two trees, get the children for the other one too + if first_top_parent != second_top_parent { + work_queue.push_back(second_top_parent); } - // Grab all ascendants and see who is impacted. - if check_ascendants { - let mut work_queue = VecDeque::new(); - if let Some(parent) = Component::get_parent_by_id(ctx, child_id).await? { - work_queue.push_front(parent); - } + while let Some(child) = work_queue.pop_front() { + let input = + Component::build_map_for_component_id_inferred_incoming_connections(ctx, child) + .await?; + input_map.extend(input); + let output = + Component::build_map_for_component_id_inferred_outgoing_connections(ctx, child) + .await?; + output_map.extend(output); - while let Some(parent) = work_queue.pop_front() { - let input = Component::build_map_for_component_id_inferred_incoming_connections( - ctx, parent, - ) - .await?; - input_map.extend(input); - let output = Component::build_map_for_component_id_inferred_outgoing_connections( - ctx, parent, - ) - .await?; - output_map.extend(output); - if let Some(grandparent) = Component::get_parent_by_id(ctx, parent).await? { - work_queue.push_back(grandparent); - } - } + let children = Component::get_children_for_id(ctx, child).await?; + work_queue.extend(children); } - // Check inferred outgoing and incoming connections. - let input: HashMap> = - Component::build_map_for_component_id_inferred_incoming_connections(ctx, child_id) - .await?; - input_map.extend(input); - let output: HashMap> = - Component::build_map_for_component_id_inferred_outgoing_connections(ctx, child_id) - .await?; - output_map.extend(output); - // Process everything collecting in the input map and output map. for (input_socket, output_sockets) in input_map.into_iter() { for output_socket in output_sockets { @@ -290,7 +324,7 @@ impl Frame { }); } } - + debug!("imapcted connections: {:?}", impacted_connections); Ok(impacted_connections) } } diff --git a/lib/dal/src/job/definition/dependent_values_update.rs b/lib/dal/src/job/definition/dependent_values_update.rs index ba56abd7f9..02b2fe1285 100644 --- a/lib/dal/src/job/definition/dependent_values_update.rs +++ b/lib/dal/src/job/definition/dependent_values_update.rs @@ -27,7 +27,7 @@ use crate::{ prop::PropError, status::{StatusMessageState, StatusUpdate, StatusUpdateError}, AccessBuilder, AttributeValue, AttributeValueId, DalContext, TransactionsError, Visibility, - WorkspaceSnapshotError, WsEvent, WsEventError, + WorkspacePk, WorkspaceSnapshotError, WsEvent, WsEventError, }; const MAX_RETRIES: u32 = 8; @@ -106,8 +106,25 @@ impl JobConsumerMetadata for DependentValuesUpdate { #[async_trait] impl JobConsumer for DependentValuesUpdate { - #[instrument(name = "dependent_values_update.run", skip_all, level = "info")] + #[instrument( + level="info", + name = "dependent_values_update.run", + skip_all, + fields( + si.change_set.id = Empty, + si.workspace.pk = Empty, + ), + )] async fn run(&self, ctx: &mut DalContext) -> JobConsumerResult { + let span = Span::current(); + span.record("si.change_set.id", ctx.change_set_id().to_string()); + span.record( + "si.workspace.pk", + ctx.tenancy() + .workspace_pk() + .unwrap_or(WorkspacePk::NONE) + .to_string(), + ); Ok(self.inner_run(ctx).await?) } } @@ -118,7 +135,7 @@ impl DependentValuesUpdate { ctx: &mut DalContext, ) -> DependentValueUpdateResult { let start = tokio::time::Instant::now(); - + let span = Span::current(); let node_ids = ctx .workspace_snapshot()? .take_dependent_values(ctx.change_set()?) @@ -151,15 +168,20 @@ impl DependentValuesUpdate { for attribute_value_id in &independent_value_ids { let attribute_value_id = attribute_value_id.to_owned(); // release our borrow - + let parent_span = span.clone(); if !seen_ids.contains(&attribute_value_id) { let id = Ulid::new(); - update_join_set.spawn(values_from_prototype_function_execution( - id, - ctx.clone(), - attribute_value_id, - self.set_value_lock.clone(), - )); + update_join_set.spawn( + values_from_prototype_function_execution( + id, + ctx.clone(), + attribute_value_id, + self.set_value_lock.clone(), + ) + .instrument(info_span!(parent: parent_span, "dependent_values_update.values_from_prototype_function_execution", + attribute_value.id = %attribute_value_id, + )), + ); task_id_to_av_id.insert(id, attribute_value_id); seen_ids.insert(attribute_value_id); } @@ -299,14 +321,6 @@ async fn execution_error_detail( /// Wrapper around `AttributeValue.values_from_prototype_function_execution(&ctx)` to get it to /// play more nicely with being spawned into a `JoinSet`. -#[instrument( - name = "dependent_values_update.values_from_prototype_function_execution", - skip_all, - level = "info", - fields( - attribute_value.id = %attribute_value_id, - ) -)] async fn values_from_prototype_function_execution( task_id: Ulid, ctx: DalContext, @@ -323,9 +337,10 @@ async fn values_from_prototype_function_execution( { return (task_id, Err(err)); } - + let parent_span = Span::current(); let result = AttributeValue::execute_prototype_function(&ctx, attribute_value_id, set_value_lock) + .instrument(info_span!(parent:parent_span, "value.execute_prototype_function", attribute_value.id= %attribute_value_id)) .await .map_err(Into::into); diff --git a/lib/dal/tests/integration_test/frame.rs b/lib/dal/tests/integration_test/frame.rs index ba583158e6..05b224aa9c 100644 --- a/lib/dal/tests/integration_test/frame.rs +++ b/lib/dal/tests/integration_test/frame.rs @@ -5,8 +5,8 @@ use dal::{AttributeValue, Component, DalContext, Schema, SchemaVariant}; use dal::{ComponentType, InputSocket, OutputSocket}; use dal_test::helpers::{ connect_components_with_socket_names, create_component_for_schema_name, - get_component_input_socket_value, get_component_output_socket_value, - update_attribute_value_for_component, ChangeSetTestHelpers, + create_component_for_schema_name_with_type, get_component_input_socket_value, + get_component_output_socket_value, update_attribute_value_for_component, ChangeSetTestHelpers, }; use dal_test::test; use pretty_assertions_sorted::assert_eq; @@ -579,6 +579,280 @@ async fn output_sockets_can_have_both(ctx: &mut DalContext) { assert_eq!(odd_component_2_mat_view, serde_json::json!("1")); } +#[test] +async fn up_frames_take_inputs_from_down_frames_too(ctx: &mut DalContext) { + // create an odd down frame + let level_one = create_component_for_schema_name_with_type( + ctx, + "medium even lego", + "level one", + ComponentType::ConfigurationFrameDown, + ) + .await + .expect("could not create component"); + + // create an even up frame + let level_two = create_component_for_schema_name_with_type( + ctx, + "large odd lego", + "level two", + ComponentType::ConfigurationFrameUp, + ) + .await + .expect("could not create component"); + + // upsert even frame into odd frame + Frame::upsert_parent(ctx, level_two.id(), level_one.id()) + .await + .expect("could not upsert frame"); + + // create odd component to go inside even up frame + let level_three = create_component_for_schema_name_with_type( + ctx, + "small even lego", + "level three", + ComponentType::Component, + ) + .await + .expect("could not create component"); + + // upsert component into up frame + Frame::upsert_parent(ctx, level_three.id(), level_two.id()) + .await + .expect("could not upsert parent"); + + update_attribute_value_for_component( + ctx, + level_one.id(), + &["root", "domain", "three"], + serde_json::json!["3"], + ) + .await + .expect("could not update attribute value"); + + update_attribute_value_for_component( + ctx, + level_three.id(), + &["root", "domain", "one"], + serde_json::json!["1"], + ) + .await + .expect("could not update attribute value"); + update_attribute_value_for_component( + ctx, + level_one.id(), + &["root", "domain", "one"], + serde_json::json!["2"], + ) + .await + .expect("could not update attribute value"); + + ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx) + .await + .expect("could not commit and update snapshot to visibility"); + + // make sure everything looks as expected + + let input_value = get_component_input_socket_value(ctx, level_two.id(), "one") + .await + .expect("could not get input socket value") + .expect("has value"); + assert_eq!( + serde_json::json![vec!["2", "1"]], // expected + input_value, // actual + ); + let input_value = get_component_input_socket_value(ctx, level_two.id(), "three") + .await + .expect("could not get input socket value") + .expect("has value"); + assert_eq!( + "3", // expected + input_value, // actual + ); +} + +#[test] +async fn orphan_frames_deeply_nested(ctx: &mut DalContext) { + // create a large up frame + let even_level_one = create_component_for_schema_name_with_type( + ctx, + "large even lego", + "level one", + ComponentType::ConfigurationFrameDown, + ) + .await + .expect("created frame"); + // put another medium frame inside + let even_level_two = create_component_for_schema_name_with_type( + ctx, + "medium even lego", + "level two", + ComponentType::ConfigurationFrameDown, + ) + .await + .expect("could not create component"); + + Frame::upsert_parent(ctx, even_level_two.id(), even_level_one.id()) + .await + .expect("could not upsert parent"); + + // create an odd frame inside level 2 (that we will later detach) + let odd_level_three = create_component_for_schema_name_with_type( + ctx, + "large odd lego", + "level three", + ComponentType::ConfigurationFrameDown, + ) + .await + .expect("could not create component"); + + Frame::upsert_parent(ctx, odd_level_three.id(), even_level_two.id()) + .await + .expect("could not create upsert frame"); + + // create an odd component inside level 3 (that will move when level 3 is detached) + let odd_level_four = create_component_for_schema_name_with_type( + ctx, + "large odd lego", + "level four", + ComponentType::Component, + ) + .await + .expect("could not create component"); + + Frame::upsert_parent(ctx, odd_level_four.id(), odd_level_three.id()) + .await + .expect("could not upsert parent"); + + // create an even component, also inside level 3 (that will move when level 3 is detached AND take a value from level 3) + let even_level_four = create_component_for_schema_name_with_type( + ctx, + "large even lego", + "level four even", + ComponentType::Component, + ) + .await + .expect("could not create component"); + Frame::upsert_parent(ctx, even_level_four.id(), odd_level_three.id()) + .await + .expect("could not upsert parent"); + + ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx) + .await + .expect("could not commit and update snapshot to visibility"); + + // now let's set some values + // level one sets output socket 5 which should pass to the level 3 and 4 items + update_attribute_value_for_component( + ctx, + even_level_one.id(), + &["root", "domain", "five"], + serde_json::json!["5"], + ) + .await + .expect("could not update attribute value"); + // level two sets output socket 3 which should pass to level 3 and 4 items + update_attribute_value_for_component( + ctx, + even_level_two.id(), + &["root", "domain", "three"], + serde_json::json!["3"], + ) + .await + .expect("could not update attribute value"); + // level 3 sets output socket 2 which should pass to level 4 even component + update_attribute_value_for_component( + ctx, + odd_level_three.id(), + &["root", "domain", "two"], + serde_json::json!["2"], + ) + .await + .expect("could not update attribute value"); + + ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx) + .await + .expect("could not commit and update snapshot to visibility"); + + // let's make sure everything is as we expect + let input_value = get_component_input_socket_value(ctx, odd_level_three.id(), "five") + .await + .expect("could not get input socket value") + .expect("has value"); + assert_eq!( + "5", // expected + input_value, // actual + ); + let input_value = get_component_input_socket_value(ctx, odd_level_four.id(), "five") + .await + .expect("could not get input socket value") + .expect("has value"); + assert_eq!( + "5", // expected + input_value, // actual + ); + let input_value = get_component_input_socket_value(ctx, odd_level_three.id(), "three") + .await + .expect("could not get input socket value") + .expect("has value"); + assert_eq!( + "3", // expected + input_value, // actual + ); + let input_value = get_component_input_socket_value(ctx, odd_level_four.id(), "three") + .await + .expect("could not get input socket value") + .expect("has value"); + assert_eq!( + "3", // expected + input_value, // actual + ); + let input_value = get_component_input_socket_value(ctx, even_level_four.id(), "two") + .await + .expect("could not get input socket value") + .expect("has value"); + assert_eq!( + "2", // expected + input_value, // actual + ); + + // now let's orphan level 3 + Frame::orphan_child(ctx, odd_level_three.id()) + .await + .expect("could not orphan component"); + ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx) + .await + .expect("could not commit and update snapshot to visibility"); + + // let's make sure everything updated accordingly + let input_value = get_component_input_socket_value(ctx, odd_level_three.id(), "five") + .await + .expect("could not get input socket value"); + assert!(input_value.is_none()); + let input_value = get_component_input_socket_value(ctx, odd_level_four.id(), "five") + .await + .expect("could not get input socket value"); + assert!(input_value.is_none()); + let input_value = get_component_input_socket_value(ctx, odd_level_three.id(), "three") + .await + .expect("could not get input socket value"); + + assert!(input_value.is_none()); + let input_value = get_component_input_socket_value(ctx, odd_level_four.id(), "three") + .await + .expect("could not get input socket value"); + + assert!(input_value.is_none()); + let input_value = get_component_input_socket_value(ctx, even_level_four.id(), "two") + .await + .expect("could not get input socket value") + .expect("has value"); + assert_eq!( + "2", // expected + input_value, // actual + ); +} + #[test] async fn simple_down_frames_no_nesting(ctx: &mut DalContext) { let even_frame = create_component_for_schema_name(ctx, "large even lego", "even")