Files
2026-07-13 12:32:25 +08:00

138 lines
3.9 KiB
Go

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