From c3cf9e83eacd6d9a73d0dfec825d3d610d8f7e29 Mon Sep 17 00:00:00 2001 From: Victor Bustamante Date: Wed, 23 Oct 2024 08:21:56 -0300 Subject: [PATCH 1/2] Reapply "Feat: migrate current diagram to view" This reverts commit e871d7fd0ae62e317f2b6c4617fbc156f6e378b7. --- lib/dal-test/src/expected.rs | 11 +- lib/dal/src/component.rs | 177 +- lib/dal/src/diagram.rs | 34 +- lib/dal/src/diagram/geometry.rs | 275 +++ lib/dal/src/diagram/view.rs | 254 +++ lib/dal/src/layer_db_types/content_types.rs | 71 +- lib/dal/src/lib.rs | 4 +- lib/dal/src/management/prototype.rs | 2 +- lib/dal/src/schema/variant.rs | 1 + lib/dal/src/workspace.rs | 2 +- lib/dal/src/workspace_snapshot.rs | 38 +- .../src/workspace_snapshot/content_address.rs | 6 +- lib/dal/src/workspace_snapshot/edge_weight.rs | 2 + lib/dal/src/workspace_snapshot/graph.rs | 21 +- .../graph/correct_transforms.rs | 6 +- .../graph/detect_updates.rs | 10 +- .../graph/{v3 => }/tests.rs | 116 +- .../graph/{v3 => }/tests/detect_updates.rs | 44 +- .../tests/exclusive_outgoing_edges.rs | 8 +- .../graph/{v3 => }/tests/rebase.rs | 12 +- lib/dal/src/workspace_snapshot/graph/v2.rs | 1 + lib/dal/src/workspace_snapshot/graph/v3.rs | 28 +- .../workspace_snapshot/graph/v3/component.rs | 3 +- .../src/workspace_snapshot/graph/v3/schema.rs | 3 +- .../graph/v3/schema/variant.rs | 3 +- lib/dal/src/workspace_snapshot/graph/v4.rs | 1710 +++++++++++++++++ .../workspace_snapshot/graph/v4/component.rs | 66 + .../src/workspace_snapshot/graph/v4/schema.rs | 42 + .../graph/v4/schema/variant.rs | 48 + lib/dal/src/workspace_snapshot/migrator.rs | 65 +- lib/dal/src/workspace_snapshot/migrator/v4.rs | 116 ++ lib/dal/src/workspace_snapshot/node_weight.rs | 113 +- .../node_weight/category_node_weight.rs | 2 + .../node_weight/component_node_weight.rs | 140 +- .../node_weight/content_node_weight.rs | 20 +- .../node_weight/geometry_node_weight.rs | 22 + .../node_weight/geometry_node_weight/v1.rs | 48 + .../node_weight/input_socket_node_weight.rs | 3 +- .../input_socket_node_weight/v1.rs | 6 +- .../node_weight/ordering_node_weight.rs | 2 +- .../node_weight/schema_variant_node_weight.rs | 2 +- .../schema_variant_node_weight/v1.rs | 3 +- .../workspace_snapshot/node_weight/traits.rs | 6 +- .../traits/correct_exclusive_outgoing_edge.rs | 4 +- .../node_weight/view_node_weight.rs | 22 + .../node_weight/view_node_weight/v1.rs | 48 + lib/dal/tests/integration_test/component.rs | 9 +- .../tests/integration_test/deserialize/mod.rs | 32 +- lib/dal/tests/integration_test/frame.rs | 36 - .../integration_test/node_weight/ordering.rs | 2 +- lib/dal/tests/integration_test/secret.rs | 48 +- ...erialization-test-data-2024-10-14.snapshot | Bin 1474 -> 0 bytes ...erialization-test-data-2024-10-17.snapshot | Bin 0 -> 2083 bytes lib/sdf-server/src/migrations.rs | 33 +- .../src/service/component/set_type.rs | 20 +- .../src/service/diagram/create_component.rs | 7 +- .../src/service/diagram/paste_component.rs | 24 +- .../service/diagram/set_component_position.rs | 36 +- 58 files changed, 3418 insertions(+), 449 deletions(-) create mode 100644 lib/dal/src/diagram/geometry.rs create mode 100644 lib/dal/src/diagram/view.rs rename lib/dal/src/workspace_snapshot/graph/{v3 => }/tests.rs (94%) rename lib/dal/src/workspace_snapshot/graph/{v3 => }/tests/detect_updates.rs (95%) rename lib/dal/src/workspace_snapshot/graph/{v3 => }/tests/exclusive_outgoing_edges.rs (98%) rename lib/dal/src/workspace_snapshot/graph/{v3 => }/tests/rebase.rs (94%) create mode 100644 lib/dal/src/workspace_snapshot/graph/v4.rs create mode 100644 lib/dal/src/workspace_snapshot/graph/v4/component.rs create mode 100644 lib/dal/src/workspace_snapshot/graph/v4/schema.rs create mode 100644 lib/dal/src/workspace_snapshot/graph/v4/schema/variant.rs create mode 100644 lib/dal/src/workspace_snapshot/migrator/v4.rs create mode 100644 lib/dal/src/workspace_snapshot/node_weight/geometry_node_weight.rs create mode 100644 lib/dal/src/workspace_snapshot/node_weight/geometry_node_weight/v1.rs create mode 100644 lib/dal/src/workspace_snapshot/node_weight/view_node_weight.rs create mode 100644 lib/dal/src/workspace_snapshot/node_weight/view_node_weight/v1.rs delete mode 100644 lib/dal/tests/serialization-test-data-2024-10-14.snapshot create mode 100644 lib/dal/tests/serialization-test-data-2024-10-17.snapshot diff --git a/lib/dal-test/src/expected.rs b/lib/dal-test/src/expected.rs index 3a52f85477..69df0090ce 100644 --- a/lib/dal-test/src/expected.rs +++ b/lib/dal-test/src/expected.rs @@ -3,9 +3,9 @@ #![allow(clippy::expect_used)] use crate::helpers::ChangeSetTestHelpers; +use dal::diagram::geometry::RawGeometry; use dal::{ self, - component::ComponentGeometry, prop::{Prop, PropPath}, property_editor::values::PropertyEditorValues, schema::variant::authoring::VariantAuthoringClient, @@ -380,8 +380,13 @@ impl ExpectComponent { .expect("get component by id") } - pub async fn geometry(self, ctx: &DalContext) -> ComponentGeometry { - self.component(ctx).await.geometry() + pub async fn geometry(self, ctx: &DalContext) -> RawGeometry { + self.component(ctx) + .await + .geometry(ctx) + .await + .expect("get geometry for component") + .raw() } pub async fn view(self, ctx: &DalContext) -> Option { diff --git a/lib/dal/src/component.rs b/lib/dal/src/component.rs index c53c239662..8b5156c29d 100644 --- a/lib/dal/src/component.rs +++ b/lib/dal/src/component.rs @@ -1,5 +1,5 @@ //! This module contains [`Component`], which is an instance of a -//! [`SchemaVariant`](crate::SchemaVariant) and a _model_ of a "real world resource". +//! [`SchemaVariant`](SchemaVariant) and a _model_ of a "real world resource". use itertools::Itertools; use petgraph::Direction::Outgoing; @@ -33,7 +33,7 @@ use crate::code_view::CodeViewError; use crate::diagram::{SummaryDiagramEdge, SummaryDiagramInferredEdge}; use crate::func::argument::FuncArgumentError; use crate::history_event::HistoryEventMetadata; -use crate::layer_db_types::{ComponentContent, ComponentContentV1}; +use crate::layer_db_types::{ComponentContent, ComponentContentV2}; use crate::module::{Module, ModuleError}; use crate::prop::{PropError, PropPath}; use crate::qualification::QualificationError; @@ -56,6 +56,9 @@ use si_frontend_types::{ SummaryDiagramComponent, }; +use self::inferred_connection_graph::InferredConnectionGraphError; +use crate::diagram::geometry::{Geometry, RawGeometry}; +use crate::diagram::view::View; use crate::{ id, implement_add_edge_to, AttributePrototype, AttributeValue, AttributeValueId, ChangeSetId, DalContext, Func, FuncError, FuncId, HelperError, InputSocket, InputSocketId, OutputSocket, @@ -64,8 +67,6 @@ use crate::{ WsEventError, WsEventResult, WsPayload, }; -use self::inferred_connection_graph::InferredConnectionGraphError; - pub mod code; pub mod debug; pub mod diff; @@ -76,11 +77,6 @@ pub mod qualification; pub mod resource; pub mod socket; -pub const DEFAULT_COMPONENT_X_POSITION: &str = "0"; -pub const DEFAULT_COMPONENT_Y_POSITION: &str = "0"; -pub const DEFAULT_COMPONENT_WIDTH: &str = "500"; -pub const DEFAULT_COMPONENT_HEIGHT: &str = "500"; - #[remain::sorted] #[derive(Debug, Error)] pub enum ComponentError { @@ -120,6 +116,8 @@ pub enum ComponentError { ComponentMissingValue(ComponentId, PropId), #[error("connection destination component {0} has no attribute value for input socket {1}")] DestinationComponentMissingAttributeValueForInputSocket(ComponentId, InputSocketId), + #[error("diagram error: {0}")] + Diagram(String), #[error("frame error: {0}")] Frame(#[from] Box), #[error("func error: {0}")] @@ -278,33 +276,16 @@ pub struct Component { #[serde(flatten)] timestamp: Timestamp, to_delete: bool, - x: String, - y: String, - width: Option, - height: Option, } -impl From for ComponentContentV1 { +impl From for ComponentContentV2 { fn from(value: Component) -> Self { Self { timestamp: value.timestamp, - x: value.x, - y: value.y, - width: value.width, - height: value.height, } } } -// Used to transfer the size and position of a component -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct ComponentGeometry { - pub x: String, - pub y: String, - pub width: Option, - pub height: Option, -} - #[derive(Copy, Clone, Debug)] pub struct ControllingFuncData { pub func_id: FuncId, @@ -313,15 +294,11 @@ pub struct ControllingFuncData { } impl Component { - pub fn assemble(node_weight: &ComponentNodeWeight, content: ComponentContentV1) -> Self { + pub fn assemble(node_weight: &ComponentNodeWeight, content: ComponentContentV2) -> Self { Self { id: node_weight.id().into(), timestamp: content.timestamp, to_delete: node_weight.to_delete(), - x: content.x, - y: content.y, - width: content.width, - height: content.height, } } @@ -329,31 +306,6 @@ impl Component { self.id } - pub fn x(&self) -> &str { - &self.x - } - - pub fn y(&self) -> &str { - &self.y - } - - pub fn width(&self) -> Option<&str> { - self.width.as_deref() - } - - pub fn height(&self) -> Option<&str> { - self.height.as_deref() - } - - pub fn geometry(&self) -> ComponentGeometry { - ComponentGeometry { - x: self.x.to_owned(), - y: self.y.to_owned(), - width: self.width.to_owned(), - height: self.height.to_owned(), - } - } - pub fn timestamp(&self) -> &Timestamp { &self.timestamp } @@ -454,19 +406,15 @@ impl Component { name: impl Into, schema_variant_id: SchemaVariantId, ) -> ComponentResult { - let content = ComponentContentV1 { + let content = ComponentContentV2 { timestamp: Timestamp::now(), - x: DEFAULT_COMPONENT_X_POSITION.to_string(), - y: DEFAULT_COMPONENT_Y_POSITION.to_string(), - width: None, - height: None, }; let (hash, _) = ctx .layer_db() .cas() .write( - Arc::new(ComponentContent::V1(content.clone()).into()), + Arc::new(ComponentContent::V2(content.clone()).into()), None, ctx.events_tenancy(), ctx.events_actor(), @@ -476,6 +424,7 @@ impl Component { Self::new_with_content_address(ctx, name, schema_variant_id, hash).await } + /// Create new component node but retain existing content address pub async fn new_with_content_address( ctx: &DalContext, name: impl Into, @@ -505,6 +454,14 @@ impl Component { ) .await?; + // Create geometry node + let view_id = View::get_id_for_default(ctx) + .await + .map_err(|e| ComponentError::Diagram(e.to_string()))?; + Geometry::new(ctx, id.into(), view_id) + .await + .map_err(|e| ComponentError::Diagram(e.to_string()))?; + let mut dvu_roots = vec![]; // Create attribute values for all socket corresponding to input and output sockets. @@ -1316,7 +1273,7 @@ impl Component { async fn try_get_node_weight_and_content( ctx: &DalContext, component_id: ComponentId, - ) -> ComponentResult> { + ) -> ComponentResult> { if let Some((component_node_weight, content_hash)) = Self::try_get_node_weight_and_content_hash(ctx, component_id).await? { @@ -1329,9 +1286,7 @@ impl Component { component_id.into(), ))?; - let ComponentContent::V1(inner) = content; - - return Ok(Some((component_node_weight, inner))); + return Ok(Some((component_node_weight, content.extract()))); } Ok(None) @@ -1340,7 +1295,7 @@ impl Component { async fn get_node_weight_and_content( ctx: &DalContext, component_id: ComponentId, - ) -> ComponentResult<(ComponentNodeWeight, ComponentContentV1)> { + ) -> ComponentResult<(ComponentNodeWeight, ComponentContentV2)> { Self::try_get_node_weight_and_content(ctx, component_id) .await? .ok_or(ComponentError::NotFound(component_id)) @@ -1426,10 +1381,7 @@ impl Component { for node_weight in node_weights { match contents.get(&node_weight.content_hash()) { Some(content) => { - // NOTE(nick,jacob,zack): if we had a v2, then there would be migration logic here. - let ComponentContent::V1(inner) = content; - - components.push(Self::assemble(&node_weight, inner.to_owned())); + components.push(Self::assemble(&node_weight, content.to_owned().extract())); } None => Err(WorkspaceSnapshotError::MissingContentFromStore( node_weight.id(), @@ -1503,6 +1455,18 @@ impl Component { Ok(None) } + // TODO take in view id + pub async fn geometry(&self, ctx: &DalContext) -> ComponentResult { + let view_id = View::get_id_for_default(ctx) + .await + .map_err(|e| ComponentError::Diagram(e.to_string()))?; + + Geometry::get_by_component_and_view(ctx, self.id, view_id) + .await + .map_err(|e| ComponentError::Diagram(e.to_string())) + } + + // TODO take in view id pub async fn set_geometry( &mut self, ctx: &DalContext, @@ -1510,35 +1474,24 @@ impl Component { y: impl Into, width: Option>, height: Option>, - ) -> ComponentResult { - let id: ComponentId = self.id; - - let before = ComponentContentV1::from(self.clone()); - self.x = x.into(); - self.y = y.into(); - self.width = width.map(|w| w.into()); - self.height = height.map(|h| h.into()); - let updated = ComponentContentV1::from(self.clone()); + ) -> ComponentResult<()> { + let mut geometry_pre = self.geometry(ctx).await?; - if updated != before { - let (hash, _) = ctx - .layer_db() - .cas() - .write( - Arc::new(ComponentContent::V1(updated).into()), - None, - ctx.events_tenancy(), - ctx.events_actor(), - ) - .await?; + let geometry_post = RawGeometry { + x: x.into(), + y: y.into(), + width: width.map(|w| w.into()), + height: height.map(|h| h.into()), + }; - ctx.workspace_snapshot()? - .update_content(id.into(), hash) - .await?; + if geometry_pre.clone().raw() != geometry_post { + geometry_pre + .update(ctx, geometry_post) + .await + .map_err(|e| ComponentError::Diagram(e.to_string()))?; } - let (node_weight, content) = Self::get_node_weight_and_content(ctx, id).await?; - Ok(Self::assemble(&node_weight, content)) + Ok(()) } pub async fn set_resource_id( @@ -2480,7 +2433,7 @@ impl Component { let original_component = self.clone(); let mut component = self; - let before = ComponentContentV1::from(component.clone()); + let before = ComponentContentV2::from(component.clone()); lambda(&mut component)?; // The `to_delete` lives on the node itself, not in the content, so we need to be a little @@ -2502,13 +2455,13 @@ impl Component { .await?; } - let updated = ComponentContentV1::from(component.clone()); + let updated = ComponentContentV2::from(component.clone()); if updated != before { let (hash, _) = ctx .layer_db() .cas() .write( - Arc::new(ComponentContent::V1(updated.clone()).into()), + Arc::new(ComponentContent::V2(updated.clone()).into()), None, ctx.events_tenancy(), ctx.events_actor(), @@ -2775,7 +2728,7 @@ impl Component { pub async fn copy_paste( &self, ctx: &DalContext, - component_geometry: ComponentGeometry, + component_geometry: RawGeometry, ) -> ComponentResult { let schema_variant = self.schema_variant(ctx).await?; @@ -3428,11 +3381,13 @@ impl Component { let schema = SchemaVariant::schema_for_schema_variant_id(ctx, schema_variant.id()).await?; let schema_id = schema.id(); + let geometry = self.geometry(ctx).await?; + let position = GridPoint { - x: self.x().parse::()?.round() as isize, - y: self.y().parse::()?.round() as isize, + x: geometry.x().parse::()?.round() as isize, + y: geometry.y().parse::()?.round() as isize, }; - let size = match (self.width(), self.height()) { + let size = match (geometry.width(), geometry.height()) { (Some(w), Some(h)) => Size2D { height: h.parse()?, width: w.parse()?, @@ -3639,21 +3594,21 @@ impl WsEvent { pub async fn set_component_position( ctx: &DalContext, change_set_id: ChangeSetId, - components: Vec, + geometries: Vec<(ComponentId, RawGeometry)>, client_ulid: Option, ) -> WsEventResult { let mut positions: Vec = vec![]; - for component in components { + for (component_id, geometry) in geometries { let position = ComponentPosition { - x: component.x.parse()?, - y: component.y.parse()?, + x: geometry.x.parse()?, + y: geometry.y.parse()?, }; let size = ComponentSize { - width: component.width.as_ref().map(|w| w.parse()).transpose()?, - height: component.height.as_ref().map(|w| w.parse()).transpose()?, + width: geometry.width.as_ref().map(|w| w.parse()).transpose()?, + height: geometry.height.as_ref().map(|w| w.parse()).transpose()?, }; positions.push(ComponentSetPosition { - component_id: component.id(), + component_id, position, size: Some(size), }); diff --git a/lib/dal/src/diagram.rs b/lib/dal/src/diagram.rs index a7afdaaae8..cebdd3ce2f 100644 --- a/lib/dal/src/diagram.rs +++ b/lib/dal/src/diagram.rs @@ -1,3 +1,6 @@ +pub mod geometry; +pub mod view; + use serde::{Deserialize, Serialize}; use si_data_pg::PgError; use std::collections::HashMap; @@ -15,17 +18,21 @@ use crate::component::inferred_connection_graph::InferredConnectionGraphError; use crate::component::{ ComponentError, ComponentResult, IncomingConnection, InferredConnection, OutgoingConnection, }; +use crate::diagram::geometry::GeometryId; +use crate::diagram::view::ViewId; use crate::schema::variant::SchemaVariantError; use crate::socket::input::InputSocketError; use crate::socket::output::OutputSocketError; use crate::workspace_snapshot::node_weight::category_node_weight::CategoryNodeKind; +use crate::workspace_snapshot::node_weight::NodeWeightError; use crate::workspace_snapshot::WorkspaceSnapshotError; use crate::{ - AttributePrototypeId, ChangeSetError, Component, ComponentId, DalContext, HistoryEventError, - InputSocketId, OutputSocketId, SchemaVariantId, StandardModelError, TransactionsError, - Workspace, WorkspaceError, WorkspaceSnapshot, + AttributePrototypeId, ChangeSetError, Component, ComponentId, DalContext, HelperError, + HistoryEventError, InputSocketId, OutputSocketId, SchemaVariantId, StandardModelError, + TransactionsError, Workspace, WorkspaceError, WorkspaceSnapshot, }; use si_frontend_types::{DiagramSocket, SummaryDiagramComponent}; +use si_layer_cache::LayerDbError; #[remain::sorted] #[derive(Error, Debug)] @@ -46,6 +53,8 @@ pub enum DiagramError { ComponentNotFound, #[error("component status not found for component: {0}")] ComponentStatusNotFound(ComponentId), + #[error("default view not found")] + DefaultViewNotFound, #[error("deletion timestamp not found")] DeletionTimeStamp, #[error("destination attribute prototype not found for inter component attribute prototype argument: {0}")] @@ -54,14 +63,24 @@ pub enum DiagramError { DestinationInputSocketNotFound(AttributePrototypeId, AttributePrototypeArgumentId), #[error("edge not found")] EdgeNotFound, + #[error("geometry not found: {0}")] + GeometryNotFound(GeometryId), + #[error("geometry not found for component {0} on view {1}")] + GeometryNotFoundForComponentAndView(ComponentId, ViewId), + #[error("Helper error: {0}")] + Helper(#[from] HelperError), #[error("history event error: {0}")] HistoryEvent(#[from] HistoryEventError), #[error("InferredConnectionGraph error: {0}")] InferredConnectionGraph(#[from] InferredConnectionGraphError), #[error("input socket error: {0}")] InputSocket(#[from] InputSocketError), + #[error("layerdb error: {0}")] + LayerDb(#[from] LayerDbError), #[error("node not found")] NodeNotFound, + #[error("node weight error: {0}")] + NodeWeight(#[from] NodeWeightError), #[error("output socket error: {0}")] OutputSocket(#[from] OutputSocketError), #[error(transparent)] @@ -88,15 +107,18 @@ pub enum DiagramError { Transactions(#[from] TransactionsError), #[error("could not acquire lock: {0}")] TryLock(#[from] tokio::sync::TryLockError), + #[error("view category node not found")] + ViewCategoryNotFound, + #[error("view not found: {0}")] + ViewNotFound(ViewId), + #[error("view not found for geometry id: {0}")] + ViewNotFoundForGeometry(GeometryId), #[error("Workspace error: {0}")] Workspace(#[from] WorkspaceError), #[error("workspace snapshot error: {0}")] WorkspaceSnapshot(#[from] WorkspaceSnapshotError), } -pub type NodeId = ComponentId; -pub type EdgeId = AttributePrototypeArgumentId; - pub type DiagramResult = Result; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] diff --git a/lib/dal/src/diagram/geometry.rs b/lib/dal/src/diagram/geometry.rs new file mode 100644 index 0000000000..75e6234271 --- /dev/null +++ b/lib/dal/src/diagram/geometry.rs @@ -0,0 +1,275 @@ +use crate::diagram::view::{View, ViewId}; +use crate::diagram::{DiagramError, DiagramResult}; +use crate::layer_db_types::{GeometryContent, GeometryContentV1}; +use crate::workspace_snapshot::node_weight::geometry_node_weight::GeometryNodeWeight; +use crate::workspace_snapshot::node_weight::traits::SiVersionedNodeWeight; +use crate::workspace_snapshot::node_weight::NodeWeight; +use crate::{ + id, implement_add_edge_to, ComponentId, EdgeWeightKindDiscriminants, Timestamp, + WorkspaceSnapshotError, +}; +use crate::{DalContext, EdgeWeightKind}; +use jwt_simple::prelude::{Deserialize, Serialize}; +use si_events::ulid::Ulid; +use si_events::ContentHash; +use std::sync::Arc; + +const DEFAULT_COMPONENT_X_POSITION: &str = "0"; +const DEFAULT_COMPONENT_Y_POSITION: &str = "0"; +const DEFAULT_COMPONENT_WIDTH: &str = "500"; +const DEFAULT_COMPONENT_HEIGHT: &str = "500"; + +#[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq)] +pub struct RawGeometry { + pub x: String, + pub y: String, + pub width: Option, + pub height: Option, +} + +impl From for RawGeometry { + fn from(value: Geometry) -> Self { + Self { + x: value.x, + y: value.y, + width: value.width, + height: value.height, + } + } +} + +id!(GeometryId); + +/// Represents spatial data for something to be shown on a view +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] +pub struct Geometry { + id: GeometryId, + #[serde(flatten)] + timestamp: Timestamp, + x: String, + y: String, + width: Option, + height: Option, +} + +impl Geometry { + implement_add_edge_to!( + source_id: GeometryId, + destination_id: ComponentId, + add_fn: add_edge_to_component, + discriminant: EdgeWeightKindDiscriminants::Represents, + result: DiagramResult, + ); + + pub fn raw(self) -> RawGeometry { + self.into() + } + + pub fn x(&self) -> &str { + &self.x + } + + pub fn y(&self) -> &str { + &self.y + } + + pub fn width(&self) -> Option<&str> { + self.width.as_deref() + } + + pub fn height(&self) -> Option<&str> { + self.height.as_deref() + } + + fn assemble(node_weight: GeometryNodeWeight, content: GeometryContent) -> Self { + let content = content.extract(); + + Self { + id: node_weight.id().into(), + timestamp: content.timestamp, + x: content.x, + y: content.y, + width: content.width, + height: content.height, + } + } + + pub async fn new( + ctx: &DalContext, + component_id: ComponentId, + view_id: ViewId, + ) -> DiagramResult { + let snap = ctx.workspace_snapshot()?; + let id = snap.generate_ulid().await?; + let lineage_id = snap.generate_ulid().await?; + + let content = GeometryContent::V1(GeometryContentV1 { + timestamp: Timestamp::now(), + x: DEFAULT_COMPONENT_X_POSITION.to_string(), + y: DEFAULT_COMPONENT_Y_POSITION.to_string(), + width: Some(DEFAULT_COMPONENT_WIDTH.to_string()), + height: Some(DEFAULT_COMPONENT_HEIGHT.to_string()), + }); + + let (content_address, _) = ctx + .layer_db() + .cas() + .write( + Arc::new(content.clone().into()), + None, + ctx.events_tenancy(), + ctx.events_actor(), + ) + .await?; + + let node_weight = NodeWeight::new_geometry(id, lineage_id, content_address); + snap.add_or_replace_node(node_weight.clone()).await?; + + Self::add_edge_to_component(ctx, id.into(), component_id, EdgeWeightKind::Represents) + .await?; + + View::add_geometry_by_id(ctx, view_id, id.into()).await?; + + Ok(Self::assemble( + node_weight.get_geometry_node_weight()?, + content, + )) + } + + pub async fn get_by_id(ctx: &DalContext, component_id: GeometryId) -> DiagramResult { + let (node_weight, content) = Self::get_node_weight_and_content(ctx, component_id).await?; + Ok(Self::assemble(node_weight, content)) + } + + pub async fn get_by_component_and_view( + ctx: &DalContext, + component_id: ComponentId, + view_id: ViewId, + ) -> DiagramResult { + let snap = ctx.workspace_snapshot()?; + + let mut maybe_weight = None; + for geometry_idx in snap + .incoming_sources_for_edge_weight_kind( + component_id, + EdgeWeightKindDiscriminants::Represents, + ) + .await? + { + let node_weight = snap + .get_node_weight(geometry_idx) + .await? + .get_geometry_node_weight()?; + + let this_view_id = Self::get_view_id_by_id(ctx, node_weight.id().into()).await?; + + if this_view_id == view_id { + maybe_weight = Some(node_weight); + } + } + + let Some(node_weight) = maybe_weight else { + return Err(DiagramError::GeometryNotFoundForComponentAndView( + component_id, + view_id, + )); + }; + + let content = Self::try_get_content(ctx, &node_weight.content_hash()) + .await? + .ok_or(WorkspaceSnapshotError::MissingContentFromStore( + node_weight.id(), + ))?; + + Ok(Self::assemble(node_weight, content)) + } + + pub async fn get_view_id_by_id(ctx: &DalContext, id: GeometryId) -> DiagramResult { + let snap = ctx.workspace_snapshot()?; + let view_idx = snap + .incoming_sources_for_edge_weight_kind(id, EdgeWeightKindDiscriminants::Use) + .await? + .pop() + .ok_or(DiagramError::ViewNotFoundForGeometry(id))?; + + Ok(snap + .get_node_weight(view_idx) + .await? + .get_view_node_weight()? + .id() + .into()) + } + + async fn get_node_weight_and_content( + ctx: &DalContext, + geometry_id: GeometryId, + ) -> DiagramResult<(GeometryNodeWeight, GeometryContent)> { + Self::try_get_node_weight_and_content(ctx, geometry_id) + .await? + .ok_or(DiagramError::GeometryNotFound(geometry_id)) + } + + async fn try_get_node_weight_and_content( + ctx: &DalContext, + geometry_id: GeometryId, + ) -> DiagramResult> { + let id: Ulid = geometry_id.into(); + + let Some(node_index) = ctx.workspace_snapshot()?.get_node_index_by_id_opt(id).await else { + return Ok(None); + }; + + let node_weight = ctx + .workspace_snapshot()? + .get_node_weight(node_index) + .await?; + + let hash = node_weight.content_hash(); + let component_node_weight = node_weight.get_geometry_node_weight()?; + + let content = Self::try_get_content(ctx, &hash).await?.ok_or( + WorkspaceSnapshotError::MissingContentFromStore(geometry_id.into()), + )?; + + Ok(Some((component_node_weight, content))) + } + + async fn try_get_content( + ctx: &DalContext, + hash: &ContentHash, + ) -> DiagramResult> { + Ok(ctx.layer_db().cas().try_read_as(hash).await?) + } + + pub async fn update( + &mut self, + ctx: &DalContext, + new_geometry: RawGeometry, + ) -> DiagramResult<()> { + let (hash, _) = ctx + .layer_db() + .cas() + .write( + Arc::new( + GeometryContent::V1(GeometryContentV1 { + timestamp: Timestamp::now(), + x: new_geometry.x, + y: new_geometry.y, + width: new_geometry.width, + height: new_geometry.height, + }) + .into(), + ), + None, + ctx.events_tenancy(), + ctx.events_actor(), + ) + .await?; + + ctx.workspace_snapshot()? + .update_content(self.id.into(), hash) + .await?; + + Ok(()) + } +} diff --git a/lib/dal/src/diagram/view.rs b/lib/dal/src/diagram/view.rs new file mode 100644 index 0000000000..d24315fe48 --- /dev/null +++ b/lib/dal/src/diagram/view.rs @@ -0,0 +1,254 @@ +use crate::diagram::geometry::GeometryId; +use crate::diagram::{DiagramError, DiagramResult}; +use crate::layer_db_types::{ViewContent, ViewContentV1}; +use crate::workspace_snapshot::node_weight::category_node_weight::CategoryNodeKind; +use crate::workspace_snapshot::node_weight::traits::SiVersionedNodeWeight; +use crate::workspace_snapshot::node_weight::view_node_weight::ViewNodeWeight; +use crate::workspace_snapshot::node_weight::NodeWeight; +use crate::{ + id, implement_add_edge_to, EdgeWeightKindDiscriminants, Timestamp, WorkspaceSnapshotError, +}; +use crate::{DalContext, EdgeWeightKind}; +use jwt_simple::prelude::{Deserialize, Serialize}; +use petgraph::Outgoing; +use si_events::ulid::Ulid; +use si_events::ContentHash; +use std::sync::Arc; + +id!(ViewId); + +/// Represents spatial data for something to be shown on a view +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] +pub struct View { + id: ViewId, + name: String, + #[serde(flatten)] + timestamp: Timestamp, +} + +impl View { + implement_add_edge_to!( + source_id: ViewId, + destination_id: GeometryId, + add_fn: add_edge_to_geometry, + discriminant: EdgeWeightKindDiscriminants::Use, + result: DiagramResult, + ); + + implement_add_edge_to!( + source_id: Ulid, + destination_id: ViewId, + add_fn: add_category_edge, + discriminant: EdgeWeightKindDiscriminants::Use, + result: DiagramResult, + ); + + pub fn name(&self) -> &str { + self.name.as_ref() + } + + fn assemble(node_weight: ViewNodeWeight, content: ViewContent) -> Self { + let content = content.extract(); + + Self { + id: node_weight.id().into(), + timestamp: content.timestamp, + name: content.name, + } + } + + pub async fn new(ctx: &DalContext, name: impl AsRef) -> DiagramResult { + let snap = ctx.workspace_snapshot()?; + let id = snap.generate_ulid().await?; + let lineage_id = snap.generate_ulid().await?; + + let content = ViewContent::V1(ViewContentV1 { + timestamp: Timestamp::now(), + name: name.as_ref().to_owned(), + }); + + let (content_address, _) = ctx + .layer_db() + .cas() + .write( + Arc::new(content.clone().into()), + None, + ctx.events_tenancy(), + ctx.events_actor(), + ) + .await?; + + let node_weight = NodeWeight::new_view(id, lineage_id, content_address); + snap.add_or_replace_node(node_weight.clone()).await?; + + // Root --> View Category --> View (this) + let view_category_id = snap + .get_category_node_or_err(None, CategoryNodeKind::View) + .await?; + Self::add_category_edge(ctx, view_category_id, id.into(), EdgeWeightKind::new_use()) + .await?; + + Ok(Self::assemble(node_weight.get_view_node_weight()?, content)) + } + + pub async fn get_by_id(ctx: &DalContext, view_id: ViewId) -> DiagramResult { + let (node_weight, content) = Self::get_node_weight_and_content(ctx, view_id).await?; + Ok(Self::assemble(node_weight, content)) + } + + pub async fn get_id_for_default(ctx: &DalContext) -> DiagramResult { + let snap = ctx.workspace_snapshot()?; + + let view_category_id = snap + .get_category_node(None, CategoryNodeKind::View) + .await? + .ok_or(DiagramError::ViewCategoryNotFound)?; + + let mut maybe_default_view = None; + for (edge, _from_idx, to_idx) in snap + .edges_directed_for_edge_weight_kind( + view_category_id, + Outgoing, + EdgeWeightKindDiscriminants::Use, + ) + .await? + { + if *edge.kind() == EdgeWeightKind::new_use_default() { + maybe_default_view = Some(snap.get_node_weight(to_idx).await?.id()) + } + } + + let Some(default_view) = maybe_default_view else { + return Err(DiagramError::DefaultViewNotFound); + }; + + Ok(default_view.into()) + } + + async fn get_node_weight_and_content( + ctx: &DalContext, + view_id: ViewId, + ) -> DiagramResult<(ViewNodeWeight, ViewContent)> { + Self::try_get_node_weight_and_content(ctx, view_id) + .await? + .ok_or(DiagramError::ViewNotFound(view_id)) + } + + async fn try_get_node_weight_and_content( + ctx: &DalContext, + view_id: ViewId, + ) -> DiagramResult> { + let id: Ulid = view_id.into(); + + let Some(node_index) = ctx.workspace_snapshot()?.get_node_index_by_id_opt(id).await else { + return Ok(None); + }; + + let node_weight = ctx + .workspace_snapshot()? + .get_node_weight(node_index) + .await? + .get_view_node_weight()?; + + let hash = node_weight.content_hash(); + + let content = Self::try_get_content(ctx, &hash).await?.ok_or( + WorkspaceSnapshotError::MissingContentFromStore(view_id.into()), + )?; + + Ok(Some((node_weight, content))) + } + + async fn try_get_content( + ctx: &DalContext, + hash: &ContentHash, + ) -> DiagramResult> { + Ok(ctx.layer_db().cas().try_read_as(hash).await?) + } + + pub async fn add_geometry_by_id( + ctx: &DalContext, + view_id: ViewId, + geometry_id: GeometryId, + ) -> DiagramResult<()> { + Self::add_edge_to_geometry( + ctx, + view_id, + geometry_id, + EdgeWeightKind::Use { is_default: false }, + ) + .await + } + + pub async fn set_name(&mut self, ctx: &DalContext, name: impl AsRef) -> DiagramResult<()> { + let (hash, _) = ctx + .layer_db() + .cas() + .write( + Arc::new( + ViewContent::V1(ViewContentV1 { + timestamp: self.timestamp, + name: name.as_ref().to_owned(), + }) + .into(), + ), + None, + ctx.events_tenancy(), + ctx.events_actor(), + ) + .await?; + + ctx.workspace_snapshot()? + .update_content(self.id.into(), hash) + .await?; + + Ok(()) + } + + pub async fn set_default(ctx: &DalContext, view_id: ViewId) -> DiagramResult<()> { + let snap = ctx.workspace_snapshot()?; + + let view_category_id = snap + .get_category_node(None, CategoryNodeKind::View) + .await? + .ok_or(DiagramError::ViewCategoryNotFound)?; + + // Update edge to old default + for (edge, from_idx, to_idx) in snap + .edges_directed_for_edge_weight_kind( + view_category_id, + Outgoing, + EdgeWeightKindDiscriminants::Use, + ) + .await? + { + if *edge.kind() == EdgeWeightKind::new_use_default() { + // We have found the existing previous default view + // we now need to update that edge to be a Use + snap.remove_edge(from_idx, to_idx, edge.kind().into()) + .await?; + + Self::add_category_edge( + ctx, + view_category_id, + snap.get_node_weight(to_idx).await?.id().into(), + EdgeWeightKind::new_use(), + ) + .await?; + } + } + + snap.remove_edge_for_ulids(view_category_id, view_id, EdgeWeightKindDiscriminants::Use) + .await?; + + Self::add_category_edge( + ctx, + view_category_id, + view_id, + EdgeWeightKind::new_use_default(), + ) + .await?; + + Ok(()) + } +} diff --git a/lib/dal/src/layer_db_types/content_types.rs b/lib/dal/src/layer_db_types/content_types.rs index 5c086b5db6..e09020890b 100644 --- a/lib/dal/src/layer_db_types/content_types.rs +++ b/lib/dal/src/layer_db_types/content_types.rs @@ -58,6 +58,8 @@ pub enum ContentTypes { Validation(ValidationContent), OutputSocket(OutputSocketContent), ManagementPrototype(ManagementPrototypeContent), + Geometry(GeometryContent), + View(ViewContent), } macro_rules! impl_into_content_types { @@ -113,6 +115,8 @@ impl_into_content_types!(Secret); impl_into_content_types!(StaticArgumentValue); impl_into_content_types!(Validation); impl_into_content_types!(ManagementPrototype); +impl_into_content_types!(Geometry); +impl_into_content_types!(View); // Here we've broken the Foo, FooContent convention so we need to implement // these traits manually @@ -214,8 +218,21 @@ pub struct AttributePrototypeContentV1 { } #[derive(Debug, Clone, EnumDiscriminants, Serialize, Deserialize, PartialEq)] +#[strum_discriminants(derive(strum::Display))] pub enum ComponentContent { V1(ComponentContentV1), + V2(ComponentContentV2), +} + +impl ComponentContent { + pub fn extract(self) -> ComponentContentV2 { + match self { + ComponentContent::V1(v1) => ComponentContentV2 { + timestamp: v1.timestamp, + }, + ComponentContent::V2(v2) => v2, + } + } } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] @@ -227,6 +244,50 @@ pub struct ComponentContentV1 { pub height: Option, } +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +pub struct ComponentContentV2 { + pub timestamp: Timestamp, +} + +#[derive(Debug, Clone, EnumDiscriminants, Serialize, Deserialize, PartialEq)] +pub enum ViewContent { + V1(ViewContentV1), +} + +impl ViewContent { + pub fn extract(self) -> ViewContentV1 { + let ViewContent::V1(content) = self; + content + } +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +pub struct ViewContentV1 { + pub timestamp: Timestamp, + pub name: String, +} + +#[derive(Debug, Clone, EnumDiscriminants, Serialize, Deserialize, PartialEq)] +pub enum GeometryContent { + V1(GeometryContentV1), +} + +impl GeometryContent { + pub fn extract(self) -> GeometryContentV1 { + let GeometryContent::V1(content) = self; + content + } +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +pub struct GeometryContentV1 { + pub timestamp: Timestamp, + pub x: String, + pub y: String, + pub width: Option, + pub height: Option, +} + #[derive(Debug, Clone, EnumDiscriminants, Serialize, Deserialize, PartialEq)] pub enum FuncContent { V1(FuncContentV1), @@ -265,11 +326,6 @@ pub struct FuncContentV2 { pub is_locked: bool, } -#[derive(Debug, Clone, EnumDiscriminants, Serialize, Deserialize, PartialEq)] -pub enum FuncArgumentContent { - V1(FuncArgumentContentV1), -} - impl FuncContent { pub fn extract(self) -> FuncContentV2 { match self { @@ -292,6 +348,11 @@ impl FuncContent { } } +#[derive(Debug, Clone, EnumDiscriminants, Serialize, Deserialize, PartialEq)] +pub enum FuncArgumentContent { + V1(FuncArgumentContentV1), +} + #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] pub struct FuncArgumentContentV1 { pub kind: FuncArgumentKind, diff --git a/lib/dal/src/lib.rs b/lib/dal/src/lib.rs index b64aa5af3a..6213aea2eb 100644 --- a/lib/dal/src/lib.rs +++ b/lib/dal/src/lib.rs @@ -129,9 +129,7 @@ pub use timestamp::{Timestamp, TimestampError}; pub use user::{User, UserClaim, UserError, UserPk, UserResult}; pub use visibility::Visibility; pub use workspace::{Workspace, WorkspaceError, WorkspacePk, WorkspaceResult}; -pub use workspace_snapshot::graph::{ - WorkspaceSnapshotGraph, WorkspaceSnapshotGraphV3, WorkspaceSnapshotGraphVCurrent, -}; +pub use workspace_snapshot::graph::{WorkspaceSnapshotGraph, WorkspaceSnapshotGraphVCurrent}; pub use workspace_snapshot::{ edge_weight::{EdgeWeight, EdgeWeightKind, EdgeWeightKindDiscriminants}, node_weight::NodeWeightDiscriminants, diff --git a/lib/dal/src/management/prototype.rs b/lib/dal/src/management/prototype.rs index 8b60b17848..4b5c1c828b 100644 --- a/lib/dal/src/management/prototype.rs +++ b/lib/dal/src/management/prototype.rs @@ -188,7 +188,7 @@ impl ManagementPrototype { let management_func_id = ManagementPrototype::func_id(ctx, id).await?; let manager_component = Component::get_by_id(ctx, manager_component_id).await?; let manager_component_view = manager_component.view(ctx).await?; - let geometry = manager_component.geometry(); + let geometry = manager_component.geometry(ctx).await?; let args = serde_json::json!({ "this_component": { diff --git a/lib/dal/src/schema/variant.rs b/lib/dal/src/schema/variant.rs index 10676ec09c..53a58eea7c 100644 --- a/lib/dal/src/schema/variant.rs +++ b/lib/dal/src/schema/variant.rs @@ -2233,6 +2233,7 @@ impl SchemaVariant { EdgeWeightKindDiscriminants::Action | EdgeWeightKindDiscriminants::Contain | EdgeWeightKindDiscriminants::FrameContains + | EdgeWeightKindDiscriminants::Represents | EdgeWeightKindDiscriminants::Ordering | EdgeWeightKindDiscriminants::Ordinal | EdgeWeightKindDiscriminants::Prop diff --git a/lib/dal/src/workspace.rs b/lib/dal/src/workspace.rs index e574eaae6b..3786e6312d 100644 --- a/lib/dal/src/workspace.rs +++ b/lib/dal/src/workspace.rs @@ -200,7 +200,7 @@ impl Workspace { pub async fn setup_builtin(ctx: &mut DalContext) -> WorkspaceResult<()> { // Check if the builtin already exists. If so, update our tenancy and visibility using it. if let Some(mut found_builtin) = Self::find_builtin(ctx).await? { - // 'reset' the workspace snapshot' so that we remigrate all builtins on each startup + // 'reset' the workspace snapshot so that we remigrate all builtins on each startup let new_snapshot = WorkspaceSnapshot::initial(ctx).await?; let new_snap_address = new_snapshot.write(ctx).await?; let new_change_set = diff --git a/lib/dal/src/workspace_snapshot.rs b/lib/dal/src/workspace_snapshot.rs index b1c67a5427..d1c68d1fe3 100644 --- a/lib/dal/src/workspace_snapshot.rs +++ b/lib/dal/src/workspace_snapshot.rs @@ -44,7 +44,6 @@ use serde::{Deserialize, Serialize}; use si_data_pg::PgError; use si_events::{ulid::Ulid, ContentHash, WorkspaceSnapshotAddress}; use si_layer_cache::LayerDbError; -use strum::IntoEnumIterator; use telemetry::prelude::*; use thiserror::Error; use tokio::sync::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard}; @@ -170,11 +169,7 @@ impl WorkspaceSnapshotError { pub fn is_node_with_id_not_found(&self) -> bool { matches!( self, - Self::WorkspaceSnapshotGraph( - crate::workspace_snapshot::graph::WorkspaceSnapshotGraphError::NodeWithIdNotFound( - _, - ), - ) + Self::WorkspaceSnapshotGraph(WorkspaceSnapshotGraphError::NodeWithIdNotFound(_,),) ) } } @@ -232,7 +227,7 @@ pub struct CycleCheckGuard { cycle_check: Arc, } -impl std::ops::Drop for CycleCheckGuard { +impl Drop for CycleCheckGuard { fn drop(&mut self) { if Arc::strong_count(&self.cycle_check) <= 2 { self.cycle_check @@ -361,26 +356,14 @@ impl From for Ulid { impl WorkspaceSnapshot { #[instrument(name = "workspace_snapshot.initial", level = "debug", skip_all)] pub async fn initial(ctx: &DalContext) -> WorkspaceSnapshotResult { - let mut graph: WorkspaceSnapshotGraphVCurrent = WorkspaceSnapshotGraphVCurrent::new()?; - - // Create the category nodes under root. - for category_node_kind in CategoryNodeKind::iter() { - let id = graph.generate_ulid()?; - let lineage_id = graph.generate_ulid()?; - let category_node_index = - graph.add_category_node(id, lineage_id, category_node_kind)?; - graph.add_edge( - graph.root(), - EdgeWeight::new(EdgeWeightKind::new_use()), - category_node_index, - )?; - } + let graph: WorkspaceSnapshotGraphVCurrent = + WorkspaceSnapshotGraphVCurrent::new(ctx).await?; // We do not care about any field other than "working_copy" because // "write" will populate them using the assigned working copy. let initial = Self { address: Arc::new(RwLock::new(WorkspaceSnapshotAddress::nil())), - read_only_graph: Arc::new(WorkspaceSnapshotGraph::V3(graph)), + read_only_graph: Arc::new(WorkspaceSnapshotGraph::V4(graph)), working_copy: Arc::new(RwLock::new(None)), cycle_check: Arc::new(AtomicBool::new(false)), dvu_roots: Arc::new(Mutex::new(HashSet::new())), @@ -513,7 +496,7 @@ impl WorkspaceSnapshot { let (new_address, _) = layer_db .workspace_snapshot() .write( - Arc::new(WorkspaceSnapshotGraph::V3(working_copy.clone())), + Arc::new(WorkspaceSnapshotGraph::V4(working_copy.clone())), None, events_tenancy, events_actor, @@ -605,7 +588,7 @@ impl WorkspaceSnapshot { pub async fn serialized(&self) -> WorkspaceSnapshotResult> { let graph = self.working_copy().await.clone(); Ok(si_layer_cache::db::serialize::to_vec( - &WorkspaceSnapshotGraph::V3(graph), + &WorkspaceSnapshotGraph::V4(graph), )?) } @@ -1325,8 +1308,7 @@ impl WorkspaceSnapshot { let idx = self.get_node_index_by_id(current_id).await?; self.working_copy_mut() .await - .update_node_id(idx, new_id, new_lineage_id) - .await?; + .update_node_id(idx, new_id, new_lineage_id)?; Ok(()) } @@ -1612,6 +1594,7 @@ impl WorkspaceSnapshot { | ContentAddressDiscriminants::DeprecatedActionRunner | ContentAddressDiscriminants::Func | ContentAddressDiscriminants::FuncArg + | ContentAddressDiscriminants::Geometry | ContentAddressDiscriminants::InputSocket | ContentAddressDiscriminants::JsonValue | ContentAddressDiscriminants::Module @@ -1622,6 +1605,7 @@ impl WorkspaceSnapshot { | ContentAddressDiscriminants::SchemaVariant | ContentAddressDiscriminants::Secret | ContentAddressDiscriminants::ValidationPrototype + | ContentAddressDiscriminants::View | ContentAddressDiscriminants::ManagementPrototype => None, }, @@ -1633,6 +1617,8 @@ impl WorkspaceSnapshot { | NodeWeight::FinishedDependentValueRoot(_) | NodeWeight::Func(_) | NodeWeight::FuncArgument(_) + | NodeWeight::Geometry(_) + | NodeWeight::View(_) | NodeWeight::InputSocket(_) | NodeWeight::Prop(_) | NodeWeight::SchemaVariant(_) diff --git a/lib/dal/src/workspace_snapshot/content_address.rs b/lib/dal/src/workspace_snapshot/content_address.rs index 62b9744d6a..d1c0cde0c2 100644 --- a/lib/dal/src/workspace_snapshot/content_address.rs +++ b/lib/dal/src/workspace_snapshot/content_address.rs @@ -36,6 +36,8 @@ pub enum ContentAddress { // TODO(victor): remove this when we migrate the graph next ValidationPrototype(ContentHash), ManagementPrototype(ContentHash), + Geometry(ContentHash), + View(ContentHash), } impl ContentAddress { @@ -48,12 +50,13 @@ impl ContentAddress { | ContentAddress::DeprecatedAction(id) | ContentAddress::DeprecatedActionBatch(id) | ContentAddress::DeprecatedActionRunner(id) - | ContentAddress::OutputSocket(id) | ContentAddress::FuncArg(id) | ContentAddress::Func(id) + | ContentAddress::Geometry(id) | ContentAddress::InputSocket(id) | ContentAddress::JsonValue(id) | ContentAddress::Module(id) + | ContentAddress::OutputSocket(id) | ContentAddress::Prop(id) | ContentAddress::Schema(id) | ContentAddress::SchemaVariant(id) @@ -61,6 +64,7 @@ impl ContentAddress { | ContentAddress::StaticArgumentValue(id) | ContentAddress::ValidationPrototype(id) | ContentAddress::ValidationOutput(id) + | ContentAddress::View(id) | ContentAddress::ManagementPrototype(id) => Some(*id), } .unwrap_or_default() diff --git a/lib/dal/src/workspace_snapshot/edge_weight.rs b/lib/dal/src/workspace_snapshot/edge_weight.rs index c4fc73d8b5..cf9e7dd465 100644 --- a/lib/dal/src/workspace_snapshot/edge_weight.rs +++ b/lib/dal/src/workspace_snapshot/edge_weight.rs @@ -63,6 +63,8 @@ pub enum EdgeWeightKind { ValidationOutput, /// Edge from [`SchemaVariant`](crate::SchemaVariant) to [`ManagementPrototype`](crate::ManagementPrototype). ManagementPrototype, + /// From a Geometry node to the node it represents on a view. + Represents, } impl EdgeWeightKind { diff --git a/lib/dal/src/workspace_snapshot/graph.rs b/lib/dal/src/workspace_snapshot/graph.rs index 6d935e257c..4a1a1bec54 100644 --- a/lib/dal/src/workspace_snapshot/graph.rs +++ b/lib/dal/src/workspace_snapshot/graph.rs @@ -8,10 +8,10 @@ pub use petgraph::{graph::NodeIndex, Direction}; use serde::{Deserialize, Serialize}; use si_events::{merkle_tree_hash::MerkleTreeHash, ulid::Ulid}; use si_layer_cache::db::serialize; +use si_layer_cache::LayerDbError; use strum::{EnumDiscriminants, EnumIter, EnumString, IntoEnumIterator}; -use thiserror::Error; - use telemetry::prelude::*; +use thiserror::Error; use crate::{ workspace_snapshot::node_weight::{category_node_weight::CategoryNodeKind, NodeWeightError}, @@ -21,14 +21,17 @@ use crate::{ pub mod correct_transforms; pub mod deprecated; pub mod detect_updates; +mod tests; pub mod v2; pub mod v3; +pub mod v4; pub use v2::WorkspaceSnapshotGraphV2; pub use v3::WorkspaceSnapshotGraphV3; +pub use v4::WorkspaceSnapshotGraphV4; pub type LineageId = Ulid; -pub type WorkspaceSnapshotGraphVCurrent = WorkspaceSnapshotGraphV3; +pub type WorkspaceSnapshotGraphVCurrent = WorkspaceSnapshotGraphV4; #[allow(clippy::large_enum_variant)] #[remain::sorted] @@ -58,6 +61,8 @@ pub enum WorkspaceSnapshotGraphError { IncompatibleNodeTypes, #[error("Invalid value graph")] InvalidValueGraph, + #[error("layerdb error: {0}")] + LayerDb(#[from] LayerDbError), #[error("monotonic error: {0}")] Monotonic(#[from] ulid::MonotonicError), #[error("mutex poisoning: {0}")] @@ -94,6 +99,8 @@ pub enum WorkspaceSnapshotGraph { V2(WorkspaceSnapshotGraphV2), /// Added `InputSocket` and `SchemaVariant` `NodeWeight` variants. V3(WorkspaceSnapshotGraphV3), + /// Added `View`, `Geometry` and `DiagramObject` categories, + V4(WorkspaceSnapshotGraphV4), } impl std::ops::Deref for WorkspaceSnapshotGraph { @@ -114,19 +121,19 @@ impl WorkspaceSnapshotGraph { /// Return a reference to the most up to date enum variant for the graph type pub fn inner(&self) -> &WorkspaceSnapshotGraphVCurrent { match self { - Self::Legacy | Self::V1(_) | Self::V2(_) => { + Self::Legacy | Self::V1(_) | Self::V2(_) | Self::V3(_) => { unimplemented!("Attempted to access an unmigrated snapshot!") } - Self::V3(inner) => inner, + Self::V4(inner) => inner, } } pub fn inner_mut(&mut self) -> &mut WorkspaceSnapshotGraphVCurrent { match self { - Self::Legacy | Self::V1(_) | Self::V2(_) => { + Self::Legacy | Self::V1(_) | Self::V2(_) | Self::V3(_) => { unimplemented!("Attempted to access an unmigrated snapshot!") } - Self::V3(inner) => inner, + Self::V4(inner) => inner, } } diff --git a/lib/dal/src/workspace_snapshot/graph/correct_transforms.rs b/lib/dal/src/workspace_snapshot/graph/correct_transforms.rs index cb022a15d2..13ab42ab22 100644 --- a/lib/dal/src/workspace_snapshot/graph/correct_transforms.rs +++ b/lib/dal/src/workspace_snapshot/graph/correct_transforms.rs @@ -9,10 +9,10 @@ use crate::workspace_snapshot::node_weight::NodeWeight; use crate::workspace_snapshot::NodeInformation; use crate::{EdgeWeight, EdgeWeightKind, NodeWeightDiscriminants}; -use super::{detect_updates::Update, WorkspaceSnapshotGraphV3}; +use super::{detect_updates::Update, WorkspaceSnapshotGraphVCurrent}; pub fn correct_transforms( - graph: &WorkspaceSnapshotGraphV3, + graph: &WorkspaceSnapshotGraphVCurrent, mut updates: Vec, from_different_change_set: bool, ) -> CorrectTransformsResult> { @@ -67,7 +67,7 @@ pub fn correct_transforms( /// Produce the NewNode and NewEdge updates required for adding a dependent value root to the graph pub fn add_dependent_value_root_updates( - graph: &WorkspaceSnapshotGraphV3, + graph: &WorkspaceSnapshotGraphVCurrent, value_ids: &HashSet, ) -> CorrectTransformsResult> { let mut updates = vec![]; diff --git a/lib/dal/src/workspace_snapshot/graph/detect_updates.rs b/lib/dal/src/workspace_snapshot/graph/detect_updates.rs index d700c51620..1b39963bbf 100644 --- a/lib/dal/src/workspace_snapshot/graph/detect_updates.rs +++ b/lib/dal/src/workspace_snapshot/graph/detect_updates.rs @@ -13,7 +13,7 @@ use crate::{ EdgeWeight, EdgeWeightKind, EdgeWeightKindDiscriminants, }; -use super::WorkspaceSnapshotGraphV3; +use super::WorkspaceSnapshotGraphVCurrent; #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, EnumDiscriminants)] pub enum Update { @@ -42,14 +42,14 @@ enum NodeDifference { } pub struct Detector<'a, 'b> { - base_graph: &'a WorkspaceSnapshotGraphV3, - updated_graph: &'b WorkspaceSnapshotGraphV3, + base_graph: &'a WorkspaceSnapshotGraphVCurrent, + updated_graph: &'b WorkspaceSnapshotGraphVCurrent, } impl<'a, 'b> Detector<'a, 'b> { pub fn new( - base_graph: &'a WorkspaceSnapshotGraphV3, - updated_graph: &'b WorkspaceSnapshotGraphV3, + base_graph: &'a WorkspaceSnapshotGraphVCurrent, + updated_graph: &'b WorkspaceSnapshotGraphVCurrent, ) -> Self { Self { base_graph, diff --git a/lib/dal/src/workspace_snapshot/graph/v3/tests.rs b/lib/dal/src/workspace_snapshot/graph/tests.rs similarity index 94% rename from lib/dal/src/workspace_snapshot/graph/v3/tests.rs rename to lib/dal/src/workspace_snapshot/graph/tests.rs index 187f7d3352..6ac1bf2a05 100644 --- a/lib/dal/src/workspace_snapshot/graph/v3/tests.rs +++ b/lib/dal/src/workspace_snapshot/graph/tests.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use si_events::{ulid::Ulid, ContentHash}; use crate::{ - workspace_snapshot::{graph::WorkspaceSnapshotGraphV3, node_weight::NodeWeight}, + workspace_snapshot::{graph::WorkspaceSnapshotGraphVCurrent, node_weight::NodeWeight}, EdgeWeight, EdgeWeightKind, PropKind, }; @@ -13,7 +13,7 @@ mod rebase; #[allow(dead_code)] fn add_prop_nodes_to_graph<'a, 'b>( - graph: &'a mut WorkspaceSnapshotGraphV3, + graph: &'a mut WorkspaceSnapshotGraphVCurrent, nodes: &'a [&'b str], ordered: bool, ) -> HashMap<&'b str, Ulid> { @@ -47,7 +47,7 @@ fn add_prop_nodes_to_graph<'a, 'b>( #[allow(dead_code)] fn add_edges( - graph: &mut WorkspaceSnapshotGraphV3, + graph: &mut WorkspaceSnapshotGraphVCurrent, node_id_map: &HashMap<&str, Ulid>, edges: &[(Option<&str>, &str)], ) { @@ -82,7 +82,8 @@ fn add_edges( #[allow(clippy::panic)] #[cfg(test)] mod test { - use petgraph::{graph::NodeIndex, visit::EdgeRef, Outgoing}; + use petgraph::visit::EdgeRef; + use petgraph::{graph::NodeIndex, Outgoing}; use pretty_assertions_sorted::assert_eq; use si_events::{merkle_tree_hash::MerkleTreeHash, ulid::Ulid, ContentHash}; use std::{collections::HashSet, str::FromStr}; @@ -90,7 +91,7 @@ mod test { use crate::workspace_snapshot::{ content_address::ContentAddress, edge_weight::{EdgeWeight, EdgeWeightKind, EdgeWeightKindDiscriminants}, - graph::{detect_updates::Update, WorkspaceSnapshotGraphV3}, + graph::{detect_updates::Update, WorkspaceSnapshotGraphVCurrent}, node_weight::NodeWeight, }; use crate::{ComponentId, FuncId, PropId, SchemaId, SchemaVariantId}; @@ -99,8 +100,8 @@ mod test { #[test] fn new() { - let graph = - WorkspaceSnapshotGraphV3::new().expect("Unable to create WorkspaceSnapshotGraph"); + let graph = WorkspaceSnapshotGraphVCurrent::new_for_unit_tests() + .expect("Unable to create WorkspaceSnapshotGraph"); assert!(graph.is_acyclic_directed()); } @@ -109,11 +110,11 @@ mod test { // on a fresh graph (like add_ordered_node) #[test] fn get_root_index_by_root_id_on_fresh_graph() { - let graph = - WorkspaceSnapshotGraphV3::new().expect("Unable to create WorkspaceSnapshotGraph"); + let graph = WorkspaceSnapshotGraphVCurrent::new_for_unit_tests() + .expect("Unable to create WorkspaceSnapshotGraph"); let root_id = graph - .get_node_weight(graph.root_index) + .get_node_weight(graph.root()) .expect("get root weight") .id(); @@ -121,7 +122,7 @@ mod test { .get_node_index_by_id(root_id) .expect("get root node index from ULID"); - assert_eq!(graph.root_index, root_node_idx); + assert_eq!(graph.root(), root_node_idx); } #[test] @@ -168,8 +169,8 @@ mod test { (Some("a"), "b"), ]; - let mut graph = - WorkspaceSnapshotGraphV3::new().expect("Unable to create WorkspaceSnapshotGraph"); + let mut graph = WorkspaceSnapshotGraphVCurrent::new_for_unit_tests() + .expect("Unable to create WorkspaceSnapshotGraph"); let node_id_map = add_prop_nodes_to_graph(&mut graph, &nodes, false); add_edges(&mut graph, &node_id_map, &edges); @@ -180,7 +181,7 @@ mod test { for (source, target) in edges { let source_idx = match source { - None => graph.root_index, + None => graph.root(), Some(node) => graph .get_node_index_by_id( node_id_map @@ -223,8 +224,8 @@ mod test { #[test] fn add_nodes_and_edges() { - let mut graph = - WorkspaceSnapshotGraphV3::new().expect("Unable to create WorkspaceSnapshotGraph"); + let mut graph = WorkspaceSnapshotGraphVCurrent::new_for_unit_tests() + .expect("Unable to create WorkspaceSnapshotGraph"); let schema_id = graph.generate_ulid().expect("Unable to generate Ulid"); let schema_index = graph @@ -259,14 +260,14 @@ mod test { graph .add_edge( - graph.root_index, + graph.root(), EdgeWeight::new(EdgeWeightKind::new_use()), component_index, ) .expect("Unable to add root -> component edge"); graph .add_edge( - graph.root_index, + graph.root(), EdgeWeight::new(EdgeWeightKind::new_use()), schema_index, ) @@ -311,7 +312,7 @@ mod test { graph .add_edge( - graph.root_index, + graph.root(), EdgeWeight::new(EdgeWeightKind::new_use()), func_index, ) @@ -342,8 +343,8 @@ mod test { #[test] fn cyclic_failure() { - let mut graph = - WorkspaceSnapshotGraphV3::new().expect("Unable to create WorkspaceSnapshotGraph"); + let mut graph = WorkspaceSnapshotGraphVCurrent::new_for_unit_tests() + .expect("Unable to create WorkspaceSnapshotGraph"); let schema_id = graph.generate_ulid().expect("Unable to generate Ulid"); let initial_schema_node_index = graph @@ -378,14 +379,14 @@ mod test { graph .add_edge( - graph.root_index, + graph.root(), EdgeWeight::new(EdgeWeightKind::new_use()), initial_component_node_index, ) .expect("Unable to add root -> component edge"); graph .add_edge( - graph.root_index, + graph.root(), EdgeWeight::new(EdgeWeightKind::new_use()), initial_schema_node_index, ) @@ -411,7 +412,7 @@ mod test { ) .expect("Unable to add component -> schema variant edge"); - let pre_cycle_root_index = graph.root_index; + let pre_cycle_root_index = graph.root(); // This should cause a cycle. graph @@ -426,13 +427,13 @@ mod test { ) .expect_err("Created a cycle"); - assert_eq!(pre_cycle_root_index, graph.root_index,); + assert_eq!(pre_cycle_root_index, graph.root(),); } #[test] fn update_content() { - let mut graph = - WorkspaceSnapshotGraphV3::new().expect("Unable to create WorkspaceSnapshotGraph"); + let mut graph = WorkspaceSnapshotGraphVCurrent::new_for_unit_tests() + .expect("Unable to create WorkspaceSnapshotGraph"); let schema_id = graph.generate_ulid().expect("Unable to generate Ulid"); let schema_index = graph @@ -461,14 +462,14 @@ mod test { graph .add_edge( - graph.root_index, + graph.root(), EdgeWeight::new(EdgeWeightKind::new_use()), component_index, ) .expect("Unable to add root -> component edge"); graph .add_edge( - graph.root_index, + graph.root(), EdgeWeight::new(EdgeWeightKind::new_use()), schema_index, ) @@ -499,12 +500,12 @@ mod test { .cleanup_and_merkle_tree_hash() .expect("cleanup and merkle"); let pre_update_root_node_merkle_tree_hash: MerkleTreeHash = - MerkleTreeHash::from_str("49a6baef5d1c29f43653e0b7c02dfb73") + MerkleTreeHash::from_str("389d15c7a2f5db02d32232ab8304bfe7") .expect("able to create hash from hex string"); assert_eq!( pre_update_root_node_merkle_tree_hash, // expected graph - .get_node_weight(graph.root_index) + .get_node_weight(graph.root()) .expect("could not get node weight") .merkle_tree_hash(), // actual ); @@ -518,12 +519,12 @@ mod test { .expect("cleanup and merkle"); let post_update_root_node_merkle_tree_hash: MerkleTreeHash = - MerkleTreeHash::from_str("75febafba241026c63e27ab5b129cb26") + MerkleTreeHash::from_str("2525d60d0669121d11240f6aef299eb6") .expect("able to create hash from hex string"); assert_eq!( post_update_root_node_merkle_tree_hash, // expected graph - .get_node_weight(graph.root_index) + .get_node_weight(graph.root()) .expect("could not get node weight") .merkle_tree_hash(), // actual ); @@ -543,14 +544,15 @@ mod test { .cleanup_and_merkle_tree_hash() .expect("cleanup and merkle"); - // Ensure that there are not more nodes than the ones that should be in use. - assert_eq!(4, graph.node_count()); + // Ensure that there are not more nodes than the ones that should be in use, and the + // initial ones (categories and default views) + assert_eq!(15, graph.node_count()); // The hashes must not change upon cleanup. assert_eq!( post_update_root_node_merkle_tree_hash, // expected graph - .get_node_weight(graph.root_index) + .get_node_weight(graph.root()) .expect("could not get node weight") .merkle_tree_hash(), // actual ); @@ -569,8 +571,8 @@ mod test { #[test] fn add_ordered_node() { - let mut graph = - WorkspaceSnapshotGraphV3::new().expect("Unable to create WorkspaceSnapshotGraph"); + let mut graph = WorkspaceSnapshotGraphVCurrent::new_for_unit_tests() + .expect("Unable to create WorkspaceSnapshotGraph"); let schema_id = graph.generate_ulid().expect("Unable to generate Ulid"); let schema_index = graph @@ -595,7 +597,7 @@ mod test { graph .add_edge( - graph.root_index, + graph.root(), EdgeWeight::new(EdgeWeightKind::new_use()), schema_index, ) @@ -620,7 +622,7 @@ mod test { .expect("Unable to add func"); graph .add_edge( - graph.root_index, + graph.root(), EdgeWeight::new(EdgeWeightKind::new_use()), func_index, ) @@ -736,8 +738,8 @@ mod test { #[test] fn add_ordered_node_below_root() { - let mut graph = - WorkspaceSnapshotGraphV3::new().expect("Unable to create WorkspaceSnapshotGraph"); + let mut graph = WorkspaceSnapshotGraphVCurrent::new_for_unit_tests() + .expect("Unable to create WorkspaceSnapshotGraph"); let prop_id = graph.generate_ulid().expect("Unable to generate Ulid"); let prop_index = graph @@ -750,7 +752,7 @@ mod test { graph .add_edge( - graph.root_index, + graph.root(), EdgeWeight::new(EdgeWeightKind::new_use()), prop_index, ) @@ -774,8 +776,8 @@ mod test { #[test] fn reorder_ordered_node() { - let mut graph = - WorkspaceSnapshotGraphV3::new().expect("Unable to create WorkspaceSnapshotGraph"); + let mut graph = WorkspaceSnapshotGraphVCurrent::new_for_unit_tests() + .expect("Unable to create WorkspaceSnapshotGraph"); let schema_id = graph.generate_ulid().expect("Unable to generate Ulid"); let schema_index = graph @@ -800,7 +802,7 @@ mod test { graph .add_edge( - graph.root_index, + graph.root(), EdgeWeight::new(EdgeWeightKind::new_use()), schema_index, ) @@ -825,7 +827,7 @@ mod test { .expect("Unable to add func"); graph .add_edge( - graph.root_index, + graph.root(), EdgeWeight::new(EdgeWeightKind::new_use()), func_index, ) @@ -989,8 +991,8 @@ mod test { #[test] fn remove_unordered_node_and_detect_edge_removal() { - let mut graph = - WorkspaceSnapshotGraphV3::new().expect("Unable to create WorkspaceSnapshotGraph"); + let mut graph = WorkspaceSnapshotGraphVCurrent::new_for_unit_tests() + .expect("Unable to create WorkspaceSnapshotGraph"); let schema_id = graph.generate_ulid().expect("Unable to generate Ulid"); let schema_index = graph @@ -1015,7 +1017,7 @@ mod test { graph .add_edge( - graph.root_index, + graph.root(), EdgeWeight::new(EdgeWeightKind::new_use()), schema_index, ) @@ -1115,8 +1117,8 @@ mod test { #[test] fn remove_unordered_node() { - let mut graph = - WorkspaceSnapshotGraphV3::new().expect("Unable to create WorkspaceSnapshotGraph"); + let mut graph = WorkspaceSnapshotGraphVCurrent::new_for_unit_tests() + .expect("Unable to create WorkspaceSnapshotGraph"); let schema_id = graph.generate_ulid().expect("Unable to generate Ulid"); let schema_index = graph @@ -1141,7 +1143,7 @@ mod test { graph .add_edge( - graph.root_index, + graph.root(), EdgeWeight::new(EdgeWeightKind::new_use()), schema_index, ) @@ -1223,8 +1225,8 @@ mod test { #[test] fn remove_ordered_node() { - let mut graph = - WorkspaceSnapshotGraphV3::new().expect("Unable to create WorkspaceSnapshotGraph"); + let mut graph = WorkspaceSnapshotGraphVCurrent::new_for_unit_tests() + .expect("Unable to create WorkspaceSnapshotGraph"); let schema_id = graph.generate_ulid().expect("Unable to generate Ulid"); let schema_index = graph @@ -1249,7 +1251,7 @@ mod test { graph .add_edge( - graph.root_index, + graph.root(), EdgeWeight::new(EdgeWeightKind::new_use()), schema_index, ) @@ -1274,7 +1276,7 @@ mod test { .expect("Unable to add func"); graph .add_edge( - graph.root_index, + graph.root(), EdgeWeight::new(EdgeWeightKind::new_use()), func_index, ) diff --git a/lib/dal/src/workspace_snapshot/graph/v3/tests/detect_updates.rs b/lib/dal/src/workspace_snapshot/graph/tests/detect_updates.rs similarity index 95% rename from lib/dal/src/workspace_snapshot/graph/v3/tests/detect_updates.rs rename to lib/dal/src/workspace_snapshot/graph/tests/detect_updates.rs index fd980d1a17..af42c597c0 100644 --- a/lib/dal/src/workspace_snapshot/graph/v3/tests/detect_updates.rs +++ b/lib/dal/src/workspace_snapshot/graph/tests/detect_updates.rs @@ -14,13 +14,13 @@ mod test { node_weight::NodeWeight, NodeInformation, }, - NodeWeightDiscriminants, PropKind, WorkspaceSnapshotGraphV3, + NodeWeightDiscriminants, PropKind, WorkspaceSnapshotGraphVCurrent, }; #[test] fn detect_updates_simple_no_conflicts_with_purely_new_content_in_base() { - let mut base_graph = - WorkspaceSnapshotGraphV3::new().expect("Unable to create WorkspaceSnapshotGraph"); + let mut base_graph = WorkspaceSnapshotGraphVCurrent::new_for_unit_tests() + .expect("Unable to create WorkspaceSnapshotGraph"); let schema_id = base_graph.generate_ulid().expect("Unable to generate Ulid"); let schema_index = base_graph @@ -41,7 +41,7 @@ mod test { base_graph .add_edge( - base_graph.root_index, + base_graph.root(), EdgeWeight::new(EdgeWeightKind::new_use()), schema_index, ) @@ -73,7 +73,7 @@ mod test { .expect("Unable to add Component B"); let _new_onto_root_component_edge_index = base_graph .add_edge( - base_graph.root_index, + base_graph.root(), EdgeWeight::new(EdgeWeightKind::new_use()), new_onto_component_index, ) @@ -111,8 +111,8 @@ mod test { #[test] fn detect_updates_with_purely_new_content_in_new_graph() { - let mut base_graph = - WorkspaceSnapshotGraphV3::new().expect("Unable to create WorkspaceSnapshotGraph"); + let mut base_graph = WorkspaceSnapshotGraphVCurrent::new_for_unit_tests() + .expect("Unable to create WorkspaceSnapshotGraph"); let component_id = base_graph.generate_ulid().expect("Unable to generate Ulid"); let component_index = base_graph @@ -124,7 +124,7 @@ mod test { .expect("Unable to add Schema A"); base_graph .add_edge( - base_graph.root_index, + base_graph.root(), EdgeWeight::new(EdgeWeightKind::new_use()), component_index, ) @@ -147,7 +147,7 @@ mod test { .expect("Unable to add Component B"); new_graph .add_edge( - new_graph.root_index, + new_graph.root(), EdgeWeight::new(EdgeWeightKind::new_use()), new_component_index, ) @@ -170,8 +170,8 @@ mod test { #[test] fn detect_updates_ordered_container_insert_and_remove() { - let mut base_graph = - WorkspaceSnapshotGraphV3::new().expect("Unable to create WorkspaceSnapshotGraph"); + let mut base_graph = WorkspaceSnapshotGraphVCurrent::new_for_unit_tests() + .expect("Unable to create WorkspaceSnapshotGraph"); let active_graph = &mut base_graph; // Create base prop node @@ -189,7 +189,7 @@ mod test { active_graph .add_edge( - active_graph.root_index, + active_graph.root(), EdgeWeight::new(EdgeWeightKind::new_use()), prop_index, ) @@ -329,8 +329,8 @@ mod test { #[test] fn detect_updates_add_unordered_child_to_ordered_container() { - let mut base_graph = - WorkspaceSnapshotGraphV3::new().expect("Unable to create WorkspaceSnapshotGraph"); + let mut base_graph = WorkspaceSnapshotGraphVCurrent::new_for_unit_tests() + .expect("Unable to create WorkspaceSnapshotGraph"); let active_graph = &mut base_graph; // Create base prop node @@ -348,7 +348,7 @@ mod test { active_graph .add_edge( - active_graph.root_index, + active_graph.root(), EdgeWeight::new(EdgeWeightKind::new_use()), prop_index, ) @@ -406,7 +406,7 @@ mod test { active_graph .add_edge( - active_graph.root_index, + active_graph.root(), EdgeWeight::new(EdgeWeightKind::new_use()), node_index, ) @@ -472,8 +472,8 @@ mod test { let nodes = ["a", "b", "c"]; let edges = [(None, "a"), (None, "b"), (Some("a"), "c"), (Some("c"), "b")]; - let mut base_graph = - WorkspaceSnapshotGraphV3::new().expect("Unable to create WorkspaceSnapshotGraph"); + let mut base_graph = WorkspaceSnapshotGraphVCurrent::new_for_unit_tests() + .expect("Unable to create WorkspaceSnapshotGraph"); // Add all nodes from the slice and store their references in a hash map. let mut node_id_map = HashMap::new(); @@ -498,7 +498,7 @@ mod test { // Add all edges from the slice. for (source, target) in edges { let source = match source { - None => base_graph.root_index, + None => base_graph.root(), Some(node) => base_graph .get_node_index_by_id( node_id_map @@ -531,7 +531,7 @@ mod test { // Ensure the graph construction worked. for (source, target) in edges { let source_idx = match source { - None => base_graph.root_index, + None => base_graph.root(), Some(node) => base_graph .get_node_index_by_id( node_id_map @@ -633,8 +633,8 @@ mod test { #[test] fn detect_updates_remove_edge_simple() { - let mut to_rebase_graph = - WorkspaceSnapshotGraphV3::new().expect("unable to make to_rebase_graph"); + let mut to_rebase_graph = WorkspaceSnapshotGraphVCurrent::new_for_unit_tests() + .expect("unable to make to_rebase_graph"); let prototype_node_id = to_rebase_graph.generate_ulid().expect("gen ulid"); let prototype_node = NodeWeight::new_content( diff --git a/lib/dal/src/workspace_snapshot/graph/v3/tests/exclusive_outgoing_edges.rs b/lib/dal/src/workspace_snapshot/graph/tests/exclusive_outgoing_edges.rs similarity index 98% rename from lib/dal/src/workspace_snapshot/graph/v3/tests/exclusive_outgoing_edges.rs rename to lib/dal/src/workspace_snapshot/graph/tests/exclusive_outgoing_edges.rs index 6111f3089b..d8555e193f 100644 --- a/lib/dal/src/workspace_snapshot/graph/v3/tests/exclusive_outgoing_edges.rs +++ b/lib/dal/src/workspace_snapshot/graph/tests/exclusive_outgoing_edges.rs @@ -15,12 +15,12 @@ mod test { }, }, EdgeWeight, EdgeWeightKind, EdgeWeightKindDiscriminants, NodeWeightDiscriminants, - WorkspaceSnapshotGraphV3, + WorkspaceSnapshotGraphVCurrent, }; #[test] fn correct_exclusive_outgoing_edges() -> WorkspaceSnapshotGraphResult<()> { - let mut graph = WorkspaceSnapshotGraphV3::new()?; + let mut graph = WorkspaceSnapshotGraphVCurrent::new_for_unit_tests()?; let schema_variant_1_id = graph.generate_ulid()?; let schema_variant_2_id = graph.generate_ulid()?; @@ -59,7 +59,7 @@ mod test { let component_idx = graph.add_or_replace_node(component.clone())?; graph.add_edge( - graph.root_index, + graph.root(), EdgeWeight::new(EdgeWeightKind::new_use()), component_idx, )?; @@ -172,7 +172,7 @@ mod test { #[test] fn correct_exclusive_outgoing_action_edges() -> WorkspaceSnapshotGraphResult<()> { - let mut graph = WorkspaceSnapshotGraphV3::new()?; + let mut graph = WorkspaceSnapshotGraphVCurrent::new_for_unit_tests()?; let action_id = graph.generate_ulid()?; let prototype_1_id = graph.generate_ulid()?; diff --git a/lib/dal/src/workspace_snapshot/graph/v3/tests/rebase.rs b/lib/dal/src/workspace_snapshot/graph/tests/rebase.rs similarity index 94% rename from lib/dal/src/workspace_snapshot/graph/v3/tests/rebase.rs rename to lib/dal/src/workspace_snapshot/graph/tests/rebase.rs index 220a45c346..5829292868 100644 --- a/lib/dal/src/workspace_snapshot/graph/v3/tests/rebase.rs +++ b/lib/dal/src/workspace_snapshot/graph/tests/rebase.rs @@ -11,12 +11,12 @@ mod test { use crate::workspace_snapshot::node_weight::category_node_weight::CategoryNodeKind; use crate::workspace_snapshot::node_weight::NodeWeight; use crate::workspace_snapshot::node_weight::{ContentNodeWeight, FuncNodeWeight}; - use crate::WorkspaceSnapshotGraphV3; + use crate::WorkspaceSnapshotGraphVCurrent; #[test] fn simulate_rebase() { - let mut to_rebase = - WorkspaceSnapshotGraphV3::new().expect("Unable to create WorkspaceSnapshotGraph"); + let mut to_rebase = WorkspaceSnapshotGraphVCurrent::new_for_unit_tests() + .expect("Unable to create WorkspaceSnapshotGraph"); // Set up the to rebase graph. let schema_category_node_index = to_rebase @@ -24,7 +24,7 @@ mod test { .expect("could not add category node"); to_rebase .add_edge( - to_rebase.root_index, + to_rebase.root(), EdgeWeight::new(EdgeWeightKind::new_use()), schema_category_node_index, ) @@ -34,7 +34,7 @@ mod test { .expect("could not add category node"); to_rebase .add_edge( - to_rebase.root_index, + to_rebase.root(), EdgeWeight::new(EdgeWeightKind::new_use()), func_category_node_index, ) @@ -114,7 +114,7 @@ mod test { .cleanup_and_merkle_tree_hash() .expect("merkle it!"); assert_eq!( - 6, // expected + 17, // expected onto.node_count() // actual ); diff --git a/lib/dal/src/workspace_snapshot/graph/v2.rs b/lib/dal/src/workspace_snapshot/graph/v2.rs index 630390efc9..e1469bcc45 100644 --- a/lib/dal/src/workspace_snapshot/graph/v2.rs +++ b/lib/dal/src/workspace_snapshot/graph/v2.rs @@ -1221,6 +1221,7 @@ impl WorkspaceSnapshotGraphV2 { | EdgeWeightKind::ActionPrototype | EdgeWeightKind::Contain(None) | EdgeWeightKind::FrameContains + | EdgeWeightKind::Represents | EdgeWeightKind::PrototypeArgument | EdgeWeightKind::PrototypeArgumentValue | EdgeWeightKind::Socket diff --git a/lib/dal/src/workspace_snapshot/graph/v3.rs b/lib/dal/src/workspace_snapshot/graph/v3.rs index 2b47234e4a..13a3e7ad4e 100644 --- a/lib/dal/src/workspace_snapshot/graph/v3.rs +++ b/lib/dal/src/workspace_snapshot/graph/v3.rs @@ -21,8 +21,8 @@ use crate::{ workspace_snapshot::{ content_address::ContentAddress, graph::{ - detect_updates::{Detector, Update}, - MerkleTreeHash, WorkspaceSnapshotGraphError, WorkspaceSnapshotGraphResult, + detect_updates::Update, MerkleTreeHash, WorkspaceSnapshotGraphError, + WorkspaceSnapshotGraphResult, }, node_weight::{CategoryNodeWeight, NodeWeight}, CategoryNodeKind, ContentAddressDiscriminants, LineageId, OrderingNodeWeight, @@ -32,7 +32,6 @@ use crate::{ pub mod component; pub mod schema; -mod tests; #[derive(Default, Deserialize, Serialize, Clone)] pub struct WorkspaceSnapshotGraphV3 { @@ -119,6 +118,14 @@ impl WorkspaceSnapshotGraphV3 { &self.graph } + pub(crate) fn node_index_by_id(&self) -> &HashMap { + &self.node_index_by_id + } + + pub(crate) fn node_indices_by_lineage_id(&self) -> &HashMap> { + &self.node_indices_by_lineage_id + } + pub fn generate_ulid(&self) -> WorkspaceSnapshotGraphResult { Ok(self .ulid_generator @@ -561,10 +568,6 @@ impl WorkspaceSnapshotGraphV3 { Ok(maybe_equivalent_node) } - pub fn detect_updates(&self, updated_graph: &Self) -> Vec { - Detector::new(self, updated_graph).detect_updates() - } - #[allow(dead_code)] pub fn dot(&self) { // NOTE(nick): copy the output and execute this on macOS. It will create a file in the @@ -735,6 +738,7 @@ impl WorkspaceSnapshotGraphV3 { EdgeWeightKindDiscriminants::AuthenticationPrototype => "black", EdgeWeightKindDiscriminants::Contain => "blue", EdgeWeightKindDiscriminants::FrameContains => "black", + EdgeWeightKindDiscriminants::Represents => "black", EdgeWeightKindDiscriminants::Ordering => "gray", EdgeWeightKindDiscriminants::Ordinal => "gray", EdgeWeightKindDiscriminants::Prop => "orange", @@ -781,6 +785,7 @@ impl WorkspaceSnapshotGraphV3 { ContentAddressDiscriminants::OutputSocket => "red", ContentAddressDiscriminants::Func => "black", ContentAddressDiscriminants::FuncArg => "black", + ContentAddressDiscriminants::Geometry => "black", ContentAddressDiscriminants::InputSocket => "red", ContentAddressDiscriminants::JsonValue => "fuchsia", ContentAddressDiscriminants::Module => "yellow", @@ -793,6 +798,7 @@ impl WorkspaceSnapshotGraphV3 { ContentAddressDiscriminants::ValidationPrototype => "black", ContentAddressDiscriminants::ValidationOutput => "darkcyan", ContentAddressDiscriminants::ManagementPrototype => "black", + ContentAddressDiscriminants::View => "black", }; (discrim.to_string(), color) } @@ -825,6 +831,10 @@ impl WorkspaceSnapshotGraphV3 { CategoryNodeKind::DependentValueRoots => { ("Dependent Values (Category)".into(), "black") } + CategoryNodeKind::View => ("Views (Category)".into(), "black"), + CategoryNodeKind::DiagramObject => { + ("Diagram Objects (Category)".into(), "black") + } }, NodeWeight::Component(component) => ( "Component".to_string(), @@ -841,6 +851,7 @@ impl WorkspaceSnapshotGraphV3 { format!("Func Arg\n{}", func_arg_node_weight.name()), "black", ), + NodeWeight::Geometry(_) => ("Geometry\n".to_string(), "green"), NodeWeight::InputSocket(_) => ("Input Socket".to_string(), "black"), NodeWeight::Ordering(_) => { (NodeWeightDiscriminants::Ordering.to_string(), "gray") @@ -861,6 +872,7 @@ impl WorkspaceSnapshotGraphV3 { format!("FinishedDependentValue\n{}", node_weight.value_id()), "red", ), + NodeWeight::View(_) => ("View\n".to_string(), "black"), NodeWeight::ManagementPrototype(_) => { ("ManagementPrototype".to_string(), "black") } @@ -1396,6 +1408,7 @@ impl WorkspaceSnapshotGraphV3 { | EdgeWeightKind::Prop | EdgeWeightKind::Prototype(None) | EdgeWeightKind::Proxy + | EdgeWeightKind::Represents | EdgeWeightKind::Root | EdgeWeightKind::SocketValue | EdgeWeightKind::ValidationOutput @@ -1514,6 +1527,7 @@ impl WorkspaceSnapshotGraphV3 { /// we treat node weights as immutable and replace them by creating a new /// node with a new node weight and replacing references to point to the new /// node. + #[allow(dead_code)] pub(crate) fn update_node_weight( &mut self, node_idx: NodeIndex, diff --git a/lib/dal/src/workspace_snapshot/graph/v3/component.rs b/lib/dal/src/workspace_snapshot/graph/v3/component.rs index c531725ae5..838bea0571 100644 --- a/lib/dal/src/workspace_snapshot/graph/v3/component.rs +++ b/lib/dal/src/workspace_snapshot/graph/v3/component.rs @@ -1,11 +1,12 @@ use petgraph::prelude::*; +use crate::workspace_snapshot::graph::WorkspaceSnapshotGraphV3; use crate::{ component::ComponentResult, workspace_snapshot::{ edge_weight::EdgeWeightKindDiscriminants, node_weight::NodeWeightDiscriminants, }, - ComponentError, ComponentId, SchemaVariantId, WorkspaceSnapshotError, WorkspaceSnapshotGraphV3, + ComponentError, ComponentId, SchemaVariantId, WorkspaceSnapshotError, }; impl WorkspaceSnapshotGraphV3 { diff --git a/lib/dal/src/workspace_snapshot/graph/v3/schema.rs b/lib/dal/src/workspace_snapshot/graph/v3/schema.rs index e7a8c31d5a..fca83c65d4 100644 --- a/lib/dal/src/workspace_snapshot/graph/v3/schema.rs +++ b/lib/dal/src/workspace_snapshot/graph/v3/schema.rs @@ -1,8 +1,9 @@ use petgraph::prelude::*; +use crate::workspace_snapshot::graph::WorkspaceSnapshotGraphV3; use crate::{ workspace_snapshot::graph::{WorkspaceSnapshotGraphError, WorkspaceSnapshotGraphResult}, - EdgeWeightKindDiscriminants, SchemaId, SchemaVariantId, WorkspaceSnapshotGraphV3, + EdgeWeightKindDiscriminants, SchemaId, SchemaVariantId, }; pub mod variant; diff --git a/lib/dal/src/workspace_snapshot/graph/v3/schema/variant.rs b/lib/dal/src/workspace_snapshot/graph/v3/schema/variant.rs index 93e6537774..fd8c72bbb2 100644 --- a/lib/dal/src/workspace_snapshot/graph/v3/schema/variant.rs +++ b/lib/dal/src/workspace_snapshot/graph/v3/schema/variant.rs @@ -1,11 +1,12 @@ use petgraph::prelude::*; +use crate::workspace_snapshot::graph::WorkspaceSnapshotGraphV3; use crate::{ workspace_snapshot::{ content_address::ContentAddressDiscriminants, edge_weight::EdgeWeightKindDiscriminants, graph::WorkspaceSnapshotGraphResult, node_weight::NodeWeight, }, - SchemaId, SchemaVariantError, SchemaVariantId, WorkspaceSnapshotGraphV3, + SchemaId, SchemaVariantError, SchemaVariantId, }; impl WorkspaceSnapshotGraphV3 { diff --git a/lib/dal/src/workspace_snapshot/graph/v4.rs b/lib/dal/src/workspace_snapshot/graph/v4.rs new file mode 100644 index 0000000000..96430902f8 --- /dev/null +++ b/lib/dal/src/workspace_snapshot/graph/v4.rs @@ -0,0 +1,1710 @@ +pub mod component; +pub mod schema; + +use std::{ + collections::{hash_map::Entry, HashMap, HashSet, VecDeque}, + fs::File, + io::Write, + sync::{Arc, Mutex}, +}; +use strum::IntoEnumIterator; + +use petgraph::{ + algo, + prelude::*, + stable_graph::{Edges, Neighbors}, + visit::DfsEvent, +}; +use serde::{Deserialize, Serialize}; +use si_events::{ulid::Ulid, ContentHash}; +use si_layer_cache::db::serialize; +use telemetry::prelude::*; +use ulid::Generator; + +use crate::layer_db_types::{ViewContent, ViewContentV1}; +use crate::{ + workspace_snapshot::{ + content_address::ContentAddress, + graph::{ + detect_updates::{Detector, Update}, + MerkleTreeHash, WorkspaceSnapshotGraphError, WorkspaceSnapshotGraphResult, + }, + node_weight::{CategoryNodeWeight, NodeWeight}, + CategoryNodeKind, ContentAddressDiscriminants, LineageId, OrderingNodeWeight, + }, + DalContext, EdgeWeight, EdgeWeightKind, EdgeWeightKindDiscriminants, NodeWeightDiscriminants, + Timestamp, +}; + +#[derive(Default, Deserialize, Serialize, Clone)] +pub struct WorkspaceSnapshotGraphV4 { + graph: StableDiGraph, + node_index_by_id: HashMap, + node_indices_by_lineage_id: HashMap>, + root_index: NodeIndex, + + #[serde(skip)] + ulid_generator: Arc>, + #[serde(skip)] + touched_node_indices: HashSet, +} + +impl std::fmt::Debug for WorkspaceSnapshotGraphV4 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WorkspaceSnapshotGraph") + .field("root_index", &self.root_index) + .field("node_index_by_id", &self.node_index_by_id) + .field("graph", &self.graph) + .finish() + } +} + +impl WorkspaceSnapshotGraphV4 { + pub async fn new(ctx: &DalContext) -> WorkspaceSnapshotGraphResult { + let mut result = Self::new_with_categories_only()?; + + let (_, view_category_idx) = result + .get_category_node(None, CategoryNodeKind::View)? + .ok_or(WorkspaceSnapshotGraphError::CategoryNodeNotFound( + CategoryNodeKind::View, + ))?; + + // Create default view + { + let id = result.generate_ulid()?; + let lineage_id = result.generate_ulid()?; + + let content = ViewContent::V1(ViewContentV1 { + timestamp: Timestamp::now(), + name: "DEFAULT".to_owned(), + }); + + let (content_address, _) = ctx + .layer_db() + .cas() + .write( + Arc::new(content.clone().into()), + None, + ctx.events_tenancy(), + ctx.events_actor(), + ) + .await?; + + let node_weight = NodeWeight::new_view(id, lineage_id, content_address); + let default_view_node_idx = result.add_or_replace_node(node_weight.clone())?; + + result.add_edge( + view_category_idx, + EdgeWeight::new(EdgeWeightKind::new_use_default()), + default_view_node_idx, + )?; + + default_view_node_idx + }; + + Ok(result) + } + + // Creates a graph with default view node with faked content, so we can unit test the graph without + // passing in a context with access to the content store + #[allow(unused)] + pub(crate) fn new_for_unit_tests() -> WorkspaceSnapshotGraphResult { + let mut result = Self::new_with_categories_only()?; + + let (_, view_category_idx) = result + .get_category_node(None, CategoryNodeKind::View)? + .ok_or(WorkspaceSnapshotGraphError::CategoryNodeNotFound( + CategoryNodeKind::View, + ))?; + + // Create default view + { + let id = result.generate_ulid()?; + let lineage_id = result.generate_ulid()?; + + let content_address = ContentHash::from("PLACEHOLDER"); + + let node_weight = NodeWeight::new_view(id, lineage_id, content_address); + let default_view_node_idx = result.add_or_replace_node(node_weight.clone())?; + + result.add_edge( + view_category_idx, + EdgeWeight::new(EdgeWeightKind::new_use_default()), + default_view_node_idx, + )?; + + default_view_node_idx + }; + + Ok(result) + } + + pub fn new_with_categories_only() -> WorkspaceSnapshotGraphResult { + let mut graph: StableDiGraph = + StableDiGraph::with_capacity(1024, 1024); + let mut generator = Generator::new(); + + let root_node = NodeWeight::new_content( + generator.generate()?.into(), + generator.generate()?.into(), + ContentAddress::Root, + ); + + let node_id = root_node.id(); + let lineage_id = root_node.lineage_id(); + let root_index = graph.add_node(root_node); + + let mut result = Self { + root_index, + graph, + ulid_generator: Arc::new(Mutex::new(generator)), + ..Default::default() + }; + + result.add_node_finalize(node_id, lineage_id, root_index)?; + + // Create the category nodes under root. + for category_node_kind in CategoryNodeKind::iter() { + let id = result.generate_ulid()?; + let lineage_id = result.generate_ulid()?; + let category_node_index = + result.add_category_node(id, lineage_id, category_node_kind)?; + result.add_edge( + result.root(), + EdgeWeight::new(EdgeWeightKind::new_use()), + category_node_index, + )?; + } + + Ok(result) + } + + /// Add a node to the list of touched nodes, so that it is taken into + /// account when recalculating the merkle tree hash for this graph. If a + /// node weight is modified, or if a an outgoing edge is added or removed + /// to/from this node, you must touch the node, or the merkel tree hash will + /// not be updated correctly. + pub fn touch_node(&mut self, node_index: NodeIndex) { + self.touched_node_indices.insert(node_index); + } + + pub fn new_from_parts( + graph: StableDiGraph, + node_index_by_id: HashMap, + node_indices_by_lineage_id: HashMap>, + root_index: NodeIndex, + ) -> Self { + Self { + graph, + node_index_by_id, + node_indices_by_lineage_id, + root_index, + ulid_generator: Arc::new(Mutex::new(Generator::new())), + touched_node_indices: HashSet::new(), + } + } + + pub fn root(&self) -> NodeIndex { + self.root_index + } + + /// Access the internal petgraph for this snapshot + pub fn graph(&self) -> &StableGraph { + &self.graph + } + + pub fn generate_ulid(&self) -> WorkspaceSnapshotGraphResult { + Ok(self + .ulid_generator + .lock() + .map_err(|e| WorkspaceSnapshotGraphError::MutexPoison(e.to_string()))? + .generate()? + .into()) + } + + pub fn update_node_id( + &mut self, + current_idx: NodeIndex, + new_id: impl Into, + new_lineage_id: LineageId, + ) -> WorkspaceSnapshotGraphResult<()> { + let new_id = new_id.into(); + + self.graph + .node_weight_mut(current_idx) + .ok_or(WorkspaceSnapshotGraphError::NodeWeightNotFound)? + .set_id_and_lineage(new_id, new_lineage_id); + + self.add_node_finalize(new_id, new_lineage_id, current_idx)?; + + Ok(()) + } + + pub fn get_latest_node_idx_opt( + &self, + node_idx: NodeIndex, + ) -> WorkspaceSnapshotGraphResult> { + if !self.graph.contains_node(node_idx) { + return Ok(None); + } + + Ok(Some(self.get_latest_node_idx(node_idx)?)) + } + + #[inline(always)] + pub fn get_latest_node_idx( + &self, + node_idx: NodeIndex, + ) -> WorkspaceSnapshotGraphResult { + let node_id = self.get_node_weight(node_idx)?.id(); + self.get_node_index_by_id(node_id) + } + + fn add_edge_inner( + &mut self, + from_node_index: NodeIndex, + edge_weight: EdgeWeight, + to_node_index: NodeIndex, + cycle_check: bool, + ) -> WorkspaceSnapshotGraphResult { + if cycle_check { + self.add_temp_edge_cycle_check(from_node_index, edge_weight.clone(), to_node_index)?; + } + + self.touch_node(from_node_index); + + // Add the new edge to the new version of the "from" node. + let edge_index = self + .graph + .update_edge(from_node_index, to_node_index, edge_weight); + + Ok(edge_index) + } + + fn add_temp_edge_cycle_check( + &mut self, + from_node_index: NodeIndex, + edge_weight: EdgeWeight, + to_node_index: NodeIndex, + ) -> WorkspaceSnapshotGraphResult<()> { + let temp_edge = self + .graph + .update_edge(from_node_index, to_node_index, edge_weight.clone()); + + let would_create_a_cycle = !self.is_acyclic_directed(); + self.graph.remove_edge(temp_edge); + + if would_create_a_cycle { + // if you want to find out how the two nodes are already connected, + // this will give you that info.. + + // let paths: Vec> = petgraph::algo::all_simple_paths( + // &self.graph, + // to_node_index, + // from_node_index, + // 0, + // None, + // ) + // .collect(); + + // for path in paths { + // for node_index in path { + // let node_weight = self.get_node_weight(node_index).expect("should exist"); + // dbg!(node_weight); + // } + // } + + Err(WorkspaceSnapshotGraphError::CreateGraphCycle) + } else { + Ok(()) + } + } + + pub fn add_edge_with_cycle_check( + &mut self, + from_node_index: NodeIndex, + edge_weight: EdgeWeight, + to_node_index: NodeIndex, + ) -> WorkspaceSnapshotGraphResult { + self.add_edge_inner(from_node_index, edge_weight, to_node_index, true) + } + + pub fn add_edge( + &mut self, + from_node_index: NodeIndex, + edge_weight: EdgeWeight, + to_node_index: NodeIndex, + ) -> WorkspaceSnapshotGraphResult { + // Temporarily add the edge to the existing tree to see if it would create a cycle. + // Configured to run only in tests because it has a major perf impact otherwise + #[cfg(test)] + { + self.add_temp_edge_cycle_check(from_node_index, edge_weight.clone(), to_node_index)?; + } + + self.add_edge_inner(from_node_index, edge_weight, to_node_index, false) + } + + pub fn remove_node_id(&mut self, id: impl Into) { + self.node_index_by_id.remove(&id.into()); + } + + fn add_node_finalize( + &mut self, + node_id: Ulid, + lineage_id: Ulid, + node_idx: NodeIndex, + ) -> WorkspaceSnapshotGraphResult<()> { + // Update the accessor maps using the new index. + self.node_index_by_id.insert(node_id, node_idx); + self.node_indices_by_lineage_id + .entry(lineage_id) + .and_modify(|set| { + set.insert(node_idx); + }) + .or_insert_with(|| HashSet::from([node_idx])); + self.update_merkle_tree_hash(node_idx)?; + self.touch_node(node_idx); + + Ok(()) + } + + /// Adds this node to the graph, or replaces it if a node with the same id + /// already exists. Then, adds it to the list of touched nodes so that the + /// merkle tree hash for it, and any parents, is recalculated. + pub fn add_or_replace_node( + &mut self, + node: NodeWeight, + ) -> WorkspaceSnapshotGraphResult { + let node_id = node.id(); + let lineage_id = node.lineage_id(); + let node_idx = self + .get_node_index_by_id_opt(node_id) + .and_then(|current_index| { + self.graph.node_weight_mut(current_index).map(|weight_mut| { + node.clone_into(weight_mut); + current_index + }) + }); + + let node_idx = match node_idx { + Some(swapped_node_idx) => swapped_node_idx, + None => self.graph.add_node(node), + }; + + self.add_node_finalize(node_id, lineage_id, node_idx)?; + + Ok(node_idx) + } + + pub fn add_category_node( + &mut self, + id: Ulid, + lineage_id: Ulid, + kind: CategoryNodeKind, + ) -> WorkspaceSnapshotGraphResult { + let inner_weight = CategoryNodeWeight::new(id, lineage_id, kind); + let new_node_index = self.add_or_replace_node(NodeWeight::Category(inner_weight))?; + Ok(new_node_index) + } + + pub fn get_category_node( + &self, + source: Option, + kind: CategoryNodeKind, + ) -> WorkspaceSnapshotGraphResult> { + let source_index = match source { + Some(provided_source) => self.get_node_index_by_id(provided_source)?, + None => self.root_index, + }; + + // TODO(nick): ensure that two target category nodes of the same kind don't exist for the + // same source node. + for edgeref in self.graph.edges_directed(source_index, Outgoing) { + let maybe_category_node_index = edgeref.target(); + let maybe_category_node_weight = self.get_node_weight(maybe_category_node_index)?; + + if let NodeWeight::Category(category_node_weight) = maybe_category_node_weight { + if category_node_weight.kind() == kind { + return Ok(Some((category_node_weight.id(), maybe_category_node_index))); + } + } + } + + Ok(None) + } + + pub fn edges_directed( + &self, + node_index: NodeIndex, + direction: Direction, + ) -> Edges<'_, EdgeWeight, Directed, u32> { + self.graph.edges_directed(node_index, direction) + } + + pub fn neighbors_directed( + &self, + node_index: NodeIndex, + direction: Direction, + ) -> Neighbors<'_, EdgeWeight> { + self.graph.neighbors_directed(node_index, direction) + } + + pub fn find_edge(&self, from_idx: NodeIndex, to_idx: NodeIndex) -> Option<&EdgeWeight> { + self.graph + .find_edge(from_idx, to_idx) + .and_then(|edge_idx| self.graph.edge_weight(edge_idx)) + } + + pub fn edges_directed_for_edge_weight_kind( + &self, + node_index: NodeIndex, + direction: Direction, + edge_kind: EdgeWeightKindDiscriminants, + ) -> Vec<(EdgeWeight, NodeIndex, NodeIndex)> { + self.graph + .edges_directed(node_index, direction) + .filter_map(|edge_ref| { + if edge_kind == edge_ref.weight().kind().into() { + Some(( + edge_ref.weight().to_owned(), + edge_ref.source(), + edge_ref.target(), + )) + } else { + None + } + }) + .collect() + } + + pub fn nodes(&self) -> impl Iterator { + self.graph.node_indices().filter_map(|node_idx| { + self.get_node_weight_opt(node_idx) + .map(|weight| (weight, node_idx)) + }) + } + + pub fn edges(&self) -> impl Iterator { + self.graph.edge_indices().filter_map(|edge_idx| { + self.get_edge_weight_opt(edge_idx) + .ok() + .flatten() + .and_then(|weight| { + self.graph + .edge_endpoints(edge_idx) + .map(|(source, target)| (weight, source, target)) + }) + }) + } + + pub fn add_ordered_edge( + &mut self, + from_node_index: NodeIndex, + edge_weight: EdgeWeight, + to_node_index: NodeIndex, + ) -> WorkspaceSnapshotGraphResult<(EdgeIndex, Option)> { + let new_edge_index = self.add_edge(from_node_index, edge_weight, to_node_index)?; + + // Find the ordering node of the "container" if there is one, and add the thing pointed to + // by the `to_node_index` to the ordering. Also point the ordering node at the thing with + // an `Ordinal` edge, so that Ordering nodes must be touched *after* the things they order + // in a depth first search + let maybe_ordinal_edge_index = if let Some(container_ordering_node_index) = + self.ordering_node_index_for_container(from_node_index)? + { + let ordinal_edge_index = self.add_edge( + container_ordering_node_index, + EdgeWeight::new(EdgeWeightKind::Ordinal), + to_node_index, + )?; + + let element_id = self + .node_index_to_id(to_node_index) + .ok_or(WorkspaceSnapshotGraphError::NodeWeightNotFound)?; + + if let NodeWeight::Ordering(ordering_node_weight) = + self.get_node_weight_mut(container_ordering_node_index)? + { + ordering_node_weight.push_to_order(element_id); + self.touch_node(container_ordering_node_index); + } + + Some(ordinal_edge_index) + } else { + None + }; + + Ok((new_edge_index, maybe_ordinal_edge_index)) + } + + pub fn add_ordered_node( + &mut self, + node: NodeWeight, + ) -> WorkspaceSnapshotGraphResult { + let new_node_index = self.add_or_replace_node(node)?; + + let ordering_node_id = self.generate_ulid()?; + let ordering_node_lineage_id = self.generate_ulid()?; + let ordering_node_index = self.add_or_replace_node(NodeWeight::Ordering( + OrderingNodeWeight::new(ordering_node_id, ordering_node_lineage_id), + ))?; + + let edge_index = self.add_edge( + new_node_index, + EdgeWeight::new(EdgeWeightKind::Ordering), + ordering_node_index, + )?; + let (source, _) = self.edge_endpoints(edge_index)?; + Ok(source) + } + + /// Remove any orphaned nodes from the graph, then recalculate the merkle + /// tree hash based on the nodes touched. *ALWAYS* call this before + /// persisting a snapshot + pub fn cleanup_and_merkle_tree_hash(&mut self) -> WorkspaceSnapshotGraphResult<()> { + self.cleanup(); + self.recalculate_entire_merkle_tree_hash_based_on_touched_nodes()?; + + Ok(()) + } + + /// Remove any orphaned nodes from the graph. If you are about to persist + /// the graph, or calculate updates based on this graph and another one, then + /// you want to call `Self::cleanup_and_merkle_tree_hash` instead. + pub fn cleanup(&mut self) { + let start = tokio::time::Instant::now(); + + // We want to remove all of the "garbage" we've accumulated while operating on the graph. + // Anything that is no longer reachable from the current `self.root_index` should be + // removed as it is no longer referenced by anything in the current version of the graph. + // Fortunately, we don't need to walk the graph to find out if something is reachable from + // the root, since `has_path_connecting` is slow (depth-first search). Any node that does + // *NOT* have any incoming edges (aside from the `self.root_index` node) is not reachable, + // by definition. Finding the list of nodes with no incoming edges is very fast. If we + // remove all nodes (that are not the `self.root_index` node) that do not have any + // incoming edges, and we keep doing this until the only one left is the `self.root_index` + // node, then all remaining nodes are reachable from `self.root_index`. + let mut old_root_ids: HashSet; + loop { + old_root_ids = self + .graph + .externals(Incoming) + .filter(|node_id| *node_id != self.root_index) + .collect(); + if old_root_ids.is_empty() { + break; + } + + for stale_node_index in &old_root_ids { + self.graph.remove_node(*stale_node_index); + } + } + debug!("Removed stale NodeIndex: {:?}", start.elapsed()); + + // After we retain the nodes, collect the remaining ids and indices. + let remaining_node_ids: HashSet = self.graph.node_weights().map(|n| n.id()).collect(); + debug!( + "Got remaining node IDs: {} ({:?})", + remaining_node_ids.len(), + start.elapsed() + ); + let remaining_node_indices: HashSet = self.graph.node_indices().collect(); + debug!( + "Got remaining NodeIndex: {} ({:?})", + remaining_node_indices.len(), + start.elapsed() + ); + + // Cleanup the node index by id map. + self.node_index_by_id + .retain(|id, _index| remaining_node_ids.contains(id)); + debug!("Removed stale node_index_by_id: {:?}", start.elapsed()); + + // Cleanup the node indices by lineage id map. + self.node_indices_by_lineage_id + .iter_mut() + .for_each(|(_lineage_id, node_indices)| { + node_indices.retain(|node_index| remaining_node_indices.contains(node_index)); + }); + self.node_indices_by_lineage_id + .retain(|_lineage_id, node_indices| !node_indices.is_empty()); + debug!( + "Removed stale node_indices_by_lineage_id: {:?}", + start.elapsed() + ); + } + + pub fn find_equivalent_node( + &self, + id: Ulid, + lineage_id: Ulid, + ) -> WorkspaceSnapshotGraphResult> { + let maybe_equivalent_node = match self.get_node_index_by_id(id) { + Ok(node_index) => { + let node_indices = self.get_node_index_by_lineage(lineage_id); + if node_indices.contains(&node_index) { + Some(node_index) + } else { + None + } + } + Err(WorkspaceSnapshotGraphError::NodeWithIdNotFound(_)) => None, + Err(e) => return Err(e), + }; + Ok(maybe_equivalent_node) + } + + pub fn detect_updates(&self, updated_graph: &Self) -> Vec { + Detector::new(self, updated_graph).detect_updates() + } + + #[allow(dead_code)] + pub fn dot(&self) { + // NOTE(nick): copy the output and execute this on macOS. It will create a file in the + // process and open a new tab in your browser. + // ``` + // pbpaste | dot -Tsvg -o foo.svg && open foo.svg + // ``` + let current_root_weight = self + .get_node_weight(self.root_index) + .expect("this should be impossible and this code should only be used for debugging"); + println!( + "Root Node Weight: {current_root_weight:?}\n{:?}", + petgraph::dot::Dot::with_config(&self.graph, &[petgraph::dot::Config::EdgeNoLabel]) + ); + } + + /// Produces a subgraph of self that includes only the parent and child trees + /// of `subgraph_root`. Useful for producing a manageable slice of the graph + /// for debugging. + pub fn subgraph(&self, subgraph_root: NodeIndex) -> Option { + let mut subgraph: StableDiGraph = StableDiGraph::new(); + let mut index_map = HashMap::new(); + let mut node_index_by_id = HashMap::new(); + let mut node_indices_by_lineage_id = HashMap::new(); + let mut new_root = None; + + let mut add_node_to_idx = |node_id: Ulid, lineage_id: Ulid, node_idx: NodeIndex| { + node_index_by_id.insert(node_id, node_idx); + node_indices_by_lineage_id + .entry(lineage_id) + .and_modify(|set: &mut HashSet| { + set.insert(node_idx); + }) + .or_insert_with(|| HashSet::from([node_idx])); + }; + + let mut parent_q = VecDeque::from([subgraph_root]); + let node_weight = self.graph.node_weight(subgraph_root)?.to_owned(); + let sub_id = node_weight.id(); + let sub_lineage_id = node_weight.lineage_id(); + let new_subgraph_root = subgraph.add_node(node_weight); + add_node_to_idx(sub_id, sub_lineage_id, new_subgraph_root); + index_map.insert(subgraph_root, new_subgraph_root); + + // Walk to parent + while let Some(node_idx) = parent_q.pop_front() { + let mut has_parents = false; + for edge_ref in self.edges_directed(node_idx, Incoming) { + has_parents = true; + let source_idx = edge_ref.source(); + let source_node_weight = self.graph.node_weight(source_idx)?.to_owned(); + let id = source_node_weight.id(); + let lineage_id = source_node_weight.lineage_id(); + let new_source_idx = match index_map.get(&source_idx).copied() { + Some(node_idx) => node_idx, + None => { + let new_source_idx = subgraph.add_node(source_node_weight); + index_map.insert(source_idx, new_source_idx); + node_index_by_id.insert(id, new_source_idx); + node_indices_by_lineage_id + .entry(lineage_id) + .and_modify(|set: &mut HashSet| { + set.insert(new_source_idx); + }) + .or_insert_with(|| HashSet::from([new_source_idx])); + + new_source_idx + } + }; + + let edge_weight = edge_ref.weight().to_owned(); + + let current_node_idx_in_sub = index_map.get(&node_idx).copied()?; + if subgraph + .find_edge(new_source_idx, current_node_idx_in_sub) + .is_none() + { + subgraph.add_edge(new_source_idx, current_node_idx_in_sub, edge_weight); + } + + parent_q.push_back(source_idx); + } + if !has_parents { + new_root = Some(index_map.get(&node_idx).copied()?); + } + } + + // Walk to leaves from subgraph_root + let mut child_q: VecDeque = VecDeque::from([subgraph_root]); + while let Some(node_idx) = child_q.pop_front() { + for edge_ref in self.edges_directed(node_idx, Outgoing) { + let target_idx = edge_ref.target(); + let target_node_weight = self.graph.node_weight(target_idx)?.to_owned(); + let id = target_node_weight.id(); + let lineage_id = target_node_weight.lineage_id(); + let new_target_idx = match index_map.get(&target_idx).copied() { + Some(node_idx) => node_idx, + None => { + let new_target_idx = subgraph.add_node(target_node_weight); + index_map.insert(target_idx, new_target_idx); + node_index_by_id.insert(id, new_target_idx); + node_indices_by_lineage_id + .entry(lineage_id) + .and_modify(|set: &mut HashSet| { + set.insert(new_target_idx); + }) + .or_insert_with(|| HashSet::from([new_target_idx])); + + new_target_idx + } + }; + + let edge_weight = edge_ref.weight().to_owned(); + let current_node_idx_in_sub = index_map.get(&edge_ref.source()).copied()?; + if subgraph + .find_edge(current_node_idx_in_sub, new_target_idx) + .is_none() + { + subgraph.add_edge(current_node_idx_in_sub, new_target_idx, edge_weight); + } + + child_q.push_back(target_idx); + } + } + + Some(Self { + graph: subgraph, + node_index_by_id, + node_indices_by_lineage_id, + root_index: new_root?, + ..Default::default() + }) + } + + /// Write the graph to disk. This *MAY PANIC*. Use only for debugging. + #[allow(clippy::disallowed_methods)] + pub fn write_to_disk(&self, file_suffix: &str) { + let serialized = serialize::to_vec(self).expect("unable to serialize"); + let filename = format!("{}-{}", Ulid::new(), file_suffix); + + let home_env = std::env::var("HOME").expect("No HOME environment variable set"); + let home = std::path::Path::new(&home_env); + let mut file = File::create(home.join(&filename)).expect("could not create file"); + file.write_all(&serialized).expect("could not write file"); + + println!("Wrote graph to {}", home.join(&filename).display()); + } + + #[allow(clippy::disallowed_methods)] + pub fn tiny_dot_to_file(&self, suffix: Option<&str>) { + let suffix = suffix.unwrap_or("dot"); + // NOTE(nick): copy the output and execute this on macOS. It will create a file in the + // process and open a new tab in your browser. + // ``` + // GRAPHFILE=; cat $GRAPHFILE.txt | dot -Tsvg -o processed-$GRAPHFILE.svg; open processed-$GRAPHFILE.svg + // ``` + let dot = petgraph::dot::Dot::with_attr_getters( + &self.graph, + &[ + petgraph::dot::Config::NodeNoLabel, + petgraph::dot::Config::EdgeNoLabel, + ], + &|_, edgeref| { + let discrim: EdgeWeightKindDiscriminants = edgeref.weight().kind().into(); + let color = match discrim { + EdgeWeightKindDiscriminants::Action => "black", + EdgeWeightKindDiscriminants::ActionPrototype => "black", + EdgeWeightKindDiscriminants::AuthenticationPrototype => "black", + EdgeWeightKindDiscriminants::Contain => "blue", + EdgeWeightKindDiscriminants::FrameContains => "black", + EdgeWeightKindDiscriminants::Represents => "black", + EdgeWeightKindDiscriminants::Ordering => "gray", + EdgeWeightKindDiscriminants::Ordinal => "gray", + EdgeWeightKindDiscriminants::Prop => "orange", + EdgeWeightKindDiscriminants::Prototype => "green", + EdgeWeightKindDiscriminants::PrototypeArgument => "green", + EdgeWeightKindDiscriminants::PrototypeArgumentValue => "green", + EdgeWeightKindDiscriminants::Socket => "red", + EdgeWeightKindDiscriminants::SocketValue => "purple", + EdgeWeightKindDiscriminants::Proxy => "gray", + EdgeWeightKindDiscriminants::Root => "black", + EdgeWeightKindDiscriminants::Use => "black", + EdgeWeightKindDiscriminants::ValidationOutput => "darkcyan", + EdgeWeightKindDiscriminants::ManagementPrototype => "pink", + }; + + match edgeref.weight().kind() { + EdgeWeightKind::Contain(key) => { + let key = key + .as_deref() + .map(|key| format!(" ({key}")) + .unwrap_or("".into()); + format!( + "label = \"{discrim:?}{key}\"\nfontcolor = {color}\ncolor = {color}" + ) + } + _ => format!("label = \"{discrim:?}\"\nfontcolor = {color}\ncolor = {color}"), + } + }, + &|_, (node_index, node_weight)| { + let (label, color) = match node_weight { + NodeWeight::Action(_) => ("Action".to_string(), "cyan"), + NodeWeight::ActionPrototype(_) => ("Action Prototype".to_string(), "cyan"), + NodeWeight::Content(weight) => { + let discrim = ContentAddressDiscriminants::from(weight.content_address()); + let color = match discrim { + // Some of these should never happen as they have their own top-level + // NodeWeight variant. + ContentAddressDiscriminants::ActionPrototype => "green", + ContentAddressDiscriminants::AttributePrototype => "green", + ContentAddressDiscriminants::Component => "black", + ContentAddressDiscriminants::DeprecatedAction => "green", + ContentAddressDiscriminants::DeprecatedActionBatch => "green", + ContentAddressDiscriminants::DeprecatedActionRunner => "green", + ContentAddressDiscriminants::OutputSocket => "red", + ContentAddressDiscriminants::Func => "black", + ContentAddressDiscriminants::FuncArg => "black", + ContentAddressDiscriminants::Geometry => "black", + ContentAddressDiscriminants::InputSocket => "red", + ContentAddressDiscriminants::JsonValue => "fuchsia", + ContentAddressDiscriminants::Module => "yellow", + ContentAddressDiscriminants::Prop => "orange", + ContentAddressDiscriminants::Root => "black", + ContentAddressDiscriminants::Schema => "black", + ContentAddressDiscriminants::SchemaVariant => "black", + ContentAddressDiscriminants::Secret => "black", + ContentAddressDiscriminants::StaticArgumentValue => "green", + ContentAddressDiscriminants::ValidationPrototype => "black", + ContentAddressDiscriminants::ValidationOutput => "darkcyan", + ContentAddressDiscriminants::ManagementPrototype => "black", + ContentAddressDiscriminants::View => "black", + }; + (discrim.to_string(), color) + } + NodeWeight::AttributePrototypeArgument(apa) => ( + format!( + "Attribute Prototype Argument{}", + apa.targets() + .map(|targets| format!( + "\nsource: {}\nto: {}", + targets.source_component_id, targets.destination_component_id + )) + .unwrap_or("".to_string()) + ), + "green", + ), + NodeWeight::AttributeValue(_) => ("Attribute Value".to_string(), "blue"), + NodeWeight::Category(category_node_weight) => match category_node_weight.kind() + { + CategoryNodeKind::Action => ("Actions (Category)".to_string(), "black"), + CategoryNodeKind::Component => { + ("Components (Category)".to_string(), "black") + } + CategoryNodeKind::DeprecatedActionBatch => { + ("Action Batches (Category)".to_string(), "black") + } + CategoryNodeKind::Func => ("Funcs (Category)".to_string(), "black"), + CategoryNodeKind::Schema => ("Schemas (Category)".to_string(), "black"), + CategoryNodeKind::Secret => ("Secrets (Category)".to_string(), "black"), + CategoryNodeKind::Module => ("Modules (Category)".to_string(), "black"), + CategoryNodeKind::DependentValueRoots => { + ("Dependent Values (Category)".into(), "black") + } + CategoryNodeKind::View => ("Views (Category)".into(), "black"), + CategoryNodeKind::DiagramObject => { + ("Diagram Objects (Category)".into(), "black") + } + }, + NodeWeight::Component(component) => ( + "Component".to_string(), + if component.to_delete() { + "gray" + } else { + "black" + }, + ), + NodeWeight::Func(func_node_weight) => { + (format!("Func\n{}", func_node_weight.name()), "black") + } + NodeWeight::FuncArgument(func_arg_node_weight) => ( + format!("Func Arg\n{}", func_arg_node_weight.name()), + "black", + ), + NodeWeight::Geometry(_) => ("Geometry\n".to_string(), "green"), + NodeWeight::InputSocket(_) => ("Input Socket".to_string(), "black"), + NodeWeight::Ordering(_) => { + (NodeWeightDiscriminants::Ordering.to_string(), "gray") + } + NodeWeight::Prop(prop_node_weight) => { + (format!("Prop\n{}", prop_node_weight.name()), "orange") + } + NodeWeight::SchemaVariant(_) => ("Schema Variant".to_string(), "black"), + NodeWeight::Secret(secret_node_weight) => ( + format!("Secret\n{}", secret_node_weight.encrypted_secret_key()), + "black", + ), + NodeWeight::DependentValueRoot(node_weight) => ( + format!("UnfinishedDependentValue\n{}", node_weight.value_id()), + "purple", + ), + NodeWeight::FinishedDependentValueRoot(node_weight) => ( + format!("FinishedDependentValue\n{}", node_weight.value_id()), + "red", + ), + NodeWeight::View(_) => ("View\n".to_string(), "black"), + NodeWeight::ManagementPrototype(_) => { + ("ManagementPrototype".to_string(), "black") + } + }; + let color = color.to_string(); + let id = node_weight.id(); + format!( + "label = \"\n\n{label}\n{node_index:?}\n{id}\n\n{:?}\n{:?}\"\nfontcolor = {color}\ncolor = {color}", node_weight.merkle_tree_hash(), node_weight.node_hash(), + ) + }, + ); + let filename_no_extension = format!("{}-{}", Ulid::new(), suffix); + + let home_str = std::env::var("HOME").expect("could not find home directory via env"); + let home = std::path::Path::new(&home_str); + + let mut file = File::create(home.join(format!("{filename_no_extension}.txt"))) + .expect("could not create file"); + file.write_all(format!("{dot:?}").as_bytes()) + .expect("could not write file"); + println!("dot output stored in file (filename without extension: {filename_no_extension})"); + } + + #[inline(always)] + pub fn get_node_index_by_id( + &self, + id: impl Into, + ) -> WorkspaceSnapshotGraphResult { + let id = id.into(); + + self.node_index_by_id + .get(&id) + .copied() + .ok_or(WorkspaceSnapshotGraphError::NodeWithIdNotFound(id)) + } + + #[inline(always)] + pub fn get_node_index_by_id_opt(&self, id: impl Into) -> Option { + let id = id.into(); + + self.node_index_by_id.get(&id).copied() + } + + pub fn get_node_index_by_lineage(&self, lineage_id: Ulid) -> HashSet { + self.node_indices_by_lineage_id + .get(&lineage_id) + .cloned() + .unwrap_or_default() + } + + pub fn node_index_to_id(&self, node_idx: NodeIndex) -> Option { + self.graph + .node_weight(node_idx) + .map(|node_weight| node_weight.id()) + } + + pub fn get_node_weight_opt(&self, node_index: NodeIndex) -> Option<&NodeWeight> { + self.graph.node_weight(node_index) + } + + pub fn get_node_weight( + &self, + node_index: NodeIndex, + ) -> WorkspaceSnapshotGraphResult<&NodeWeight> { + self.get_node_weight_opt(node_index) + .ok_or(WorkspaceSnapshotGraphError::NodeWeightNotFound) + } + + pub fn get_node_weight_by_id_opt(&self, id: impl Into) -> Option<&NodeWeight> { + self.get_node_index_by_id_opt(id) + .and_then(|index| self.get_node_weight_opt(index)) + } + + /// Gets a mutable reference to the node weight at `node_index`. If you + /// modify this node, you must also touch it by calling `Self::touch_node` + /// so that its merkle tree hash and the merkle tree hash of its parents are + /// both updated. + fn get_node_weight_mut( + &mut self, + node_index: NodeIndex, + ) -> WorkspaceSnapshotGraphResult<&mut NodeWeight> { + self.graph + .node_weight_mut(node_index) + .ok_or(WorkspaceSnapshotGraphError::NodeWeightNotFound) + } + + pub fn get_edge_weight_opt( + &self, + edge_index: EdgeIndex, + ) -> WorkspaceSnapshotGraphResult> { + Ok(self.graph.edge_weight(edge_index)) + } + + pub fn import_component_subgraph( + &mut self, + other: &WorkspaceSnapshotGraphV4, + component_node_index: NodeIndex, + ) -> WorkspaceSnapshotGraphResult<()> { + // * DFS event-based traversal. + // * DfsEvent::Discover(attribute_prototype_argument_node_index, _): + // If APA has targets, skip & return Control::Prune, since we don't want to bring in + // Components other than the one specified. Only arguments linking Inout & Output + // Sockets will have targets (the source & destination ComponentIDs). + // * DfsEvent::Discover(func_node_index, _): + // Add edge from Funcs Category node to imported Func node. + let mut edges_by_tail = HashMap::new(); + petgraph::visit::depth_first_search(&other.graph, Some(component_node_index), |event| { + self.import_component_subgraph_process_dfs_event(other, &mut edges_by_tail, event) + })?; + + Ok(()) + } + + /// This assumes that the SchemaVariant for the Component is already present in [`self`][Self]. + fn import_component_subgraph_process_dfs_event( + &mut self, + other: &WorkspaceSnapshotGraphV4, + edges_by_tail: &mut HashMap>, + event: DfsEvent, + ) -> WorkspaceSnapshotGraphResult> { + match event { + // We only check to see if we can prune graph traversal in the node discovery event. + // The "real" work is done in the node finished event. + DfsEvent::Discover(other_node_index, _) => { + let other_node_weight = other.get_node_weight(other_node_index)?; + + // AttributePrototypeArguments with targets connect Input & Output Sockets, and we + // don't want to import either the Component on the other end of the connection, or + // the connection itself. Unfortunately, we can't prune when looking at the + // relevant edge, as that would prune _all remaining edges_ outgoing from the + // AttributePrototype. + if NodeWeightDiscriminants::AttributePrototypeArgument == other_node_weight.into() { + let apa_node_weight = + other_node_weight.get_attribute_prototype_argument_node_weight()?; + if apa_node_weight.targets().is_some() { + return Ok(petgraph::visit::Control::Prune); + } + } + + // When we hit something that already exists, we're pretty much guaranteed to have + // left "the component" and have gone into already-existing Funcs or the Schema + // Variant. + if self + .find_equivalent_node(other_node_weight.id(), other_node_weight.lineage_id())? + .is_some() + { + return Ok(petgraph::visit::Control::Prune); + } + + Ok(petgraph::visit::Control::Continue) + } + // We wait to do the "real" import work in the Finish event as these happen in + // post-order, so we are guaranteed that all of the targets of the outgoing edges + // have been imported. + DfsEvent::Finish(other_node_index, _) => { + // See if we already have the node from other in self. + let other_node_weight = other.get_node_weight(other_node_index)?; + // Even though we prune when the equivalent node is_some() in the Discover event, + // we will still get a Finish event for the node that returned Control::Prune in + // its Discover event. + if self + .find_equivalent_node(other_node_weight.id(), other_node_weight.lineage_id())? + .is_none() + { + // AttributePrototypeArguments for cross-component connections will still have + // their DfsEvent::Finish fire, and won't already exist in self, but we do not + // want to import them. + if let NodeWeight::AttributePrototypeArgument( + attribute_prototype_argument_node_weight, + ) = other_node_weight + { + if attribute_prototype_argument_node_weight.targets().is_some() { + return Ok(petgraph::visit::Control::Prune); + } + } + + // Import the node. + self.add_or_replace_node(other_node_weight.clone())?; + + // Create all edges with this node as the tail. + if let Entry::Occupied(edges) = edges_by_tail.entry(other_node_index) { + for (other_head_node_index, edge_weight) in edges.get() { + // Need to get this on every iteration as the node index changes as we + // add edges. + let self_node_index = + self.get_node_index_by_id(other_node_weight.id())?; + let other_head_node_weight = + other.get_node_weight(*other_head_node_index)?; + let self_head_node_index = + self.get_node_index_by_id(other_head_node_weight.id())?; + self.add_edge( + self_node_index, + edge_weight.clone(), + self_head_node_index, + )?; + } + } + + // Funcs and Components have incoming edges from their Category nodes that we + // want to exist, but won't be discovered by the graph traversal on its own. + let category_kind = if NodeWeightDiscriminants::Func == other_node_weight.into() + { + Some(CategoryNodeKind::Func) + } else if NodeWeightDiscriminants::Component == other_node_weight.into() { + Some(CategoryNodeKind::Component) + } else { + None + }; + + if let Some(category_node_kind) = category_kind { + let self_node_index = self.get_node_index_by_id(other_node_weight.id())?; + let (_, category_node_idx) = self + .get_category_node(None, category_node_kind)? + .ok_or(WorkspaceSnapshotGraphError::CategoryNodeNotFound( + CategoryNodeKind::Func, + ))?; + self.add_edge( + category_node_idx, + EdgeWeight::new(EdgeWeightKind::new_use()), + self_node_index, + )?; + } + } + + Ok(petgraph::visit::Control::Continue) + } + DfsEvent::BackEdge(tail_node_index, head_node_index) + | DfsEvent::CrossForwardEdge(tail_node_index, head_node_index) + | DfsEvent::TreeEdge(tail_node_index, head_node_index) => { + // We'll keep track of the edges we encounter, instead of doing something with them + // right away as the common case (TreeEdge) is that we'll encounter the edge before + // we encounter the node for the head (target) of the edge. We can't actually do + // anything about importing the edge until we've also imported the head node. + for edgeref in other + .graph + .edges_connecting(tail_node_index, head_node_index) + { + edges_by_tail + .entry(tail_node_index) + .and_modify(|entry| { + entry.push((head_node_index, edgeref.weight().clone())); + }) + .or_insert_with(|| vec![(head_node_index, edgeref.weight().clone())]); + } + Ok(petgraph::visit::Control::Continue) + } + } + } + + pub fn is_acyclic_directed(&self) -> bool { + // Using this because "is_cyclic_directed" is recursive. + algo::toposort(&self.graph, None).is_ok() + } + + pub fn node_count(&self) -> usize { + self.graph.node_count() + } + + /// Returns an `Option>`. If there is an ordering node, then the return will be a + /// [`Some`], where the [`Vec`] is populated with the [`NodeIndex`] of the nodes specified by + /// the ordering node, in the order defined by the ordering node. If there is not an ordering + /// node, then the return will be [`None`]. + pub fn ordered_children_for_node( + &self, + container_node_index: NodeIndex, + ) -> WorkspaceSnapshotGraphResult>> { + let mut ordered_child_indexes = Vec::new(); + if let Some(container_ordering_index) = + self.ordering_node_index_for_container(container_node_index)? + { + if let NodeWeight::Ordering(ordering_weight) = + self.get_node_weight(container_ordering_index)? + { + for ordered_id in ordering_weight.order() { + if let Some(child_index) = self.node_index_by_id.get(ordered_id).copied() { + ordered_child_indexes.push(child_index); + } + } + } + } else { + return Ok(None); + } + + Ok(Some(ordered_child_indexes)) + } + + pub fn ordering_node_for_container( + &self, + container_node_index: NodeIndex, + ) -> WorkspaceSnapshotGraphResult> { + Ok( + match self.ordering_node_index_for_container(container_node_index)? { + Some(ordering_node_idx) => match self.get_node_weight_opt(ordering_node_idx) { + Some(node_weight) => Some(node_weight.get_ordering_node_weight()?.clone()), + None => None, + }, + None => None, + }, + ) + } + + pub fn ordering_node_index_for_container( + &self, + container_node_index: NodeIndex, + ) -> WorkspaceSnapshotGraphResult> { + let onto_ordering_node_indexes = + ordering_node_indexes_for_node_index(self, container_node_index); + if onto_ordering_node_indexes.len() > 1 { + error!( + "Too many ordering nodes found for container NodeIndex {:?}", + container_node_index + ); + return Err(WorkspaceSnapshotGraphError::TooManyOrderingForNode( + container_node_index, + )); + } + Ok(onto_ordering_node_indexes.first().copied()) + } + + pub fn prop_node_index_for_node_index( + &self, + node_index: NodeIndex, + ) -> WorkspaceSnapshotGraphResult> { + let prop_node_indexes = prop_node_indexes_for_node_index(self, node_index); + if prop_node_indexes.len() > 1 { + error!("Too many prop nodes found for NodeIndex {:?}", node_index); + return Err(WorkspaceSnapshotGraphError::TooManyPropForNode(node_index)); + } + Ok(prop_node_indexes.first().copied()) + } + + /// Removes the node from the graph. Edges to this node will be + /// automatically removed by petgraph. Be sure to remove the node id from + /// the mappings with `Self::remove_node_id` + pub fn remove_node(&mut self, node_index: NodeIndex) { + let incoming_sources: Vec<_> = self + .graph + .neighbors_directed(node_index, Incoming) + .collect(); + + // We have to be sure that we recalculate the merkle tree hash for every + // node that had an outgoing edge to this node + for incoming in incoming_sources { + self.touch_node(incoming); + } + + self.graph.remove_node(node_index); + } + + /// Removes an edge of the specified kind between `source_node_index` and + /// `target_node_index`. + /// + /// If the source node has an associated ordering node, the function also + /// removes the edge from the ordering node to the target node, updating the + /// ordering node's order + pub fn remove_edge( + &mut self, + source_node_index: NodeIndex, + target_node_index: NodeIndex, + edge_kind: EdgeWeightKindDiscriminants, + ) -> WorkspaceSnapshotGraphResult<()> { + self.remove_edge_of_kind(source_node_index, target_node_index, edge_kind); + self.touch_node(source_node_index); + + if let Some(container_ordering_node_idx) = + self.ordering_node_index_for_container(source_node_index)? + { + let element_id = self + .node_index_to_id(target_node_index) + .ok_or(WorkspaceSnapshotGraphError::NodeWeightNotFound)?; + + if let NodeWeight::Ordering(container_ordering_node_weight) = + self.get_node_weight_mut(container_ordering_node_idx)? + { + if container_ordering_node_weight.remove_from_order(element_id) { + self.remove_edge_of_kind( + container_ordering_node_idx, + target_node_index, + EdgeWeightKindDiscriminants::Ordinal, + ); + self.touch_node(container_ordering_node_idx); + } + } + } + + Ok(()) + } + + fn remove_edge_of_kind( + &mut self, + source_node_index: NodeIndex, + target_node_index: NodeIndex, + edge_kind: EdgeWeightKindDiscriminants, + ) { + let mut edges_to_remove = vec![]; + for edgeref in self + .graph + .edges_connecting(source_node_index, target_node_index) + { + if edge_kind == edgeref.weight().kind().into() { + edges_to_remove.push(edgeref.id()); + } + } + for edge_to_remove in edges_to_remove { + self.graph.remove_edge(edge_to_remove); + } + } + + pub fn edge_endpoints( + &self, + edge_index: EdgeIndex, + ) -> WorkspaceSnapshotGraphResult<(NodeIndex, NodeIndex)> { + let (source, destination) = self + .graph + .edge_endpoints(edge_index) + .ok_or(WorkspaceSnapshotGraphError::EdgeDoesNotExist(edge_index))?; + Ok((source, destination)) + } + + pub fn update_content( + &mut self, + id: Ulid, + new_content_hash: ContentHash, + ) -> WorkspaceSnapshotGraphResult<()> { + let node_index = self.get_node_index_by_id(id)?; + let node_weight = self.get_node_weight_mut(node_index)?; + node_weight.new_content_hash(new_content_hash)?; + self.touch_node(node_index); + Ok(()) + } + + pub fn update_order( + &mut self, + container_id: Ulid, + new_order: Vec, + ) -> WorkspaceSnapshotGraphResult<()> { + let node_index = self + .ordering_node_index_for_container(self.get_node_index_by_id(container_id)?)? + .ok_or(WorkspaceSnapshotGraphError::NodeWeightNotFound)?; + let node_weight = self.get_node_weight_mut(node_index)?; + node_weight.set_order(new_order)?; + self.touch_node(node_index); + + Ok(()) + } + + fn update_merkle_tree_hash( + &mut self, + node_index_to_update: NodeIndex, + ) -> WorkspaceSnapshotGraphResult<()> { + let mut hasher = MerkleTreeHash::hasher(); + hasher.update( + self.get_node_weight(node_index_to_update)? + .node_hash() + .to_string() + .as_bytes(), + ); + + // Need to make sure that ordered containers have their ordered children in the + // order specified by the ordering graph node. + let explicitly_ordered_children = self + .ordered_children_for_node(node_index_to_update)? + .unwrap_or_default(); + + // Need to make sure the unordered neighbors are added to the hash in a stable order to + // ensure the merkle tree hash is identical for identical trees. + let mut unordered_neighbors = Vec::new(); + for neighbor_node in self + .graph + .neighbors_directed(node_index_to_update, Outgoing) + { + // Only add the neighbor if it's not one of the ones with an explicit ordering. + if !explicitly_ordered_children.contains(&neighbor_node) { + if let Some(neighbor_id) = self.node_index_to_id(neighbor_node) { + unordered_neighbors.push((neighbor_id, neighbor_node)); + } + } + } + // We'll sort the neighbors by the ID in the NodeWeight, as that will result in more stable + // results than if we sorted by the NodeIndex itself. + unordered_neighbors.sort_by_cached_key(|(id, _index)| *id); + // It's not important whether the explicitly ordered children are first or last, as long as + // they are always in that position, and are always in the sequence specified by the + // container's Ordering node. + let mut ordered_neighbors = + Vec::with_capacity(explicitly_ordered_children.len() + unordered_neighbors.len()); + ordered_neighbors.extend(explicitly_ordered_children); + ordered_neighbors.extend::>( + unordered_neighbors + .iter() + .map(|(_id, index)| *index) + .collect(), + ); + + for neighbor_node in ordered_neighbors { + hasher.update( + self.get_node_weight(neighbor_node)? + .merkle_tree_hash() + .to_string() + .as_bytes(), + ); + + // The edge(s) between `node_index_to_update`, and `neighbor_node` potentially encode + // important information related to the "identity" of `node_index_to_update`. + for connecting_edgeref in self + .graph + .edges_connecting(node_index_to_update, neighbor_node) + { + match connecting_edgeref.weight().kind() { + // This is the key for an entry in a map. + EdgeWeightKind::Contain(Some(key)) => hasher.update(key.as_bytes()), + + EdgeWeightKind::Use { is_default } => { + hasher.update(is_default.to_string().as_bytes()) + } + + // This is the key representing an element in a container type corresponding + // to an AttributePrototype + EdgeWeightKind::Prototype(Some(key)) => hasher.update(key.as_bytes()), + + // Nothing to do, as these EdgeWeightKind do not encode extra information + // in the edge itself. + EdgeWeightKind::AuthenticationPrototype + | EdgeWeightKind::Action + | EdgeWeightKind::ActionPrototype + | EdgeWeightKind::Contain(None) + | EdgeWeightKind::FrameContains + | EdgeWeightKind::PrototypeArgument + | EdgeWeightKind::PrototypeArgumentValue + | EdgeWeightKind::Socket + | EdgeWeightKind::Ordering + | EdgeWeightKind::Ordinal + | EdgeWeightKind::Prop + | EdgeWeightKind::Prototype(None) + | EdgeWeightKind::Proxy + | EdgeWeightKind::Represents + | EdgeWeightKind::Root + | EdgeWeightKind::SocketValue + | EdgeWeightKind::ValidationOutput + | EdgeWeightKind::ManagementPrototype => {} + } + } + } + + let new_node_weight = self + .graph + .node_weight_mut(node_index_to_update) + .ok_or(WorkspaceSnapshotGraphError::NodeWeightNotFound)?; + new_node_weight.set_merkle_tree_hash(hasher.finalize()); + + Ok(()) + } + + /// Does a depth first post-order walk to recalculate the entire merkle tree + /// hash. This operation can be expensive, so should be done only when we + /// know it needs to be (like when migrating snapshots between versions) + pub fn recalculate_entire_merkle_tree_hash(&mut self) -> WorkspaceSnapshotGraphResult<()> { + let mut dfs = petgraph::visit::DfsPostOrder::new(&self.graph, self.root_index); + + while let Some(node_index) = dfs.next(&self.graph) { + self.update_merkle_tree_hash(node_index)?; + } + + Ok(()) + } + + /// Does a dfs post-order walk of the DAG, recalculating the merkle tree + /// hash for any nodes we have touched while working on the graph. Should be + /// more efficient than recalculating the entire merkle tree hash, since we + /// will only update the hash for the branches of the graph that have been + /// touched and thus need to be recalculated. + pub fn recalculate_entire_merkle_tree_hash_based_on_touched_nodes( + &mut self, + ) -> WorkspaceSnapshotGraphResult<()> { + let mut dfs = petgraph::visit::DfsPostOrder::new(&self.graph, self.root_index); + + let mut discovered_nodes = HashSet::new(); + + while let Some(node_index) = dfs.next(&self.graph) { + if self.touched_node_indices.contains(&node_index) + || discovered_nodes.contains(&node_index) + { + self.update_merkle_tree_hash(node_index)?; + self.graph + .neighbors_directed(node_index, Incoming) + .for_each(|node_idx| { + discovered_nodes.insert(node_idx); + }); + } + } + + self.touched_node_indices.clear(); + + Ok(()) + } + + pub fn perform_updates(&mut self, updates: &[Update]) -> WorkspaceSnapshotGraphResult<()> { + for update in updates { + match update { + Update::NewEdge { + source, + destination, + edge_weight, + } => { + let source_idx = self.get_node_index_by_id_opt(source.id); + let destination_idx = self.get_node_index_by_id_opt(destination.id); + + if let (Some(source_idx), Some(destination_idx)) = (source_idx, destination_idx) + { + if let EdgeWeightKind::Use { is_default: true } = edge_weight.kind() { + ensure_only_one_default_use_edge(self, source_idx)?; + } + + self.add_edge_inner( + source_idx, + edge_weight.clone(), + destination_idx, + false, + )?; + } + } + Update::RemoveEdge { + source, + destination, + edge_kind, + } => { + let source_idx = self.get_node_index_by_id_opt(source.id); + let destination_idx = self.get_node_index_by_id_opt(destination.id); + + if let (Some(source_idx), Some(destination_idx)) = (source_idx, destination_idx) + { + self.remove_edge(source_idx, destination_idx, *edge_kind)?; + } + } + Update::NewNode { node_weight } => { + if self.get_node_index_by_id_opt(node_weight.id()).is_none() { + self.add_or_replace_node(node_weight.to_owned())?; + } + } + Update::ReplaceNode { node_weight } => { + if self.get_node_index_by_id_opt(node_weight.id()).is_some() { + self.add_or_replace_node(node_weight.to_owned())?; + } + } + } + } + + Ok(()) + } + + /// Update node weight in place with a lambda. Use with caution. Generally + /// we treat node weights as immutable and replace them by creating a new + /// node with a new node weight and replacing references to point to the new + /// node. + pub(crate) fn update_node_weight( + &mut self, + node_idx: NodeIndex, + lambda: L, + ) -> WorkspaceSnapshotGraphResult<()> + where + L: FnOnce(&mut NodeWeight) -> WorkspaceSnapshotGraphResult<()>, + { + let node_weight = self + .graph + .node_weight_mut(node_idx) + .ok_or(WorkspaceSnapshotGraphError::NodeWeightNotFound)?; + + lambda(node_weight)?; + self.touch_node(node_idx); + + Ok(()) + } +} + +fn ordering_node_indexes_for_node_index( + snapshot: &WorkspaceSnapshotGraphV4, + node_index: NodeIndex, +) -> Vec { + snapshot + .graph + .edges_directed(node_index, Outgoing) + .filter_map(|edge_reference| { + if edge_reference.weight().kind() == &EdgeWeightKind::Ordering + && matches!( + snapshot.get_node_weight(edge_reference.target()), + Ok(NodeWeight::Ordering(_)) + ) + { + return Some(edge_reference.target()); + } + + None + }) + .collect() +} + +fn prop_node_indexes_for_node_index( + snapshot: &WorkspaceSnapshotGraphV4, + node_index: NodeIndex, +) -> Vec { + snapshot + .graph + .edges_directed(node_index, Outgoing) + .filter_map(|edge_reference| { + if edge_reference.weight().kind() == &EdgeWeightKind::Prop + && matches!( + snapshot.get_node_weight(edge_reference.target()), + Ok(NodeWeight::Prop(_)) + ) + { + return Some(edge_reference.target()); + } + None + }) + .collect() +} + +fn ensure_only_one_default_use_edge( + graph: &mut WorkspaceSnapshotGraphV4, + source_idx: NodeIndex, +) -> WorkspaceSnapshotGraphResult<()> { + let existing_default_targets: Vec = graph + .edges_directed(source_idx, Outgoing) + .filter(|edge_ref| { + matches!( + edge_ref.weight().kind(), + EdgeWeightKind::Use { is_default: true } + ) + }) + .map(|edge_ref| edge_ref.target()) + .collect(); + + for target_idx in existing_default_targets { + graph.remove_edge(source_idx, target_idx, EdgeWeightKindDiscriminants::Use)?; + graph.add_edge_inner( + source_idx, + EdgeWeight::new(EdgeWeightKind::new_use()), + target_idx, + false, + )?; + } + + Ok(()) +} diff --git a/lib/dal/src/workspace_snapshot/graph/v4/component.rs b/lib/dal/src/workspace_snapshot/graph/v4/component.rs new file mode 100644 index 0000000000..5f35ed345c --- /dev/null +++ b/lib/dal/src/workspace_snapshot/graph/v4/component.rs @@ -0,0 +1,66 @@ +use petgraph::prelude::*; + +use crate::workspace_snapshot::graph::WorkspaceSnapshotGraphV4; +use crate::{ + component::ComponentResult, + workspace_snapshot::{ + edge_weight::EdgeWeightKindDiscriminants, node_weight::NodeWeightDiscriminants, + }, + ComponentError, ComponentId, SchemaVariantId, WorkspaceSnapshotError, +}; + +impl WorkspaceSnapshotGraphV4 { + pub fn frame_contains_components( + &self, + component_id: ComponentId, + ) -> ComponentResult> { + let component_node_index = *self + .node_index_by_id + .get(&component_id.into()) + .ok_or(ComponentError::NotFound(component_id))?; + let mut results = Vec::new(); + for (_edge_weight, _source_node_index, destination_node_index) in self + .edges_directed_for_edge_weight_kind( + component_node_index, + Outgoing, + EdgeWeightKindDiscriminants::FrameContains, + ) + { + let node_weight = self + .get_node_weight(destination_node_index) + .map_err(WorkspaceSnapshotError::from)?; + if NodeWeightDiscriminants::from(node_weight) == NodeWeightDiscriminants::Component { + results.push(node_weight.id().into()); + } + } + + Ok(results) + } + + pub fn schema_variant_id_for_component_id( + &self, + component_id: ComponentId, + ) -> ComponentResult { + let component_node_index = *self + .node_index_by_id + .get(&component_id.into()) + .ok_or(ComponentError::NotFound(component_id))?; + for (_edge_weight, _source_node_index, destination_node_index) in self + .edges_directed_for_edge_weight_kind( + component_node_index, + Outgoing, + EdgeWeightKindDiscriminants::Use, + ) + { + let node_weight = self + .get_node_weight(destination_node_index) + .map_err(WorkspaceSnapshotError::from)?; + if NodeWeightDiscriminants::from(node_weight) == NodeWeightDiscriminants::SchemaVariant + { + return Ok(node_weight.id().into()); + } + } + + Err(ComponentError::SchemaVariantNotFound(component_id)) + } +} diff --git a/lib/dal/src/workspace_snapshot/graph/v4/schema.rs b/lib/dal/src/workspace_snapshot/graph/v4/schema.rs new file mode 100644 index 0000000000..36252265f3 --- /dev/null +++ b/lib/dal/src/workspace_snapshot/graph/v4/schema.rs @@ -0,0 +1,42 @@ +use petgraph::prelude::*; + +use crate::workspace_snapshot::graph::WorkspaceSnapshotGraphV4; +use crate::{ + workspace_snapshot::graph::{WorkspaceSnapshotGraphError, WorkspaceSnapshotGraphResult}, + EdgeWeightKindDiscriminants, SchemaId, SchemaVariantId, +}; + +pub mod variant; + +impl WorkspaceSnapshotGraphV4 { + pub fn schema_variant_ids_for_schema_id( + &self, + schema_id: SchemaId, + ) -> WorkspaceSnapshotGraphResult> { + self.schema_variant_ids_for_schema_id_opt(schema_id)? + .ok_or_else(|| WorkspaceSnapshotGraphError::NodeWithIdNotFound(schema_id.into())) + } + + pub fn schema_variant_ids_for_schema_id_opt( + &self, + schema_id: SchemaId, + ) -> WorkspaceSnapshotGraphResult>> { + let mut schema_variant_ids = Vec::new(); + if let Some(schema_node_idx) = self.get_node_index_by_id_opt(schema_id) { + for (_, _source_idx, destination_idx) in self.edges_directed_for_edge_weight_kind( + schema_node_idx, + Outgoing, + EdgeWeightKindDiscriminants::Use, + ) { + let dest_node_weight = self.get_node_weight(destination_idx)?; + if dest_node_weight.get_schema_variant_node_weight().is_ok() { + schema_variant_ids.push(dest_node_weight.id().into()); + } + } + } else { + return Ok(None); + } + + Ok(Some(schema_variant_ids)) + } +} diff --git a/lib/dal/src/workspace_snapshot/graph/v4/schema/variant.rs b/lib/dal/src/workspace_snapshot/graph/v4/schema/variant.rs new file mode 100644 index 0000000000..8fe4f3e51d --- /dev/null +++ b/lib/dal/src/workspace_snapshot/graph/v4/schema/variant.rs @@ -0,0 +1,48 @@ +use petgraph::prelude::*; + +use crate::workspace_snapshot::graph::WorkspaceSnapshotGraphV4; +use crate::{ + workspace_snapshot::{ + content_address::ContentAddressDiscriminants, edge_weight::EdgeWeightKindDiscriminants, + graph::WorkspaceSnapshotGraphResult, node_weight::NodeWeight, + }, + SchemaId, SchemaVariantError, SchemaVariantId, +}; + +impl WorkspaceSnapshotGraphV4 { + pub fn schema_id_for_schema_variant_id( + &self, + schema_variant_id: SchemaVariantId, + ) -> WorkspaceSnapshotGraphResult { + let schema_variant_node_index = self + .node_index_by_id + .get(&schema_variant_id.into()) + .ok_or(SchemaVariantError::NotFound(schema_variant_id)) + .map_err(Box::new)?; + let incoming_edges = self.edges_directed_for_edge_weight_kind( + *schema_variant_node_index, + Incoming, + EdgeWeightKindDiscriminants::Use, + ); + + let mut schema_id: Option = None; + for (_edge_weight, source_node_index, _destination_node_index) in incoming_edges { + if let NodeWeight::Content(content) = self.get_node_weight(source_node_index)? { + if content.content_address_discriminants() == ContentAddressDiscriminants::Schema { + schema_id = match schema_id { + None => Some(content.id().into()), + Some(_already_found_schema_id) => { + return Err(Box::new(SchemaVariantError::MoreThanOneSchemaFound( + schema_variant_id, + )) + .into()); + } + }; + } + } + } + Ok(schema_id + .ok_or(SchemaVariantError::SchemaNotFound(schema_variant_id)) + .map_err(Box::new)?) + } +} diff --git a/lib/dal/src/workspace_snapshot/migrator.rs b/lib/dal/src/workspace_snapshot/migrator.rs index a899316ac1..4184280bb4 100644 --- a/lib/dal/src/workspace_snapshot/migrator.rs +++ b/lib/dal/src/workspace_snapshot/migrator.rs @@ -1,8 +1,3 @@ -use si_layer_cache::LayerDbError; -use std::sync::Arc; -use telemetry::prelude::*; -use thiserror::Error; - use super::{ graph::{ WorkspaceSnapshotGraph, WorkspaceSnapshotGraphDiscriminants, WorkspaceSnapshotGraphError, @@ -12,26 +7,38 @@ use super::{ schema_variant_node_weight::SchemaVariantNodeWeightError, }, }; +use crate::workspace_snapshot::migrator::v4::migrate_v3_to_v4; +use crate::workspace_snapshot::node_weight::NodeWeightError; use crate::{ dependency_graph::DependencyGraph, workspace_snapshot::migrator::{v2::migrate_v1_to_v2, v3::migrate_v2_to_v3}, - ChangeSet, ChangeSetError, ChangeSetStatus, DalContext, Workspace, WorkspaceError, - WorkspaceSnapshotError, + ChangeSet, ChangeSetError, ChangeSetStatus, DalContext, Visibility, Workspace, WorkspaceError, + WorkspaceSnapshot, WorkspaceSnapshotError, }; use si_events::WorkspaceSnapshotAddress; +use si_layer_cache::LayerDbError; +use std::sync::Arc; +use telemetry::prelude::*; +use thiserror::Error; +use ulid::DecodeError; pub mod v2; pub mod v3; +pub mod v4; #[derive(Error, Debug)] #[remain::sorted] pub enum SnapshotGraphMigratorError { #[error("change set error: {0}")] ChangeSet(#[from] ChangeSetError), + #[error("ulid decode error: {0}")] + Decode(#[from] DecodeError), #[error("InputSocketNodeWeight error: {0}")] InputSocketNodeWeight(#[from] InputSocketNodeWeightError), #[error("layer db error: {0}")] LayerDb(#[from] LayerDbError), + #[error("node weight error: {0}")] + NodeWeight(#[from] NodeWeightError), #[error("SchemaVariantNodeWeight error: {0}")] SchemaVariantNodeWeight(#[from] SchemaVariantNodeWeightError), #[error("unexpected graph version {1:?} for snapshot {0}, cannot migrate")] @@ -88,6 +95,11 @@ impl SnapshotGraphMigrator { } } + info!( + "Migrating {} snapshot(s)", + change_set_graph.independent_ids().len(), + ); + loop { let change_sets_to_migrate = change_set_graph.independent_ids(); if change_sets_to_migrate.is_empty() { @@ -98,7 +110,8 @@ impl SnapshotGraphMigrator { let mut change_set = ChangeSet::find(ctx, change_set_id) .await? .ok_or(ChangeSetError::ChangeSetNotFound(change_set_id))?; - if change_set.workspace_id.is_none() { + if change_set.workspace_id.is_none() || change_set.status == ChangeSetStatus::Failed + { // These are broken/garbage change sets generated during migrations of the // "universal" workspace/change set. They're not actually accessible via normal // means, as we generally follow the chain starting at the workspace, and these @@ -108,16 +121,20 @@ impl SnapshotGraphMigrator { } let snapshot_address = change_set.workspace_snapshot_address; - info!( + trace!( "Migrating snapshot {} for change set {} with base change set of {:?}", - snapshot_address, change_set_id, change_set.base_change_set_id, + snapshot_address, + change_set_id, + change_set.base_change_set_id, ); let new_snapshot = match self.migrate_snapshot(ctx, snapshot_address).await { Ok(new_snapshot) => new_snapshot, Err(err) => { let err_string = err.to_string(); - if err_string.contains("missing from store for node") { + if err_string.contains("missing from store for node") + || err_string.contains("workspace snapshot graph missing at address") + { error!(error = ?err, "Migration error: {err_string}, marking change set {} for workspace {:?} as failed", change_set.id, change_set.workspace_id); change_set .update_status(ctx, ChangeSetStatus::Failed) @@ -140,10 +157,24 @@ impl SnapshotGraphMigrator { ctx.events_actor(), ) .await?; - change_set.update_pointer(ctx, new_snapshot_address).await?; - info!( + + // NOTE(victor): The context that gets passed in does not have a workspace snapshot + // on it, since its main purpose is to allow access to the services context. + // We need to create a context for each migrated changeset here to run operations + // that depend on the graph + let mut ctx_after_migration = + ctx.clone_with_new_visibility(Visibility::from(change_set.id)); + let migrated_snapshot = WorkspaceSnapshot::find(ctx, new_snapshot_address).await?; + ctx_after_migration.set_workspace_snapshot(migrated_snapshot); + + change_set + .update_pointer(&ctx_after_migration, new_snapshot_address) + .await?; + trace!( "Migrated snapshot {} for change set {} with base change set of {:?}", - snapshot_address, change_set_id, change_set.base_change_set_id, + snapshot_address, + change_set_id, + change_set.base_change_set_id, ); change_set_graph.remove_id(change_set_id); @@ -197,7 +228,11 @@ impl SnapshotGraphMigrator { working_graph = WorkspaceSnapshotGraph::V3(migrate_v2_to_v3(ctx, inner_graph).await?); } - WorkspaceSnapshotGraph::V3(_) => { + WorkspaceSnapshotGraph::V3(inner_graph) => { + working_graph = + WorkspaceSnapshotGraph::V4(migrate_v3_to_v4(ctx, inner_graph).await?); + } + WorkspaceSnapshotGraph::V4(_) => { // Nothing to do, this is the newest version, break; } diff --git a/lib/dal/src/workspace_snapshot/migrator/v4.rs b/lib/dal/src/workspace_snapshot/migrator/v4.rs new file mode 100644 index 0000000000..ebb3af1f83 --- /dev/null +++ b/lib/dal/src/workspace_snapshot/migrator/v4.rs @@ -0,0 +1,116 @@ +use si_events::ulid::Ulid; +use std::sync::Arc; +use telemetry::prelude::*; + +use super::SnapshotGraphMigratorResult; +use crate::layer_db_types::{ViewContent, ViewContentV1}; +use crate::workspace_snapshot::node_weight::category_node_weight::CategoryNodeKind; +use crate::workspace_snapshot::node_weight::ComponentNodeWeight; +use crate::{ + workspace_snapshot::{ + graph::{WorkspaceSnapshotGraphV3, WorkspaceSnapshotGraphV4}, + node_weight::NodeWeight, + }, + DalContext, EdgeWeight, EdgeWeightKind, Timestamp, +}; + +#[instrument(skip_all)] +pub async fn migrate_v3_to_v4( + ctx: &DalContext, + v3_graph: WorkspaceSnapshotGraphV3, +) -> SnapshotGraphMigratorResult { + let mut v4_graph = WorkspaceSnapshotGraphV4::new_from_parts( + v3_graph.graph().clone(), + v3_graph.node_index_by_id().clone(), + v3_graph.node_indices_by_lineage_id().clone(), + v3_graph.root(), + ); + + // Create new category nodes + { + let id = Ulid::from_string("01JAV0CFT9T1D4PQPPC97EJMCH")?; + let lineage_id = Ulid::from_string("01JAV0CFT9T1D4PQPPC97EJMCH")?; + let category_node_index = + v4_graph.add_category_node(id, lineage_id, CategoryNodeKind::DiagramObject)?; + v4_graph.add_edge( + v4_graph.root(), + EdgeWeight::new(EdgeWeightKind::new_use()), + category_node_index, + )?; + } + + let category_node_index = { + let id = Ulid::from_string("01JAV09VAT1A091VNXW3JQJ90B")?; + let lineage_id = Ulid::from_string("01JAV09VAT1A091VNXW3JQJ90B")?; + let category_node_index = + v4_graph.add_category_node(id, lineage_id, CategoryNodeKind::View)?; + v4_graph.add_edge( + v4_graph.root(), + EdgeWeight::new(EdgeWeightKind::new_use()), + category_node_index, + )?; + + category_node_index + }; + + // Create default view + let default_view_idx = { + // Note(victor): This is hardcoded so we can ensure migrated changesets all have the same view + // when we merge it to main. Since right now workspaces are isolated, having the same default view id + // shouldn't cause any problems + let id = Ulid::from_string("01JATWJV2RA407RZFZBQ9PT5ES")?; + let lineage_id = Ulid::from_string("01JATWJV2RA407RZFZBQ9PT5ES")?; + + let content = ViewContent::V1(ViewContentV1 { + timestamp: Timestamp::now(), + name: "DEFAULT".to_owned(), + }); + + let (content_address, _) = ctx + .layer_db() + .cas() + .write( + Arc::new(content.clone().into()), + None, + ctx.events_tenancy(), + ctx.events_actor(), + ) + .await?; + + let node_weight = NodeWeight::new_view(id, lineage_id, content_address); + let default_view_node_idx = v4_graph.add_or_replace_node(node_weight.clone())?; + + v4_graph.add_edge( + category_node_index, + EdgeWeight::new(EdgeWeightKind::new_use_default()), + default_view_node_idx, + )?; + + default_view_node_idx + }; + + // Gather component nodes to upgrade + let mut node_ids_to_upgrade = Vec::new(); + for (node_weight, _) in v4_graph.nodes() { + if let NodeWeight::Component(content) = node_weight { + node_ids_to_upgrade.push(content.id()); + } + } + + for node_id in node_ids_to_upgrade { + let old_node_weight = v4_graph + .get_node_weight(v4_graph.get_node_index_by_id(node_id)?)? + .clone(); + if let NodeWeight::Component(content) = old_node_weight { + ComponentNodeWeight::try_upgrade_and_create_external_geometry( + ctx, + &mut v4_graph, + default_view_idx, + &content, + ) + .await?; + } + } + + Ok(v4_graph) +} diff --git a/lib/dal/src/workspace_snapshot/node_weight.rs b/lib/dal/src/workspace_snapshot/node_weight.rs index 45f69ad26b..dad0d92f74 100644 --- a/lib/dal/src/workspace_snapshot/node_weight.rs +++ b/lib/dal/src/workspace_snapshot/node_weight.rs @@ -18,10 +18,20 @@ use crate::{ vector_clock::VectorClockError, }, ChangeSetId, EdgeWeightKindDiscriminants, PropKind, SocketArity, WorkspaceSnapshotError, - WorkspaceSnapshotGraphV3, + WorkspaceSnapshotGraphVCurrent, }; -use traits::SiNodeWeight; +use self::{ + input_socket_node_weight::InputSocketNodeWeightError, + schema_variant_node_weight::SchemaVariantNodeWeightError, +}; +use super::graph::{ + deprecated::v1::DeprecatedNodeWeightV1, detect_updates::Update, WorkspaceSnapshotGraphError, +}; +use crate::layer_db_types::ComponentContentDiscriminants; +use crate::workspace_snapshot::node_weight::geometry_node_weight::GeometryNodeWeight; +use crate::workspace_snapshot::node_weight::traits::SiVersionedNodeWeight; +use crate::workspace_snapshot::node_weight::view_node_weight::ViewNodeWeight; pub use action_node_weight::ActionNodeWeight; pub use action_prototype_node_weight::ActionPrototypeNodeWeight; pub use attribute_prototype_argument_node_weight::{ @@ -40,13 +50,6 @@ pub use ordering_node_weight::OrderingNodeWeight; pub use prop_node_weight::PropNodeWeight; pub use schema_variant_node_weight::SchemaVariantNodeWeight; -use self::{ - input_socket_node_weight::InputSocketNodeWeightError, - schema_variant_node_weight::SchemaVariantNodeWeightError, -}; - -use super::graph::{deprecated::v1::DeprecatedNodeWeightV1, detect_updates::Update}; - pub mod action_node_weight; pub mod action_prototype_node_weight; pub mod attribute_prototype_argument_node_weight; @@ -58,12 +61,14 @@ pub mod dependent_value_root_node_weight; pub mod finished_dependent_value_root_node_weight; pub mod func_argument_node_weight; pub mod func_node_weight; +pub mod geometry_node_weight; pub mod input_socket_node_weight; pub mod management_prototype_node_weight; pub mod ordering_node_weight; pub mod prop_node_weight; pub mod schema_variant_node_weight; pub mod secret_node_weight; +pub mod view_node_weight; pub mod traits; @@ -91,11 +96,13 @@ pub enum NodeWeightError { #[error("Content missing from store for node: {0}")] MissingContentFromStore(Ulid), #[error("Missing Key for Child Entry {0}")] - MissingKeytForChildEntry(Ulid), + MissingKeyForChildEntry(Ulid), #[error("SchemaVariant node weight error: {0}")] SchemaVariantNodeWeight(#[from] Box), #[error("try from int error: {0}")] TryFromIntError(#[from] TryFromIntError), + #[error("Unexpected content version. Got {1} but expected {0}")] + UnexpectedComponentContentVersion(ComponentContentDiscriminants, ComponentContentDiscriminants), #[error("Unexpected content address variant: {1} expected {0}")] UnexpectedContentAddressVariant(ContentAddressDiscriminants, ContentAddressDiscriminants), #[error("Unexpected node weight variant. Got {1} but expected {0}")] @@ -104,6 +111,8 @@ pub enum NodeWeightError { VectorClock(#[from] VectorClockError), #[error("WorkspaceSnapshot error: {0}")] WorkspaceSnapshot(#[from] Box), + #[error("WorkspaceSnapshotGraph error: {0}")] + WorkspaceSnapshotGraph(#[from] Box), } pub type NodeWeightResult = Result; @@ -130,6 +139,8 @@ pub enum NodeWeight { InputSocket(InputSocketNodeWeight), SchemaVariant(SchemaVariantNodeWeight), ManagementPrototype(ManagementPrototypeNodeWeight), + Geometry(GeometryNodeWeight), + View(ViewNodeWeight), } impl NodeWeight { @@ -152,6 +163,8 @@ impl NodeWeight { NodeWeight::InputSocket(weight) => weight.content_hash(), NodeWeight::SchemaVariant(weight) => weight.content_hash(), NodeWeight::ManagementPrototype(weight) => weight.content_hash(), + NodeWeight::Geometry(w) => w.content_hash(), + NodeWeight::View(w) => w.content_hash(), } } @@ -174,6 +187,8 @@ impl NodeWeight { NodeWeight::InputSocket(weight) => weight.content_store_hashes(), NodeWeight::SchemaVariant(weight) => weight.content_store_hashes(), NodeWeight::ManagementPrototype(weight) => weight.content_store_hashes(), + NodeWeight::Geometry(weight) => weight.content_store_hashes(), + NodeWeight::View(weight) => weight.content_store_hashes(), } } @@ -188,6 +203,7 @@ impl NodeWeight { | NodeWeight::Component(_) | NodeWeight::Func(_) | NodeWeight::FuncArgument(_) + | NodeWeight::Geometry(_) | NodeWeight::Ordering(_) | NodeWeight::Prop(_) | NodeWeight::Secret(_) @@ -195,6 +211,7 @@ impl NodeWeight { | NodeWeight::FinishedDependentValueRoot(_) | NodeWeight::InputSocket(_) | NodeWeight::ManagementPrototype(_) + | NodeWeight::View(_) | NodeWeight::SchemaVariant(_) => None, } } @@ -218,6 +235,8 @@ impl NodeWeight { NodeWeight::InputSocket(weight) => weight.id(), NodeWeight::SchemaVariant(weight) => weight.id(), NodeWeight::ManagementPrototype(weight) => weight.id(), + NodeWeight::Geometry(weight) => weight.id(), + NodeWeight::View(weight) => weight.id(), } } @@ -240,6 +259,8 @@ impl NodeWeight { NodeWeight::InputSocket(weight) => weight.lineage_id(), NodeWeight::SchemaVariant(weight) => weight.lineage_id(), NodeWeight::ManagementPrototype(weight) => weight.lineage_id(), + NodeWeight::Geometry(weight) => weight.lineage_id(), + NodeWeight::View(weight) => weight.lineage_id(), } } @@ -313,6 +334,14 @@ impl NodeWeight { weight.set_id(id.into()); weight.set_lineage_id(lineage_id); } + NodeWeight::Geometry(weight) => { + weight.set_id(id.into()); + weight.set_lineage_id(lineage_id); + } + NodeWeight::View(weight) => { + weight.set_id(id.into()); + weight.set_lineage_id(lineage_id); + } } } @@ -335,6 +364,8 @@ impl NodeWeight { NodeWeight::InputSocket(weight) => weight.merkle_tree_hash(), NodeWeight::SchemaVariant(weight) => weight.merkle_tree_hash(), NodeWeight::ManagementPrototype(weight) => weight.merkle_tree_hash(), + NodeWeight::Geometry(w) => w.merkle_tree_hash(), + NodeWeight::View(w) => w.merkle_tree_hash(), } } @@ -366,6 +397,14 @@ impl NodeWeight { | NodeWeight::DependentValueRoot(_) | NodeWeight::FinishedDependentValueRoot(_) | NodeWeight::Ordering(_) => Err(NodeWeightError::CannotSetContentHashOnKind), + NodeWeight::Geometry(w) => { + traits::SiVersionedNodeWeight::inner_mut(w).new_content_hash(content_hash); + Ok(()) + } + NodeWeight::View(w) => { + traits::SiVersionedNodeWeight::inner_mut(w).new_content_hash(content_hash); + Ok(()) + } } } @@ -391,6 +430,8 @@ impl NodeWeight { NodeWeight::InputSocket(weight) => weight.node_hash(), NodeWeight::SchemaVariant(weight) => weight.node_hash(), NodeWeight::ManagementPrototype(weight) => weight.node_hash(), + NodeWeight::Geometry(weight) => weight.node_hash(), + NodeWeight::View(weight) => weight.node_hash(), } } @@ -413,6 +454,8 @@ impl NodeWeight { NodeWeight::InputSocket(weight) => weight.set_merkle_tree_hash(new_hash), NodeWeight::SchemaVariant(weight) => weight.set_merkle_tree_hash(new_hash), NodeWeight::ManagementPrototype(weight) => weight.set_merkle_tree_hash(new_hash), + NodeWeight::Geometry(weight) => weight.set_merkle_tree_hash(new_hash), + NodeWeight::View(weight) => weight.set_merkle_tree_hash(new_hash), } } @@ -433,6 +476,8 @@ impl NodeWeight { | NodeWeight::FinishedDependentValueRoot(_) | NodeWeight::Func(_) | NodeWeight::FuncArgument(_) + | NodeWeight::Geometry(_) + | NodeWeight::View(_) | NodeWeight::Prop(_) | NodeWeight::Secret(_) | NodeWeight::InputSocket(_) @@ -462,6 +507,8 @@ impl NodeWeight { NodeWeight::Content(weight) => weight.exclusive_outgoing_edges(), NodeWeight::Func(weight) => weight.exclusive_outgoing_edges(), NodeWeight::FuncArgument(weight) => weight.exclusive_outgoing_edges(), + NodeWeight::Geometry(weight) => weight.exclusive_outgoing_edges(), + NodeWeight::View(weight) => weight.exclusive_outgoing_edges(), NodeWeight::Ordering(weight) => weight.exclusive_outgoing_edges(), NodeWeight::Prop(weight) => weight.exclusive_outgoing_edges(), NodeWeight::Secret(weight) => weight.exclusive_outgoing_edges(), @@ -541,6 +588,26 @@ impl NodeWeight { } } + pub fn get_geometry_node_weight(&self) -> NodeWeightResult { + match self { + NodeWeight::Geometry(inner) => Ok(inner.to_owned()), + other => Err(NodeWeightError::UnexpectedNodeWeightVariant( + NodeWeightDiscriminants::Geometry, + other.into(), + )), + } + } + + pub fn get_view_node_weight(&self) -> NodeWeightResult { + match self { + NodeWeight::View(inner) => Ok(inner.to_owned()), + other => Err(NodeWeightError::UnexpectedNodeWeightVariant( + NodeWeightDiscriminants::View, + other.into(), + )), + } + } + pub fn get_prop_node_weight(&self) -> NodeWeightResult { match self { NodeWeight::Prop(inner) => Ok(inner.to_owned()), @@ -737,6 +804,18 @@ impl NodeWeight { )) } + pub fn new_geometry(geometry_id: Ulid, lineage_id: Ulid, content_hash: ContentHash) -> Self { + NodeWeight::Geometry(GeometryNodeWeight::new( + geometry_id, + lineage_id, + content_hash, + )) + } + + pub fn new_view(view_id: Ulid, lineage_id: Ulid, content_hash: ContentHash) -> Self { + NodeWeight::View(ViewNodeWeight::new(view_id, lineage_id, content_hash)) + } + pub fn new_prop( prop_id: Ulid, lineage_id: Ulid, @@ -865,7 +944,7 @@ impl From for NodeWeight { impl CorrectTransforms for NodeWeight { fn correct_transforms( &self, - workspace_snapshot_graph: &WorkspaceSnapshotGraphV3, + workspace_snapshot_graph: &WorkspaceSnapshotGraphVCurrent, updates: Vec, from_different_change_set: bool, ) -> CorrectTransformsResult> { @@ -955,6 +1034,16 @@ impl CorrectTransforms for NodeWeight { updates, from_different_change_set, ), + NodeWeight::Geometry(weight) => weight.correct_transforms( + workspace_snapshot_graph, + updates, + from_different_change_set, + ), + NodeWeight::View(weight) => weight.correct_transforms( + workspace_snapshot_graph, + updates, + from_different_change_set, + ), }?; Ok(self.correct_exclusive_outgoing_edges(workspace_snapshot_graph, updates)) @@ -981,6 +1070,8 @@ impl CorrectExclusiveOutgoingEdge for NodeWeight { NodeWeight::InputSocket(weight) => weight.exclusive_outgoing_edges(), NodeWeight::SchemaVariant(weight) => weight.exclusive_outgoing_edges(), NodeWeight::ManagementPrototype(weight) => weight.exclusive_outgoing_edges(), + NodeWeight::Geometry(weight) => weight.exclusive_outgoing_edges(), + NodeWeight::View(weight) => weight.exclusive_outgoing_edges(), } } } diff --git a/lib/dal/src/workspace_snapshot/node_weight/category_node_weight.rs b/lib/dal/src/workspace_snapshot/node_weight/category_node_weight.rs index 04054a313e..4ff6fa0803 100644 --- a/lib/dal/src/workspace_snapshot/node_weight/category_node_weight.rs +++ b/lib/dal/src/workspace_snapshot/node_weight/category_node_weight.rs @@ -23,6 +23,8 @@ pub enum CategoryNodeKind { Schema, Secret, DependentValueRoots, + View, + DiagramObject, } #[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] diff --git a/lib/dal/src/workspace_snapshot/node_weight/component_node_weight.rs b/lib/dal/src/workspace_snapshot/node_weight/component_node_weight.rs index 5540b0b828..898e9c50b0 100644 --- a/lib/dal/src/workspace_snapshot/node_weight/component_node_weight.rs +++ b/lib/dal/src/workspace_snapshot/node_weight/component_node_weight.rs @@ -1,9 +1,15 @@ -use std::collections::HashSet; - -use petgraph::{prelude::*, visit::EdgeRef, Direction::Incoming}; -use serde::{Deserialize, Serialize}; -use si_events::{merkle_tree_hash::MerkleTreeHash, ulid::Ulid, ContentHash}; - +use super::{ + category_node_weight::CategoryNodeKind, + traits::CorrectTransformsResult, + NodeWeight, + NodeWeightDiscriminants::{self, Component}, + NodeWeightError, NodeWeightResult, +}; +use crate::layer_db_types::{ + ComponentContent, ComponentContentDiscriminants, ComponentContentV2, GeometryContent, + GeometryContentV1, +}; +use crate::workspace_snapshot::graph::WorkspaceSnapshotGraphV4; use crate::{ workspace_snapshot::{ content_address::{ContentAddress, ContentAddressDiscriminants}, @@ -14,16 +20,14 @@ use crate::{ node_weight::traits::CorrectTransforms, NodeInformation, }, - EdgeWeightKindDiscriminants, WorkspaceSnapshotGraphVCurrent, -}; - -use super::{ - category_node_weight::CategoryNodeKind, - traits::CorrectTransformsResult, - NodeWeight, - NodeWeightDiscriminants::{self, Component}, - NodeWeightError, NodeWeightResult, + DalContext, EdgeWeight, EdgeWeightKind, EdgeWeightKindDiscriminants, Timestamp, + WorkspaceSnapshotGraphVCurrent, }; +use petgraph::{prelude::*, visit::EdgeRef, Direction::Incoming}; +use serde::{Deserialize, Serialize}; +use si_events::{merkle_tree_hash::MerkleTreeHash, ulid::Ulid, ContentHash}; +use std::collections::HashSet; +use std::sync::Arc; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ComponentNodeWeight { @@ -120,6 +124,112 @@ impl ComponentNodeWeight { EdgeWeightKindDiscriminants::Root, ] } + + pub(crate) async fn try_upgrade_and_create_external_geometry( + ctx: &DalContext, + v4_graph: &mut WorkspaceSnapshotGraphV4, + default_view_idx: NodeIndex, + component_node_weight: &ComponentNodeWeight, + ) -> NodeWeightResult<()> { + let component_idx = v4_graph + .get_node_index_by_id(component_node_weight.id) + .map_err(|e| NodeWeightError::WorkspaceSnapshotGraph(Box::new(e)))?; + + let layer_db = ctx.layer_db(); + let cas = layer_db.cas(); + + let component_content: ComponentContent = cas + .try_read_as(&component_node_weight.content_hash()) + .await? + .ok_or_else(|| NodeWeightError::MissingContentFromStore(component_node_weight.id))?; + + // When migrating from graph v3 to v4 all components should be on v1 + let ComponentContent::V1(content) = component_content.clone() else { + let actual = ComponentContentDiscriminants::from(component_content); + return Err(NodeWeightError::UnexpectedComponentContentVersion( + actual, + ComponentContentDiscriminants::V1, + )); + }; + + // Create geometry node + let geometry_content = GeometryContent::V1(GeometryContentV1 { + timestamp: Timestamp::now(), + x: content.x, + y: content.y, + width: content.width, + height: content.height, + }); + + let (content_address, _) = cas + .write( + Arc::new(geometry_content.clone().into()), + None, + ctx.events_tenancy(), + ctx.events_actor(), + ) + .await?; + + let geometry_id = v4_graph + .generate_ulid() + .map_err(|e| NodeWeightError::WorkspaceSnapshotGraph(Box::new(e)))?; + + let geometry_node_weight = NodeWeight::new_geometry( + geometry_id, + v4_graph + .generate_ulid() + .map_err(|e| NodeWeightError::WorkspaceSnapshotGraph(Box::new(e)))?, + content_address, + ); + + let geometry_idx = v4_graph + .add_or_replace_node(geometry_node_weight) + .map_err(|e| NodeWeightError::WorkspaceSnapshotGraph(Box::new(e)))?; + + // Connect geometry to component + v4_graph + .add_edge( + geometry_idx, + EdgeWeight::new(EdgeWeightKind::Represents), + component_idx, + ) + .map_err(|e| NodeWeightError::WorkspaceSnapshotGraph(Box::new(e)))?; + + // Connect geometry to default view + v4_graph + .add_edge( + default_view_idx, + EdgeWeight::new(EdgeWeightKind::new_use()), + geometry_idx, + ) + .map_err(|e| NodeWeightError::WorkspaceSnapshotGraph(Box::new(e)))?; + + // Upgrade component content + let updated_content = ComponentContent::V2(ComponentContentV2 { + timestamp: content.timestamp, + }); + + let (updated_content_address, _) = cas + .write( + Arc::new(updated_content.into()), + None, + ctx.events_tenancy(), + ctx.events_actor(), + ) + .await?; + + let upgraded_node_weight = NodeWeight::new_component( + component_node_weight.id, + component_node_weight.lineage_id, + updated_content_address, + ); + + v4_graph + .add_or_replace_node(upgraded_node_weight) + .map_err(|e| NodeWeightError::WorkspaceSnapshotGraph(Box::new(e)))?; + + Ok(()) + } } impl From for ComponentNodeWeight { diff --git a/lib/dal/src/workspace_snapshot/node_weight/content_node_weight.rs b/lib/dal/src/workspace_snapshot/node_weight/content_node_weight.rs index 8a68b608c0..95268b1b69 100644 --- a/lib/dal/src/workspace_snapshot/node_weight/content_node_weight.rs +++ b/lib/dal/src/workspace_snapshot/node_weight/content_node_weight.rs @@ -12,7 +12,7 @@ use crate::{ node_weight::{traits::CorrectTransforms, NodeWeightError, NodeWeightResult}, NodeInformation, }, - ComponentId, EdgeWeightKindDiscriminants, SocketArity, WorkspaceSnapshotGraphV3, + ComponentId, EdgeWeightKindDiscriminants, SocketArity, WorkspaceSnapshotGraphVCurrent, }; use super::{ @@ -103,6 +103,12 @@ impl ContentNodeWeight { ContentAddress::OutputSocket(_) => ContentAddress::OutputSocket(content_hash), ContentAddress::FuncArg(_) => ContentAddress::FuncArg(content_hash), ContentAddress::Func(_) => ContentAddress::Func(content_hash), + ContentAddress::Geometry(_) => { + return Err(NodeWeightError::InvalidContentAddressForWeightKind( + "Geometry".to_string(), + "Content".to_string(), + )); + } ContentAddress::InputSocket(_) => { return Err(NodeWeightError::InvalidContentAddressForWeightKind( "InputSocket".to_string(), @@ -141,6 +147,12 @@ impl ContentNodeWeight { ContentAddress::ManagementPrototype(_) => { ContentAddress::ManagementPrototype(content_hash) } + ContentAddress::View(_) => { + return Err(NodeWeightError::InvalidContentAddressForWeightKind( + "Geometry".to_string(), + "Content".to_string(), + )); + } }; self.content_address = new_address; @@ -189,7 +201,7 @@ impl From for ContentNodeWeight { } fn remove_outgoing_prototype_argument_value_targets_to_dest( - graph: &WorkspaceSnapshotGraphV3, + graph: &WorkspaceSnapshotGraphVCurrent, prototype_idx: NodeIndex, source: NodeInformation, destination_component_id: ComponentId, @@ -220,7 +232,7 @@ fn remove_outgoing_prototype_argument_value_targets_to_dest( } fn protect_arity_for_input_socket( - graph: &WorkspaceSnapshotGraphV3, + graph: &WorkspaceSnapshotGraphVCurrent, mut updates: Vec, self_node: &ContentNodeWeight, ) -> Vec { @@ -295,7 +307,7 @@ fn protect_arity_for_input_socket( impl CorrectTransforms for ContentNodeWeight { fn correct_transforms( &self, - graph: &WorkspaceSnapshotGraphV3, + graph: &WorkspaceSnapshotGraphVCurrent, updates: Vec, _from_different_change_set: bool, ) -> CorrectTransformsResult> { diff --git a/lib/dal/src/workspace_snapshot/node_weight/geometry_node_weight.rs b/lib/dal/src/workspace_snapshot/node_weight/geometry_node_weight.rs new file mode 100644 index 0000000000..7dcc441161 --- /dev/null +++ b/lib/dal/src/workspace_snapshot/node_weight/geometry_node_weight.rs @@ -0,0 +1,22 @@ +mod v1; + +use super::NodeWeightDiscriminants; +use crate::workspace_snapshot::node_weight::geometry_node_weight::v1::GeometryNodeWeightV1; +use crate::workspace_snapshot::node_weight::traits::SiVersionedNodeWeight; +use serde::{Deserialize, Serialize}; +use si_events::ulid::Ulid; +use si_events::ContentHash; + +#[derive( + Debug, Clone, Serialize, Deserialize, PartialEq, Eq, dal_macros::SiVersionedNodeWeight, +)] +pub enum GeometryNodeWeight { + #[si_versioned_node_weight(current)] + V1(GeometryNodeWeightV1), +} + +impl GeometryNodeWeight { + pub fn new(id: Ulid, lineage_id: Ulid, content_hash: ContentHash) -> Self { + Self::V1(GeometryNodeWeightV1::new(id, lineage_id, content_hash)) + } +} diff --git a/lib/dal/src/workspace_snapshot/node_weight/geometry_node_weight/v1.rs b/lib/dal/src/workspace_snapshot/node_weight/geometry_node_weight/v1.rs new file mode 100644 index 0000000000..f198a4c118 --- /dev/null +++ b/lib/dal/src/workspace_snapshot/node_weight/geometry_node_weight/v1.rs @@ -0,0 +1,48 @@ +use super::NodeWeightDiscriminants; +use crate::workspace_snapshot::{ + content_address::ContentAddress, + graph::LineageId, + node_weight::traits::{CorrectExclusiveOutgoingEdge, CorrectTransforms, SiNodeWeight}, +}; + +use crate::{EdgeWeightKindDiscriminants, Timestamp}; +use dal_macros::SiNodeWeight; +use jwt_simple::prelude::{Deserialize, Serialize}; +use si_events::merkle_tree_hash::MerkleTreeHash; +use si_events::ulid::Ulid; +use si_events::ContentHash; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, SiNodeWeight)] +#[si_node_weight(discriminant = NodeWeightDiscriminants::Geometry)] +pub struct GeometryNodeWeightV1 { + pub id: Ulid, + pub lineage_id: LineageId, + merkle_tree_hash: MerkleTreeHash, + #[si_node_weight(node_hash = "self.content_address.content_hash().as_bytes()")] + content_address: ContentAddress, + timestamp: Timestamp, +} + +impl GeometryNodeWeightV1 { + pub fn new(id: Ulid, lineage_id: Ulid, content_hash: ContentHash) -> Self { + Self { + id, + lineage_id, + content_address: ContentAddress::Geometry(content_hash), + merkle_tree_hash: MerkleTreeHash::default(), + timestamp: Timestamp::now(), + } + } + + pub fn new_content_hash(&mut self, new_content_hash: ContentHash) { + self.content_address = ContentAddress::Geometry(new_content_hash); + } +} + +impl CorrectTransforms for GeometryNodeWeightV1 {} + +impl CorrectExclusiveOutgoingEdge for GeometryNodeWeightV1 { + fn exclusive_outgoing_edges(&self) -> &[EdgeWeightKindDiscriminants] { + &[EdgeWeightKindDiscriminants::Represents] + } +} diff --git a/lib/dal/src/workspace_snapshot/node_weight/input_socket_node_weight.rs b/lib/dal/src/workspace_snapshot/node_weight/input_socket_node_weight.rs index d09476db1e..23521eb04a 100644 --- a/lib/dal/src/workspace_snapshot/node_weight/input_socket_node_weight.rs +++ b/lib/dal/src/workspace_snapshot/node_weight/input_socket_node_weight.rs @@ -6,11 +6,12 @@ use thiserror::Error; use super::{traits::SiVersionedNodeWeight, ContentNodeWeight, NodeWeightError}; use crate::{ workspace_snapshot::graph::WorkspaceSnapshotGraphError, DalContext, SocketArity, - WorkspaceSnapshotError, WorkspaceSnapshotGraphV3, + WorkspaceSnapshotError, }; pub mod v1; +use crate::workspace_snapshot::graph::WorkspaceSnapshotGraphV3; pub use v1::InputSocketNodeWeightV1; #[remain::sorted] diff --git a/lib/dal/src/workspace_snapshot/node_weight/input_socket_node_weight/v1.rs b/lib/dal/src/workspace_snapshot/node_weight/input_socket_node_weight/v1.rs index 1f09dc5c38..ba2446e0d6 100644 --- a/lib/dal/src/workspace_snapshot/node_weight/input_socket_node_weight/v1.rs +++ b/lib/dal/src/workspace_snapshot/node_weight/input_socket_node_weight/v1.rs @@ -3,6 +3,8 @@ use std::sync::Arc; use serde::{Deserialize, Serialize}; use si_events::{merkle_tree_hash::MerkleTreeHash, ulid::Ulid, ContentHash}; +use super::{InputSocketNodeWeight, InputSocketNodeWeightError, InputSocketNodeWeightResult}; +use crate::workspace_snapshot::graph::WorkspaceSnapshotGraphV3; use crate::{ layer_db_types::{InputSocketContent, InputSocketContentV2}, workspace_snapshot::{ @@ -14,11 +16,9 @@ use crate::{ }, ContentAddressDiscriminants, }, - DalContext, EdgeWeightKindDiscriminants, SocketArity, Timestamp, WorkspaceSnapshotGraphV3, + DalContext, EdgeWeightKindDiscriminants, SocketArity, Timestamp, }; -use super::{InputSocketNodeWeight, InputSocketNodeWeightError, InputSocketNodeWeightResult}; - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, dal_macros::SiNodeWeight)] #[si_node_weight(discriminant = NodeWeightDiscriminants::InputSocket)] pub struct InputSocketNodeWeightV1 { diff --git a/lib/dal/src/workspace_snapshot/node_weight/ordering_node_weight.rs b/lib/dal/src/workspace_snapshot/node_weight/ordering_node_weight.rs index dff546bc87..76bf2928a7 100644 --- a/lib/dal/src/workspace_snapshot/node_weight/ordering_node_weight.rs +++ b/lib/dal/src/workspace_snapshot/node_weight/ordering_node_weight.rs @@ -89,7 +89,7 @@ impl OrderingNodeWeight { .order .iter() .position(|&key| key == id) - .ok_or(NodeWeightError::MissingKeytForChildEntry(id))?; + .ok_or(NodeWeightError::MissingKeyForChildEntry(id))?; let ret: i64 = (*index) .try_into() diff --git a/lib/dal/src/workspace_snapshot/node_weight/schema_variant_node_weight.rs b/lib/dal/src/workspace_snapshot/node_weight/schema_variant_node_weight.rs index 24d3319f2d..69eab86774 100644 --- a/lib/dal/src/workspace_snapshot/node_weight/schema_variant_node_weight.rs +++ b/lib/dal/src/workspace_snapshot/node_weight/schema_variant_node_weight.rs @@ -6,11 +6,11 @@ use thiserror::Error; use super::{traits::SiVersionedNodeWeight, ContentNodeWeight, NodeWeightError}; use crate::{ workspace_snapshot::graph::WorkspaceSnapshotGraphError, DalContext, WorkspaceSnapshotError, - WorkspaceSnapshotGraphV3, }; pub mod v1; +use crate::workspace_snapshot::graph::WorkspaceSnapshotGraphV3; pub use v1::SchemaVariantNodeWeightV1; #[remain::sorted] diff --git a/lib/dal/src/workspace_snapshot/node_weight/schema_variant_node_weight/v1.rs b/lib/dal/src/workspace_snapshot/node_weight/schema_variant_node_weight/v1.rs index f6ac6f9fc9..59313f8a83 100644 --- a/lib/dal/src/workspace_snapshot/node_weight/schema_variant_node_weight/v1.rs +++ b/lib/dal/src/workspace_snapshot/node_weight/schema_variant_node_weight/v1.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; use si_events::{merkle_tree_hash::MerkleTreeHash, ulid::Ulid, ContentHash}; use super::{SchemaVariantNodeWeight, SchemaVariantNodeWeightError, SchemaVariantNodeWeightResult}; +use crate::workspace_snapshot::graph::WorkspaceSnapshotGraphV3; use crate::{ layer_db_types::{SchemaVariantContent, SchemaVariantContentV3}, workspace_snapshot::{ @@ -20,7 +21,7 @@ use crate::{ ContentAddressDiscriminants, }, DalContext, EdgeWeightKindDiscriminants, SchemaId, SchemaVariantError, SchemaVariantId, - Timestamp, WorkspaceSnapshotGraphV3, WorkspaceSnapshotGraphVCurrent, + Timestamp, WorkspaceSnapshotGraphVCurrent, }; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, dal_macros::SiNodeWeight)] diff --git a/lib/dal/src/workspace_snapshot/node_weight/traits.rs b/lib/dal/src/workspace_snapshot/node_weight/traits.rs index c3c37558e6..27734cf38c 100644 --- a/lib/dal/src/workspace_snapshot/node_weight/traits.rs +++ b/lib/dal/src/workspace_snapshot/node_weight/traits.rs @@ -3,7 +3,7 @@ use crate::{ graph::{detect_updates::Update, WorkspaceSnapshotGraphError}, NodeInformation, }, - EdgeWeightKindDiscriminants, WorkspaceSnapshotGraphV3, + EdgeWeightKindDiscriminants, WorkspaceSnapshotGraphVCurrent, }; use si_events::{merkle_tree_hash::MerkleTreeHash, ulid::Ulid, ContentHash}; use thiserror::Error; @@ -29,7 +29,7 @@ pub type CorrectTransformsResult = Result; pub trait CorrectTransforms { fn correct_transforms( &self, - _workspace_snapshot_graph: &WorkspaceSnapshotGraphV3, + _workspace_snapshot_graph: &WorkspaceSnapshotGraphVCurrent, updates: Vec, _from_different_change_set: bool, ) -> CorrectTransformsResult> { @@ -154,7 +154,7 @@ where { fn correct_transforms( &self, - workspace_snapshot_graph: &WorkspaceSnapshotGraphV3, + workspace_snapshot_graph: &WorkspaceSnapshotGraphVCurrent, updates: Vec, from_different_change_set: bool, ) -> CorrectTransformsResult> { diff --git a/lib/dal/src/workspace_snapshot/node_weight/traits/correct_exclusive_outgoing_edge.rs b/lib/dal/src/workspace_snapshot/node_weight/traits/correct_exclusive_outgoing_edge.rs index 3320f188ca..da46f51c76 100644 --- a/lib/dal/src/workspace_snapshot/node_weight/traits/correct_exclusive_outgoing_edge.rs +++ b/lib/dal/src/workspace_snapshot/node_weight/traits/correct_exclusive_outgoing_edge.rs @@ -6,7 +6,7 @@ use crate::{ workspace_snapshot::{ edge_weight::EdgeWeightKindDiscriminants, graph::detect_updates::Update, NodeInformation, }, - WorkspaceSnapshotGraphV3, + WorkspaceSnapshotGraphVCurrent, }; pub trait CorrectExclusiveOutgoingEdge @@ -25,7 +25,7 @@ where /// outgoing NewEdge update for the exclusive edge) fn correct_exclusive_outgoing_edges( &self, - graph: &WorkspaceSnapshotGraphV3, + graph: &WorkspaceSnapshotGraphVCurrent, mut updates: Vec, ) -> Vec { let exclusive_edge_kinds = self.exclusive_outgoing_edges(); diff --git a/lib/dal/src/workspace_snapshot/node_weight/view_node_weight.rs b/lib/dal/src/workspace_snapshot/node_weight/view_node_weight.rs new file mode 100644 index 0000000000..6c2ce6f5c4 --- /dev/null +++ b/lib/dal/src/workspace_snapshot/node_weight/view_node_weight.rs @@ -0,0 +1,22 @@ +mod v1; + +use super::NodeWeightDiscriminants; +use crate::workspace_snapshot::node_weight::traits::SiVersionedNodeWeight; +use crate::workspace_snapshot::node_weight::view_node_weight::v1::ViewNodeWeightV1; +use serde::{Deserialize, Serialize}; +use si_events::ulid::Ulid; +use si_events::ContentHash; + +#[derive( + Debug, Clone, Serialize, Deserialize, PartialEq, Eq, dal_macros::SiVersionedNodeWeight, +)] +pub enum ViewNodeWeight { + #[si_versioned_node_weight(current)] + V1(ViewNodeWeightV1), +} + +impl ViewNodeWeight { + pub fn new(id: Ulid, lineage_id: Ulid, content_hash: ContentHash) -> Self { + Self::V1(ViewNodeWeightV1::new(id, lineage_id, content_hash)) + } +} diff --git a/lib/dal/src/workspace_snapshot/node_weight/view_node_weight/v1.rs b/lib/dal/src/workspace_snapshot/node_weight/view_node_weight/v1.rs new file mode 100644 index 0000000000..73be71d046 --- /dev/null +++ b/lib/dal/src/workspace_snapshot/node_weight/view_node_weight/v1.rs @@ -0,0 +1,48 @@ +use super::NodeWeightDiscriminants; +use crate::workspace_snapshot::{ + content_address::ContentAddress, + graph::LineageId, + node_weight::traits::{CorrectExclusiveOutgoingEdge, CorrectTransforms, SiNodeWeight}, +}; + +use crate::{EdgeWeightKindDiscriminants, Timestamp}; +use dal_macros::SiNodeWeight; +use jwt_simple::prelude::{Deserialize, Serialize}; +use si_events::merkle_tree_hash::MerkleTreeHash; +use si_events::ulid::Ulid; +use si_events::ContentHash; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, SiNodeWeight)] +#[si_node_weight(discriminant = NodeWeightDiscriminants::View)] +pub struct ViewNodeWeightV1 { + pub id: Ulid, + pub lineage_id: LineageId, + merkle_tree_hash: MerkleTreeHash, + #[si_node_weight(node_hash = "self.content_address.content_hash().as_bytes()")] + content_address: ContentAddress, + timestamp: Timestamp, +} + +impl ViewNodeWeightV1 { + pub fn new(id: Ulid, lineage_id: Ulid, content_hash: ContentHash) -> Self { + Self { + id, + lineage_id, + content_address: ContentAddress::View(content_hash), + merkle_tree_hash: MerkleTreeHash::default(), + timestamp: Timestamp::now(), + } + } + + pub fn new_content_hash(&mut self, new_content_hash: ContentHash) { + self.content_address = ContentAddress::View(new_content_hash); + } +} + +impl CorrectTransforms for ViewNodeWeightV1 {} + +impl CorrectExclusiveOutgoingEdge for ViewNodeWeightV1 { + fn exclusive_outgoing_edges(&self) -> &[EdgeWeightKindDiscriminants] { + &[] + } +} diff --git a/lib/dal/tests/integration_test/component.rs b/lib/dal/tests/integration_test/component.rs index 587e843fc1..27a7e004d8 100644 --- a/lib/dal/tests/integration_test/component.rs +++ b/lib/dal/tests/integration_test/component.rs @@ -1,5 +1,4 @@ use dal::attribute::value::DependentValueGraph; -use dal::component::{DEFAULT_COMPONENT_HEIGHT, DEFAULT_COMPONENT_WIDTH}; use dal::diagram::Diagram; use dal::prop::{Prop, PropPath}; use dal::property_editor::values::PropertyEditorValues; @@ -189,13 +188,7 @@ async fn create_and_determine_lineage(ctx: &DalContext) { .await .expect("could not create component"); component - .set_geometry( - ctx, - "1", - "-1", - Some(DEFAULT_COMPONENT_WIDTH), - Some(DEFAULT_COMPONENT_HEIGHT), - ) + .set_geometry(ctx, "1", "-1", Some("500"), Some("500")) .await .expect("could not set geometry"); diff --git a/lib/dal/tests/integration_test/deserialize/mod.rs b/lib/dal/tests/integration_test/deserialize/mod.rs index f5e453beb9..f44761ac4c 100644 --- a/lib/dal/tests/integration_test/deserialize/mod.rs +++ b/lib/dal/tests/integration_test/deserialize/mod.rs @@ -19,7 +19,7 @@ use si_layer_cache::db::serialize; use strum::IntoEnumIterator; const CURRENT_SERIALIZED_GRAPH_DIR_PATH: &str = "./lib/dal/tests"; -const CURRENT_SERIALIZED_GRAPH_FILENAME: &str = "serialization-test-data-2024-10-14.snapshot"; +const CURRENT_SERIALIZED_GRAPH_FILENAME: &str = "serialization-test-data-2024-10-17.snapshot"; // If you're modifying this, you probably just added a new node or edge weight. Before you replace // the snapshot with one that includes the new weights, ensure that your current code passes the @@ -131,6 +131,16 @@ fn make_me_one_with_everything(graph: &mut WorkspaceSnapshotGraphVCurrent) { Ulid::new(), ContentHash::new("management".as_bytes()), ), + NodeWeightDiscriminants::Geometry => NodeWeight::new_geometry( + Ulid::new(), + Ulid::new(), + ContentHash::new("geometry".as_bytes()), + ), + NodeWeightDiscriminants::View => NodeWeight::new_view( + Ulid::new(), + Ulid::new(), + ContentHash::new("geometry".as_bytes()), + ), }; let idx = graph.add_or_replace_node(weight).expect("add node"); @@ -178,6 +188,7 @@ fn make_me_one_with_everything(graph: &mut WorkspaceSnapshotGraphVCurrent) { EdgeWeightKindDiscriminants::Use => EdgeWeightKind::new_use(), EdgeWeightKindDiscriminants::ValidationOutput => EdgeWeightKind::ValidationOutput, EdgeWeightKindDiscriminants::ManagementPrototype => EdgeWeightKind::ManagementPrototype, + EdgeWeightKindDiscriminants::Represents => EdgeWeightKind::Represents, }; let edge_weight = EdgeWeight::new(edge_weight_kind); @@ -206,14 +217,16 @@ fn make_me_one_with_everything(graph: &mut WorkspaceSnapshotGraphVCurrent) { // `CURRENT_SERIALIZED_GRAPH_FILENAME` with the filename of the new graph. #[test] #[ignore = "only run this when you want to produce a new serialized graph"] -async fn write_deserialization_data(_ctx: &DalContext) { - let mut graph = WorkspaceSnapshotGraphVCurrent::new().expect("make new"); +async fn write_deserialization_data(ctx: &DalContext) { + let mut graph = WorkspaceSnapshotGraphVCurrent::new(ctx) + .await + .expect("make new"); make_me_one_with_everything(&mut graph); graph.cleanup_and_merkle_tree_hash().expect("hash it"); - let real_graph = WorkspaceSnapshotGraph::V3(graph); + let real_graph = WorkspaceSnapshotGraph::V4(graph); let serialized = serialize::to_vec(&real_graph).expect("serialize"); let date = chrono::Utc::now().format("%Y-%m-%d").to_string(); @@ -236,7 +249,7 @@ async fn graph_can_be_deserialized(_ctx: &DalContext) { let graph: WorkspaceSnapshotGraph = serialize::from_bytes(&bytes).expect("deserialize"); - assert_eq!(18, graph.node_count()); + assert_eq!(31, graph.node_count()); // Where we can, verify that the enums on the node weights match what we expect for (node_weight, _) in graph.nodes() { @@ -247,12 +260,7 @@ async fn graph_can_be_deserialized(_ctx: &DalContext) { } NodeWeight::AttributePrototypeArgument(_) => {} NodeWeight::AttributeValue(_) => {} - NodeWeight::Category(category_node_weight) => { - assert_eq!( - CategoryNodeKind::DependentValueRoots, - category_node_weight.kind() - ); - } + NodeWeight::Category(_) => {} NodeWeight::Component(_) => {} NodeWeight::Content(_) => {} NodeWeight::DependentValueRoot(_) => {} @@ -271,6 +279,8 @@ async fn graph_can_be_deserialized(_ctx: &DalContext) { } NodeWeight::SchemaVariant(_) => {} NodeWeight::ManagementPrototype(_) => {} + NodeWeight::Geometry(_) => {} + NodeWeight::View(_) => {} } } } diff --git a/lib/dal/tests/integration_test/frame.rs b/lib/dal/tests/integration_test/frame.rs index 66b3f34c6c..1c2f100687 100644 --- a/lib/dal/tests/integration_test/frame.rs +++ b/lib/dal/tests/integration_test/frame.rs @@ -1274,42 +1274,6 @@ async fn up_frames_moving_deeply_nested_frames(ctx: &mut DalContext) { .expect("is some"); assert_eq!(input_value, serde_json::json!("1")); - { - let workspace_snapshot = ctx.workspace_snapshot().unwrap(); - let mut inferred_connection_graph = workspace_snapshot - .inferred_connection_graph(ctx) - .await - .unwrap(); - let first_child_schema_variant = - Component::schema_variant_id(ctx, first_child_component.id()) - .await - .unwrap(); - let parent_schema_variant = Component::schema_variant_id(ctx, parent_frame.id()) - .await - .unwrap(); - let first_grand_parent_schema_variant = - Component::schema_variant_id(ctx, first_grand_parent_frame.id()) - .await - .unwrap(); - dbg!( - first_child_component.id(), - SchemaVariant::list_all_sockets(ctx, first_child_schema_variant) - .await - .unwrap(), - first_grand_parent_frame.id(), - SchemaVariant::list_all_sockets(ctx, first_grand_parent_schema_variant) - .await - .unwrap(), - parent_frame.id(), - SchemaVariant::list_all_sockets(ctx, parent_schema_variant) - .await - .unwrap(), - inferred_connection_graph - .inferred_incoming_connections_for_component(ctx, parent_frame.id()) - .await - .unwrap() - ); - } // the parent is updated with 2 let input_value = get_component_input_socket_value(ctx, parent_frame.id(), "two") .await diff --git a/lib/dal/tests/integration_test/node_weight/ordering.rs b/lib/dal/tests/integration_test/node_weight/ordering.rs index d0f22533cd..52fae97ae4 100644 --- a/lib/dal/tests/integration_test/node_weight/ordering.rs +++ b/lib/dal/tests/integration_test/node_weight/ordering.rs @@ -97,7 +97,7 @@ async fn correct_transforms_removed_edges(ctx: &mut DalContext) { let change_set_3 = expected::fork_from_head_change_set(ctx).await; exposed_ports.children(ctx).await[1].remove(ctx).await; expected::commit_and_update_snapshot_to_visibility(ctx).await; - assert_eq!(json!(["1", "22"]), dbg!(exposed_ports.get(ctx).await)); + assert_eq!(json!(["1", "22"]), exposed_ports.get(ctx).await); // Apply both changesets expected::update_visibility_and_snapshot_to_visibility(ctx, change_set_2.id).await; diff --git a/lib/dal/tests/integration_test/secret.rs b/lib/dal/tests/integration_test/secret.rs index df6d1c206e..6a1d071d11 100644 --- a/lib/dal/tests/integration_test/secret.rs +++ b/lib/dal/tests/integration_test/secret.rs @@ -1,4 +1,4 @@ -use dal::component::ComponentGeometry; +use dal::diagram::geometry::RawGeometry; use dal::prop::PropPath; use dal::property_editor::values::PropertyEditorValues; use dal::qualification::QualificationSubCheckStatus; @@ -299,31 +299,45 @@ async fn copy_paste_component_with_secrets_being_used(ctx: &mut DalContext, nw: expected::commit_and_update_snapshot_to_visibility(ctx).await; // Copy and paste the secret component - secret_component + { + let component = secret_component.component(ctx).await; + + let geometry = component + .geometry(ctx) + .await + .expect("couldn't get geometry"); + + component + .copy_paste( + ctx, + RawGeometry { + x: geometry.x().to_string(), + y: geometry.y().to_string(), + width: None, + height: None, + }, + ) + .await + .expect("paste"); + } + expected::commit_and_update_snapshot_to_visibility(ctx).await; + + // Copy and paste the user component + let user_component_geometry = user_component .component(ctx) .await - .copy_paste( - ctx, - ComponentGeometry { - x: secret_component.component(ctx).await.x().to_string(), - y: secret_component.component(ctx).await.y().to_string(), - width: None, - height: None, - }, - ) + .geometry(ctx) .await - .expect("paste"); - expected::commit_and_update_snapshot_to_visibility(ctx).await; + .expect("couldn't get geometry"); - // Copy and paste the user component user_component .component(ctx) .await .copy_paste( ctx, - ComponentGeometry { - x: user_component.component(ctx).await.x().to_string(), - y: user_component.component(ctx).await.y().to_string(), + RawGeometry { + x: user_component_geometry.x().to_string(), + y: user_component_geometry.y().to_string(), width: None, height: None, }, diff --git a/lib/dal/tests/serialization-test-data-2024-10-14.snapshot b/lib/dal/tests/serialization-test-data-2024-10-14.snapshot deleted file mode 100644 index 520bdad4f84da75e60dbb87c0d6da3586fc05ffc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1474 zcmV;z1wHzOR(mv*X&nCExqWk=m(y0O4uxX1tQm7L%GM;8)w)G)XDGUG*v6i)ooZEd zN{SXyY;;*lizr7bCF@jHtxD3Ou(h>i*-Vs5S=LwY=k09k>_5*R&-?z~dA{H68Kxlz zg3MH(_I{83>jvz@3?;1{|0X&`u*3UxCO?Fc)pfOOzOLzTBtf7{b@eAJ7DdJy-PSRU zY^ClRrNYkJ#Vycjv6H7twon%2qzDKI@JE;LdOG+XZP`3LkP91%`(kAS&NWp;^K1jJ z&PnpxXi;PBuRj;(WCCXP7Q&XLpUU%vL71mfBiN<$h9o2RoI6t;$9&m)fb73aK>s ze~y_*SN6g=nt280!rG~bh`GLvnBRRyWfKf%t`3Y$f``S`i*H_Pxlxo1cyMRJCUWI6 zUCB(1^H!Uovi1K#vCu<0Fg*9w?d{8mW<;3WuYM43yKCN*l_Wft8lG-bYQ8TkB=Du~ z(?Km8-N{`r=e+5<=IBM!5pi05xLsu;iw!x=fZ?}fhn6?2Xpx$LiaVchv!Q9nISsp1 zS`jr;{PNoTl34`ZJ_=Wpg6~_i7prX0Gi*1bkrZExt;w4w!C5IWb|BW+s>+;U`6Bx9 zFubB!ni`at{3I*CeMoCOFPI=vlv_^W=-%9(Ot~Hrn%T{LPwMI}Tpfie7kCO}_d?bZQ*ccU&Hdd|1D#)!WNQ-*Fic)02KAbL*Q! z^Jb7REutsgv2--C(6Q2`vZ>1tMtUpK%KO6Hf?fn#C)PPeGQ-wJ@KK3K=n7-G8QDY)gB{p+1EWqR@nc<$b?ajQ*8z3lReCv8eQ}4!b2n z2vhyrcbHjksK~DKvWPR>lmWLqW@;I|qEN`mHr~Rn*Ta(6BJYP*KA)`&U*R#is4GK+ zUSW4hv&g~F?Hv~op?|ZZ@8osK?Qh&@Li4x|h`YOtc;MX78L?k~$2nb0 z4>;+&cj&p)+%)G53fg_PVX0N4*~gcUL-Do*-LeB-2dutK&bz>NJV9_j`1FZJPpMR^ zA4sx<+d z!vY+V4M;8raCs!)@wtE*9<~HeQT+BwQ|rMJS<|wVA~}SgZ+Vp z7`$6a#Ncs}AO<&$0b;N<1H|Cv%B7Qtjg>0UTh-V%0i86wKgZk$yuBqkM{h4crv$qV zpeuss2S6kOZ!kbt06Pw#6NQ%upfh#Xlm7Pr=nujh4A2$Fdt_r=^p>RGnyHfG4ISUu1Obu<%AiRAyQ2wn++KvTZlGNQ8-Ic#0yC%7uGF#$4(f7+XR&ebCr{D5I88Q8Y?ZB#si$ z{F6GT(YE*&ABI}s_Y27dN}^+mqI3ZBAAfYU-?~i3i1lt5G>%PF5@jfgG8u61#@a#C z*DY;38bh0d*Q+HfiLw+$*-S|E6Yn-;fB5ot-Q~AmyS{N&5*=3*<*2}_UA4k>FB$=7 z&ZhIG*NVrKM7fHhygAS!`xcv{Zr}RH(*w?H%F?2hMEQ!M0v3$%Tu@V3TA)Ivix_4; zXSXPcK35bKvY~isPV9A~m6v;;=J~&my-}(pDpC}k;J`7?nb0Rkd^H4z@(qcB6sXmBH>y0&9M3dyL6}%d74e4>5s0KC8c6?7uMWm2I$cb;nC~&bIdJ17u#V5+7%Y*wudHEEi%emPls1tY6od(+K{ee*jk!P-W*j)k-WC*DHD9(S*<$SCzW+m^ z&>^{LQp~!S_NRN#=p8PnK{BAvx@9{4%e3G?mem@!g=~31o#>lLC5o7V_&i--ulSidb!ZzFYdI1M4 zzUtgFO0bb?SKv_Miw;2+>4%8+-Gc2u3QH%_!V$5$U-V6VRO{xnVOZ$>JgDZNcG9CR zYqP^nL4V*Nlh)AnH{{aT{EK@Kv1+hMmN3};JlB&2JMM0tte9xrIdL{|vEQrlYItT| zd<1bT_$vYblf;~YE^jL0II2h)e4pq0(uQ{+F#!<~y_%y7bj5eai`g(c>|eP~=O>fS zI9+qS_P3`#@PTapd_kg%!J~HTBd|QBnz=PPObrS9o=yCv?D}F}AQwLvh+>)52 zdr|^M4srXu0v%+};KQP*l<~#t$VTQ^!0PGIUb^KvBHYuOPnE~-X-Yn)q5L>?Vvowx z?pMYfSx0V$?^jvQqCSP7-!*yi_6jevi~IIDpuM!^0S%5LKi-KNfG}%g=dp$630Dgn zJjqzW!8~}jt8TnJuJ(o4W2Ic9*xwgcz2kcInr*P$781H%bna;$7wuB1wpx92pp9@x z3nF&d1d+y<5V# z{>SG`>+h^Z?$eC=9;>|bUF8@CSET;1)MbFHf7{N?f8+iNP0{agZ~GhWZ374(1_a;{ zfM8I-q(Fs&ITWxcU{k=MK$QZuDNt4C0YGR>{UW#&Xi~tV08+rGKtO?z0xb%(DG*Vh zLxC;@dKBnWU_gN(1#>AdngR{30B|({(Bv@y4>AFj#{lwG0ADZ%2!t#k)M5iIZ4O|D zY>fumB2^&LQ3E=<>Ofad1L)~vefWs^pC*PFoEwIS!Qndfb<`kD;s`RDwlTdkGee+C zWr}A5O=L4OexFiTWO7;442e4cog28-36M>t8eIeR5VLEhj|rz4Q#>P~f{btea}KhZ zIZqa9>DcBZ>LZvr#+X)-xU+~5gBze0VsL%cM+~lb2Gb1Qxk!}`7u@Xt{q?vo1GE(u zrUIpC$OiPMw9U+Y0NntIX9PfBCGH-8zDrz-0R7x}WSYv4U;j#xxSIi`AT0n&F_{j~ z_2JVW1t^+5p9>>EW3H6exvV2|#g2;$Z;LkBDm^ zpksRG5Saj_6h1^DprZ(n$$$?18DSov;~LLI00jx&>jY4pjtLSEgMdy@JX8XRtngF< NP+1{y{{#lae*r873y1&! literal 0 HcmV?d00001 diff --git a/lib/sdf-server/src/migrations.rs b/lib/sdf-server/src/migrations.rs index 1dcb732673..96287ee061 100644 --- a/lib/sdf-server/src/migrations.rs +++ b/lib/sdf-server/src/migrations.rs @@ -84,12 +84,35 @@ impl Migrator { Self { services_context } } - #[instrument(name = "sdf.migrator.run_migrations", level = "info", skip_all)] + #[instrument( + name = "sdf.migrator.run_migrations", + level = "info", + skip_all, + fields( + otel.status_code = Empty, + otel.status_message = Empty, + ) + )] pub async fn run_migrations(self) -> MigratorResult<()> { - self.migrate_layer_db_database().await?; - self.migrate_dal_database().await?; - self.migrate_snapshots().await?; - self.migrate_builtins_from_module_index().await?; + let span = current_span_for_instrument_at!("info"); + + self.migrate_layer_db_database() + .await + .map_err(|err| span.record_err(err))?; + + self.migrate_dal_database() + .await + .map_err(|err| span.record_err(err))?; + + self.migrate_snapshots() + .await + .map_err(|err| span.record_err(err))?; + + self.migrate_builtins_from_module_index() + .await + .map_err(|err| span.record_err(err))?; + + span.record_ok(); Ok(()) } diff --git a/lib/sdf-server/src/service/component/set_type.rs b/lib/sdf-server/src/service/component/set_type.rs index ec9c67a9d4..ce546d3794 100644 --- a/lib/sdf-server/src/service/component/set_type.rs +++ b/lib/sdf-server/src/service/component/set_type.rs @@ -1,15 +1,5 @@ use std::collections::HashMap; -use axum::{ - extract::{Host, OriginalUri}, - Json, -}; -use dal::{ - component::ComponentGeometry, ChangeSet, Component, ComponentId, ComponentType, Visibility, - WsEvent, -}; -use serde::{Deserialize, Serialize}; - use super::ComponentResult; use crate::{ extract::{AccessBuilder, HandlerContext, PosthogClient}, @@ -17,12 +7,20 @@ use crate::{ track, }; +use axum::{ + extract::{Host, OriginalUri}, + Json, +}; +use dal::diagram::geometry::RawGeometry; +use dal::{ChangeSet, Component, ComponentId, ComponentType, Visibility, WsEvent}; +use serde::{Deserialize, Serialize}; + #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct SetTypeRequest { pub component_id: ComponentId, pub component_type: ComponentType, - pub overridden_geometry: Option, + pub overridden_geometry: Option, #[serde(flatten)] pub visibility: Visibility, } diff --git a/lib/sdf-server/src/service/diagram/create_component.rs b/lib/sdf-server/src/service/diagram/create_component.rs index 4e135a8c9b..80a7667d8b 100644 --- a/lib/sdf-server/src/service/diagram/create_component.rs +++ b/lib/sdf-server/src/service/diagram/create_component.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; use dal::{ cached_module::CachedModule, change_status::ChangeStatus, - component::{frame::Frame, DEFAULT_COMPONENT_HEIGHT, DEFAULT_COMPONENT_WIDTH}, + component::frame::Frame, generate_name, pkg::{import_pkg_from_pkg, ImportOptions}, ChangeSet, Component, ComponentId, Schema, SchemaId, SchemaVariant, SchemaVariantId, @@ -124,6 +124,7 @@ pub async fn create_component( let variant = SchemaVariant::get_by_id_or_error(&ctx, schema_variant_id).await?; let mut component = Component::new(&ctx, &name, variant.id()).await?; + let initial_geometry = component.geometry(&ctx).await?; component .set_geometry( @@ -132,10 +133,10 @@ pub async fn create_component( request.y.clone(), request .width - .or_else(|| Some(DEFAULT_COMPONENT_WIDTH.to_string())), + .or_else(|| initial_geometry.width().map(ToString::to_string)), request .height - .or_else(|| Some(DEFAULT_COMPONENT_HEIGHT.to_string())), + .or_else(|| initial_geometry.height().map(ToString::to_string)), ) .await?; diff --git a/lib/sdf-server/src/service/diagram/paste_component.rs b/lib/sdf-server/src/service/diagram/paste_component.rs index bd9b614e9d..0121e25b6b 100644 --- a/lib/sdf-server/src/service/diagram/paste_component.rs +++ b/lib/sdf-server/src/service/diagram/paste_component.rs @@ -1,30 +1,28 @@ use std::collections::HashMap; +use super::{DiagramError, DiagramResult}; +use crate::{ + extract::{AccessBuilder, HandlerContext, PosthogClient}, + service::force_change_set_response::ForceChangeSetResponse, + track, +}; use axum::{ extract::{Host, OriginalUri}, http::uri::Uri, Json, }; +use dal::diagram::geometry::RawGeometry; use dal::{ - change_status::ChangeStatus, - component::{frame::Frame, ComponentGeometry}, - diagram::SummaryDiagramEdge, - ChangeSet, Component, ComponentId, DalContext, Visibility, WsEvent, + change_status::ChangeStatus, component::frame::Frame, diagram::SummaryDiagramEdge, ChangeSet, + Component, ComponentId, DalContext, Visibility, WsEvent, }; use serde::{Deserialize, Serialize}; -use super::{DiagramError, DiagramResult}; -use crate::{ - extract::{AccessBuilder, HandlerContext, PosthogClient}, - service::force_change_set_response::ForceChangeSetResponse, - track, -}; - #[allow(clippy::too_many_arguments)] async fn paste_single_component( ctx: &DalContext, component_id: ComponentId, - component_geometry: ComponentGeometry, + component_geometry: RawGeometry, original_uri: &Uri, host_name: &String, PosthogClient(posthog_client): &PosthogClient, @@ -53,7 +51,7 @@ async fn paste_single_component( #[serde(rename_all = "camelCase")] pub struct PasteSingleComponentPayload { id: ComponentId, - component_geometry: ComponentGeometry, + component_geometry: RawGeometry, } #[derive(Deserialize, Serialize, Debug)] diff --git a/lib/sdf-server/src/service/diagram/set_component_position.rs b/lib/sdf-server/src/service/diagram/set_component_position.rs index 4ea9699b77..355e2e4a6c 100644 --- a/lib/sdf-server/src/service/diagram/set_component_position.rs +++ b/lib/sdf-server/src/service/diagram/set_component_position.rs @@ -1,24 +1,24 @@ use std::collections::HashMap; +use super::DiagramResult; +use crate::{ + extract::{AccessBuilder, HandlerContext}, + service::force_change_set_response::ForceChangeSetResponse, +}; use axum::Json; +use dal::diagram::geometry::RawGeometry; use dal::{ - component::{frame::Frame, ComponentGeometry, InferredConnection}, + component::{frame::Frame, InferredConnection}, diagram::SummaryDiagramInferredEdge, ChangeSet, Component, ComponentId, ComponentType, Visibility, WsEvent, }; use serde::{Deserialize, Serialize}; use ulid::Ulid; -use super::DiagramResult; -use crate::{ - extract::{AccessBuilder, HandlerContext}, - service::force_change_set_response::ForceChangeSetResponse, -}; - #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct SingleComponentGeometryUpdate { - pub geometry: ComponentGeometry, + pub geometry: RawGeometry, pub detach: bool, pub new_parent: Option, } @@ -52,6 +52,7 @@ pub async fn set_component_position( let mut diagram_inferred_edges: Vec = vec![]; let mut socket_map = HashMap::new(); + let mut geometry_list = vec![]; for (id, update) in request.data_by_component_id { let mut component = Component::get_by_id(&ctx, id).await?; @@ -108,6 +109,9 @@ pub async fn set_component_position( .await?; } + let geometry = component.geometry(&ctx).await?; + let new_geometry = update.geometry.clone(); + let (width, height) = { let mut size = (None, None); @@ -115,14 +119,12 @@ pub async fn set_component_position( if component_type != ComponentType::Component { size = ( - update - .geometry + new_geometry .width - .or_else(|| component.width().map(|v| v.to_string())), - update - .geometry + .or_else(|| geometry.width().map(|v| v.to_string())), + new_geometry .height - .or_else(|| component.height().map(|v| v.to_string())), + .or_else(|| geometry.width().map(|v| v.to_string())), ); } @@ -130,15 +132,17 @@ pub async fn set_component_position( }; component - .set_geometry(&ctx, update.geometry.x, update.geometry.y, width, height) + .set_geometry(&ctx, new_geometry.x, new_geometry.y, width, height) .await?; components.push(component); + + geometry_list.push((id, update.geometry)) } WsEvent::set_component_position( &ctx, ctx.change_set_id(), - components, + geometry_list, Some(request.client_ulid), ) .await? From 68caa95297941cb90909eb5b7e710121de971d46 Mon Sep 17 00:00:00 2001 From: Victor Bustamante Date: Wed, 23 Oct 2024 08:42:56 -0300 Subject: [PATCH 2/2] fix: Migrate all open changesets without navigating a graph and raise migration logs --- lib/dal/src/workspace_snapshot/migrator.rs | 175 ++++++++++----------- 1 file changed, 79 insertions(+), 96 deletions(-) diff --git a/lib/dal/src/workspace_snapshot/migrator.rs b/lib/dal/src/workspace_snapshot/migrator.rs index 4184280bb4..b319901859 100644 --- a/lib/dal/src/workspace_snapshot/migrator.rs +++ b/lib/dal/src/workspace_snapshot/migrator.rs @@ -10,10 +10,9 @@ use super::{ use crate::workspace_snapshot::migrator::v4::migrate_v3_to_v4; use crate::workspace_snapshot::node_weight::NodeWeightError; use crate::{ - dependency_graph::DependencyGraph, workspace_snapshot::migrator::{v2::migrate_v1_to_v2, v3::migrate_v2_to_v3}, - ChangeSet, ChangeSetError, ChangeSetStatus, DalContext, Visibility, Workspace, WorkspaceError, - WorkspaceSnapshot, WorkspaceSnapshotError, + ChangeSet, ChangeSetError, ChangeSetStatus, DalContext, TransactionsError, Visibility, + Workspace, WorkspaceError, WorkspaceSnapshot, WorkspaceSnapshotError, }; use si_events::WorkspaceSnapshotAddress; use si_layer_cache::LayerDbError; @@ -41,6 +40,8 @@ pub enum SnapshotGraphMigratorError { NodeWeight(#[from] NodeWeightError), #[error("SchemaVariantNodeWeight error: {0}")] SchemaVariantNodeWeight(#[from] SchemaVariantNodeWeightError), + #[error("transactions error: {0}")] + Transactions(#[from] TransactionsError), #[error("unexpected graph version {1:?} for snapshot {0}, cannot migrate")] UnexpectedGraphVersion( WorkspaceSnapshotAddress, @@ -83,102 +84,71 @@ impl SnapshotGraphMigrator { let open_change_sets = ChangeSet::list_open_for_all_workspaces(ctx).await?; - let mut change_set_graph = DependencyGraph::new(); - for change_set in open_change_sets { - match change_set.base_change_set_id { - Some(base_change_set_id) => { - change_set_graph.id_depends_on(change_set.id, base_change_set_id); - } - None => { - change_set_graph.add_id(change_set.id); - } - } - } + info!("Migrating {} snapshot(s)", open_change_sets.len(),); - info!( - "Migrating {} snapshot(s)", - change_set_graph.independent_ids().len(), - ); - - loop { - let change_sets_to_migrate = change_set_graph.independent_ids(); - if change_sets_to_migrate.is_empty() { - break; + for change_set in open_change_sets { + let mut change_set = ChangeSet::find(ctx, change_set.id) + .await? + .ok_or(ChangeSetError::ChangeSetNotFound(change_set.id))?; + if change_set.workspace_id.is_none() || change_set.status == ChangeSetStatus::Failed { + // These are broken/garbage change sets generated during migrations of the + // "universal" workspace/change set. They're not actually accessible via normal + // means, as we generally follow the chain starting at the workspace, and these + // aren't associated with any workspace. + continue; } - for change_set_id in change_sets_to_migrate { - let mut change_set = ChangeSet::find(ctx, change_set_id) - .await? - .ok_or(ChangeSetError::ChangeSetNotFound(change_set_id))?; - if change_set.workspace_id.is_none() || change_set.status == ChangeSetStatus::Failed - { - // These are broken/garbage change sets generated during migrations of the - // "universal" workspace/change set. They're not actually accessible via normal - // means, as we generally follow the chain starting at the workspace, and these - // aren't associated with any workspace. - change_set_graph.remove_id(change_set_id); - continue; - } - - let snapshot_address = change_set.workspace_snapshot_address; - trace!( - "Migrating snapshot {} for change set {} with base change set of {:?}", - snapshot_address, - change_set_id, - change_set.base_change_set_id, - ); - - let new_snapshot = match self.migrate_snapshot(ctx, snapshot_address).await { - Ok(new_snapshot) => new_snapshot, - Err(err) => { - let err_string = err.to_string(); - if err_string.contains("missing from store for node") - || err_string.contains("workspace snapshot graph missing at address") - { - error!(error = ?err, "Migration error: {err_string}, marking change set {} for workspace {:?} as failed", change_set.id, change_set.workspace_id); - change_set - .update_status(ctx, ChangeSetStatus::Failed) - .await?; - change_set_graph.remove_id(change_set_id); - continue; - } else { - return Err(err)?; - } + // NOTE(victor): The context that gets passed in does not have a workspace snapshot + // on it, since its main purpose is to allow access to the services context. + // We need to create a context for each migrated changeset here to run operations + // that depend on the graph + let mut ctx_after_migration = + ctx.clone_with_new_visibility(Visibility::from(change_set.id)); + // TODO make sure that when we clone with a changeset id we also set changeset + // (or there's no clone anymore and we always change it via following method) + ctx_after_migration.set_change_set(change_set.clone())?; + + let snapshot_address = change_set.workspace_snapshot_address; + + let new_snapshot = match self + .migrate_snapshot(&ctx_after_migration, snapshot_address) + .await + { + Ok(new_snapshot) => new_snapshot, + Err(err) => { + let err_string = err.to_string(); + if err_string.contains("missing from store for node") + || err_string.contains("workspace snapshot graph missing at address") + { + error!(error = ?err, "Migration error: {err_string}, marking change set {} for workspace {:?} as failed", change_set.id, change_set.workspace_id); + change_set + .update_status(ctx, ChangeSetStatus::Failed) + .await?; + continue; + } else { + return Err(err)?; } - }; - - let (new_snapshot_address, _) = ctx - .layer_db() - .workspace_snapshot() - .write( - Arc::new(new_snapshot), - None, - ctx.events_tenancy(), - ctx.events_actor(), - ) - .await?; - - // NOTE(victor): The context that gets passed in does not have a workspace snapshot - // on it, since its main purpose is to allow access to the services context. - // We need to create a context for each migrated changeset here to run operations - // that depend on the graph - let mut ctx_after_migration = - ctx.clone_with_new_visibility(Visibility::from(change_set.id)); - let migrated_snapshot = WorkspaceSnapshot::find(ctx, new_snapshot_address).await?; - ctx_after_migration.set_workspace_snapshot(migrated_snapshot); - - change_set - .update_pointer(&ctx_after_migration, new_snapshot_address) - .await?; - trace!( - "Migrated snapshot {} for change set {} with base change set of {:?}", - snapshot_address, - change_set_id, - change_set.base_change_set_id, - ); - - change_set_graph.remove_id(change_set_id); - } + } + }; + + let (new_snapshot_address, _) = ctx_after_migration + .layer_db() + .workspace_snapshot() + .write( + Arc::new(new_snapshot), + None, + ctx.events_tenancy(), + ctx.events_actor(), + ) + .await?; + + let migrated_snapshot = + WorkspaceSnapshot::find(&ctx_after_migration, new_snapshot_address).await?; + ctx_after_migration.set_workspace_snapshot(migrated_snapshot); + + change_set + .update_pointer(&ctx_after_migration, new_snapshot_address) + .await?; } info!("Migration finished, marking all workspaces as migrated to latest version"); @@ -207,7 +177,15 @@ impl SnapshotGraphMigrator { workspace_snapshot_address, ))?; - info!("snapshot is {} bytes", snapshot_bytes.len()); + let change_set = ctx.change_set()?; + + info!( + "Migrating snapshot {} for change set {} with base change set of {:?} ({} bytes)", + workspace_snapshot_address, + change_set.id, + change_set.base_change_set_id, + snapshot_bytes.len() + ); let mut working_graph: WorkspaceSnapshotGraph = si_layer_cache::db::serialize::from_bytes(&snapshot_bytes)?; @@ -239,6 +217,11 @@ impl SnapshotGraphMigrator { } } + info!( + "Migrated snapshot {} for change set {}", + workspace_snapshot_address, change_set.id, + ); + Ok(working_graph) } }