github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/doltcore/doltdb/workingset.go (about) 1 // Copyright 2021 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 package doltdb 16 17 import ( 18 "context" 19 "fmt" 20 "time" 21 22 "github.com/dolthub/go-mysql-server/sql" 23 24 "github.com/dolthub/dolt/go/libraries/doltcore/ref" 25 "github.com/dolthub/dolt/go/libraries/doltcore/schema" 26 "github.com/dolthub/dolt/go/store/datas" 27 "github.com/dolthub/dolt/go/store/hash" 28 "github.com/dolthub/dolt/go/store/prolly/tree" 29 "github.com/dolthub/dolt/go/store/types" 30 ) 31 32 // RebaseState tracks the state of an in-progress rebase action. It records the name of the branch being rebased, the 33 // commit onto which the new commits will be rebased, and the root value of the previous working set, which is used if 34 // the rebase is aborted and the working set needs to be restored to its previous state. 35 type RebaseState struct { 36 preRebaseWorking RootValue 37 ontoCommit *Commit 38 branch string 39 } 40 41 // Branch returns the name of the branch being actively rebased. This is the branch that will be updated to point 42 // at the new commits created by the rebase operation. 43 func (rs RebaseState) Branch() string { 44 return rs.branch 45 } 46 47 // OntoCommit returns the commit onto which new commits are being rebased by the active rebase operation. 48 func (rs RebaseState) OntoCommit() *Commit { 49 return rs.ontoCommit 50 } 51 52 // PreRebaseWorkingRoot stores the RootValue of the working set immediately before the current rebase operation was 53 // started. This value is used when a rebase is aborted, so that the working set can be restored to its previous state. 54 func (rs RebaseState) PreRebaseWorkingRoot() RootValue { 55 return rs.preRebaseWorking 56 } 57 58 type MergeState struct { 59 // the source commit 60 commit *Commit 61 // the spec string that was used to specify |commit| 62 commitSpecStr string 63 preMergeWorking RootValue 64 unmergableTables []string 65 mergedTables []string 66 // isCherryPick is set to true when the in-progress merge is a cherry-pick. This is needed so that 67 // commit knows to NOT create a commit with multiple parents when creating a commit for a cherry-pick. 68 isCherryPick bool 69 } 70 71 // todo(andy): this might make more sense in pkg merge 72 type SchemaConflict struct { 73 ToSch, FromSch schema.Schema 74 ToFks, FromFks []ForeignKey 75 ToParentSchemas map[string]schema.Schema 76 FromParentSchemas map[string]schema.Schema 77 toTbl, fromTbl *Table 78 } 79 80 func (sc SchemaConflict) GetConflictingTables() (ours, theirs *Table) { 81 return sc.toTbl, sc.fromTbl 82 } 83 84 // TodoWorkingSetMeta returns an incomplete WorkingSetMeta, suitable for methods that don't have the means to construct 85 // a real one. These should be considered temporary and cleaned up when possible, similar to Context.TODO 86 func TodoWorkingSetMeta() *datas.WorkingSetMeta { 87 return &datas.WorkingSetMeta{ 88 Name: "TODO", 89 Email: "TODO", 90 Timestamp: uint64(time.Now().Unix()), 91 Description: "TODO", 92 } 93 } 94 95 // MergeStateFromCommitAndWorking returns a new MergeState. 96 // Most clients should not construct MergeState objects directly, but instead use WorkingSet.StartMerge 97 func MergeStateFromCommitAndWorking(commit *Commit, preMergeWorking RootValue) *MergeState { 98 return &MergeState{commit: commit, preMergeWorking: preMergeWorking} 99 } 100 101 func (m MergeState) Commit() *Commit { 102 return m.commit 103 } 104 105 func (m MergeState) CommitSpecStr() string { 106 return m.commitSpecStr 107 } 108 109 // IsCherryPick returns true if the current merge state is for a cherry-pick operation. Cherry-picks use the same 110 // code as merge, but need slightly different behavior (e.g. only recording one commit parent, instead of two). 111 func (m MergeState) IsCherryPick() bool { 112 return m.isCherryPick 113 } 114 115 func (m MergeState) PreMergeWorkingRoot() RootValue { 116 return m.preMergeWorking 117 } 118 119 type SchemaConflictFn func(table string, conflict SchemaConflict) error 120 121 func (m MergeState) HasSchemaConflicts() bool { 122 return len(m.unmergableTables) > 0 123 } 124 125 func (m MergeState) TablesWithSchemaConflicts() []string { 126 return m.unmergableTables 127 } 128 129 func (m MergeState) MergedTables() []string { 130 return m.mergedTables 131 } 132 133 func (m MergeState) IterSchemaConflicts(ctx context.Context, ddb *DoltDB, cb SchemaConflictFn) (err error) { 134 var to, from RootValue 135 136 to = m.preMergeWorking 137 if from, err = m.commit.GetRootValue(ctx); err != nil { 138 return err 139 } 140 141 toFKs, err := to.GetForeignKeyCollection(ctx) 142 if err != nil { 143 return err 144 } 145 toSchemas, err := GetAllSchemas(ctx, to) 146 if err != nil { 147 return err 148 } 149 150 fromFKs, err := from.GetForeignKeyCollection(ctx) 151 if err != nil { 152 return err 153 } 154 fromSchemas, err := GetAllSchemas(ctx, from) 155 if err != nil { 156 return err 157 } 158 159 for _, name := range m.unmergableTables { 160 var sc SchemaConflict 161 var hasToTable bool 162 if sc.toTbl, hasToTable, err = to.GetTable(ctx, TableName{Name: name}); err != nil { 163 return err 164 } 165 if hasToTable { 166 if sc.ToSch, err = sc.toTbl.GetSchema(ctx); err != nil { 167 return err 168 } 169 } 170 171 var hasFromTable bool 172 // todo: handle schema conflicts for renamed tables 173 if sc.fromTbl, hasFromTable, err = from.GetTable(ctx, TableName{Name: name}); err != nil { 174 return err 175 } 176 if hasFromTable { 177 if sc.FromSch, err = sc.fromTbl.GetSchema(ctx); err != nil { 178 return err 179 } 180 } 181 182 sc.ToFks, _ = toFKs.KeysForTable(name) 183 sc.ToParentSchemas = toSchemas 184 185 sc.FromFks, _ = fromFKs.KeysForTable(name) 186 sc.FromParentSchemas = fromSchemas 187 188 if err = cb(name, sc); err != nil { 189 return err 190 } 191 } 192 return nil 193 } 194 195 type WorkingSet struct { 196 Name string 197 meta *datas.WorkingSetMeta 198 addr *hash.Hash 199 workingRoot RootValue 200 stagedRoot RootValue 201 mergeState *MergeState 202 rebaseState *RebaseState 203 } 204 205 var _ Rootish = &WorkingSet{} 206 207 // TODO: remove this, require working and staged 208 func EmptyWorkingSet(wsRef ref.WorkingSetRef) *WorkingSet { 209 return &WorkingSet{ 210 Name: wsRef.GetPath(), 211 } 212 } 213 214 func (ws WorkingSet) WithStagedRoot(stagedRoot RootValue) *WorkingSet { 215 ws.stagedRoot = stagedRoot 216 return &ws 217 } 218 219 func (ws WorkingSet) WithWorkingRoot(workingRoot RootValue) *WorkingSet { 220 ws.workingRoot = workingRoot 221 return &ws 222 } 223 224 func (ws WorkingSet) WithMergeState(mergeState *MergeState) *WorkingSet { 225 ws.mergeState = mergeState 226 return &ws 227 } 228 229 func (ws WorkingSet) WithRebaseState(rebaseState *RebaseState) *WorkingSet { 230 ws.rebaseState = rebaseState 231 return &ws 232 } 233 234 func (ws WorkingSet) WithUnmergableTables(tables []string) *WorkingSet { 235 ws.mergeState.unmergableTables = tables 236 return &ws 237 } 238 239 func (ws WorkingSet) WithMergedTables(tables []string) *WorkingSet { 240 ws.mergeState.mergedTables = tables 241 return &ws 242 } 243 244 func (ws WorkingSet) StartMerge(commit *Commit, commitSpecStr string) *WorkingSet { 245 ws.mergeState = &MergeState{ 246 commit: commit, 247 commitSpecStr: commitSpecStr, 248 preMergeWorking: ws.workingRoot, 249 } 250 251 return &ws 252 } 253 254 // StartRebase adds rebase tracking metadata to a new working set instance and returns it. Callers must then persist 255 // the returned working set in a session in order for the new working set to be recorded. |ontoCommit| specifies the 256 // commit that serves as the base commit for the new commits that will be created by the rebase process, |branch| is 257 // the branch that is being rebased, and |previousRoot| is root value of the branch being rebased. The HEAD and STAGED 258 // root values of the branch being rebased must match |previousRoot|; WORKING may be a different root value, but ONLY 259 // if it contains only ignored tables. 260 func (ws WorkingSet) StartRebase(ctx *sql.Context, ontoCommit *Commit, branch string, previousRoot RootValue) (*WorkingSet, error) { 261 ws.rebaseState = &RebaseState{ 262 ontoCommit: ontoCommit, 263 preRebaseWorking: previousRoot, 264 branch: branch, 265 } 266 267 ontoRoot, err := ontoCommit.GetRootValue(ctx) 268 if err != nil { 269 return nil, err 270 } 271 ws.workingRoot = ontoRoot 272 ws.stagedRoot = ontoRoot 273 274 return &ws, nil 275 } 276 277 // StartCherryPick creates and returns a new working set based off of the current |ws| with the specified |commit| 278 // and |commitSpecStr| referring to the commit being cherry-picked. The returned WorkingSet records that a cherry-pick 279 // operation is in progress (i.e. conflicts being resolved). Note that this function does not update the current 280 // session – the returned WorkingSet must still be set using DoltSession.SetWorkingSet(). 281 func (ws WorkingSet) StartCherryPick(commit *Commit, commitSpecStr string) *WorkingSet { 282 ws.mergeState = &MergeState{ 283 commit: commit, 284 commitSpecStr: commitSpecStr, 285 preMergeWorking: ws.workingRoot, 286 isCherryPick: true, 287 } 288 return &ws 289 } 290 291 func (ws WorkingSet) AbortMerge() *WorkingSet { 292 ws.workingRoot = ws.mergeState.PreMergeWorkingRoot() 293 ws.stagedRoot = ws.workingRoot 294 ws.mergeState = nil 295 return &ws 296 } 297 298 func (ws WorkingSet) AbortRebase() *WorkingSet { 299 ws.workingRoot = ws.rebaseState.preRebaseWorking 300 ws.stagedRoot = ws.workingRoot 301 ws.rebaseState = nil 302 return &ws 303 } 304 305 func (ws WorkingSet) ClearMerge() *WorkingSet { 306 ws.mergeState = nil 307 return &ws 308 } 309 310 func (ws WorkingSet) ClearRebase() *WorkingSet { 311 ws.rebaseState = nil 312 return &ws 313 } 314 315 func (ws *WorkingSet) WorkingRoot() RootValue { 316 return ws.workingRoot 317 } 318 319 func (ws *WorkingSet) StagedRoot() RootValue { 320 return ws.stagedRoot 321 } 322 323 func (ws *WorkingSet) MergeState() *MergeState { 324 return ws.mergeState 325 } 326 327 func (ws *WorkingSet) RebaseState() *RebaseState { 328 return ws.rebaseState 329 } 330 331 func (ws *WorkingSet) MergeActive() bool { 332 return ws.mergeState != nil 333 } 334 335 func (ws *WorkingSet) RebaseActive() bool { 336 return ws.rebaseState != nil 337 } 338 339 // MergeCommitParents returns true if there is an active merge in progress and 340 // the recorded commit being merged into the active branch should be included as 341 // a second parent of the created commit. This is the expected behavior for a 342 // normal merge, but not for other pseudo-merges, like cherry-picks or reverts, 343 // where the created commit should only have one parent. 344 func (ws *WorkingSet) MergeCommitParents() bool { 345 if !ws.MergeActive() { 346 return false 347 } 348 return ws.MergeState().IsCherryPick() == false 349 } 350 351 func (ws WorkingSet) Meta() *datas.WorkingSetMeta { 352 return ws.meta 353 } 354 355 // newWorkingSet creates a new WorkingSet object. 356 func newWorkingSet(ctx context.Context, name string, vrw types.ValueReadWriter, ns tree.NodeStore, ds datas.Dataset) (*WorkingSet, error) { 357 dsws, err := ds.HeadWorkingSet() 358 if err != nil { 359 return nil, err 360 } 361 362 meta := dsws.Meta 363 if meta == nil { 364 meta = &datas.WorkingSetMeta{ 365 Name: "not present", 366 Email: "not present", 367 Timestamp: 0, 368 Description: "not present", 369 } 370 } 371 372 workingRootVal, err := vrw.ReadValue(ctx, dsws.WorkingAddr) 373 if err != nil { 374 return nil, err 375 } 376 workingRoot, err := NewRootValue(ctx, vrw, ns, workingRootVal) 377 if err != nil { 378 return nil, err 379 } 380 381 var stagedRoot RootValue 382 if dsws.StagedAddr != nil { 383 stagedRootVal, err := vrw.ReadValue(ctx, *dsws.StagedAddr) 384 if err != nil { 385 return nil, err 386 } 387 388 stagedRoot, err = NewRootValue(ctx, vrw, ns, stagedRootVal) 389 if err != nil { 390 return nil, err 391 } 392 } 393 394 var mergeState *MergeState 395 if dsws.MergeState != nil { 396 preMergeWorkingAddr, err := dsws.MergeState.PreMergeWorkingAddr(ctx, vrw) 397 if err != nil { 398 return nil, err 399 } 400 fromDCommit, err := dsws.MergeState.FromCommit(ctx, vrw) 401 if err != nil { 402 return nil, err 403 } 404 commitSpec, err := dsws.MergeState.FromCommitSpec(ctx, vrw) 405 if err != nil { 406 return nil, err 407 } 408 409 if fromDCommit.IsGhost() { 410 return nil, ErrGhostCommitEncountered 411 } 412 413 commit, err := NewCommit(ctx, vrw, ns, fromDCommit) 414 if err != nil { 415 return nil, err 416 } 417 418 preMergeWorkingV, err := vrw.ReadValue(ctx, preMergeWorkingAddr) 419 if err != nil { 420 return nil, err 421 } 422 423 preMergeWorkingRoot, err := NewRootValue(ctx, vrw, ns, preMergeWorkingV) 424 if err != nil { 425 return nil, err 426 } 427 428 unmergableTables, err := dsws.MergeState.UnmergableTables(ctx, vrw) 429 if err != nil { 430 return nil, err 431 } 432 433 isCherryPick, err := dsws.MergeState.IsCherryPick(ctx, vrw) 434 if err != nil { 435 return nil, err 436 } 437 438 mergeState = &MergeState{ 439 commit: commit, 440 commitSpecStr: commitSpec, 441 preMergeWorking: preMergeWorkingRoot, 442 unmergableTables: unmergableTables, 443 isCherryPick: isCherryPick, 444 } 445 } 446 447 var rebaseState *RebaseState 448 if dsws.RebaseState != nil { 449 preRebaseWorkingAddr := dsws.RebaseState.PreRebaseWorkingAddr() 450 preRebaseWorkingV, err := vrw.ReadValue(ctx, preRebaseWorkingAddr) 451 if err != nil { 452 return nil, err 453 } 454 455 preRebaseWorkingRoot, err := NewRootValue(ctx, vrw, ns, preRebaseWorkingV) 456 if err != nil { 457 return nil, err 458 } 459 460 datasOntoCommit, err := dsws.RebaseState.OntoCommit(ctx, vrw) 461 if err != nil { 462 return nil, err 463 } 464 465 if datasOntoCommit.IsGhost() { 466 return nil, ErrGhostCommitEncountered 467 } 468 469 ontoCommit, err := NewCommit(ctx, vrw, ns, datasOntoCommit) 470 if err != nil { 471 return nil, err 472 } 473 474 rebaseState = &RebaseState{ 475 preRebaseWorking: preRebaseWorkingRoot, 476 ontoCommit: ontoCommit, 477 branch: dsws.RebaseState.Branch(ctx), 478 } 479 } 480 481 addr, _ := ds.MaybeHeadAddr() 482 483 return &WorkingSet{ 484 Name: name, 485 meta: meta, 486 addr: &addr, 487 workingRoot: workingRoot, 488 stagedRoot: stagedRoot, 489 mergeState: mergeState, 490 rebaseState: rebaseState, 491 }, nil 492 } 493 494 // ResolveRootValue implements Rootish. 495 func (ws *WorkingSet) ResolveRootValue(context.Context) (RootValue, error) { 496 return ws.WorkingRoot(), nil 497 } 498 499 // HashOf returns the hash of the workingset struct, which is not the same as the hash of the root value stored in the 500 // working set. This value is used for optimistic locking when updating a working set for a head ref. 501 func (ws *WorkingSet) HashOf() (hash.Hash, error) { 502 if ws == nil || ws.addr == nil { 503 return hash.Hash{}, nil 504 } 505 return *ws.addr, nil 506 } 507 508 // Ref returns a WorkingSetRef for this WorkingSet. 509 func (ws *WorkingSet) Ref() ref.WorkingSetRef { 510 return ref.NewWorkingSetRef(ws.Name) 511 } 512 513 // writeValues write the values in this working set to the database and returns a datas.WorkingSetSpec with the 514 // new values in it. 515 func (ws *WorkingSet) writeValues(ctx context.Context, db *DoltDB, meta *datas.WorkingSetMeta) (spec *datas.WorkingSetSpec, err error) { 516 if ws.stagedRoot == nil || ws.workingRoot == nil { 517 return nil, fmt.Errorf("StagedRoot and workingRoot must be set. This is a bug.") 518 } 519 520 var r RootValue 521 r, workingRoot, err := db.writeRootValue(ctx, ws.workingRoot) 522 if err != nil { 523 return nil, err 524 } 525 ws.workingRoot = r 526 527 r, stagedRoot, err := db.writeRootValue(ctx, ws.stagedRoot) 528 if err != nil { 529 return nil, err 530 } 531 ws.stagedRoot = r 532 533 var mergeState *datas.MergeState 534 if ws.mergeState != nil { 535 r, preMergeWorking, err := db.writeRootValue(ctx, ws.mergeState.preMergeWorking) 536 if err != nil { 537 return nil, err 538 } 539 ws.mergeState.preMergeWorking = r 540 541 h, err := ws.mergeState.commit.HashOf() 542 if err != nil { 543 return nil, err 544 } 545 dCommit, err := datas.LoadCommitAddr(ctx, db.vrw, h) 546 if err != nil { 547 return nil, err 548 } 549 550 mergeState, err = datas.NewMergeState(ctx, db.vrw, preMergeWorking, dCommit, ws.mergeState.commitSpecStr, ws.mergeState.unmergableTables, ws.mergeState.isCherryPick) 551 if err != nil { 552 return nil, err 553 } 554 } 555 556 var rebaseState *datas.RebaseState 557 if ws.rebaseState != nil { 558 r, preRebaseWorking, err := db.writeRootValue(ctx, ws.rebaseState.preRebaseWorking) 559 if err != nil { 560 return nil, err 561 } 562 ws.rebaseState.preRebaseWorking = r 563 564 h, err := ws.rebaseState.ontoCommit.HashOf() 565 if err != nil { 566 return nil, err 567 } 568 dCommit, err := datas.LoadCommitAddr(ctx, db.vrw, h) 569 if err != nil { 570 return nil, err 571 } 572 573 rebaseState = datas.NewRebaseState(preRebaseWorking.TargetHash(), dCommit.Addr(), ws.rebaseState.branch) 574 } 575 576 return &datas.WorkingSetSpec{ 577 Meta: meta, 578 WorkingRoot: workingRoot, 579 StagedRoot: stagedRoot, 580 MergeState: mergeState, 581 RebaseState: rebaseState, 582 }, nil 583 }