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  }