Files
dolthub--doltgresql/core/conflicts/collection.go
T
2026-07-13 12:32:25 +08:00

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")
}