chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:32:25 +08:00
commit e014feafe1
2285 changed files with 1131979 additions and 0 deletions
+474
View File
@@ -0,0 +1,474 @@
// Copyright 2024 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 sequences
import (
"context"
"fmt"
"io"
"math"
"sort"
"strings"
"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/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
)
// Collection contains a collection of sequences.
type Collection struct {
accessedMap map[id.Sequence]*Sequence // Whenever a sequence is accessed, it is added to the access map for faster retrieval
underlyingMap prolly.AddressMap
ns tree.NodeStore
}
// Persistence controls the persistence of a Sequence.
type Persistence uint8
const (
Persistence_Permanent Persistence = 0
Persistence_Temporary Persistence = 1
Persistence_Unlogged Persistence = 2
)
// Sequence represents a single sequence within the pg_sequence table.
type Sequence struct {
Id id.Sequence
DataTypeID id.Type
Persistence Persistence
Start int64
Current int64
Increment int64
Minimum int64
Maximum int64
Cache int64
Cycle bool
IsAtEnd bool
HasBeenCalled bool
OwnerTable id.Table
OwnerColumn string
}
var _ objinterface.Collection = (*Collection)(nil)
var _ objinterface.RootObject = (*Sequence)(nil)
var _ doltdb.RootObject = (*Sequence)(nil)
// GetSequence returns the sequence with the given schema and name. Returns nil if the sequence cannot be found.
func (pgs *Collection) GetSequence(ctx context.Context, name id.Sequence) (*Sequence, error) {
return pgs.getSequence(ctx, name)
}
// GetSequencesWithTable returns all sequences with the given table as the owner.
func (pgs *Collection) GetSequencesWithTable(ctx context.Context, name doltdb.TableName) ([]*Sequence, error) {
// For now, this function isn't used in a critical path, so we're not too worried about performance
if err := pgs.cacheAllSequences(ctx); err != nil {
return nil, err
}
var seqs []*Sequence
nameID := id.NewTable(name.Schema, name.Name)
for _, seq := range pgs.accessedMap {
if seq.OwnerTable == nameID {
seqs = append(seqs, seq)
}
}
return seqs, nil
}
// GetAllSequences returns a map containing all sequences in the collection, grouped by the schema they're contained in.
// Each sequence array is also sorted by the sequence name.
func (pgs *Collection) GetAllSequences(ctx context.Context) (sequences map[string][]*Sequence, schemaNames []string, totalCount int, err error) {
// For now, this function is only used by the "reg" types, so we're not too worried about performance
if err = pgs.cacheAllSequences(ctx); err != nil {
return nil, nil, 0, err
}
totalCount = len(pgs.accessedMap)
schemaNamesMap := make(map[string]struct{})
sequences = make(map[string][]*Sequence)
for seqID, seq := range pgs.accessedMap {
schemaNamesMap[seqID.SchemaName()] = struct{}{}
sequences[seqID.SchemaName()] = append(sequences[seqID.SchemaName()], seq)
}
// Sort the sequences in the sequence map
for _, seqs := range sequences {
sort.Slice(seqs, func(i, j int) bool {
return seqs[i].Id < seqs[j].Id
})
}
// Create and sort the schema names
schemaNames = make([]string, 0, len(schemaNamesMap))
for name := range schemaNamesMap {
schemaNames = append(schemaNames, name)
}
sort.Slice(schemaNames, func(i, j int) bool {
return schemaNames[i] < schemaNames[j]
})
return
}
// HasSequence returns whether the sequence is present.
func (pgs *Collection) HasSequence(ctx context.Context, name id.Sequence) bool {
// Subsequent loads are cached
if _, ok := pgs.accessedMap[name]; ok {
return true
}
// The initial load is from the internal map
ok, err := pgs.underlyingMap.Has(ctx, string(name))
if err == nil && ok {
return true
}
return false
}
// CreateSequence creates a new sequence.
func (pgs *Collection) CreateSequence(ctx context.Context, seq *Sequence) error {
// Ensure that the sequence does not already exist
if _, ok := pgs.accessedMap[seq.Id]; ok {
return errors.Errorf(`relation "%s" already exists`, seq.Id.SequenceName())
}
if ok, err := pgs.underlyingMap.Has(ctx, string(seq.Id)); err != nil {
return err
} else if ok {
return errors.Errorf(`relation "%s" already exists`, seq.Id.SequenceName())
}
// Add it to our cache, which will be emptied when we do anything permanent
pgs.accessedMap[seq.Id] = seq
return nil
}
// DropSequence drops existing sequences.
func (pgs *Collection) DropSequence(ctx context.Context, names ...id.Sequence) (err error) {
// We need to clear the cache so that we only need to worry about the underlying map
if err = pgs.writeCache(ctx); err != nil {
return err
}
for _, name := range names {
if ok, err := pgs.underlyingMap.Has(ctx, string(name)); err != nil {
return err
} else if !ok {
return errors.Errorf(`sequence "%s" does not exist`, name.SequenceName())
}
}
// Now we'll remove the sequences from the underlying map
mapEditor := pgs.underlyingMap.Editor()
for _, name := range names {
if err = mapEditor.Delete(ctx, string(name)); err != nil {
return err
}
}
flushed, err := mapEditor.Flush(ctx)
if err != nil {
return err
}
pgs.underlyingMap = flushed
return nil
}
// resolveName returns the fully resolved name of the given sequence. Returns an error if the name is ambiguous.
func (pgs *Collection) resolveName(ctx context.Context, schemaName string, sequenceName string) (id.Sequence, error) {
if err := pgs.writeCache(ctx); err != nil {
return id.NullSequence, err
}
count, err := pgs.underlyingMap.Count()
if err != nil || count == 0 {
return id.NullSequence, err
}
// First check for an exact match
inputID := id.NewSequence(schemaName, sequenceName)
ok, err := pgs.underlyingMap.Has(ctx, string(inputID))
if err != nil {
return id.NullSequence, err
} else if ok {
return inputID, nil
}
// Now we'll iterate over all the names
var resolvedID id.Sequence
if len(schemaName) > 0 {
err = pgs.underlyingMap.IterAll(ctx, func(k string, _ hash.Hash) error {
seqID := id.Sequence(k)
if strings.EqualFold(sequenceName, seqID.SequenceName()) &&
strings.EqualFold(schemaName, seqID.SchemaName()) {
if resolvedID.IsValid() {
return fmt.Errorf("`%s.%s` is ambiguous, matches `%s.%s` and `%s.%s`",
schemaName, sequenceName, seqID.SchemaName(), seqID.SequenceName(), resolvedID.SchemaName(), resolvedID.SequenceName())
}
resolvedID = seqID
}
return nil
})
if err != nil {
return id.NullSequence, err
}
} else {
err = pgs.underlyingMap.IterAll(ctx, func(k string, _ hash.Hash) error {
seqID := id.Sequence(k)
if strings.EqualFold(sequenceName, seqID.SequenceName()) {
if resolvedID.IsValid() {
return fmt.Errorf("`%s` is ambiguous, matches `%s.%s` and `%s.%s`",
sequenceName, seqID.SchemaName(), seqID.SequenceName(), resolvedID.SchemaName(), resolvedID.SequenceName())
}
resolvedID = seqID
}
return nil
})
if err != nil {
return id.NullSequence, err
}
}
return resolvedID, nil
}
// iterateIDs iterates over all sequence IDs in the collection.
func (pgs *Collection) iterateIDs(ctx context.Context, f func(seqID id.Sequence) (stop bool, err error)) (err error) {
if err = pgs.writeCache(ctx); err != nil {
return err
}
return pgs.underlyingMap.IterAll(ctx, func(k string, _ hash.Hash) error {
seqID := id.Sequence(k)
stop, err := f(seqID)
if err != nil {
return err
} else if stop {
return io.EOF
} else {
return nil
}
})
}
// IterateSequences iterates over all sequences in the collection.
func (pgs *Collection) IterateSequences(ctx context.Context, f func(seq *Sequence) (stop bool, err error)) (err error) {
// For now, this function isn't used in a critical path, so we're not too worried about performance
if err = pgs.cacheAllSequences(ctx); err != nil {
return err
}
for _, seq := range pgs.accessedMap {
if stop, err := f(seq); err != nil {
return err
} else if stop {
break
}
}
return nil
}
// NextVal returns the next value in the sequence.
func (pgs *Collection) NextVal(ctx context.Context, name id.Sequence) (int64, error) {
seq, err := pgs.getSequence(ctx, name)
if err != nil {
return 0, err
}
if seq == nil {
return 0, errors.Errorf(`relation "%s" does not exist`, name.SequenceName())
}
return seq.nextValForSequence()
}
// SetVal sets the sequence to the
func (pgs *Collection) SetVal(ctx context.Context, name id.Sequence, newValue int64, autoAdvance bool) error {
seq, err := pgs.getSequence(ctx, name)
if err != nil {
return err
}
if seq == nil {
return errors.Errorf(`relation "%s" does not exist`, name.SequenceName())
}
if newValue < seq.Minimum || newValue > seq.Maximum {
return errors.Errorf(`setval: value %d is out of bounds for sequence "%s" (%d..%d)`,
newValue, name, seq.Minimum, seq.Maximum)
}
seq.Current = newValue
seq.IsAtEnd = false
seq.HasBeenCalled = false
if autoAdvance {
_, err := seq.nextValForSequence()
return err
}
return nil
}
// Clone returns a new *Collection with the same contents as the original.
func (pgs *Collection) Clone(ctx context.Context) *Collection {
newCollection := &Collection{
accessedMap: make(map[id.Sequence]*Sequence),
underlyingMap: pgs.underlyingMap,
ns: pgs.ns,
}
for seqID, seq := range pgs.accessedMap {
newCollection.accessedMap[seqID] = seq
}
return newCollection
}
// Map writes any cached sequences to the underlying map, and then returns the underlying map.
func (pgs *Collection) Map(ctx context.Context) (prolly.AddressMap, error) {
if err := pgs.writeCache(ctx); err != nil {
return prolly.AddressMap{}, err
}
return pgs.underlyingMap, nil
}
// GetID implements the interface objinterface.RootObject.
func (sequence *Sequence) GetID() id.Id {
return sequence.Id.AsId()
}
// GetRootObjectID implements the interface objinterface.RootObject.
func (sequence *Sequence) GetRootObjectID() objinterface.RootObjectID {
return objinterface.RootObjectID_Sequences
}
// HashOf implements the interface rootobject.RootObject.
func (sequence *Sequence) HashOf(ctx context.Context) (hash.Hash, error) {
data, err := sequence.Serialize(ctx)
if err != nil {
return hash.Hash{}, err
}
return hash.Of(data), nil
}
// Name implements the interface rootobject.RootObject.
func (sequence *Sequence) Name() doltdb.TableName {
return doltdb.TableName{
Name: sequence.Id.SequenceName(),
Schema: sequence.Id.SchemaName(),
}
}
// cacheAllSequences loads every sequence from the Dolt map into our local map. This exists to simplify any iteration
// logic, and shouldn't be used on a performance-critical path.
func (pgs *Collection) cacheAllSequences(ctx context.Context) error {
found := make(map[id.Sequence]struct{})
for seqID := range pgs.accessedMap {
found[seqID] = struct{}{}
}
return pgs.underlyingMap.IterAll(ctx, func(k string, v hash.Hash) error {
seqID := id.Sequence(k)
if _, ok := found[seqID]; ok {
return nil
}
found[seqID] = struct{}{}
data, err := pgs.ns.ReadBytes(ctx, v)
if err != nil {
return err
}
seq, err := DeserializeSequence(ctx, data)
if err != nil {
return err
}
pgs.accessedMap[seq.Id] = seq
return nil
})
}
// getSequence gets the sequence matching the given name.
func (pgs *Collection) getSequence(ctx context.Context, name id.Sequence) (*Sequence, error) {
// Subsequent loads are cached
if seq, ok := pgs.accessedMap[name]; ok {
return seq, nil
}
// The initial load is from the internal map
h, err := pgs.underlyingMap.Get(ctx, string(name))
if err != nil || h.IsEmpty() {
return nil, err
}
data, err := pgs.ns.ReadBytes(ctx, h)
if err != nil {
return nil, err
}
seq, err := DeserializeSequence(ctx, data)
if err != nil {
return nil, err
}
pgs.accessedMap[seq.Id] = seq
return seq, nil
}
// writeCache writes every Sequence in the cache to the underlying map.
func (pgs *Collection) writeCache(ctx context.Context) (err error) {
if len(pgs.accessedMap) == 0 {
return nil
}
mapEditor := pgs.underlyingMap.Editor()
for _, seq := range pgs.accessedMap {
data, err := seq.Serialize(ctx)
if err != nil {
return err
}
h, err := pgs.ns.WriteBytes(ctx, data)
if err != nil {
return err
}
if err = mapEditor.Update(ctx, string(seq.Id), h); err != nil {
return err
}
}
// Assign underlyingMap only after the error check. Flush returns a
// zero AddressMap on failure, which would corrupt the Collection.
flushed, err := mapEditor.Flush(ctx)
if err != nil {
return err
}
pgs.underlyingMap = flushed
clear(pgs.accessedMap)
return nil
}
// nextValForSequence increments the calling sequence.
func (sequence *Sequence) nextValForSequence() (int64, error) {
// First we'll check if we've reached the end, and cycle or error as necessary
if sequence.IsAtEnd {
if !sequence.Cycle {
if sequence.Increment > 0 {
return 0, errors.Errorf(`nextval: reached maximum value of sequence "%s" (%d)`, sequence.Id, sequence.Maximum)
} else {
return 0, errors.Errorf(`nextval: reached minimum value of sequence "%s" (%d)`, sequence.Id, sequence.Minimum)
}
}
sequence.IsAtEnd = false
if sequence.Increment > 0 {
sequence.Current = sequence.Minimum
} else {
sequence.Current = sequence.Maximum
}
}
// We'll return the current value, so everything after this sets the value for the next call
sequence.HasBeenCalled = true
valueToReturn := sequence.Current
// Increment the current value
if sequence.Increment > 0 {
// Check for overflow or crossing the maximum, meaning we're at the end
if sequence.Current > math.MaxInt64-sequence.Increment || sequence.Current+sequence.Increment > sequence.Maximum {
sequence.IsAtEnd = true
} else {
sequence.Current += sequence.Increment
}
} else {
// Check for underflow or crossing the minimum, meaning we're at the end
if sequence.Current < math.MinInt64-sequence.Increment || sequence.Current+sequence.Increment < sequence.Minimum {
sequence.IsAtEnd = true
} else {
sequence.Current += sequence.Increment
}
}
return valueToReturn, nil
}
+190
View File
@@ -0,0 +1,190 @@
// 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 sequences
import (
"context"
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/libraries/doltcore/merge"
"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/doltgresql/core/id"
merge2 "github.com/dolthub/doltgresql/core/merge"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
"github.com/dolthub/doltgresql/flatbuffers/gen/serial"
pgtypes "github.com/dolthub/doltgresql/server/types"
"github.com/dolthub/doltgresql/utils"
)
// storage is used to read from and write to the root.
var storage = objinterface.RootObjectSerializer{
Bytes: (*serial.RootValue).SequencesBytes,
RootValueAdd: serial.RootValueAddSequences,
}
// HandleMerge implements the interface objinterface.Collection.
func (*Collection) HandleMerge(ctx context.Context, mro merge.MergeRootObject) (doltdb.RootObject, *merge.MergeStats, error) {
ourSeq := mro.OurRootObj.(*Sequence)
theirSeq := mro.TheirRootObj.(*Sequence)
// Ensure that they have the same identifier
if ourSeq.Id != theirSeq.Id {
return nil, nil, errors.Newf("attempted to merge different sequences: `%s` and `%s`",
ourSeq.Name().String(), theirSeq.Name().String())
}
// Check if an ancestor is present
var ancSeq Sequence
hasAncestor := false
if mro.AncestorRootObj != nil {
ancSeq = *(mro.AncestorRootObj.(*Sequence))
hasAncestor = true
}
// Take the min/max of fields that aren't dependent on the increment direction
mergedSeq := *ourSeq
mergedSeq.Minimum = merge2.ResolveMergeValuesVariadic(ourSeq.Minimum, theirSeq.Minimum, ancSeq.Minimum, hasAncestor, utils.Min)
mergedSeq.Maximum = merge2.ResolveMergeValuesVariadic(ourSeq.Maximum, theirSeq.Maximum, ancSeq.Maximum, hasAncestor, utils.Max)
mergedSeq.Cache = merge2.ResolveMergeValuesVariadic(ourSeq.Cache, theirSeq.Cache, ancSeq.Cache, hasAncestor, utils.Min)
mergedSeq.Cycle = merge2.ResolveMergeValues(ourSeq.Cycle, theirSeq.Cycle, ancSeq.Cycle, hasAncestor, func(ourCycle, theirCycle bool) bool {
return ourCycle || theirCycle
})
// Take the largest type specified
mergedSeq.DataTypeID = merge2.ResolveMergeValues(ourSeq.DataTypeID, theirSeq.DataTypeID, ancSeq.DataTypeID, hasAncestor, func(ourID, theirID id.Type) id.Type {
if (ourID == pgtypes.Int16.ID && (theirID == pgtypes.Int32.ID || theirID == pgtypes.Int64.ID)) ||
(ourID == pgtypes.Int32.ID && theirID == pgtypes.Int64.ID) {
return theirID
} else {
return ourID
}
})
// Handle the fields that are dependent on the increment direction.
// We'll always take the increment size that's the smallest for the most granularity, along with the one that
// has progressed the furthest.
// For opposing increment directions, we'll take whatever is in our collection.
mergedSeq.Increment = merge2.ResolveMergeValues(ourSeq.Increment, theirSeq.Increment, ancSeq.Increment, hasAncestor, func(ourIncrement, theirIncrement int64) int64 {
if ourSeq.Increment >= 0 && theirSeq.Increment >= 0 {
return utils.Min(ourIncrement, theirIncrement)
} else if ourSeq.Increment < 0 && theirSeq.Increment < 0 {
return utils.Max(ourIncrement, theirIncrement)
} else {
return ourIncrement
}
})
mergedSeq.Start = merge2.ResolveMergeValues(ourSeq.Start, theirSeq.Start, ancSeq.Start, hasAncestor, func(ourStart, theirStart int64) int64 {
if ourSeq.Increment >= 0 && theirSeq.Increment >= 0 {
return utils.Min(ourStart, theirStart)
} else if ourSeq.Increment < 0 && theirSeq.Increment < 0 {
return utils.Max(ourStart, theirStart)
} else {
return ourStart
}
})
mergedSeq.Current = merge2.ResolveMergeValues(ourSeq.Current, theirSeq.Current, ancSeq.Current, hasAncestor, func(ourCurrent, theirCurrent int64) int64 {
if ourSeq.Increment >= 0 && theirSeq.Increment >= 0 {
return utils.Max(ourCurrent, theirCurrent)
} else if ourSeq.Increment < 0 && theirSeq.Increment < 0 {
return utils.Min(ourCurrent, theirCurrent)
} else {
return ourCurrent
}
})
mergedSeq.HasBeenCalled = merge2.ResolveMergeValues(ourSeq.HasBeenCalled, theirSeq.HasBeenCalled, ancSeq.HasBeenCalled, hasAncestor, func(ourcalled, theirCalled bool) bool {
return ourcalled || theirCalled
})
return &mergedSeq, &merge.MergeStats{
Operation: merge.TableModified,
Adds: 0,
Deletes: 0,
Modifications: 1,
DataConflicts: 0,
SchemaConflicts: 0,
ConstraintViolations: 0,
}, nil
}
// LoadCollection implements the interface objinterface.Collection.
func (*Collection) LoadCollection(ctx context.Context, root objinterface.RootValue) (objinterface.Collection, error) {
return LoadSequences(ctx, root)
}
// LoadCollectionHash implements the interface objinterface.Collection.
func (*Collection) LoadCollectionHash(ctx context.Context, root objinterface.RootValue) (hash.Hash, error) {
m, ok, err := storage.GetProllyMap(ctx, root)
if err != nil || !ok {
return hash.Hash{}, err
}
return m.HashOf(), nil
}
// LoadSequences loads the sequences collection from the given root.
func LoadSequences(ctx context.Context, root objinterface.RootValue) (*Collection, error) {
m, ok, err := storage.GetProllyMap(ctx, root)
if err != nil {
return nil, err
}
if !ok {
m, err = prolly.NewEmptyAddressMap(root.NodeStore())
if err != nil {
return nil, err
}
}
return &Collection{
accessedMap: make(map[id.Sequence]*Sequence),
underlyingMap: m,
ns: root.NodeStore(),
}, nil
}
// ResolveNameFromObjects implements the interface objinterface.Collection.
func (*Collection) ResolveNameFromObjects(ctx context.Context, name doltdb.TableName, rootObjects []objinterface.RootObject) (doltdb.TableName, id.Id, error) {
// First we'll check if there are any objects to search through in the first place
accessedMap := make(map[id.Sequence]*Sequence)
for _, rootObject := range rootObjects {
if obj, ok := rootObject.(*Sequence); ok {
accessedMap[obj.Id] = obj
}
}
if len(accessedMap) == 0 {
return doltdb.TableName{}, id.Null, nil
}
// There are root objects to search through, so we'll create a temporary store
ns := tree.NewTestNodeStore()
addressMap, err := prolly.NewEmptyAddressMap(ns)
if err != nil {
return doltdb.TableName{}, id.Null, err
}
tempCollection := Collection{
accessedMap: accessedMap,
underlyingMap: addressMap,
ns: ns,
}
return tempCollection.ResolveName(ctx, name)
}
// Serializer implements the interface objinterface.Collection.
func (*Collection) Serializer() objinterface.RootObjectSerializer {
return storage
}
// UpdateRoot implements the interface objinterface.Collection.
func (pgs *Collection) UpdateRoot(ctx context.Context, root objinterface.RootValue) (objinterface.RootValue, error) {
m, err := pgs.Map(ctx)
if err != nil {
return nil, err
}
return storage.WriteProllyMap(ctx, root, m)
}
+137
View File
@@ -0,0 +1,137 @@
// Copyright 2026 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 sequences
import (
"context"
"errors"
"math"
"testing"
"github.com/dolthub/dolt/go/store/hash"
"github.com/dolthub/dolt/go/store/prolly"
"github.com/dolthub/dolt/go/store/prolly/tree"
"github.com/stretchr/testify/require"
"github.com/dolthub/doltgresql/core/id"
)
// TestMap_RemainsUsableAfterFlushFailure asserts that Collection.Map
// remains usable after a flush returns an error. A subsequent Map call
// against a healthy store must succeed.
func TestMap_RemainsUsableAfterFlushFailure(t *testing.T) {
t.Parallel()
ctx := context.Background()
ns := newCountingFailNodeStore(t)
coll := newTestCollection(t, ns)
coll.accessedMap[id.NewSequence("public", "seq_one")] = newTestSequence("public", "seq_one")
_, err := coll.Map(ctx)
require.NoError(t, err)
ns.failAfter(0)
coll.accessedMap[id.NewSequence("public", "seq_two")] = newTestSequence("public", "seq_two")
_, err = coll.Map(ctx)
require.Error(t, err)
ns.allowAll()
require.NotPanics(t, func() {
_, _ = coll.Map(ctx)
})
}
// TestDropSequence_RemainsUsableAfterFlushFailure asserts the same
// recovery contract for DropSequence.
func TestDropSequence_RemainsUsableAfterFlushFailure(t *testing.T) {
t.Parallel()
ctx := context.Background()
ns := newCountingFailNodeStore(t)
coll := newTestCollection(t, ns)
pending := newTestSequence("public", "pending")
coll.accessedMap[pending.Id] = pending
ns.failAfter(0)
err := coll.DropSequence(ctx, pending.Id)
require.Error(t, err)
ns.allowAll()
require.NotPanics(t, func() {
_ = coll.DropSequence(ctx, id.NewSequence("public", "pending"))
})
}
// newTestCollection returns a Collection backed by |ns| with an empty
// address map.
func newTestCollection(t *testing.T, ns tree.NodeStore) *Collection {
t.Helper()
addrMap, err := prolly.NewEmptyAddressMap(ns)
require.NoError(t, err)
return &Collection{
accessedMap: map[id.Sequence]*Sequence{},
underlyingMap: addrMap,
ns: ns,
}
}
// newTestSequence returns a Sequence with valid bounds that round-trip
// through Serialize and Deserialize. The exact values are not significant.
func newTestSequence(schema, name string) *Sequence {
return &Sequence{
Id: id.NewSequence(schema, name),
Start: 1,
Current: 1,
Increment: 1,
Minimum: 1,
Maximum: math.MaxInt64,
Cache: 1,
}
}
// countingFailNodeStore wraps a real test NodeStore so that callers can
// induce Write failures at chosen points without otherwise altering
// behavior. Used to drive the writeCache flush-failure path.
type countingFailNodeStore struct {
tree.NodeStore
writes int
budget int // -1 means unlimited
}
func newCountingFailNodeStore(t *testing.T) *countingFailNodeStore {
t.Helper()
return &countingFailNodeStore{NodeStore: tree.NewTestNodeStore(), budget: -1}
}
// failAfter permits |allowed| additional Write calls then fails the
// rest, until allowAll is called.
func (f *countingFailNodeStore) failAfter(allowed int) {
f.writes = 0
f.budget = allowed
}
// allowAll restores the wrapper to delegate every Write to the
// underlying NodeStore.
func (f *countingFailNodeStore) allowAll() {
f.budget = -1
}
func (f *countingFailNodeStore) Write(ctx context.Context, nd *tree.Node) (hash.Hash, error) {
if f.budget >= 0 {
f.writes++
if f.writes > f.budget {
return hash.Hash{}, errors.New("induced node store failure")
}
}
return f.NodeStore.Write(ctx, nd)
}
+362
View File
@@ -0,0 +1,362 @@
// 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 sequences
import (
"context"
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/doltgresql/core/id"
pgmerge "github.com/dolthub/doltgresql/core/merge"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
const (
FIELD_NAME_DATA_TYPE = "data_type"
FIELD_NAME_PERSISTENCE = "persistence"
FIELD_NAME_START = "start"
FIELD_NAME_CURRENT = "current"
FIELD_NAME_INCREMENT = "increment"
FIELD_NAME_MINIMUM = "minimum"
FIELD_NAME_MAXIMUM = "maximum"
FIELD_NAME_CACHE = "cache"
FIELD_NAME_CYCLE = "cycle"
FIELD_NAME_IS_AT_END = "is_at_end"
FIELD_NAME_HAS_BEEN_CALLED = "has_been_called"
FIELD_NAME_OWNER_TABLE = "owner_table"
FIELD_NAME_OWNER_COLUMN = "owner_column"
)
// DeserializeRootObject implements the interface objinterface.Collection.
func (pgs *Collection) DeserializeRootObject(ctx context.Context, data []byte) (objinterface.RootObject, error) {
return DeserializeSequence(ctx, data)
}
// DiffRootObjects implements the interface objinterface.Collection.
func (pgs *Collection) DiffRootObjects(ctx context.Context, fromHash string, o objinterface.RootObject, t objinterface.RootObject, a objinterface.RootObject) ([]objinterface.RootObjectDiff, objinterface.RootObject, error) {
ours := o.(*Sequence)
{
copiedOurs := *ours
ours = &copiedOurs
}
theirs := t.(*Sequence)
var ancestor Sequence
hasAncestor := false
if ancestorPtr, ok := a.(*Sequence); ok {
ancestor = *ancestorPtr
hasAncestor = true
}
var diffs []objinterface.RootObjectDiff
if ours.DataTypeID != theirs.DataTypeID {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Text,
FromHash: fromHash,
FieldName: FIELD_NAME_DATA_TYPE,
}
if pgmerge.DiffValues(&diff, ours.DataTypeID.TypeName(), theirs.DataTypeID.TypeName(), ancestor.DataTypeID.TypeName(), hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.DataTypeID = id.NewType(ours.DataTypeID.SchemaName(), diff.OurValue.(string))
}
}
if ours.Persistence != theirs.Persistence {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Int32,
FromHash: fromHash,
FieldName: FIELD_NAME_PERSISTENCE,
}
if pgmerge.DiffValues(&diff, int32(ours.Persistence), int32(theirs.Persistence), int32(ancestor.Persistence), hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.Persistence = Persistence(diff.OurValue.(int32))
}
}
if ours.Start != theirs.Start {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Int64,
FromHash: fromHash,
FieldName: FIELD_NAME_START,
}
if pgmerge.DiffValues(&diff, ours.Start, theirs.Start, ancestor.Start, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.Start = diff.OurValue.(int64)
}
}
if ours.Current != theirs.Current {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Int64,
FromHash: fromHash,
FieldName: FIELD_NAME_CURRENT,
}
if pgmerge.DiffValues(&diff, ours.Current, theirs.Current, ancestor.Current, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.Current = diff.OurValue.(int64)
}
}
if ours.HasBeenCalled != theirs.HasBeenCalled {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Bool,
FromHash: fromHash,
FieldName: FIELD_NAME_HAS_BEEN_CALLED,
}
if pgmerge.DiffValues(&diff, ours.HasBeenCalled, theirs.HasBeenCalled, ancestor.HasBeenCalled, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.HasBeenCalled = diff.OurValue.(bool)
}
}
if ours.Increment != theirs.Increment {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Int64,
FromHash: fromHash,
FieldName: FIELD_NAME_INCREMENT,
}
if pgmerge.DiffValues(&diff, ours.Increment, theirs.Increment, ancestor.Increment, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.Increment = diff.OurValue.(int64)
}
}
if ours.Minimum != theirs.Minimum {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Int64,
FromHash: fromHash,
FieldName: FIELD_NAME_MINIMUM,
}
if pgmerge.DiffValues(&diff, ours.Minimum, theirs.Minimum, ancestor.Minimum, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.Minimum = diff.OurValue.(int64)
}
}
if ours.Maximum != theirs.Maximum {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Int64,
FromHash: fromHash,
FieldName: FIELD_NAME_MAXIMUM,
}
if pgmerge.DiffValues(&diff, ours.Maximum, theirs.Maximum, ancestor.Maximum, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.Maximum = diff.OurValue.(int64)
}
}
if ours.Cache != theirs.Cache {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Int64,
FromHash: fromHash,
FieldName: FIELD_NAME_CACHE,
}
if pgmerge.DiffValues(&diff, ours.Cache, theirs.Cache, ancestor.Cache, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.Cache = diff.OurValue.(int64)
}
}
if ours.Cycle != theirs.Cycle {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Bool,
FromHash: fromHash,
FieldName: FIELD_NAME_CYCLE,
}
if pgmerge.DiffValues(&diff, ours.Cycle, theirs.Cycle, ancestor.Cycle, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.Cycle = diff.OurValue.(bool)
}
}
if ours.IsAtEnd != theirs.IsAtEnd {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Bool,
FromHash: fromHash,
FieldName: FIELD_NAME_IS_AT_END,
}
if pgmerge.DiffValues(&diff, ours.IsAtEnd, theirs.IsAtEnd, ancestor.IsAtEnd, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.IsAtEnd = diff.OurValue.(bool)
}
}
if ours.OwnerTable != theirs.OwnerTable {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Text,
FromHash: fromHash,
FieldName: FIELD_NAME_OWNER_TABLE,
}
if pgmerge.DiffValues(&diff, ours.OwnerTable.TableName(), theirs.OwnerTable.TableName(), ancestor.OwnerTable.TableName(), hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.OwnerTable = id.NewTable(ours.OwnerTable.SchemaName(), diff.OurValue.(string))
}
}
if ours.OwnerColumn != theirs.OwnerColumn {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Text,
FromHash: fromHash,
FieldName: FIELD_NAME_OWNER_COLUMN,
}
if pgmerge.DiffValues(&diff, ours.OwnerColumn, theirs.OwnerColumn, ancestor.OwnerColumn, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.OwnerColumn = diff.OurValue.(string)
}
}
return diffs, ours, nil
}
// DropRootObject implements the interface objinterface.Collection.
func (pgs *Collection) DropRootObject(ctx context.Context, identifier id.Id) error {
if identifier.Section() != id.Section_Sequence {
return errors.Errorf(`sequence %s does not exist`, identifier.String())
}
return pgs.DropSequence(ctx, id.Sequence(identifier))
}
// GetFieldType implements the interface objinterface.Collection.
func (pgs *Collection) GetFieldType(ctx context.Context, fieldName string) *pgtypes.DoltgresType {
switch fieldName {
case FIELD_NAME_DATA_TYPE:
return pgtypes.Text
case FIELD_NAME_PERSISTENCE:
return pgtypes.Int32
case FIELD_NAME_START:
return pgtypes.Int64
case FIELD_NAME_CURRENT:
return pgtypes.Int64
case FIELD_NAME_INCREMENT:
return pgtypes.Int64
case FIELD_NAME_MINIMUM:
return pgtypes.Int64
case FIELD_NAME_MAXIMUM:
return pgtypes.Int64
case FIELD_NAME_CACHE:
return pgtypes.Int64
case FIELD_NAME_CYCLE:
return pgtypes.Bool
case FIELD_NAME_IS_AT_END:
return pgtypes.Bool
case FIELD_NAME_HAS_BEEN_CALLED:
return pgtypes.Bool
case FIELD_NAME_OWNER_TABLE:
return pgtypes.Text
case FIELD_NAME_OWNER_COLUMN:
return pgtypes.Text
default:
return nil
}
}
// GetID implements the interface objinterface.Collection.
func (pgs *Collection) GetID() objinterface.RootObjectID {
return objinterface.RootObjectID_Sequences
}
// GetRootObject implements the interface objinterface.Collection.
func (pgs *Collection) GetRootObject(ctx context.Context, identifier id.Id) (objinterface.RootObject, bool, error) {
if identifier.Section() != id.Section_Sequence {
return nil, false, nil
}
seq, err := pgs.GetSequence(ctx, id.Sequence(identifier))
return seq, err == nil && seq != nil, err
}
// HasRootObject implements the interface objinterface.Collection.
func (pgs *Collection) HasRootObject(ctx context.Context, identifier id.Id) (bool, error) {
if identifier.Section() != id.Section_Sequence {
return false, nil
}
return pgs.HasSequence(ctx, id.Sequence(identifier)), nil
}
// IDToTableName implements the interface objinterface.Collection.
func (pgs *Collection) IDToTableName(identifier id.Id) doltdb.TableName {
if identifier.Section() != id.Section_Sequence {
return doltdb.TableName{}
}
seqID := id.Sequence(identifier)
return doltdb.TableName{
Name: seqID.SequenceName(),
Schema: seqID.SchemaName(),
}
}
// IterAll implements the interface objinterface.Collection.
func (pgs *Collection) IterAll(ctx context.Context, callback func(rootObj objinterface.RootObject) (stop bool, err error)) error {
return pgs.IterateSequences(ctx, func(seq *Sequence) (stop bool, err error) {
return callback(seq)
})
}
// IterIDs implements the interface objinterface.Collection.
func (pgs *Collection) IterIDs(ctx context.Context, callback func(identifier id.Id) (stop bool, err error)) error {
return pgs.iterateIDs(ctx, func(seqID id.Sequence) (stop bool, err error) {
return callback(seqID.AsId())
})
}
// PutRootObject implements the interface objinterface.Collection.
func (pgs *Collection) PutRootObject(ctx context.Context, rootObj objinterface.RootObject) error {
seq, ok := rootObj.(*Sequence)
if !ok {
return errors.Newf("invalid sequence root object: %T", rootObj)
}
return pgs.CreateSequence(ctx, seq)
}
// RenameRootObject implements the interface objinterface.Collection.
func (pgs *Collection) RenameRootObject(ctx context.Context, oldName id.Id, newName id.Id) error {
if !oldName.IsValid() || !newName.IsValid() || oldName.Section() != newName.Section() || oldName.Section() != id.Section_Sequence {
return errors.New("cannot rename sequence due to invalid name")
}
oldSeqName := id.Sequence(oldName)
newSeqName := id.Sequence(newName)
seq, err := pgs.GetSequence(ctx, oldSeqName)
if err != nil {
return err
}
if err = pgs.DropSequence(ctx, oldSeqName); err != nil {
return err
}
newSeq := *seq
newSeq.Id = newSeqName
return pgs.CreateSequence(ctx, &newSeq)
}
// ResolveName implements the interface objinterface.Collection.
func (pgs *Collection) ResolveName(ctx context.Context, name doltdb.TableName) (doltdb.TableName, id.Id, error) {
rawID, err := pgs.resolveName(ctx, name.Schema, name.Name)
if err != nil || !rawID.IsValid() {
return doltdb.TableName{}, id.Null, err
}
return doltdb.TableName{
Name: rawID.SequenceName(),
Schema: rawID.SchemaName(),
}, rawID.AsId(), nil
}
// TableNameToID implements the interface objinterface.Collection.
func (pgs *Collection) TableNameToID(name doltdb.TableName) id.Id {
return id.NewSequence(name.Schema, name.Name).AsId()
}
// UpdateField implements the interface objinterface.Collection.
func (pgs *Collection) UpdateField(ctx context.Context, rootObject objinterface.RootObject, fieldName string, newValue any) (objinterface.RootObject, error) {
return nil, errors.New("updating through the conflicts table for this object type is not yet supported")
}
+89
View File
@@ -0,0 +1,89 @@
// Copyright 2024 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 sequences
import (
"context"
"github.com/cockroachdb/errors"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/utils"
)
// Serialize returns the Sequence as a byte slice. If the Sequence is nil, then this returns a nil slice.
func (sequence *Sequence) Serialize(ctx context.Context) ([]byte, error) {
if sequence == nil {
return nil, nil
}
// Create the writer
writer := utils.NewWriter(256)
writer.VariableUint(1) // Version
// Write the sequence data
writer.Id(sequence.Id.AsId())
writer.Id(sequence.DataTypeID.AsId())
writer.Uint8(uint8(sequence.Persistence))
writer.Int64(sequence.Start)
writer.Int64(sequence.Current)
writer.Int64(sequence.Increment)
writer.Int64(sequence.Minimum)
writer.Int64(sequence.Maximum)
writer.Int64(sequence.Cache)
writer.Bool(sequence.Cycle)
writer.Bool(sequence.IsAtEnd)
writer.Bool(sequence.HasBeenCalled)
writer.Id(sequence.OwnerTable.AsId())
writer.String(sequence.OwnerColumn)
// Returns the data
return writer.Data(), nil
}
// DeserializeSequence returns the Sequence that was serialized in the byte slice. Returns an empty Sequence if data is
// nil or empty.
func DeserializeSequence(_ context.Context, data []byte) (*Sequence, error) {
if len(data) == 0 {
return nil, nil
}
reader := utils.NewReader(data)
version := reader.VariableUint()
if version > 1 {
return nil, errors.Errorf("version %d of sequences is not supported, please upgrade the server", version)
}
// Read from the reader
sequence := &Sequence{}
sequence.Id = id.Sequence(reader.Id())
sequence.DataTypeID = id.Type(reader.Id())
sequence.Persistence = Persistence(reader.Uint8())
sequence.Start = reader.Int64()
sequence.Current = reader.Int64()
sequence.Increment = reader.Int64()
sequence.Minimum = reader.Int64()
sequence.Maximum = reader.Int64()
sequence.Cache = reader.Int64()
sequence.Cycle = reader.Bool()
sequence.IsAtEnd = reader.Bool()
if version >= 1 {
sequence.HasBeenCalled = reader.Bool()
}
sequence.OwnerTable = id.Table(reader.Id())
sequence.OwnerColumn = reader.String()
if !reader.IsEmpty() {
return nil, errors.Errorf("extra data found while deserializing a sequence")
}
// Return the deserialized object
return sequence, nil
}