diff --git a/README.md b/README.md index d9dcccf..9608fa7 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Performance is tested by running - [ ] custom SQL functions - [ ] custom VFSes - [ ] in-memory VFS - - [ ] read-only VFS, wrapping an [`io.ReaderAt`](https://pkg.go.dev/io#ReaderAt) + - [x] read-only VFS, wrapping an [`io.ReaderAt`](https://pkg.go.dev/io#ReaderAt) - [ ] cloud-based VFS, based on [Cloud Backed SQLite](https://sqlite.org/cloudsqlite/doc/trunk/www/index.wiki) - [x] custom VFS API diff --git a/go.mod b/go.mod index cc7d9ea..6b05a56 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.19 require ( github.com/ncruces/julianday v0.1.5 + github.com/psanford/httpreadat v0.1.0 github.com/tetratelabs/wazero v1.1.0 golang.org/x/sync v0.2.0 golang.org/x/sys v0.8.0 diff --git a/go.sum b/go.sum index 06dd8e9..19e3eb0 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/ncruces/julianday v0.1.5 h1:hDJ9ejiMp3DHsoZ5KW4c1lwfMjbARS7u/gbYcd0FBZk= github.com/ncruces/julianday v0.1.5/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g= +github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE= +github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE= github.com/tetratelabs/wazero v1.1.0 h1:EByoAhC+QcYpwSZJSs/aV0uokxPwBgKxfiokSUwAknQ= github.com/tetratelabs/wazero v1.1.0/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ= golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= diff --git a/sqlite3vfs/api.go b/sqlite3vfs/api.go index cddc44a..7bdc3b9 100644 --- a/sqlite3vfs/api.go +++ b/sqlite3vfs/api.go @@ -25,7 +25,7 @@ type File interface { WriteAt(p []byte, off int64) (n int, err error) Truncate(size int64) error Sync(flags SyncFlag) error - FileSize() (int64, error) + Size() (int64, error) Lock(lock LockLevel) error Unlock(lock LockLevel) error CheckReservedLock() (bool, error) diff --git a/sqlite3vfs/const.go b/sqlite3vfs/const.go index 8b9d270..756be70 100644 --- a/sqlite3vfs/const.go +++ b/sqlite3vfs/const.go @@ -19,6 +19,7 @@ const ( _OK _ErrorCode = util.OK _PERM _ErrorCode = util.PERM _BUSY _ErrorCode = util.BUSY + _READONLY _ErrorCode = util.READONLY _IOERR _ErrorCode = util.IOERR _NOTFOUND _ErrorCode = util.NOTFOUND _CANTOPEN _ErrorCode = util.CANTOPEN diff --git a/sqlite3vfs/file.go b/sqlite3vfs/file.go index eb874bf..1d1694e 100644 --- a/sqlite3vfs/file.go +++ b/sqlite3vfs/file.go @@ -151,7 +151,7 @@ func (f *vfsFile) Sync(flags SyncFlag) error { return nil } -func (f *vfsFile) FileSize() (int64, error) { +func (f *vfsFile) Size() (int64, error) { return f.Seek(0, io.SeekEnd) } diff --git a/sqlite3vfs/reader.go b/sqlite3vfs/reader.go new file mode 100644 index 0000000..fb9be59 --- /dev/null +++ b/sqlite3vfs/reader.go @@ -0,0 +1,118 @@ +package sqlite3vfs + +import ( + "io" + "io/fs" +) + +// A ReaderVFS is [VFS] for immutable databases. +type ReaderVFS map[string]SizeReaderAt + +var _ VFS = ReaderVFS{} + +// A SizeReaderAt is a ReaderAt with a Size method. +// Use [NewSizeReaderAt] to adapt different Size interfaces. +type SizeReaderAt interface { + Size() (int64, error) + io.ReaderAt +} + +// Open implements the [VFS] interface. +func (vfs ReaderVFS) Open(name string, flags OpenFlag) (File, OpenFlag, error) { + if flags&OPEN_MAIN_DB == 0 { + return nil, flags, _CANTOPEN + } + if ra, ok := vfs[name]; ok { + return readerFile{ra}, flags, nil + } + return nil, flags, _CANTOPEN +} + +// Delete implements the [VFS] interface. +func (vfs ReaderVFS) Delete(name string, dirSync bool) error { + return _IOERR_DELETE +} + +// Access implements the [VFS] interface. +func (vfs ReaderVFS) Access(name string, flag AccessFlag) (bool, error) { + return false, nil +} + +// FullPathname implements the [VFS] interface. +func (vfs ReaderVFS) FullPathname(name string) (string, error) { + return name, nil +} + +type readerFile struct{ SizeReaderAt } + +func (r readerFile) Close() error { + if c, ok := r.SizeReaderAt.(io.Closer); ok { + return c.Close() + } + return nil +} + +func (readerFile) WriteAt(b []byte, off int64) (n int, err error) { + return 0, _READONLY +} + +func (readerFile) Truncate(size int64) error { + return _READONLY +} + +func (readerFile) Sync(flag SyncFlag) error { + return nil +} + +func (readerFile) Lock(elock LockLevel) error { + return nil +} + +func (readerFile) Unlock(elock LockLevel) error { + return nil +} + +func (readerFile) CheckReservedLock() (bool, error) { + return false, nil +} + +func (readerFile) SectorSize() int { + return 0 +} + +func (readerFile) DeviceCharacteristics() DeviceCharacteristic { + return IOCAP_IMMUTABLE +} + +// NewSizeReaderAt returns a SizeReaderAt given an io.ReaderAt +// that implements one of: +// - Size() (int64, error) +// - Size() int64 +// - Len() int +// - Stat() (fs.FileInfo, error) +// - Seek(offset int64, whence int) (int64, error) +func NewSizeReaderAt(r io.ReaderAt) SizeReaderAt { + return sizer{r} +} + +type sizer struct{ io.ReaderAt } + +func (s sizer) Size() (int64, error) { + switch s := s.ReaderAt.(type) { + case interface{ Size() (int64, error) }: + return s.Size() + case interface{ Size() int64 }: + return s.Size(), nil + case interface{ Len() int }: + return int64(s.Len()), nil + case interface{ Stat() (fs.FileInfo, error) }: + fi, err := s.Stat() + if err != nil { + return 0, err + } + return fi.Size(), nil + case io.Seeker: + return s.Seek(0, io.SeekEnd) + } + return 0, _IOERR_SEEK +} diff --git a/sqlite3vfs/reader_test.go b/sqlite3vfs/reader_test.go new file mode 100644 index 0000000..786e71d --- /dev/null +++ b/sqlite3vfs/reader_test.go @@ -0,0 +1,60 @@ +package sqlite3vfs_test + +import ( + "database/sql" + "fmt" + "log" + + _ "github.com/ncruces/go-sqlite3/driver" + _ "github.com/ncruces/go-sqlite3/embed" + "github.com/ncruces/go-sqlite3/sqlite3vfs" + "github.com/psanford/httpreadat" +) + +func ExampleReaderVFS() { + sqlite3vfs.Register("httpvfs", sqlite3vfs.ReaderVFS{ + "demo.db": httpreadat.New("https://www.sanford.io/demo.db"), + }) + + db, err := sql.Open("sqlite3", "file:demo.db?vfs=httpvfs&mode=ro") + if err != nil { + log.Fatal(err) + } + defer db.Close() + + magname := map[int]string{ + 3: "thousand", + 6: "million", + 9: "billion", + } + rows, err := db.Query(` + SELECT period, data_value, magntude, units FROM csv + WHERE period > '2010' + LIMIT 10`) + if err != nil { + log.Fatal(err) + } + defer rows.Close() + + for rows.Next() { + var period, units string + var value int64 + var mag int + err = rows.Scan(&period, &value, &mag, &units) + if err != nil { + log.Fatal(err) + } + fmt.Printf("%s: %d %s %s\n", period, value, magname[mag], units) + } + // Output: + // 2010.03: 17463 million Dollars + // 2010.06: 17260 million Dollars + // 2010.09: 15419 million Dollars + // 2010.12: 17088 million Dollars + // 2011.03: 18516 million Dollars + // 2011.06: 18835 million Dollars + // 2011.09: 16390 million Dollars + // 2011.12: 18748 million Dollars + // 2012.03: 18477 million Dollars + // 2012.06: 18270 million Dollars +} diff --git a/sqlite3vfs/vfs.go b/sqlite3vfs/vfs.go index 5783c20..5e79aef 100644 --- a/sqlite3vfs/vfs.go +++ b/sqlite3vfs/vfs.go @@ -234,7 +234,7 @@ func vfsSync(ctx context.Context, mod api.Module, pFile uint32, flags SyncFlag) func vfsFileSize(ctx context.Context, mod api.Module, pFile, pSize uint32) _ErrorCode { file := vfsFileGet(ctx, mod, pFile) - size, err := file.FileSize() + size, err := file.Size() util.WriteUint64(mod, pSize, uint64(size)) return vfsErrorCode(err, _IOERR_SEEK) } diff --git a/tests/conn_test.go b/tests/conn_test.go index f6848d5..745f84e 100644 --- a/tests/conn_test.go +++ b/tests/conn_test.go @@ -23,6 +23,18 @@ func TestConn_Open_dir(t *testing.T) { } } +func TestConn_Open_notfound(t *testing.T) { + t.Parallel() + + _, err := sqlite3.OpenFlags("test.db", sqlite3.OPEN_READONLY) + if err == nil { + t.Fatal("want error") + } + if !errors.Is(err, sqlite3.CANTOPEN) { + t.Errorf("got %v, want sqlite3.CANTOPEN", err) + } +} + func TestConn_Close(t *testing.T) { var conn *sqlite3.Conn conn.Close()