// Package mvcc implements the "mvcc" SQLite VFS. // // The "mvcc" [vfs.VFS] allows the same in-memory database to be shared // among multiple database connections in the same process, // as long as the database name begins with "/". // // Importing package mvcc registers the VFS: // // import _ "github.com/ncruces/go-sqlite3/vfs/mvcc" package mvcc import ( "crypto/rand" "fmt" "net/url" "strings" "sync" "testing" "github.com/ncruces/go-sqlite3/vfs" "github.com/ncruces/wbt" ) func init() { vfs.Register("mvcc", mvccVFS{}) } var ( memoryMtx sync.Mutex // +checklocks:memoryMtx memoryDBs = map[string]*mvccDB{} ) // Create creates a shared memory database, // using a snapshot as its initial contents. func Create(name string, snapshot Snapshot) { memoryMtx.Lock() memoryDBs[name] = &mvccDB{ refs: 1, name: name, data: snapshot.Tree, } memoryMtx.Unlock() } // Delete deletes a shared memory database. func Delete(name string) { name = getName(name) memoryMtx.Lock() delete(memoryDBs, name) memoryMtx.Unlock() } // Snapshot represents a database snapshot. type Snapshot struct { *wbt.Tree[int64, string] } // NewSnapshot creates a snapshot from data. func NewSnapshot(data string) Snapshot { var tree *wbt.Tree[int64, string] // Convert data from WAL/2 to rollback journal. if len(data) >= 20 && (false || data[18] == 2 && data[19] == 2 || data[18] == 3 && data[19] == 3) { tree = tree. Put(0, data[:18]). Put(18, "\001\001"). Put(20, data[20:]) } else if len(data) > 0 { tree = tree.Put(0, data) } return Snapshot{tree} } // TakeSnapshot takes a snapshot of a database. // Name may be a URI filename. func TakeSnapshot(name string) Snapshot { name = getName(name) memoryMtx.Lock() db := memoryDBs[name] memoryMtx.Unlock() if db == nil { return Snapshot{} } db.mtx.Lock() defer db.mtx.Unlock() return Snapshot{db.data} } // TestDB creates a shared database from a snapshot for the test to use. // The database is automatically deleted when the test and all its subtests complete. // Returns a URI filename appropriate to call Open with. // Each subsequent call to TestDB returns a unique database. // // func Test_something(t *testing.T) { // t.Parallel() // dsn := mvcc.TestDB(t, snapshot, url.Values{ // "_pragma": {"busy_timeout(1000)"}, // }) // // db, err := sql.Open("sqlite3", dsn) // if err != nil { // t.Fatal(err) // } // defer db.Close() // // // ... // } func TestDB(tb testing.TB, snapshot Snapshot, params ...url.Values) string { tb.Helper() name := fmt.Sprintf("%s_%s", tb.Name(), rand.Text()) tb.Cleanup(func() { Delete(name) }) Create(name, snapshot) p := url.Values{"vfs": {"mvcc"}} for _, v := range params { for k, v := range v { for _, v := range v { p.Add(k, v) } } } return (&url.URL{ Scheme: "file", OmitHost: true, Path: "/" + name, RawQuery: p.Encode(), }).String() } func getName(dsn string) string { u, err := url.Parse(dsn) if err == nil && u.Scheme == "file" && strings.HasPrefix(u.Path, "/") && u.Query().Get("vfs") == "mvcc" { return u.Path[1:] } return dsn }