mirror of
https://github.com/ncruces/go-sqlite3.git
synced 2026-01-19 09:04:16 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71ae26e5c9 | ||
|
|
e91758c6a4 | ||
|
|
b749b32a62 | ||
|
|
3b4df71a94 | ||
|
|
df687a1c54 | ||
|
|
2f5b9837e1 | ||
|
|
c351400be7 | ||
|
|
231d3a0438 | ||
|
|
2f25e4eedb | ||
|
|
ad27d5d840 | ||
|
|
ec5bd236f8 | ||
|
|
a51cdb04e6 | ||
|
|
f50d5df3d0 | ||
|
|
4ac2ccf473 | ||
|
|
5f7a72a553 | ||
|
|
643b004727 | ||
|
|
72e0415184 | ||
|
|
28cb558d10 | ||
|
|
23806b0db1 | ||
|
|
6a80499823 | ||
|
|
110f36bdf9 | ||
|
|
f85426022d | ||
|
|
78fd0cbee5 | ||
|
|
0d59065719 | ||
|
|
6110e2d6e2 | ||
|
|
275b8c38a2 | ||
|
|
fd1244c471 | ||
|
|
f11d294825 | ||
|
|
22b702fcda | ||
|
|
831817a737 | ||
|
|
7329d9f2fb | ||
|
|
3aad1d5d79 | ||
|
|
f72c599d2d |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
custom: https://www.paypal.com/donate/buttons/manage/33P59ELZWGMK6
|
||||
10
.github/workflows/go.yml
vendored
10
.github/workflows/go.yml
vendored
@@ -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
|
||||
|
||||
17
README.md
17
README.md
@@ -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
25
api.go
@@ -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
6
blob.go
Normal 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
|
||||
19
compile.go
19
compile.go
@@ -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
212
conn.go
@@ -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
|
||||
}
|
||||
|
||||
136
conn_test.go
136
conn_test.go
@@ -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)
|
||||
|
||||
25
const.go
25
const.go
@@ -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 {
|
||||
|
||||
@@ -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
361
driver/driver.go
Normal 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
348
driver/driver_test.go
Normal 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
11
driver/error.go
Normal 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
149
driver/example_test.go
Normal 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
31
driver/time.go
Normal 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
46
driver/time_test.go
Normal 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
14
driver/util.go
Normal 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
18
driver/util_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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 \
|
||||
|
||||
@@ -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.
7
error.go
7
error.go
@@ -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 {
|
||||
|
||||
@@ -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
2
go.mod
@@ -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
4
go.sum
@@ -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
8
mem.go
@@ -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
161
mock_test.go
Normal 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))
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
155
stmt.go
@@ -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
180
tests/bradfitz/sql_test.go
Normal 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()
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package compile_empty
|
||||
package compile
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
14
tests/compile/nil/compile_test.go
Normal file
14
tests/compile/nil/compile_test.go
Normal 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
229
tests/conn_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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
103
tests/driver_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
191
tests/parallel/parallel_test.go
Normal file
191
tests/parallel/parallel_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
317
time.go
Normal 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
118
time_test.go
Normal 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
16
util.go
Normal 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
|
||||
}
|
||||
195
util_test.go
195
util_test.go
@@ -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
35
vfs.go
@@ -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)
|
||||
}
|
||||
|
||||
86
vfs_lock.go
86
vfs_lock.go
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
31
vfs_test.go
31
vfs_test.go
@@ -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) {
|
||||
|
||||
58
vfs_unix.go
58
vfs_unix.go
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user