Compare commits

...

33 Commits

Author SHA1 Message Date
Nuno Cruces
71ae26e5c9 Documentation. 2023-02-22 17:51:30 +00:00
Nuno Cruces
e91758c6a4 Zero blobs, tests, documentation 2023-02-22 14:19:56 +00:00
Nuno Cruces
b749b32a62 Unlock tweaks, tests. 2023-02-21 12:56:39 +00:00
Nuno Cruces
3b4df71a94 Time handling. 2023-02-21 04:45:25 +00:00
Nuno Cruces
df687a1c54 Tests. 2023-02-20 14:43:19 +00:00
Edoardo Vacchi
2f5b9837e1 deps: updates wazero to 1.0.0-pre.9
This updates [wazero](https://wazero.io/) to [1.0.0-pre.9][1]. Notably:

* This release includes our last breaking changes before 1.0.0 final:
  * Requires at least Go 1.8
  * Renames `Runtime.InstantiateModuleFromBinary` to `Runtime.Instantiate`
* This release also integrates Go context to limit execution time.
  More details on the [Release Notes][1]
* We are now passing third-party integration test suites: wasi-testsuite,
  TinyGo's, Zig's.

[1]: https://github.com/tetratelabs/wazero/releases/tag/v1.0.0-pre.9

Signed-off-by: Edoardo Vacchi <evacchi@users.noreply.github.com>
2023-02-20 13:32:52 +00:00
Nuno Cruces
c351400be7 Tests. 2023-02-20 13:30:01 +00:00
Nuno Cruces
231d3a0438 Read-only transactions, locking. 2023-02-19 16:16:13 +00:00
Nuno Cruces
2f25e4eedb Bug fixes, optimizations, fuzz testing. 2023-02-19 12:44:26 +00:00
Nuno Cruces
ad27d5d840 Support pragmas, integration test. 2023-02-18 13:15:01 +00:00
Nuno Cruces
ec5bd236f8 Documentation. 2023-02-18 03:46:52 +00:00
Nuno Cruces
a51cdb04e6 Exec fast path. 2023-02-18 02:57:47 +00:00
Nuno Cruces
f50d5df3d0 Context cancellation. 2023-02-18 02:16:11 +00:00
Nuno Cruces
4ac2ccf473 Named parameters. 2023-02-18 00:47:56 +00:00
Nuno Cruces
5f7a72a553 Connection reuse. 2023-02-17 16:36:47 +00:00
Nuno Cruces
643b004727 Reuse byte slices. 2023-02-17 12:30:07 +00:00
Nuno Cruces
72e0415184 Time handling. 2023-02-17 10:40:43 +00:00
Nuno Cruces
28cb558d10 Minimal database/sql driver. 2023-02-17 02:21:07 +00:00
Nuno Cruces
23806b0db1 More tests. 2023-02-16 13:58:53 +00:00
Nuno Cruces
6a80499823 Panic consistently. 2023-02-16 13:52:05 +00:00
Nuno Cruces
110f36bdf9 Fix flakiness. 2023-02-16 13:37:29 +00:00
Nuno Cruces
f85426022d Test data races. 2023-02-15 16:24:34 +00:00
Nuno Cruces
78fd0cbee5 Towards database/sql. 2023-02-15 16:15:14 +00:00
Nuno Cruces
0d59065719 Lock errors. 2023-02-14 11:38:05 +00:00
Nuno Cruces
6110e2d6e2 Memory arenas. 2023-02-14 11:34:24 +00:00
Nuno Cruces
275b8c38a2 Documentation. 2023-02-14 11:33:41 +00:00
Nuno Cruces
fd1244c471 Support utf16 DBs. 2023-02-14 01:21:12 +00:00
Nuno Cruces
f11d294825 Check integrity. 2023-02-13 16:00:27 +00:00
Nuno Cruces
22b702fcda Synchronize IPC test. 2023-02-13 15:23:11 +00:00
Nuno Cruces
831817a737 Test IPC. 2023-02-13 15:01:36 +00:00
Nuno Cruces
7329d9f2fb Avoid writer starvation. 2023-02-13 13:53:32 +00:00
Nuno Cruces
3aad1d5d79 Towards xFileControl. 2023-02-13 13:52:52 +00:00
Nuno Cruces
f72c599d2d illumos OFD locks. 2023-02-13 13:51:35 +00:00
51 changed files with 3254 additions and 694 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
custom: https://www.paypal.com/donate/buttons/manage/33P59ELZWGMK6

View File

@@ -28,7 +28,13 @@ jobs:
- name: Test
run: go test -v ./...
- if: matrix.os == 'ubuntu-latest'
name: Update coverage report
- name: Test data races
run: go test -v -race ./...
if: matrix.os == 'ubuntu-latest'
- name: Update coverage report
uses: ncruces/go-coverage-report@main
if: |
matrix.os == 'ubuntu-latest' &&
github.event_name == 'push'
continue-on-error: true

View File

@@ -6,13 +6,22 @@
⚠️ CAUTION ⚠️
This is still very much a WIP.\
DO NOT USE this with data you care about.
This is a WIP.
Roadmap:
- [x] build SQLite using `zig cc --target=wasm32-wasi`
- [x] `:memory:` databases
- [x] port [`test_demovfs.c`](https://www.sqlite.org/src/doc/trunk/src/test_demovfs.c) to Go
- branch [`wasi`](https://github.com/ncruces/go-sqlite3/tree/wasi) uses `test_demovfs.c` directly
- [x] come up with a simple, nice API, enough for simple queries
- [x] file locking, compatible with SQLite on Windows/Unix
- [x] design a simple, nice API, enough for simple use cases
- [x] provide a simple `database/sql` driver
- [x] file locking, compatible with SQLite on Windows/Unix
- [ ] shared memory, compatible with SQLite on Windows/Unix
- needed for improved WAL mode
- [ ] advanced SQLite features
- [ ] nested transactions
- [ ] incremental BLOB I/O
- [ ] online backup
- [ ] session extension
- [ ] snapshots
- [ ] SQL functions

25
api.go
View File

@@ -1,3 +1,4 @@
// Package sqlite3 wraps the C SQLite API.
package sqlite3
import (
@@ -44,18 +45,26 @@ func newConn(ctx context.Context, module api.Module) (_ *Conn, err error) {
step: getFun("sqlite3_step"),
exec: getFun("sqlite3_exec"),
clearBindings: getFun("sqlite3_clear_bindings"),
bindCount: getFun("sqlite3_bind_parameter_count"),
bindIndex: getFun("sqlite3_bind_parameter_index"),
bindName: getFun("sqlite3_bind_parameter_name"),
bindNull: getFun("sqlite3_bind_null"),
bindInteger: getFun("sqlite3_bind_int64"),
bindFloat: getFun("sqlite3_bind_double"),
bindText: getFun("sqlite3_bind_text64"),
bindBlob: getFun("sqlite3_bind_blob64"),
bindZeroBlob: getFun("sqlite3_bind_zeroblob64"),
bindNull: getFun("sqlite3_bind_null"),
columnCount: getFun("sqlite3_column_count"),
columnName: getFun("sqlite3_column_name"),
columnType: getFun("sqlite3_column_type"),
columnInteger: getFun("sqlite3_column_int64"),
columnFloat: getFun("sqlite3_column_double"),
columnText: getFun("sqlite3_column_text"),
columnBlob: getFun("sqlite3_column_blob"),
columnBytes: getFun("sqlite3_column_bytes"),
columnType: getFun("sqlite3_column_type"),
lastRowid: getFun("sqlite3_last_insert_rowid"),
changes: getFun("sqlite3_changes64"),
interrupt: getFun("sqlite3_interrupt"),
},
}
if err != nil {
@@ -80,16 +89,24 @@ type sqliteAPI struct {
step api.Function
exec api.Function
clearBindings api.Function
bindNull api.Function
bindCount api.Function
bindIndex api.Function
bindName api.Function
bindInteger api.Function
bindFloat api.Function
bindText api.Function
bindBlob api.Function
bindZeroBlob api.Function
bindNull api.Function
columnCount api.Function
columnName api.Function
columnType api.Function
columnInteger api.Function
columnFloat api.Function
columnText api.Function
columnBlob api.Function
columnBytes api.Function
columnType api.Function
lastRowid api.Function
changes api.Function
interrupt api.Function
}

6
blob.go Normal file
View File

@@ -0,0 +1,6 @@
package sqlite3
// ZeroBlob represents a zero-filled, length n BLOB
// that can be used as an argument to
// [database/sql.DB.Exec] and similar methods.
type ZeroBlob int64

View File

@@ -24,13 +24,11 @@ type sqlite3Runtime struct {
runtime wazero.Runtime
compiled wazero.CompiledModule
instances atomic.Uint64
ctx context.Context
err error
}
func (s *sqlite3Runtime) instantiateModule(ctx context.Context) (api.Module, error) {
s.ctx = ctx
s.once.Do(s.compileModule)
s.once.Do(func() { s.compileModule(ctx) })
if s.err != nil {
return nil, s.err
}
@@ -40,12 +38,9 @@ func (s *sqlite3Runtime) instantiateModule(ctx context.Context) (api.Module, err
return s.runtime.InstantiateModule(ctx, s.compiled, cfg)
}
func (s *sqlite3Runtime) compileModule() {
s.runtime = wazero.NewRuntime(s.ctx)
s.err = vfsInstantiate(s.ctx, s.runtime)
if s.err != nil {
return
}
func (s *sqlite3Runtime) compileModule(ctx context.Context) {
s.runtime = wazero.NewRuntime(ctx)
vfsInstantiate(ctx, s.runtime)
bin := Binary
if bin == nil && Path != "" {
@@ -54,6 +49,10 @@ func (s *sqlite3Runtime) compileModule() {
return
}
}
if bin == nil {
s.err = binaryErr
return
}
s.compiled, s.err = s.runtime.CompileModule(s.ctx, bin)
s.compiled, s.err = s.runtime.CompileModule(ctx, bin)
}

212
conn.go
View File

@@ -13,6 +13,11 @@ type Conn struct {
api sqliteAPI
mem memory
handle uint32
arena arena
pending *Stmt
waiter chan struct{}
done <-chan struct{}
}
// Open calls [OpenFlags] with [OPEN_READWRITE] and [OPEN_CREATE].
@@ -39,15 +44,15 @@ func OpenFlags(filename string, flags OpenFlag) (conn *Conn, err error) {
if err != nil {
return nil, err
}
c.arena = c.newArena(1024)
namePtr := c.newString(filename)
connPtr := c.new(ptrlen)
defer c.free(namePtr)
defer c.free(connPtr)
defer c.arena.reset()
connPtr := c.arena.new(ptrlen)
namePtr := c.arena.string(filename)
r, err := c.api.open.Call(c.ctx, uint64(namePtr), uint64(connPtr), uint64(flags), 0)
if err != nil {
return nil, err
panic(err)
}
c.handle = c.mem.readUint32(connPtr)
@@ -63,15 +68,19 @@ func OpenFlags(filename string, flags OpenFlag) (conn *Conn, err error) {
// open blob handles, and/or unfinished backup objects,
// Close will leave the database connection open and return [BUSY].
//
// It is safe to close a nil, zero or closed connection.
//
// https://www.sqlite.org/c3ref/close.html
func (c *Conn) Close() error {
if c == nil {
if c == nil || c.handle == 0 {
return nil
}
c.SetInterrupt(nil)
r, err := c.api.close.Call(c.ctx, uint64(c.handle))
if err != nil {
return err
panic(err)
}
if err := c.error(r[0]); err != nil {
@@ -82,17 +91,80 @@ func (c *Conn) Close() error {
return c.mem.mod.Close(c.ctx)
}
// SetInterrupt interrupts a long-running query when done is closed.
//
// Subsequent uses of the connection will return [INTERRUPT]
// until done is reset by another call to SetInterrupt.
//
// Typically, done is provided by [context.Context.Done]:
//
// ctx, cancel := context.WithTimeout(context.TODO(), 100*time.Millisecond)
// conn.SetInterrupt(ctx.Done())
// defer cancel()
//
// https://www.sqlite.org/c3ref/interrupt.html
func (c *Conn) SetInterrupt(done <-chan struct{}) (old <-chan struct{}) {
// Is a waiter running?
if c.waiter != nil {
c.waiter <- struct{}{} // Cancel the waiter.
<-c.waiter // Wait for it to finish.
c.waiter = nil
}
// Finalize the uncompleted SQL statement.
if c.pending != nil {
c.pending.Close()
c.pending = nil
}
old = c.done
c.done = done
if done == nil {
return old
}
// Creating an uncompleted SQL statement prevents SQLite from ignoring
// an interrupt that comes before any other statements are started.
c.pending, _, _ = c.Prepare(`SELECT 1 UNION ALL SELECT 2`)
c.pending.Step()
waiter := make(chan struct{})
c.waiter = waiter
go func() {
select {
case <-waiter: // Waiter was cancelled.
break
case <-done: // Done was closed.
// This is safe to call from a goroutine
// because it doesn't touch the C stack.
_, err := c.api.interrupt.Call(c.ctx, uint64(c.handle))
if err != nil {
panic(err)
}
// Wait for the next call to SetInterrupt.
<-waiter
}
// Signal that the waiter has finished.
waiter <- struct{}{}
}()
return old
}
// Exec is a convenience function that allows an application to run
// multiple statements of SQL without having to use a lot of code.
//
// https://www.sqlite.org/c3ref/exec.html
func (c *Conn) Exec(sql string) error {
sqlPtr := c.newString(sql)
defer c.free(sqlPtr)
defer c.arena.reset()
sqlPtr := c.arena.string(sql)
r, err := c.api.exec.Call(c.ctx, uint64(c.handle), uint64(sqlPtr), 0, 0, 0)
if err != nil {
return err
panic(err)
}
return c.error(r[0])
}
@@ -109,18 +181,20 @@ func (c *Conn) Prepare(sql string) (stmt *Stmt, tail string, err error) {
//
// https://www.sqlite.org/c3ref/prepare.html
func (c *Conn) PrepareFlags(sql string, flags PrepareFlag) (stmt *Stmt, tail string, err error) {
sqlPtr := c.newString(sql)
stmtPtr := c.new(ptrlen)
tailPtr := c.new(ptrlen)
defer c.free(sqlPtr)
defer c.free(stmtPtr)
defer c.free(tailPtr)
if emptyStatement(sql) {
return nil, "", nil
}
defer c.arena.reset()
stmtPtr := c.arena.new(ptrlen)
tailPtr := c.arena.new(ptrlen)
sqlPtr := c.arena.string(sql)
r, err := c.api.prepare.Call(c.ctx, uint64(c.handle),
uint64(sqlPtr), uint64(len(sql)+1), uint64(flags),
uint64(stmtPtr), uint64(tailPtr))
if err != nil {
return nil, "", err
panic(err)
}
stmt = &Stmt{c: c}
@@ -137,6 +211,31 @@ func (c *Conn) PrepareFlags(sql string, flags PrepareFlag) (stmt *Stmt, tail str
return
}
// LastInsertRowID returns the rowid of the most recent successful INSERT
// on the database connection.
//
// https://www.sqlite.org/c3ref/last_insert_rowid.html
func (c *Conn) LastInsertRowID() uint64 {
r, err := c.api.lastRowid.Call(c.ctx, uint64(c.handle))
if err != nil {
panic(err)
}
return r[0]
}
// Changes returns the number of rows modified, inserted or deleted
// by the most recently completed INSERT, UPDATE or DELETE statement
// on the database connection.
//
// https://www.sqlite.org/c3ref/changes.html
func (c *Conn) Changes() uint64 {
r, err := c.api.changes.Call(c.ctx, uint64(c.handle))
if err != nil {
panic(err)
}
return r[0]
}
func (c *Conn) error(rc uint64, sql ...string) error {
if rc == _OK {
return nil
@@ -150,28 +249,26 @@ func (c *Conn) error(rc uint64, sql ...string) error {
var r []uint64
// sqlite3_errmsg is guaranteed to never change the value of the error code.
r, _ = c.api.errstr.Call(c.ctx, rc)
if r != nil {
err.str = c.mem.readString(uint32(r[0]), _MAX_STRING)
}
r, _ = c.api.errmsg.Call(c.ctx, uint64(c.handle))
if r != nil {
err.msg = c.mem.readString(uint32(r[0]), 512)
err.msg = c.mem.readString(uint32(r[0]), _MAX_STRING)
}
if sql != nil {
// sqlite3_error_offset is guaranteed to never change the value of the error code.
r, _ = c.api.erroff.Call(c.ctx, uint64(c.handle))
if r != nil && r[0] != math.MaxUint32 {
err.sql = sql[0][r[0]:]
}
}
r, _ = c.api.errstr.Call(c.ctx, rc)
if r != nil {
err.str = c.mem.readString(uint32(r[0]), 512)
}
if err.msg == err.str {
switch err.msg {
case err.str, "not an error":
err.msg = ""
}
return &err
}
@@ -186,13 +283,13 @@ func (c *Conn) free(ptr uint32) {
}
}
func (c *Conn) new(len uint32) uint32 {
r, err := c.api.malloc.Call(c.ctx, uint64(len))
func (c *Conn) new(size uint32) uint32 {
r, err := c.api.malloc.Call(c.ctx, uint64(size))
if err != nil {
panic(err)
}
ptr := uint32(r[0])
if ptr == 0 && len != 0 {
if ptr == 0 && size != 0 {
panic(oomErr)
}
return ptr
@@ -202,19 +299,54 @@ func (c *Conn) newBytes(b []byte) uint32 {
if b == nil {
return 0
}
siz := uint32(len(b))
ptr := c.new(siz)
buf := c.mem.view(ptr, siz)
copy(buf, b)
ptr := c.new(uint32(len(b)))
c.mem.writeBytes(ptr, b)
return ptr
}
func (c *Conn) newString(s string) uint32 {
siz := uint32(len(s) + 1)
ptr := c.new(siz)
buf := c.mem.view(ptr, siz)
buf[len(s)] = 0
copy(buf, s)
ptr := c.new(uint32(len(s) + 1))
c.mem.writeString(ptr, s)
return ptr
}
func (c *Conn) newArena(size uint32) arena {
return arena{
c: c,
size: size,
base: c.new(size),
}
}
type arena struct {
c *Conn
base uint32
next uint32
size uint32
ptrs []uint32
}
func (a *arena) reset() {
for _, ptr := range a.ptrs {
a.c.free(ptr)
}
a.ptrs = nil
a.next = 0
}
func (a *arena) new(size uint32) uint32 {
if a.next+size <= a.size {
ptr := a.base + a.next
a.next += size
return ptr
}
ptr := a.c.new(size)
a.ptrs = append(a.ptrs, ptr)
return ptr
}
func (a *arena) string(s string) uint32 {
ptr := a.new(uint32(len(s) + 1))
a.c.mem.writeString(ptr, s)
return ptr
}

View File

@@ -2,105 +2,13 @@ package sqlite3
import (
"bytes"
"errors"
"math"
"testing"
)
func TestConn_Close(t *testing.T) {
var conn *Conn
conn.Close()
}
func TestConn_Close_BUSY(t *testing.T) {
db, err := Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
stmt, _, err := db.Prepare("BEGIN")
if err != nil {
t.Fatal(err)
}
defer stmt.Close()
err = db.Close()
if err == nil {
t.Fatal("want error")
}
var serr *Error
if !errors.As(err, &serr) {
t.Fatalf("got %T, want sqlite3.Error", err)
}
if rc := serr.Code(); rc != BUSY {
t.Errorf("got %d, want sqlite3.BUSY", rc)
}
if got := err.Error(); got != `sqlite3: database is locked: unable to close due to unfinalized statements or unfinished backups` {
t.Error("got message: ", got)
}
}
func TestConn_Prepare_Empty(t *testing.T) {
db, err := Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
stmt, _, err := db.Prepare("")
if err != nil {
t.Fatal(err)
}
defer stmt.Close()
if stmt != nil {
t.Error("want nil")
}
}
func TestConn_Prepare_Invalid(t *testing.T) {
db, err := Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
var serr *Error
_, _, err = db.Prepare("SELECT")
if err == nil {
t.Fatal("want error")
}
if !errors.As(err, &serr) {
t.Fatalf("got %T, want sqlite3.Error", err)
}
if rc := serr.Code(); rc != ERROR {
t.Errorf("got %d, want sqlite3.ERROR", rc)
}
if got := err.Error(); got != `sqlite3: SQL logic error: incomplete input` {
t.Error("got message: ", got)
}
_, _, err = db.Prepare("SELECT * FRM sqlite_schema")
if err == nil {
t.Fatal("want error")
}
if !errors.As(err, &serr) {
t.Fatalf("got %T, want sqlite3.ERROR", err)
}
if rc := serr.Code(); rc != ERROR {
t.Errorf("got %d, want sqlite3.ERROR", rc)
}
if got := serr.SQL(); got != `FRM sqlite_schema` {
t.Error("got SQL: ", got)
}
if got := serr.Error(); got != `sqlite3: SQL logic error: near "FRM": syntax error` {
t.Error("got message: ", got)
}
}
func TestConn_new(t *testing.T) {
t.Parallel()
db, err := Open(":memory:")
if err != nil {
t.Fatal(err)
@@ -112,7 +20,41 @@ func TestConn_new(t *testing.T) {
t.Error("want panic")
}
func TestConn_newArena(t *testing.T) {
t.Parallel()
db, err := Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
arena := db.newArena(16)
defer arena.reset()
const title = "Lorem ipsum"
ptr := arena.string(title)
if ptr == 0 {
t.Fatalf("got nullptr")
}
if got := db.mem.readString(ptr, math.MaxUint32); got != title {
t.Errorf("got %q, want %q", got, title)
}
const body = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
ptr = arena.string(body)
if ptr == 0 {
t.Fatalf("got nullptr")
}
if got := db.mem.readString(ptr, math.MaxUint32); got != body {
t.Errorf("got %q, want %q", got, body)
}
}
func TestConn_newBytes(t *testing.T) {
t.Parallel()
db, err := Open(":memory:")
if err != nil {
t.Fatal(err)
@@ -137,6 +79,8 @@ func TestConn_newBytes(t *testing.T) {
}
func TestConn_newString(t *testing.T) {
t.Parallel()
db, err := Open(":memory:")
if err != nil {
t.Fatal(err)
@@ -161,6 +105,8 @@ func TestConn_newString(t *testing.T) {
}
func TestConn_getString(t *testing.T) {
t.Parallel()
db, err := Open(":memory:")
if err != nil {
t.Fatal(err)
@@ -200,6 +146,8 @@ func TestConn_getString(t *testing.T) {
}
func TestConn_free(t *testing.T) {
t.Parallel()
db, err := Open(":memory:")
if err != nil {
t.Fatal(err)

View File

@@ -9,11 +9,15 @@ const (
_UTF8 = 1
_MAX_STRING = 512 // Used for short strings: names, error messages…
_MAX_PATHNAME = 512
ptrlen = 4
)
// ErrorCode is a result code that [Error.Code] might return.
//
// https://www.sqlite.org/rescode.html
type ErrorCode uint8
const (
@@ -47,6 +51,9 @@ const (
WARNING ErrorCode = 28 /* Warnings from sqlite3_log() */
)
// ExtendedErrorCode is a result code that [Error.ExtendedCode] might return.
//
// https://www.sqlite.org/rescode.html
type (
ExtendedErrorCode uint16
xErrorCode = ExtendedErrorCode
@@ -128,6 +135,9 @@ const (
AUTH_USER ExtendedErrorCode = xErrorCode(AUTH) | (1 << 8)
)
// OpenFlag is a flag for a file open operation.
//
// https://www.sqlite.org/c3ref/c_open_autoproxy.html
type OpenFlag uint32
const (
@@ -155,14 +165,17 @@ const (
OPEN_EXRESCODE OpenFlag = 0x02000000 /* Extended result codes */
)
type AccessFlag uint32
type _AccessFlag uint32
const (
ACCESS_EXISTS AccessFlag = 0
ACCESS_READWRITE AccessFlag = 1 /* Used by PRAGMA temp_store_directory */
ACCESS_READ AccessFlag = 2 /* Unused */
_ACCESS_EXISTS _AccessFlag = 0
_ACCESS_READWRITE _AccessFlag = 1 /* Used by PRAGMA temp_store_directory */
_ACCESS_READ _AccessFlag = 2 /* Unused */
)
// PrepareFlag is a flag that can be passed to [Conn.PrepareFlags].
//
// https://www.sqlite.org/c3ref/c_prepare_normalize.html
type PrepareFlag uint32
const (
@@ -171,6 +184,9 @@ const (
PREPARE_NO_VTAB PrepareFlag = 0x04
)
// Datatype is a fundamental datatype of SQLite.
//
// https://www.sqlite.org/c3ref/c_blob.html
type Datatype uint32
const (
@@ -181,6 +197,7 @@ const (
NULL Datatype = 5
)
// String implements the [fmt.Stringer] interface.
func (t Datatype) String() string {
const name = "INTEGERFLOATTEXTBLOBNULL"
switch t {

View File

@@ -3,6 +3,8 @@ package sqlite3
import "testing"
func TestDatatype_String(t *testing.T) {
t.Parallel()
tests := []struct {
data Datatype
want string

361
driver/driver.go Normal file
View File

@@ -0,0 +1,361 @@
// Package driver provides a database/sql driver for SQLite.
package driver
import (
"context"
"database/sql"
"database/sql/driver"
"fmt"
"io"
"net/url"
"strings"
"time"
"github.com/ncruces/go-sqlite3"
)
func init() {
sql.Register("sqlite3", sqlite{})
}
type sqlite struct{}
func (sqlite) Open(name string) (driver.Conn, error) {
c, err := sqlite3.OpenFlags(name, sqlite3.OPEN_READWRITE|sqlite3.OPEN_CREATE|sqlite3.OPEN_URI|sqlite3.OPEN_EXRESCODE)
if err != nil {
return nil, err
}
var txBegin string
var pragmas strings.Builder
if _, after, ok := strings.Cut(name, "?"); ok {
query, _ := url.ParseQuery(after)
switch s := query.Get("_txlock"); s {
case "":
txBegin = "BEGIN"
case "deferred", "immediate", "exclusive":
txBegin = "BEGIN " + s
default:
return nil, fmt.Errorf("sqlite3: invalid _txlock: %s", s)
}
for _, p := range query["_pragma"] {
pragmas.WriteString(`PRAGMA `)
pragmas.WriteString(p)
pragmas.WriteByte(';')
}
}
if pragmas.Len() == 0 {
pragmas.WriteString(`PRAGMA locking_mode=normal;`)
pragmas.WriteString(`PRAGMA busy_timeout=60000;`)
}
err = c.Exec(pragmas.String())
if err != nil {
return nil, fmt.Errorf("sqlite3: invalid _pragma: %w", err)
}
return conn{
conn: c,
txBegin: txBegin,
pragmas: pragmas.String(),
}, nil
}
type conn struct {
conn *sqlite3.Conn
pragmas string
txBegin string
txReadOnly bool
}
var (
// Ensure these interfaces are implemented:
_ driver.Validator = conn{}
_ driver.SessionResetter = conn{}
_ driver.ExecerContext = conn{}
_ driver.ConnBeginTx = conn{}
)
func (c conn) Close() error {
return c.conn.Close()
}
func (c conn) IsValid() bool {
// Pool only normal locking mode connections.
stmt, _, err := c.conn.Prepare(`PRAGMA locking_mode`)
if err != nil {
return false
}
defer stmt.Close()
return stmt.Step() && stmt.ColumnText(0) == "normal"
}
func (c conn) ResetSession(ctx context.Context) error {
return c.conn.Exec(c.pragmas)
}
func (c conn) Begin() (driver.Tx, error) {
return c.BeginTx(context.Background(), driver.TxOptions{})
}
func (c conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
switch opts.Isolation {
default:
return nil, isolationErr
case driver.IsolationLevel(sql.LevelDefault):
case driver.IsolationLevel(sql.LevelSerializable):
}
txBegin := c.txBegin
if opts.ReadOnly {
txBegin = `
BEGIN deferred;
PRAGMA query_only=on;
`
}
c.txReadOnly = opts.ReadOnly
err := c.conn.Exec(txBegin)
if err != nil {
return nil, err
}
return c, nil
}
func (c conn) Commit() error {
if c.txReadOnly {
return c.Rollback()
}
err := c.conn.Exec(`COMMIT`)
if err != nil {
c.Rollback()
}
return err
}
func (c conn) Rollback() error {
return c.conn.Exec(`ROLLBACK`)
}
func (c conn) Prepare(query string) (driver.Stmt, error) {
s, tail, err := c.conn.Prepare(query)
if err != nil {
return nil, err
}
if tail != "" {
// Check if the tail contains any SQL.
st, _, err := c.conn.Prepare(tail)
if err != nil {
s.Close()
return nil, err
}
if st != nil {
s.Close()
st.Close()
return nil, tailErr
}
}
return stmt{s, c.conn}, nil
}
func (c conn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
if len(args) != 0 {
// Slow path.
return nil, driver.ErrSkip
}
ch := c.conn.SetInterrupt(ctx.Done())
defer c.conn.SetInterrupt(ch)
err := c.conn.Exec(query)
if err != nil {
return nil, err
}
return result{
int64(c.conn.LastInsertRowID()),
int64(c.conn.Changes()),
}, nil
}
type stmt struct {
stmt *sqlite3.Stmt
conn *sqlite3.Conn
}
var (
// Ensure these interfaces are implemented:
_ driver.StmtExecContext = stmt{}
_ driver.StmtQueryContext = stmt{}
_ driver.NamedValueChecker = stmt{}
)
func (s stmt) Close() error {
return s.stmt.Close()
}
func (s stmt) NumInput() int {
n := s.stmt.BindCount()
for i := 1; i <= n; i++ {
if s.stmt.BindName(i) != "" {
return -1
}
}
return n
}
// Deprecated: use ExecContext instead.
func (s stmt) Exec(args []driver.Value) (driver.Result, error) {
return s.ExecContext(context.Background(), namedValues(args))
}
// Deprecated: use QueryContext instead.
func (s stmt) Query(args []driver.Value) (driver.Rows, error) {
return s.QueryContext(context.Background(), namedValues(args))
}
func (s stmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) {
// Use QueryContext to setup bindings.
// No need to close rows: that simply resets the statement, exec does the same.
_, err := s.QueryContext(ctx, args)
if err != nil {
return nil, err
}
err = s.stmt.Exec()
if err != nil {
return nil, err
}
return result{
int64(s.conn.LastInsertRowID()),
int64(s.conn.Changes()),
}, nil
}
func (s stmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
err := s.stmt.ClearBindings()
if err != nil {
return nil, err
}
var ids [3]int
for _, arg := range args {
ids := ids[:0]
if arg.Name == "" {
ids = append(ids, arg.Ordinal)
} else {
for _, prefix := range []string{":", "@", "$"} {
if id := s.stmt.BindIndex(prefix + arg.Name); id != 0 {
ids = append(ids, id)
}
}
}
for _, id := range ids {
switch a := arg.Value.(type) {
case bool:
err = s.stmt.BindBool(id, a)
case int:
err = s.stmt.BindInt(id, a)
case int64:
err = s.stmt.BindInt64(id, a)
case float64:
err = s.stmt.BindFloat(id, a)
case string:
err = s.stmt.BindText(id, a)
case []byte:
err = s.stmt.BindBlob(id, a)
case sqlite3.ZeroBlob:
err = s.stmt.BindZeroBlob(id, int64(a))
case time.Time:
err = s.stmt.BindText(id, a.Format(time.RFC3339Nano))
case nil:
err = s.stmt.BindNull(id)
default:
panic(assertErr)
}
}
if err != nil {
return nil, err
}
}
return rows{ctx, s.stmt, s.conn}, nil
}
func (s stmt) CheckNamedValue(arg *driver.NamedValue) error {
switch arg.Value.(type) {
case bool, int, int64, float64, string, []byte,
sqlite3.ZeroBlob, time.Time, nil:
return nil
default:
return driver.ErrSkip
}
}
type result struct{ lastInsertId, rowsAffected int64 }
func (r result) LastInsertId() (int64, error) {
return r.lastInsertId, nil
}
func (r result) RowsAffected() (int64, error) {
return r.rowsAffected, nil
}
type rows struct {
ctx context.Context
stmt *sqlite3.Stmt
conn *sqlite3.Conn
}
func (r rows) Close() error {
return r.stmt.Reset()
}
func (r rows) Columns() []string {
count := r.stmt.ColumnCount()
columns := make([]string, count)
for i := range columns {
columns[i] = r.stmt.ColumnName(i)
}
return columns
}
func (r rows) Next(dest []driver.Value) error {
ch := r.conn.SetInterrupt(r.ctx.Done())
defer r.conn.SetInterrupt(ch)
if !r.stmt.Step() {
if err := r.stmt.Err(); err != nil {
return err
}
return io.EOF
}
for i := range dest {
switch r.stmt.ColumnType(i) {
case sqlite3.INTEGER:
dest[i] = r.stmt.ColumnInt64(i)
case sqlite3.FLOAT:
dest[i] = r.stmt.ColumnFloat(i)
case sqlite3.TEXT:
dest[i] = maybeDate(r.stmt.ColumnText(i))
case sqlite3.BLOB:
buf, _ := dest[i].([]byte)
dest[i] = r.stmt.ColumnBlob(i, buf)
case sqlite3.NULL:
if buf, ok := dest[i].([]byte); ok {
dest[i] = buf[0:0]
} else {
dest[i] = nil
}
default:
panic(assertErr)
}
}
return r.stmt.Err()
}

348
driver/driver_test.go Normal file
View File

@@ -0,0 +1,348 @@
// Package driver provides a database/sql driver for SQLite.
package driver
import (
"bytes"
"context"
"database/sql"
"errors"
"math"
"path/filepath"
"testing"
"time"
"github.com/ncruces/go-sqlite3"
)
func Test_Open_dir(t *testing.T) {
db, err := sql.Open("sqlite3", ".")
if err != nil {
t.Fatal(err)
}
defer db.Close()
_, err = db.Conn(context.TODO())
if err == nil {
t.Fatal("want error")
}
var serr *sqlite3.Error
if !errors.As(err, &serr) {
t.Fatalf("got %T, want sqlite3.Error", err)
}
if rc := serr.Code(); rc != sqlite3.CANTOPEN {
t.Errorf("got %d, want sqlite3.CANTOPEN", rc)
}
if got := err.Error(); got != `sqlite3: unable to open database file` {
t.Error("got message: ", got)
}
}
func Test_Open_pragma(t *testing.T) {
db, err := sql.Open("sqlite3", "file::memory:?_pragma=busy_timeout(1000)")
if err != nil {
t.Fatal(err)
}
defer db.Close()
var timeout int
err = db.QueryRow(`PRAGMA busy_timeout`).Scan(&timeout)
if err != nil {
t.Fatal(err)
}
if timeout != 1000 {
t.Errorf("got %v, want 1000", timeout)
}
}
func Test_Open_pragma_invalid(t *testing.T) {
db, err := sql.Open("sqlite3", "file::memory:?_pragma=busy_timeout+1000")
if err != nil {
t.Fatal(err)
}
defer db.Close()
_, err = db.Conn(context.TODO())
if err == nil {
t.Fatal("want error")
}
var serr *sqlite3.Error
if !errors.As(err, &serr) {
t.Fatalf("got %T, want sqlite3.Error", err)
}
if rc := serr.Code(); rc != sqlite3.ERROR {
t.Errorf("got %d, want sqlite3.ERROR", rc)
}
if got := err.Error(); got != `sqlite3: invalid _pragma: sqlite3: SQL logic error: near "1000": syntax error` {
t.Error("got message: ", got)
}
}
func Test_Open_txLock(t *testing.T) {
db, err := sql.Open("sqlite3", "file:"+
filepath.Join(t.TempDir(), "test.db")+
"?_txlock=exclusive&_pragma=busy_timeout(0)")
if err != nil {
t.Fatal(err)
}
defer db.Close()
tx1, err := db.Begin()
if err != nil {
t.Fatal(err)
}
_, err = db.Begin()
if err == nil {
t.Error("want error")
}
var serr *sqlite3.Error
if !errors.As(err, &serr) {
t.Fatalf("got %T, want sqlite3.Error", err)
}
if rc := serr.Code(); rc != sqlite3.BUSY {
t.Errorf("got %d, want sqlite3.BUSY", rc)
}
var terr interface{ Temporary() bool }
if !errors.As(err, &terr) || !terr.Temporary() {
t.Error("not temporary", err)
}
if got := err.Error(); got != `sqlite3: database is locked` {
t.Error("got message: ", got)
}
err = tx1.Commit()
if err != nil {
t.Fatal(err)
}
}
func Test_Open_txLock_invalid(t *testing.T) {
db, err := sql.Open("sqlite3", "file::memory:?_txlock=xclusive")
if err != nil {
t.Fatal(err)
}
defer db.Close()
_, err = db.Conn(context.TODO())
if err == nil {
t.Fatal("want error")
}
if got := err.Error(); got != `sqlite3: invalid _txlock: xclusive` {
t.Error("got message: ", got)
}
}
func Test_BeginTx(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
db, err := sql.Open("sqlite3", filepath.Join(t.TempDir(), "test.db"))
if err != nil {
t.Fatal(err)
}
defer db.Close()
_, err = db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
if err.Error() != string(isolationErr) {
t.Error("want isolationErr")
}
tx1, err := db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
if err != nil {
t.Fatal(err)
}
tx2, err := db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
if err != nil {
t.Fatal(err)
}
_, err = tx1.Exec(`CREATE TABLE IF NOT EXISTS test (col)`)
if err == nil {
t.Error("want error")
}
var serr *sqlite3.Error
if !errors.As(err, &serr) {
t.Fatalf("got %T, want sqlite3.Error", err)
}
if rc := serr.Code(); rc != sqlite3.READONLY {
t.Errorf("got %d, want sqlite3.READONLY", rc)
}
if got := err.Error(); got != `sqlite3: attempt to write a readonly database` {
t.Error("got message: ", got)
}
err = tx2.Commit()
if err != nil {
t.Fatal(err)
}
err = tx1.Commit()
if err != nil {
t.Fatal(err)
}
}
func Test_Prepare(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
stmt, err := db.Prepare(`SELECT 1; -- HERE`)
if err != nil {
t.Error(err)
}
defer stmt.Close()
var serr *sqlite3.Error
_, err = db.Prepare(`SELECT`)
if err == nil {
t.Error("want error")
}
if !errors.As(err, &serr) {
t.Fatalf("got %T, want sqlite3.Error", err)
}
if rc := serr.Code(); rc != sqlite3.ERROR {
t.Errorf("got %d, want sqlite3.ERROR", rc)
}
if got := err.Error(); got != `sqlite3: SQL logic error: incomplete input` {
t.Error("got message: ", got)
}
_, err = db.Prepare(`SELECT 1; SELECT`)
if err == nil {
t.Error("want error")
}
if !errors.As(err, &serr) {
t.Fatalf("got %T, want sqlite3.Error", err)
}
if rc := serr.Code(); rc != sqlite3.ERROR {
t.Errorf("got %d, want sqlite3.ERROR", rc)
}
if got := err.Error(); got != `sqlite3: SQL logic error: incomplete input` {
t.Error("got message: ", got)
}
_, err = db.Prepare(`SELECT 1; SELECT 2`)
if err.Error() != string(tailErr) {
t.Error("want tailErr")
}
}
func Test_QueryRow_named(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
conn, err := db.Conn(ctx)
if err != nil {
t.Fatal(err)
}
defer conn.Close()
stmt, err := conn.PrepareContext(ctx, `SELECT ?, ?5, :AAA, @AAA, $AAA`)
if err != nil {
t.Fatal(err)
}
defer stmt.Close()
date := time.Now()
row := stmt.QueryRow(true, sql.Named("AAA", math.Pi), nil /*3*/, nil /*4*/, date /*5*/)
var first bool
var fifth time.Time
var colon, at, dollar float32
err = row.Scan(&first, &fifth, &colon, &at, &dollar)
if err != nil {
t.Fatal(err)
}
if first != true {
t.Errorf("want true, got %v", first)
}
if colon != math.Pi {
t.Errorf("want π, got %v", colon)
}
if at != math.Pi {
t.Errorf("want π, got %v", at)
}
if dollar != math.Pi {
t.Errorf("want π, got %v", dollar)
}
if !fifth.Equal(date) {
t.Errorf("want %v, got %v", date, fifth)
}
}
func Test_QueryRow_blob_null(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
rows, err := db.Query(`
SELECT NULL UNION ALL
SELECT x'cafe' UNION ALL
SELECT x'babe' UNION ALL
SELECT NULL
`)
if err != nil {
t.Fatal(err)
}
want := [][]byte{nil, {0xca, 0xfe}, {0xba, 0xbe}, nil}
for i := 0; rows.Next(); i++ {
var buf []byte
err = rows.Scan(&buf)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(buf, want[i]) {
t.Errorf("got %q, want %q", buf, want[i])
}
}
}
func Test_ZeroBlob(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
conn, err := db.Conn(ctx)
if err != nil {
t.Fatal(err)
}
defer conn.Close()
_, err = conn.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS test (col)`)
if err != nil {
t.Fatal(err)
}
_, err = conn.ExecContext(ctx, `INSERT INTO test(col) VALUES(?)`, sqlite3.ZeroBlob(4))
if err != nil {
t.Fatal(err)
}
var got []byte
err = conn.QueryRowContext(ctx, `SELECT col FROM test`).Scan(&got)
if err != nil {
t.Fatal(err)
}
if string(got) != "\x00\x00\x00\x00" {
t.Errorf(`got %q, want "\x00\x00\x00\x00"`, got)
}
}

11
driver/error.go Normal file
View File

@@ -0,0 +1,11 @@
package driver
type errorString string
func (e errorString) Error() string { return string(e) }
const (
assertErr = errorString("sqlite3: assertion failed")
tailErr = errorString("sqlite3: multiple statements")
isolationErr = errorString("sqlite3: unsupported isolation level")
)

149
driver/example_test.go Normal file
View File

@@ -0,0 +1,149 @@
package driver_test
// Adapted from: https://go.dev/doc/tutorial/database-access
import (
"database/sql"
"fmt"
"log"
"os"
_ "github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
)
var db *sql.DB
type Album struct {
ID int64
Title string
Artist string
Price float32
}
func Example() {
// Get a database handle.
var err error
db, err = sql.Open("sqlite3", "./recordings.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
defer os.Remove("./recordings.db")
err = createAlbumsTable()
if err != nil {
log.Fatal(err)
}
albums, err := albumsByArtist("John Coltrane")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Albums found: %v\n", albums)
// Hard-code ID 2 here to test the query.
alb, err := albumByID(2)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Album found: %v\n", alb)
albID, err := addAlbum(Album{
Title: "The Modern Sound of Betty Carter",
Artist: "Betty Carter",
Price: 49.99,
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("ID of added album: %v\n", albID)
// Output:
// Albums found: [{1 Blue Train John Coltrane 56.99} {2 Giant Steps John Coltrane 63.99}]
// Album found: {2 Giant Steps John Coltrane 63.99}
// ID of added album: 5
}
func createAlbumsTable() error {
_, err := db.Exec(`
DROP TABLE IF EXISTS album;
CREATE TABLE album (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title VARCHAR(128) NOT NULL,
artist VARCHAR(255) NOT NULL,
price DECIMAL(5,2) NOT NULL
);
`)
if err != nil {
return err
}
_, err = db.Exec(`
INSERT INTO album
(title, artist, price)
VALUES
('Blue Train', 'John Coltrane', 56.99),
('Giant Steps', 'John Coltrane', 63.99),
('Jeru', 'Gerry Mulligan', 17.99),
('Sarah Vaughan', 'Sarah Vaughan', 34.98)
`)
if err != nil {
return err
}
return nil
}
// albumsByArtist queries for albums that have the specified artist name.
func albumsByArtist(name string) ([]Album, error) {
// An albums slice to hold data from returned rows.
var albums []Album
rows, err := db.Query("SELECT * FROM album WHERE artist = ?", name)
if err != nil {
return nil, fmt.Errorf("albumsByArtist %q: %w", name, err)
}
defer rows.Close()
// Loop through rows, using Scan to assign column data to struct fields.
for rows.Next() {
var alb Album
if err := rows.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price); err != nil {
return nil, fmt.Errorf("albumsByArtist %q: %w", name, err)
}
albums = append(albums, alb)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("albumsByArtist %q: %w", name, err)
}
return albums, nil
}
// albumByID queries for the album with the specified ID.
func albumByID(id int64) (Album, error) {
// An album to hold data from the returned row.
var alb Album
row := db.QueryRow("SELECT * FROM album WHERE id = ?", id)
if err := row.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price); err != nil {
if err == sql.ErrNoRows {
return alb, fmt.Errorf("albumsById %d: no such album", id)
}
return alb, fmt.Errorf("albumsById %d: %w", id, err)
}
return alb, nil
}
// addAlbum adds the specified album to the database,
// returning the album ID of the new entry
func addAlbum(alb Album) (int64, error) {
result, err := db.Exec("INSERT INTO album (title, artist, price) VALUES (?, ?, ?)", alb.Title, alb.Artist, alb.Price)
if err != nil {
return 0, fmt.Errorf("addAlbum: %w", err)
}
id, err := result.LastInsertId()
if err != nil {
return 0, fmt.Errorf("addAlbum: %w", err)
}
return id, nil
}

31
driver/time.go Normal file
View File

@@ -0,0 +1,31 @@
package driver
import (
"database/sql/driver"
"time"
)
// Convert a string in [time.RFC3339Nano] format into a [time.Time]
// if it roundtrips back to the same string.
// This way times can be persisted to, and recovered from, the database,
// but if a string is needed, [database/sql] will recover the same string.
func maybeDate(text string) driver.Value {
// Weed out (some) values that can't possibly be
// [time.RFC3339Nano] timestamps.
if len(text) < len("2006-01-02T15:04:05Z") {
return text
}
if len(text) > len(time.RFC3339Nano) {
return text
}
if text[4] != '-' || text[10] != 'T' || text[16] != ':' {
return text
}
// Slow path.
date, err := time.Parse(time.RFC3339Nano, text)
if err == nil && date.Format(time.RFC3339Nano) == text {
return date
}
return text
}

46
driver/time_test.go Normal file
View File

@@ -0,0 +1,46 @@
package driver
import (
"testing"
"time"
)
func Fuzz_maybeDate(f *testing.F) {
f.Add("")
f.Add(" ")
f.Add("SQLite")
f.Add(time.RFC3339)
f.Add(time.RFC3339Nano)
f.Add(time.Layout)
f.Add(time.DateTime)
f.Add(time.DateOnly)
f.Add(time.TimeOnly)
f.Add("2006-01-02T15:04:05Z")
f.Add("2006-01-02T15:04:05.000Z")
f.Add("2006-01-02T15:04:05.9999999999Z")
f.Add("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.")
f.Fuzz(func(t *testing.T, str string) {
value := maybeDate(str)
switch v := value.(type) {
case time.Time:
// Make sure times round-trip to the same string:
// https://pkg.go.dev/database/sql#Rows.Scan
if v.Format(time.RFC3339Nano) != str {
t.Fatalf("did not round-trip: %q", str)
}
case string:
if v != str {
t.Fatalf("did not round-trip: %q", str)
}
date, err := time.Parse(time.RFC3339Nano, str)
if err == nil && date.Format(time.RFC3339Nano) == str {
t.Fatalf("would round-trip: %q", str)
}
default:
t.Fatalf("invalid type %T: %q", v, str)
}
})
}

14
driver/util.go Normal file
View File

@@ -0,0 +1,14 @@
package driver
import "database/sql/driver"
func namedValues(args []driver.Value) []driver.NamedValue {
named := make([]driver.NamedValue, len(args))
for i, v := range args {
named[i] = driver.NamedValue{
Ordinal: i + 1,
Value: v,
}
}
return named
}

18
driver/util_test.go Normal file
View File

@@ -0,0 +1,18 @@
package driver
import (
"database/sql/driver"
"reflect"
"testing"
)
func Test_namedValues(t *testing.T) {
want := []driver.NamedValue{
{Ordinal: 1, Value: true},
{Ordinal: 2, Value: false},
}
got := namedValues([]driver.Value{true, false})
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v, want %v", got, want)
}
}

View File

@@ -28,15 +28,23 @@ zig cc --target=wasm32-wasi -flto -g0 -Os \
-Wl,--export=sqlite3_step \
-Wl,--export=sqlite3_exec \
-Wl,--export=sqlite3_clear_bindings \
-Wl,--export=sqlite3_bind_parameter_count \
-Wl,--export=sqlite3_bind_parameter_index \
-Wl,--export=sqlite3_bind_parameter_name \
-Wl,--export=sqlite3_bind_null \
-Wl,--export=sqlite3_bind_int64 \
-Wl,--export=sqlite3_bind_double \
-Wl,--export=sqlite3_bind_text64 \
-Wl,--export=sqlite3_bind_blob64 \
-Wl,--export=sqlite3_bind_zeroblob64 \
-Wl,--export=sqlite3_bind_null \
-Wl,--export=sqlite3_column_count \
-Wl,--export=sqlite3_column_name \
-Wl,--export=sqlite3_column_type \
-Wl,--export=sqlite3_column_int64 \
-Wl,--export=sqlite3_column_double \
-Wl,--export=sqlite3_column_text \
-Wl,--export=sqlite3_column_blob \
-Wl,--export=sqlite3_column_bytes \
-Wl,--export=sqlite3_column_type \
-Wl,--export=sqlite3_last_insert_rowid \
-Wl,--export=sqlite3_changes64 \
-Wl,--export=sqlite3_interrupt \

View File

@@ -1,3 +1,7 @@
// Package embed embeds SQLite into your application.
//
// You can obtain this build of SQLite from:
// https://github.com/ncruces/go-sqlite3/tree/main/embed
package embed
import (

Binary file not shown.

View File

@@ -50,6 +50,11 @@ func (e *Error) Error() string {
return b.String()
}
// Temporary returns true for [BUSY] errors.
func (e *Error) Temporary() bool {
return e.Code() == BUSY
}
// SQL returns the SQL starting at the token that triggered a syntax error.
func (e *Error) SQL() string {
return e.sql
@@ -60,12 +65,14 @@ type errorString string
func (e errorString) Error() string { return string(e) }
const (
binaryErr = errorString("sqlite3: no SQLite binary embed/set/loaded")
nilErr = errorString("sqlite3: invalid memory address or null pointer dereference")
oomErr = errorString("sqlite3: out of memory")
rangeErr = errorString("sqlite3: index out of range")
noNulErr = errorString("sqlite3: missing NUL terminator")
noGlobalErr = errorString("sqlite3: could not find global: ")
noFuncErr = errorString("sqlite3: could not find function: ")
timeErr = errorString("sqlite3: invalid time value")
)
func assertErr() errorString {

View File

@@ -1,4 +1,4 @@
package main
package sqlite3_test
import (
"fmt"
@@ -8,8 +8,10 @@ import (
_ "github.com/ncruces/go-sqlite3/embed"
)
func main() {
db, err := sqlite3.Open(":memory:")
const memory = ":memory:"
func Example() {
db, err := sqlite3.Open(memory)
if err != nil {
log.Fatal(err)
}
@@ -45,4 +47,9 @@ func main() {
if err != nil {
log.Fatal(err)
}
// Output:
// 0 go
// 1 zig
// 2 whatever
}

2
go.mod
View File

@@ -4,7 +4,7 @@ go 1.19
require (
github.com/ncruces/julianday v0.1.5
github.com/tetratelabs/wazero v1.0.0-pre.8
github.com/tetratelabs/wazero v1.0.0-pre.9
golang.org/x/sync v0.1.0
golang.org/x/sys v0.5.0
)

4
go.sum
View File

@@ -1,7 +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/tetratelabs/wazero v1.0.0-pre.8 h1:Ir82PWj79WCppH+9ny73eGY2qv+oCnE3VwMY92cBSyI=
github.com/tetratelabs/wazero v1.0.0-pre.8/go.mod h1:u8wrFmpdrykiFK0DFPiFm5a4+0RzsdmXYVtijBKqUVo=
github.com/tetratelabs/wazero v1.0.0-pre.9 h1:2uVdi2bvTi/JQxG2cp3LRm2aRadd3nURn5jcfbvqZcw=
github.com/tetratelabs/wazero v1.0.0-pre.9/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=

8
mem.go
View File

@@ -99,9 +99,13 @@ func (m memory) readString(ptr, maxlen uint32) string {
}
}
func (m memory) writeBytes(ptr uint32, b []byte) {
buf := m.view(ptr, uint32(len(b)))
copy(buf, b)
}
func (m memory) writeString(ptr uint32, s string) {
siz := uint32(len(s) + 1)
buf := m.view(ptr, siz)
buf := m.view(ptr, uint32(len(s)+1))
buf[len(s)] = 0
copy(buf, s)
}

161
mock_test.go Normal file
View File

@@ -0,0 +1,161 @@
package sqlite3
import (
"context"
"encoding/binary"
"math"
"github.com/tetratelabs/wazero/api"
)
func init() {
Path = "./embed/sqlite3.wasm"
}
func newMemory(size uint32) memory {
mem := make(mockMemory, size)
return memory{mockModule{&mem}}
}
type mockModule struct {
memory api.Memory
}
func (m mockModule) Memory() api.Memory { return m.memory }
func (m mockModule) String() string { return "mockModule" }
func (m mockModule) Name() string { return "mockModule" }
func (m mockModule) ExportedGlobal(name string) api.Global { return nil }
func (m mockModule) ExportedMemory(name string) api.Memory { return nil }
func (m mockModule) ExportedFunction(name string) api.Function { return nil }
func (m mockModule) ExportedMemoryDefinitions() map[string]api.MemoryDefinition { return nil }
func (m mockModule) ExportedFunctionDefinitions() map[string]api.FunctionDefinition { return nil }
func (m mockModule) CloseWithExitCode(ctx context.Context, exitCode uint32) error { return nil }
func (m mockModule) Close(context.Context) error { return nil }
type mockMemory []byte
func (m mockMemory) Definition() api.MemoryDefinition { return nil }
func (m mockMemory) Size() uint32 { return uint32(len(m)) }
func (m mockMemory) ReadByte(offset uint32) (byte, bool) {
if offset >= m.Size() {
return 0, false
}
return m[offset], true
}
func (m mockMemory) ReadUint16Le(offset uint32) (uint16, bool) {
if !m.hasSize(offset, 2) {
return 0, false
}
return binary.LittleEndian.Uint16(m[offset : offset+2]), true
}
func (m mockMemory) ReadUint32Le(offset uint32) (uint32, bool) {
if !m.hasSize(offset, 4) {
return 0, false
}
return binary.LittleEndian.Uint32(m[offset : offset+4]), true
}
func (m mockMemory) ReadFloat32Le(offset uint32) (float32, bool) {
v, ok := m.ReadUint32Le(offset)
if !ok {
return 0, false
}
return math.Float32frombits(v), true
}
func (m mockMemory) ReadUint64Le(offset uint32) (uint64, bool) {
if !m.hasSize(offset, 8) {
return 0, false
}
return binary.LittleEndian.Uint64(m[offset : offset+8]), true
}
func (m mockMemory) ReadFloat64Le(offset uint32) (float64, bool) {
v, ok := m.ReadUint64Le(offset)
if !ok {
return 0, false
}
return math.Float64frombits(v), true
}
func (m mockMemory) Read(offset, byteCount uint32) ([]byte, bool) {
if !m.hasSize(offset, byteCount) {
return nil, false
}
return m[offset : offset+byteCount : offset+byteCount], true
}
func (m mockMemory) WriteByte(offset uint32, v byte) bool {
if offset >= m.Size() {
return false
}
m[offset] = v
return true
}
func (m mockMemory) WriteUint16Le(offset uint32, v uint16) bool {
if !m.hasSize(offset, 2) {
return false
}
binary.LittleEndian.PutUint16(m[offset:], v)
return true
}
func (m mockMemory) WriteUint32Le(offset, v uint32) bool {
if !m.hasSize(offset, 4) {
return false
}
binary.LittleEndian.PutUint32(m[offset:], v)
return true
}
func (m mockMemory) WriteFloat32Le(offset uint32, v float32) bool {
return m.WriteUint32Le(offset, math.Float32bits(v))
}
func (m mockMemory) WriteUint64Le(offset uint32, v uint64) bool {
if !m.hasSize(offset, 8) {
return false
}
binary.LittleEndian.PutUint64(m[offset:], v)
return true
}
func (m mockMemory) WriteFloat64Le(offset uint32, v float64) bool {
return m.WriteUint64Le(offset, math.Float64bits(v))
}
func (m mockMemory) Write(offset uint32, val []byte) bool {
if !m.hasSize(offset, uint32(len(val))) {
return false
}
copy(m[offset:], val)
return true
}
func (m mockMemory) WriteString(offset uint32, val string) bool {
if !m.hasSize(offset, uint32(len(val))) {
return false
}
copy(m[offset:], val)
return true
}
func (m *mockMemory) Grow(delta uint32) (result uint32, ok bool) {
prev := (len(*m) + 65535) / 65536
*m = append(*m, make([]byte, 65536*delta)...)
return uint32(prev), true
}
func (m mockMemory) PageSize() (result uint32) {
return uint32(len(m) / 65536)
}
func (m mockMemory) hasSize(offset uint32, byteCount uint32) bool {
return uint64(offset)+uint64(byteCount) <= uint64(len(m))
}

View File

@@ -1,3 +1,4 @@
#include <stdbool.h>
#include <stdlib.h>
#include <time.h>
@@ -33,6 +34,7 @@ int go_write(sqlite3_file *, const void *, int iAmt, sqlite3_int64 iOfst);
int go_truncate(sqlite3_file *, sqlite3_int64 size);
int go_sync(sqlite3_file *, int flags);
int go_file_size(sqlite3_file *, sqlite3_int64 *pSize);
int go_file_control(sqlite3_file *pFile, int op, void *pArg);
int go_lock(sqlite3_file *pFile, int eLock);
int go_unlock(sqlite3_file *pFile, int eLock);
@@ -94,7 +96,7 @@ int sqlite3_os_init() {
.xCurrentTime = go_current_time,
.xCurrentTimeInt64 = go_current_time_64,
};
return sqlite3_vfs_register(&go_vfs, /*default=*/1);
return sqlite3_vfs_register(&go_vfs, /*default=*/true);
}
sqlite3_destructor_type malloc_destructor = &free;

View File

@@ -5,6 +5,9 @@
#define SQLITE_OS_OTHER 1
#define SQLITE_BYTEORDER 1234
#define HAVE_STDINT_H 1
#define HAVE_INTTYPES_H 1
#define HAVE_ISNAN 1
#define HAVE_USLEEP 1
#define HAVE_LOCALTIME_S 1
@@ -25,12 +28,18 @@
#define SQLITE_OMIT_AUTOINIT
#define SQLITE_USE_ALLOCA
// Recommended Extensions
// #define SQLITE_ENABLE_MATH_FUNCTIONS 1
// #define SQLITE_ENABLE_FTS3 1
// #define SQLITE_ENABLE_FTS3_PARENTHESIS 1
// #define SQLITE_ENABLE_FTS4 1
// #define SQLITE_ENABLE_FTS5 1
// #define SQLITE_ENABLE_RTREE 1
// #define SQLITE_ENABLE_GEOPOLY 1
// Need this to access WAL databases without the use of shared memory.
#define SQLITE_DEFAULT_LOCKING_MODE 1
// Go uses UTF-8 everywhere.
#define SQLITE_OMIT_UTF16
// Remove some testing code.
#define SQLITE_UNTESTABLE
// Implemented in Go.
int localtime_s(struct tm *const pTm, time_t const *const pTime);

155
stmt.go
View File

@@ -2,6 +2,7 @@ package sqlite3
import (
"math"
"time"
)
// Stmt is a prepared statement object.
@@ -15,15 +16,17 @@ type Stmt struct {
// Close destroys the prepared statement object.
//
// It is safe to close a nil, zero or closed prepared statement.
//
// https://www.sqlite.org/c3ref/finalize.html
func (s *Stmt) Close() error {
if s == nil {
if s == nil || s.handle == 0 {
return nil
}
r, err := s.c.api.finalize.Call(s.c.ctx, uint64(s.handle))
if err != nil {
return err
panic(err)
}
s.handle = 0
@@ -36,7 +39,7 @@ func (s *Stmt) Close() error {
func (s *Stmt) Reset() error {
r, err := s.c.api.reset.Call(s.c.ctx, uint64(s.handle))
if err != nil {
return err
panic(err)
}
s.err = nil
return s.c.error(r[0])
@@ -48,7 +51,7 @@ func (s *Stmt) Reset() error {
func (s *Stmt) ClearBindings() error {
r, err := s.c.api.clearBindings.Call(s.c.ctx, uint64(s.handle))
if err != nil {
return err
panic(err)
}
return s.c.error(r[0])
}
@@ -65,8 +68,7 @@ func (s *Stmt) ClearBindings() error {
func (s *Stmt) Step() bool {
r, err := s.c.api.step.Call(s.c.ctx, uint64(s.handle))
if err != nil {
s.err = err
return false
panic(err)
}
if r[0] == _ROW {
return true
@@ -95,6 +97,51 @@ func (s *Stmt) Exec() error {
return s.Reset()
}
// BindCount returns the number of SQL parameters in the prepared statement.
//
// https://www.sqlite.org/c3ref/bind_parameter_count.html
func (s *Stmt) BindCount() int {
r, err := s.c.api.bindCount.Call(s.c.ctx,
uint64(s.handle))
if err != nil {
panic(err)
}
return int(r[0])
}
// BindIndex returns the index of a parameter in the prepared statement
// given its name.
//
// https://www.sqlite.org/c3ref/bind_parameter_index.html
func (s *Stmt) BindIndex(name string) int {
defer s.c.arena.reset()
namePtr := s.c.arena.string(name)
r, err := s.c.api.bindIndex.Call(s.c.ctx,
uint64(s.handle), uint64(namePtr))
if err != nil {
panic(err)
}
return int(r[0])
}
// BindName returns the name of a parameter in the prepared statement.
// The leftmost SQL parameter has an index of 1.
//
// https://www.sqlite.org/c3ref/bind_parameter_name.html
func (s *Stmt) BindName(param int) string {
r, err := s.c.api.bindName.Call(s.c.ctx,
uint64(s.handle), uint64(param))
if err != nil {
panic(err)
}
ptr := uint32(r[0])
if ptr == 0 {
return ""
}
return s.c.mem.readString(ptr, _MAX_STRING)
}
// BindBool binds a bool to the prepared statement.
// The leftmost SQL parameter has an index of 1.
// SQLite does not have a separate boolean storage class.
@@ -124,7 +171,7 @@ func (s *Stmt) BindInt64(param int, value int64) error {
r, err := s.c.api.bindInteger.Call(s.c.ctx,
uint64(s.handle), uint64(param), uint64(value))
if err != nil {
return err
panic(err)
}
return s.c.error(r[0])
}
@@ -137,7 +184,7 @@ func (s *Stmt) BindFloat(param int, value float64) error {
r, err := s.c.api.bindFloat.Call(s.c.ctx,
uint64(s.handle), uint64(param), math.Float64bits(value))
if err != nil {
return err
panic(err)
}
return s.c.error(r[0])
}
@@ -153,7 +200,7 @@ func (s *Stmt) BindText(param int, value string) error {
uint64(ptr), uint64(len(value)),
s.c.api.destructor, _UTF8)
if err != nil {
return err
panic(err)
}
return s.c.error(r[0])
}
@@ -170,7 +217,20 @@ func (s *Stmt) BindBlob(param int, value []byte) error {
uint64(ptr), uint64(len(value)),
s.c.api.destructor)
if err != nil {
return err
panic(err)
}
return s.c.error(r[0])
}
// BindZeroBlob binds a zero-filled, length n BLOB to the prepared statement.
// The leftmost SQL parameter has an index of 1.
//
// https://www.sqlite.org/c3ref/bind_blob.html
func (s *Stmt) BindZeroBlob(param int, n int64) error {
r, err := s.c.api.bindZeroBlob.Call(s.c.ctx,
uint64(s.handle), uint64(param), uint64(n))
if err != nil {
panic(err)
}
return s.c.error(r[0])
}
@@ -183,11 +243,59 @@ func (s *Stmt) BindNull(param int) error {
r, err := s.c.api.bindNull.Call(s.c.ctx,
uint64(s.handle), uint64(param))
if err != nil {
return err
panic(err)
}
return s.c.error(r[0])
}
// BindTime binds a [time.Time] to the prepared statement.
// The leftmost SQL parameter has an index of 1.
//
// https://www.sqlite.org/c3ref/bind_blob.html
func (s *Stmt) BindTime(param int, value time.Time, format TimeFormat) error {
switch v := format.Encode(value).(type) {
case string:
s.BindText(param, v)
case int64:
s.BindInt64(param, v)
case float64:
s.BindFloat(param, v)
default:
panic(assertErr())
}
return nil
}
// ColumnCount returns the number of columns in a result set.
//
// https://www.sqlite.org/c3ref/column_count.html
func (s *Stmt) ColumnCount() int {
r, err := s.c.api.columnCount.Call(s.c.ctx,
uint64(s.handle))
if err != nil {
panic(err)
}
return int(r[0])
}
// ColumnName returns the name of the result column.
// The leftmost column of the result set has the index 0.
//
// https://www.sqlite.org/c3ref/column_name.html
func (s *Stmt) ColumnName(col int) string {
r, err := s.c.api.columnName.Call(s.c.ctx,
uint64(s.handle), uint64(col))
if err != nil {
panic(err)
}
ptr := uint32(r[0])
if ptr == 0 {
panic(oomErr)
}
return s.c.mem.readString(ptr, _MAX_STRING)
}
// ColumnType returns the initial [Datatype] of the result column.
// The leftmost column of the result set has the index 0.
//
@@ -249,6 +357,31 @@ func (s *Stmt) ColumnFloat(col int) float64 {
return math.Float64frombits(r[0])
}
// ColumnTime returns the value of the result column as a [time.Time].
// The leftmost column of the result set has the index 0.
//
// https://www.sqlite.org/c3ref/column_blob.html
func (s *Stmt) ColumnTime(col int, format TimeFormat) time.Time {
var v any
switch s.ColumnType(col) {
case INTEGER:
v = s.ColumnInt64(col)
case FLOAT:
v = s.ColumnFloat(col)
case TEXT, BLOB:
v = s.ColumnText(col)
case NULL:
return time.Time{}
default:
panic(assertErr())
}
t, err := format.Decode(v)
if err != nil {
s.err = err
}
return t
}
// ColumnText returns the value of the result column as a string.
// The leftmost column of the result set has the index 0.
//

180
tests/bradfitz/sql_test.go Normal file
View File

@@ -0,0 +1,180 @@
package bradfitz
// Adapted from: https://github.com/bradfitz/go-sql-test
import (
"database/sql"
"fmt"
"math/rand"
"path/filepath"
"sync"
"testing"
_ "github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
)
type Tester interface {
RunTest(*testing.T, func(params))
}
var (
sqlite Tester = sqliteDB{}
)
const TablePrefix = "gosqltest_"
type sqliteDB struct{}
type params struct {
dbType Tester
*testing.T
*sql.DB
}
func (t params) mustExec(sql string, args ...interface{}) sql.Result {
res, err := t.DB.Exec(sql, args...)
if err != nil {
t.Fatalf("Error running %q: %v", sql, err)
}
return res
}
func (sqliteDB) RunTest(t *testing.T, fn func(params)) {
db, err := sql.Open("sqlite3", filepath.Join(t.TempDir(), "foo.db"))
if err != nil {
t.Fatalf("foo.db open fail: %v", err)
}
fn(params{sqlite, t, db})
if err := db.Close(); err != nil {
t.Fatalf("foo.db close fail: %v", err)
}
}
func TestBlobs_SQLite(t *testing.T) { sqlite.RunTest(t, testBlobs) }
func testBlobs(t params) {
var blob = []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
t.mustExec("create table " + TablePrefix + "foo (id integer primary key, bar blob)")
t.mustExec("insert into "+TablePrefix+"foo (id, bar) values(?,?)", 0, blob)
want := fmt.Sprintf("%x", blob)
b := make([]byte, 16)
err := t.QueryRow("select bar from "+TablePrefix+"foo where id = ?", 0).Scan(&b)
got := fmt.Sprintf("%x", b)
if err != nil {
t.Errorf("[]byte scan: %v", err)
} else if got != want {
t.Errorf("for []byte, got %q; want %q", got, want)
}
err = t.QueryRow("select bar from "+TablePrefix+"foo where id = ?", 0).Scan(&got)
want = string(blob)
if err != nil {
t.Errorf("string scan: %v", err)
} else if got != want {
t.Errorf("for string, got %q; want %q", got, want)
}
}
func TestManyQueryRow_SQLite(t *testing.T) { sqlite.RunTest(t, testManyQueryRow) }
func testManyQueryRow(t params) {
if testing.Short() {
t.Skip("skipping in short mode")
}
t.mustExec("create table " + TablePrefix + "foo (id integer primary key, name varchar(50))")
t.mustExec("insert into "+TablePrefix+"foo (id, name) values(?,?)", 1, "bob")
var name string
for i := 0; i < 10000; i++ {
err := t.QueryRow("select name from "+TablePrefix+"foo where id = ?", 1).Scan(&name)
if err != nil || name != "bob" {
t.Fatalf("on query %d: err=%v, name=%q", i, err, name)
}
}
}
func TestTxQuery_SQLite(t *testing.T) { sqlite.RunTest(t, testTxQuery) }
func testTxQuery(t params) {
tx, err := t.Begin()
if err != nil {
t.Fatal(err)
}
defer tx.Rollback()
_, err = t.DB.Exec("create table " + TablePrefix + "foo (id integer primary key, name varchar(50))")
if err != nil {
t.Logf("cannot drop table "+TablePrefix+"foo: %s", err)
}
_, err = tx.Exec("insert into "+TablePrefix+"foo (id, name) values(?,?)", 1, "bob")
if err != nil {
t.Fatal(err)
}
r, err := tx.Query("select name from "+TablePrefix+"foo where id = ?", 1)
if err != nil {
t.Fatal(err)
}
defer r.Close()
if !r.Next() {
if r.Err() != nil {
t.Fatal(err)
}
t.Fatal("expected one rows")
}
var name string
err = r.Scan(&name)
if err != nil {
t.Fatal(err)
}
}
func TestPreparedStmt_SQLite(t *testing.T) { sqlite.RunTest(t, testPreparedStmt) }
func testPreparedStmt(t params) {
if testing.Short() {
t.Skip("skipping in short mode")
}
t.mustExec("CREATE TABLE " + TablePrefix + "t (count INT)")
sel, err := t.Prepare("SELECT count FROM " + TablePrefix + "t ORDER BY count DESC")
if err != nil {
t.Fatalf("prepare 1: %v", err)
}
ins, err := t.Prepare("INSERT INTO " + TablePrefix + "t (count) VALUES (?)")
if err != nil {
t.Fatalf("prepare 2: %v", err)
}
for n := 1; n <= 3; n++ {
if _, err := ins.Exec(n); err != nil {
t.Fatalf("insert(%d) = %v", n, err)
}
}
const nRuns = 10
var wg sync.WaitGroup
for i := 0; i < nRuns; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 10; j++ {
count := 0
if err := sel.QueryRow().Scan(&count); err != nil && err != sql.ErrNoRows {
t.Errorf("Query: %v", err)
return
}
if _, err := ins.Exec(rand.Intn(100)); err != nil {
t.Errorf("Insert: %v", err)
return
}
}
}()
}
wg.Wait()
}

View File

@@ -1,4 +1,4 @@
package compile_empty
package compile
import (
"testing"

View File

@@ -1,4 +1,4 @@
package compile_empty
package compile
import (
"testing"
@@ -6,7 +6,7 @@ import (
"github.com/ncruces/go-sqlite3"
)
func TestCompile_empty(t *testing.T) {
func TestCompile_missing(t *testing.T) {
sqlite3.Path = "sqlite3.wasm"
_, err := sqlite3.Open(":memory:")
if err == nil {

View File

@@ -0,0 +1,14 @@
package compile
import (
"testing"
"github.com/ncruces/go-sqlite3"
)
func TestCompile_nil(t *testing.T) {
_, err := sqlite3.Open(":memory:")
if err == nil {
t.Error("want error")
}
}

229
tests/conn_test.go Normal file
View File

@@ -0,0 +1,229 @@
package tests
import (
"context"
"errors"
"strings"
"testing"
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/embed"
)
func TestConn_Open_dir(t *testing.T) {
t.Parallel()
_, err := sqlite3.Open(".")
if err == nil {
t.Fatal("want error")
}
var serr *sqlite3.Error
if !errors.As(err, &serr) {
t.Fatalf("got %T, want sqlite3.Error", err)
}
if rc := serr.Code(); rc != sqlite3.CANTOPEN {
t.Errorf("got %d, want sqlite3.CANTOPEN", rc)
}
if got := err.Error(); got != `sqlite3: unable to open database file` {
t.Error("got message: ", got)
}
}
func TestConn_Close(t *testing.T) {
var conn *sqlite3.Conn
conn.Close()
}
func TestConn_Close_BUSY(t *testing.T) {
t.Parallel()
db, err := sqlite3.Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
stmt, _, err := db.Prepare(`BEGIN`)
if err != nil {
t.Fatal(err)
}
defer stmt.Close()
err = db.Close()
if err == nil {
t.Fatal("want error")
}
var serr *sqlite3.Error
if !errors.As(err, &serr) {
t.Fatalf("got %T, want sqlite3.Error", err)
}
if rc := serr.Code(); rc != sqlite3.BUSY {
t.Errorf("got %d, want sqlite3.BUSY", rc)
}
var terr interface{ Temporary() bool }
if !errors.As(err, &terr) || !terr.Temporary() {
t.Error("not temporary", err)
}
if got := err.Error(); got != `sqlite3: database is locked: unable to close due to unfinalized statements or unfinished backups` {
t.Error("got message: ", got)
}
}
func TestConn_SetInterrupt(t *testing.T) {
db, err := sqlite3.Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
ctx, cancel := context.WithCancel(context.Background())
db.SetInterrupt(ctx.Done())
// Interrupt doesn't interrupt this.
err = db.Exec(`SELECT 1`)
if err != nil {
t.Fatal(err)
}
db.SetInterrupt(nil)
stmt, _, err := db.Prepare(`
WITH RECURSIVE
fibonacci (curr, next)
AS (
SELECT 0, 1
UNION ALL
SELECT next, curr + next FROM fibonacci
LIMIT 1e6
)
SELECT min(curr) FROM fibonacci
`)
if err != nil {
t.Fatal(err)
}
defer stmt.Close()
cancel()
db.SetInterrupt(ctx.Done())
var serr *sqlite3.Error
// Interrupting works.
err = stmt.Exec()
if err != nil {
if !errors.As(err, &serr) {
t.Fatalf("got %T, want sqlite3.Error", err)
}
if rc := serr.Code(); rc != sqlite3.INTERRUPT {
t.Errorf("got %d, want sqlite3.INTERRUPT", rc)
}
if got := err.Error(); got != `sqlite3: interrupted` {
t.Error("got message: ", got)
}
}
// Interrupting sticks.
err = db.Exec(`SELECT 1`)
if err != nil {
if !errors.As(err, &serr) {
t.Fatalf("got %T, want sqlite3.Error", err)
}
if rc := serr.Code(); rc != sqlite3.INTERRUPT {
t.Errorf("got %d, want sqlite3.INTERRUPT", rc)
}
if got := err.Error(); got != `sqlite3: interrupted` {
t.Error("got message: ", got)
}
}
db.SetInterrupt(nil)
// Interrupting can be cleared.
err = db.Exec(`SELECT 1`)
if err != nil {
t.Fatal(err)
}
}
func TestConn_Prepare_empty(t *testing.T) {
t.Parallel()
db, err := sqlite3.Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
stmt, _, err := db.Prepare(``)
if err != nil {
t.Fatal(err)
}
defer stmt.Close()
if stmt != nil {
t.Error("want nil")
}
}
func TestConn_Prepare_tail(t *testing.T) {
t.Parallel()
db, err := sqlite3.Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
stmt, tail, err := db.Prepare(`SELECT 1; -- HERE`)
if err != nil {
t.Fatal(err)
}
defer stmt.Close()
if !strings.Contains(tail, "-- HERE") {
t.Errorf("got %q", tail)
}
}
func TestConn_Prepare_invalid(t *testing.T) {
t.Parallel()
db, err := sqlite3.Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
var serr *sqlite3.Error
_, _, err = db.Prepare(`SELECT`)
if err == nil {
t.Fatal("want error")
}
if !errors.As(err, &serr) {
t.Fatalf("got %T, want sqlite3.Error", err)
}
if rc := serr.Code(); rc != sqlite3.ERROR {
t.Errorf("got %d, want sqlite3.ERROR", rc)
}
if got := err.Error(); got != `sqlite3: SQL logic error: incomplete input` {
t.Error("got message: ", got)
}
_, _, err = db.Prepare(`SELECT * FRM sqlite_schema`)
if err == nil {
t.Fatal("want error")
}
if !errors.As(err, &serr) {
t.Fatalf("got %T, want sqlite3.ERROR", err)
}
if rc := serr.Code(); rc != sqlite3.ERROR {
t.Errorf("got %d, want sqlite3.ERROR", rc)
}
if got := serr.SQL(); got != `FRM sqlite_schema` {
t.Error("got SQL: ", got)
}
if got := serr.Error(); got != `sqlite3: SQL logic error: near "FRM": syntax error` {
t.Error("got message: ", got)
}
}

View File

@@ -1,7 +1,6 @@
package tests
import (
"os"
"path/filepath"
"testing"
@@ -14,16 +13,12 @@ func TestDB_memory(t *testing.T) {
}
func TestDB_file(t *testing.T) {
dir, err := os.MkdirTemp("", "sqlite3-")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
testDB(t, filepath.Join(dir, "test.db"))
testDB(t, filepath.Join(t.TempDir(), "test.db"))
}
func testDB(t *testing.T, name string) {
t.Parallel()
db, err := sqlite3.Open(name)
if err != nil {
t.Fatal(err)
@@ -39,28 +34,37 @@ func testDB(t *testing.T, name string) {
if err != nil {
t.Fatal(err)
}
changes := db.Changes()
if changes != 3 {
t.Errorf("got %d want 3", changes)
}
stmt, _, err := db.Prepare(`SELECT id, name FROM users`)
if err != nil {
t.Fatal(err)
}
defer stmt.Close()
row := 0
ids := []int{0, 1, 2}
names := []string{"go", "zig", "whatever"}
for ; stmt.Step(); row++ {
if ids[row] != stmt.ColumnInt(0) {
t.Errorf("got %d, want %d", stmt.ColumnInt(0), ids[row])
id := stmt.ColumnInt(0)
name := stmt.ColumnText(1)
if id != ids[row] {
t.Errorf("got %d, want %d", id, ids[row])
}
if names[row] != stmt.ColumnText(1) {
t.Errorf("got %q, want %q", stmt.ColumnText(1), names[row])
if name != names[row] {
t.Errorf("got %q, want %q", name, names[row])
}
}
if err := stmt.Err(); err != nil {
t.Fatal(err)
}
if row != 3 {
t.Errorf("got %d rows, want %d", row, len(ids))
t.Errorf("got %d, want %d", row, len(ids))
}
if err := stmt.Err(); err != nil {
t.Fatal(err)
}
err = stmt.Close()

View File

@@ -1,26 +0,0 @@
package tests
import (
"errors"
"testing"
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/embed"
)
func TestDir(t *testing.T) {
_, err := sqlite3.Open(".")
if err == nil {
t.Fatal("want error")
}
var serr *sqlite3.Error
if !errors.As(err, &serr) {
t.Fatalf("got %T, want sqlite3.Error", err)
}
if rc := serr.Code(); rc != sqlite3.CANTOPEN {
t.Errorf("got %d, want sqlite3.CANTOPEN", rc)
}
if got := err.Error(); got != `sqlite3: unable to open database file` {
t.Error("got message: ", got)
}
}

103
tests/driver_test.go Normal file
View File

@@ -0,0 +1,103 @@
package tests
import (
"context"
"database/sql"
"testing"
_ "github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
)
func TestDriver(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
conn, err := db.Conn(ctx)
if err != nil {
t.Fatal(err)
}
defer conn.Close()
_, err = conn.ExecContext(ctx,
`CREATE TABLE IF NOT EXISTS users (id INT, name VARCHAR(10))`)
if err != nil {
t.Fatal(err)
}
res, err := conn.ExecContext(ctx,
`INSERT INTO users(id, name) VALUES(0, 'go'), (1, 'zig'), (2, 'whatever')`)
if err != nil {
t.Fatal(err)
}
changes, err := res.RowsAffected()
if err != nil {
t.Fatal(err)
}
if changes != 3 {
t.Errorf("got %d want 3", changes)
}
stmt, err := conn.PrepareContext(context.Background(),
`SELECT id, name FROM users`)
if err != nil {
t.Fatal(err)
}
defer stmt.Close()
rows, err := stmt.Query()
if err != nil {
t.Fatal(err)
}
defer rows.Close()
row := 0
ids := []int{0, 1, 2}
names := []string{"go", "zig", "whatever"}
for ; rows.Next(); row++ {
var id int
var name string
err := rows.Scan(&id, &name)
if err != nil {
t.Fatal(err)
}
if id != ids[row] {
t.Errorf("got %d, want %d", id, ids[row])
}
if name != names[row] {
t.Errorf("got %q, want %q", name, names[row])
}
}
if row != 3 {
t.Errorf("got %d, want %d", row, len(ids))
}
err = rows.Close()
if err != nil {
t.Fatal(err)
}
err = stmt.Close()
if err != nil {
t.Fatal(err)
}
err = conn.Close()
if err != nil {
t.Fatal(err)
}
err = db.Close()
if err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,191 @@
package tests
import (
"errors"
"io"
"os"
"os/exec"
"path/filepath"
"testing"
"golang.org/x/sync/errgroup"
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/embed"
)
func TestParallel(t *testing.T) {
name := filepath.Join(t.TempDir(), "test.db")
testParallel(t, name, 100)
testIntegrity(t, name)
}
func TestMultiProcess(t *testing.T) {
if testing.Short() {
t.Skip()
}
name := filepath.Join(t.TempDir(), "test.db")
t.Setenv("TestMultiProcess_dbname", name)
cmd := exec.Command("go", "test", "-v", "-run", "TestChildProcess")
out, err := cmd.StdoutPipe()
if err != nil {
t.Fatal(err)
}
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
var buf [3]byte
// Wait for child to start.
if _, err := io.ReadFull(out, buf[:]); err != nil || string(buf[:]) != "===" {
t.Fatal(err)
}
testParallel(t, name, 1000)
if err := cmd.Wait(); err != nil {
t.Error(err)
var eerr *exec.ExitError
if errors.As(err, &eerr) {
t.Error(eerr.Stderr)
}
}
testIntegrity(t, name)
}
func TestChildProcess(t *testing.T) {
name := os.Getenv("TestMultiProcess_dbname")
if name == "" || testing.Short() {
t.SkipNow()
}
testParallel(t, name, 1000)
}
func testParallel(t *testing.T, name string, n int) {
writer := func() error {
db, err := sqlite3.Open(name)
if err != nil {
return err
}
defer db.Close()
err = db.Exec(`
PRAGMA locking_mode = NORMAL;
PRAGMA busy_timeout = 10000;
`)
if err != nil {
return err
}
err = db.Exec(`CREATE TABLE IF NOT EXISTS users (id INT, name VARCHAR(10))`)
if err != nil {
return err
}
err = db.Exec(`INSERT INTO users(id, name) VALUES(0, 'go'), (1, 'zig'), (2, 'whatever')`)
if err != nil {
return err
}
return db.Close()
}
reader := func() error {
db, err := sqlite3.Open(name)
if err != nil {
return err
}
defer db.Close()
err = db.Exec(`
PRAGMA locking_mode = NORMAL;
PRAGMA busy_timeout = 10000;
`)
if err != nil {
return err
}
stmt, _, err := db.Prepare(`SELECT id, name FROM users`)
if err != nil {
return err
}
defer stmt.Close()
row := 0
for stmt.Step() {
row++
}
if err := stmt.Err(); err != nil {
return err
}
if row%3 != 0 {
t.Errorf("got %d rows, want multiple of 3", row)
}
err = stmt.Close()
if err != nil {
return err
}
return db.Close()
}
err := writer()
if err != nil {
t.Fatal(err)
}
var group errgroup.Group
group.SetLimit(4)
for i := 0; i < n; i++ {
if i&7 != 7 {
group.Go(reader)
} else {
group.Go(writer)
}
}
err = group.Wait()
if err != nil {
t.Fatal(err)
}
}
func testIntegrity(t *testing.T, name string) {
db, err := sqlite3.Open(name)
if err != nil {
t.Fatal(err)
}
defer db.Close()
test := `PRAGMA integrity_check`
if testing.Short() {
test = `PRAGMA quick_check`
}
stmt, _, err := db.Prepare(test)
if err != nil {
t.Fatal(err)
}
defer stmt.Close()
for stmt.Step() {
if row := stmt.ColumnText(0); row != "ok" {
t.Error(row)
}
}
if err := stmt.Err(); err != nil {
t.Fatal(err)
}
err = stmt.Close()
if err != nil {
t.Fatal(err)
}
err = db.Close()
if err != nil {
t.Fatal(err)
}
}

View File

@@ -1,111 +0,0 @@
package tests
import (
"os"
"path/filepath"
"runtime"
"testing"
"golang.org/x/sync/errgroup"
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/embed"
)
func TestParallel(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip()
}
dir, err := os.MkdirTemp("", "sqlite3-")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
writer := func() error {
db, err := sqlite3.Open(filepath.Join(dir, "test.db"))
if err != nil {
return err
}
defer db.Close()
err = db.Exec(`
PRAGMA locking_mode = NORMAL;
PRAGMA busy_timeout = 1000;
`)
if err != nil {
return err
}
err = db.Exec(`CREATE TABLE IF NOT EXISTS users (id INT, name VARCHAR(10))`)
if err != nil {
return err
}
err = db.Exec(`INSERT INTO users(id, name) VALUES(0, 'go'), (1, 'zig'), (2, 'whatever')`)
if err != nil {
return err
}
return db.Close()
}
reader := func() error {
db, err := sqlite3.Open(filepath.Join(dir, "test.db"))
if err != nil {
return err
}
defer db.Close()
err = db.Exec(`
PRAGMA locking_mode = NORMAL;
PRAGMA busy_timeout = 1000;
`)
if err != nil {
return err
}
stmt, _, err := db.Prepare(`SELECT id, name FROM users`)
if err != nil {
return err
}
row := 0
for stmt.Step() {
row++
}
if err := stmt.Err(); err != nil {
return err
}
if row%3 != 0 {
t.Errorf("got %d rows, want multiple of 3", row)
}
err = stmt.Close()
if err != nil {
return err
}
return db.Close()
}
err = writer()
if err != nil {
t.Fatal(err)
}
var group errgroup.Group
group.SetLimit(4)
for i := 0; i < 32; i++ {
if i&7 != 7 {
group.Go(reader)
} else {
group.Go(writer)
}
}
err = group.Wait()
if err != nil {
t.Fatal(err)
}
}

View File

@@ -1,12 +1,17 @@
package sqlite3
package tests
import (
"math"
"testing"
"time"
"github.com/ncruces/go-sqlite3"
)
func TestStmt(t *testing.T) {
db, err := Open(":memory:")
t.Parallel()
db, err := sqlite3.Open(":memory:")
if err != nil {
t.Fatal(err)
}
@@ -23,103 +28,84 @@ func TestStmt(t *testing.T) {
}
defer stmt.Close()
err = stmt.BindBool(1, false)
if err != nil {
if got := stmt.BindCount(); got != 1 {
t.Errorf("got %d, want 1", got)
}
if err := stmt.BindBool(1, false); err != nil {
t.Fatal(err)
}
if err := stmt.Exec(); err != nil {
t.Fatal(err)
}
err = stmt.Exec()
if err != nil {
if err := stmt.BindBool(1, true); err != nil {
t.Fatal(err)
}
if err := stmt.Exec(); err != nil {
t.Fatal(err)
}
err = stmt.ClearBindings()
if err != nil {
if err := stmt.BindInt(1, 2); err != nil {
t.Fatal(err)
}
if err = stmt.Exec(); err != nil {
t.Fatal(err)
}
err = stmt.Exec()
if err != nil {
if err := stmt.BindFloat(1, math.Pi); err != nil {
t.Fatal(err)
}
if err := stmt.Exec(); err != nil {
t.Fatal(err)
}
err = stmt.BindBool(1, true)
if err != nil {
if err := stmt.BindNull(1); err != nil {
t.Fatal(err)
}
if err := stmt.Exec(); err != nil {
t.Fatal(err)
}
err = stmt.Exec()
if err != nil {
if err := stmt.BindText(1, ""); err != nil {
t.Fatal(err)
}
if err := stmt.Exec(); err != nil {
t.Fatal(err)
}
err = stmt.BindInt(1, 2)
if err != nil {
if err := stmt.BindText(1, "text"); err != nil {
t.Fatal(err)
}
if err := stmt.Exec(); err != nil {
t.Fatal(err)
}
err = stmt.Exec()
if err != nil {
if err := stmt.BindBlob(1, []byte("blob")); err != nil {
t.Fatal(err)
}
if err := stmt.Exec(); err != nil {
t.Fatal(err)
}
err = stmt.BindFloat(1, math.Pi)
if err != nil {
if err := stmt.BindBlob(1, nil); err != nil {
t.Fatal(err)
}
if err := stmt.Exec(); err != nil {
t.Fatal(err)
}
err = stmt.Exec()
if err != nil {
if err := stmt.BindZeroBlob(1, 4); err != nil {
t.Fatal(err)
}
if err := stmt.Exec(); err != nil {
t.Fatal(err)
}
err = stmt.BindNull(1)
if err != nil {
if err := stmt.ClearBindings(); err != nil {
t.Fatal(err)
}
err = stmt.Exec()
if err != nil {
t.Fatal(err)
}
err = stmt.BindText(1, "")
if err != nil {
t.Fatal(err)
}
err = stmt.Exec()
if err != nil {
t.Fatal(err)
}
err = stmt.BindText(1, "text")
if err != nil {
t.Fatal(err)
}
err = stmt.Exec()
if err != nil {
t.Fatal(err)
}
err = stmt.BindBlob(1, []byte("blob"))
if err != nil {
t.Fatal(err)
}
err = stmt.Exec()
if err != nil {
t.Fatal(err)
}
err = stmt.BindBlob(1, nil)
if err != nil {
t.Fatal(err)
}
err = stmt.Exec()
if err != nil {
if err := stmt.Exec(); err != nil {
t.Fatal(err)
}
@@ -128,14 +114,15 @@ func TestStmt(t *testing.T) {
t.Fatal(err)
}
// The table should have: 0, NULL, 1, 2, π, NULL, "", "text", `blob`, NULL
// The table should have: 0, 1, 2, π, NULL, "", "text", "blob", NULL, "\0\0\0\0", NULL
stmt, _, err = db.Prepare(`SELECT col FROM test`)
if err != nil {
t.Fatal(err)
}
defer stmt.Close()
if stmt.Step() {
if got := stmt.ColumnType(0); got != INTEGER {
if got := stmt.ColumnType(0); got != sqlite3.INTEGER {
t.Errorf("got %v, want INTEGER", got)
}
if got := stmt.ColumnBool(0); got != false {
@@ -156,28 +143,7 @@ func TestStmt(t *testing.T) {
}
if stmt.Step() {
if got := stmt.ColumnType(0); got != NULL {
t.Errorf("got %v, want NULL", got)
}
if got := stmt.ColumnBool(0); got != false {
t.Errorf("got %v, want false", got)
}
if got := stmt.ColumnInt(0); got != 0 {
t.Errorf("got %v, want zero", got)
}
if got := stmt.ColumnFloat(0); got != 0 {
t.Errorf("got %v, want zero", got)
}
if got := stmt.ColumnText(0); got != "" {
t.Errorf("got %q, want empty", got)
}
if got := stmt.ColumnBlob(0, nil); got != nil {
t.Errorf("got %q, want nil", got)
}
}
if stmt.Step() {
if got := stmt.ColumnType(0); got != INTEGER {
if got := stmt.ColumnType(0); got != sqlite3.INTEGER {
t.Errorf("got %v, want INTEGER", got)
}
if got := stmt.ColumnBool(0); got != true {
@@ -198,7 +164,7 @@ func TestStmt(t *testing.T) {
}
if stmt.Step() {
if got := stmt.ColumnType(0); got != INTEGER {
if got := stmt.ColumnType(0); got != sqlite3.INTEGER {
t.Errorf("got %v, want INTEGER", got)
}
if got := stmt.ColumnBool(0); got != true {
@@ -219,7 +185,7 @@ func TestStmt(t *testing.T) {
}
if stmt.Step() {
if got := stmt.ColumnType(0); got != FLOAT {
if got := stmt.ColumnType(0); got != sqlite3.FLOAT {
t.Errorf("got %v, want FLOAT", got)
}
if got := stmt.ColumnBool(0); got != true {
@@ -240,7 +206,7 @@ func TestStmt(t *testing.T) {
}
if stmt.Step() {
if got := stmt.ColumnType(0); got != NULL {
if got := stmt.ColumnType(0); got != sqlite3.NULL {
t.Errorf("got %v, want NULL", got)
}
if got := stmt.ColumnBool(0); got != false {
@@ -261,7 +227,7 @@ func TestStmt(t *testing.T) {
}
if stmt.Step() {
if got := stmt.ColumnType(0); got != TEXT {
if got := stmt.ColumnType(0); got != sqlite3.TEXT {
t.Errorf("got %v, want TEXT", got)
}
if got := stmt.ColumnBool(0); got != false {
@@ -282,7 +248,7 @@ func TestStmt(t *testing.T) {
}
if stmt.Step() {
if got := stmt.ColumnType(0); got != TEXT {
if got := stmt.ColumnType(0); got != sqlite3.TEXT {
t.Errorf("got %v, want TEXT", got)
}
if got := stmt.ColumnBool(0); got != false {
@@ -303,7 +269,7 @@ func TestStmt(t *testing.T) {
}
if stmt.Step() {
if got := stmt.ColumnType(0); got != BLOB {
if got := stmt.ColumnType(0); got != sqlite3.BLOB {
t.Errorf("got %v, want BLOB", got)
}
if got := stmt.ColumnBool(0); got != false {
@@ -324,7 +290,7 @@ func TestStmt(t *testing.T) {
}
if stmt.Step() {
if got := stmt.ColumnType(0); got != NULL {
if got := stmt.ColumnType(0); got != sqlite3.NULL {
t.Errorf("got %v, want NULL", got)
}
if got := stmt.ColumnBool(0); got != false {
@@ -344,18 +310,154 @@ func TestStmt(t *testing.T) {
}
}
err = stmt.Close()
if err != nil {
if stmt.Step() {
if got := stmt.ColumnType(0); got != sqlite3.BLOB {
t.Errorf("got %v, want BLOB", got)
}
if got := stmt.ColumnBool(0); got != false {
t.Errorf("got %v, want false", got)
}
if got := stmt.ColumnInt(0); got != 0 {
t.Errorf("got %v, want zero", got)
}
if got := stmt.ColumnFloat(0); got != 0 {
t.Errorf("got %v, want zero", got)
}
if got := stmt.ColumnText(0); got != "\x00\x00\x00\x00" {
t.Errorf(`got %q, want "\x00\x00\x00\x00"`, got)
}
if got := stmt.ColumnBlob(0, nil); string(got) != "\x00\x00\x00\x00" {
t.Errorf(`got %q, want "\x00\x00\x00\x00"`, got)
}
}
if stmt.Step() {
if got := stmt.ColumnType(0); got != sqlite3.NULL {
t.Errorf("got %v, want NULL", got)
}
if got := stmt.ColumnBool(0); got != false {
t.Errorf("got %v, want false", got)
}
if got := stmt.ColumnInt(0); got != 0 {
t.Errorf("got %v, want zero", got)
}
if got := stmt.ColumnFloat(0); got != 0 {
t.Errorf("got %v, want zero", got)
}
if got := stmt.ColumnText(0); got != "" {
t.Errorf("got %q, want empty", got)
}
if got := stmt.ColumnBlob(0, nil); got != nil {
t.Errorf("got %q, want nil", got)
}
}
if err := stmt.Close(); err != nil {
t.Fatal(err)
}
err = db.Close()
if err != nil {
if err := db.Close(); err != nil {
t.Fatal(err)
}
}
func TestStmt_Close(t *testing.T) {
var stmt *Stmt
var stmt *sqlite3.Stmt
stmt.Close()
}
func TestStmt_BindName(t *testing.T) {
t.Parallel()
db, err := sqlite3.Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
want := []string{"", "", "", "", "?5", ":AAA", "@AAA", "$AAA"}
stmt, _, err := db.Prepare(`SELECT ?, ?5, :AAA, @AAA, $AAA`)
if err != nil {
t.Fatal(err)
}
defer stmt.Close()
if got := stmt.BindCount(); got != len(want) {
t.Errorf("got %d, want %d", got, len(want))
}
for i, name := range want {
id := i + 1
if got := stmt.BindName(id); got != name {
t.Errorf("got %q, want %q", got, name)
}
if name == "" {
id = 0
}
if got := stmt.BindIndex(name); got != id {
t.Errorf("got %d, want %d", got, id)
}
}
}
func TestStmt_Time(t *testing.T) {
t.Parallel()
db, err := sqlite3.Open(":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
stmt, _, err := db.Prepare(`SELECT ?, ?, ?, datetime(), unixepoch(), julianday(), NULL, 'abc'`)
if err != nil {
t.Fatal(err)
}
defer stmt.Close()
reference := time.Date(2013, 10, 7, 4, 23, 19, 120_000_000, time.FixedZone("", -4*3600))
err = stmt.BindTime(1, reference, sqlite3.TimeFormat4)
if err != nil {
t.Fatal(err)
}
err = stmt.BindTime(2, reference, sqlite3.TimeFormatUnixMilli)
if err != nil {
t.Fatal(err)
}
err = stmt.BindTime(3, reference, sqlite3.TimeFormatJulianDay)
if err != nil {
t.Fatal(err)
}
if now := time.Now(); stmt.Step() {
if got := stmt.ColumnTime(0, sqlite3.TimeFormatAuto); !reference.Equal(got) {
t.Errorf("got %v, want %v", got, reference)
}
if got := stmt.ColumnTime(1, sqlite3.TimeFormatAuto); !reference.Equal(got) {
t.Errorf("got %v, want %v", got, reference)
}
if got := stmt.ColumnTime(2, sqlite3.TimeFormatAuto); reference.Sub(got) > time.Millisecond {
t.Errorf("got %v, want %v", got, reference)
}
if got := stmt.ColumnTime(3, sqlite3.TimeFormatAuto); now.Sub(got) > time.Second {
t.Errorf("got %v, want %v", got, now)
}
if got := stmt.ColumnTime(4, sqlite3.TimeFormatAuto); now.Sub(got) > time.Second {
t.Errorf("got %v, want %v", got, now)
}
if got := stmt.ColumnTime(5, sqlite3.TimeFormatAuto); now.Sub(got) > time.Millisecond {
t.Errorf("got %v, want %v", got, now)
}
if got := stmt.ColumnTime(6, sqlite3.TimeFormatAuto); got != (time.Time{}) {
t.Errorf("got %v, want zero", got)
}
if got := stmt.ColumnTime(7, sqlite3.TimeFormatAuto); got != (time.Time{}) {
t.Errorf("got %v, want zero", got)
}
if stmt.Err() == nil {
t.Errorf("want error")
}
}
}

317
time.go Normal file
View File

@@ -0,0 +1,317 @@
package sqlite3
import (
"math"
"strconv"
"strings"
"time"
"github.com/ncruces/julianday"
)
// TimeFormat specifies how to encode/decode time values.
//
// https://www.sqlite.org/lang_datefunc.html
type TimeFormat string
// TimeFormats recognized by SQLite to encode/decode time values.
//
// https://www.sqlite.org/lang_datefunc.html
const (
TimeFormatDefault TimeFormat = "" // time.RFC3339Nano
// Text formats
TimeFormat1 TimeFormat = "2006-01-02"
TimeFormat2 TimeFormat = "2006-01-02 15:04"
TimeFormat3 TimeFormat = "2006-01-02 15:04:05"
TimeFormat4 TimeFormat = "2006-01-02 15:04:05.000"
TimeFormat5 TimeFormat = "2006-01-02T15:04"
TimeFormat6 TimeFormat = "2006-01-02T15:04:05"
TimeFormat7 TimeFormat = "2006-01-02T15:04:05.000"
TimeFormat8 TimeFormat = "15:04"
TimeFormat9 TimeFormat = "15:04:05"
TimeFormat10 TimeFormat = "15:04:05.000"
TimeFormat2TZ = TimeFormat2 + "Z07:00"
TimeFormat3TZ = TimeFormat3 + "Z07:00"
TimeFormat4TZ = TimeFormat4 + "Z07:00"
TimeFormat5TZ = TimeFormat5 + "Z07:00"
TimeFormat6TZ = TimeFormat6 + "Z07:00"
TimeFormat7TZ = TimeFormat7 + "Z07:00"
TimeFormat8TZ = TimeFormat8 + "Z07:00"
TimeFormat9TZ = TimeFormat9 + "Z07:00"
TimeFormat10TZ = TimeFormat10 + "Z07:00"
// Numeric formats
TimeFormatJulianDay TimeFormat = "julianday"
TimeFormatUnix TimeFormat = "unixepoch"
TimeFormatUnixFrac TimeFormat = "unixepoch_frac"
TimeFormatUnixMilli TimeFormat = "unixepoch_milli" // not an SQLite format
TimeFormatUnixMicro TimeFormat = "unixepoch_micro" // not an SQLite format
TimeFormatUnixNano TimeFormat = "unixepoch_nano" // not an SQLite format
// Auto
TimeFormatAuto TimeFormat = "auto"
)
// Encode encodes a time value using this format.
//
// [TimeFormatDefault] and [TimeFormatAuto] encode using [time.RFC3339Nano],
// with nanosecond accuracy, and preserving timezone.
//
// Formats [TimeFormat1] through [TimeFormat10]
// convert time values to UTC before encoding.
//
// Returns a string for the text formats,
// a float64 for [TimeFormatJulianDay] and [TimeFormatUnixFrac],
// or an int64 for the other numeric formats.
//
// https://www.sqlite.org/lang_datefunc.html
func (f TimeFormat) Encode(t time.Time) any {
switch f {
// Numeric formats
case TimeFormatJulianDay:
return julianday.Float(t)
case TimeFormatUnix:
return t.Unix()
case TimeFormatUnixFrac:
return float64(t.Unix()) + float64(t.Nanosecond())*1e-9
case TimeFormatUnixMilli:
return t.UnixMilli()
case TimeFormatUnixMicro:
return t.UnixMicro()
case TimeFormatUnixNano:
return t.UnixNano()
// Special formats
case TimeFormatDefault, TimeFormatAuto:
f = time.RFC3339Nano
// SQLite assumes UTC if unspecified.
case
TimeFormat1, TimeFormat2,
TimeFormat3, TimeFormat4,
TimeFormat5, TimeFormat6,
TimeFormat7, TimeFormat8,
TimeFormat9, TimeFormat10:
t = t.UTC()
}
return t.Format(string(f))
}
// Decode decodes a time value using this format.
//
// The time value can be a string, an int64, or a float64.
//
// Formats [TimeFormat8] through [TimeFormat10]
// assume a date of 2000-01-01.
//
// The timezone indicator and fractional seconds are always optional
// for formats [TimeFormat2] through [TimeFormat10].
//
// [TimeFormatAuto] implements (and extends) the SQLite auto modifier.
// The julian day number is safe to use for historical dates,
// from 4712BC through 9999AD.
// Unix timestamps (expressed in seconds, milliseconds, microseconds, or nanoseconds),
// are safe to use for current events, from 1980 through at least 2260.
//
// https://www.sqlite.org/lang_datefunc.html
func (f TimeFormat) Decode(v any) (time.Time, error) {
switch f {
// Numeric formats
case TimeFormatJulianDay:
switch v := v.(type) {
case string:
return julianday.Parse(v)
case float64:
return julianday.FloatTime(v), nil
case int64:
return julianday.Time(v, 0), nil
default:
return time.Time{}, timeErr
}
case TimeFormatUnix, TimeFormatUnixFrac:
if s, ok := v.(string); ok {
f, err := strconv.ParseFloat(s, 64)
if err != nil {
return time.Time{}, err
}
v = f
}
switch v := v.(type) {
case float64:
sec, frac := math.Modf(v)
nsec := math.Floor(frac * 1e9)
return time.Unix(int64(sec), int64(nsec)), nil
case int64:
return time.Unix(v, 0), nil
default:
return time.Time{}, timeErr
}
case TimeFormatUnixMilli:
if s, ok := v.(string); ok {
i, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return time.Time{}, err
}
v = i
}
switch v := v.(type) {
case float64:
return time.UnixMilli(int64(math.Floor(v))), nil
case int64:
return time.UnixMilli(int64(v)), nil
default:
return time.Time{}, timeErr
}
case TimeFormatUnixMicro:
if s, ok := v.(string); ok {
i, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return time.Time{}, err
}
v = i
}
switch v := v.(type) {
case float64:
return time.UnixMicro(int64(math.Floor(v))), nil
case int64:
return time.UnixMicro(int64(v)), nil
default:
return time.Time{}, timeErr
}
case TimeFormatUnixNano:
if s, ok := v.(string); ok {
i, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return time.Time{}, timeErr
}
v = i
}
switch v := v.(type) {
case float64:
return time.Unix(0, int64(math.Floor(v))), nil
case int64:
return time.Unix(0, int64(v)), nil
default:
return time.Time{}, timeErr
}
// Special formats
case TimeFormatAuto:
switch s := v.(type) {
case string:
i, err := strconv.ParseInt(s, 10, 64)
if err == nil {
v = i
break
}
f, err := strconv.ParseFloat(s, 64)
if err == nil {
v = f
break
}
dates := []TimeFormat{
TimeFormat6TZ, TimeFormat6, TimeFormat3TZ, TimeFormat3,
TimeFormat5TZ, TimeFormat5, TimeFormat2TZ, TimeFormat2,
TimeFormat1,
}
for _, f := range dates {
t, err := time.Parse(string(f), s)
if err == nil {
return t, nil
}
}
times := []TimeFormat{
TimeFormat9TZ, TimeFormat9, TimeFormat8TZ, TimeFormat8,
}
for _, f := range times {
t, err := time.Parse(string(f), s)
if err == nil {
return t.AddDate(2000, 0, 0), nil
}
}
}
switch v := v.(type) {
case float64:
if 0 <= v && v < 5373484.5 {
return TimeFormatJulianDay.Decode(v)
}
if v < 253402300800 {
return TimeFormatUnixFrac.Decode(v)
}
if v < 253402300800_000 {
return TimeFormatUnixMilli.Decode(v)
}
if v < 253402300800_000000 {
return TimeFormatUnixMicro.Decode(v)
}
return TimeFormatUnixNano.Decode(v)
case int64:
if 0 <= v && v < 5373485 {
return TimeFormatJulianDay.Decode(v)
}
if v < 253402300800 {
return TimeFormatUnixFrac.Decode(v)
}
if v < 253402300800_000 {
return TimeFormatUnixMilli.Decode(v)
}
if v < 253402300800_000000 {
return TimeFormatUnixMicro.Decode(v)
}
return TimeFormatUnixNano.Decode(v)
default:
return time.Time{}, timeErr
}
case
TimeFormat2, TimeFormat2TZ,
TimeFormat3, TimeFormat3TZ,
TimeFormat4, TimeFormat4TZ,
TimeFormat5, TimeFormat5TZ,
TimeFormat6, TimeFormat6TZ,
TimeFormat7, TimeFormat7TZ:
s, ok := v.(string)
if !ok {
return time.Time{}, timeErr
}
return f.parseRelaxed(s)
case
TimeFormat8, TimeFormat8TZ,
TimeFormat9, TimeFormat9TZ,
TimeFormat10, TimeFormat10TZ:
s, ok := v.(string)
if !ok {
return time.Time{}, timeErr
}
t, err := f.parseRelaxed(s)
return t.AddDate(2000, 0, 0), err
default:
s, ok := v.(string)
if !ok {
return time.Time{}, timeErr
}
if f == "" {
f = time.RFC3339Nano
}
return time.Parse(string(f), s)
}
}
func (f TimeFormat) parseRelaxed(s string) (time.Time, error) {
fs := string(f)
fs = strings.TrimSuffix(fs, "Z07:00")
fs = strings.TrimSuffix(fs, ".000")
t, err := time.Parse(fs+"Z07:00", s)
if err != nil {
return time.Parse(fs, s)
}
return t, nil
}

118
time_test.go Normal file
View File

@@ -0,0 +1,118 @@
package sqlite3
import (
"reflect"
"testing"
"time"
)
func TestTimeFormat_Encode(t *testing.T) {
t.Parallel()
reference := time.Date(2013, 10, 7, 4, 23, 19, 120_000_000, time.FixedZone("", -4*3600))
tests := []struct {
fmt TimeFormat
time time.Time
want any
}{
{TimeFormatDefault, reference, "2013-10-07T04:23:19.12-04:00"},
{TimeFormatJulianDay, reference, 2456572.849526851851852},
{TimeFormatUnix, reference, int64(1381134199)},
{TimeFormatUnixFrac, reference, 1381134199.120},
{TimeFormatUnixMilli, reference, int64(1381134199_120)},
{TimeFormatUnixMicro, reference, int64(1381134199_120000)},
{TimeFormatUnixNano, reference, int64(1381134199_120000000)},
{TimeFormat7, reference, "2013-10-07T08:23:19.120"},
}
for _, tt := range tests {
t.Run("", func(t *testing.T) {
if got := tt.fmt.Encode(tt.time); !reflect.DeepEqual(got, tt.want) {
t.Errorf("%q.Encode(%v) = %v, want %v", tt.fmt, tt.time, got, tt.want)
}
})
}
}
func TestTimeFormat_Decode(t *testing.T) {
t.Parallel()
reference := time.Date(2013, 10, 7, 4, 23, 19, 120_000_000, time.FixedZone("", -4*3600))
reftime := time.Date(2000, 1, 1, 4, 23, 19, 120_000_000, time.FixedZone("", -4*3600))
tests := []struct {
fmt TimeFormat
val any
want time.Time
wantDelta time.Duration
wantErr bool
}{
{TimeFormatJulianDay, "2456572.849526851851852", reference, 0, false},
{TimeFormatJulianDay, 2456572.849526851851852, reference, time.Millisecond, false},
{TimeFormatJulianDay, int64(2456572), reference, 24 * time.Hour, false},
{TimeFormatJulianDay, false, time.Time{}, 0, true},
{TimeFormatUnix, "1381134199.120", reference, time.Microsecond, false},
{TimeFormatUnix, 1381134199.120, reference, time.Microsecond, false},
{TimeFormatUnix, int64(1381134199), reference, time.Second, false},
{TimeFormatUnix, "abc", time.Time{}, 0, true},
{TimeFormatUnix, false, time.Time{}, 0, true},
{TimeFormatUnixMilli, "1381134199120", reference, 0, false},
{TimeFormatUnixMilli, 1381134199.120e3, reference, 0, false},
{TimeFormatUnixMilli, int64(1381134199_120), reference, 0, false},
{TimeFormatUnixMilli, "abc", time.Time{}, 0, true},
{TimeFormatUnixMilli, false, time.Time{}, 0, true},
{TimeFormatUnixMicro, "1381134199120000", reference, 0, false},
{TimeFormatUnixMicro, 1381134199.120e6, reference, 0, false},
{TimeFormatUnixMicro, int64(1381134199_120000), reference, 0, false},
{TimeFormatUnixMicro, "abc", time.Time{}, 0, true},
{TimeFormatUnixMicro, false, time.Time{}, 0, true},
{TimeFormatUnixNano, "1381134199120000000", reference, 0, false},
{TimeFormatUnixNano, 1381134199.120e9, reference, 0, false},
{TimeFormatUnixNano, int64(1381134199_120000000), reference, 0, false},
{TimeFormatUnixNano, "abc", time.Time{}, 0, true},
{TimeFormatUnixNano, false, time.Time{}, 0, true},
{TimeFormatAuto, "2456572.849526851851852", reference, time.Millisecond, false},
{TimeFormatAuto, "2456572", reference, 24 * time.Hour, false},
{TimeFormatAuto, "1381134199.120", reference, time.Microsecond, false},
{TimeFormatAuto, "1381134199.120e3", reference, time.Microsecond, false},
{TimeFormatAuto, "1381134199.120e6", reference, time.Microsecond, false},
{TimeFormatAuto, "1381134199.120e9", reference, time.Microsecond, false},
{TimeFormatAuto, "1381134199", reference, time.Second, false},
{TimeFormatAuto, "1381134199120", reference, 0, false},
{TimeFormatAuto, "1381134199120000", reference, 0, false},
{TimeFormatAuto, "1381134199120000000", reference, 0, false},
{TimeFormatAuto, "2013-10-07 04:23:19.12-04:00", reference, 0, false},
{TimeFormatAuto, "04:23:19.12-04:00", reftime, 0, false},
{TimeFormatAuto, "abc", time.Time{}, 0, true},
{TimeFormatAuto, false, time.Time{}, 0, true},
{TimeFormat3, "2013-10-07 04:23:19.12-04:00", reference, 0, false},
{TimeFormat3, "2013-10-07 08:23:19.12", reference, 0, false},
{TimeFormat9, "04:23:19.12-04:00", reftime, 0, false},
{TimeFormat9, "08:23:19.12", reftime, 0, false},
{TimeFormat3, false, time.Time{}, 0, true},
{TimeFormat9, false, time.Time{}, 0, true},
{TimeFormatDefault, "2013-10-07T04:23:19.12-04:00", reference, 0, false},
{TimeFormatDefault, "2013-10-07T08:23:19.12Z", reference, 0, false},
{TimeFormatDefault, false, time.Time{}, 0, true},
}
for _, tt := range tests {
t.Run("", func(t *testing.T) {
got, err := tt.fmt.Decode(tt.val)
if (err != nil) != tt.wantErr {
t.Errorf("%q.Decode(%v) error = %v, wantErr %v", tt.fmt, tt.val, err, tt.wantErr)
return
}
if tt.want.Sub(got).Abs() > tt.wantDelta {
t.Errorf("%q.Decode(%v) = %v, want %v", tt.fmt, tt.val, got, tt.want)
}
})
}
}

16
util.go Normal file
View File

@@ -0,0 +1,16 @@
package sqlite3
// Return true if stmt is an empty SQL statement.
// This is used as an optimization.
// It's OK to always return false here.
func emptyStatement(stmt string) bool {
for _, b := range []byte(stmt) {
switch b {
case ' ', '\n', '\r', '\t', '\v', '\f':
case ';':
default:
return false
}
}
return true
}

View File

@@ -1,161 +1,60 @@
package sqlite3
import (
"context"
"encoding/binary"
"math"
"github.com/tetratelabs/wazero/api"
"testing"
)
func init() {
Path = "./embed/sqlite3.wasm"
}
func Test_emptyStatement(t *testing.T) {
t.Parallel()
func newMemory(size uint32) memory {
mem := make(mockMemory, size)
return memory{mockModule{&mem}}
}
type mockModule struct {
memory api.Memory
}
func (m mockModule) Memory() api.Memory { return m.memory }
func (m mockModule) String() string { return "mockModule" }
func (m mockModule) Name() string { return "mockModule" }
func (m mockModule) ExportedGlobal(name string) api.Global { return nil }
func (m mockModule) ExportedMemory(name string) api.Memory { return nil }
func (m mockModule) ExportedFunction(name string) api.Function { return nil }
func (m mockModule) ExportedMemoryDefinitions() map[string]api.MemoryDefinition { return nil }
func (m mockModule) ExportedFunctionDefinitions() map[string]api.FunctionDefinition { return nil }
func (m mockModule) CloseWithExitCode(ctx context.Context, exitCode uint32) error { return nil }
func (m mockModule) Close(context.Context) error { return nil }
type mockMemory []byte
func (m mockMemory) Definition() api.MemoryDefinition { return nil }
func (m mockMemory) Size() uint32 { return uint32(len(m)) }
func (m mockMemory) ReadByte(offset uint32) (byte, bool) {
if offset >= m.Size() {
return 0, false
tests := []struct {
name string
stmt string
want bool
}{
{"empty", "", true},
{"space", " ", true},
{"separator", ";\n ", true},
{"begin", "BEGIN", false},
{"select", "SELECT 1;", false},
}
return m[offset], true
}
func (m mockMemory) ReadUint16Le(offset uint32) (uint16, bool) {
if !m.hasSize(offset, 2) {
return 0, false
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := emptyStatement(tt.stmt); got != tt.want {
t.Errorf("got %v, want %v", got, tt.want)
}
})
}
return binary.LittleEndian.Uint16(m[offset : offset+2]), true
}
func (m mockMemory) ReadUint32Le(offset uint32) (uint32, bool) {
if !m.hasSize(offset, 4) {
return 0, false
func Fuzz_emptyStatement(f *testing.F) {
f.Add("")
f.Add(" ")
f.Add(";\n ")
f.Add("; ;\v")
f.Add("BEGIN")
f.Add("SELECT 1;")
db, err := Open(":memory:")
if err != nil {
f.Fatal(err)
}
return binary.LittleEndian.Uint32(m[offset : offset+4]), true
}
defer db.Close()
func (m mockMemory) ReadFloat32Le(offset uint32) (float32, bool) {
v, ok := m.ReadUint32Le(offset)
if !ok {
return 0, false
}
return math.Float32frombits(v), true
}
func (m mockMemory) ReadUint64Le(offset uint32) (uint64, bool) {
if !m.hasSize(offset, 8) {
return 0, false
}
return binary.LittleEndian.Uint64(m[offset : offset+8]), true
}
func (m mockMemory) ReadFloat64Le(offset uint32) (float64, bool) {
v, ok := m.ReadUint64Le(offset)
if !ok {
return 0, false
}
return math.Float64frombits(v), true
}
func (m mockMemory) Read(offset, byteCount uint32) ([]byte, bool) {
if !m.hasSize(offset, byteCount) {
return nil, false
}
return m[offset : offset+byteCount : offset+byteCount], true
}
func (m mockMemory) WriteByte(offset uint32, v byte) bool {
if offset >= m.Size() {
return false
}
m[offset] = v
return true
}
func (m mockMemory) WriteUint16Le(offset uint32, v uint16) bool {
if !m.hasSize(offset, 2) {
return false
}
binary.LittleEndian.PutUint16(m[offset:], v)
return true
}
func (m mockMemory) WriteUint32Le(offset, v uint32) bool {
if !m.hasSize(offset, 4) {
return false
}
binary.LittleEndian.PutUint32(m[offset:], v)
return true
}
func (m mockMemory) WriteFloat32Le(offset uint32, v float32) bool {
return m.WriteUint32Le(offset, math.Float32bits(v))
}
func (m mockMemory) WriteUint64Le(offset uint32, v uint64) bool {
if !m.hasSize(offset, 8) {
return false
}
binary.LittleEndian.PutUint64(m[offset:], v)
return true
}
func (m mockMemory) WriteFloat64Le(offset uint32, v float64) bool {
return m.WriteUint64Le(offset, math.Float64bits(v))
}
func (m mockMemory) Write(offset uint32, val []byte) bool {
if !m.hasSize(offset, uint32(len(val))) {
return false
}
copy(m[offset:], val)
return true
}
func (m mockMemory) WriteString(offset uint32, val string) bool {
if !m.hasSize(offset, uint32(len(val))) {
return false
}
copy(m[offset:], val)
return true
}
func (m *mockMemory) Grow(delta uint32) (result uint32, ok bool) {
prev := (len(*m) + 65535) / 65536
*m = append(*m, make([]byte, 65536*delta)...)
return uint32(prev), true
}
func (m mockMemory) PageSize() (result uint32) {
return uint32(len(m) / 65536)
}
func (m mockMemory) hasSize(offset uint32, byteCount uint32) bool {
return uint64(offset)+uint64(byteCount) <= uint64(len(m))
f.Fuzz(func(t *testing.T, sql string) {
// If empty, SQLite parses it as empty.
if emptyStatement(sql) {
stmt, tail, err := db.Prepare(sql)
if err != nil {
t.Errorf("%q, %v", sql, err)
}
if stmt != nil {
t.Errorf("%q, %v", sql, stmt)
}
if tail != "" {
t.Errorf("%q", sql)
}
stmt.Close()
}
})
}

35
vfs.go
View File

@@ -18,12 +18,12 @@ import (
"github.com/tetratelabs/wazero/sys"
)
func vfsInstantiate(ctx context.Context, r wazero.Runtime) (err error) {
func vfsInstantiate(ctx context.Context, r wazero.Runtime) {
wasi := r.NewHostModuleBuilder("wasi_snapshot_preview1")
wasi.NewFunctionBuilder().WithFunc(vfsExit).Export("proc_exit")
_, err = wasi.Instantiate(ctx)
_, err := wasi.Instantiate(ctx)
if err != nil {
return err
panic(err)
}
env := r.NewHostModuleBuilder("env")
@@ -45,8 +45,11 @@ func vfsInstantiate(ctx context.Context, r wazero.Runtime) (err error) {
env.NewFunctionBuilder().WithFunc(vfsLock).Export("go_lock")
env.NewFunctionBuilder().WithFunc(vfsUnlock).Export("go_unlock")
env.NewFunctionBuilder().WithFunc(vfsCheckReservedLock).Export("go_check_reserved_lock")
env.NewFunctionBuilder().WithFunc(vfsFileControl).Export("go_file_control")
_, err = env.Instantiate(ctx)
return err
if err != nil {
panic(err)
}
}
func vfsExit(ctx context.Context, mod api.Module, exitCode uint32) {
@@ -112,11 +115,11 @@ func vfsFullPathname(ctx context.Context, mod api.Module, pVfs, zRelative, nFull
// Or using [os.Readlink] to resolve a symbolic link (as the Unix VFS did).
// This might be buggy on Windows (the Windows VFS doesn't try).
siz := uint32(len(abs) + 1)
if siz > nFull {
size := uint32(len(abs) + 1)
if size > nFull {
return uint32(CANTOPEN_FULLPATH)
}
mem := memory{mod}.view(zFull, siz)
mem := memory{mod}.view(zFull, size)
mem[len(abs)] = 0
copy(mem, abs)
@@ -145,7 +148,7 @@ func vfsDelete(ctx context.Context, mod api.Module, pVfs, zPath, syncDir uint32)
return _OK
}
func vfsAccess(ctx context.Context, mod api.Module, pVfs, zPath uint32, flags AccessFlag, pResOut uint32) uint32 {
func vfsAccess(ctx context.Context, mod api.Module, pVfs, zPath uint32, flags _AccessFlag, pResOut uint32) uint32 {
// Consider using [syscall.Access] for [ACCESS_READWRITE]/[ACCESS_READ]
// (as the Unix VFS does).
@@ -154,7 +157,7 @@ func vfsAccess(ctx context.Context, mod api.Module, pVfs, zPath uint32, flags Ac
var res uint32
switch {
case flags == ACCESS_EXISTS:
case flags == _ACCESS_EXISTS:
switch {
case err == nil:
res = 1
@@ -166,7 +169,7 @@ func vfsAccess(ctx context.Context, mod api.Module, pVfs, zPath uint32, flags Ac
case err == nil:
var want fs.FileMode = syscall.S_IRUSR
if flags == ACCESS_READWRITE {
if flags == _ACCESS_READWRITE {
want |= syscall.S_IWUSR
}
if fi.IsDir() {
@@ -304,3 +307,15 @@ func vfsFileSize(ctx context.Context, mod api.Module, pFile, pSize uint32) uint3
memory{mod}.writeUint64(pSize, uint64(off))
return _OK
}
func vfsFileControl(ctx context.Context, pFile, op, pArg uint32) uint32 {
// SQLite calls vfsFileControl with these opcodes:
// SQLITE_FCNTL_SIZE_HINT
// SQLITE_FCNTL_PRAGMA
// SQLITE_FCNTL_BUSYHANDLER
// SQLITE_FCNTL_HAS_MOVED
// SQLITE_FCNTL_SYNC
// SQLITE_FCNTL_COMMIT_PHASETWO
// SQLITE_FCNTL_PDB
return uint32(NOTFOUND)
}

View File

@@ -64,7 +64,7 @@ type vfsFileLocker struct {
}
func vfsLock(ctx context.Context, mod api.Module, pFile uint32, eLock vfsLockState) uint32 {
// SQLite never explicitly requests a pendig lock.
// Argument check. SQLite never explicitly requests a pendig lock.
if eLock != _SHARED_LOCK && eLock != _RESERVED_LOCK && eLock != _EXCLUSIVE_LOCK {
panic(assertErr())
}
@@ -72,12 +72,10 @@ func vfsLock(ctx context.Context, mod api.Module, pFile uint32, eLock vfsLockSta
ptr := vfsFilePtr{mod, pFile}
cLock := ptr.Lock()
// If we already have an equal or more restrictive lock, do nothing.
if cLock >= eLock {
return _OK
}
switch {
case cLock < _NO_LOCK || cLock > _EXCLUSIVE_LOCK:
// Connection state check.
panic(assertErr())
case cLock == _NO_LOCK && eLock > _SHARED_LOCK:
// We never move from unlocked to anything higher than a shared lock.
panic(assertErr())
@@ -86,31 +84,51 @@ func vfsLock(ctx context.Context, mod api.Module, pFile uint32, eLock vfsLockSta
panic(assertErr())
}
// If we already have an equal or more restrictive lock, do nothing.
if cLock >= eLock {
return _OK
}
fLock := ptr.Locker()
fLock.Lock()
defer fLock.Unlock()
// File state check.
switch {
case fLock.state < _NO_LOCK || fLock.state > _EXCLUSIVE_LOCK:
panic(assertErr())
case fLock.state == _NO_LOCK && fLock.shared != 0:
panic(assertErr())
case fLock.state == _EXCLUSIVE_LOCK && fLock.shared != 1:
panic(assertErr())
case fLock.state != _NO_LOCK && fLock.shared <= 0:
panic(assertErr())
case fLock.state < cLock:
panic(assertErr())
}
// If some other connection has a lock that precludes the requested lock, return BUSY.
if cLock != fLock.state && (eLock > _SHARED_LOCK || fLock.state >= _PENDING_LOCK) {
return uint32(BUSY)
}
// If a SHARED lock is requested, and some other connection has a SHARED or RESERVED lock,
// then increment the reference count and return OK.
if eLock == _SHARED_LOCK && (fLock.state == _SHARED_LOCK || fLock.state == _RESERVED_LOCK) {
if cLock != _NO_LOCK || fLock.shared <= 0 {
panic(assertErr())
}
ptr.SetLock(_SHARED_LOCK)
fLock.shared++
return _OK
}
// If control gets to this point, then actually go ahead and make
// operating system calls for the specified lock.
switch eLock {
case _SHARED_LOCK:
if fLock.state != _NO_LOCK || fLock.shared != 0 {
// Test the PENDING lock before acquiring a new SHARED lock.
if locked, _ := fLock.CheckPending(); locked {
return uint32(BUSY)
}
// If some other connection has a SHARED or RESERVED lock,
// increment the reference count and return OK.
if fLock.state == _SHARED_LOCK || fLock.state == _RESERVED_LOCK {
ptr.SetLock(_SHARED_LOCK)
fLock.shared++
return _OK
}
// Must be unlocked to get SHARED.
if fLock.state != _NO_LOCK {
panic(assertErr())
}
if rc := fLock.GetShared(); rc != _OK {
@@ -122,7 +140,8 @@ func vfsLock(ctx context.Context, mod api.Module, pFile uint32, eLock vfsLockSta
return _OK
case _RESERVED_LOCK:
if fLock.state != _SHARED_LOCK || fLock.shared <= 0 {
// Must be SHARED to get RESERVED.
if fLock.state != _SHARED_LOCK {
panic(assertErr())
}
if rc := fLock.GetReserved(); rc != _OK {
@@ -133,7 +152,8 @@ func vfsLock(ctx context.Context, mod api.Module, pFile uint32, eLock vfsLockSta
return _OK
case _EXCLUSIVE_LOCK:
if fLock.state <= _NO_LOCK || fLock.state >= _EXCLUSIVE_LOCK || fLock.shared <= 0 {
// Must be SHARED, PENDING or RESERVED to get EXCLUSIVE.
if fLock.state <= _NO_LOCK || fLock.state >= _EXCLUSIVE_LOCK {
panic(assertErr())
}
@@ -164,6 +184,7 @@ func vfsLock(ctx context.Context, mod api.Module, pFile uint32, eLock vfsLockSta
}
func vfsUnlock(ctx context.Context, mod api.Module, pFile uint32, eLock vfsLockState) uint32 {
// Argument check.
if eLock != _NO_LOCK && eLock != _SHARED_LOCK {
panic(assertErr())
}
@@ -171,6 +192,11 @@ func vfsUnlock(ctx context.Context, mod api.Module, pFile uint32, eLock vfsLockS
ptr := vfsFilePtr{mod, pFile}
cLock := ptr.Lock()
// Connection state check.
if cLock < _NO_LOCK || cLock > _EXCLUSIVE_LOCK {
panic(assertErr())
}
// If we don't have a more restrictive lock, do nothing.
if cLock <= eLock {
return _OK
@@ -180,10 +206,20 @@ func vfsUnlock(ctx context.Context, mod api.Module, pFile uint32, eLock vfsLockS
fLock.Lock()
defer fLock.Unlock()
if fLock.shared <= 0 {
// File state check.
switch {
case fLock.state <= _NO_LOCK || fLock.state > _EXCLUSIVE_LOCK:
panic(assertErr())
case fLock.state == _EXCLUSIVE_LOCK && fLock.shared != 1:
panic(assertErr())
case fLock.shared <= 0:
panic(assertErr())
case fLock.state < cLock:
panic(assertErr())
}
if cLock > _SHARED_LOCK {
// The connection must own the lock to release it.
if cLock != fLock.state {
panic(assertErr())
}
@@ -197,6 +233,7 @@ func vfsUnlock(ctx context.Context, mod api.Module, pFile uint32, eLock vfsLockS
}
}
// If we get here, make sure we're dropping all locks.
if eLock != _NO_LOCK {
panic(assertErr())
}
@@ -205,8 +242,9 @@ func vfsUnlock(ctx context.Context, mod api.Module, pFile uint32, eLock vfsLockS
// Release the file lock only when all connections have released the lock.
ptr.SetLock(_NO_LOCK)
if fLock.shared--; fLock.shared == 0 {
rc := fLock.Release()
fLock.state = _NO_LOCK
return uint32(fLock.Release())
return uint32(rc)
}
return _OK
}

View File

@@ -3,6 +3,7 @@ package sqlite3
import (
"context"
"os"
"path/filepath"
"runtime"
"testing"
)
@@ -10,22 +11,21 @@ import (
func Test_vfsLock(t *testing.T) {
// Other OSes lack open file descriptors locks.
switch runtime.GOOS {
case "linux", "darwin", "solaris", "windows":
case "linux", "darwin", "illumos", "windows":
//
default:
t.Skip()
}
name := filepath.Join(t.TempDir(), "test.db")
// Create a temporary file.
file1, err := os.CreateTemp("", "sqlite3-")
file1, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
if err != nil {
t.Fatal(err)
}
defer file1.Close()
name := file1.Name()
defer os.RemoveAll(name)
// Open the temporary file again.
file2, err := os.OpenFile(name, os.O_RDWR, 0)
if err != nil {

View File

@@ -7,6 +7,7 @@ import (
"io/fs"
"os"
"path/filepath"
"syscall"
"testing"
"time"
@@ -136,12 +137,12 @@ func Test_vfsFullPathname(t *testing.T) {
}
func Test_vfsDelete(t *testing.T) {
file, err := os.CreateTemp("", "sqlite3-")
name := filepath.Join(t.TempDir(), "test.db")
file, err := os.Create(name)
if err != nil {
t.Fatal(err)
}
name := file.Name()
defer os.RemoveAll(name)
file.Close()
mem := newMemory(128 + _MAX_PATHNAME)
@@ -163,16 +164,21 @@ func Test_vfsDelete(t *testing.T) {
}
func Test_vfsAccess(t *testing.T) {
dir, err := os.MkdirTemp("", "sqlite3-")
if err != nil {
dir := t.TempDir()
file := filepath.Join(t.TempDir(), "test.db")
if f, err := os.Create(file); err != nil {
t.Fatal(err)
} else {
f.Close()
}
if err := os.Chmod(file, syscall.S_IRUSR); err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
mem := newMemory(128 + _MAX_PATHNAME)
mem.writeString(8, dir)
rc := vfsAccess(context.TODO(), mem.mod, 0, 8, ACCESS_EXISTS, 4)
rc := vfsAccess(context.TODO(), mem.mod, 0, 8, _ACCESS_EXISTS, 4)
if rc != _OK {
t.Fatal("returned", rc)
}
@@ -180,13 +186,22 @@ func Test_vfsAccess(t *testing.T) {
t.Error("directory did not exist")
}
rc = vfsAccess(context.TODO(), mem.mod, 0, 8, ACCESS_READWRITE, 4)
rc = vfsAccess(context.TODO(), mem.mod, 0, 8, _ACCESS_READWRITE, 4)
if rc != _OK {
t.Fatal("returned", rc)
}
if got := mem.readUint32(4); got != 1 {
t.Error("can't access directory")
}
mem.writeString(8, file)
rc = vfsAccess(context.TODO(), mem.mod, 0, 8, _ACCESS_READWRITE, 4)
if rc != _OK {
t.Fatal("returned", rc)
}
if got := mem.readUint32(4); got != 0 {
t.Error("can access file")
}
}
func Test_vfsFile(t *testing.T) {

View File

@@ -13,19 +13,8 @@ func deleteOnClose(f *os.File) {
}
func (l *vfsFileLocker) GetShared() xErrorCode {
// A PENDING lock is needed before acquiring a SHARED lock.
if rc := l.readLock(_PENDING_BYTE, 1); rc != _OK {
return rc
}
// Acquire the SHARED lock.
rc := l.readLock(_SHARED_FIRST, _SHARED_SIZE)
// Drop the temporary PENDING lock.
if rc2 := l.unlock(_PENDING_BYTE, 1); rc == _OK {
return rc2
}
return rc
return l.readLock(_SHARED_FIRST, _SHARED_SIZE)
}
func (l *vfsFileLocker) GetReserved() xErrorCode {
@@ -44,16 +33,17 @@ func (l *vfsFileLocker) GetExclusive() xErrorCode {
}
func (l *vfsFileLocker) Downgrade() xErrorCode {
// Downgrade to a SHARED lock.
if rc := l.readLock(_SHARED_FIRST, _SHARED_SIZE); rc != _OK {
// In theory, the downgrade to a SHARED cannot fail because another
// process is holding an incompatible lock. If it does, this
// indicates that the other process is not following the locking
// protocol. If this happens, return IOERR_RDLOCK. Returning
// BUSY would confuse the upper layer.
return IOERR_RDLOCK
if l.state >= _EXCLUSIVE_LOCK {
// Downgrade to a SHARED lock.
if rc := l.readLock(_SHARED_FIRST, _SHARED_SIZE); rc != _OK {
// In theory, the downgrade to a SHARED cannot fail because another
// process is holding an incompatible lock. If it does, this
// indicates that the other process is not following the locking
// protocol. If this happens, return IOERR_RDLOCK. Returning
// BUSY would confuse the upper layer.
return IOERR_RDLOCK
}
}
// Release the PENDING and RESERVED locks.
return l.unlock(_PENDING_BYTE, 2)
}
@@ -68,6 +58,11 @@ func (l *vfsFileLocker) CheckReserved() (bool, xErrorCode) {
return l.checkLock(_RESERVED_BYTE, 1)
}
func (l *vfsFileLocker) CheckPending() (bool, xErrorCode) {
// Test the PENDING lock.
return l.checkLock(_PENDING_BYTE, 1)
}
func (l *vfsFileLocker) unlock(start, len int64) xErrorCode {
err := l.fcntlSetLock(&syscall.Flock_t{
Type: syscall.F_UNLCK,
@@ -85,7 +80,7 @@ func (l *vfsFileLocker) readLock(start, len int64) xErrorCode {
Type: syscall.F_RDLCK,
Start: start,
Len: len,
}), IOERR_LOCK)
}), IOERR_RDLOCK)
}
func (l *vfsFileLocker) writeLock(start, len int64) xErrorCode {
@@ -117,7 +112,7 @@ func (l *vfsFileLocker) fcntlGetLock(lock *syscall.Flock_t) error {
case "darwin":
// https://github.com/apple/darwin-xnu/blob/main/bsd/sys/fcntl.h
F_GETLK = 92 // F_OFD_GETLK
case "solaris":
case "illumos":
// https://github.com/illumos/illumos-gate/blob/master/usr/src/uts/common/sys/fcntl.h
F_GETLK = 47 // F_OFD_GETLK
}
@@ -133,7 +128,7 @@ func (l *vfsFileLocker) fcntlSetLock(lock *syscall.Flock_t) error {
case "darwin":
// https://github.com/apple/darwin-xnu/blob/main/bsd/sys/fcntl.h
F_SETLK = 90 // F_OFD_SETLK
case "solaris":
case "illumos":
// https://github.com/illumos/illumos-gate/blob/master/usr/src/uts/common/sys/fcntl.h
F_SETLK = 48 // F_OFD_SETLK
}
@@ -146,13 +141,14 @@ func (*vfsFileLocker) errorCode(err error, def xErrorCode) xErrorCode {
}
if errno, ok := err.(syscall.Errno); ok {
switch errno {
case syscall.EACCES:
case syscall.EAGAIN:
case syscall.EBUSY:
case syscall.EINTR:
case syscall.ENOLCK:
case syscall.EDEADLK:
case syscall.ETIMEDOUT:
case
syscall.EACCES,
syscall.EAGAIN,
syscall.EBUSY,
syscall.EINTR,
syscall.ENOLCK,
syscall.EDEADLK,
syscall.ETIMEDOUT:
return xErrorCode(BUSY)
case syscall.EPERM:
return xErrorCode(PERM)

View File

@@ -10,19 +10,8 @@ import (
func deleteOnClose(f *os.File) {}
func (l *vfsFileLocker) GetShared() xErrorCode {
// A PENDING lock is needed before acquiring a SHARED lock.
if rc := l.readLock(_PENDING_BYTE, 1); rc != _OK {
return rc
}
// Acquire the SHARED lock.
rc := l.readLock(_SHARED_FIRST, _SHARED_SIZE)
// Drop the temporary PENDING lock.
if rc2 := l.unlock(_PENDING_BYTE, 1); rc == _OK {
return rc2
}
return rc
return l.readLock(_SHARED_FIRST, _SHARED_SIZE)
}
func (l *vfsFileLocker) GetReserved() xErrorCode {
@@ -50,27 +39,39 @@ func (l *vfsFileLocker) GetExclusive() xErrorCode {
}
func (l *vfsFileLocker) Downgrade() xErrorCode {
// Release the SHARED lock.
l.unlock(_SHARED_FIRST, _SHARED_SIZE)
if l.state >= _EXCLUSIVE_LOCK {
// Release the SHARED lock.
l.unlock(_SHARED_FIRST, _SHARED_SIZE)
// Reacquire the SHARED lock.
if rc := l.readLock(_SHARED_FIRST, _SHARED_SIZE); rc != _OK {
// This should never happen.
// We should always be able to reacquire the read lock.
return IOERR_RDLOCK
// Reacquire the SHARED lock.
if rc := l.readLock(_SHARED_FIRST, _SHARED_SIZE); rc != _OK {
// This should never happen.
// We should always be able to reacquire the read lock.
return IOERR_RDLOCK
}
}
// Release the PENDING and RESERVED locks.
l.unlock(_RESERVED_BYTE, 1)
l.unlock(_PENDING_BYTE, 1)
if l.state >= _RESERVED_LOCK {
l.unlock(_RESERVED_BYTE, 1)
}
if l.state >= _PENDING_LOCK {
l.unlock(_PENDING_BYTE, 1)
}
return _OK
}
func (l *vfsFileLocker) Release() xErrorCode {
// Release all locks.
l.unlock(_SHARED_FIRST, _SHARED_SIZE)
l.unlock(_RESERVED_BYTE, 1)
l.unlock(_PENDING_BYTE, 1)
if l.state >= _RESERVED_LOCK {
l.unlock(_RESERVED_BYTE, 1)
}
if l.state >= _SHARED_LOCK {
l.unlock(_SHARED_FIRST, _SHARED_SIZE)
}
if l.state >= _PENDING_LOCK {
l.unlock(_PENDING_BYTE, 1)
}
return _OK
}
@@ -83,6 +84,15 @@ func (l *vfsFileLocker) CheckReserved() (bool, xErrorCode) {
return rc != _OK, _OK
}
func (l *vfsFileLocker) CheckPending() (bool, xErrorCode) {
// Test the PENDING lock.
rc := l.readLock(_PENDING_BYTE, 1)
if rc == _OK {
l.unlock(_PENDING_BYTE, 1)
}
return rc != _OK, _OK
}
func (l *vfsFileLocker) unlock(start, len uint32) xErrorCode {
err := windows.UnlockFileEx(windows.Handle(l.file.Fd()),
0, len, 0, &windows.Overlapped{Offset: start})
@@ -96,7 +106,7 @@ func (l *vfsFileLocker) readLock(start, len uint32) xErrorCode {
return l.errorCode(windows.LockFileEx(windows.Handle(l.file.Fd()),
windows.LOCKFILE_FAIL_IMMEDIATELY,
0, len, 0, &windows.Overlapped{Offset: start}),
IOERR_LOCK)
IOERR_RDLOCK)
}
func (l *vfsFileLocker) writeLock(start, len uint32) xErrorCode {