mirror of
https://github.com/ncruces/go-sqlite3.git
synced 2026-01-18 16:39:13 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56e8281bdb | ||
|
|
f61d430e65 | ||
|
|
dbaed53b9a | ||
|
|
8b1bfd04e3 | ||
|
|
11c1687146 | ||
|
|
94c43a8685 | ||
|
|
a25159a070 | ||
|
|
e007e9b060 | ||
|
|
66a730893f | ||
|
|
926adeb3f5 | ||
|
|
677f51bec1 | ||
|
|
5d6f92b733 |
23
README.md
23
README.md
@@ -18,6 +18,10 @@ embeds a build of SQLite into your application.
|
||||
|
||||
### Caveats
|
||||
|
||||
This module replaces the SQLite [OS Interface](https://www.sqlite.org/vfs.html) (aka VFS)
|
||||
with a pure Go implementation.
|
||||
This has numerous benefits, but also comes with some caveats.
|
||||
|
||||
#### Write-Ahead Logging
|
||||
|
||||
Because WASM does not support shared memory,
|
||||
@@ -30,8 +34,10 @@ 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.
|
||||
the `database/sql` driver defaults to `NORMAL` locking mode.
|
||||
To open WAL databases, or use `EXCLUSIVE` locking mode,
|
||||
disable connection pooling by calling
|
||||
[`db.SetMaxOpenConns(1)`](https://pkg.go.dev/database/sql#DB.SetMaxOpenConns).
|
||||
|
||||
#### Open File Description Locks
|
||||
|
||||
@@ -43,15 +49,14 @@ OFD locks are fully compatible with process-associated POSIX advisory locks,
|
||||
and are supported on Linux, macOS and illumos.
|
||||
As a work around for other Unixes, you can use [`nolock=1`](https://www.sqlite.org/uri.html).
|
||||
|
||||
#### Testing
|
||||
|
||||
The pure Go VFS is stress tested by running an unmodified build of SQLite's
|
||||
[mptest](https://github.com/sqlite/sqlite/blob/master/mptest/mptest.c)
|
||||
on Linux, macOS and Windows.
|
||||
|
||||
### 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 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
|
||||
- [x] nested transactions
|
||||
- [x] incremental BLOB I/O
|
||||
|
||||
@@ -11,7 +11,7 @@ type Backup struct {
|
||||
|
||||
// Backup backs up srcDB on the src connection to the "main" database in dstURI.
|
||||
//
|
||||
// Backup calls [Conn.Open] to open the SQLite database file dstURI,
|
||||
// Backup calls [Open] to open the SQLite database file dstURI,
|
||||
// and blocks until the entire backup is complete.
|
||||
// Use [Conn.BackupInit] for incremental backup.
|
||||
//
|
||||
@@ -28,7 +28,7 @@ func (src *Conn) Backup(srcDB, dstURI string) error {
|
||||
|
||||
// Restore restores dstDB on the dst connection from the "main" database in srcURI.
|
||||
//
|
||||
// Restore calls [Conn.Open] to open the SQLite database file srcURI,
|
||||
// Restore calls [Open] to open the SQLite database file srcURI,
|
||||
// and blocks until the entire restore is complete.
|
||||
//
|
||||
// https://www.sqlite.org/backup.html
|
||||
@@ -48,7 +48,7 @@ func (dst *Conn) Restore(dstDB, srcURI string) error {
|
||||
|
||||
// BackupInit initializes a backup operation to copy the content of one database into another.
|
||||
//
|
||||
// BackupInit calls [Conn.Open] to open the SQLite database file dstURI,
|
||||
// BackupInit calls [Open] to open the SQLite database file dstURI,
|
||||
// then initializes a backup that copies the contents of srcDB on the src connection
|
||||
// to the "main" database in dstURI.
|
||||
//
|
||||
|
||||
1
blob.go
1
blob.go
@@ -25,6 +25,7 @@ var _ io.ReadWriteSeeker = &Blob{}
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/blob_open.html
|
||||
func (c *Conn) OpenBlob(db, table, column string, row int64, write bool) (*Blob, error) {
|
||||
c.checkInterrupt()
|
||||
defer c.arena.reset()
|
||||
blobPtr := c.arena.new(ptrlen)
|
||||
dbPtr := c.arena.string(db)
|
||||
|
||||
54
conn.go
54
conn.go
@@ -3,6 +3,7 @@ package sqlite3
|
||||
import (
|
||||
"context"
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"runtime"
|
||||
@@ -87,13 +88,19 @@ func (c *Conn) openDB(filename string, flags OpenFlag) (uint32, error) {
|
||||
}
|
||||
}
|
||||
|
||||
c.arena.reset()
|
||||
pragmaPtr := c.arena.string(pragmas.String())
|
||||
r := c.call(c.api.exec, uint64(handle), uint64(pragmaPtr), 0, 0, 0)
|
||||
if err := c.module.error(r[0], handle, pragmas.String()); err != nil {
|
||||
if errors.Is(err, ERROR) {
|
||||
err = fmt.Errorf("sqlite3: invalid _pragma: %w", err)
|
||||
}
|
||||
c.closeDB(handle)
|
||||
return 0, fmt.Errorf("sqlite3: invalid _pragma: %w", err)
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
c.call(c.api.timeCollation, uint64(handle))
|
||||
return handle, nil
|
||||
}
|
||||
|
||||
@@ -119,6 +126,8 @@ func (c *Conn) Close() error {
|
||||
}
|
||||
|
||||
c.SetInterrupt(context.Background())
|
||||
c.pending.Close()
|
||||
c.pending = nil
|
||||
|
||||
r := c.call(c.api.close, uint64(c.handle))
|
||||
if err := c.error(r[0]); err != nil {
|
||||
@@ -143,23 +152,6 @@ func (c *Conn) Exec(sql string) error {
|
||||
return c.error(r[0])
|
||||
}
|
||||
|
||||
// MustPrepare calls [Conn.Prepare] and panics on error,
|
||||
// a nil Stmt, or a non-empty tail.
|
||||
func (c *Conn) MustPrepare(sql string) *Stmt {
|
||||
s, tail, err := c.PrepareFlags(sql, 0)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if s == nil {
|
||||
panic(emptyErr)
|
||||
}
|
||||
if !emptyStatement(tail) {
|
||||
s.Close()
|
||||
panic(tailErr)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Prepare calls [Conn.PrepareFlags] with no flags.
|
||||
func (c *Conn) Prepare(sql string) (stmt *Stmt, tail string, err error) {
|
||||
return c.PrepareFlags(sql, 0)
|
||||
@@ -247,26 +239,23 @@ func (c *Conn) SetInterrupt(ctx context.Context) (old context.Context) {
|
||||
<-c.waiter // Wait for it to finish.
|
||||
c.waiter = nil
|
||||
}
|
||||
// Reset the pending statement.
|
||||
if c.pending != nil {
|
||||
c.pending.Reset()
|
||||
}
|
||||
|
||||
old = c.interrupt
|
||||
c.interrupt = ctx
|
||||
if ctx == nil || ctx.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.
|
||||
if c.pending == nil {
|
||||
c.pending = c.MustPrepare(`SELECT 1 UNION ALL SELECT 2`)
|
||||
c.pending.Step()
|
||||
} else {
|
||||
c.pending.Reset()
|
||||
c.pending, _, _ = c.Prepare(`SELECT 1 UNION ALL SELECT 2`)
|
||||
}
|
||||
c.pending.Step()
|
||||
|
||||
// Don't create the goroutine if we're already interrupted.
|
||||
// This happens frequently while restoring to a previously interrupted state.
|
||||
@@ -306,15 +295,18 @@ func (c *Conn) checkInterrupt() bool {
|
||||
// Pragma executes a PRAGMA statement and returns any results.
|
||||
//
|
||||
// https://www.sqlite.org/pragma.html
|
||||
func (c *Conn) Pragma(str string) []string {
|
||||
stmt := c.MustPrepare(`PRAGMA ` + str)
|
||||
func (c *Conn) Pragma(str string) ([]string, error) {
|
||||
stmt, _, err := c.Prepare(`PRAGMA ` + str)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
var pragmas []string
|
||||
for stmt.Step() {
|
||||
pragmas = append(pragmas, stmt.ColumnText(0))
|
||||
}
|
||||
return pragmas
|
||||
return pragmas, stmt.Close()
|
||||
}
|
||||
|
||||
func (c *Conn) error(rc uint64, sql ...string) error {
|
||||
@@ -333,6 +325,6 @@ type DriverConn interface {
|
||||
driver.ExecerContext
|
||||
driver.ConnPrepareContext
|
||||
|
||||
Savepoint() (release func(*error))
|
||||
Savepoint() Savepoint
|
||||
OpenBlob(db, table, column string, row int64, write bool) (*Blob, error)
|
||||
}
|
||||
|
||||
25
const.go
25
const.go
@@ -133,6 +133,7 @@ const (
|
||||
CONSTRAINT_DATATYPE ExtendedErrorCode = xErrorCode(CONSTRAINT) | (12 << 8)
|
||||
NOTICE_RECOVER_WAL ExtendedErrorCode = xErrorCode(NOTICE) | (1 << 8)
|
||||
NOTICE_RECOVER_ROLLBACK ExtendedErrorCode = xErrorCode(NOTICE) | (2 << 8)
|
||||
NOTICE_RBU ExtendedErrorCode = xErrorCode(NOTICE) | (3 << 8)
|
||||
WARNING_AUTOINDEX ExtendedErrorCode = xErrorCode(WARNING) | (1 << 8)
|
||||
AUTH_USER ExtendedErrorCode = xErrorCode(AUTH) | (1 << 8)
|
||||
)
|
||||
@@ -167,14 +168,6 @@ const (
|
||||
OPEN_EXRESCODE OpenFlag = 0x02000000 /* Extended result codes */
|
||||
)
|
||||
|
||||
type _AccessFlag uint32
|
||||
|
||||
const (
|
||||
_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
|
||||
@@ -216,3 +209,19 @@ func (t Datatype) String() string {
|
||||
}
|
||||
return strconv.FormatUint(uint64(t), 10)
|
||||
}
|
||||
|
||||
type _AccessFlag uint32
|
||||
|
||||
const (
|
||||
_ACCESS_EXISTS _AccessFlag = 0
|
||||
_ACCESS_READWRITE _AccessFlag = 1 /* Used by PRAGMA temp_store_directory */
|
||||
_ACCESS_READ _AccessFlag = 2 /* Unused */
|
||||
)
|
||||
|
||||
type _SyncFlag uint32
|
||||
|
||||
const (
|
||||
_SYNC_NORMAL _SyncFlag = 0x00002
|
||||
_SYNC_FULL _SyncFlag = 0x00003
|
||||
_SYNC_DATAONLY _SyncFlag = 0x00010
|
||||
)
|
||||
|
||||
@@ -86,15 +86,17 @@ func (sqlite) Open(name string) (_ driver.Conn, err error) {
|
||||
}
|
||||
|
||||
type conn struct {
|
||||
conn *sqlite3.Conn
|
||||
txBegin string
|
||||
txCommit string
|
||||
conn *sqlite3.Conn
|
||||
txBegin string
|
||||
txCommit string
|
||||
txRollback string
|
||||
}
|
||||
|
||||
var (
|
||||
// Ensure these interfaces are implemented:
|
||||
_ driver.ExecerContext = conn{}
|
||||
_ driver.ConnBeginTx = conn{}
|
||||
_ driver.Validator = conn{}
|
||||
_ sqlite3.DriverConn = conn{}
|
||||
)
|
||||
|
||||
@@ -102,27 +104,49 @@ func (c conn) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
func (c conn) IsValid() (valid bool) {
|
||||
r, err := c.conn.Pragma("locking_mode")
|
||||
return err == nil && len(r) == 1 && r[0] == "normal"
|
||||
}
|
||||
|
||||
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):
|
||||
}
|
||||
|
||||
func (c conn) BeginTx(_ context.Context, opts driver.TxOptions) (driver.Tx, error) {
|
||||
txBegin := c.txBegin
|
||||
c.txCommit = `COMMIT`
|
||||
c.txRollback = `ROLLBACK`
|
||||
|
||||
if opts.ReadOnly {
|
||||
c.txCommit = `
|
||||
ROLLBACK;
|
||||
PRAGMA query_only=` + c.conn.Pragma("query_only")[0]
|
||||
query_only, err := c.conn.Pragma("query_only")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
txBegin = `
|
||||
BEGIN deferred;
|
||||
PRAGMA query_only=on`
|
||||
c.txCommit = `
|
||||
ROLLBACK;
|
||||
PRAGMA query_only=` + query_only[0]
|
||||
c.txRollback = c.txCommit
|
||||
}
|
||||
|
||||
switch opts.Isolation {
|
||||
default:
|
||||
return nil, isolationErr
|
||||
case
|
||||
driver.IsolationLevel(sql.LevelDefault),
|
||||
driver.IsolationLevel(sql.LevelSerializable):
|
||||
break
|
||||
case driver.IsolationLevel(sql.LevelReadUncommitted):
|
||||
read_uncommitted, err := c.conn.Pragma("read_uncommitted")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
txBegin += `; PRAGMA read_uncommitted=on`
|
||||
c.txCommit += `; PRAGMA read_uncommitted=` + read_uncommitted[0]
|
||||
c.txRollback += `; PRAGMA read_uncommitted=` + read_uncommitted[0]
|
||||
}
|
||||
|
||||
err := c.conn.Exec(txBegin)
|
||||
@@ -134,14 +158,14 @@ func (c conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, er
|
||||
|
||||
func (c conn) Commit() error {
|
||||
err := c.conn.Exec(c.txCommit)
|
||||
if err != nil {
|
||||
if err != nil && !c.conn.GetAutocommit() {
|
||||
c.Rollback()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c conn) Rollback() error {
|
||||
return c.conn.Exec(`ROLLBACK`)
|
||||
return c.conn.Exec(c.txRollback)
|
||||
}
|
||||
|
||||
func (c conn) Prepare(query string) (driver.Stmt, error) {
|
||||
@@ -189,7 +213,7 @@ func (c conn) ExecContext(ctx context.Context, query string, args []driver.Named
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c conn) Savepoint() (release func(*error)) {
|
||||
func (c conn) Savepoint() sqlite3.Savepoint {
|
||||
return c.conn.Savepoint()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// Package driver provides a database/sql driver for SQLite.
|
||||
package driver
|
||||
|
||||
import (
|
||||
@@ -134,7 +133,9 @@ 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"))
|
||||
db, err := sql.Open("sqlite3", "file:"+
|
||||
filepath.ToSlash(filepath.Join(t.TempDir(), "test.db"))+
|
||||
"?_txlock=exclusive&_pragma=busy_timeout(0)")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -145,6 +146,16 @@ func Test_BeginTx(t *testing.T) {
|
||||
t.Error("want isolationErr")
|
||||
}
|
||||
|
||||
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadUncommitted})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = tx.Rollback()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tx1, err := db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -48,7 +48,8 @@ func ExampleDriverConn() {
|
||||
|
||||
err = conn.Raw(func(driverConn any) error {
|
||||
conn := driverConn.(sqlite3.DriverConn)
|
||||
defer conn.Savepoint()(&err)
|
||||
savept := conn.Savepoint()
|
||||
defer savept.Release(&err)
|
||||
|
||||
blob, err := conn.OpenBlob("main", "test", "col", id, true)
|
||||
if err != nil {
|
||||
|
||||
15
embed/README.md
Normal file
15
embed/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Embeddable WASM build of SQLite
|
||||
|
||||
This folder includes an embeddable WASM build of SQLite 3.41.0 for use with
|
||||
[`github.com/ncruces/go-sqlite3`](https://pkg.go.dev/github.com/ncruces/go-sqlite3).
|
||||
|
||||
The following optional features are compiled in:
|
||||
- math functions
|
||||
- FTS3/4/5
|
||||
- JSON
|
||||
- R*Tree
|
||||
- GeoPoly
|
||||
|
||||
See the [configuration options](../sqlite3/sqlite_cfg.h).
|
||||
|
||||
Built using [`zig`](https://ziglang.org/) version 0.10.1.
|
||||
@@ -13,4 +13,4 @@ zig cc --target=wasm32-wasi -flto -g0 -Os \
|
||||
-mbulk-memory -mreference-types \
|
||||
-mnontrapping-fptoint -msign-ext \
|
||||
-D_HAVE_SQLITE_CONFIG_H \
|
||||
$(awk '{print "-Wl,--export="$0}' ../sqlite3/exports.txt)
|
||||
$(awk '{print "-Wl,--export="$0}' exports.txt)
|
||||
@@ -46,4 +46,5 @@ sqlite3_backup_step
|
||||
sqlite3_backup_finish
|
||||
sqlite3_backup_remaining
|
||||
sqlite3_backup_pagecount
|
||||
sqlite3_time_collation
|
||||
sqlite3_interrupt_offset
|
||||
@@ -4,9 +4,6 @@
|
||||
// with an appropriate build of SQLite:
|
||||
//
|
||||
// import _ "github.com/ncruces/go-sqlite3/embed"
|
||||
//
|
||||
// You can obtain this build of SQLite from:
|
||||
// https://github.com/ncruces/go-sqlite3/tree/main/embed
|
||||
package embed
|
||||
|
||||
import (
|
||||
|
||||
Binary file not shown.
2
error.go
2
error.go
@@ -202,8 +202,6 @@ const (
|
||||
noFuncErr = errorString("sqlite3: could not find function: ")
|
||||
binaryErr = errorString("sqlite3: no SQLite binary embed/set/loaded")
|
||||
timeErr = errorString("sqlite3: invalid time value")
|
||||
emptyErr = errorString("sqlite3: empty statement")
|
||||
tailErr = errorString("sqlite3: non-empty tail")
|
||||
notImplErr = errorString("sqlite3: not implemented")
|
||||
whenceErr = errorString("sqlite3: invalid whence")
|
||||
offsetErr = errorString("sqlite3: invalid offset")
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package sqlite3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -9,7 +8,7 @@ import (
|
||||
|
||||
func Test_assertErr(t *testing.T) {
|
||||
err := assertErr()
|
||||
if s := err.Error(); !strings.HasPrefix(s, "sqlite3: assertion failed") || !strings.HasSuffix(s, "error_test.go:11)") {
|
||||
if s := err.Error(); !strings.HasPrefix(s, "sqlite3: assertion failed") || !strings.HasSuffix(s, "error_test.go:10)") {
|
||||
t.Errorf("got %q", s)
|
||||
}
|
||||
}
|
||||
@@ -120,10 +119,8 @@ func Test_ErrorCode_Error(t *testing.T) {
|
||||
// Test all error codes.
|
||||
for i := 0; i == int(ErrorCode(i)); i++ {
|
||||
want := "sqlite3: "
|
||||
r, _ := db.api.errstr.Call(context.TODO(), uint64(i))
|
||||
if r != nil {
|
||||
want += db.mem.readString(uint32(r[0]), _MAX_STRING)
|
||||
}
|
||||
r := db.call(db.api.errstr, uint64(i))
|
||||
want += db.mem.readString(uint32(r[0]), _MAX_STRING)
|
||||
|
||||
got := ErrorCode(i).Error()
|
||||
if got != want {
|
||||
@@ -144,10 +141,8 @@ func Test_ExtendedErrorCode_Error(t *testing.T) {
|
||||
// Test all extended error codes.
|
||||
for i := 0; i == int(ExtendedErrorCode(i)); i++ {
|
||||
want := "sqlite3: "
|
||||
r, _ := db.api.errstr.Call(context.TODO(), uint64(i))
|
||||
if r != nil {
|
||||
want += db.mem.readString(uint32(r[0]), _MAX_STRING)
|
||||
}
|
||||
r := db.call(db.api.errstr, uint64(i))
|
||||
want += db.mem.readString(uint32(r[0]), _MAX_STRING)
|
||||
|
||||
got := ExtendedErrorCode(i).Error()
|
||||
if got != want {
|
||||
|
||||
@@ -26,7 +26,11 @@ func Example() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
stmt := db.MustPrepare(`SELECT id, name FROM users`)
|
||||
stmt, _, err := db.Prepare(`SELECT id, name FROM users`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
for stmt.Step() {
|
||||
fmt.Println(stmt.ColumnInt(0), stmt.ColumnText(1))
|
||||
|
||||
@@ -155,6 +155,7 @@ func newModule(mod api.Module) (m *module, err error) {
|
||||
backupFinish: getFun("sqlite3_backup_finish"),
|
||||
backupRemaining: getFun("sqlite3_backup_remaining"),
|
||||
backupPageCount: getFun("sqlite3_backup_pagecount"),
|
||||
timeCollation: getFun("sqlite3_time_collation"),
|
||||
interrupt: getVal("sqlite3_interrupt_offset"),
|
||||
}
|
||||
if err != nil {
|
||||
@@ -348,5 +349,6 @@ type sqliteAPI struct {
|
||||
backupFinish api.Function
|
||||
backupRemaining api.Function
|
||||
backupPageCount api.Function
|
||||
timeCollation api.Function
|
||||
interrupt uint32
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include "main.c"
|
||||
#include "os.c"
|
||||
#include "qsort.c"
|
||||
#include "time.c"
|
||||
|
||||
#include "sqlite3.c"
|
||||
|
||||
sqlite3_destructor_type malloc_destructor = &free;
|
||||
|
||||
28
sqlite3/time.c
Normal file
28
sqlite3/time.c
Normal file
@@ -0,0 +1,28 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "sqlite3.h"
|
||||
|
||||
static int time_collation(void *pArg, int nKey1, const void *pKey1, int nKey2,
|
||||
const void *pKey2) {
|
||||
// If keys are of different length, and both terminated by a Z,
|
||||
// ignore the Z for collation purposes.
|
||||
if (nKey1 && nKey2 && nKey1 != nKey2) {
|
||||
const char *pK1 = (const char *)pKey1;
|
||||
const char *pK2 = (const char *)pKey2;
|
||||
if (pK1[nKey1 - 1] == 'Z' && pK2[nKey2 - 1] == 'Z') {
|
||||
nKey1--;
|
||||
nKey2--;
|
||||
}
|
||||
}
|
||||
|
||||
int n = nKey1 < nKey2 ? nKey1 : nKey2;
|
||||
int rc = memcmp(pKey1, pKey2, n);
|
||||
if (rc == 0) {
|
||||
rc = nKey1 - nKey2;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
int sqlite3_time_collation(sqlite3 *db) {
|
||||
return sqlite3_create_collation(db, "TIME", SQLITE_UTF8, 0, time_collation);
|
||||
}
|
||||
@@ -202,59 +202,3 @@ func TestConn_Prepare_invalid(t *testing.T) {
|
||||
t.Error("got message:", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConn_MustPrepare_empty(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
defer func() { _ = recover() }()
|
||||
_ = db.MustPrepare(``)
|
||||
t.Error("want panic")
|
||||
}
|
||||
|
||||
func TestConn_MustPrepare_tail(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
defer func() { _ = recover() }()
|
||||
_ = db.MustPrepare(`SELECT 1; -- HERE`)
|
||||
t.Error("want panic")
|
||||
}
|
||||
|
||||
func TestConn_MustPrepare_invalid(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
defer func() { _ = recover() }()
|
||||
_ = db.MustPrepare(`SELECT`)
|
||||
t.Error("want panic")
|
||||
}
|
||||
|
||||
func TestConn_Pragma(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
defer func() { _ = recover() }()
|
||||
_ = db.Pragma("encoding=''")
|
||||
t.Error("want panic")
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ 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))
|
||||
refnodate := time.Date(2000, 01, 1, 4, 23, 19, 120_000_000, time.FixedZone("", -4*3600))
|
||||
|
||||
tests := []struct {
|
||||
fmt sqlite3.TimeFormat
|
||||
@@ -89,14 +89,14 @@ func TestTimeFormat_Decode(t *testing.T) {
|
||||
{sqlite3.TimeFormatAuto, "1381134199120000", reference, 0, false},
|
||||
{sqlite3.TimeFormatAuto, "1381134199120000000", reference, 0, false},
|
||||
{sqlite3.TimeFormatAuto, "2013-10-07 04:23:19.12-04:00", reference, 0, false},
|
||||
{sqlite3.TimeFormatAuto, "04:23:19.12-04:00", reftime, 0, false},
|
||||
{sqlite3.TimeFormatAuto, "04:23:19.12-04:00", refnodate, 0, false},
|
||||
{sqlite3.TimeFormatAuto, "abc", time.Time{}, 0, true},
|
||||
{sqlite3.TimeFormatAuto, false, time.Time{}, 0, true},
|
||||
|
||||
{sqlite3.TimeFormat3, "2013-10-07 04:23:19.12-04:00", reference, 0, false},
|
||||
{sqlite3.TimeFormat3, "2013-10-07 08:23:19.12", reference, 0, false},
|
||||
{sqlite3.TimeFormat9, "04:23:19.12-04:00", reftime, 0, false},
|
||||
{sqlite3.TimeFormat9, "08:23:19.12", reftime, 0, false},
|
||||
{sqlite3.TimeFormat9, "04:23:19.12-04:00", refnodate, 0, false},
|
||||
{sqlite3.TimeFormat9, "08:23:19.12", refnodate, 0, false},
|
||||
{sqlite3.TimeFormat3, false, time.Time{}, 0, true},
|
||||
{sqlite3.TimeFormat9, false, time.Time{}, 0, true},
|
||||
|
||||
@@ -118,3 +118,52 @@ func TestTimeFormat_Decode(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDB_timeCollation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Exec(`CREATE TABLE IF NOT EXISTS times (tstamp COLLATE TIME)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, _, err := db.Prepare(`INSERT INTO times VALUES (?), (?), (?)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
stmt.BindTime(1, time.Unix(0, 0).UTC(), sqlite3.TimeFormatDefault)
|
||||
stmt.BindTime(2, time.Unix(0, -1).UTC(), sqlite3.TimeFormatDefault)
|
||||
stmt.BindTime(3, time.Unix(0, +1).UTC(), sqlite3.TimeFormatDefault)
|
||||
stmt.Step()
|
||||
|
||||
err = stmt.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, _, err = db.Prepare(`SELECT tstamp FROM times ORDER BY tstamp`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var t0 time.Time
|
||||
for stmt.Step() {
|
||||
t1 := stmt.ColumnTime(0, sqlite3.TimeFormatAuto)
|
||||
if t0.After(t1) {
|
||||
t.Errorf("got %v after %v", t0, t1)
|
||||
}
|
||||
t0 = t1
|
||||
}
|
||||
err = stmt.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,10 +185,10 @@ func TestConn_Transaction_interrupt(t *testing.T) {
|
||||
t.Errorf("got %v, want sqlite3.INTERRUPT", err)
|
||||
}
|
||||
|
||||
var nilErr error
|
||||
tx.End(&nilErr)
|
||||
if !errors.Is(nilErr, sqlite3.INTERRUPT) {
|
||||
t.Errorf("got %v, want sqlite3.INTERRUPT", nilErr)
|
||||
err = nil
|
||||
tx.End(&err)
|
||||
if !errors.Is(err, sqlite3.INTERRUPT) {
|
||||
t.Errorf("got %v, want sqlite3.INTERRUPT", err)
|
||||
}
|
||||
|
||||
db.SetInterrupt(context.Background())
|
||||
@@ -210,6 +210,33 @@ func TestConn_Transaction_interrupt(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConn_Transaction_interrupted(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
db.SetInterrupt(ctx)
|
||||
cancel()
|
||||
|
||||
tx := db.Begin()
|
||||
|
||||
err = tx.Commit()
|
||||
if !errors.Is(err, sqlite3.INTERRUPT) {
|
||||
t.Errorf("got %v, want sqlite3.INTERRUPT", err)
|
||||
}
|
||||
|
||||
err = nil
|
||||
tx.End(&err)
|
||||
if !errors.Is(err, sqlite3.INTERRUPT) {
|
||||
t.Errorf("got %v, want sqlite3.INTERRUPT", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConn_Transaction_rollback(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -286,7 +313,7 @@ func TestConn_Savepoint_exec(t *testing.T) {
|
||||
}
|
||||
|
||||
insert := func(succeed bool) (err error) {
|
||||
defer db.Savepoint()(&err)
|
||||
defer db.Savepoint().Release(&err)
|
||||
|
||||
err = db.Exec(`INSERT INTO test VALUES ('hello')`)
|
||||
if err != nil {
|
||||
@@ -344,7 +371,7 @@ func TestConn_Savepoint_panic(t *testing.T) {
|
||||
}
|
||||
|
||||
panics := func() (err error) {
|
||||
defer db.Savepoint()(&err)
|
||||
defer db.Savepoint().Release(&err)
|
||||
|
||||
err = db.Exec(`INSERT INTO test VALUES ('hello')`)
|
||||
if err != nil {
|
||||
@@ -395,12 +422,12 @@ func TestConn_Savepoint_interrupt(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
release := db.Savepoint()
|
||||
savept := db.Savepoint()
|
||||
err = db.Exec(`INSERT INTO test VALUES (1)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
release(&err)
|
||||
savept.Release(&err)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -408,19 +435,19 @@ func TestConn_Savepoint_interrupt(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
db.SetInterrupt(ctx)
|
||||
|
||||
release1 := db.Savepoint()
|
||||
savept1 := db.Savepoint()
|
||||
err = db.Exec(`INSERT INTO test VALUES (2)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
release2 := db.Savepoint()
|
||||
savept2 := db.Savepoint()
|
||||
err = db.Exec(`INSERT INTO test VALUES (3)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cancel()
|
||||
db.Savepoint()(&err)
|
||||
db.Savepoint().Release(&err)
|
||||
if !errors.Is(err, sqlite3.INTERRUPT) {
|
||||
t.Errorf("got %v, want sqlite3.INTERRUPT", err)
|
||||
}
|
||||
@@ -431,15 +458,15 @@ func TestConn_Savepoint_interrupt(t *testing.T) {
|
||||
}
|
||||
|
||||
err = context.Canceled
|
||||
release2(&err)
|
||||
savept2.Release(&err)
|
||||
if err != context.Canceled {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var nilErr error
|
||||
release1(&nilErr)
|
||||
if !errors.Is(nilErr, sqlite3.INTERRUPT) {
|
||||
t.Errorf("got %v, want sqlite3.INTERRUPT", nilErr)
|
||||
err = nil
|
||||
savept1.Release(&err)
|
||||
if !errors.Is(err, sqlite3.INTERRUPT) {
|
||||
t.Errorf("got %v, want sqlite3.INTERRUPT", err)
|
||||
}
|
||||
|
||||
db.SetInterrupt(context.Background())
|
||||
@@ -475,7 +502,7 @@ func TestConn_Savepoint_rollback(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
release := db.Savepoint()
|
||||
savept := db.Savepoint()
|
||||
err = db.Exec(`INSERT INTO test VALUES (1)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -484,7 +511,7 @@ func TestConn_Savepoint_rollback(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
release(&err)
|
||||
savept.Release(&err)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
19
time.go
19
time.go
@@ -62,13 +62,18 @@ const (
|
||||
// [TimeFormatDefault] and [TimeFormatAuto] encode using [time.RFC3339Nano],
|
||||
// with nanosecond accuracy, and preserving any timezone offset.
|
||||
//
|
||||
// This is the format used by the database/sql driver:
|
||||
// [database/sql.Row.Scan] is able to decode as [time.Time]
|
||||
// This is the format used by the [database/sql] driver:
|
||||
// [database/sql.Row.Scan] will decode as [time.Time]
|
||||
// values encoded with [time.RFC3339Nano].
|
||||
//
|
||||
// Time values encoded with [time.RFC3339Nano] cannot be sorted as strings
|
||||
// to produce a time-ordered sequence.
|
||||
// Use [TimeFormat7] for time-ordered encoding.
|
||||
//
|
||||
// Assuming that the time zones of the time values are the same (e.g., all in UTC),
|
||||
// and expressed using the same string (e.g., all "Z" or all "+00:00"),
|
||||
// use the TIME [collating sequence] to produce a time-ordered sequence.
|
||||
//
|
||||
// Otherwise, use [TimeFormat7] for time-ordered encoding.
|
||||
//
|
||||
// Formats [TimeFormat1] through [TimeFormat10]
|
||||
// convert time values to UTC before encoding.
|
||||
@@ -78,6 +83,8 @@ const (
|
||||
// or an int64 for the other numeric formats.
|
||||
//
|
||||
// https://www.sqlite.org/lang_datefunc.html
|
||||
//
|
||||
// [collating sequence]: https://www.sqlite.org/datatype3.html#collating_sequences
|
||||
func (f TimeFormat) Encode(t time.Time) any {
|
||||
switch f {
|
||||
// Numeric formats
|
||||
@@ -123,9 +130,9 @@ func (f TimeFormat) Encode(t time.Time) any {
|
||||
// [TimeFormatAuto] implements (and extends) the SQLite auto modifier.
|
||||
// Julian day numbers are 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.
|
||||
// Unix timestamps before 1980 may be misinterpreted as julian day numbers,
|
||||
// Unix timestamps (expressed in seconds, milliseconds, microseconds, or nanoseconds)
|
||||
// are safe to use for current events, from at least 1980 through at least 2260.
|
||||
// Unix timestamps before 1980 and after 9999 may be misinterpreted as julian day numbers,
|
||||
// or have the wrong time unit.
|
||||
//
|
||||
// https://www.sqlite.org/lang_datefunc.html
|
||||
|
||||
156
tx.go
156
tx.go
@@ -4,10 +4,14 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"runtime"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Tx is an in-progress database transaction.
|
||||
//
|
||||
// https://www.sqlite.org/lang_transaction.html
|
||||
type Tx struct {
|
||||
c *Conn
|
||||
}
|
||||
@@ -16,8 +20,9 @@ type Tx struct {
|
||||
//
|
||||
// https://www.sqlite.org/lang_transaction.html
|
||||
func (c *Conn) Begin() Tx {
|
||||
err := c.Exec(`BEGIN DEFERRED`)
|
||||
if err != nil && !errors.Is(err, INTERRUPT) {
|
||||
// BEGIN even if interrupted.
|
||||
err := c.txExecInterrupted(`BEGIN DEFERRED`)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return Tx{c}
|
||||
@@ -64,21 +69,22 @@ func (tx Tx) End(errp *error) {
|
||||
defer panic(recovered)
|
||||
}
|
||||
|
||||
if tx.c.GetAutocommit() {
|
||||
// There is nothing to commit/rollback.
|
||||
return
|
||||
}
|
||||
|
||||
if *errp == nil && recovered == nil {
|
||||
if (errp == nil || *errp == nil) && recovered == nil {
|
||||
// Success path.
|
||||
if tx.c.GetAutocommit() { // There is nothing to commit.
|
||||
return
|
||||
}
|
||||
*errp = tx.Commit()
|
||||
if *errp == nil {
|
||||
return
|
||||
}
|
||||
// Possible interrupt, fall through to the error path.
|
||||
// Fall through to the error path.
|
||||
}
|
||||
|
||||
// Error path.
|
||||
if tx.c.GetAutocommit() { // There is nothing to rollback.
|
||||
return
|
||||
}
|
||||
err := tx.Rollback()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -92,33 +98,28 @@ func (tx Tx) Commit() error {
|
||||
return tx.c.Exec(`COMMIT`)
|
||||
}
|
||||
|
||||
// Rollback rollsback the transaction.
|
||||
// Rollback rolls back the transaction,
|
||||
// even if the connection has been interrupted.
|
||||
//
|
||||
// https://www.sqlite.org/lang_transaction.html
|
||||
func (tx Tx) Rollback() error {
|
||||
// ROLLBACK even if the connection has been interrupted.
|
||||
old := tx.c.SetInterrupt(context.Background())
|
||||
defer tx.c.SetInterrupt(old)
|
||||
return tx.c.Exec(`ROLLBACK`)
|
||||
return tx.c.txExecInterrupted(`ROLLBACK`)
|
||||
}
|
||||
|
||||
// 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
|
||||
// }
|
||||
// Savepoint is a marker within a transaction
|
||||
// that allows for partial rollback.
|
||||
//
|
||||
// https://www.sqlite.org/lang_savepoint.html
|
||||
func (c *Conn) Savepoint() (release func(*error)) {
|
||||
name := "sqlite3.Savepoint" // names can be reused
|
||||
type Savepoint struct {
|
||||
c *Conn
|
||||
name string
|
||||
}
|
||||
|
||||
// Savepoint establishes a new transaction savepoint.
|
||||
//
|
||||
// https://www.sqlite.org/lang_savepoint.html
|
||||
func (c *Conn) Savepoint() Savepoint {
|
||||
name := "sqlite3.Savepoint"
|
||||
var pc [1]uintptr
|
||||
if n := runtime.Callers(2, pc[:]); n > 0 {
|
||||
frames := runtime.CallersFrames(pc[:n])
|
||||
@@ -127,52 +128,75 @@ func (c *Conn) Savepoint() (release func(*error)) {
|
||||
name = frame.Function
|
||||
}
|
||||
}
|
||||
// Names can be reused; this makes catching bugs more likely.
|
||||
name += "#" + strconv.Itoa(int(rand.Int31()))
|
||||
|
||||
err := c.Exec(fmt.Sprintf("SAVEPOINT %q;", name))
|
||||
err := c.txExecInterrupted(fmt.Sprintf("SAVEPOINT %q;", name))
|
||||
if err != nil {
|
||||
if errors.Is(err, INTERRUPT) {
|
||||
return func(errp *error) {
|
||||
if *errp == nil {
|
||||
*errp = err
|
||||
}
|
||||
}
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
return Savepoint{c: c, name: name}
|
||||
}
|
||||
|
||||
return func(errp *error) {
|
||||
recovered := recover()
|
||||
if recovered != nil {
|
||||
defer panic(recovered)
|
||||
}
|
||||
// Release releases the savepoint rolling back any changes
|
||||
// if *error points to a non-nil error.
|
||||
//
|
||||
// This is meant to be deferred:
|
||||
//
|
||||
// func doWork(conn *sqlite3.Conn) (err error) {
|
||||
// savept := conn.Savepoint()
|
||||
// defer savept.Release(&err)
|
||||
//
|
||||
// // ... do work in the transaction
|
||||
// }
|
||||
func (s Savepoint) Release(errp *error) {
|
||||
recovered := recover()
|
||||
if recovered != nil {
|
||||
defer panic(recovered)
|
||||
}
|
||||
|
||||
if c.GetAutocommit() {
|
||||
// There is nothing to commit/rollback.
|
||||
if (errp == nil || *errp == nil) && recovered == nil {
|
||||
// Success path.
|
||||
if s.c.GetAutocommit() { // There is nothing to commit.
|
||||
return
|
||||
}
|
||||
|
||||
if *errp == nil && recovered == nil {
|
||||
// Success path.
|
||||
// RELEASE the savepoint successfully.
|
||||
*errp = c.Exec(fmt.Sprintf("RELEASE %q;", name))
|
||||
if *errp == nil {
|
||||
return
|
||||
}
|
||||
// Possible interrupt, fall through to the error path.
|
||||
*errp = s.c.Exec(fmt.Sprintf("RELEASE %q;", s.name))
|
||||
if *errp == nil {
|
||||
return
|
||||
}
|
||||
// Fall through to the error path.
|
||||
}
|
||||
|
||||
// Error path.
|
||||
// Always ROLLBACK even if the connection has been interrupted.
|
||||
old := c.SetInterrupt(context.Background())
|
||||
defer c.SetInterrupt(old)
|
||||
|
||||
err := c.Exec(fmt.Sprintf("ROLLBACK TO %q;", name))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = c.Exec(fmt.Sprintf("RELEASE %q;", name))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Error path.
|
||||
if s.c.GetAutocommit() { // There is nothing to rollback.
|
||||
return
|
||||
}
|
||||
// ROLLBACK and RELEASE even if interrupted.
|
||||
err := s.c.txExecInterrupted(fmt.Sprintf(`
|
||||
ROLLBACK TO %[1]q;
|
||||
RELEASE %[1]q;
|
||||
`, s.name))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Rollback rolls the transaction back to the savepoint,
|
||||
// even if the connection has been interrupted.
|
||||
// Rollback does not release the savepoint.
|
||||
//
|
||||
// https://www.sqlite.org/lang_transaction.html
|
||||
func (s Savepoint) Rollback() error {
|
||||
// ROLLBACK even if interrupted.
|
||||
return s.c.txExecInterrupted(fmt.Sprintf("ROLLBACK TO %q;", s.name))
|
||||
}
|
||||
|
||||
func (c *Conn) txExecInterrupted(sql string) error {
|
||||
err := c.Exec(sql)
|
||||
if errors.Is(err, INTERRUPT) {
|
||||
old := c.SetInterrupt(context.Background())
|
||||
defer c.SetInterrupt(old)
|
||||
err = c.Exec(sql)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
68
vfs.go
68
vfs.go
@@ -9,7 +9,6 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/ncruces/julianday"
|
||||
@@ -166,66 +165,34 @@ func vfsDelete(ctx context.Context, mod api.Module, pVfs, zPath, syncDir uint32)
|
||||
path := memory{mod}.readString(zPath, _MAX_PATHNAME)
|
||||
err := os.Remove(path)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return _OK
|
||||
return uint32(IOERR_DELETE_NOENT)
|
||||
}
|
||||
if err != nil {
|
||||
return uint32(IOERR_DELETE)
|
||||
}
|
||||
if runtime.GOOS != "windows" && syncDir != 0 {
|
||||
f, err := os.Open(filepath.Dir(path))
|
||||
if err == nil {
|
||||
err = f.Sync()
|
||||
f.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return uint32(IOERR_DELETE)
|
||||
return _OK
|
||||
}
|
||||
defer f.Close()
|
||||
err = vfsOS.Sync(f, false, false)
|
||||
if err != nil {
|
||||
return uint32(IOERR_DIR_FSYNC)
|
||||
}
|
||||
}
|
||||
return _OK
|
||||
}
|
||||
|
||||
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).
|
||||
|
||||
path := memory{mod}.readString(zPath, _MAX_PATHNAME)
|
||||
fi, err := os.Stat(path)
|
||||
|
||||
ok, rc := vfsOS.Access(path, flags)
|
||||
var res uint32
|
||||
switch {
|
||||
case flags == _ACCESS_EXISTS:
|
||||
switch {
|
||||
case err == nil:
|
||||
res = 1
|
||||
case errors.Is(err, fs.ErrNotExist):
|
||||
res = 0
|
||||
default:
|
||||
return uint32(IOERR_ACCESS)
|
||||
}
|
||||
|
||||
case err == nil:
|
||||
var want fs.FileMode = syscall.S_IRUSR
|
||||
if flags == _ACCESS_READWRITE {
|
||||
want |= syscall.S_IWUSR
|
||||
}
|
||||
if fi.IsDir() {
|
||||
want |= syscall.S_IXUSR
|
||||
}
|
||||
if fi.Mode()&want == want {
|
||||
res = 1
|
||||
} else {
|
||||
res = 0
|
||||
}
|
||||
|
||||
case errors.Is(err, fs.ErrPermission):
|
||||
res = 0
|
||||
|
||||
default:
|
||||
return uint32(IOERR_ACCESS)
|
||||
if ok {
|
||||
res = 1
|
||||
}
|
||||
|
||||
memory{mod}.writeUint32(pResOut, res)
|
||||
return _OK
|
||||
return uint32(rc)
|
||||
}
|
||||
|
||||
func vfsOpen(ctx context.Context, mod api.Module, pVfs, zName, pFile uint32, flags OpenFlag, pOutFlags uint32) uint32 {
|
||||
@@ -249,14 +216,14 @@ func vfsOpen(ctx context.Context, mod api.Module, pVfs, zName, pFile uint32, fla
|
||||
file, err = os.CreateTemp("", "*.db")
|
||||
} else {
|
||||
name := memory{mod}.readString(zName, _MAX_PATHNAME)
|
||||
file, err = os.OpenFile(name, oflags, 0600)
|
||||
file, err = vfsOS.OpenFile(name, oflags, 0600)
|
||||
}
|
||||
if err != nil {
|
||||
return uint32(CANTOPEN)
|
||||
}
|
||||
|
||||
if flags&OPEN_DELETEONCLOSE != 0 {
|
||||
vfsOS.DeleteOnClose(file)
|
||||
os.Remove(file.Name())
|
||||
}
|
||||
|
||||
vfsFile.Open(ctx, mod, pFile, file)
|
||||
@@ -312,9 +279,11 @@ func vfsTruncate(ctx context.Context, mod api.Module, pFile uint32, nByte uint64
|
||||
return _OK
|
||||
}
|
||||
|
||||
func vfsSync(ctx context.Context, mod api.Module, pFile, flags uint32) uint32 {
|
||||
func vfsSync(ctx context.Context, mod api.Module, pFile uint32, flags _SyncFlag) uint32 {
|
||||
dataonly := (flags & _SYNC_DATAONLY) != 0
|
||||
fullsync := (flags & 0x0f) == _SYNC_FULL
|
||||
file := vfsFile.GetOS(ctx, mod, pFile)
|
||||
err := file.Sync()
|
||||
err := vfsOS.Sync(file, fullsync, dataonly)
|
||||
if err != nil {
|
||||
return uint32(IOERR_FSYNC)
|
||||
}
|
||||
@@ -322,9 +291,6 @@ func vfsSync(ctx context.Context, mod api.Module, pFile, flags uint32) uint32 {
|
||||
}
|
||||
|
||||
func vfsFileSize(ctx context.Context, mod api.Module, pFile, pSize uint32) uint32 {
|
||||
// This uses [os.File.Seek] because we don't care about the offset for reading/writing.
|
||||
// But consider using [os.File.Stat] instead (as other VFSes do).
|
||||
|
||||
file := vfsFile.GetOS(ctx, mod, pFile)
|
||||
off, err := file.Seek(0, io.SeekEnd)
|
||||
if err != nil {
|
||||
|
||||
@@ -165,7 +165,7 @@ func Test_vfsDelete(t *testing.T) {
|
||||
}
|
||||
|
||||
rc = vfsDelete(ctx, mem.mod, 0, 4, 1)
|
||||
if rc != _OK {
|
||||
if rc != uint32(IOERR_DELETE_NOENT) {
|
||||
t.Fatal("returned", rc)
|
||||
}
|
||||
}
|
||||
|
||||
95
vfs_unix.go
95
vfs_unix.go
@@ -3,13 +3,46 @@
|
||||
package sqlite3
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"runtime"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func (vfsOSMethods) DeleteOnClose(file *os.File) {
|
||||
_ = os.Remove(file.Name())
|
||||
func (vfsOSMethods) OpenFile(name string, flag int, perm fs.FileMode) (*os.File, error) {
|
||||
return os.OpenFile(name, flag, perm)
|
||||
}
|
||||
|
||||
func (vfsOSMethods) Access(path string, flags _AccessFlag) (bool, xErrorCode) {
|
||||
var access uint32 = unix.F_OK
|
||||
switch flags {
|
||||
case _ACCESS_READWRITE:
|
||||
access = unix.R_OK | unix.W_OK
|
||||
case _ACCESS_READ:
|
||||
access = unix.R_OK
|
||||
}
|
||||
|
||||
err := unix.Access(path, access)
|
||||
if err == nil {
|
||||
return true, _OK
|
||||
}
|
||||
return false, _OK
|
||||
}
|
||||
|
||||
func (vfsOSMethods) Sync(file *os.File, fullsync, dataonly bool) error {
|
||||
if runtime.GOOS == "darwin" && !fullsync {
|
||||
return unix.Fsync(int(file.Fd()))
|
||||
}
|
||||
if runtime.GOOS == "linux" && dataonly {
|
||||
//lint:ignore SA1019 OK on linux
|
||||
_, _, err := unix.Syscall(unix.SYS_FDATASYNC, file.Fd(), 0, 0)
|
||||
if err != 0 {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return file.Sync()
|
||||
}
|
||||
|
||||
func (vfsOSMethods) GetExclusiveLock(file *os.File) xErrorCode {
|
||||
@@ -39,8 +72,8 @@ func (vfsOSMethods) ReleaseLock(file *os.File, _ vfsLockState) xErrorCode {
|
||||
}
|
||||
|
||||
func (vfsOSMethods) unlock(file *os.File, start, len int64) xErrorCode {
|
||||
err := vfsOS.fcntlSetLock(file, &syscall.Flock_t{
|
||||
Type: syscall.F_UNLCK,
|
||||
err := vfsOS.fcntlSetLock(file, &unix.Flock_t{
|
||||
Type: unix.F_UNLCK,
|
||||
Start: start,
|
||||
Len: len,
|
||||
})
|
||||
@@ -51,85 +84,85 @@ func (vfsOSMethods) unlock(file *os.File, start, len int64) xErrorCode {
|
||||
}
|
||||
|
||||
func (vfsOSMethods) readLock(file *os.File, start, len int64) xErrorCode {
|
||||
return vfsOS.lockErrorCode(vfsOS.fcntlSetLock(file, &syscall.Flock_t{
|
||||
Type: syscall.F_RDLCK,
|
||||
return vfsOS.lockErrorCode(vfsOS.fcntlSetLock(file, &unix.Flock_t{
|
||||
Type: unix.F_RDLCK,
|
||||
Start: start,
|
||||
Len: len,
|
||||
}), IOERR_RDLOCK)
|
||||
}
|
||||
|
||||
func (vfsOSMethods) writeLock(file *os.File, start, len int64) xErrorCode {
|
||||
return vfsOS.lockErrorCode(vfsOS.fcntlSetLock(file, &syscall.Flock_t{
|
||||
Type: syscall.F_WRLCK,
|
||||
return vfsOS.lockErrorCode(vfsOS.fcntlSetLock(file, &unix.Flock_t{
|
||||
Type: unix.F_WRLCK,
|
||||
Start: start,
|
||||
Len: len,
|
||||
}), IOERR_LOCK)
|
||||
}
|
||||
|
||||
func (vfsOSMethods) checkLock(file *os.File, start, len int64) (bool, xErrorCode) {
|
||||
lock := syscall.Flock_t{
|
||||
Type: syscall.F_RDLCK,
|
||||
lock := unix.Flock_t{
|
||||
Type: unix.F_RDLCK,
|
||||
Start: start,
|
||||
Len: len,
|
||||
}
|
||||
if vfsOS.fcntlGetLock(file, &lock) != nil {
|
||||
return false, IOERR_CHECKRESERVEDLOCK
|
||||
}
|
||||
return lock.Type != syscall.F_UNLCK, _OK
|
||||
return lock.Type != unix.F_UNLCK, _OK
|
||||
}
|
||||
|
||||
func (vfsOSMethods) fcntlGetLock(file *os.File, lock *syscall.Flock_t) error {
|
||||
func (vfsOSMethods) fcntlGetLock(file *os.File, lock *unix.Flock_t) error {
|
||||
var F_OFD_GETLK int
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
// https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/fcntl.h
|
||||
F_OFD_GETLK = 36 // F_OFD_GETLK
|
||||
F_OFD_GETLK = 36
|
||||
case "darwin":
|
||||
// https://github.com/apple/darwin-xnu/blob/main/bsd/sys/fcntl.h
|
||||
F_OFD_GETLK = 92 // F_OFD_GETLK
|
||||
F_OFD_GETLK = 92
|
||||
case "illumos":
|
||||
// https://github.com/illumos/illumos-gate/blob/master/usr/src/uts/common/sys/fcntl.h
|
||||
F_OFD_GETLK = 47 // F_OFD_GETLK
|
||||
F_OFD_GETLK = 47
|
||||
default:
|
||||
return notImplErr
|
||||
}
|
||||
return syscall.FcntlFlock(file.Fd(), F_OFD_GETLK, lock)
|
||||
return unix.FcntlFlock(file.Fd(), F_OFD_GETLK, lock)
|
||||
}
|
||||
|
||||
func (vfsOSMethods) fcntlSetLock(file *os.File, lock *syscall.Flock_t) error {
|
||||
func (vfsOSMethods) fcntlSetLock(file *os.File, lock *unix.Flock_t) error {
|
||||
var F_OFD_SETLK int
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
// https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/fcntl.h
|
||||
F_OFD_SETLK = 37 // F_OFD_SETLK
|
||||
F_OFD_SETLK = 37
|
||||
case "darwin":
|
||||
// https://github.com/apple/darwin-xnu/blob/main/bsd/sys/fcntl.h
|
||||
F_OFD_SETLK = 90 // F_OFD_SETLK
|
||||
F_OFD_SETLK = 90
|
||||
case "illumos":
|
||||
// https://github.com/illumos/illumos-gate/blob/master/usr/src/uts/common/sys/fcntl.h
|
||||
F_OFD_SETLK = 48 // F_OFD_SETLK
|
||||
F_OFD_SETLK = 48
|
||||
default:
|
||||
return notImplErr
|
||||
}
|
||||
return syscall.FcntlFlock(file.Fd(), F_OFD_SETLK, lock)
|
||||
return unix.FcntlFlock(file.Fd(), F_OFD_SETLK, lock)
|
||||
}
|
||||
|
||||
func (vfsOSMethods) lockErrorCode(err error, def xErrorCode) xErrorCode {
|
||||
if err == nil {
|
||||
return _OK
|
||||
}
|
||||
if errno, ok := err.(syscall.Errno); ok {
|
||||
if errno, ok := err.(unix.Errno); ok {
|
||||
switch errno {
|
||||
case
|
||||
syscall.EACCES,
|
||||
syscall.EAGAIN,
|
||||
syscall.EBUSY,
|
||||
syscall.EINTR,
|
||||
syscall.ENOLCK,
|
||||
syscall.EDEADLK,
|
||||
syscall.ETIMEDOUT:
|
||||
unix.EACCES,
|
||||
unix.EAGAIN,
|
||||
unix.EBUSY,
|
||||
unix.EINTR,
|
||||
unix.ENOLCK,
|
||||
unix.EDEADLK,
|
||||
unix.ETIMEDOUT:
|
||||
return xErrorCode(BUSY)
|
||||
case syscall.EPERM:
|
||||
case unix.EPERM:
|
||||
return xErrorCode(PERM)
|
||||
}
|
||||
}
|
||||
|
||||
125
vfs_windows.go
125
vfs_windows.go
@@ -1,13 +1,69 @@
|
||||
package sqlite3
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func (vfsOSMethods) DeleteOnClose(file *os.File) {}
|
||||
// OpenFile is a simplified copy of [os.openFileNolog]
|
||||
// that uses syscall.FILE_SHARE_DELETE.
|
||||
// https://go.dev/src/os/file_windows.go
|
||||
//
|
||||
// See: https://go.dev/issue/32088
|
||||
func (vfsOSMethods) OpenFile(name string, flag int, perm fs.FileMode) (*os.File, error) {
|
||||
if name == "" {
|
||||
return nil, &os.PathError{Op: "open", Path: name, Err: syscall.ENOENT}
|
||||
}
|
||||
r, e := syscallOpen(name, flag, uint32(perm.Perm()))
|
||||
if e != nil {
|
||||
return nil, &os.PathError{Op: "open", Path: name, Err: e}
|
||||
}
|
||||
return os.NewFile(uintptr(r), name), nil
|
||||
}
|
||||
|
||||
func (vfsOSMethods) Sync(file *os.File, fullsync, dataonly bool) error {
|
||||
return file.Sync()
|
||||
}
|
||||
|
||||
func (vfsOSMethods) Access(path string, flags _AccessFlag) (bool, xErrorCode) {
|
||||
fi, err := os.Stat(path)
|
||||
|
||||
switch {
|
||||
case flags == _ACCESS_EXISTS:
|
||||
switch {
|
||||
case err == nil:
|
||||
return true, _OK
|
||||
case errors.Is(err, fs.ErrNotExist):
|
||||
return false, _OK
|
||||
default:
|
||||
return false, IOERR_ACCESS
|
||||
}
|
||||
|
||||
case err == nil:
|
||||
var want fs.FileMode = syscall.S_IRUSR
|
||||
if flags == _ACCESS_READWRITE {
|
||||
want |= syscall.S_IWUSR
|
||||
}
|
||||
if fi.IsDir() {
|
||||
want |= syscall.S_IXUSR
|
||||
}
|
||||
if fi.Mode()&want == want {
|
||||
return true, _OK
|
||||
} else {
|
||||
return false, _OK
|
||||
}
|
||||
|
||||
case errors.Is(err, fs.ErrPermission):
|
||||
return false, _OK
|
||||
|
||||
default:
|
||||
return false, IOERR_ACCESS
|
||||
}
|
||||
}
|
||||
|
||||
func (vfsOSMethods) GetExclusiveLock(file *os.File) xErrorCode {
|
||||
// Release the SHARED lock.
|
||||
@@ -63,6 +119,9 @@ func (vfsOSMethods) ReleaseLock(file *os.File, state vfsLockState) xErrorCode {
|
||||
func (vfsOSMethods) unlock(file *os.File, start, len uint32) xErrorCode {
|
||||
err := windows.UnlockFileEx(windows.Handle(file.Fd()),
|
||||
0, len, 0, &windows.Overlapped{Offset: start})
|
||||
if err == windows.ERROR_NOT_LOCKED {
|
||||
return _OK
|
||||
}
|
||||
if err != nil {
|
||||
return IOERR_UNLOCK
|
||||
}
|
||||
@@ -95,8 +154,66 @@ func (vfsOSMethods) lockErrorCode(err error, def xErrorCode) xErrorCode {
|
||||
if err == nil {
|
||||
return _OK
|
||||
}
|
||||
if errno, _ := err.(syscall.Errno); errno == windows.ERROR_INVALID_HANDLE {
|
||||
return def
|
||||
if errno, ok := err.(syscall.Errno); ok {
|
||||
// https://devblogs.microsoft.com/oldnewthing/20140905-00/?p=63
|
||||
switch errno {
|
||||
case
|
||||
windows.ERROR_LOCK_VIOLATION,
|
||||
windows.ERROR_IO_PENDING:
|
||||
return xErrorCode(BUSY)
|
||||
}
|
||||
}
|
||||
return xErrorCode(BUSY)
|
||||
return def
|
||||
}
|
||||
|
||||
// syscallOpen is a simplified copy of [syscall.Open]
|
||||
// that uses syscall.FILE_SHARE_DELETE.
|
||||
// https://go.dev/src/syscall/syscall_windows.go
|
||||
func syscallOpen(path string, mode int, perm uint32) (fd syscall.Handle, err error) {
|
||||
if len(path) == 0 {
|
||||
return syscall.InvalidHandle, syscall.ERROR_FILE_NOT_FOUND
|
||||
}
|
||||
pathp, err := syscall.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return syscall.InvalidHandle, err
|
||||
}
|
||||
var access uint32
|
||||
switch mode & (syscall.O_RDONLY | syscall.O_WRONLY | syscall.O_RDWR) {
|
||||
case syscall.O_RDONLY:
|
||||
access = syscall.GENERIC_READ
|
||||
case syscall.O_WRONLY:
|
||||
access = syscall.GENERIC_WRITE
|
||||
case syscall.O_RDWR:
|
||||
access = syscall.GENERIC_READ | syscall.GENERIC_WRITE
|
||||
}
|
||||
if mode&syscall.O_CREAT != 0 {
|
||||
access |= syscall.GENERIC_WRITE
|
||||
}
|
||||
if mode&syscall.O_APPEND != 0 {
|
||||
access &^= syscall.GENERIC_WRITE
|
||||
access |= syscall.FILE_APPEND_DATA
|
||||
}
|
||||
sharemode := uint32(syscall.FILE_SHARE_READ | syscall.FILE_SHARE_WRITE | syscall.FILE_SHARE_DELETE)
|
||||
var createmode uint32
|
||||
switch {
|
||||
case mode&(syscall.O_CREAT|syscall.O_EXCL) == (syscall.O_CREAT | syscall.O_EXCL):
|
||||
createmode = syscall.CREATE_NEW
|
||||
case mode&(syscall.O_CREAT|syscall.O_TRUNC) == (syscall.O_CREAT | syscall.O_TRUNC):
|
||||
createmode = syscall.CREATE_ALWAYS
|
||||
case mode&syscall.O_CREAT == syscall.O_CREAT:
|
||||
createmode = syscall.OPEN_ALWAYS
|
||||
case mode&syscall.O_TRUNC == syscall.O_TRUNC:
|
||||
createmode = syscall.TRUNCATE_EXISTING
|
||||
default:
|
||||
createmode = syscall.OPEN_EXISTING
|
||||
}
|
||||
var attrs uint32 = syscall.FILE_ATTRIBUTE_NORMAL
|
||||
if perm&syscall.S_IWRITE == 0 {
|
||||
attrs = syscall.FILE_ATTRIBUTE_READONLY
|
||||
}
|
||||
if createmode == syscall.OPEN_EXISTING && access == syscall.GENERIC_READ {
|
||||
// Necessary for opening directory handles.
|
||||
attrs |= syscall.FILE_FLAG_BACKUP_SEMANTICS
|
||||
}
|
||||
return syscall.CreateFile(pathp, access, sharemode, nil, createmode, attrs, 0)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user