Compare commits

..

35 Commits

Author SHA1 Message Date
Nuno Cruces
e08c7b3adf Refactor. 2024-12-19 15:14:20 +00:00
Nuno Cruces
66601dd3cb More BCE. 2024-12-19 14:00:46 +00:00
Nuno Cruces
58b66b75f1 Improved assertions. 2024-12-19 13:19:10 +00:00
Nuno Cruces
e0c6086aa9 Fix POSIX locks. 2024-12-18 16:21:24 +00:00
Nuno Cruces
9bc39c5b91 Remove dependency. 2024-12-17 15:43:19 +00:00
Nuno Cruces
12193cedea binaryen-version_121. 2024-12-17 15:25:25 +00:00
Nuno Cruces
71d95bf9d5 Fix #205. 2024-12-17 14:21:56 +00:00
Nuno Cruces
7e23100ff7 Help checklocks. 2024-12-16 13:47:59 +00:00
Nuno Cruces
e32d8401fb Improve Wal locking on BSD (#204) 2024-12-16 13:15:00 +00:00
Nuno Cruces
503db60927 Fix repro.sh. 2024-12-13 16:22:44 +00:00
Nuno Cruces
1227fa7a04 Skip sleeping if blocked. 2024-12-13 16:04:37 +00:00
Nuno Cruces
e455b5f729 Coverage. 2024-12-13 10:30:08 +00:00
Nuno Cruces
2bb1c8c795 Fair retry interval. 2024-12-13 10:23:43 +00:00
Nuno Cruces
844fab4167 Fix fuzzer. 2024-12-12 13:38:46 +00:00
Nuno Cruces
5ed4a6cb9d Fix #201. 2024-12-12 12:57:18 +00:00
Nuno Cruces
37f2145588 Use CancelIoEx. 2024-12-12 10:42:23 +00:00
Nuno Cruces
e17b3ef2c8 wasi-sdk-25. 2024-12-12 09:51:23 +00:00
Nuno Cruces
a75b8887db Updated dependencies. 2024-12-11 19:02:03 +00:00
Nuno Cruces
9f456fecb9 Updated dependencies. 2024-12-11 18:38:57 +00:00
Nuno Cruces
36bbd674c2 Add ColumnTypeScanType to driver (#199). 2024-12-11 18:35:50 +00:00
Nuno Cruces
7f5ea54009 Windows blocking locks. (#200) 2024-12-11 15:05:22 +00:00
Nuno Cruces
5f1d5727cd SQLite 3.47.2. 2024-12-10 18:38:49 +00:00
Nuno Cruces
6fb259e2b9 Fix. 2024-12-10 18:25:49 +00:00
Nuno Cruces
301f6bc2bd Fix. 2024-12-09 19:26:47 +00:00
Nuno Cruces
9e112c54b0 Fix, see #197. 2024-12-08 11:47:11 +00:00
Nuno Cruces
270efcb4af Updated dependencies. 2024-12-05 12:46:46 +00:00
dependabot[bot]
8252198dd2 Bump actions/attest-build-provenance from 1 to 2 (#196)
Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 1 to 2.
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/v1...v2)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-05 13:04:47 +00:00
dependabot[bot]
dff825ae81 Bump golang.org/x/crypto from 0.29.0 to 0.30.0 (#193)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.29.0 to 0.30.0.
- [Commits](https://github.com/golang/crypto/compare/v0.29.0...v0.30.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-05 03:03:36 +00:00
Nuno Cruces
aae732e530 API fix. 2024-12-04 17:09:22 +00:00
Nuno Cruces
2e3ba3949e Remove sqlite3_nosys. 2024-11-29 15:51:51 +00:00
Nuno Cruces
a44690035f Refactor. 2024-11-28 00:15:55 +00:00
Nuno Cruces
7e12105b22 Split module. 2024-11-27 14:25:01 +00:00
Nuno Cruces
987db177ad Split module. 2024-11-27 14:22:31 +00:00
Nuno Cruces
1469cb9f1a Imports. 2024-11-27 11:42:25 +00:00
Nuno Cruces
4ede2c7216 Updated dependencies. 2024-11-26 14:18:53 +00:00
75 changed files with 865 additions and 345 deletions

View File

@@ -18,12 +18,8 @@ echo js ; GOOS=js GOARCH=wasm go build .
echo wasip1 ; GOOS=wasip1 GOARCH=wasm go build .
echo linux-flock ; GOOS=linux GOARCH=amd64 go build -tags sqlite3_flock .
echo linux-dotlk ; GOOS=linux GOARCH=amd64 go build -tags sqlite3_dotlk .
echo linux-nosys ; GOOS=linux GOARCH=amd64 go build -tags sqlite3_nosys .
echo darwin-flock ; GOOS=darwin GOARCH=amd64 go build -tags sqlite3_flock .
echo darwin-dotlk ; GOOS=darwin GOARCH=amd64 go build -tags sqlite3_dotlk .
echo darwin-nosys ; GOOS=darwin GOARCH=amd64 go build -tags sqlite3_nosys .
echo windows-dotlk ; GOOS=windows GOARCH=amd64 go build -tags sqlite3_dotlk .
echo windows-nosys ; GOOS=windows GOARCH=amd64 go build -tags sqlite3_nosys .
echo freebsd-dotlk ; GOOS=freebsd GOARCH=amd64 go build -tags sqlite3_dotlk .
echo freebsd-nosys ; GOOS=freebsd GOARCH=amd64 go build -tags sqlite3_nosys .
echo solaris-dotlk ; GOOS=solaris GOARCH=amd64 go build -tags sqlite3_dotlk .

View File

@@ -2,14 +2,14 @@
set -euo pipefail
if [[ "$OSTYPE" == "linux"* ]]; then
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/wasi-sdk-24.0-x86_64-linux.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_120/binaryen-version_120-x86_64-linux.tar.gz"
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-linux.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_121/binaryen-version_121-x86_64-linux.tar.gz"
elif [[ "$OSTYPE" == "darwin"* ]]; then
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/wasi-sdk-24.0-arm64-macos.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_120/binaryen-version_120-arm64-macos.tar.gz"
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-arm64-macos.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_121/binaryen-version_121-arm64-macos.tar.gz"
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/wasi-sdk-24.0-x86_64-windows.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_120/binaryen-version_120-x86_64-windows.tar.gz"
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-windows.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_121/binaryen-version_121-x86_64-windows.tar.gz"
fi
# Download tools

View File

@@ -18,14 +18,12 @@ jobs:
steps:
- uses: ilammy/msvc-dev-cmd@v1
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version: stable }
- name: Build
shell: bash
run: .github/workflows/repro.sh
- uses: actions/attest-build-provenance@v1
- uses: actions/attest-build-provenance@v2
if: matrix.os == 'ubuntu-latest'
with:
subject-path: |

View File

@@ -54,10 +54,6 @@ jobs:
- name: Test
run: go test -v ./... -bench . -benchtime=1x
- name: Test no locks
run: go test -v -tags sqlite3_nosys ./...
if: matrix.os == 'ubuntu-latest'
- name: Test BSD locks
run: go test -v -tags sqlite3_flock ./...
if: matrix.os == 'macos-latest'
@@ -90,13 +86,13 @@ jobs:
matrix:
os:
- name: freebsd
version: '14.1'
version: '14.2'
flags: '-test.v'
- name: netbsd
version: '10.0'
flags: '-test.v'
- name: openbsd
version: '7.5'
version: '7.6'
flags: '-test.v -test.short'
runs-on: ubuntu-latest
needs: test
@@ -114,7 +110,7 @@ jobs:
run: .github/workflows/build-test.sh
- name: Test
uses: cross-platform-actions/action@v0.25.0
uses: cross-platform-actions/action@v0.26.0
with:
operating_system: ${{ matrix.os.name }}
version: ${{ matrix.os.version }}

View File

@@ -10,7 +10,7 @@ as well as direct access to most of the [C SQLite API](https://sqlite.org/cintro
It wraps a [Wasm](https://webassembly.org/) [build](embed/) of SQLite,
and uses [wazero](https://wazero.io/) as the runtime.\
Go, wazero and [`x/sys`](https://pkg.go.dev/golang.org/x/sys) are the _only_ runtime dependencies.
Go, wazero and [`x/sys`](https://pkg.go.dev/golang.org/x/sys) are the _only_ direct dependencies.
### Getting started
@@ -74,7 +74,7 @@ This project aims for [high test coverage](https://github.com/ncruces/go-sqlite3
It also benefits greatly from [SQLite's](https://sqlite.org/testing.html) and
[wazero's](https://tetrate.io/blog/introducing-wazero-from-tetrate/#:~:text=Rock%2Dsolid%20test%20approach) thorough testing.
Every commit is [tested](https://github.com/ncruces/go-sqlite3/wiki/Test-matrix) on
Every commit is [tested](https://github.com/ncruces/go-sqlite3/wiki/Support-matrix) on
Linux (amd64/arm64/386/riscv64/ppc64le/s390x), macOS (amd64/arm64),
Windows (amd64), FreeBSD (amd64), OpenBSD (amd64), NetBSD (amd64),
DragonFly BSD (amd64), illumos (amd64), and Solaris (amd64).

54
conn.go
View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"math"
"math/rand"
"net/url"
"strings"
"time"
@@ -24,7 +25,6 @@ type Conn struct {
interrupt context.Context
pending *Stmt
stmts []*Stmt
timer *time.Timer
busy func(context.Context, int) bool
log func(xErrorCode, string)
collation func(*Conn, string)
@@ -36,7 +36,9 @@ type Conn struct {
rollback func()
arena arena
handle uint32
busy1st time.Time
busylst time.Time
handle uint32
}
// Open calls [OpenFlags] with [OPEN_READWRITE], [OPEN_CREATE] and [OPEN_URI].
@@ -389,38 +391,20 @@ func (c *Conn) BusyTimeout(timeout time.Duration) error {
}
func timeoutCallback(ctx context.Context, mod api.Module, count, tmout int32) (retry uint32) {
// https://fractaledmind.github.io/2024/04/15/sqlite-on-rails-the-how-and-why-of-optimal-performance/
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.interrupt.Err() == nil {
const delays = "\x01\x02\x05\x0a\x0f\x14\x19\x19\x19\x32\x32\x64"
const totals = "\x00\x01\x03\x08\x12\x21\x35\x4e\x67\x80\xb2\xe4"
const ndelay = int32(len(delays) - 1)
var delay, prior int32
if count <= ndelay {
delay = int32(delays[count])
prior = int32(totals[count])
} else {
delay = int32(delays[ndelay])
prior = int32(totals[ndelay]) + delay*(count-ndelay)
switch {
case count == 0:
c.busy1st = time.Now()
case time.Since(c.busy1st) >= time.Duration(tmout)*time.Millisecond:
return 0
}
if delay = min(delay, tmout-prior); delay > 0 {
delay := time.Duration(delay) * time.Millisecond
if c.interrupt.Done() == nil {
time.Sleep(delay)
return 1
}
if c.timer == nil {
c.timer = time.NewTimer(delay)
} else {
c.timer.Reset(delay)
}
select {
case <-c.interrupt.Done():
c.timer.Stop()
case <-c.timer.C:
return 1
}
if time.Since(c.busylst) < time.Millisecond {
const sleepIncrement = 2*1024*1024 - 1 // power of two, ~2ms
time.Sleep(time.Duration(rand.Int63() & sleepIncrement))
}
c.busylst = time.Now()
return 1
}
return 0
}
@@ -501,8 +485,12 @@ func (c *Conn) TableColumnMetadata(schema, table, column string) (declType, coll
uint64(declTypePtr), uint64(collSeqPtr),
uint64(notNullPtr), uint64(primaryKeyPtr), uint64(autoIncPtr))
if err = c.error(r); err == nil && column != "" {
declType = util.ReadString(c.mod, util.ReadUint32(c.mod, declTypePtr), _MAX_NAME)
collSeq = util.ReadString(c.mod, util.ReadUint32(c.mod, collSeqPtr), _MAX_NAME)
if ptr := util.ReadUint32(c.mod, declTypePtr); ptr != 0 {
declType = util.ReadString(c.mod, ptr, _MAX_NAME)
}
if ptr := util.ReadUint32(c.mod, collSeqPtr); ptr != 0 {
collSeq = util.ReadString(c.mod, ptr, _MAX_NAME)
}
notNull = util.ReadUint32(c.mod, notNullPtr) != 0
autoInc = util.ReadUint32(c.mod, autoIncPtr) != 0
primaryKey = util.ReadUint32(c.mod, primaryKeyPtr) != 0

View File

@@ -81,6 +81,7 @@ import (
"fmt"
"io"
"net/url"
"reflect"
"strings"
"time"
"unsafe"
@@ -107,17 +108,17 @@ func init() {
// The second callback is called before the driver closes a connection.
// The [sqlite3.Conn] can be used to execute queries, register functions, etc.
func Open(dataSourceName string, fn ...func(*sqlite3.Conn) error) (*sql.DB, error) {
var drv SQLite
if len(fn) > 2 {
return nil, sqlite3.MISUSE
}
var init, term func(*sqlite3.Conn) error
if len(fn) > 1 {
drv.term = fn[1]
term = fn[1]
}
if len(fn) > 0 {
drv.init = fn[0]
init = fn[0]
}
c, err := drv.OpenConnector(dataSourceName)
c, err := newConnector(dataSourceName, init, term)
if err != nil {
return nil, err
}
@@ -125,10 +126,7 @@ func Open(dataSourceName string, fn ...func(*sqlite3.Conn) error) (*sql.DB, erro
}
// SQLite implements [database/sql/driver.Driver].
type SQLite struct {
init func(*sqlite3.Conn) error
term func(*sqlite3.Conn) error
}
type SQLite struct{}
var (
// Ensure these interfaces are implemented:
@@ -137,7 +135,7 @@ var (
// Open implements [database/sql/driver.Driver].
func (d *SQLite) Open(name string) (driver.Conn, error) {
c, err := d.newConnector(name)
c, err := newConnector(name, nil, nil)
if err != nil {
return nil, err
}
@@ -146,11 +144,11 @@ func (d *SQLite) Open(name string) (driver.Conn, error) {
// OpenConnector implements [database/sql/driver.DriverContext].
func (d *SQLite) OpenConnector(name string) (driver.Connector, error) {
return d.newConnector(name)
return newConnector(name, nil, nil)
}
func (d *SQLite) newConnector(name string) (*connector, error) {
c := connector{driver: d, name: name}
func newConnector(name string, init, term func(*sqlite3.Conn) error) (*connector, error) {
c := connector{name: name, init: init, term: term}
var txlock, timefmt string
if strings.HasPrefix(name, "file:") {
@@ -190,7 +188,8 @@ func (d *SQLite) newConnector(name string) (*connector, error) {
}
type connector struct {
driver *SQLite
init func(*sqlite3.Conn) error
term func(*sqlite3.Conn) error
name string
txLock string
tmRead sqlite3.TimeFormat
@@ -199,7 +198,7 @@ type connector struct {
}
func (n *connector) Driver() driver.Driver {
return n.driver
return &SQLite{}
}
func (n *connector) Connect(ctx context.Context) (res driver.Conn, err error) {
@@ -228,13 +227,13 @@ func (n *connector) Connect(ctx context.Context) (res driver.Conn, err error) {
return nil, err
}
}
if n.driver.init != nil {
err = n.driver.init(c.Conn)
if n.init != nil {
err = n.init(c.Conn)
if err != nil {
return nil, err
}
}
if n.pragmas || n.driver.init != nil {
if n.pragmas || n.init != nil {
s, _, err := c.Conn.Prepare(`PRAGMA query_only`)
if err != nil {
return nil, err
@@ -250,9 +249,9 @@ func (n *connector) Connect(ctx context.Context) (res driver.Conn, err error) {
return nil, err
}
}
if n.driver.term != nil {
if n.term != nil {
err = c.Conn.Trace(sqlite3.TRACE_CLOSE, func(sqlite3.TraceEvent, any, any) error {
return n.driver.term(c.Conn)
return n.term(c.Conn)
})
if err != nil {
return nil, err
@@ -275,6 +274,7 @@ func (n *connector) Connect(ctx context.Context) (res driver.Conn, err error) {
// if err != nil {
// log.Fatal(err)
// }
// defer conn.Close()
//
// err = conn.Raw(func(driverConn any) error {
// conn := driverConn.(driver.Conn)
@@ -288,6 +288,8 @@ func (n *connector) Connect(ctx context.Context) (res driver.Conn, err error) {
type Conn interface {
Raw() *sqlite3.Conn
driver.Conn
driver.ConnBeginTx
driver.ConnPrepareContext
}
type conn struct {
@@ -301,10 +303,8 @@ type conn struct {
var (
// Ensure these interfaces are implemented:
_ Conn = &conn{}
_ driver.ConnBeginTx = &conn{}
_ driver.ConnPrepareContext = &conn{}
_ driver.ExecerContext = &conn{}
_ Conn = &conn{}
_ driver.ExecerContext = &conn{}
)
func (c *conn) Raw() *sqlite3.Conn {
@@ -380,7 +380,7 @@ func (c *conn) PrepareContext(ctx context.Context, query string) (driver.Stmt, e
if err != nil {
return nil, err
}
if tail != "" {
if notWhitespace(tail) {
s.Close()
return nil, util.TailErr
}
@@ -581,8 +581,22 @@ type rows struct {
names []string
types []string
nulls []bool
scans []scantype
}
type scantype byte
const (
_ANY scantype = iota
_INT scantype = scantype(sqlite3.INTEGER)
_REAL scantype = scantype(sqlite3.FLOAT)
_TEXT scantype = scantype(sqlite3.TEXT)
_BLOB scantype = scantype(sqlite3.BLOB)
_NULL scantype = scantype(sqlite3.NULL)
_BOOL scantype = iota
_TIME
)
var (
// Ensure these interfaces are implemented:
_ driver.RowsColumnTypeDatabaseTypeName = &rows{}
@@ -606,21 +620,42 @@ func (r *rows) Columns() []string {
return r.names
}
func (r *rows) loadTypes() {
func (r *rows) loadColumnMetadata() {
if r.nulls == nil {
count := r.Stmt.ColumnCount()
nulls := make([]bool, count)
types := make([]string, count)
scans := make([]scantype, count)
for i := range nulls {
if col := r.Stmt.ColumnOriginName(i); col != "" {
types[i], _, nulls[i], _, _, _ = r.Stmt.Conn().TableColumnMetadata(
r.Stmt.ColumnDatabaseName(i),
r.Stmt.ColumnTableName(i),
col)
types[i] = strings.ToUpper(types[i])
// These types are only used before we have rows,
// and otherwise as type hints.
// The first few ensure STRICT tables are strictly typed.
// The other two are type hints for booleans and time.
switch types[i] {
case "INT", "INTEGER":
scans[i] = _INT
case "REAL":
scans[i] = _REAL
case "TEXT":
scans[i] = _TEXT
case "BLOB":
scans[i] = _BLOB
case "BOOLEAN":
scans[i] = _BOOL
case "DATE", "TIME", "DATETIME", "TIMESTAMP":
scans[i] = _TIME
}
}
}
r.nulls = nulls
r.types = types
r.scans = scans
}
}
@@ -637,7 +672,7 @@ func (r *rows) declType(index int) string {
}
func (r *rows) ColumnTypeDatabaseTypeName(index int) string {
r.loadTypes()
r.loadColumnMetadata()
decltype := r.types[index]
if len := len(decltype); len > 0 && decltype[len-1] == ')' {
if i := strings.LastIndexByte(decltype, '('); i >= 0 {
@@ -648,13 +683,57 @@ func (r *rows) ColumnTypeDatabaseTypeName(index int) string {
}
func (r *rows) ColumnTypeNullable(index int) (nullable, ok bool) {
r.loadTypes()
r.loadColumnMetadata()
if r.nulls[index] {
return false, true
}
return true, false
}
func (r *rows) ColumnTypeScanType(index int) (typ reflect.Type) {
r.loadColumnMetadata()
scan := r.scans[index]
if r.Stmt.Busy() {
// SQLite is dynamically typed and we now have a row.
// Always use the type of the value itself,
// unless the scan type is more specific
// and can scan the actual value.
val := scantype(r.Stmt.ColumnType(index))
useValType := true
switch {
case scan == _TIME && val != _BLOB && val != _NULL:
t := r.Stmt.ColumnTime(index, r.tmRead)
useValType = t == time.Time{}
case scan == _BOOL && val == _INT:
i := r.Stmt.ColumnInt64(index)
useValType = i != 0 && i != 1
case scan == _BLOB && val == _NULL:
useValType = false
}
if useValType {
scan = val
}
}
switch scan {
case _INT:
return reflect.TypeOf(int64(0))
case _REAL:
return reflect.TypeOf(float64(0))
case _TEXT:
return reflect.TypeOf("")
case _BLOB:
return reflect.TypeOf([]byte{})
case _BOOL:
return reflect.TypeOf(false)
case _TIME:
return reflect.TypeOf(time.Time{})
default:
return reflect.TypeOf((*any)(nil)).Elem()
}
}
func (r *rows) Next(dest []driver.Value) error {
old := r.Stmt.Conn().SetInterrupt(r.ctx)
defer r.Stmt.Conn().SetInterrupt(old)
@@ -667,7 +746,7 @@ func (r *rows) Next(dest []driver.Value) error {
}
data := unsafe.Slice((*any)(unsafe.SliceData(dest)), len(dest))
err := r.Stmt.Columns(data)
err := r.Stmt.Columns(data...)
for i := range dest {
if t, ok := r.decodeTime(i, dest[i]); ok {
dest[i] = t

View File

@@ -7,6 +7,7 @@ import (
"errors"
"math"
"net/url"
"reflect"
"testing"
"time"
@@ -224,8 +225,8 @@ func Test_Prepare(t *testing.T) {
}
_, err = db.Prepare(`SELECT 1; `)
if err.Error() != string(util.TailErr) {
t.Error("want tailErr")
if err != nil {
t.Error(err)
}
_, err = db.Prepare(`SELECT 1; SELECT`)
@@ -365,3 +366,104 @@ func Test_time(t *testing.T) {
})
}
}
func Test_ColumnType_ScanType(t *testing.T) {
var (
INT = reflect.TypeOf(int64(0))
REAL = reflect.TypeOf(float64(0))
TEXT = reflect.TypeOf("")
BLOB = reflect.TypeOf([]byte{})
BOOL = reflect.TypeOf(false)
TIME = reflect.TypeOf(time.Time{})
ANY = reflect.TypeOf((*any)(nil)).Elem()
)
t.Parallel()
tmp := memdb.TestDB(t)
db, err := sql.Open("sqlite3", tmp)
if err != nil {
t.Fatal(err)
}
defer db.Close()
_, err = db.Exec(`
CREATE TABLE test (
col_int INTEGER,
col_real REAL,
col_text TEXT,
col_blob BLOB,
col_bool BOOLEAN,
col_time DATETIME,
col_decimal DECIMAL
);
INSERT INTO test VALUES
(1, 1, 1, 1, 1, 1, 1),
(2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0),
('1', '1', '1', '1', '1', '1', '1'),
('x', 'x', 'x', 'x', 'x', 'x', 'x'),
(x'', x'', x'', x'', x'', x'', x''),
('2006-01-02T15:04:05Z', '2006-01-02T15:04:05Z', '2006-01-02T15:04:05Z', '2006-01-02T15:04:05Z',
'2006-01-02T15:04:05Z', '2006-01-02T15:04:05Z', '2006-01-02T15:04:05Z'),
(TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE),
(NULL, NULL, NULL, NULL, NULL, NULL, NULL);
`)
if err != nil {
t.Fatal(err)
}
rows, err := db.Query(`SELECT * FROM test`)
if err != nil {
t.Fatal(err)
}
defer rows.Close()
cols, err := rows.ColumnTypes()
if err != nil {
t.Fatal(err)
}
want := [][]reflect.Type{
{INT, REAL, TEXT, BLOB, BOOL, TIME, ANY},
{INT, REAL, TEXT, INT, BOOL, TIME, INT},
{INT, REAL, TEXT, REAL, INT, TIME, INT},
{INT, REAL, TEXT, TEXT, BOOL, TIME, INT},
{TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT},
{BLOB, BLOB, BLOB, BLOB, BLOB, BLOB, BLOB},
{TEXT, TEXT, TEXT, TEXT, TEXT, TIME, TEXT},
{INT, REAL, TEXT, INT, BOOL, TIME, INT},
{ANY, ANY, ANY, BLOB, ANY, ANY, ANY},
}
for j, c := range cols {
got := c.ScanType()
if got != want[0][j] {
t.Errorf("want %v, got %v, at column %d", want[0][j], got, j)
}
}
dest := make([]any, len(cols))
for i := 1; rows.Next(); i++ {
cols, err := rows.ColumnTypes()
if err != nil {
t.Fatal(err)
}
for j, c := range cols {
got := c.ScanType()
if got != want[i][j] {
t.Errorf("want %v, got %v, at row %d column %d", want[i][j], got, i, j)
}
dest[j] = reflect.New(got).Interface()
}
err = rows.Scan(dest...)
if err != nil {
t.Error(err)
}
}
err = rows.Err()
if err != nil {
t.Fatal(err)
}
}

View File

@@ -1,4 +1,4 @@
//go:build (linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos || sqlite3_flock) && !sqlite3_nosys
//go:build linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos || sqlite3_flock || sqlite3_dotlk
package driver_test

View File

@@ -1,4 +1,4 @@
//go:build ((linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos) && !sqlite3_nosys) || sqlite3_flock || sqlite3_dotlk
//go:build linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos || sqlite3_flock || sqlite3_dotlk
package driver_test

View File

@@ -6,6 +6,7 @@ import (
"github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
_ "github.com/ncruces/go-sqlite3/vfs/memdb"
)

View File

@@ -1,8 +1,6 @@
package driver
import (
"time"
)
import "time"
// Convert a string in [time.RFC3339Nano] format into a [time.Time]
// if it roundtrips back to the same string.

View File

@@ -27,12 +27,12 @@ func Fuzz_stringOrTime_1(f *testing.F) {
// 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)
t.Errorf("did not round-trip: %q", str)
}
} else {
date, err := time.Parse(time.RFC3339Nano, str)
if err == nil && date.Format(time.RFC3339Nano) == str {
t.Fatalf("would round-trip: %q", str)
t.Errorf("would round-trip: %q", str)
}
}
})

View File

@@ -12,3 +12,63 @@ func namedValues(args []driver.Value) []driver.NamedValue {
}
return named
}
func notWhitespace(sql string) bool {
const (
code = iota
slash
minus
ccomment
sqlcomment
endcomment
)
state := code
for _, b := range ([]byte)(sql) {
if b == 0 {
break
}
switch state {
case code:
switch b {
case '/':
state = slash
case '-':
state = minus
case ' ', ';', '\t', '\n', '\v', '\f', '\r':
continue
default:
return true
}
case slash:
if b != '*' {
return true
}
state = ccomment
case minus:
if b != '-' {
return true
}
state = sqlcomment
case ccomment:
if b == '*' {
state = endcomment
}
case sqlcomment:
if b == '\n' {
state = code
}
case endcomment:
switch b {
case '/':
state = code
case '*':
state = endcomment
default:
state = ccomment
}
}
}
return state == slash || state == minus
}

View File

@@ -1,9 +1,13 @@
package driver
import (
"context"
"database/sql/driver"
"reflect"
"testing"
_ "github.com/ncruces/go-sqlite3/embed"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
)
func Test_namedValues(t *testing.T) {
@@ -16,3 +20,67 @@ func Test_namedValues(t *testing.T) {
t.Errorf("got %v, want %v", got, want)
}
}
func Fuzz_notWhitespace(f *testing.F) {
f.Add("")
f.Add(" ")
f.Add(";")
f.Add("0")
f.Add("-")
f.Add("-0")
f.Add("--")
f.Add("--0")
f.Add("--\n")
f.Add("--0\n")
f.Add("/0")
f.Add("/*")
f.Add("/*/")
f.Add("/**")
f.Add("/*0")
f.Add("/**/")
f.Add("/***/")
f.Add("/**0/")
f.Add("\v")
f.Add(" \v")
f.Add("\xf0")
f.Add("\000")
db, err := Open(":memory:")
if err != nil {
f.Fatal(err)
}
defer db.Close()
f.Fuzz(func(t *testing.T, str string) {
if len(str) > 128 {
t.SkipNow()
}
c, err := db.Conn(context.Background())
if err != nil {
t.Fatal(err)
}
defer c.Close()
c.Raw(func(driverConn any) error {
conn := driverConn.(*conn).Conn
stmt, tail, err := conn.Prepare(str)
stmt.Close()
// It's hard to be bug for bug compatible with SQLite.
// We settle for somewhat less:
// - if SQLite reports whitespace, we must too
// - if we report whitespace, SQLite must not parse a statement
if notWhitespace(str) {
if stmt == nil && tail == "" && err == nil {
t.Errorf("was whitespace: %q", str)
}
} else {
if stmt != nil {
t.Errorf("was not whitespace: %q (%v)", str, err)
}
}
return nil
})
})
}

View File

@@ -1,6 +1,6 @@
# Embeddable Wasm build of SQLite
This folder includes an embeddable Wasm build of SQLite 3.47.1 for use with
This folder includes an embeddable Wasm build of SQLite 3.47.2 for use with
[`github.com/ncruces/go-sqlite3`](https://pkg.go.dev/github.com/ncruces/go-sqlite3).
The following optional features are compiled in:

Binary file not shown.

View File

@@ -32,6 +32,11 @@ func Test_bcw2(t *testing.T) {
t.Fatal(err)
}
_, err = tx.Exec(`DELETE FROM test LIMIT 1`)
if err != nil {
t.Fatal(err)
}
err = tx.Commit()
if err != nil {
t.Fatal(err)

View File

@@ -13,15 +13,14 @@ mkdir -p build/ext/
cp "$ROOT"/sqlite3/*.[ch] build/
cp "$ROOT"/sqlite3/*.patch build/
# https://sqlite.org/src/info/e03dd0bd313817da
curl -# https://sqlite.org/src/tarball/sqlite.tar.gz?r=e03dd0bd | tar xz
# https://sqlite.org/src/info/ec5d7025cba9f4ac
curl -# https://sqlite.org/src/tarball/sqlite.tar.gz?r=ec5d7025 | tar xz
cd sqlite
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
MSYS_NO_PATHCONV=1 nmake /f makefile.msc sqlite3.c
MSYS_NO_PATHCONV=1 nmake /f makefile.msc sqlite3.c OPTS=-DSQLITE_ENABLE_UPDATE_DELETE_LIMIT
else
sh configure
make sqlite3.c
sh configure --enable-update-limit && make sqlite3.c
fi
cd ~-
@@ -54,6 +53,7 @@ cd ~-
-Wl,--import-undefined \
-Wl,--initial-memory=327680 \
-D_HAVE_SQLITE_CONFIG_H \
-DSQLITE_ENABLE_UPDATE_DELETE_LIMIT \
-DSQLITE_CUSTOM_INCLUDE=sqlite_opt.h \
$(awk '{print "-Wl,--export="$0}' ../exports.txt)

13
embed/bcw2/go.mod Normal file
View File

@@ -0,0 +1,13 @@
module github.com/ncruces/go-sqlite3/embed/bcw2
go 1.21
toolchain go1.23.0
require github.com/ncruces/go-sqlite3 v0.21.0
require (
github.com/ncruces/julianday v1.0.0 // indirect
github.com/tetratelabs/wazero v1.8.2 // indirect
golang.org/x/sys v0.28.0 // indirect
)

10
embed/bcw2/go.sum Normal file
View File

@@ -0,0 +1,10 @@
github.com/ncruces/go-sqlite3 v0.21.0 h1:EwKFoy1hHEopN4sFZarmi+McXdbCcbTuLixhEayXVbQ=
github.com/ncruces/go-sqlite3 v0.21.0/go.mod h1:zxMOaSG5kFYVFK4xQa0pdwIszqxqJ0W0BxBgwdrNjuA=
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4=
github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=

View File

@@ -19,7 +19,7 @@ func Test_init(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if version != "3.47.1" {
if version != "3.47.2" {
t.Error(version)
}
}

Binary file not shown.

View File

@@ -44,6 +44,8 @@ func Register(db *sqlite3.Conn) error {
type OpenCallback func(*sqlite3.Blob, ...sqlite3.Value) error
func readblob(ctx sqlite3.Context, arg ...sqlite3.Value) {
_ = arg[5] // bounds check
blob, err := getAuxBlob(ctx, arg, false)
if err != nil {
ctx.ResultError(err)
@@ -78,6 +80,8 @@ func readblob(ctx sqlite3.Context, arg ...sqlite3.Value) {
}
func writeblob(ctx sqlite3.Context, arg ...sqlite3.Value) {
_ = arg[5] // bounds check
blob, err := getAuxBlob(ctx, arg, true)
if err != nil {
ctx.ResultError(err)

View File

@@ -76,6 +76,7 @@ func load(ctx sqlite3.Context, i int, expr string) (*regexp.Regexp, error) {
}
func regex(ctx sqlite3.Context, arg ...sqlite3.Value) {
_ = arg[1] // bounds check
re, err := load(ctx, 0, arg[0].Text())
if err != nil {
ctx.ResultError(err)
@@ -165,6 +166,8 @@ func regexInstr(ctx sqlite3.Context, arg ...sqlite3.Value) {
}
func regexReplace(ctx sqlite3.Context, arg ...sqlite3.Value) {
_ = arg[2] // bounds check
re, err := load(ctx, 1, arg[1].Text())
if err != nil {
ctx.ResultError(err)

View File

@@ -189,6 +189,7 @@ func like(ctx sqlite3.Context, arg ...sqlite3.Value) {
return
}
}
_ = arg[1] // bounds check
type likeData struct {
*regexp.Regexp

8
go.mod
View File

@@ -8,16 +8,16 @@ require (
github.com/ncruces/julianday v1.0.0
github.com/ncruces/sort v0.1.2
github.com/tetratelabs/wazero v1.8.2
golang.org/x/crypto v0.29.0
golang.org/x/sys v0.27.0
golang.org/x/crypto v0.31.0
golang.org/x/sys v0.28.0
)
require (
github.com/dchest/siphash v1.2.3 // ext/bloom
github.com/google/uuid v1.6.0 // ext/uuid
github.com/psanford/httpreadat v0.1.0 // example
golang.org/x/sync v0.9.0 // test
golang.org/x/text v0.20.0 // ext/unicode
golang.org/x/sync v0.10.0 // test
golang.org/x/text v0.21.0 // ext/unicode
lukechampine.com/adiantum v1.1.1 // vfs/adiantum
)

16
go.sum
View File

@@ -10,13 +10,13 @@ github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIw
github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE=
github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4=
github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
lukechampine.com/adiantum v1.1.1 h1:4fp6gTxWCqpEbLy40ExiYDDED3oUNWx5cTqBCtPdZqA=
lukechampine.com/adiantum v1.1.1/go.mod h1:LrAYVnTYLnUtE/yMp5bQr0HstAf060YUF8nM0B6+rUw=

View File

@@ -3,4 +3,5 @@ go 1.21
use (
.
./gormlite
./embed/bcw2
)

View File

@@ -1,3 +1,4 @@
github.com/ncruces/go-sqlite3 v0.21.0/go.mod h1:zxMOaSG5kFYVFK4xQa0pdwIszqxqJ0W0BxBgwdrNjuA=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
@@ -10,5 +11,6 @@ golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=

View File

@@ -5,7 +5,7 @@ go 1.21
toolchain go1.23.0
require (
github.com/ncruces/go-sqlite3 v0.20.2
github.com/ncruces/go-sqlite3 v0.21.0
gorm.io/gorm v1.25.12
)
@@ -14,6 +14,6 @@ require (
github.com/jinzhu/now v1.1.5 // indirect
github.com/ncruces/julianday v1.0.0 // indirect
github.com/tetratelabs/wazero v1.8.2 // indirect
golang.org/x/sys v0.27.0 // indirect
golang.org/x/text v0.20.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
)

View File

@@ -2,15 +2,15 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/ncruces/go-sqlite3 v0.20.2 h1:cMLIwrLZQuCWVCEOowSqlIlpzgbag3jnYVW4NM5u01M=
github.com/ncruces/go-sqlite3 v0.20.2/go.mod h1:yL4ZNWGsr1/8pcLfpPW1RT1WFdvyeHonrgIwwi4rvkg=
github.com/ncruces/go-sqlite3 v0.21.0 h1:EwKFoy1hHEopN4sFZarmi+McXdbCcbTuLixhEayXVbQ=
github.com/ncruces/go-sqlite3 v0.21.0/go.mod h1:zxMOaSG5kFYVFK4xQa0pdwIszqxqJ0W0BxBgwdrNjuA=
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4=
github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=

View File

@@ -1,4 +1,4 @@
//go:build !(unix || windows) || sqlite3_nosys
//go:build !unix && !windows
package alloc

View File

@@ -1,4 +1,4 @@
//go:build unix && !sqlite3_nosys
//go:build unix
package alloc

View File

@@ -1,5 +1,3 @@
//go:build !sqlite3_nosys
package alloc
import (

29
internal/dotlk/dotlk.go Normal file
View File

@@ -0,0 +1,29 @@
package dotlk
import (
"errors"
"io/fs"
"os"
)
// LockShm creates a directory on disk to prevent SQLite
// from using this path for a shared memory file.
func LockShm(name string) error {
err := os.Mkdir(name, 0777)
if errors.Is(err, fs.ErrExist) {
s, err := os.Lstat(name)
if err == nil && s.IsDir() {
return nil
}
}
return err
}
// Unlock removes the lock or shared memory file.
func Unlock(name string) error {
err := os.Remove(name)
if errors.Is(err, fs.ErrNotExist) {
return nil
}
return err
}

View File

@@ -0,0 +1,13 @@
//go:build !unix
package dotlk
import "os"
// TryLock returns nil if it acquired the lock,
// fs.ErrExist if another process has the lock.
func TryLock(name string) error {
f, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
f.Close()
return err
}

View File

@@ -0,0 +1,50 @@
//go:build unix
package dotlk
import (
"errors"
"io/fs"
"os"
"strconv"
"golang.org/x/sys/unix"
)
// TryLock returns nil if it acquired the lock,
// fs.ErrExist if another process has the lock.
func TryLock(name string) error {
for retry := true; retry; retry = false {
f, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
if err == nil {
f.WriteString(strconv.Itoa(os.Getpid()))
f.Close()
return nil
}
if !errors.Is(err, fs.ErrExist) {
return err
}
if !removeStale(name) {
break
}
}
return fs.ErrExist
}
func removeStale(name string) bool {
buf, err := os.ReadFile(name)
if err != nil {
return errors.Is(err, fs.ErrNotExist)
}
pid, err := strconv.Atoi(string(buf))
if err != nil {
return false
}
if unix.Kill(pid, 0) == nil {
return false
}
err = os.Remove(name)
return err == nil || errors.Is(err, fs.ErrNotExist)
}

View File

@@ -1,4 +1,4 @@
//go:build !unix || sqlite3_nosys
//go:build !unix
package util

View File

@@ -1,4 +1,4 @@
//go:build unix && !sqlite3_nosys
//go:build unix
package util
@@ -39,13 +39,13 @@ func (s *mmapState) new(ctx context.Context, mod api.Module, size int32) *Mapped
// Save the newly allocated region.
ptr := uint32(stack[0])
buf := View(mod, ptr, uint64(size))
addr := unsafe.Pointer(&buf[0])
s.regions = append(s.regions, &MappedRegion{
res := &MappedRegion{
Ptr: ptr,
addr: addr,
size: size,
})
return s.regions[len(s.regions)-1]
addr: unsafe.Pointer(&buf[0]),
}
s.regions = append(s.regions, res)
return res
}
type MappedRegion struct {

View File

@@ -1,5 +1,3 @@
//go:build !sqlite3_nosys
package util
import (

View File

@@ -265,10 +265,11 @@ func (a *arena) mark() (reset func()) {
ptrs := len(a.ptrs)
next := a.next
return func() {
for _, ptr := range a.ptrs[ptrs:] {
rest := a.ptrs[ptrs:]
for _, ptr := range a.ptrs[:ptrs] {
a.sqlt.free(ptr)
}
a.ptrs = a.ptrs[:ptrs]
a.ptrs = rest
a.next = next
}
}

View File

@@ -38,6 +38,9 @@ int sqlite3_columns_go(sqlite3_stmt *stmt, int nCol, char *aType,
}
if (ptr == NULL && rc == SQLITE_OK) {
rc = sqlite3_errcode(sqlite3_db_handle(stmt));
if (rc == SQLITE_ROW || rc == SQLITE_DONE) {
rc = SQLITE_OK;
}
}
aData[i].ptr = ptr;
aData[i].len = sqlite3_column_bytes(stmt, i);

View File

@@ -3,7 +3,7 @@ set -euo pipefail
cd -P -- "$(dirname -- "$0")"
curl -#OL "https://sqlite.org/2024/sqlite-amalgamation-3470100.zip"
curl -#OL "https://sqlite.org/2024/sqlite-amalgamation-3470200.zip"
unzip -d . sqlite-amalgamation-*.zip
mv sqlite-amalgamation-*/sqlite3.c .
mv sqlite-amalgamation-*/sqlite3.h .
@@ -21,25 +21,25 @@ cat *.patch | patch --no-backup-if-mismatch
mkdir -p ext/
cd ext/
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.1/ext/misc/anycollseq.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.1/ext/misc/base64.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.1/ext/misc/decimal.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.1/ext/misc/ieee754.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.1/ext/misc/regexp.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.1/ext/misc/series.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.1/ext/misc/spellfix.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.1/ext/misc/uint.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.2/ext/misc/anycollseq.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.2/ext/misc/base64.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.2/ext/misc/decimal.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.2/ext/misc/ieee754.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.2/ext/misc/regexp.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.2/ext/misc/series.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.2/ext/misc/spellfix.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.2/ext/misc/uint.c"
cd ~-
cd ../vfs/tests/mptest/testdata/
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.1/mptest/mptest.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.1/mptest/config01.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.1/mptest/config02.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.1/mptest/crash01.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.1/mptest/crash02.subtest"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.1/mptest/multiwrite01.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.2/mptest/mptest.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.2/mptest/config01.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.2/mptest/config02.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.2/mptest/crash01.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.2/mptest/crash02.subtest"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.2/mptest/multiwrite01.test"
cd ~-
cd ../vfs/tests/speedtest1/testdata/
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.1/test/speedtest1.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.47.2/test/speedtest1.c"
cd ~-

10
stmt.go
View File

@@ -582,7 +582,9 @@ func (s *Stmt) ColumnRawBlob(col int) []byte {
func (s *Stmt) columnRawBytes(col int, ptr uint32) []byte {
if ptr == 0 {
r := s.c.call("sqlite3_errcode", uint64(s.c.handle))
s.err = s.c.error(r)
if r != _ROW && r != _DONE {
s.err = s.c.error(r)
}
return nil
}
@@ -637,7 +639,7 @@ func (s *Stmt) ColumnValue(col int) Value {
// [TEXT] as string, and [BLOB] as []byte.
// Any []byte are owned by SQLite and may be invalidated by
// subsequent calls to [Stmt] methods.
func (s *Stmt) Columns(dest []any) error {
func (s *Stmt) Columns(dest ...any) error {
defer s.c.arena.mark()()
count := uint64(len(dest))
typePtr := s.c.arena.new(count)
@@ -666,6 +668,10 @@ func (s *Stmt) Columns(dest []any) error {
dest[i] = nil
default:
ptr := util.ReadUint32(s.c.mod, dataPtr+0)
if ptr == 0 {
dest[i] = []byte{}
continue
}
len := util.ReadUint32(s.c.mod, dataPtr+4)
buf := util.View(s.c.mod, ptr, uint64(len))
if types[i] == byte(TEXT) {

View File

@@ -1,4 +1,4 @@
//go:build ((linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos) && !sqlite3_nosys) || sqlite3_flock || sqlite3_dotlk
//go:build linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos || sqlite3_flock || sqlite3_dotlk
package bradfitz

View File

@@ -101,6 +101,9 @@ func syscallOpen(path string, mode int, perm uint32) (fd Handle, err error) {
const _FILE_FLAG_WRITE_THROUGH = 0x80000000
attrs |= _FILE_FLAG_WRITE_THROUGH
}
if mode&O_NONBLOCK != 0 {
attrs |= FILE_FLAG_OVERLAPPED
}
return CreateFile(pathp, access, sharemode, sa, createmode, attrs, 0)
}

View File

@@ -48,11 +48,6 @@ On Unix, this package may use `mmap` to implement
[shared-memory for the WAL-index](https://sqlite.org/wal.html#implementation_of_shared_memory_for_the_wal_index),
like SQLite.
With [BSD locks](https://man.freebsd.org/cgi/man.cgi?query=flock&sektion=2)
a WAL database can only be accessed by a single proccess.
Other processes that attempt to access a database locked with BSD locks,
will fail with the [`SQLITE_PROTOCOL`](https://sqlite.org/rescode.html#protocol) error code.
On Windows, this package may use `MapViewOfFile`, like SQLite.
You can also opt into a cross-platform, in-process, memory sharing implementation
@@ -91,7 +86,6 @@ The implementation is compatible with SQLite's
The VFS can be customized with a few build tags:
- `sqlite3_flock` forces the use of BSD locks.
- `sqlite3_dotlk` forces the use of dot-file locks.
- `sqlite3_nosys` prevents importing [`x/sys`](https://pkg.go.dev/golang.org/x/sys).
> [!IMPORTANT]
> The default configuration of this package is compatible with the standard

View File

@@ -1,4 +1,4 @@
//go:build ((linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos) && !sqlite3_nosys) || sqlite3_flock || sqlite3_dotlk
//go:build linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos || sqlite3_flock || sqlite3_dotlk
package adiantum_test

View File

@@ -1,4 +1,4 @@
//go:build ((linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos) && !sqlite3_nosys) || sqlite3_flock || sqlite3_dotlk
//go:build linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos || sqlite3_flock || sqlite3_dotlk
package vfs
@@ -20,12 +20,10 @@ const (
)
func (f *vfsFile) Lock(lock LockLevel) error {
// Argument check. SQLite never explicitly requests a pending lock.
if lock != LOCK_SHARED && lock != LOCK_RESERVED && lock != LOCK_EXCLUSIVE {
panic(util.AssertErr())
}
switch {
case lock != LOCK_SHARED && lock != LOCK_RESERVED && lock != LOCK_EXCLUSIVE:
// Argument check. SQLite never explicitly requests a pending lock.
panic(util.AssertErr())
case f.lock < LOCK_NONE || f.lock > LOCK_EXCLUSIVE:
// Connection state check.
panic(util.AssertErr())
@@ -87,13 +85,12 @@ func (f *vfsFile) Lock(lock LockLevel) error {
}
func (f *vfsFile) Unlock(lock LockLevel) error {
// Argument check.
if lock != LOCK_NONE && lock != LOCK_SHARED {
switch {
case lock != LOCK_NONE && lock != LOCK_SHARED:
// Argument check.
panic(util.AssertErr())
}
// Connection state check.
if f.lock < LOCK_NONE || f.lock > LOCK_EXCLUSIVE {
case f.lock < LOCK_NONE || f.lock > LOCK_EXCLUSIVE:
// Connection state check.
panic(util.AssertErr())
}

View File

@@ -1,4 +1,4 @@
//go:build !(((linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos) && !sqlite3_nosys) || sqlite3_flock || sqlite3_dotlk)
//go:build !(linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos || sqlite3_flock || sqlite3_dotlk)
package vfs

View File

@@ -1,4 +1,4 @@
//go:build ((freebsd || openbsd || netbsd || dragonfly || illumos) && !(sqlite3_dotlk || sqlite3_nosys)) || sqlite3_flock
//go:build ((freebsd || openbsd || netbsd || dragonfly || illumos) && !sqlite3_dotlk) || sqlite3_flock
package vfs
@@ -9,11 +9,11 @@ import (
)
func osGetSharedLock(file *os.File) _ErrorCode {
return osLock(file, unix.LOCK_SH|unix.LOCK_NB, _IOERR_RDLOCK)
return osFlock(file, unix.LOCK_SH|unix.LOCK_NB, _IOERR_RDLOCK)
}
func osGetReservedLock(file *os.File) _ErrorCode {
rc := osLock(file, unix.LOCK_EX|unix.LOCK_NB, _IOERR_LOCK)
rc := osFlock(file, unix.LOCK_EX|unix.LOCK_NB, _IOERR_LOCK)
if rc == _BUSY {
// The documentation states that a lock is upgraded by
// releasing the previous lock, then acquiring the new lock.
@@ -37,7 +37,7 @@ func osGetExclusiveLock(file *os.File, state *LockLevel) _ErrorCode {
}
func osDowngradeLock(file *os.File, _ LockLevel) _ErrorCode {
rc := osLock(file, unix.LOCK_SH|unix.LOCK_NB, _IOERR_RDLOCK)
rc := osFlock(file, unix.LOCK_SH|unix.LOCK_NB, _IOERR_RDLOCK)
if rc == _BUSY {
// The documentation states that a lock is downgraded by
// releasing the previous lock then acquiring the new lock.
@@ -66,7 +66,36 @@ func osCheckReservedLock(file *os.File) (bool, _ErrorCode) {
return lock == unix.F_WRLCK, rc
}
func osLock(file *os.File, how int, def _ErrorCode) _ErrorCode {
func osFlock(file *os.File, how int, def _ErrorCode) _ErrorCode {
err := unix.Flock(int(file.Fd()), how)
return osLockErrorCode(err, def)
}
func osReadLock(file *os.File, start, len int64) _ErrorCode {
return osLock(file, unix.F_RDLCK, start, len, _IOERR_RDLOCK)
}
func osWriteLock(file *os.File, start, len int64) _ErrorCode {
return osLock(file, unix.F_WRLCK, start, len, _IOERR_LOCK)
}
func osLock(file *os.File, typ int16, start, len int64, def _ErrorCode) _ErrorCode {
err := unix.FcntlFlock(file.Fd(), unix.F_SETLK, &unix.Flock_t{
Type: typ,
Start: start,
Len: len,
})
return osLockErrorCode(err, def)
}
func osUnlock(file *os.File, start, len int64) _ErrorCode {
err := unix.FcntlFlock(file.Fd(), unix.F_SETLK, &unix.Flock_t{
Type: unix.F_UNLCK,
Start: start,
Len: len,
})
if err != nil {
return _IOERR_UNLOCK
}
return _OK
}

View File

@@ -1,4 +1,4 @@
//go:build !(sqlite3_flock || sqlite3_nosys)
//go:build !sqlite3_flock
package vfs
@@ -56,16 +56,12 @@ func osAllocate(file *os.File, size int64) error {
return file.Truncate(size)
}
func osUnlock(file *os.File, start, len int64) _ErrorCode {
err := unix.FcntlFlock(file.Fd(), _F_OFD_SETLK, &unix.Flock_t{
Type: unix.F_UNLCK,
Start: start,
Len: len,
})
if err != nil {
return _IOERR_UNLOCK
}
return _OK
func osReadLock(file *os.File, start, len int64, timeout time.Duration) _ErrorCode {
return osLock(file, unix.F_RDLCK, start, len, timeout, _IOERR_RDLOCK)
}
func osWriteLock(file *os.File, start, len int64, timeout time.Duration) _ErrorCode {
return osLock(file, unix.F_WRLCK, start, len, timeout, _IOERR_LOCK)
}
func osLock(file *os.File, typ int16, start, len int64, timeout time.Duration, def _ErrorCode) _ErrorCode {
@@ -88,10 +84,14 @@ func osLock(file *os.File, typ int16, start, len int64, timeout time.Duration, d
return osLockErrorCode(err, def)
}
func osReadLock(file *os.File, start, len int64, timeout time.Duration) _ErrorCode {
return osLock(file, unix.F_RDLCK, start, len, timeout, _IOERR_RDLOCK)
}
func osWriteLock(file *os.File, start, len int64, timeout time.Duration) _ErrorCode {
return osLock(file, unix.F_WRLCK, start, len, timeout, _IOERR_LOCK)
func osUnlock(file *os.File, start, len int64) _ErrorCode {
err := unix.FcntlFlock(file.Fd(), _F_OFD_SETLK, &unix.Flock_t{
Type: unix.F_UNLCK,
Start: start,
Len: len,
})
if err != nil {
return _IOERR_UNLOCK
}
return _OK
}

View File

@@ -7,6 +7,8 @@ import (
"io/fs"
"os"
"sync"
"github.com/ncruces/go-sqlite3/internal/dotlk"
)
var (
@@ -28,12 +30,10 @@ func osGetSharedLock(file *os.File) _ErrorCode {
name := file.Name()
locker := vfsDotLocks[name]
if locker == nil {
f, err := os.OpenFile(name+".lock", os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
f.Close()
if errors.Is(err, fs.ErrExist) {
return _BUSY // Another process has the lock.
}
if err != nil {
if err := dotlk.TryLock(name + ".lock"); err != nil {
if errors.Is(err, fs.ErrExist) {
return _BUSY // Another process has the lock.
}
return _IOERR_LOCK
}
locker = &vfsDotLocker{}
@@ -114,8 +114,7 @@ func osReleaseLock(file *os.File, state LockLevel) _ErrorCode {
}
if locker.shared == 1 {
err := os.Remove(name + ".lock")
if err != nil && !errors.Is(err, fs.ErrNotExist) {
if err := dotlk.Unlock(name + ".lock"); err != nil {
return _IOERR_UNLOCK
}
delete(vfsDotLocks, name)

View File

@@ -1,4 +1,4 @@
//go:build (amd64 || arm64 || riscv64) && !sqlite3_nosys
//go:build amd64 || arm64 || riscv64
package vfs

View File

@@ -1,9 +1,8 @@
//go:build !(sqlite3_flock || sqlite3_nosys)
//go:build !sqlite3_flock
package vfs
import (
"math/rand"
"os"
"time"
@@ -22,6 +21,30 @@ func osAllocate(file *os.File, size int64) error {
return unix.Fallocate(int(file.Fd()), 0, 0, size)
}
func osReadLock(file *os.File, start, len int64, timeout time.Duration) _ErrorCode {
return osLock(file, unix.F_RDLCK, start, len, timeout, _IOERR_RDLOCK)
}
func osWriteLock(file *os.File, start, len int64, timeout time.Duration) _ErrorCode {
return osLock(file, unix.F_WRLCK, start, len, timeout, _IOERR_LOCK)
}
func osLock(file *os.File, typ int16, start, len int64, timeout time.Duration, def _ErrorCode) _ErrorCode {
lock := unix.Flock_t{
Type: typ,
Start: start,
Len: len,
}
var err error
switch {
case timeout < 0:
err = unix.FcntlFlock(file.Fd(), unix.F_OFD_SETLKW, &lock)
default:
err = unix.FcntlFlock(file.Fd(), unix.F_OFD_SETLK, &lock)
}
return osLockErrorCode(err, def)
}
func osUnlock(file *os.File, start, len int64) _ErrorCode {
err := unix.FcntlFlock(file.Fd(), unix.F_OFD_SETLK, &unix.Flock_t{
Type: unix.F_UNLCK,
@@ -33,40 +56,3 @@ func osUnlock(file *os.File, start, len int64) _ErrorCode {
}
return _OK
}
func osLock(file *os.File, typ int16, start, len int64, timeout time.Duration, def _ErrorCode) _ErrorCode {
lock := unix.Flock_t{
Type: typ,
Start: start,
Len: len,
}
var err error
switch {
case timeout == 0:
err = unix.FcntlFlock(file.Fd(), unix.F_OFD_SETLK, &lock)
case timeout < 0:
err = unix.FcntlFlock(file.Fd(), unix.F_OFD_SETLKW, &lock)
default:
before := time.Now()
for {
err = unix.FcntlFlock(file.Fd(), unix.F_OFD_SETLK, &lock)
if errno, _ := err.(unix.Errno); errno != unix.EAGAIN {
break
}
if time.Since(before) > timeout {
break
}
const sleepIncrement = 1024*1024 - 1 // power of two, ~1ms
time.Sleep(time.Duration(rand.Int63() & sleepIncrement))
}
}
return osLockErrorCode(err, def)
}
func osReadLock(file *os.File, start, len int64, timeout time.Duration) _ErrorCode {
return osLock(file, unix.F_RDLCK, start, len, timeout, _IOERR_RDLOCK)
}
func osWriteLock(file *os.File, start, len int64, timeout time.Duration) _ErrorCode {
return osLock(file, unix.F_WRLCK, start, len, timeout, _IOERR_LOCK)
}

View File

@@ -1,4 +1,4 @@
//go:build (linux || darwin) && !(sqlite3_flock || sqlite3_dotlk || sqlite3_nosys)
//go:build (linux || darwin) && !(sqlite3_flock || sqlite3_dotlk)
package vfs

View File

@@ -1,4 +1,4 @@
//go:build !unix || sqlite3_nosys
//go:build !unix
package vfs

View File

@@ -1,4 +1,4 @@
//go:build !(linux || darwin) || sqlite3_flock || sqlite3_nosys
//go:build !(linux || darwin) || sqlite3_flock
package vfs

View File

@@ -1,4 +1,4 @@
//go:build !linux || !(amd64 || arm64 || riscv64) || sqlite3_nosys
//go:build !linux || !(amd64 || arm64 || riscv64)
package vfs

View File

@@ -1,4 +1,4 @@
//go:build !(linux || darwin) || sqlite3_flock || sqlite3_nosys
//go:build !(linux || darwin) || sqlite3_flock
package vfs

View File

@@ -1,4 +1,4 @@
//go:build unix && !sqlite3_nosys
//go:build unix
package vfs

View File

@@ -1,4 +1,4 @@
//go:build unix && !(sqlite3_flock || sqlite3_nosys)
//go:build unix && !sqlite3_flock
package vfs

View File

@@ -1,9 +1,8 @@
//go:build !(sqlite3_dotlk || sqlite3_nosys)
//go:build !sqlite3_dotlk
package vfs
import (
"math/rand"
"os"
"time"
@@ -46,7 +45,8 @@ func osGetExclusiveLock(file *os.File, state *LockLevel) _ErrorCode {
osUnlock(file, _SHARED_FIRST, _SHARED_SIZE)
// Acquire the EXCLUSIVE lock.
rc := osWriteLock(file, _SHARED_FIRST, _SHARED_SIZE, time.Millisecond)
// Can't wait here, because the file is not OVERLAPPED.
rc := osWriteLock(file, _SHARED_FIRST, _SHARED_SIZE, 0)
if rc != _OK {
// Reacquire the SHARED lock.
@@ -107,6 +107,27 @@ func osCheckReservedLock(file *os.File) (bool, _ErrorCode) {
return false, rc
}
func osReadLock(file *os.File, start, len uint32, timeout time.Duration) _ErrorCode {
return osLock(file, 0, start, len, timeout, _IOERR_RDLOCK)
}
func osWriteLock(file *os.File, start, len uint32, timeout time.Duration) _ErrorCode {
return osLock(file, windows.LOCKFILE_EXCLUSIVE_LOCK, start, len, timeout, _IOERR_LOCK)
}
func osLock(file *os.File, flags, start, len uint32, timeout time.Duration, def _ErrorCode) _ErrorCode {
var err error
switch {
case timeout == 0:
err = osLockEx(file, flags|windows.LOCKFILE_FAIL_IMMEDIATELY, start, len)
case timeout < 0:
err = osLockEx(file, flags, start, len)
default:
err = osLockExTimeout(file, flags, start, len, timeout)
}
return osLockErrorCode(err, def)
}
func osUnlock(file *os.File, start, len uint32) _ErrorCode {
err := windows.UnlockFileEx(windows.Handle(file.Fd()),
0, len, 0, &windows.Overlapped{Offset: start})
@@ -119,41 +140,40 @@ func osUnlock(file *os.File, start, len uint32) _ErrorCode {
return _OK
}
func osLock(file *os.File, flags, start, len uint32, timeout time.Duration, def _ErrorCode) _ErrorCode {
var err error
switch {
case timeout == 0:
err = osLockEx(file, flags|windows.LOCKFILE_FAIL_IMMEDIATELY, start, len)
case timeout < 0:
err = osLockEx(file, flags, start, len)
default:
before := time.Now()
for {
err = osLockEx(file, flags|windows.LOCKFILE_FAIL_IMMEDIATELY, start, len)
if errno, _ := err.(windows.Errno); errno != windows.ERROR_LOCK_VIOLATION {
break
}
if time.Since(before) > timeout {
break
}
const sleepIncrement = 1024*1024 - 1 // power of two, ~1ms
time.Sleep(time.Duration(rand.Int63() & sleepIncrement))
}
}
return osLockErrorCode(err, def)
}
func osLockEx(file *os.File, flags, start, len uint32) error {
return windows.LockFileEx(windows.Handle(file.Fd()), flags,
0, len, 0, &windows.Overlapped{Offset: start})
}
func osReadLock(file *os.File, start, len uint32, timeout time.Duration) _ErrorCode {
return osLock(file, 0, start, len, timeout, _IOERR_RDLOCK)
}
func osLockExTimeout(file *os.File, flags, start, len uint32, timeout time.Duration) error {
event, err := windows.CreateEvent(nil, 1, 0, nil)
if err != nil {
return err
}
defer windows.CloseHandle(event)
func osWriteLock(file *os.File, start, len uint32, timeout time.Duration) _ErrorCode {
return osLock(file, windows.LOCKFILE_EXCLUSIVE_LOCK, start, len, timeout, _IOERR_LOCK)
fd := windows.Handle(file.Fd())
overlapped := &windows.Overlapped{
Offset: start,
HEvent: event,
}
err = windows.LockFileEx(fd, flags, 0, len, 0, overlapped)
if err != windows.ERROR_IO_PENDING {
return err
}
ms := (timeout + time.Millisecond - 1) / time.Millisecond
rc, err := windows.WaitForSingleObject(event, uint32(ms))
if rc == windows.WAIT_OBJECT_0 {
return nil
}
defer windows.CancelIoEx(fd, overlapped)
if err != nil {
return err
}
return windows.Errno(rc)
}
func osLockErrorCode(err error, def _ErrorCode) _ErrorCode {
@@ -165,8 +185,9 @@ func osLockErrorCode(err error, def _ErrorCode) _ErrorCode {
switch errno {
case
windows.ERROR_LOCK_VIOLATION,
windows.ERROR_OPERATION_ABORTED,
windows.ERROR_IO_PENDING,
windows.ERROR_OPERATION_ABORTED:
windows.WAIT_TIMEOUT:
return _BUSY
}
}

View File

@@ -1,4 +1,4 @@
//go:build ((linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !sqlite3_nosys) || sqlite3_flock || sqlite3_dotlk
//go:build ((linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le)) || sqlite3_flock || sqlite3_dotlk
package vfs

View File

@@ -1,10 +1,12 @@
//go:build ((freebsd || openbsd || netbsd || dragonfly || illumos) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !(sqlite3_dotlk || sqlite3_nosys)) || sqlite3_flock
//go:build ((freebsd || openbsd || netbsd || dragonfly || illumos) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !sqlite3_dotlk) || sqlite3_flock
package vfs
import (
"context"
"errors"
"io"
"io/fs"
"os"
"sync"
@@ -20,7 +22,7 @@ type vfsShmParent struct {
refs int // +checklocks:vfsShmListMtx
lock [_SHM_NLOCK]int16 // +checklocks:Mutex
lock [_SHM_NLOCK]int8 // +checklocks:Mutex
sync.Mutex
}
@@ -71,23 +73,21 @@ func (s *vfsShm) shmOpen() _ErrorCode {
return _OK
}
// Always open file read-write, as it will be shared.
f, err := os.OpenFile(s.path,
unix.O_RDWR|unix.O_CREAT|unix.O_NOFOLLOW, 0666)
if err != nil {
return _CANTOPEN
}
// Closes file if it's not nil.
var f *os.File
// Close file on error.
// Keep this here to avoid confusing checklocks.
defer func() { f.Close() }()
fi, err := f.Stat()
if err != nil {
return _IOERR_FSTAT
}
vfsShmListMtx.Lock()
defer vfsShmListMtx.Unlock()
// Stat file without opening it.
// Closing it would release all POSIX locks on it.
fi, err := os.Stat(s.path)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return _IOERR_FSTAT
}
// Find a shared file, increase the reference count.
for _, g := range vfsShmList {
if g != nil && os.SameFile(fi, g.info) {
@@ -97,13 +97,33 @@ func (s *vfsShm) shmOpen() _ErrorCode {
}
}
// Lock and truncate the file.
// The lock is only released by closing the file.
if rc := osLock(f, unix.LOCK_EX|unix.LOCK_NB, _IOERR_LOCK); rc != _OK {
// Always open file read-write, as it will be shared.
f, err = os.OpenFile(s.path,
os.O_RDWR|os.O_CREATE|_O_NOFOLLOW, 0666)
if err != nil {
return _CANTOPEN
}
// Dead man's switch.
if lock, rc := osTestLock(f, _SHM_DMS, 1); rc != _OK {
return _IOERR_LOCK
} else if lock == unix.F_WRLCK {
return _BUSY
} else if lock == unix.F_UNLCK {
if rc := osWriteLock(f, _SHM_DMS, 1); rc != _OK {
return rc
}
if err := f.Truncate(0); err != nil {
return _IOERR_SHMOPEN
}
}
if rc := osReadLock(f, _SHM_DMS, 1); rc != _OK {
return rc
}
if err := f.Truncate(0); err != nil {
return _IOERR_SHMOPEN
fi, err = f.Stat()
if err != nil {
return _IOERR_FSTAT
}
// Add the new shared file.
@@ -157,7 +177,42 @@ func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, ext
func (s *vfsShm) shmLock(offset, n int32, flags _ShmFlag) _ErrorCode {
s.Lock()
defer s.Unlock()
return s.shmMemLock(offset, n, flags)
// Check if we could obtain/release the lock locally.
rc := s.shmMemLock(offset, n, flags)
if rc != _OK {
return rc
}
// Obtain/release the appropriate file locks.
switch {
case flags&_SHM_UNLOCK != 0:
begin, end := offset, offset+n
for i := begin; i < end; i++ {
if s.vfsShmParent.lock[i] != 0 {
if i > begin {
rc |= osUnlock(s.File, _SHM_BASE+int64(begin), int64(i-begin))
}
begin = i + 1
}
}
if end > begin {
rc |= osUnlock(s.File, _SHM_BASE+int64(begin), int64(end-begin))
}
return rc
case flags&_SHM_SHARED != 0:
rc = osReadLock(s.File, _SHM_BASE+int64(offset), int64(n))
case flags&_SHM_EXCLUSIVE != 0:
rc = osWriteLock(s.File, _SHM_BASE+int64(offset), int64(n))
default:
panic(util.AssertErr())
}
// Release the local lock we had acquired.
if rc != _OK {
s.shmMemLock(offset, n, flags^(_SHM_UNLOCK|_SHM_LOCK))
}
return rc
}
func (s *vfsShm) shmUnmap(delete bool) {

View File

@@ -1,4 +1,4 @@
//go:build (windows && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !sqlite3_nosys) || sqlite3_dotlk
//go:build (windows && (386 || arm || amd64 || arm64 || riscv64 || ppc64le)) || sqlite3_dotlk
package vfs
@@ -13,7 +13,7 @@ const (
_WALINDEX_PGSZ = 32768
)
// This looks like a safe way of keeping the WAL-index in sync.
// This seems a safe way of keeping the WAL-index in sync.
//
// The WAL-index file starts with a header,
// and the index doesn't meaningfully change if the header doesn't change.
@@ -27,7 +27,7 @@ const (
//
// Since all the data is either redundant+checksummed,
// 4 byte aligned, or modified under an exclusive lock,
// the copies below should correctly keep copies in sync.
// the copies below should correctly keep memory in sync.
//
// https://sqlite.org/walformat.html#the_wal_index_file_format
@@ -35,7 +35,7 @@ func (s *vfsShm) shmAcquire(ptr *_ErrorCode) {
if ptr != nil && *ptr != _OK {
return
}
if len(s.ptrs) == 0 || shmUnmodified(s.shadow[0][:], s.shared[0][:]) {
if len(s.ptrs) == 0 || shmEqual(s.shadow[0][:], s.shared[0][:]) {
return
}
// Copies modified words from shared to private memory.
@@ -53,7 +53,7 @@ func (s *vfsShm) shmAcquire(ptr *_ErrorCode) {
}
func (s *vfsShm) shmRelease() {
if len(s.ptrs) == 0 || shmUnmodified(s.shadow[0][:], util.View(s.mod, s.ptrs[0], _WALINDEX_HDR_SIZE)) {
if len(s.ptrs) == 0 || shmEqual(s.shadow[0][:], util.View(s.mod, s.ptrs[0], _WALINDEX_HDR_SIZE)) {
return
}
// Copies modified words from private to shared memory.
@@ -82,6 +82,6 @@ func shmPage(s []byte) *[_WALINDEX_PGSZ / 4]uint32 {
return (*[_WALINDEX_PGSZ / 4]uint32)(unsafe.Slice(p, _WALINDEX_PGSZ/4))
}
func shmUnmodified(v1, v2 []byte) bool {
func shmEqual(v1, v2 []byte) bool {
return *(*[_WALINDEX_HDR_SIZE]byte)(v1[:]) == *(*[_WALINDEX_HDR_SIZE]byte)(v2[:])
}

View File

@@ -6,18 +6,19 @@ import (
"context"
"errors"
"io/fs"
"os"
"sync"
"github.com/ncruces/go-sqlite3/internal/util"
"github.com/tetratelabs/wazero/api"
"github.com/ncruces/go-sqlite3/internal/dotlk"
"github.com/ncruces/go-sqlite3/internal/util"
)
type vfsShmParent struct {
shared [][_WALINDEX_PGSZ]byte
refs int // +checklocks:vfsShmListMtx
lock [_SHM_NLOCK]int16 // +checklocks:Mutex
lock [_SHM_NLOCK]int8 // +checklocks:Mutex
sync.Mutex
}
@@ -57,8 +58,7 @@ func (s *vfsShm) Close() error {
return nil
}
err := os.Remove(s.path)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
if err := dotlk.Unlock(s.path); err != nil {
return _IOERR_UNLOCK
}
delete(vfsShmList, s.path)
@@ -81,9 +81,8 @@ func (s *vfsShm) shmOpen() _ErrorCode {
return _OK
}
// Create a directory on disk to ensure only this process
// uses this path to register a shared memory.
err := os.Mkdir(s.path, 0777)
// Dead man's switch.
err := dotlk.LockShm(s.path)
if errors.Is(err, fs.ErrExist) {
return _BUSY
}

View File

@@ -1,4 +1,4 @@
//go:build ((freebsd || openbsd || netbsd || dragonfly || illumos) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !sqlite3_nosys) || sqlite3_flock || sqlite3_dotlk
//go:build ((freebsd || openbsd || netbsd || dragonfly || illumos) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le)) || sqlite3_flock || sqlite3_dotlk
package vfs
@@ -10,9 +10,6 @@ func (s *vfsShm) shmMemLock(offset, n int32, flags _ShmFlag) _ErrorCode {
case flags&_SHM_UNLOCK != 0:
for i := offset; i < offset+n; i++ {
if s.lock[i] {
if s.vfsShmParent.lock[i] == 0 {
panic(util.AssertErr())
}
if s.vfsShmParent.lock[i] <= 0 {
s.vfsShmParent.lock[i] = 0
} else {
@@ -23,20 +20,21 @@ func (s *vfsShm) shmMemLock(offset, n int32, flags _ShmFlag) _ErrorCode {
}
case flags&_SHM_SHARED != 0:
for i := offset; i < offset+n; i++ {
if s.lock[i] {
panic(util.AssertErr())
}
if s.vfsShmParent.lock[i]+1 <= 0 {
if !s.lock[i] &&
s.vfsShmParent.lock[i]+1 <= 0 {
return _BUSY
}
}
for i := offset; i < offset+n; i++ {
s.vfsShmParent.lock[i]++
s.lock[i] = true
if !s.lock[i] {
s.vfsShmParent.lock[i]++
s.lock[i] = true
}
}
case flags&_SHM_EXCLUSIVE != 0:
for i := offset; i < offset+n; i++ {
if s.lock[i] {
// SQLite never requests an exclusive lock that it already holds.
panic(util.AssertErr())
}
if s.vfsShmParent.lock[i] != 0 {

View File

@@ -1,4 +1,4 @@
//go:build (linux || darwin) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !(sqlite3_flock || sqlite3_dotlk || sqlite3_nosys)
//go:build (linux || darwin) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !(sqlite3_flock || sqlite3_dotlk)
package vfs
@@ -20,6 +20,7 @@ type vfsShm struct {
path string
regions []*util.MappedRegion
readOnly bool
fileLock bool
blocking bool
sync.Mutex
}
@@ -29,10 +30,10 @@ var _ blockingSharedMemory = &vfsShm{}
func (s *vfsShm) shmOpen() _ErrorCode {
if s.File == nil {
f, err := os.OpenFile(s.path,
unix.O_RDWR|unix.O_CREAT|unix.O_NOFOLLOW, 0666)
os.O_RDWR|os.O_CREATE|_O_NOFOLLOW, 0666)
if err != nil {
f, err = os.OpenFile(s.path,
unix.O_RDONLY|unix.O_CREAT|unix.O_NOFOLLOW, 0666)
os.O_RDONLY|os.O_CREATE|_O_NOFOLLOW, 0666)
s.readOnly = true
}
if err != nil {
@@ -40,6 +41,9 @@ func (s *vfsShm) shmOpen() _ErrorCode {
}
s.File = f
}
if s.fileLock {
return _OK
}
// Dead man's switch.
if lock, rc := osTestLock(s.File, _SHM_DMS, 1); rc != _OK {
@@ -64,7 +68,9 @@ func (s *vfsShm) shmOpen() _ErrorCode {
return _IOERR_SHMOPEN
}
}
return osReadLock(s.File, _SHM_DMS, 1, time.Millisecond)
rc := osReadLock(s.File, _SHM_DMS, 1, time.Millisecond)
s.fileLock = rc == _OK
return rc
}
func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, extend bool) (uint32, _ErrorCode) {
@@ -104,7 +110,12 @@ func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, ext
func (s *vfsShm) shmLock(offset, n int32, flags _ShmFlag) _ErrorCode {
// Argument check.
if n <= 0 || offset < 0 || offset+n > _SHM_NLOCK {
switch {
case n <= 0:
panic(util.AssertErr())
case offset < 0 || offset+n > _SHM_NLOCK:
panic(util.AssertErr())
case n != 1 && flags&_SHM_EXCLUSIVE == 0:
panic(util.AssertErr())
}
switch flags {
@@ -117,9 +128,6 @@ func (s *vfsShm) shmLock(offset, n int32, flags _ShmFlag) _ErrorCode {
default:
panic(util.AssertErr())
}
if n != 1 && flags&_SHM_EXCLUSIVE == 0 {
panic(util.AssertErr())
}
var timeout time.Duration
if s.blocking {

View File

@@ -1,4 +1,4 @@
//go:build !(((linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !sqlite3_nosys) || sqlite3_flock || sqlite3_dotlk)
//go:build !(((linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le)) || sqlite3_flock || sqlite3_dotlk)
package vfs

View File

@@ -1,4 +1,4 @@
//go:build (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !(sqlite3_dotlk || sqlite3_nosys)
//go:build (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !sqlite3_dotlk
package vfs
@@ -7,6 +7,7 @@ import (
"io"
"os"
"sync"
"syscall"
"time"
"github.com/tetratelabs/wazero/api"
@@ -27,6 +28,7 @@ type vfsShm struct {
shadow [][_WALINDEX_PGSZ]byte
ptrs []uint32
stack [1]uint64
fileLock bool
blocking bool
sync.Mutex
}
@@ -46,12 +48,16 @@ func (s *vfsShm) Close() error {
func (s *vfsShm) shmOpen() _ErrorCode {
if s.File == nil {
f, err := osutil.OpenFile(s.path, os.O_RDWR|os.O_CREATE, 0666)
f, err := osutil.OpenFile(s.path,
os.O_RDWR|os.O_CREATE|syscall.O_NONBLOCK, 0666)
if err != nil {
return _CANTOPEN
}
s.File = f
}
if s.fileLock {
return _OK
}
// Dead man's switch.
if rc := osWriteLock(s.File, _SHM_DMS, 1, 0); rc == _OK {
@@ -61,7 +67,9 @@ func (s *vfsShm) shmOpen() _ErrorCode {
return _IOERR_SHMOPEN
}
}
return osReadLock(s.File, _SHM_DMS, 1, time.Millisecond)
rc := osReadLock(s.File, _SHM_DMS, 1, time.Millisecond)
s.fileLock = rc == _OK
return rc
}
func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, extend bool) (_ uint32, rc _ErrorCode) {

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:802b6453e73f0e94773b93a0e5a7abfe427187e916a3258cd5093959def61bda
size 480661
oid sha256:e20f37d94223a88d8f94b3a20177c0fbf53392df2f9c59a28cc7f1f2b5d3de81
size 477370

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7ba2fd84fb88e54f60280c41bc7389c946ef1abc5cb9c795bbb32bc0f5760c65
size 493557
oid sha256:eebe395695c739a24e9cded13553b97d232eb268a5bc36f10f27cc13945e78cd
size 491003