github.com/hasnat/dolt/go@v0.0.0-20210628190320-9eb5d843fbb7/store/datas/database_common.go (about) 1 // Copyright 2019 Dolthub, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 // 15 // This file incorporates work covered by the following copyright and 16 // permission notice: 17 // 18 // Copyright 2016 Attic Labs, Inc. All rights reserved. 19 // Licensed under the Apache License, version 2.0: 20 // http://www.apache.org/licenses/LICENSE-2.0 21 22 package datas 23 24 import ( 25 "context" 26 "errors" 27 "fmt" 28 "sync" 29 30 "github.com/dolthub/dolt/go/store/chunks" 31 "github.com/dolthub/dolt/go/store/d" 32 "github.com/dolthub/dolt/go/store/hash" 33 "github.com/dolthub/dolt/go/store/merge" 34 "github.com/dolthub/dolt/go/store/types" 35 "github.com/dolthub/dolt/go/store/util/random" 36 ) 37 38 type database struct { 39 *types.ValueStore 40 rt rootTracker 41 mu sync.Mutex 42 } 43 44 var ( 45 ErrOptimisticLockFailed = errors.New("optimistic lock failed on database Root update") 46 ErrMergeNeeded = errors.New("dataset head is not ancestor of commit") 47 ) 48 49 // TODO: fix panics 50 // rootTracker is a narrowing of the ChunkStore interface, to keep Database disciplined about working directly with Chunks 51 type rootTracker interface { 52 Rebase(ctx context.Context) error 53 Root(ctx context.Context) (hash.Hash, error) 54 Commit(ctx context.Context, current, last hash.Hash) (bool, error) 55 } 56 57 func newDatabase(cs chunks.ChunkStore) *database { 58 vs := types.NewValueStore(cs) 59 60 return &database{ 61 ValueStore: vs, // ValueStore is responsible for closing |cs| 62 rt: vs, 63 } 64 } 65 66 var _ Database = &database{} 67 var _ GarbageCollector = &database{} 68 69 var _ rootTracker = &types.ValueStore{} 70 var _ GarbageCollector = &types.ValueStore{} 71 72 func (db *database) chunkStore() chunks.ChunkStore { 73 return db.ChunkStore() 74 } 75 76 func (db *database) Stats() interface{} { 77 return db.ChunkStore().Stats() 78 } 79 80 func (db *database) StatsSummary() string { 81 return db.ChunkStore().StatsSummary() 82 } 83 84 func (db *database) Flush(ctx context.Context) error { 85 ds, err := db.GetDataset(ctx, fmt.Sprintf("-/flush/%s", random.Id())) 86 87 if err != nil { 88 return err 89 } 90 91 r, err := db.WriteValue(ctx, types.Bool(true)) 92 93 if err != nil { 94 return err 95 } 96 97 ds, err = db.CommitValue(ctx, ds, r) 98 99 if err != nil { 100 return err 101 } 102 103 _, err = db.Delete(ctx, ds) 104 105 return err 106 } 107 108 func (db *database) Datasets(ctx context.Context) (types.Map, error) { 109 rootHash, err := db.rt.Root(ctx) 110 111 if err != nil { 112 return types.EmptyMap, err 113 } 114 115 if rootHash.IsEmpty() { 116 return types.NewMap(ctx, db) 117 } 118 119 val, err := db.ReadValue(ctx, rootHash) 120 121 if err != nil { 122 return types.EmptyMap, err 123 } 124 125 return val.(types.Map), nil 126 } 127 128 func (db *database) GetDataset(ctx context.Context, datasetID string) (Dataset, error) { 129 // precondition checks 130 if !DatasetFullRe.MatchString(datasetID) { 131 return Dataset{}, fmt.Errorf("Invalid dataset ID: %s", datasetID) 132 } 133 134 datasets, err := db.Datasets(ctx) 135 136 if err != nil { 137 return Dataset{}, err 138 } 139 140 var head types.Value 141 if r, ok, err := datasets.MaybeGet(ctx, types.String(datasetID)); err != nil { 142 return Dataset{}, err 143 } else if ok { 144 head, err = r.(types.Ref).TargetValue(ctx, db) 145 146 if err != nil { 147 return Dataset{}, err 148 } 149 } 150 151 return newDataset(db, datasetID, head) 152 } 153 154 func (db *database) Rebase(ctx context.Context) error { 155 return db.rt.Rebase(ctx) 156 } 157 158 func (db *database) Close() error { 159 return db.ValueStore.Close() 160 } 161 162 func (db *database) SetHead(ctx context.Context, ds Dataset, newHeadRef types.Ref) (Dataset, error) { 163 return db.doHeadUpdate(ctx, ds, func(ds Dataset) error { return db.doSetHead(ctx, ds, newHeadRef) }) 164 } 165 166 func (db *database) doSetHead(ctx context.Context, ds Dataset, newHeadRef types.Ref) error { 167 newSt, err := newHeadRef.TargetValue(ctx, db) 168 169 if err != nil { 170 return err 171 } 172 173 headType := newSt.(types.Struct).Name() 174 175 currentHeadRef, ok, err := ds.MaybeHeadRef() 176 if err != nil { 177 return err 178 } 179 if ok { 180 if newHeadRef.Equals(currentHeadRef) { 181 return nil 182 } 183 184 currSt, err := currentHeadRef.TargetValue(ctx, db) 185 186 if err != nil { 187 return err 188 } 189 190 headType = currSt.(types.Struct).Name() 191 } 192 193 // the new head value must match the type of the old head value 194 switch headType { 195 case CommitName: 196 _, err = db.validateRefAsCommit(ctx, newHeadRef) 197 case TagName: 198 err = db.validateTag(ctx, newSt.(types.Struct)) 199 default: 200 return fmt.Errorf("Unrecognized dataset value: %s", headType) 201 } 202 203 if err != nil { 204 return err 205 } 206 207 currentRootHash, err := db.rt.Root(ctx) 208 209 if err != nil { 210 return err 211 } 212 213 currentDatasets, err := db.Datasets(ctx) 214 215 if err != nil { 216 return err 217 } 218 219 refSt, err := db.WriteValue(ctx, newSt) // will be orphaned if the tryCommitChunks() below fails 220 221 if err != nil { 222 return err 223 } 224 225 ref, err := types.ToRefOfValue(refSt, db.Format()) 226 227 if err != nil { 228 return err 229 } 230 231 currentDatasets, err = currentDatasets.Edit().Set(types.String(ds.ID()), ref).Map(ctx) 232 233 if err != nil { 234 return err 235 } 236 237 return db.tryCommitChunks(ctx, currentDatasets, currentRootHash) 238 } 239 240 func (db *database) FastForward(ctx context.Context, ds Dataset, newHeadRef types.Ref) (Dataset, error) { 241 return db.doHeadUpdate(ctx, ds, func(ds Dataset) error { return db.doFastForward(ctx, ds, newHeadRef) }) 242 } 243 244 func (db *database) doFastForward(ctx context.Context, ds Dataset, newHeadRef types.Ref) error { 245 currentHeadRef, ok, err := ds.MaybeHeadRef() 246 247 if err != nil { 248 return err 249 } 250 251 if ok && newHeadRef.Equals(currentHeadRef) { 252 return nil 253 } 254 255 if ok && newHeadRef.Height() <= currentHeadRef.Height() { 256 return ErrMergeNeeded 257 } 258 259 commit, err := db.validateRefAsCommit(ctx, newHeadRef) 260 261 if err != nil { 262 return err 263 } 264 265 return db.doCommit(ctx, ds.ID(), commit, nil) 266 } 267 268 func (db *database) Commit(ctx context.Context, ds Dataset, v types.Value, opts CommitOptions) (Dataset, error) { 269 return db.doHeadUpdate( 270 ctx, 271 ds, 272 func(ds Dataset) error { 273 st, err := buildNewCommit(ctx, ds, v, opts) 274 275 if err != nil { 276 return err 277 } 278 279 return db.doCommit(ctx, ds.ID(), st, opts.Policy) 280 }, 281 ) 282 } 283 284 func (db *database) CommitDangling(ctx context.Context, v types.Value, opts CommitOptions) (types.Struct, error) { 285 if opts.ParentsList == types.EmptyList || opts.ParentsList.Len() == 0 { 286 return types.Struct{}, errors.New("cannot create commit without parents") 287 } 288 289 if opts.Meta.IsZeroValue() { 290 opts.Meta = types.EmptyStruct(db.Format()) 291 } 292 293 commitStruct, err := NewCommit(ctx, v, opts.ParentsList, opts.Meta) 294 if err != nil { 295 return types.Struct{}, err 296 } 297 298 _, err = db.WriteValue(ctx, commitStruct) 299 if err != nil { 300 return types.Struct{}, err 301 } 302 303 err = db.Flush(ctx) 304 if err != nil { 305 return types.Struct{}, err 306 } 307 308 return commitStruct, nil 309 } 310 311 func (db *database) CommitValue(ctx context.Context, ds Dataset, v types.Value) (Dataset, error) { 312 return db.Commit(ctx, ds, v, CommitOptions{}) 313 } 314 315 // doCommit manages concurrent access the single logical piece of mutable state: the current Root. doCommit is 316 // optimistic in that it is attempting to update head making the assumption that currentRootHash is the hash of the 317 // current head. The call to Commit below will return an 'ErrOptimisticLockFailed' error if that assumption fails (e.g. 318 // because of a race with another writer) and the entire algorithm must be tried again. This method will also fail and 319 // return an 'ErrMergeNeeded' error if the |commit| is not a descendent of the current dataset head 320 func (db *database) doCommit(ctx context.Context, datasetID string, commit types.Struct, mergePolicy merge.Policy) error { 321 if is, err := IsCommit(commit); err != nil { 322 return err 323 } else if !is { 324 d.Panic("Can't commit a non-Commit struct to dataset %s", datasetID) 325 } 326 327 // This could loop forever, given enough simultaneous committers. BUG 2565 328 var tryCommitErr error 329 for tryCommitErr = ErrOptimisticLockFailed; tryCommitErr == ErrOptimisticLockFailed; { 330 currentRootHash, err := db.rt.Root(ctx) 331 332 if err != nil { 333 return err 334 } 335 336 currentDatasets, err := db.Datasets(ctx) 337 338 if err != nil { 339 return err 340 } 341 342 commitRef, err := db.WriteValue(ctx, commit) // will be orphaned if the tryCommitChunks() below fails 343 344 if err != nil { 345 return err 346 } 347 348 // If there's nothing in the DB yet, skip all this logic. 349 if !currentRootHash.IsEmpty() { 350 r, hasHead, err := currentDatasets.MaybeGet(ctx, types.String(datasetID)) 351 352 if err != nil { 353 return err 354 } 355 356 // First commit in dataset is always fast-forward, so go through all this iff there's already a Head for datasetID. 357 if hasHead { 358 head, err := r.(types.Ref).TargetValue(ctx, db) 359 360 if err != nil { 361 return err 362 } 363 364 currentHeadRef, err := types.NewRef(head, db.Format()) 365 366 if err != nil { 367 return err 368 } 369 370 ancestorRef, found, err := FindCommonAncestor(ctx, commitRef, currentHeadRef, db, db) 371 372 if err != nil { 373 return err 374 } 375 376 if !found { 377 return ErrMergeNeeded 378 } 379 380 // This covers all cases where currentHeadRef is not an ancestor of commit, including the following edge cases: 381 // - commit is a duplicate of currentHead. 382 // - we hit an ErrOptimisticLockFailed and looped back around because some other process changed the Head out from under us. 383 if currentHeadRef.TargetHash() != ancestorRef.TargetHash() || currentHeadRef.TargetHash() == commitRef.TargetHash() { 384 if mergePolicy == nil { 385 return ErrMergeNeeded 386 } 387 388 ancestor, err := db.validateRefAsCommit(ctx, ancestorRef) 389 if err != nil { 390 return err 391 } 392 393 currentHead, err := db.validateRefAsCommit(ctx, currentHeadRef) 394 if err != nil { 395 return err 396 } 397 398 cmVal, _, err := commit.MaybeGet(ValueField) 399 if err != nil { 400 return err 401 } 402 403 curVal, _, err := currentHead.MaybeGet(ValueField) 404 if err != nil { 405 return err 406 } 407 408 ancVal, _, err := ancestor.MaybeGet(ValueField) 409 if err != nil { 410 return err 411 } 412 413 merged, err := mergePolicy(ctx, cmVal, curVal, ancVal, db, nil) 414 if err != nil { 415 return err 416 } 417 418 l, err := types.NewList(ctx, db, commitRef, currentHeadRef) 419 if err != nil { 420 return err 421 } 422 423 newCom, err := NewCommit(ctx, merged, l, types.EmptyStruct(db.Format())) 424 if err != nil { 425 return err 426 } 427 428 commitRef, err = db.WriteValue(ctx, newCom) 429 if err != nil { 430 return err 431 } 432 } 433 } 434 } 435 436 ref, err := types.ToRefOfValue(commitRef, db.Format()) 437 if err != nil { 438 return err 439 } 440 441 currentDatasets, err = currentDatasets.Edit().Set(types.String(datasetID), ref).Map(ctx) 442 if err != nil { 443 return err 444 } 445 446 tryCommitErr = db.tryCommitChunks(ctx, currentDatasets, currentRootHash) 447 } 448 449 return tryCommitErr 450 } 451 452 func (db *database) Tag(ctx context.Context, ds Dataset, ref types.Ref, opts TagOptions) (Dataset, error) { 453 return db.doHeadUpdate( 454 ctx, 455 ds, 456 func(ds Dataset) error { 457 st, err := NewTag(ctx, ref, opts.Meta) 458 459 if err != nil { 460 return err 461 } 462 463 return db.doTag(ctx, ds.ID(), st) 464 }, 465 ) 466 } 467 468 // doTag manages concurrent access the single logical piece of mutable state: the current Root. It uses 469 // the same optimistic writing algorithm as doCommit (see above). 470 func (db *database) doTag(ctx context.Context, datasetID string, tag types.Struct) error { 471 err := db.validateTag(ctx, tag) 472 473 if err != nil { 474 return err 475 } 476 477 // This could loop forever, given enough simultaneous writers. BUG 2565 478 var tryCommitErr error 479 for tryCommitErr = ErrOptimisticLockFailed; tryCommitErr == ErrOptimisticLockFailed; { 480 currentRootHash, err := db.rt.Root(ctx) 481 482 if err != nil { 483 return err 484 } 485 486 currentDatasets, err := db.Datasets(ctx) 487 488 if err != nil { 489 return err 490 } 491 492 tagRef, err := db.WriteValue(ctx, tag) // will be orphaned if the tryCommitChunks() below fails 493 494 if err != nil { 495 return err 496 } 497 498 _, hasHead, err := currentDatasets.MaybeGet(ctx, types.String(datasetID)) 499 500 if err != nil { 501 return err 502 } 503 504 if hasHead { 505 return fmt.Errorf(fmt.Sprintf("tag %s already exists and cannot be altered after creation", datasetID)) 506 } 507 508 ref, err := types.ToRefOfValue(tagRef, db.Format()) 509 if err != nil { 510 return err 511 } 512 513 currentDatasets, err = currentDatasets.Edit().Set(types.String(datasetID), ref).Map(ctx) 514 if err != nil { 515 return err 516 } 517 518 tryCommitErr = db.tryCommitChunks(ctx, currentDatasets, currentRootHash) 519 } 520 521 return tryCommitErr 522 } 523 524 func (db *database) UpdateWorkingSet(ctx context.Context, ds Dataset, ref types.Ref, meta WorkingSetMeta, prevHash hash.Hash) (Dataset, error) { 525 return db.doHeadUpdate( 526 ctx, 527 ds, 528 func(ds Dataset) error { 529 workspace, err := NewWorkingSet(ctx, ref) 530 if err != nil { 531 return err 532 } 533 534 return db.doUpdateWorkingSet(ctx, ds.ID(), workspace, prevHash) 535 }, 536 ) 537 } 538 539 // doUpdateWorkingSet manages concurrent access the single logical piece of mutable state: the current Root. It uses 540 // the same optimistic locking write algorithm as doCommit (see above). Unlike doCommit and other methods in this file, 541 // an error is returned if the current value of the ref being written has changed. 542 // Workspace updates are serialized, but all other changes to a database's root value can proceed independently with the 543 // normal optimistic locking. 544 func (db *database) doUpdateWorkingSet(ctx context.Context, datasetID string, workingSet types.Struct, currHash hash.Hash) error { 545 err := db.validateWorkingSet(workingSet) 546 if err != nil { 547 return err 548 } 549 550 // This could loop forever, given enough simultaneous writers. BUG 2565 551 var tryCommitErr error 552 testSetFailed := false 553 for tryCommitErr = ErrOptimisticLockFailed; tryCommitErr == ErrOptimisticLockFailed && !testSetFailed; { 554 tryCommitErr = func() error { 555 db.mu.Lock() 556 defer db.mu.Unlock() 557 558 currentRootHash, err := db.rt.Root(ctx) 559 560 if err != nil { 561 return err 562 } 563 564 currentDatasets, err := db.Datasets(ctx) 565 if err != nil { 566 return err 567 } 568 569 workingSetRef, err := db.WriteValue(ctx, workingSet) // will be orphaned if the tryCommitChunks() below fails 570 if err != nil { 571 return err 572 } 573 574 ds, err := db.GetDataset(ctx, datasetID) 575 if err != nil { 576 return err 577 } 578 579 // Second level of locking: assert that the dataset value we read is unchanged from its expected value. 580 // This is separate than the whole-DB lock in the outer loop, as it only protects the value of this dataset 581 // entry. Other writers can update other values and writes chunks in the database and we will retry 582 // indefinitely. But if we find the expected value in the dataset has changed, we immediately abort and the 583 // caller must retry after reconciliation. 584 if ds.HasHead() { 585 head, ok := ds.MaybeHead() 586 if !ok { 587 panic("no head found") 588 } 589 590 h, err := head.Hash(db.Format()) 591 if err != nil { 592 return err 593 } 594 595 if h != currHash { 596 testSetFailed = true 597 return ErrOptimisticLockFailed 598 } 599 } else { 600 if !currHash.IsEmpty() { 601 panic("No ref found for workspace " + datasetID) 602 } 603 } 604 605 valRef, err := types.ToRefOfValue(workingSetRef, db.Format()) 606 if err != nil { 607 return err 608 } 609 610 currentDatasets, err = currentDatasets.Edit().Set(types.String(datasetID), valRef).Map(ctx) 611 if err != nil { 612 return err 613 } 614 615 return db.tryCommitChunks(ctx, currentDatasets, currentRootHash) 616 }() 617 } 618 619 return tryCommitErr 620 } 621 622 func (db *database) Delete(ctx context.Context, ds Dataset) (Dataset, error) { 623 return db.doHeadUpdate(ctx, ds, func(ds Dataset) error { return db.doDelete(ctx, ds.ID()) }) 624 } 625 626 // doDelete manages concurrent access the single logical piece of mutable state: the current Root. doDelete is optimistic in that it is attempting to update head making the assumption that currentRootHash is the hash of the current head. The call to Commit below will return an 'ErrOptimisticLockFailed' error if that assumption fails (e.g. because of a race with another writer) and the entire algorithm must be tried again. 627 func (db *database) doDelete(ctx context.Context, datasetIDstr string) error { 628 datasetID := types.String(datasetIDstr) 629 currentRootHash, err := db.rt.Root(ctx) 630 631 if err != nil { 632 return err 633 } 634 635 currentDatasets, err := db.Datasets(ctx) 636 637 if err != nil { 638 return err 639 } 640 641 var initialHead types.Ref 642 if r, hasHead, err := currentDatasets.MaybeGet(ctx, datasetID); err != nil { 643 return err 644 } else if !hasHead { 645 return nil 646 } else { 647 initialHead = r.(types.Ref) 648 } 649 650 for { 651 currentDatasets, err = currentDatasets.Edit().Remove(datasetID).Map(ctx) 652 if err != nil { 653 return err 654 } 655 err = db.tryCommitChunks(ctx, currentDatasets, currentRootHash) 656 if err != ErrOptimisticLockFailed { 657 break 658 } 659 660 // If the optimistic lock failed because someone changed the Head of datasetID, then return ErrMergeNeeded. If it failed because someone changed a different Dataset, we should try again. 661 currentRootHash, err = db.rt.Root(ctx) 662 663 if err != nil { 664 return err 665 } 666 667 currentDatasets, err = db.Datasets(ctx) 668 669 if err != nil { 670 return err 671 } 672 673 var r types.Value 674 var hasHead bool 675 if r, hasHead, err = currentDatasets.MaybeGet(ctx, datasetID); err != nil { 676 return err 677 } else if !hasHead || (hasHead && !initialHead.Equals(r)) { 678 err = ErrMergeNeeded 679 break 680 } 681 } 682 return err 683 } 684 685 // GC traverses the database starting at the Root and removes all unreferenced data from persistent storage. 686 func (db *database) GC(ctx context.Context) error { 687 return db.ValueStore.GC(ctx) 688 } 689 690 func (db *database) tryCommitChunks(ctx context.Context, currentDatasets types.Map, currentRootHash hash.Hash) error { 691 newRoot, err := db.WriteValue(ctx, currentDatasets) 692 693 if err != nil { 694 return err 695 } 696 697 newRootHash := newRoot.TargetHash() 698 699 if success, err := db.rt.Commit(ctx, newRootHash, currentRootHash); err != nil { 700 return err 701 } else if !success { 702 return ErrOptimisticLockFailed 703 } 704 705 return nil 706 } 707 708 func (db *database) validateRefAsCommit(ctx context.Context, r types.Ref) (types.Struct, error) { 709 v, err := db.ReadValue(ctx, r.TargetHash()) 710 711 if err != nil { 712 return types.EmptyStruct(r.Format()), err 713 } 714 715 if v == nil { 716 panic(r.TargetHash().String() + " not found") 717 } 718 719 is, err := IsCommit(v) 720 721 if err != nil { 722 return types.EmptyStruct(r.Format()), err 723 } 724 725 if !is { 726 panic("Not a commit") 727 } 728 729 return v.(types.Struct), nil 730 } 731 732 func (db *database) validateTag(ctx context.Context, t types.Struct) error { 733 is, err := IsTag(t) 734 if err != nil { 735 return err 736 } 737 if !is { 738 return fmt.Errorf("Tag struct %s is malformed, IsTag() == false", t.String()) 739 } 740 741 r, ok, err := t.MaybeGet(TagCommitRefField) 742 if err != nil { 743 return err 744 } 745 if !ok { 746 return fmt.Errorf("tag is missing field %s", TagCommitRefField) 747 } 748 749 _, err = db.validateRefAsCommit(ctx, r.(types.Ref)) 750 751 if err != nil { 752 return err 753 } 754 755 return nil 756 } 757 758 func (db *database) validateWorkingSet(t types.Struct) error { 759 is, err := IsWorkingSet(t) 760 if err != nil { 761 return err 762 } 763 if !is { 764 return fmt.Errorf("WorkingSet struct %s is malformed, IsWorkingSet() == false", t.String()) 765 } 766 767 _, ok, err := t.MaybeGet(WorkingSetRefField) 768 if err != nil { 769 return err 770 } 771 if !ok { 772 return fmt.Errorf("WorkingSet is missing field %s", WorkingSetRefField) 773 } 774 775 return nil 776 } 777 778 func buildNewCommit(ctx context.Context, ds Dataset, v types.Value, opts CommitOptions) (types.Struct, error) { 779 parents := opts.ParentsList 780 if parents == types.EmptyList || parents.Len() == 0 { 781 var err error 782 parents, err = types.NewList(ctx, ds.Database()) 783 if err != nil { 784 return types.EmptyStruct(ds.Database().Format()), err 785 } 786 787 if headRef, ok, err := ds.MaybeHeadRef(); err != nil { 788 return types.EmptyStruct(ds.Database().Format()), err 789 } else if ok { 790 le := parents.Edit().Append(headRef) 791 parents, err = le.List(ctx) 792 if err != nil { 793 return types.EmptyStruct(ds.Database().Format()), err 794 } 795 } 796 } 797 798 meta := opts.Meta 799 if meta.IsZeroValue() { 800 meta = types.EmptyStruct(ds.Database().Format()) 801 } 802 return NewCommit(ctx, v, parents, meta) 803 } 804 805 func (db *database) doHeadUpdate(ctx context.Context, ds Dataset, updateFunc func(ds Dataset) error) (Dataset, error) { 806 err := updateFunc(ds) 807 808 if err != nil { 809 return Dataset{}, err 810 } 811 812 return db.GetDataset(ctx, ds.ID()) 813 }