mirror of
https://github.com/ncruces/go-sqlite3.git
synced 2026-01-11 21:49:13 +00:00
Nested transactions.
This commit is contained in:
48
README.md
48
README.md
@@ -4,22 +4,58 @@
|
||||
[](https://goreportcard.com/report/github.com/ncruces/go-sqlite3)
|
||||
[](https://raw.githack.com/wiki/ncruces/go-sqlite3/coverage.html)
|
||||
|
||||
⚠️ CAUTION ⚠️
|
||||
### ⚠️ Work in Progress ⚠️
|
||||
|
||||
This is a WIP.
|
||||
Go module `github.com/ncruces/go-sqlite3` wraps a [WASM](https://webassembly.org/) build of [SQLite](https://sqlite.org/),
|
||||
and uses [wazero](https://wazero.io/) to provide `cgo`-free SQLite bindings.
|
||||
|
||||
- Package [`github.com/ncruces/go-sqlite3`](https://pkg.go.dev/github.com/ncruces/go-sqlite3)
|
||||
wraps the [C SQLite API](https://www.sqlite.org/cintro.html)
|
||||
([example usage](https://pkg.go.dev/github.com/ncruces/go-sqlite3#example-package)).
|
||||
- Package [`github.com/ncruces/go-sqlite3/driver`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/driver)
|
||||
provides a [`database/sql`](https://pkg.go.dev/database/sql) driver
|
||||
([example usage](https://pkg.go.dev/github.com/ncruces/go-sqlite3/driver#example-package)).
|
||||
- Package [`github.com/ncruces/go-sqlite3/embed`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/embed)
|
||||
embeds a build of SQLite into your application.
|
||||
|
||||
### Caveats
|
||||
|
||||
Because WASM does not support shared memory,
|
||||
[WAL](https://www.sqlite.org/wal.html) support is [limited](https://www.sqlite.org/wal.html#noshm).
|
||||
|
||||
To work around this limitation, SQLite is compiled with
|
||||
[`SQLITE_DEFAULT_LOCKING_MODE=1`](https://www.sqlite.org/compile.html#default_locking_mode),
|
||||
making `EXCLUSIVE` the default locking mode.
|
||||
For non-WAL databases, `NORMAL` locking mode can be activated with
|
||||
[`PRAGMA locking_mode=NORMAL`](https://www.sqlite.org/pragma.html#pragma_locking_mode).
|
||||
|
||||
Because connection pooling is incompatible with `EXCLUSIVE` locking mode,
|
||||
the `database/sql` driver defaults to `NORMAL` locking mode,
|
||||
and WAL databases are not supported.
|
||||
|
||||
### Roadmap
|
||||
|
||||
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] design a simple, nice API, enough for simple use cases
|
||||
- [x] design a nice API, enough for simple use cases
|
||||
- [x] provide a simple `database/sql` driver
|
||||
- [x] file locking, compatible with SQLite on macOS/Linux/Windows
|
||||
- [ ] advanced SQLite features
|
||||
- [ ] nested transactions
|
||||
- [x] nested transactions
|
||||
- [ ] incremental BLOB I/O
|
||||
- [ ] online backup
|
||||
- [ ] session extension
|
||||
- [ ] snapshots
|
||||
- [ ] SQL functions
|
||||
- [ ] SQL functions
|
||||
- [ ] custom VFSes
|
||||
- [ ] read-only VFS, wrapping an [`io.ReaderAt`](https://pkg.go.dev/io#ReaderAt)
|
||||
- [ ] in-memory VFS, wrapping a [`bytes.Buffer`](https://pkg.go.dev/bytes#Buffer)
|
||||
- [ ] expose a custom VFS API
|
||||
|
||||
### Alternatives
|
||||
|
||||
- [`modernc.org/sqlite`](https://pkg.go.dev/modernc.org/sqlite)
|
||||
- [`crawshaw.io/sqlite`](https://pkg.go.dev/crawshaw.io/sqlite)
|
||||
- [`github.com/mattn/go-sqlite3`](https://pkg.go.dev/github.com/mattn/go-sqlite3)
|
||||
2
api.go
2
api.go
@@ -62,6 +62,7 @@ func newConn(ctx context.Context, module api.Module) (_ *Conn, err error) {
|
||||
columnText: getFun("sqlite3_column_text"),
|
||||
columnBlob: getFun("sqlite3_column_blob"),
|
||||
columnBytes: getFun("sqlite3_column_bytes"),
|
||||
autocommit: getFun("sqlite3_get_autocommit"),
|
||||
lastRowid: getFun("sqlite3_last_insert_rowid"),
|
||||
changes: getFun("sqlite3_changes64"),
|
||||
interrupt: getFun("sqlite3_interrupt"),
|
||||
@@ -106,6 +107,7 @@ type sqliteAPI struct {
|
||||
columnText api.Function
|
||||
columnBlob api.Function
|
||||
columnBytes api.Function
|
||||
autocommit api.Function
|
||||
lastRowid api.Function
|
||||
changes api.Function
|
||||
interrupt api.Function
|
||||
|
||||
59
conn.go
59
conn.go
@@ -91,6 +91,17 @@ func (c *Conn) Close() error {
|
||||
return c.mem.mod.Close(c.ctx)
|
||||
}
|
||||
|
||||
// GetAutocommit tests the connection for auto-commit mode.
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/get_autocommit.html
|
||||
func (c *Conn) GetAutocommit() bool {
|
||||
r, err := c.api.autocommit.Call(c.ctx, uint64(c.handle))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return r[0] != 0
|
||||
}
|
||||
|
||||
// SetInterrupt interrupts a long-running query when done is closed.
|
||||
//
|
||||
// Subsequent uses of the connection will return [INTERRUPT]
|
||||
@@ -111,22 +122,31 @@ func (c *Conn) SetInterrupt(done <-chan struct{}) (old <-chan struct{}) {
|
||||
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 {
|
||||
// Finalize the uncompleted SQL statement.
|
||||
if c.pending != nil {
|
||||
c.pending.Close()
|
||||
c.pending = 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()
|
||||
if c.pending == nil {
|
||||
c.pending, _, _ = c.Prepare(`SELECT 1 UNION ALL SELECT 2`)
|
||||
c.pending.Step()
|
||||
} else {
|
||||
c.pending.Reset()
|
||||
}
|
||||
|
||||
// Don't create the goroutine if we're already interrupted.
|
||||
// This happens frequently while restoring to a previously interrupted state.
|
||||
if c.checkInterrupt() {
|
||||
return old
|
||||
}
|
||||
|
||||
waiter := make(chan struct{})
|
||||
c.waiter = waiter
|
||||
@@ -154,11 +174,25 @@ func (c *Conn) SetInterrupt(done <-chan struct{}) (old <-chan struct{}) {
|
||||
return old
|
||||
}
|
||||
|
||||
func (c *Conn) checkInterrupt() bool {
|
||||
select {
|
||||
case <-c.done: // Done was closed.
|
||||
_, err := c.api.interrupt.Call(c.ctx, uint64(c.handle))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
c.checkInterrupt()
|
||||
defer c.arena.reset()
|
||||
sqlPtr := c.arena.string(sql)
|
||||
|
||||
@@ -326,6 +360,15 @@ type arena struct {
|
||||
ptrs []uint32
|
||||
}
|
||||
|
||||
func (a *arena) free() {
|
||||
if a.c == nil {
|
||||
return
|
||||
}
|
||||
a.reset()
|
||||
a.c.free(a.base)
|
||||
a.c = nil
|
||||
}
|
||||
|
||||
func (a *arena) reset() {
|
||||
for _, ptr := range a.ptrs {
|
||||
a.c.free(ptr)
|
||||
|
||||
@@ -30,7 +30,7 @@ func TestConn_newArena(t *testing.T) {
|
||||
defer db.Close()
|
||||
|
||||
arena := db.newArena(16)
|
||||
defer arena.reset()
|
||||
defer arena.free()
|
||||
|
||||
const title = "Lorem ipsum"
|
||||
|
||||
@@ -50,6 +50,7 @@ func TestConn_newArena(t *testing.T) {
|
||||
if got := db.mem.readString(ptr, math.MaxUint32); got != body {
|
||||
t.Errorf("got %q, want %q", got, body)
|
||||
}
|
||||
arena.free()
|
||||
}
|
||||
|
||||
func TestConn_newBytes(t *testing.T) {
|
||||
|
||||
@@ -31,7 +31,8 @@ func Example() {
|
||||
defer db.Close()
|
||||
defer os.Remove("./recordings.db")
|
||||
|
||||
err = createAlbumsTable()
|
||||
// Create a table with some data in it.
|
||||
err = albumsSetup()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -65,7 +66,7 @@ func Example() {
|
||||
// ID of added album: 5
|
||||
}
|
||||
|
||||
func createAlbumsTable() error {
|
||||
func albumsSetup() error {
|
||||
_, err := db.Exec(`
|
||||
DROP TABLE IF EXISTS album;
|
||||
CREATE TABLE album (
|
||||
|
||||
@@ -45,6 +45,7 @@ zig cc --target=wasm32-wasi -flto -g0 -Os \
|
||||
-Wl,--export=sqlite3_column_text \
|
||||
-Wl,--export=sqlite3_column_blob \
|
||||
-Wl,--export=sqlite3_column_bytes \
|
||||
-Wl,--export=sqlite3_get_autocommit \
|
||||
-Wl,--export=sqlite3_last_insert_rowid \
|
||||
-Wl,--export=sqlite3_changes64 \
|
||||
-Wl,--export=sqlite3_interrupt \
|
||||
|
||||
Binary file not shown.
76
save.go
Normal file
76
save.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package sqlite3
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// Savepoint creates a named SQLite transaction using SAVEPOINT.
|
||||
//
|
||||
// On success Savepoint returns a release func that will call
|
||||
// either RELEASE or ROLLBACK depending on whether the parameter *error
|
||||
// points to a nil or non-nil error.
|
||||
//
|
||||
// This is meant to be deferred:
|
||||
//
|
||||
// func doWork(conn *sqlite3.Conn) (err error) {
|
||||
// defer conn.Savepoint()(&err)
|
||||
//
|
||||
// // ... do work in the transaction
|
||||
// }
|
||||
func (conn *Conn) Savepoint() (release func(*error)) {
|
||||
name := "sqlite3.Savepoint" // names can be reused
|
||||
var pc [1]uintptr
|
||||
if n := runtime.Callers(2, pc[:]); n > 0 {
|
||||
frames := runtime.CallersFrames(pc[:n])
|
||||
frame, _ := frames.Next()
|
||||
if frame.Function != "" {
|
||||
name = frame.Function
|
||||
}
|
||||
}
|
||||
|
||||
err := conn.Exec(fmt.Sprintf("SAVEPOINT %q;", name))
|
||||
if err != nil {
|
||||
return func(errp *error) {
|
||||
if *errp == nil {
|
||||
*errp = err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return func(errp *error) {
|
||||
recovered := recover()
|
||||
if recovered != nil {
|
||||
defer panic(recovered)
|
||||
}
|
||||
|
||||
if conn.GetAutocommit() {
|
||||
// There is nothing to commit/rollback.
|
||||
return
|
||||
}
|
||||
|
||||
if *errp == nil && recovered == nil {
|
||||
// Success path.
|
||||
// RELEASE the savepoint successfully.
|
||||
*errp = conn.Exec(fmt.Sprintf("RELEASE %q;", name))
|
||||
if *errp == nil {
|
||||
return
|
||||
}
|
||||
// Possible interrupt, fall through to the error path.
|
||||
}
|
||||
|
||||
// Error path.
|
||||
// Always ROLLBACK even if the connection has been interrupted.
|
||||
old := conn.SetInterrupt(nil)
|
||||
defer conn.SetInterrupt(old)
|
||||
|
||||
err := conn.Exec(fmt.Sprintf("ROLLBACK TO %q;", name))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = conn.Exec(fmt.Sprintf("RELEASE %q;", name))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
1
stmt.go
1
stmt.go
@@ -66,6 +66,7 @@ func (s *Stmt) ClearBindings() error {
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/step.html
|
||||
func (s *Stmt) Step() bool {
|
||||
s.c.checkInterrupt()
|
||||
r, err := s.c.api.step.Call(s.c.ctx, uint64(s.handle))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -103,40 +103,38 @@ func TestConn_SetInterrupt(t *testing.T) {
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
cancel()
|
||||
db.SetInterrupt(ctx.Done())
|
||||
cancel()
|
||||
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
ctx, cancel = context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
db.SetInterrupt(ctx.Done())
|
||||
|
||||
// Interrupting can be cleared.
|
||||
err = db.Exec(`SELECT 1`)
|
||||
|
||||
260
tests/save_test.go
Normal file
260
tests/save_test.go
Normal file
@@ -0,0 +1,260 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
)
|
||||
|
||||
func TestConn_Savepoint_exec(t *testing.T) {
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS test (col)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
errFailed := errors.New("failed")
|
||||
|
||||
count := func() int {
|
||||
stmt, _, err := db.Prepare(`SELECT count(*) FROM test`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stmt.Step() {
|
||||
return stmt.ColumnInt(0)
|
||||
}
|
||||
t.Fatal(stmt.Err())
|
||||
return 0
|
||||
}
|
||||
|
||||
insert := func(succeed bool) (err error) {
|
||||
defer db.Savepoint()(&err)
|
||||
|
||||
err = db.Exec(`INSERT INTO test VALUES ('hello')`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if succeed {
|
||||
return nil
|
||||
}
|
||||
return errFailed
|
||||
}
|
||||
|
||||
err = insert(true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := count(); got != 1 {
|
||||
t.Errorf("got %d, want 1", got)
|
||||
}
|
||||
|
||||
err = insert(true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := count(); got != 2 {
|
||||
t.Errorf("got %d, want 2", got)
|
||||
}
|
||||
|
||||
err = insert(false)
|
||||
if err != errFailed {
|
||||
t.Errorf("got %v, want errFailed", err)
|
||||
}
|
||||
if got := count(); got != 2 {
|
||||
t.Errorf("got %d, want 2", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConn_Savepoint_panic(t *testing.T) {
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS test (col)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = db.Exec(`INSERT INTO test VALUES ('one');`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
panics := func() (err error) {
|
||||
defer db.Savepoint()(&err)
|
||||
|
||||
err = db.Exec(`INSERT INTO test VALUES ('hello')`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
panic("omg!")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
p := recover()
|
||||
if p != "omg!" {
|
||||
t.Errorf("got %v, want panic", p)
|
||||
}
|
||||
|
||||
stmt, _, err := db.Prepare(`SELECT count(*) FROM test`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stmt.Step() {
|
||||
got := stmt.ColumnInt(0)
|
||||
if got != 1 {
|
||||
t.Errorf("got %d, want 1", got)
|
||||
}
|
||||
return
|
||||
}
|
||||
t.Fatal(stmt.Err())
|
||||
}()
|
||||
|
||||
err = panics()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConn_Savepoint_interrupt(t *testing.T) {
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS test (col)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
release := db.Savepoint()
|
||||
err = db.Exec(`INSERT INTO test(col) VALUES(1)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
release(&err)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
db.SetInterrupt(ctx.Done())
|
||||
|
||||
release1 := db.Savepoint()
|
||||
err = db.Exec(`INSERT INTO test(col) VALUES(2)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
release2 := db.Savepoint()
|
||||
err = db.Exec(`INSERT INTO test(col) VALUES(3)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
checkInterrupt := func(err error) {
|
||||
var serr *sqlite3.Error
|
||||
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.INTERRUPT {
|
||||
t.Errorf("got %d, want sqlite3.INTERRUPT", rc)
|
||||
}
|
||||
if got := err.Error(); got != `sqlite3: interrupted` {
|
||||
t.Error("got message: ", got)
|
||||
}
|
||||
}
|
||||
|
||||
cancel()
|
||||
db.Savepoint()(&err)
|
||||
checkInterrupt(err)
|
||||
|
||||
err = db.Exec(`INSERT INTO test(col) VALUES(4)`)
|
||||
checkInterrupt(err)
|
||||
|
||||
err = context.Canceled
|
||||
release2(&err)
|
||||
if err != context.Canceled {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var nilErr error
|
||||
release1(&nilErr)
|
||||
checkInterrupt(nilErr)
|
||||
|
||||
db.SetInterrupt(nil)
|
||||
stmt, _, err := db.Prepare(`SELECT count(*) FROM test`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if stmt.Step() {
|
||||
got := stmt.ColumnInt(0)
|
||||
if got != 1 {
|
||||
t.Errorf("got %d, want 1", got)
|
||||
}
|
||||
}
|
||||
err = stmt.Err()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConn_Savepoint_rollback(t *testing.T) {
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS test (col)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
release := db.Savepoint()
|
||||
err = db.Exec(`INSERT INTO test(col) VALUES(1)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = db.Exec(`COMMIT`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
release(&err)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, _, err := db.Prepare(`SELECT count(*) FROM test`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if stmt.Step() {
|
||||
got := stmt.ColumnInt(0)
|
||||
if got != 1 {
|
||||
t.Errorf("got %d, want 1", got)
|
||||
}
|
||||
}
|
||||
err = stmt.Err()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user