402 lines
14 KiB
Go
402 lines
14 KiB
Go
// Copyright 2025 Dolthub, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package conflicts
|
|
|
|
import (
|
|
"context"
|
|
"maps"
|
|
"slices"
|
|
|
|
"github.com/cockroachdb/errors"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
|
|
"github.com/dolthub/dolt/go/store/hash"
|
|
"github.com/dolthub/dolt/go/store/prolly"
|
|
"github.com/dolthub/dolt/go/store/prolly/tree"
|
|
"github.com/dolthub/go-mysql-server/sql"
|
|
|
|
"github.com/dolthub/doltgresql/core/id"
|
|
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
|
|
pgtypes "github.com/dolthub/doltgresql/server/types"
|
|
)
|
|
|
|
// Collection contains a collection of conflicts.
|
|
type Collection struct {
|
|
accessCache map[id.Id]Conflict // This cache is used for general access when you know the exact ID
|
|
nameCache map[doltdb.TableName]id.Id // This cache is used for ID resolution, since exact table names are always used
|
|
idCache []id.Id // This cache simply contains the name of every root object
|
|
mapHash hash.Hash // This is cached so that we don't have to calculate the hash every time
|
|
underlyingMap prolly.AddressMap
|
|
ns tree.NodeStore
|
|
}
|
|
|
|
// Conflict represents a root object conflict.
|
|
type Conflict struct {
|
|
ID id.Id
|
|
FromHash string
|
|
RootObjectID objinterface.RootObjectID
|
|
Ours objinterface.RootObject
|
|
Theirs objinterface.RootObject
|
|
Ancestor objinterface.RootObject
|
|
}
|
|
|
|
var _ objinterface.Collection = (*Collection)(nil)
|
|
var _ objinterface.RootObject = Conflict{}
|
|
var _ objinterface.Conflict = Conflict{}
|
|
var _ doltdb.ConflictRootObject = Conflict{}
|
|
|
|
// NewCollection returns a new Collection.
|
|
func NewCollection(ctx context.Context, underlyingMap prolly.AddressMap, ns tree.NodeStore) (*Collection, error) {
|
|
collection := &Collection{
|
|
accessCache: make(map[id.Id]Conflict),
|
|
nameCache: make(map[doltdb.TableName]id.Id),
|
|
idCache: nil,
|
|
mapHash: hash.Hash{},
|
|
underlyingMap: underlyingMap,
|
|
ns: ns,
|
|
}
|
|
return collection, collection.reloadCaches(ctx)
|
|
}
|
|
|
|
// GetConflict returns the conflict with the given ID. Returns a conflict with an invalid ID if it cannot be found
|
|
// (Conflict.ID.IsValid() == false).
|
|
func (pgc *Collection) GetConflict(ctx context.Context, conflictID id.Id) (Conflict, error) {
|
|
if conflict, ok := pgc.accessCache[conflictID]; ok {
|
|
return conflict, nil
|
|
}
|
|
return Conflict{}, nil
|
|
}
|
|
|
|
// HasConflict returns whether the conflict is present.
|
|
func (pgc *Collection) HasConflict(ctx context.Context, conflictID id.Id) bool {
|
|
_, ok := pgc.accessCache[conflictID]
|
|
return ok
|
|
}
|
|
|
|
// AddConflict adds a new conflict.
|
|
func (pgc *Collection) AddConflict(ctx context.Context, conflict Conflict) error {
|
|
// First we'll check to see if it exists
|
|
if _, ok := pgc.accessCache[conflict.ID]; ok {
|
|
return errors.Errorf(`"%s" already has a conflict`, conflict.Ours.Name())
|
|
}
|
|
|
|
// Now we'll add the conflict to our map
|
|
data, err := conflict.Serialize(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
h, err := pgc.ns.WriteBytes(ctx, data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mapEditor := pgc.underlyingMap.Editor()
|
|
if err = mapEditor.Add(ctx, string(conflict.ID), h); err != nil {
|
|
return err
|
|
}
|
|
newMap, err := mapEditor.Flush(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pgc.underlyingMap = newMap
|
|
pgc.mapHash = pgc.underlyingMap.HashOf()
|
|
return pgc.reloadCaches(ctx)
|
|
}
|
|
|
|
// DropConflict drops an existing conflict.
|
|
func (pgc *Collection) DropConflict(ctx context.Context, conflictIDs ...id.Id) error {
|
|
if len(conflictIDs) == 0 {
|
|
return nil
|
|
}
|
|
// Check that each name exists before performing any deletions
|
|
for _, conflictID := range conflictIDs {
|
|
if _, ok := pgc.accessCache[conflictID]; !ok {
|
|
return errors.Errorf(`conflict %s does not exist`, conflictID.String())
|
|
}
|
|
}
|
|
|
|
// Now we'll remove the conflicts from the map
|
|
mapEditor := pgc.underlyingMap.Editor()
|
|
for _, conflictID := range conflictIDs {
|
|
err := mapEditor.Delete(ctx, string(conflictID))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
newMap, err := mapEditor.Flush(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pgc.underlyingMap = newMap
|
|
pgc.mapHash = pgc.underlyingMap.HashOf()
|
|
return pgc.reloadCaches(ctx)
|
|
}
|
|
|
|
// iterateIDs iterates over all conflict IDs in the collection.
|
|
func (pgc *Collection) iterateIDs(ctx context.Context, callback func(conflictID id.Id) (stop bool, err error)) error {
|
|
for _, conflictID := range pgc.idCache {
|
|
stop, err := callback(conflictID)
|
|
if err != nil {
|
|
return err
|
|
} else if stop {
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// IterateConflicts iterates over all conflicts in the collection.
|
|
func (pgc *Collection) IterateConflicts(ctx context.Context, callback func(conflict Conflict) (stop bool, err error)) error {
|
|
for _, conflictID := range pgc.idCache {
|
|
stop, err := callback(pgc.accessCache[conflictID])
|
|
if err != nil {
|
|
return err
|
|
} else if stop {
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Clone returns a new *Collection with the same contents as the original.
|
|
func (pgc *Collection) Clone(ctx context.Context) *Collection {
|
|
return &Collection{
|
|
accessCache: maps.Clone(pgc.accessCache),
|
|
nameCache: maps.Clone(pgc.nameCache),
|
|
idCache: slices.Clone(pgc.idCache),
|
|
underlyingMap: pgc.underlyingMap,
|
|
mapHash: pgc.mapHash,
|
|
ns: pgc.ns,
|
|
}
|
|
}
|
|
|
|
// Map writes any cached sequences to the underlying map, and then returns the underlying map.
|
|
func (pgc *Collection) Map(ctx context.Context) (prolly.AddressMap, error) {
|
|
return pgc.underlyingMap, nil
|
|
}
|
|
|
|
// DiffersFrom returns true when the hash that is associated with the underlying map for this collection is different
|
|
// from the hash in the given root.
|
|
func (pgc *Collection) DiffersFrom(ctx context.Context, root objinterface.RootValue) bool {
|
|
hashOnGivenRoot, err := pgc.LoadCollectionHash(ctx, root)
|
|
if err != nil {
|
|
return true
|
|
}
|
|
if pgc.mapHash.Equal(hashOnGivenRoot) {
|
|
return false
|
|
}
|
|
// An empty map should match an uninitialized collection on the root
|
|
count, err := pgc.underlyingMap.Count()
|
|
if err == nil && count == 0 && hashOnGivenRoot.IsEmpty() {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// reloadCaches writes the underlying map's contents to the caches.
|
|
func (pgc *Collection) reloadCaches(ctx context.Context) error {
|
|
count, err := pgc.underlyingMap.Count()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
clear(pgc.accessCache)
|
|
clear(pgc.nameCache)
|
|
pgc.mapHash = pgc.underlyingMap.HashOf()
|
|
pgc.idCache = make([]id.Id, 0, count)
|
|
|
|
return pgc.underlyingMap.IterAll(ctx, func(_ string, h hash.Hash) error {
|
|
if h.IsEmpty() {
|
|
return nil
|
|
}
|
|
data, err := pgc.ns.ReadBytes(ctx, h)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
conflict, err := DeserializeConflict(ctx, data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pgc.accessCache[conflict.ID] = conflict
|
|
pgc.nameCache[conflict.Name()] = conflict.ID
|
|
pgc.idCache = append(pgc.idCache, conflict.ID)
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// DiffCount implements the interface objinterface.Conflict.
|
|
func (conflict Conflict) DiffCount(ctx *sql.Context) (int, error) {
|
|
diffs, _, err := conflict.Diffs(ctx)
|
|
return len(diffs), err
|
|
}
|
|
|
|
// Diffs implements the interface objinterface.Conflict.
|
|
func (conflict Conflict) Diffs(ctx context.Context) ([]objinterface.RootObjectDiff, objinterface.RootObject, error) {
|
|
return DiffRootObjects(ctx, conflict.RootObjectID, conflict.FromHash, conflict.Ours, conflict.Theirs, conflict.Ancestor)
|
|
}
|
|
|
|
// FieldType implements the interface objinterface.Conflict.
|
|
func (conflict Conflict) FieldType(ctx context.Context, name string) *pgtypes.DoltgresType {
|
|
return GetFieldType(ctx, conflict.RootObjectID, name)
|
|
}
|
|
|
|
// GetContainedRootObjectID implements the interface objinterface.RootObject.
|
|
func (conflict Conflict) GetContainedRootObjectID() objinterface.RootObjectID {
|
|
return conflict.RootObjectID
|
|
}
|
|
|
|
// GetID implements the interface objinterface.RootObject.
|
|
func (conflict Conflict) GetID() id.Id {
|
|
return conflict.ID
|
|
}
|
|
|
|
// GetRootObjectID implements the interface objinterface.RootObject.
|
|
func (conflict Conflict) GetRootObjectID() objinterface.RootObjectID {
|
|
return objinterface.RootObjectID_Conflicts
|
|
}
|
|
|
|
// HashOf implements the interface objinterface.RootObject.
|
|
func (conflict Conflict) HashOf(ctx context.Context) (hash.Hash, error) {
|
|
data, err := conflict.Serialize(ctx)
|
|
if err != nil {
|
|
return hash.Hash{}, err
|
|
}
|
|
return hash.Of(data), nil
|
|
}
|
|
|
|
// Name implements the interface objinterface.RootObject.
|
|
func (conflict Conflict) Name() doltdb.TableName {
|
|
if conflict.Ours != nil {
|
|
return conflict.Ours.Name()
|
|
} else {
|
|
return conflict.Theirs.Name()
|
|
}
|
|
}
|
|
|
|
// RemoveDiffs implements the interface doltdb.ConflictRootObject.
|
|
func (conflict Conflict) RemoveDiffs(ctx *sql.Context, diffs []doltdb.RootObjectDiff) (doltdb.ConflictRootObject, error) {
|
|
// This is only called from within Dolt, and it specifically deals with modifying the conflict collection on the
|
|
// root. It is therefore safe to clear context values, to ensure the root changes are not overwritten.
|
|
ClearContextValues(ctx)
|
|
// We need to handle the root object field name in a special way, since it's how we select which side to use in full
|
|
if len(diffs) == 1 {
|
|
diff := diffs[0].(objinterface.RootObjectDiff)
|
|
if diff.FieldName == objinterface.FIELD_NAME_ROOT_OBJECT {
|
|
if conflict.Ours == nil {
|
|
conflict.Theirs = nil
|
|
} else {
|
|
conflict.Theirs = conflict.Ours
|
|
}
|
|
conflict.Ancestor = nil
|
|
return conflict, nil
|
|
}
|
|
}
|
|
// Deletion is equivalent to setting the value in "theirs" to "ours", as the same value cannot conflict with itself.
|
|
// We will only reach this point if both "ours" and "theirs" are non-nil entries, so the above relies on safe assumptions.
|
|
for _, doltDiff := range diffs {
|
|
diff := doltDiff.(objinterface.RootObjectDiff)
|
|
newTheirs, err := UpdateField(ctx, conflict.RootObjectID, conflict.Theirs, diff.FieldName, diff.OurValue)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
conflict.Theirs = newTheirs
|
|
}
|
|
return conflict, nil
|
|
}
|
|
|
|
// Rows implements the interface doltdb.ConflictRootObject.
|
|
func (conflict Conflict) Rows(ctx *sql.Context) (sql.RowIter, error) {
|
|
diffs, _, err := conflict.Diffs(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rows := make([]sql.Row, len(diffs))
|
|
for i, diff := range diffs {
|
|
rows[i], err = diff.ToRow(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return sql.RowsToRowIter(rows...), nil
|
|
}
|
|
|
|
// Schema implements the interface doltdb.ConflictRootObject.
|
|
func (conflict Conflict) Schema(originatingTableName string) sql.Schema {
|
|
sch := objinterface.RootObjectDiffSchema.Copy()
|
|
for _, col := range sch {
|
|
col.Source = originatingTableName
|
|
}
|
|
return sch
|
|
}
|
|
|
|
// UpdateField implements the interface doltdb.ConflictRootObject.
|
|
func (conflict Conflict) UpdateField(ctx *sql.Context, o doltdb.RootObjectDiff, n doltdb.RootObjectDiff) (doltdb.ConflictRootObject, error) {
|
|
// This is only called from within Dolt, and it specifically deals with modifying the conflict collection on the
|
|
// root. It is therefore safe to clear context values, to ensure the root changes are not overwritten.
|
|
ClearContextValues(ctx)
|
|
oldDiff := o.(objinterface.RootObjectDiff)
|
|
newDiff := n.(objinterface.RootObjectDiff)
|
|
// We need to handle the root object field name in a special way, since it's how we select which side to use in full
|
|
if oldDiff.FieldName == objinterface.FIELD_NAME_ROOT_OBJECT {
|
|
switch newDiff.OurValue.(string) {
|
|
case objinterface.FIELD_NAME_OURS:
|
|
conflict.Theirs = conflict.Ours
|
|
case objinterface.FIELD_NAME_THEIRS:
|
|
conflict.Ours = conflict.Theirs
|
|
case objinterface.FIELD_NAME_ANCESTOR:
|
|
conflict.Ours = conflict.Ancestor
|
|
conflict.Theirs = conflict.Ancestor
|
|
default:
|
|
return nil, errors.Newf("cannot replace the object with `%s`", newDiff.OurValue.(string))
|
|
}
|
|
conflict.Ancestor = nil
|
|
return conflict, nil
|
|
}
|
|
newOurs, err := UpdateField(ctx, conflict.RootObjectID, conflict.Ours, oldDiff.FieldName, newDiff.OurValue)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
conflict.Ours = newOurs
|
|
return conflict, nil
|
|
}
|
|
|
|
// ClearContextValues clears context values, and is declared in a different package. It is assigned here by an Init
|
|
// function to get around import cycles.
|
|
var ClearContextValues = func(ctx *sql.Context) {
|
|
panic("ClearContextValues was never initialized")
|
|
}
|
|
|
|
// DiffRootObjects handles conflict diffs, and is declared in a different package. It is assigned here by an Init
|
|
// function to get around import cycles.
|
|
var DiffRootObjects = func(ctx context.Context, rootObjID objinterface.RootObjectID, fromHash string, ours, theirs, ancestor objinterface.RootObject) ([]objinterface.RootObjectDiff, objinterface.RootObject, error) {
|
|
return nil, nil, errors.New("DiffRootObjects was never initialized")
|
|
}
|
|
|
|
// GetFieldType handles type fetching for fields, and is declared in a different package. It is assigned here by an Init
|
|
// function to get around import cycles.
|
|
var GetFieldType = func(ctx context.Context, rootObjID objinterface.RootObjectID, fieldName string) *pgtypes.DoltgresType {
|
|
panic("GetFieldType was never initialized")
|
|
}
|
|
|
|
// ResolveNameExternal handles name resolution across all collection types, and is declared in a different package. It
|
|
// is assigned here by an Init function to get around import cycles.
|
|
var ResolveNameExternal = func(ctx context.Context, name doltdb.TableName, rootObjects []objinterface.RootObject) (doltdb.TableName, id.Id, error) {
|
|
panic("ResolveNameExternal was never initialized")
|
|
}
|
|
|
|
// UpdateField handles updating fields in a root object, and is declared in a different package. It is assigned here by
|
|
// an Init function to get around import cycles.
|
|
var UpdateField = func(ctx context.Context, rootObjID objinterface.RootObjectID, rootObject objinterface.RootObject, fieldName string, newValue any) (objinterface.RootObject, error) {
|
|
return nil, errors.New("UpdateField was never initialized")
|
|
}
|