| 1 |
|
// Package browse is the read-only web-browsing data layer over bare Dolt |
| 2 |
|
// chunk stores. It is deliberately the only package in dolt.sr.ht that reaches |
| 3 |
|
// into low-level dolthub/dolt internals (nbs, prolly, durable, diff), so that |
| 4 |
|
// all version-fragile code stays contained here and a future module bump only |
| 5 |
|
// needs to be re-verified against this one package. |
| 6 |
|
// |
| 7 |
|
// # Bare stores have no working set |
| 8 |
|
// |
| 9 |
|
// dolt.sr.ht serves bare NBS chunk-store directories (created via |
| 10 |
|
// doltdb.WriteEmptyRepo and grown by pushes over the remotesapi). They have no |
| 11 |
|
// .dolt/ working set, so the sqle engine and the embedded driver cannot open |
| 12 |
|
// them. Everything here reads from committed roots only. |
| 13 |
|
// |
| 14 |
|
// # Open discipline (why not doltdb.LoadDoltDB) |
| 15 |
|
// |
| 16 |
|
// The obvious entry point, doltdb.LoadDoltDB, routes through the file |
| 17 |
|
// dbfactory, which wraps the store in a GenerationalNBS (newgen + an oldgen |
| 18 |
|
// subdir + a ghost gen). In the pinned dolt/go version |
| 19 |
|
// (v0.40.5-0.20260626152440-45335d44ad79), calling Close() on such a |
| 20 |
|
// generational store over one of our bare stores panics deep in nbs: |
| 21 |
|
// |
| 22 |
|
// panic: Close() called and reduced ref count to < 0. |
| 23 |
|
// store/nbs/table_index.go:532 onHeapTableIndex.Close |
| 24 |
|
// ... GenerationalNBS.Close -> NomsBlockStore.Close -> tableSet.close |
| 25 |
|
// |
| 26 |
|
// It reproduces on a plain open-read-only-then-Close, independent of any |
| 27 |
|
// reads, so the documented open-per-request + Close pattern would crash the |
| 28 |
|
// web process, not just tests. A single (non-generational) nbs.NewLocalStore |
| 29 |
|
// — the exact construction storage.Cache uses to serve pushes — closes |
| 30 |
|
// cleanly. We therefore build the DoltDB by hand from one NewLocalStore via |
| 31 |
|
// doltdb.DoltDBFromCS and never touch the generational path. |
| 32 |
|
// |
| 33 |
|
// # Freshness and concurrency |
| 34 |
|
// |
| 35 |
|
// Open is called once per request and Close releases the handle, so every |
| 36 |
|
// request gets a fresh read of the on-disk manifest and observes commits |
| 37 |
|
// landed by the push writer since the last request — no stale cache, no |
| 38 |
|
// refresh dance. NBS readers are manifest-based and append-only: opening a |
| 39 |
|
// second NewLocalStore over a directory the push writer is also serving is |
| 40 |
|
// safe (no exclusive lock is held for the store's lifetime; the manifest lock |
| 41 |
|
// is taken only briefly during atomic updates), so a reader always sees a |
| 42 |
|
// consistent snapshot. |
| 43 |
|
package browse |
| 44 |
|
|
| 45 |
|
import ( |
| 46 |
|
"context" |
| 47 |
|
"errors" |
| 48 |
|
"fmt" |
| 49 |
|
"path/filepath" |
| 50 |
|
|
| 51 |
|
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb" |
| 52 |
|
"github.com/dolthub/dolt/go/store/nbs" |
| 53 |
|
"github.com/dolthub/dolt/go/store/types" |
| 54 |
|
) |
| 55 |
|
|
| 56 |
|
// readStoreMemTableSize bounds the in-memory memtable of the read handle. It |
| 57 |
|
// matches the value storage.Cache uses for served stores; browsing never |
| 58 |
|
// writes, so the memtable stays empty in practice. |
| 59 |
|
const readStoreMemTableSize = 128 * 1024 * 1024 |
| 60 |
|
|
| 61 |
|
// ErrRefNotFound is returned when a ref string matches neither an existing |
| 62 |
|
// branch nor a resolvable commit hash. |
| 63 |
|
var ErrRefNotFound = errors.New("browse: ref not found") |
| 64 |
|
|
| 65 |
|
// ErrTableNotFound is returned when a table does not exist in the resolved |
| 66 |
|
// root value. |
| 67 |
|
var ErrTableNotFound = errors.New("browse: table not found") |
| 68 |
|
|
| 69 |
|
// DB is a read-only handle to a single bare Dolt chunk store. It is not safe |
| 70 |
|
// for concurrent use; open one per request and Close it when done. |
| 71 |
|
type DB struct { |
| 72 |
|
ddb *doltdb.DoltDB |
| 73 |
|
path string |
| 74 |
|
} |
| 75 |
|
|
| 76 |
|
// Open opens the bare Dolt chunk store at diskPath for read-only browsing. |
| 77 |
|
// diskPath is the store directory itself (the dir that holds the NBS manifest |
| 78 |
|
// and table files), not a parent. The caller must Close the returned DB. |
| 79 |
|
// |
| 80 |
|
// See the package doc for why this bypasses doltdb.LoadDoltDB. |
| 81 |
0 |
func Open(ctx context.Context, diskPath string) (*DB, error) { |
| 82 |
0 |
cs, err := nbs.NewLocalStore( |
| 83 |
0 |
ctx, |
| 84 |
0 |
types.Format_DOLT.VersionString(), |
| 85 |
0 |
diskPath, |
| 86 |
0 |
readStoreMemTableSize, |
| 87 |
0 |
nbs.NewUnlimitedMemQuotaProvider(), |
| 88 |
0 |
false, |
| 89 |
0 |
) |
| 90 |
0 |
if err != nil { |
| 91 |
0 |
return nil, fmt.Errorf("browse: open store %q: %w", diskPath, err) |
| 92 |
0 |
} |
| 93 |
|
|
| 94 |
0 |
ddb, err := doltdb.DoltDBFromCS(cs, filepath.Base(diskPath)) |
| 95 |
0 |
if err != nil { |
| 96 |
0 |
// cs is not yet owned by a DoltDB, close it directly. |
| 97 |
0 |
_ = cs.Close() |
| 98 |
0 |
return nil, fmt.Errorf("browse: build doltdb for %q: %w", diskPath, err) |
| 99 |
0 |
} |
| 100 |
|
|
| 101 |
0 |
return &DB{ddb: ddb, path: diskPath}, nil |
| 102 |
|
} |
| 103 |
|
|
| 104 |
|
// Close releases the underlying chunk store. It closes the single |
| 105 |
|
// non-generational NBS store, which is safe in the pinned version (unlike the |
| 106 |
|
// generational store LoadDoltDB would have produced). |
| 107 |
0 |
func (db *DB) Close() error { |
| 108 |
0 |
return db.ddb.Close() |
| 109 |
0 |
} |