mirror of
https://github.com/luxfi/database.git
synced 2026-10-30 15:01:18 +00:00
A consumer that needs crash-safe ids or a whole-range delete currently has to
import github.com/luxfi/zapdb directly and hold a *zapdb.DB — which pins it to
one backend and puts a second embedded engine in reach of the same directory.
hanzoai/git's queue is exactly that case, and having two engines able to open
one directory is what took git.hanzo.ai down: each engine's recovery deletes
the files the other's manifest does not list.
Database stays the floor — the set every backend can honour, so memdb, leveldb,
pebbledb and the wrappers are untouched. The extras are their own small
interfaces, reached by assertion:
if s, ok := db.(database.Sequencer); ok { ... }
Widening Database instead would force every backend to grow a method it cannot
honour and move the failure to runtime. This way the consumer asks for exactly
what it needs at compile time, and a backend that cannot provide it simply does
not assert.
Sequence is the crash-safe alternative to an in-memory counter: a counter
rebuilt from "highest key seen" can re-issue an id that is already on disk
after an unclean shutdown and silently overwrite the record under it. The test
asserts that property directly — ids handed out after a reopen are strictly
above every id handed out before it, without re-reading the data.
zapdb implements both by delegating to the native handle it already holds, with
the same closeMu/ErrClosed guard as every other method, and carries
compile-time assertions so dropping one breaks the build here rather than a
consumer's type assertion.
⚠ Note for whoever runs the suite: `go test -tags test ./zapdb/` does not build
on main, and has not for reasons unrelated to this change — gpu_bench_test.go
and gpu_cache_test.go reference NewWithGPU, GPUCacheStats and
InvalidateGPUCache, none of which exist anywhere in the repo. These tests were
verified with those two files moved aside.
Hanzo Dev <[email protected]>
Co-authored-by: Hanzo Dev <[email protected]>
101 lines
3.1 KiB
Go
101 lines
3.1 KiB
Go
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
//go:build test
|
|
|
|
package zapdb
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/luxfi/metric"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/luxfi/database"
|
|
)
|
|
|
|
// The property that makes a Sequence worth having over an in-memory counter:
|
|
// ids handed out after a reopen are strictly above every id handed out before
|
|
// it, WITHOUT re-reading the data. A counter rebuilt from "highest key seen"
|
|
// cannot promise that once the highest key has been consumed and deleted.
|
|
func TestSequenceSurvivesReopen(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
open := func() *Database {
|
|
db, err := New(dir, nil, "test", metric.NewRegistry())
|
|
require.NoError(t, err)
|
|
return db
|
|
}
|
|
|
|
db := open()
|
|
seqr, ok := any(db).(database.Sequencer)
|
|
require.True(t, ok, "zapdb must implement database.Sequencer")
|
|
|
|
seq, err := seqr.GetSequence([]byte("q/seq"), 100)
|
|
require.NoError(t, err)
|
|
|
|
// The sequence is 0-based: the first Next is 0, so compare pairwise rather
|
|
// than against the zero value.
|
|
var ids []uint64
|
|
for range 5 {
|
|
n, err := seq.Next()
|
|
require.NoError(t, err)
|
|
ids = append(ids, n)
|
|
}
|
|
for i := 1; i < len(ids); i++ {
|
|
require.Greater(t, ids[i], ids[i-1], "ids must increase")
|
|
}
|
|
last := ids[len(ids)-1]
|
|
// Release returns the unused band; without it the reserved ids are skipped.
|
|
require.NoError(t, seq.Release())
|
|
require.NoError(t, db.Close())
|
|
|
|
db = open()
|
|
defer db.Close()
|
|
seq2, err := any(db).(database.Sequencer).GetSequence([]byte("q/seq"), 100)
|
|
require.NoError(t, err)
|
|
defer seq2.Release()
|
|
|
|
n, err := seq2.Next()
|
|
require.NoError(t, err)
|
|
require.Greater(t, n, last, "an id was re-issued across a reopen")
|
|
}
|
|
|
|
// A closed database must refuse both capabilities rather than panic on a nil
|
|
// handle — same contract as every other method on this backend.
|
|
func TestCapabilitiesRefuseAfterClose(t *testing.T) {
|
|
db, err := New(t.TempDir(), nil, "test", metric.NewRegistry())
|
|
require.NoError(t, err)
|
|
require.NoError(t, db.Close())
|
|
|
|
_, err = db.GetSequence([]byte("k"), 10)
|
|
require.ErrorIs(t, err, database.ErrClosed)
|
|
require.ErrorIs(t, db.DropPrefix([]byte("p")), database.ErrClosed)
|
|
}
|
|
|
|
// DropPrefix removes exactly its range and leaves neighbouring keys alone.
|
|
func TestDropPrefixIsScoped(t *testing.T) {
|
|
db, err := New(t.TempDir(), nil, "test", metric.NewRegistry())
|
|
require.NoError(t, err)
|
|
defer db.Close()
|
|
|
|
require.NoError(t, db.Put([]byte("q/i/1"), []byte("a")))
|
|
require.NoError(t, db.Put([]byte("q/i/2"), []byte("b")))
|
|
require.NoError(t, db.Put([]byte("q/u/1"), []byte("c")))
|
|
require.NoError(t, db.Put([]byte("other"), []byte("d")))
|
|
|
|
var dropper database.PrefixDropper = db
|
|
require.NoError(t, dropper.DropPrefix([]byte("q/i/")))
|
|
|
|
for _, gone := range [][]byte{[]byte("q/i/1"), []byte("q/i/2")} {
|
|
has, err := db.Has(gone)
|
|
require.NoError(t, err)
|
|
require.False(t, has, "%s should have been dropped", gone)
|
|
}
|
|
for _, kept := range [][]byte{[]byte("q/u/1"), []byte("other")} {
|
|
has, err := db.Has(kept)
|
|
require.NoError(t, err)
|
|
require.True(t, has, "%s should have survived", kept)
|
|
}
|
|
}
|