go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/projects/nodes/pkg/dbmodel/store.go (about) 1 /* 2 3 Copyright (c) 2023 - Present. Will Charczuk. All rights reserved. 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository. 5 6 */ 7 8 package dbmodel 9 10 import ( 11 "bytes" 12 "context" 13 "fmt" 14 "sync" 15 "time" 16 17 "github.com/wcharczuk/go-incr" 18 "go.charczuk.com/projects/nodes/pkg/incrutil" 19 "go.charczuk.com/projects/nodes/pkg/model" 20 "go.charczuk.com/projects/nodes/pkg/types" 21 "go.charczuk.com/sdk/errutil" 22 "go.charczuk.com/sdk/iter" 23 "go.charczuk.com/sdk/uuid" 24 ) 25 26 // Store is a wrapper for a model manager that 27 // implements the graph store api against the database 28 // for a given graph by identifier. 29 type Store struct { 30 GraphID uuid.UUID 31 UserID uuid.UUID 32 Model *Manager 33 } 34 35 func (s Store) Save(ctx context.Context) (*types.GraphFull, error) { 36 return s.Model.Deserialize(ctx, s.GraphID, false, false) 37 } 38 39 func (s Store) Load(ctx context.Context, graph *types.GraphFull) (id incr.Identifier, err error) { 40 // create a new graph essentially based on the file. 41 graphObj := GraphFromType(graph.Graph) 42 graphObj.ID = uuid.V4() 43 graphObj.UserID = s.UserID 44 graphObj.CreatedUTC = time.Now().UTC() 45 graphObj.UpdatedUTC = time.Now().UTC() 46 if err = s.Model.Invoke(ctx).Create(graphObj); err != nil { 47 return 48 } 49 50 id = incr.Identifier(graphObj.ID) 51 52 // nodes 53 nodeRemap := make(map[incr.Identifier]uuid.UUID) 54 for _, n := range graph.Nodes { 55 nodeObj := NodeFromTypeNode(graphObj.ID, s.UserID, &n) 56 nodeObj.ID = uuid.V4() 57 nodeRemap[n.ID] = nodeObj.ID 58 if err = s.Model.Invoke(ctx).Create(nodeObj); err != nil { 59 return 60 } 61 } 62 63 // edges 64 for _, e := range graph.Edges { 65 edgeObj := EdgeFromType(graphObj, e) 66 edgeObj.ParentID = nodeRemap[e.ParentID] 67 edgeObj.ChildID = nodeRemap[e.ChildID] 68 if err = s.Model.Invoke(ctx).Create(edgeObj); err != nil { 69 return 70 } 71 } 72 73 // node values 74 for _, v := range graph.Values { 75 nv := &NodeValue{ 76 NodeID: nodeRemap[v.ID], 77 GraphID: graphObj.ID, 78 UserID: graphObj.UserID, 79 ValueType: v.ValueType, 80 Value: v.Value, 81 } 82 if err = s.Model.Invoke(ctx).Create(nv); err != nil { 83 return 84 } 85 } 86 return 87 } 88 89 func (s Store) Graph(ctx context.Context) (*types.Graph, error) { 90 var graphObj Graph 91 found, err := s.Model.Invoke(ctx).Get(&graphObj, s.GraphID) 92 if err != nil { 93 return nil, err 94 } 95 if !found { 96 return nil, nil 97 } 98 99 return &types.Graph{ 100 ID: incr.Identifier(graphObj.ID), 101 Label: graphObj.Label, 102 StabilizationNum: graphObj.StabilizationNum, 103 Metadata: types.GraphMetadata{ 104 UserID: graphObj.UserID, 105 CreatedUTC: graphObj.CreatedUTC, 106 UpdatedUTC: graphObj.UpdatedUTC, 107 ViewportX: graphObj.ViewportX, 108 ViewportY: graphObj.ViewportY, 109 ViewportZoom: graphObj.ViewportZoom, 110 }, 111 }, nil 112 } 113 114 func (s Store) Logs(ctx context.Context) (string, error) { 115 logsLatest, found, err := s.Model.GraphLogsLatest(ctx, s.GraphID) 116 if err != nil { 117 return "", err 118 } 119 if !found { 120 return "", nil 121 } 122 return logsLatest.Logs, nil 123 } 124 125 func (s Store) SetViewport(ctx context.Context, viewport types.Viewport) error { 126 return s.Model.SetViewport(ctx, s.GraphID, viewport) 127 } 128 129 func (s Store) Stabilize(ctx context.Context, parallel bool) (stabilizationNum uint64, err error) { 130 var data *types.GraphFull 131 data, err = s.Model.Deserialize(ctx, s.GraphID, false, false) 132 if err != nil { 133 return 134 } 135 136 var graph *incr.Graph 137 var nodes map[incr.Identifier]incr.INode 138 graph, nodes, err = s.buildGraph(ctx, data) 139 if err != nil { 140 return 141 } 142 143 var valuesMu sync.Mutex 144 values := make(map[uuid.UUID]any) 145 nodeMetadata := make(map[uuid.UUID]Node) 146 logs := new(bytes.Buffer) 147 eg := incr.ExpertGraph(graph) 148 149 defer func() { 150 if r := recover(); r != nil { 151 err = errutil.Append(err, errutil.New(fmt.Errorf("stabilization panic: %v", r))) 152 } 153 }() 154 155 updateHandler := func(n incr.INode) func(context.Context) { 156 en := incr.ExpertNode(n) 157 nodeID := uuid.UUID(n.Node().ID()) 158 return func(_ context.Context) { 159 valuesMu.Lock() 160 defer valuesMu.Unlock() 161 values[nodeID] = incrutil.GetNodeValue(n) 162 nodeMetadata[nodeID] = Node{ 163 SetAt: en.SetAt(), 164 ChangedAt: en.ChangedAt(), 165 RecomputedAt: en.RecomputedAt(), 166 } 167 } 168 } 169 for _, n := range nodes { 170 n.Node().OnUpdate(updateHandler(n)) 171 } 172 ctx = incr.WithTracingOutputs(ctx, logs, logs) 173 if parallel { 174 err = graph.ParallelStabilize(ctx) 175 } else { 176 err = graph.Stabilize(ctx) 177 } 178 179 func() { 180 defer func() { 181 if r := recover(); r != nil { 182 err = errutil.Append(err, errutil.New(fmt.Errorf("stabilization post-actions panic: %v", r))) 183 } 184 }() 185 186 var postErrs []error 187 var postErr error 188 stabilizationNum = eg.StabilizationNum() 189 190 postErr = s.Model.UpdateGraphPostStabilization(ctx, s.GraphID, stabilizationNum) 191 if postErr != nil { 192 postErrs = append(postErrs, errutil.New(postErr)) 193 } 194 195 postErr = s.Model.TouchGraph(ctx, s.GraphID) 196 if postErr != nil { 197 postErrs = append(postErrs, errutil.New(postErr)) 198 } 199 200 postErr = s.Model.Invoke(ctx).Create(GraphLogs{GraphID: s.GraphID, UserID: s.UserID, StabilizationNum: stabilizationNum, Logs: logs.String()}) 201 if postErr != nil { 202 postErrs = append(postErrs, errutil.New(postErr)) 203 } 204 205 postErr = s.Model.SetNodeMetadata(ctx, s.GraphID, nodeMetadata) 206 if postErr != nil { 207 postErrs = append(postErrs, errutil.New(postErr)) 208 } 209 210 postErr = s.Model.SetNodeValues(ctx, s.GraphID, s.UserID, values) 211 if postErr != nil { 212 postErrs = append(postErrs, errutil.New(postErr)) 213 } 214 215 recomputeHeapIDs := iter.Apply(eg.RecomputeHeapIDs(), func(id incr.Identifier) uuid.UUID { return uuid.UUID(id) }) 216 postErr = s.Model.SetRecomputeHeap(ctx, s.GraphID, s.UserID, recomputeHeapIDs...) 217 if postErr != nil { 218 postErrs = append(postErrs, errutil.New(postErr)) 219 } 220 221 if len(postErrs) > 0 { 222 err = errutil.Append(err, postErrs...) 223 } 224 }() 225 return 226 } 227 228 func (s Store) AddNode(ctx context.Context, n *types.Node) (id incr.Identifier, err error) { 229 dn := NodeFromTypeNode(s.GraphID, s.UserID, n) 230 dn.ID = uuid.V4() 231 if err = s.Model.Invoke(ctx).Create(&dn); err != nil { 232 return 233 } 234 if err = s.Model.TouchGraph(ctx, s.GraphID); err != nil { 235 return 236 } 237 id = incr.Identifier(dn.ID) 238 return 239 } 240 241 func (s Store) RemoveNode(ctx context.Context, id incr.Identifier) (found bool, err error) { 242 found, err = s.Model.DeleteNode(ctx, uuid.UUID(id)) 243 if err != nil { 244 return 245 } 246 if err = s.Model.TouchGraph(ctx, s.GraphID); err != nil { 247 return 248 } 249 return 250 } 251 252 func (s Store) Nodes(ctx context.Context) ([]types.Node, error) { 253 nodes, err := s.Model.Nodes(ctx, s.GraphID) 254 if err != nil { 255 return nil, err 256 } 257 return iter.Apply(nodes, TypeNodeFromNode), nil 258 } 259 260 func (s Store) Node(ctx context.Context, id incr.Identifier) (output types.Node, found bool, err error) { 261 var nodeObj Node 262 nodeObj, found, err = s.Model.Node(ctx, uuid.UUID(id)) 263 if err != nil { 264 return 265 } 266 if !found { 267 return 268 } 269 output = TypeNodeFromNode(nodeObj) 270 return 271 } 272 273 func (s Store) PatchNodes(ctx context.Context, ps types.PatchSet) (err error) { 274 if err = s.Model.PatchNodes(ctx, s.GraphID, ps); err != nil { 275 return 276 } 277 if err = s.Model.TouchGraph(ctx, s.GraphID); err != nil { 278 return 279 } 280 return 281 } 282 283 func (s Store) PatchNode(ctx context.Context, id incr.Identifier, ps types.PatchSet) (found bool, err error) { 284 // handle marking the node stale (and adding it to the recompute heap) if we change the expression. 285 // for multi-node patches we do _not_ have to do we never set the expression that way. 286 if _, includesExpression := ps["expression"]; includesExpression { 287 graph, found, err := s.Model.Graph(ctx, s.GraphID) 288 if err != nil { 289 return false, err 290 } 291 if !found { 292 return false, nil 293 } 294 ps["set_at"] = graph.StabilizationNum 295 if err := s.Model.Invoke(ctx).Upsert(&GraphRecomputeHeap{ 296 GraphID: s.GraphID, 297 UserID: s.UserID, 298 NodeID: uuid.UUID(id), 299 }); err != nil { 300 return false, err 301 } 302 } 303 found, err = s.Model.PatchNode(ctx, uuid.UUID(id), ps) 304 if err != nil { 305 return 306 } 307 if err = s.Model.TouchGraph(ctx, s.GraphID); err != nil { 308 return 309 } 310 return 311 } 312 313 func (s Store) NodeValue(ctx context.Context, id incr.Identifier) (any, bool, error) { 314 nv, found, err := s.Model.NodeValue(ctx, s.GraphID, uuid.UUID(id)) 315 if err != nil { 316 return nil, false, err 317 } 318 if !found { 319 return nil, false, nil 320 } 321 parsedValue, err := nv.ParsedValue() 322 if err != nil { 323 return nil, true, err 324 } 325 return parsedValue, true, nil 326 } 327 328 func (s Store) NodeValues(ctx context.Context, nodeIDs ...incr.Identifier) ([]types.NodeValue, error) { 329 values, err := s.Model.NodeValues(ctx, s.GraphID, iter.Apply(nodeIDs, func(id incr.Identifier) uuid.UUID { return uuid.UUID(id) })...) 330 if err != nil { 331 return nil, err 332 } 333 return iter.Apply(values, func(v NodeValue) types.NodeValue { 334 parsed, _ := v.ParsedValue() 335 return types.NodeValue{ 336 ID: incr.Identifier(v.NodeID), 337 Value: parsed, 338 } 339 }), nil 340 } 341 342 func (s Store) SetNodeValue(ctx context.Context, nodeID incr.Identifier, value any) (bool, error) { 343 node, found, err := s.Model.Node(ctx, uuid.UUID(nodeID)) 344 if err != nil { 345 return false, fmt.Errorf("%w; set node value; get node", err) 346 } 347 if !found { 348 return false, nil 349 } 350 graph, _, err := s.Model.Graph(ctx, s.GraphID) 351 if err != nil { 352 return false, fmt.Errorf("%w; set node value; get node graph", err) 353 } 354 node.SetAt = graph.StabilizationNum 355 if _, err = s.Model.Invoke(ctx).Update(node); err != nil { 356 return false, err 357 } 358 if err := s.Model.Invoke(ctx).Upsert(&GraphRecomputeHeap{ 359 GraphID: s.GraphID, 360 UserID: s.UserID, 361 NodeID: node.ID, 362 }); err != nil { 363 return false, fmt.Errorf("%w; set node value; upsert graph recompute heap", err) 364 } 365 var nv = NodeValue{ 366 NodeID: uuid.UUID(nodeID), 367 GraphID: s.GraphID, 368 UserID: s.UserID, 369 ValueType: DetectValueType(value), 370 Value: value, 371 } 372 if err := s.Model.Invoke(ctx).Upsert(&nv); err != nil { 373 return false, fmt.Errorf("%w; set node value; upsert node value", err) 374 } 375 if err := s.Model.TouchGraph(ctx, s.GraphID); err != nil { 376 return false, fmt.Errorf("%w; set node value; touch graph", err) 377 } 378 return true, nil 379 } 380 381 func (s Store) MarkNodeStale(ctx context.Context, nodeID incr.Identifier) (found bool, err error) { 382 found, err = s.Model.MarkNodeStale(ctx, s.GraphID, s.UserID, uuid.UUID(nodeID)) 383 if err != nil { 384 return 385 } 386 if err = s.Model.TouchGraph(ctx, s.GraphID); err != nil { 387 return 388 } 389 return 390 } 391 392 func (s Store) Edges(ctx context.Context) ([]types.Edge, error) { 393 edges, err := s.Model.Edges(ctx, s.GraphID) 394 if err != nil { 395 return nil, err 396 } 397 return iter.Apply(edges, func(e Edge) types.Edge { 398 return types.Edge{ 399 ParentID: incr.Identifier(e.ParentID), 400 ChildID: incr.Identifier(e.ChildID), 401 ChildInputName: e.ChildInputName, 402 } 403 }), nil 404 } 405 406 func (s Store) LinkInput(ctx context.Context, te types.Edge) error { 407 graph, nodes, err := s.fetchGraphBasic(ctx) 408 if err != nil { 409 return err 410 } 411 child, ok := nodes[te.ChildID] 412 if !ok { 413 return fmt.Errorf("child node not found with id %v", te.ChildID) 414 } 415 parent, ok := nodes[te.ParentID] 416 if !ok { 417 return fmt.Errorf("parent node not found with id %v", te.ParentID) 418 } 419 if err = s.linkInputToChildUnsafe(ctx, graph, nodes, child, parent, te.ChildInputName); err != nil { 420 return err 421 } 422 if err := s.Model.Invoke(ctx).Create(Edge{ 423 GraphID: s.GraphID, 424 UserID: s.UserID, 425 CreatedUTC: time.Now().UTC(), 426 ParentID: uuid.UUID(te.ParentID), 427 ChildID: uuid.UUID(te.ChildID), 428 ChildInputName: te.ChildInputName, 429 }); err != nil { 430 return err 431 } 432 433 if incr.ExpertNode(child).IsNecessary() || s.isObserver(child) { 434 if err := s.Model.Invoke(ctx).Upsert(&GraphRecomputeHeap{ 435 GraphID: s.GraphID, 436 UserID: s.UserID, 437 NodeID: uuid.UUID(te.ChildID), 438 }); err != nil { 439 return err 440 } 441 } 442 if err := s.recomputeHeights(ctx, nodes); err != nil { 443 return err 444 } 445 if err := s.Model.TouchGraph(ctx, s.GraphID); err != nil { 446 return err 447 } 448 return nil 449 } 450 451 func (s Store) UnlinkInput(ctx context.Context, te types.Edge) error { 452 if err := s.Model.DeleteEdge(ctx, uuid.UUID(te.ParentID), uuid.UUID(te.ChildID), te.ChildInputName); err != nil { 453 return err 454 } 455 _, nodes, err := s.fetchGraphBasic(ctx) 456 if err != nil { 457 return err 458 } 459 if err := s.recomputeHeights(ctx, nodes); err != nil { 460 return err 461 } 462 if err := s.Model.TouchGraph(ctx, s.GraphID); err != nil { 463 return err 464 } 465 return nil 466 } 467 468 func (s Store) PatchNodeTable(ctx context.Context, nodeID incr.Identifier, ops ...types.TableOp) (bool, error) { 469 value, found, err := s.NodeValue(ctx, nodeID) 470 if err != nil { 471 return false, err 472 } 473 if !found { 474 table := new(types.Table) 475 table.Columns = []types.TableColumn{ 476 { 477 Name: "New Column", 478 Values: []any{}, 479 }, 480 } 481 if err := table.ApplyOps(ops...); err != nil { 482 return true, err 483 } 484 if _, err := s.SetNodeValue(ctx, nodeID, table); err != nil { 485 return true, err 486 } 487 return true, nil 488 } 489 typed, ok := value.(*types.Table) 490 if !ok { 491 return true, fmt.Errorf("value is not a table for table ops") 492 } 493 if err := typed.ApplyOps(ops...); err != nil { 494 return true, err 495 } 496 if _, err := s.SetNodeValue(ctx, nodeID, typed); err != nil { 497 return true, err 498 } 499 if err := s.Model.TouchGraph(ctx, s.GraphID); err != nil { 500 return true, err 501 } 502 return true, nil 503 } 504 505 // 506 // internal helpers 507 // 508 509 func (s Store) isObserver(n incr.INode) bool { 510 if n == nil { 511 return false 512 } 513 metadata, ok := n.Node().Metadata().(*types.NodeMetadata) 514 if !ok { 515 return false 516 } 517 return metadata.NodeType == incrutil.NodeTypeObserver 518 } 519 520 func (s Store) fetchGraphBasic(ctx context.Context) (graph *incr.Graph, nodes map[incr.Identifier]incr.INode, err error) { 521 var data *types.GraphFull 522 data, err = s.Model.Deserialize(ctx, s.GraphID, true, true) 523 if err != nil { 524 return 525 } 526 graph, nodes, err = s.buildGraph(ctx, data) 527 return 528 } 529 530 func (s Store) recomputeHeights(ctx context.Context, nodes map[incr.Identifier]incr.INode) (err error) { 531 updatedHeights := make(map[uuid.UUID]int) 532 for _, n := range nodes { 533 fmt.Println("recompute height of", n.Node().String()) 534 en := incr.ExpertNode(n) 535 newHeight := en.ComputePseudoHeight() 536 updatedHeights[uuid.UUID(n.Node().ID())] = newHeight 537 } 538 return s.Model.SetNodeHeights(ctx, s.GraphID, updatedHeights) 539 } 540 541 func (s Store) buildGraph(ctx context.Context, data *types.GraphFull) (graph *incr.Graph, nodes map[incr.Identifier]incr.INode, err error) { 542 graph = incr.New() 543 expertGraph := incr.ExpertGraph(graph) 544 nodes = make(map[incr.Identifier]incr.INode) 545 graph.SetLabel(data.Graph.Label) 546 graph.SetMetadata(&data.Graph.Metadata) 547 expertGraph.SetStabilizationNum(data.Graph.StabilizationNum) 548 549 for x := 0; x < len(data.Nodes); x++ { 550 n := data.Nodes[x] 551 if _, err = s.addNodeUnsafe(graph, nodes, &n); err != nil { 552 return 553 } 554 } 555 for _, e := range data.Edges { 556 child, ok := nodes[e.ChildID] 557 if !ok { 558 err = fmt.Errorf("edge child node not found with id %v", e.ChildID) 559 return 560 } 561 input, ok := nodes[e.ParentID] 562 if !ok { 563 err = fmt.Errorf("edge parent node not found with id %v", e.ParentID) 564 return 565 } 566 if err = s.linkInputToChildUnsafe(ctx, graph, nodes, child, input, e.ChildInputName); err != nil { 567 return 568 } 569 } 570 571 for x := 0; x < len(data.Values); x++ { 572 v := data.Values[x] 573 if _, err = s.setNodeValueUnsafe(graph, nodes, v.ID, v.Value); err != nil { 574 return 575 } 576 } 577 578 for _, nodeID := range data.RecomputeHeap { 579 n, ok := nodes[nodeID] 580 if !ok { 581 continue 582 } 583 expertGraph.RecomputeHeapAdd(n) 584 } 585 return 586 } 587 588 func (s Store) addNodeUnsafe(graph *incr.Graph, nodes map[incr.Identifier]incr.INode, n *types.Node) (id incr.Identifier, err error) { 589 if n.Metadata.NodeType == "" { 590 err = fmt.Errorf(`"node_type" is required`) 591 return 592 } 593 var in incr.INode 594 in, err = model.INodeFromNode(graph, n) 595 if err != nil { 596 return 597 } 598 id = in.Node().ID() 599 nodes[id] = in 600 return 601 } 602 603 func (s Store) setNodeValueUnsafe(_ *incr.Graph, nodes map[incr.Identifier]incr.INode, id incr.Identifier, value any) (ok bool, err error) { 604 var in incr.INode 605 in, ok = nodes[id] 606 if in == nil || !ok { 607 return 608 } 609 err = incrutil.SetNodeValueDuringDeserialization(in, value) 610 return 611 } 612 613 func (s Store) linkInputToChildUnsafe(_ context.Context, _ *incr.Graph, _ map[incr.Identifier]incr.INode, child, input incr.INode, inputName string) error { 614 if err := incr.DetectCycleIfLinked(child, input); err != nil { 615 return err 616 } 617 if typed, ok := child.(incrutil.IAddInput); ok { 618 if err := typed.AddInput(input); err != nil { 619 return err 620 } 621 } else if typed, ok := child.(incrutil.ISetInput); ok { 622 if err := typed.SetInput(inputName, input, true); err != nil { 623 return err 624 } 625 } else { 626 return fmt.Errorf("cannot add input node: %v", child) 627 } 628 return nil 629 }