Compare commits

..

42 Commits

Author SHA1 Message Date
Nuno Cruces
ea860e407d Docs. 2025-10-01 11:00:13 +01:00
Nuno Cruces
d4561d08f9 Refactor. 2025-10-01 10:48:54 +01:00
Nuno Cruces
14c1e490b4 Optimize fullfsync. 2025-09-30 12:54:18 +01:00
Nuno Cruces
23aad5f62f MVCC API. 2025-09-29 12:52:01 +01:00
Nuno Cruces
e5bd10a1ff Fix TestDB. 2025-09-29 12:44:09 +01:00
Nuno Cruces
5cf06c45f7 Scan improvements. 2025-09-24 18:13:26 +01:00
Nuno Cruces
08f9fc758a JSON experiment. 2025-09-24 18:13:21 +01:00
Nuno Cruces
b588d5f991 Error messages, test contexts. 2025-09-23 12:51:36 +01:00
Nuno Cruces
4c24bd0cb6 Verify download. 2025-09-23 11:49:59 +01:00
Nuno Cruces
cc353e4848 Time improvements. 2025-09-23 11:49:53 +01:00
Nuno Cruces
c3ebb04045 Use crypto/pbkdf2. 2025-09-18 18:41:10 +01:00
Nuno Cruces
11e064574c Weight-balanced trees. 2025-09-17 11:01:24 +01:00
Nuno Cruces
770420289a Updated dependencies. 2025-09-10 17:09:00 +01:00
Nuno Cruces
62f69011f1 Updated dependencies. 2025-09-08 13:59:41 +01:00
Nuno Cruces
4f9e3f900b binaryen-version_124. 2025-09-08 12:23:58 +01:00
dependabot[bot]
4e90618350 Bump actions/setup-go from 5 to 6 (#318)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '6'
  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>
2025-09-05 00:25:35 +02:00
Nuno Cruces
54bb94ce58 Improve WithMemoryCapacityFromMax (#317). 2025-09-03 09:03:59 +02:00
Nuno Cruces
07fec784e1 Grow memory geometrically. (#316) 2025-09-01 12:57:33 +02:00
dependabot[bot]
da4638cbff Bump actions/attest-build-provenance from 2 to 3 (#313)
Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 2 to 3.
- [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/v2...v3)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-version: '3'
  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>
2025-08-29 00:19:53 +02:00
dependabot[bot]
085872c2f3 Bump github.com/ncruces/aa from 0.3.1 to 0.3.2 (#311)
Bumps [github.com/ncruces/aa](https://github.com/ncruces/aa) from 0.3.1 to 0.3.2.
- [Commits](https://github.com/ncruces/aa/compare/v0.3.1...v0.3.2)

---
updated-dependencies:
- dependency-name: github.com/ncruces/aa
  dependency-version: 0.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-26 17:20:13 +02:00
dependabot[bot]
de49aa2b06 Bump github.com/ncruces/aa from 0.3.0 to 0.3.1 (#310)
Bumps [github.com/ncruces/aa](https://github.com/ncruces/aa) from 0.3.0 to 0.3.1.
- [Commits](https://github.com/ncruces/aa/compare/v0.3.0...v0.3.1)

---
updated-dependencies:
- dependency-name: github.com/ncruces/aa
  dependency-version: 0.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-23 05:21:11 +01:00
Nuno Cruces
1f3ad0165e SQLite 3.50.4. 2025-08-21 19:05:44 +01:00
Nuno Cruces
0bda48d1d9 Gorm v1.30.1. 2025-08-21 18:56:05 +01:00
Nuno Cruces
0026bc91aa MVCC memory VFS. (#309) 2025-08-21 18:44:40 +01:00
Nuno Cruces
d84ca9d627 Fix #308. 2025-08-16 19:45:10 +01:00
Nuno Cruces
5d14e01f94 Fix #304. 2025-08-16 19:27:00 +01:00
Nuno Cruces
342df983d4 Fix #305. 2025-08-14 23:46:48 +01:00
Nuno Cruces
00476fb1e2 Tests. 2025-08-14 15:04:10 +01:00
Nuno Cruces
8a64ee6eaa Implement RowsColumnScanner. 2025-08-14 01:42:00 +01:00
Nuno Cruces
8f9a8e2752 Learnings from truffle. 2025-08-13 13:10:50 +01:00
Nuno Cruces
d8880e4cee Fixes. 2025-08-13 03:34:21 +01:00
dependabot[bot]
4b154a842c Bump actions/checkout from 4 to 5 (#303)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  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>
2025-08-12 08:47:41 +01:00
dependabot[bot]
758a53e9bf Bump golang.org/x/crypto from 0.40.0 to 0.41.0 (#300)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.40.0 to 0.41.0.
- [Commits](https://github.com/golang/crypto/compare/v0.40.0...v0.41.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.41.0
  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>
2025-08-08 00:30:25 +01:00
Nuno Cruces
1a42b4c590 Better fuzzing. 2025-08-07 16:27:45 +01:00
Nuno Cruces
7e4ec1df1c Fix #299. 2025-08-07 01:52:01 +01:00
Nuno Cruces
2c582a1d66 VFS improvements. 2025-08-05 14:15:21 +01:00
Nuno Cruces
20a67ca669 WAL mode serdes. 2025-08-02 11:48:37 +01:00
Nuno Cruces
789e2dc136 wasi-sdk-27. 2025-07-29 16:50:07 +01:00
Nuno Cruces
0399f10c06 VFS improvements. 2025-07-23 09:57:53 +01:00
Nuno Cruces
75c6744b5b FreeBSD 14.3. 2025-07-22 23:47:22 +01:00
Nuno Cruces
754e806164 Tests. 2025-07-22 10:34:30 +01:00
Nuno Cruces
2640c9fb54 SQLite 3.50.3. 2025-07-17 19:42:01 +01:00
114 changed files with 2557 additions and 1224 deletions

View File

@@ -14,8 +14,8 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with: { go-version: stable }
- name: Benchmark

View File

@@ -17,13 +17,13 @@ jobs:
steps:
- uses: ilammy/msvc-dev-cmd@v1
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Build
shell: bash
run: .github/workflows/repro.sh
- uses: actions/attest-build-provenance@v2
- uses: actions/attest-build-provenance@v3
if: matrix.os == 'ubuntu-latest'
with:
subject-path: |

View File

@@ -30,8 +30,8 @@ jobs:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with: { go-version: stable }
- name: Format
@@ -98,14 +98,14 @@ jobs:
matrix:
os:
- name: freebsd
version: '14.2'
version: '14.3'
flags: '-test.v'
- name: netbsd
version: '10.1'
flags: '-test.v'
- name: freebsd
arch: arm64
version: '14.2'
version: '14.3'
flags: '-test.v -test.short'
- name: netbsd
arch: arm64
@@ -118,7 +118,7 @@ jobs:
needs: test
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Build
env:
@@ -128,7 +128,7 @@ jobs:
run: .github/workflows/build-test.sh
- name: Test
uses: cross-platform-actions/action@v0.28.0
uses: cross-platform-actions/action@v0.29.0
with:
operating_system: ${{ matrix.os.name }}
architecture: ${{ matrix.os.arch }}
@@ -155,7 +155,7 @@ jobs:
needs: test
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Build
env:
@@ -174,8 +174,8 @@ jobs:
steps:
- uses: bytecodealliance/actions/wasmtime/setup@v1
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with: { go-version: stable }
- name: Set path
@@ -195,8 +195,8 @@ jobs:
steps:
- uses: docker/setup-qemu-action@v3
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with: { go-version: stable }
- name: Test 386 (32-bit)
@@ -216,8 +216,8 @@ jobs:
needs: test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with: { go-version: stable }
- name: Test
@@ -228,8 +228,8 @@ jobs:
needs: test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with: { go-version: stable }
- name: Test

27
conn.go
View File

@@ -444,20 +444,27 @@ func (c *Conn) Status(op DBStatus, reset bool) (current, highwater int, err erro
// https://sqlite.org/c3ref/table_column_metadata.html
func (c *Conn) TableColumnMetadata(schema, table, column string) (declType, collSeq string, notNull, primaryKey, autoInc bool, err error) {
defer c.arena.mark()()
var schemaPtr, columnPtr ptr_t
declTypePtr := c.arena.new(ptrlen)
collSeqPtr := c.arena.new(ptrlen)
notNullPtr := c.arena.new(ptrlen)
autoIncPtr := c.arena.new(ptrlen)
primaryKeyPtr := c.arena.new(ptrlen)
var (
declTypePtr ptr_t
collSeqPtr ptr_t
notNullPtr ptr_t
primaryKeyPtr ptr_t
autoIncPtr ptr_t
columnPtr ptr_t
schemaPtr ptr_t
)
if column != "" {
declTypePtr = c.arena.new(ptrlen)
collSeqPtr = c.arena.new(ptrlen)
notNullPtr = c.arena.new(ptrlen)
primaryKeyPtr = c.arena.new(ptrlen)
autoIncPtr = c.arena.new(ptrlen)
columnPtr = c.arena.string(column)
}
if schema != "" {
schemaPtr = c.arena.string(schema)
}
tablePtr := c.arena.string(table)
if column != "" {
columnPtr = c.arena.string(column)
}
rc := res_t(c.call("sqlite3_table_column_metadata", stk_t(c.handle),
stk_t(schemaPtr), stk_t(tablePtr), stk_t(columnPtr),

View File

@@ -1,7 +1,6 @@
package sqlite3
import (
"encoding/json"
"errors"
"math"
"time"
@@ -173,21 +172,6 @@ func (ctx Context) ResultPointer(ptr any) {
stk_t(ctx.handle), stk_t(valPtr))
}
// ResultJSON sets the result of the function to the JSON encoding of value.
//
// https://sqlite.org/c3ref/result_blob.html
func (ctx Context) ResultJSON(value any) {
err := json.NewEncoder(callbackWriter(func(p []byte) (int, error) {
ctx.ResultRawText(p[:len(p)-1]) // remove the newline
return 0, nil
})).Encode(value)
if err != nil {
ctx.ResultError(err)
return // notest
}
}
// ResultValue sets the result of the function to a copy of [Value].
//
// https://sqlite.org/c3ref/result_blob.html

View File

@@ -604,26 +604,27 @@ func (r resultRowsAffected) RowsAffected() (int64, error) {
return int64(r), nil
}
type rows struct {
ctx context.Context
*stmt
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
_ANY scantype = iota
_INT
_REAL
_TEXT
_BLOB
_NULL
_BOOL
_TIME
_NOT_NULL
)
var (
_ [0]struct{} = [scantype(sqlite3.INTEGER) - _INT]struct{}{}
_ [0]struct{} = [scantype(sqlite3.FLOAT) - _REAL]struct{}{}
_ [0]struct{} = [scantype(sqlite3.TEXT) - _TEXT]struct{}{}
_ [0]struct{} = [scantype(sqlite3.BLOB) - _BLOB]struct{}{}
_ [0]struct{} = [scantype(sqlite3.NULL) - _NULL]struct{}{}
_ [0]struct{} = [_NOT_NULL & (_NOT_NULL - 1)]struct{}{}
)
func scanFromDecl(decl string) scantype {
@@ -648,10 +649,20 @@ func scanFromDecl(decl string) scantype {
return _ANY
}
type rows struct {
ctx context.Context
*stmt
names []string
types []string
scans []scantype
dest []driver.Value
}
var (
// Ensure these interfaces are implemented:
_ driver.RowsColumnTypeDatabaseTypeName = &rows{}
_ driver.RowsColumnTypeNullable = &rows{}
// _ driver.RowsColumnScanner = &rows{}
)
func (r *rows) Close() error {
@@ -674,34 +685,36 @@ func (r *rows) Columns() []string {
func (r *rows) scanType(index int) scantype {
if r.scans == nil {
count := r.Stmt.ColumnCount()
count := len(r.names)
scans := make([]scantype, count)
for i := range scans {
scans[i] = scanFromDecl(strings.ToUpper(r.Stmt.ColumnDeclType(i)))
}
r.scans = scans
}
return r.scans[index]
return r.scans[index] &^ _NOT_NULL
}
func (r *rows) loadColumnMetadata() {
if r.nulls == nil {
if r.types == nil {
c := r.Stmt.Conn()
count := r.Stmt.ColumnCount()
nulls := make([]bool, count)
count := len(r.names)
types := make([]string, count)
scans := make([]scantype, count)
for i := range nulls {
for i := range types {
var notnull bool
if col := r.Stmt.ColumnOriginName(i); col != "" {
types[i], _, nulls[i], _, _, _ = c.TableColumnMetadata(
types[i], _, notnull, _, _, _ = c.TableColumnMetadata(
r.Stmt.ColumnDatabaseName(i),
r.Stmt.ColumnTableName(i),
col)
types[i] = strings.ToUpper(types[i])
scans[i] = scanFromDecl(types[i])
if notnull {
scans[i] |= _NOT_NULL
}
}
}
r.nulls = nulls
r.types = types
r.scans = scans
}
@@ -720,15 +733,13 @@ func (r *rows) ColumnTypeDatabaseTypeName(index int) string {
func (r *rows) ColumnTypeNullable(index int) (nullable, ok bool) {
r.loadColumnMetadata()
if r.nulls[index] {
return false, true
}
return true, false
nullable = r.scans[index]&^_NOT_NULL == 0
return nullable, !nullable
}
func (r *rows) ColumnTypeScanType(index int) (typ reflect.Type) {
r.loadColumnMetadata()
scan := r.scans[index]
scan := r.scans[index] &^ _NOT_NULL
if r.Stmt.Busy() {
// SQLite is dynamically typed and we now have a row.
@@ -740,7 +751,7 @@ func (r *rows) ColumnTypeScanType(index int) (typ reflect.Type) {
switch {
case scan == _TIME && val != _BLOB && val != _NULL:
t := r.Stmt.ColumnTime(index, r.tmRead)
useValType = t == time.Time{}
useValType = t.IsZero()
case scan == _BOOL && val == _INT:
i := r.Stmt.ColumnInt64(index)
useValType = i != 0 && i != 1
@@ -771,6 +782,7 @@ func (r *rows) ColumnTypeScanType(index int) (typ reflect.Type) {
}
func (r *rows) Next(dest []driver.Value) error {
r.dest = nil
c := r.Stmt.Conn()
if old := c.SetInterrupt(r.ctx); old != r.ctx {
defer c.SetInterrupt(old)
@@ -789,18 +801,7 @@ func (r *rows) Next(dest []driver.Value) error {
}
for i := range dest {
scan := r.scanType(i)
switch v := dest[i].(type) {
case int64:
if scan == _BOOL {
switch v {
case 1:
dest[i] = true
case 0:
dest[i] = false
}
continue
}
case []byte:
if v, ok := dest[i].([]byte); ok {
if len(v) == cap(v) { // a BLOB
continue
}
@@ -815,18 +816,49 @@ func (r *rows) Next(dest []driver.Value) error {
}
}
dest[i] = string(v)
case float64:
break
default:
continue
}
if scan == _TIME {
switch scan {
case _TIME:
t, err := r.tmRead.Decode(dest[i])
if err == nil {
dest[i] = t
continue
}
case _BOOL:
switch dest[i] {
case int64(0):
dest[i] = false
case int64(1):
dest[i] = true
}
}
}
r.dest = dest
return nil
}
func (r *rows) ScanColumn(dest any, index int) (err error) {
// notest // Go 1.26
var tm *time.Time
var ok *bool
switch d := dest.(type) {
case *time.Time:
tm = d
case *sql.NullTime:
tm = &d.Time
ok = &d.Valid
case *sql.Null[time.Time]:
tm = &d.V
ok = &d.Valid
default:
return driver.ErrSkip
}
value := r.dest[index]
*tm, err = r.tmRead.Decode(value)
if ok != nil {
*ok = err == nil
if value == nil {
return nil
}
}
return err
}

View File

@@ -8,6 +8,7 @@ import (
"math"
"net/url"
"reflect"
"strings"
"testing"
"time"
@@ -33,7 +34,7 @@ func Test_Open_error(t *testing.T) {
func Test_Open_dir(t *testing.T) {
t.Parallel()
db, err := sql.Open("sqlite3", ".")
db, err := Open(".")
if err != nil {
t.Fatal(err)
}
@@ -50,11 +51,11 @@ func Test_Open_dir(t *testing.T) {
func Test_Open_pragma(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t, url.Values{
dsn := memdb.TestDB(t, url.Values{
"_pragma": {"busy_timeout(1000)"},
})
db, err := sql.Open("sqlite3", tmp)
db, err := Open(dsn)
if err != nil {
t.Fatal(err)
}
@@ -72,11 +73,11 @@ func Test_Open_pragma(t *testing.T) {
func Test_Open_pragma_invalid(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t, url.Values{
dsn := memdb.TestDB(t, url.Values{
"_pragma": {"busy_timeout 1000"},
})
db, err := sql.Open("sqlite3", tmp)
db, err := Open(dsn)
if err != nil {
t.Fatal(err)
}
@@ -100,12 +101,12 @@ func Test_Open_pragma_invalid(t *testing.T) {
func Test_Open_txLock(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t, url.Values{
dsn := memdb.TestDB(t, url.Values{
"_txlock": {"exclusive"},
"_pragma": {"busy_timeout(1000)"},
})
db, err := sql.Open("sqlite3", tmp)
db, err := Open(dsn)
if err != nil {
t.Fatal(err)
}
@@ -136,11 +137,11 @@ func Test_Open_txLock(t *testing.T) {
func Test_Open_txLock_invalid(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t, url.Values{
dsn := memdb.TestDB(t, url.Values{
"_txlock": {"xclusive"},
})
_, err := sql.Open("sqlite3", tmp+"_txlock=xclusive")
_, err := Open(dsn)
if err == nil {
t.Fatal("want error")
}
@@ -151,31 +152,28 @@ func Test_Open_txLock_invalid(t *testing.T) {
func Test_BeginTx(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t, url.Values{
dsn := memdb.TestDB(t, url.Values{
"_txlock": {"exclusive"},
"_pragma": {"busy_timeout(0)"},
})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
db, err := sql.Open("sqlite3", tmp)
db, err := Open(dsn)
if err != nil {
t.Fatal(err)
}
defer db.Close()
_, err = db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
_, err = db.BeginTx(t.Context(), &sql.TxOptions{Isolation: sql.LevelReadCommitted})
if err.Error() != string(util.IsolationErr) {
t.Error("want isolationErr")
}
tx1, err := db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
tx1, err := db.BeginTx(t.Context(), &sql.TxOptions{ReadOnly: true})
if err != nil {
t.Fatal(err)
}
tx2, err := db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
tx2, err := db.BeginTx(t.Context(), &sql.TxOptions{ReadOnly: true})
if err != nil {
t.Fatal(err)
}
@@ -201,9 +199,9 @@ func Test_BeginTx(t *testing.T) {
func Test_nested_context(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := sql.Open("sqlite3", tmp)
db, err := Open(dsn)
if err != nil {
t.Fatal(err)
}
@@ -236,7 +234,7 @@ func Test_nested_context(t *testing.T) {
want(outer, 0)
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
inner, err := tx.QueryContext(ctx, `SELECT value FROM generate_series(0)`)
@@ -259,9 +257,9 @@ func Test_nested_context(t *testing.T) {
func Test_Prepare(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := sql.Open("sqlite3", tmp)
db, err := Open(dsn)
if err != nil {
t.Fatal(err)
}
@@ -300,24 +298,21 @@ func Test_Prepare(t *testing.T) {
func Test_QueryRow_named(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
db, err := sql.Open("sqlite3", tmp)
db, err := Open(dsn)
if err != nil {
t.Fatal(err)
}
defer db.Close()
conn, err := db.Conn(ctx)
conn, err := db.Conn(t.Context())
if err != nil {
t.Fatal(err)
}
defer conn.Close()
stmt, err := conn.PrepareContext(ctx, `SELECT ?, ?5, :AAA, @AAA, $AAA`)
stmt, err := conn.PrepareContext(t.Context(), `SELECT ?, ?5, :AAA, @AAA, $AAA`)
if err != nil {
t.Fatal(err)
}
@@ -353,9 +348,9 @@ func Test_QueryRow_named(t *testing.T) {
func Test_QueryRow_blob_null(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := sql.Open("sqlite3", tmp)
db, err := Open(dsn)
if err != nil {
t.Fatal(err)
}
@@ -390,11 +385,11 @@ func Test_time(t *testing.T) {
for _, fmt := range []string{"auto", "sqlite", "rfc3339", time.ANSIC} {
t.Run(fmt, func(t *testing.T) {
tmp := memdb.TestDB(t, url.Values{
dsn := memdb.TestDB(t, url.Values{
"_timefmt": {fmt},
})
db, err := sql.Open("sqlite3", tmp)
db, err := Open(dsn)
if err != nil {
t.Fatal(err)
}
@@ -437,9 +432,9 @@ func Test_ColumnType_ScanType(t *testing.T) {
)
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := sql.Open("sqlite3", tmp)
db, err := Open(dsn)
if err != nil {
t.Fatal(err)
}
@@ -526,6 +521,39 @@ func Test_ColumnType_ScanType(t *testing.T) {
}
}
func Test_rows_ScanColumn(t *testing.T) {
t.Parallel()
dsn := memdb.TestDB(t)
db, err := Open(dsn)
if err != nil {
t.Fatal(err)
}
defer db.Close()
var tm time.Time
err = db.QueryRow(`SELECT NULL`).Scan(&tm)
if err == nil {
t.Error("want error")
}
// Go 1.26
err = db.QueryRow(`SELECT datetime()`).Scan(&tm)
if err != nil && !strings.HasPrefix(err.Error(), "sql: Scan error") {
t.Error(err)
}
var nt sql.NullTime
err = db.QueryRow(`SELECT NULL`).Scan(&nt)
if err != nil {
t.Error(err)
}
// Go 1.26
err = db.QueryRow(`SELECT datetime()`).Scan(&nt)
if err != nil && !strings.HasPrefix(err.Error(), "sql: Scan error") {
t.Error(err)
}
}
func Benchmark_loop(b *testing.B) {
db, err := Open(":memory:")
if err != nil {
@@ -539,12 +567,8 @@ func Benchmark_loop(b *testing.B) {
b.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
b.Cleanup(cancel)
b.ResetTimer()
for range b.N {
_, err := db.ExecContext(ctx,
for b.Loop() {
_, err := db.ExecContext(b.Context(),
`WITH RECURSIVE c(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM c WHERE x < 1000000) SELECT x FROM c;`)
if err != nil {
b.Fatal(err)

View File

@@ -1,7 +1,6 @@
package driver
import (
"context"
"database/sql/driver"
"slices"
"testing"
@@ -56,7 +55,7 @@ func Fuzz_notWhitespace(f *testing.F) {
t.SkipNow()
}
c, err := db.Conn(context.Background())
c, err := db.Conn(t.Context())
if err != nil {
t.Fatal(err)
}

View File

@@ -1,6 +1,6 @@
# Embeddable Wasm build of SQLite
This folder includes an embeddable Wasm build of SQLite 3.50.2 for use with
This folder includes an embeddable Wasm build of SQLite 3.50.4 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

@@ -7,16 +7,18 @@ ROOT=../../
BINARYEN="$ROOT/tools/binaryen/bin"
WASI_SDK="$ROOT/tools/wasi-sdk/bin"
trap 'rm -rf build/ sqlite/ bcw2.tmp' EXIT
trap 'rm -rf sqlite/ build/ bcw2.tmp' EXIT
mkdir -p sqlite/
mkdir -p build/ext/
cp "$ROOT"/sqlite3/*.[ch] build/
cp "$ROOT"/sqlite3/*.patch build/
cd sqlite/
# https://sqlite.org/src/info/a6f6fbe6173de8a2
curl -# https://sqlite.org/src/tarball/sqlite.tar.gz?r=a6f6fbe617 | tar xz
# https://sqlite.org/src/info/ba2174bdca7d1d1a
curl -#L https://github.com/sqlite/sqlite/archive/b46738f.tar.gz | tar xz --strip-components=1
# curl -#L https://sqlite.org/src/tarball/sqlite.tar.gz?r=ba2174bdca | tar xz --strip-components=1
cd sqlite
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
MSYS_NO_PATHCONV=1 nmake /f makefile.msc sqlite3.c "OPTS=-DSQLITE_ENABLE_UPDATE_DELETE_LIMIT -DSQLITE_ENABLE_ORDERED_SET_AGGREGATES"
else
@@ -49,7 +51,8 @@ cd ~-
-mmutable-globals -mnontrapping-fptoint \
-msimd128 -mbulk-memory -msign-ext \
-mreference-types -mmultivalue \
-fno-stack-protector -fno-stack-clash-protection \
-mno-extended-const \
-fno-stack-protector \
-Wl,--stack-first \
-Wl,--import-undefined \
-Wl,--initial-memory=327680 \
@@ -59,8 +62,9 @@ cd ~-
$(awk '{print "-Wl,--export="$0}' ../exports.txt)
"$BINARYEN/wasm-ctor-eval" -g -c _initialize bcw2.wasm -o bcw2.tmp
"$BINARYEN/wasm-opt" -g --strip --strip-producers -c -O3 \
bcw2.tmp -o bcw2.wasm --low-memory-unused \
"$BINARYEN/wasm-opt" -g bcw2.tmp -o bcw2.wasm \
--low-memory-unused --gufa --generate-global-effects --converge -O3 \
--enable-mutable-globals --enable-nontrapping-float-to-int \
--enable-simd --enable-bulk-memory --enable-sign-ext \
--enable-reference-types --enable-multivalue
--enable-reference-types --enable-multivalue \
--strip --strip-producers

View File

@@ -1,14 +1,12 @@
module github.com/ncruces/go-sqlite3/embed/bcw2
go 1.23.0
go 1.24.0
toolchain go1.24.0
require github.com/ncruces/go-sqlite3 v0.26.3
require github.com/ncruces/go-sqlite3 v0.29.0
require (
github.com/ncruces/julianday v1.0.0 // indirect
github.com/ncruces/sort v0.1.5 // indirect
github.com/tetratelabs/wazero v1.9.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/sys v0.36.0 // indirect
)

View File

@@ -1,12 +1,12 @@
github.com/ncruces/go-sqlite3 v0.26.3 h1:WFkQj4KNMhbqiBPGDrVpK74w1DzcxQu3wYpmdWAvfYM=
github.com/ncruces/go-sqlite3 v0.26.3/go.mod h1:XFTPtFIo1DmGCh+XVP8KGn9b/o2f+z0WZuT09x2N6eo=
github.com/ncruces/go-sqlite3 v0.29.0 h1:1tsLiagCoqZEfcHDeKsNSv5jvrY/Iu393pAnw2wLNJU=
github.com/ncruces/go-sqlite3 v0.29.0/go.mod h1:r1hSvYKPNJ+OlUA1O3r8o9LAawzPAlqeZiIdxTBBBJ0=
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
github.com/ncruces/sort v0.1.5 h1:fiFWXXAqKI8QckPf/6hu/bGFwcEPrirIOFaJqWujs4k=
github.com/ncruces/sort v0.1.5/go.mod h1:obJToO4rYr6VWP0Uw5FYymgYGt3Br4RXcs/JdKaXAPk=
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=

View File

@@ -17,7 +17,8 @@ trap 'rm -f sqlite3.tmp' EXIT
-mmutable-globals -mnontrapping-fptoint \
-msimd128 -mbulk-memory -msign-ext \
-mreference-types -mmultivalue \
-fno-stack-protector -fno-stack-clash-protection \
-mno-extended-const \
-fno-stack-protector \
-Wl,--stack-first \
-Wl,--import-undefined \
-Wl,--initial-memory=327680 \
@@ -26,8 +27,9 @@ trap 'rm -f sqlite3.tmp' EXIT
$(awk '{print "-Wl,--export="$0}' exports.txt)
"$BINARYEN/wasm-ctor-eval" -g -c _initialize sqlite3.wasm -o sqlite3.tmp
"$BINARYEN/wasm-opt" -g --strip --strip-producers -c -O3 \
sqlite3.tmp -o sqlite3.wasm --low-memory-unused \
"$BINARYEN/wasm-opt" -g sqlite3.tmp -o sqlite3.wasm \
--low-memory-unused --gufa --generate-global-effects --converge -O3 \
--enable-mutable-globals --enable-nontrapping-float-to-int \
--enable-simd --enable-bulk-memory --enable-sign-ext \
--enable-reference-types --enable-multivalue
--enable-reference-types --enable-multivalue \
--strip --strip-producers

View File

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

Binary file not shown.

View File

@@ -38,11 +38,11 @@ you can load into your database connections.
- [`github.com/ncruces/go-sqlite3/ext/zorder`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/ext/zorder)
maps multidimensional data to one dimension.
### Pakages
### Packages
These packages may also be useful to work with SQLite:
- [`github.com/ncruces/decimal`](https://pkg.go.dev/github.com/ncruces/decimal)
decimal arithmetic.
- [`github.com/ncruces/julianday`](https://pkg.go.dev/github.com/ncruces/julianday)
Julian day math.
Julian day math.

View File

@@ -88,9 +88,9 @@ func Example() {
func Test_cursor_Column(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, array.Register)
db, err := driver.Open(dsn, array.Register)
if err != nil {
t.Fatal(err)
}

View File

@@ -18,7 +18,7 @@ func Register(db *sqlite3.Conn) error {
return RegisterFS(db, nil)
}
// Register registers SQL functions readfile, lsmode,
// RegisterFS registers SQL functions readfile, lsmode,
// and the table-valued function fsdir;
// fsys will be used to read files and list directories.
func RegisterFS(db *sqlite3.Conn, fsys fs.FS) error {

View File

@@ -17,9 +17,9 @@ import (
func Test_lsmode(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, fileio.Register)
db, err := driver.Open(dsn, fileio.Register)
if err != nil {
t.Fatal(err)
}
@@ -53,9 +53,9 @@ func Test_readfile(t *testing.T) {
for _, fsys := range []fs.FS{nil, os.DirFS(".")} {
t.Run("", func(t *testing.T) {
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, func(c *sqlite3.Conn) error {
db, err := driver.Open(dsn, func(c *sqlite3.Conn) error {
fileio.RegisterFS(c, fsys)
return nil
})

View File

@@ -21,9 +21,9 @@ func Test_fsdir(t *testing.T) {
for _, fsys := range []fs.FS{nil, os.DirFS(".")} {
t.Run("", func(t *testing.T) {
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, func(c *sqlite3.Conn) error {
db, err := driver.Open(dsn, func(c *sqlite3.Conn) error {
fileio.RegisterFS(c, fsys)
return nil
})

View File

@@ -15,9 +15,9 @@ import (
func Test_writefile(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, Register)
db, err := driver.Open(dsn, Register)
if err != nil {
t.Fatal(err)
}

View File

@@ -4,6 +4,7 @@ import (
_ "crypto/md5"
_ "crypto/sha1"
_ "crypto/sha256"
_ "crypto/sha3"
_ "crypto/sha512"
"testing"
@@ -11,7 +12,6 @@ import (
_ "golang.org/x/crypto/blake2s"
_ "golang.org/x/crypto/md4"
_ "golang.org/x/crypto/ripemd160"
_ "golang.org/x/crypto/sha3"
"github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
@@ -21,7 +21,7 @@ import (
func TestRegister(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
tests := []struct {
name string
@@ -55,7 +55,7 @@ func TestRegister(t *testing.T) {
{"blake2b('', 256)", "0E5751C026E543B2E8AB2EB06099DAA1D1E5DF47778F7787FAAB45CDF12FE3A8"},
}
db, err := driver.Open(tmp, Register)
db, err := driver.Open(dsn, Register)
if err != nil {
t.Fatal(err)
}

View File

@@ -12,9 +12,9 @@ import (
func TestRegister(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, ipaddr.Register)
db, err := driver.Open(dsn, ipaddr.Register)
if err != nil {
t.Fatal(err)
}

View File

@@ -67,9 +67,9 @@ func Example() {
func Test_lines(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, lines.Register)
db, err := driver.Open(dsn, lines.Register)
if err != nil {
log.Fatal(err)
}
@@ -98,9 +98,9 @@ func Test_lines(t *testing.T) {
func Test_lines_error(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, lines.Register)
db, err := driver.Open(dsn, lines.Register)
if err != nil {
log.Fatal(err)
}
@@ -123,9 +123,9 @@ func Test_lines_error(t *testing.T) {
func Test_lines_read(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, lines.Register)
db, err := driver.Open(dsn, lines.Register)
if err != nil {
log.Fatal(err)
}
@@ -155,9 +155,9 @@ func Test_lines_read(t *testing.T) {
func Test_lines_test(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, lines.Register)
db, err := driver.Open(dsn, lines.Register)
if err != nil {
log.Fatal(err)
}

View File

@@ -15,9 +15,9 @@ import (
func TestRegister(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, Register)
db, err := driver.Open(dsn, Register)
if err != nil {
t.Fatal(err)
}
@@ -80,9 +80,9 @@ func TestRegister(t *testing.T) {
func TestRegister_errors(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, Register)
db, err := driver.Open(dsn, Register)
if err != nil {
t.Fatal(err)
}
@@ -107,9 +107,9 @@ func TestRegister_errors(t *testing.T) {
func TestRegister_pointer(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, Register)
db, err := driver.Open(dsn, Register)
if err != nil {
t.Fatal(err)
}

View File

@@ -2,9 +2,8 @@
package serdes
import (
"io"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/util/vfsutil"
"github.com/ncruces/go-sqlite3/vfs"
)
@@ -14,16 +13,16 @@ func init() {
vfs.Register(vfsName, sliceVFS{})
}
var fileToOpen = make(chan *sliceFile, 1)
var fileToOpen = make(chan *[]byte, 1)
// Serialize backs up a database into a byte slice.
//
// https://sqlite.org/c3ref/serialize.html
func Serialize(db *sqlite3.Conn, schema string) ([]byte, error) {
var file sliceFile
var file []byte
fileToOpen <- &file
err := db.Backup(schema, "file:serdes.db?vfs="+vfsName)
return file.data, err
err := db.Backup(schema, "file:serdes.db?nolock=1&vfs="+vfsName)
return file, err
}
// Deserialize restores a database from a byte slice,
@@ -41,8 +40,8 @@ func Serialize(db *sqlite3.Conn, schema string) ([]byte, error) {
// ["memdb"]: https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/memdb
// ["reader"]: https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/readervfs
func Deserialize(db *sqlite3.Conn, schema string, data []byte) error {
fileToOpen <- &sliceFile{data}
return db.Restore(schema, "file:serdes.db?vfs="+vfsName)
fileToOpen <- &data
return db.Restore(schema, "file:serdes.db?immutable=1&vfs="+vfsName)
}
type sliceVFS struct{}
@@ -53,14 +52,14 @@ func (sliceVFS) Open(name string, flags vfs.OpenFlag) (vfs.File, vfs.OpenFlag, e
}
select {
case file := <-fileToOpen:
return file, flags | vfs.OPEN_MEMORY, nil
return (*vfsutil.SliceFile)(file), flags | vfs.OPEN_MEMORY, nil
default:
return nil, flags, sqlite3.MISUSE
}
}
func (sliceVFS) Delete(name string, dirSync bool) error {
// notest // OPEN_MEMORY
// notest // no journals to delete
return sqlite3.IOERR_DELETE
}
@@ -71,70 +70,3 @@ func (sliceVFS) Access(name string, flag vfs.AccessFlag) (bool, error) {
func (sliceVFS) FullPathname(name string) (string, error) {
return name, nil
}
type sliceFile struct{ data []byte }
func (f *sliceFile) ReadAt(b []byte, off int64) (n int, err error) {
if d := f.data; off < int64(len(d)) {
n = copy(b, d[off:])
}
if n == 0 {
err = io.EOF
}
return
}
func (f *sliceFile) WriteAt(b []byte, off int64) (n int, err error) {
if d := f.data; off > int64(len(d)) {
f.data = append(d, make([]byte, off-int64(len(d)))...)
}
d := append(f.data[:off], b...)
if len(d) > len(f.data) {
f.data = d
}
return len(b), nil
}
func (f *sliceFile) Size() (int64, error) {
return int64(len(f.data)), nil
}
func (f *sliceFile) Truncate(size int64) error {
if d := f.data; size < int64(len(d)) {
f.data = d[:size]
}
return nil
}
func (f *sliceFile) SizeHint(size int64) error {
if d := f.data; size > int64(len(d)) {
f.data = append(d, make([]byte, size-int64(len(d)))...)
}
return nil
}
func (*sliceFile) Close() error { return nil }
func (*sliceFile) Sync(flag vfs.SyncFlag) error { return nil }
func (*sliceFile) Lock(lock vfs.LockLevel) error { return nil }
func (*sliceFile) Unlock(lock vfs.LockLevel) error { return nil }
func (*sliceFile) CheckReservedLock() (bool, error) {
// notest // OPEN_MEMORY
return false, nil
}
func (*sliceFile) SectorSize() int {
// notest // IOCAP_POWERSAFE_OVERWRITE
return 0
}
func (*sliceFile) DeviceCharacteristics() vfs.DeviceCharacteristic {
return vfs.IOCAP_ATOMIC |
vfs.IOCAP_SAFE_APPEND |
vfs.IOCAP_SEQUENTIAL |
vfs.IOCAP_POWERSAFE_OVERWRITE |
vfs.IOCAP_SUBPAGE_READ
}

View File

@@ -1,6 +1,7 @@
package serdes_test
import (
_ "embed"
"errors"
"io"
"net/http"
@@ -11,7 +12,30 @@ import (
"github.com/ncruces/go-sqlite3/ext/serdes"
)
func TestDeserialize(t *testing.T) {
//go:embed testdata/wal.db
var walDB []byte
func Test_wal(t *testing.T) {
db, err := sqlite3.Open("testdata/wal.db")
if err != nil {
t.Fatal(err)
}
defer db.Close()
data, err := serdes.Serialize(db, "main")
if err != nil {
t.Fatal(err)
}
compareDBs(t, data, walDB)
err = serdes.Deserialize(db, "temp", walDB)
if err != nil {
t.Fatal(err)
}
}
func Test_northwind(t *testing.T) {
if testing.Short() {
t.Skip("skipping in short mode")
}
@@ -37,10 +61,14 @@ func TestDeserialize(t *testing.T) {
t.Fatal(err)
}
if len(input) != len(output) {
compareDBs(t, input, output)
}
func compareDBs(t *testing.T, a, b []byte) {
if len(a) != len(b) {
t.Fatal("lengths are different")
}
for i := range input {
for i := range a {
// These may be different.
switch {
case 24 <= i && i < 28:
@@ -53,14 +81,14 @@ func TestDeserialize(t *testing.T) {
// SQLite version that wrote the file.
continue
}
if input[i] != output[i] {
t.Errorf("difference at %d: %d %d", i, input[i], output[i])
if a[i] != b[i] {
t.Errorf("difference at %d: %d %d", i, a[i], b[i])
}
}
}
func httpGet() ([]byte, error) {
res, err := http.Get("https://raw.githubusercontent.com/jpwhite3/northwind-SQLite3/refs/heads/main/dist/northwind.db")
res, err := http.Get("https://github.com/jpwhite3/northwind-SQLite3/raw/refs/heads/main/dist/northwind.db")
if err != nil {
return nil, err
}

BIN
ext/serdes/testdata/wal.db vendored Normal file

Binary file not shown.

View File

@@ -43,7 +43,7 @@ import (
"github.com/ncruces/go-sqlite3/internal/util"
)
// Set RegisterLike to false to not register a Unicode aware LIKE operator.
// RegisterLike must be set to false to not register a Unicode aware LIKE operator.
// Overriding the built-in LIKE operator disables the [LIKE optimization].
//
// [LIKE optimization]: https://sqlite.org/optoverview.html#the_like_optimization

View File

@@ -194,7 +194,7 @@ func timestamp(ctx sqlite3.Context, arg ...sqlite3.Value) {
switch u.Version() {
case 1, 2, 6, 7:
ctx.ResultTime(
time.Unix(u.Time().UnixTime()),
time.Unix(u.Time().UnixTime()).UTC(),
sqlite3.TimeFormatDefault)
}
}

View File

@@ -14,9 +14,9 @@ import (
func Test_generate(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, Register)
db, err := driver.Open(dsn, Register)
if err != nil {
t.Fatal(err)
}
@@ -153,9 +153,9 @@ func Test_generate(t *testing.T) {
func Test_convert(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, Register)
db, err := driver.Open(dsn, Register)
if err != nil {
t.Fatal(err)
}

View File

@@ -19,9 +19,9 @@ func Register(db *sqlite3.Conn) error {
}
func zorder(ctx sqlite3.Context, arg ...sqlite3.Value) {
var x [63]int64
if len(arg) > len(x) {
ctx.ResultError(util.ErrorString("zorder: too many parameters"))
var x [24]int64
if n := len(arg); n < 2 || n > 24 {
ctx.ResultError(util.ErrorString("zorder: needs between 2 and 24 dimensions"))
return
}
for i := range arg {
@@ -29,17 +29,15 @@ func zorder(ctx sqlite3.Context, arg ...sqlite3.Value) {
}
var z int64
if len(arg) > 0 {
for i := range x {
j := i % len(arg)
z |= (x[j] & 1) << i
x[j] >>= 1
}
for i := range 63 {
j := i % len(arg)
z |= (x[j] & 1) << i
x[j] >>= 1
}
for i := range arg {
if x[i] != 0 {
ctx.ResultError(util.ErrorString("zorder: parameter too large"))
ctx.ResultError(util.ErrorString("zorder: argument out of range"))
return
}
}
@@ -51,6 +49,19 @@ func unzorder(ctx sqlite3.Context, arg ...sqlite3.Value) {
n := arg[1].Int64()
z := arg[0].Int64()
if n < 2 || n > 24 {
ctx.ResultError(util.ErrorString("unzorder: needs between 2 and 24 dimensions"))
return
}
if i < 0 || i >= n {
ctx.ResultError(util.ErrorString("unzorder: index out of range"))
return
}
if z < 0 {
ctx.ResultError(util.ErrorString("unzorder: argument out of range"))
return
}
var k int
var x int64
for j := i; j < 63; j += n {

View File

@@ -12,11 +12,11 @@ import (
"github.com/ncruces/go-sqlite3/vfs/memdb"
)
func TestRegister_zorder(t *testing.T) {
func Test_zorder(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, zorder.Register)
db, err := driver.Open(dsn, zorder.Register)
if err != nil {
t.Fatal(err)
}
@@ -57,11 +57,11 @@ func TestRegister_zorder(t *testing.T) {
}
}
func TestRegister_unzorder(t *testing.T) {
func Test_unzorder(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, zorder.Register)
db, err := driver.Open(dsn, zorder.Register)
if err != nil {
t.Fatal(err)
}
@@ -85,11 +85,11 @@ func TestRegister_unzorder(t *testing.T) {
}
}
func TestRegister_error(t *testing.T) {
func Test_zorder_error(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db, err := driver.Open(tmp, zorder.Register)
db, err := driver.Open(dsn, zorder.Register)
if err != nil {
t.Fatal(err)
}
@@ -103,7 +103,7 @@ func TestRegister_error(t *testing.T) {
var buf strings.Builder
buf.WriteString("SELECT zorder(0")
for i := 1; i < 80; i++ {
for i := 1; i < 25; i++ {
buf.WriteByte(',')
buf.WriteString(strconv.Itoa(0))
}
@@ -113,3 +113,30 @@ func TestRegister_error(t *testing.T) {
t.Error("want error")
}
}
func Test_unzorder_error(t *testing.T) {
t.Parallel()
dsn := memdb.TestDB(t)
db, err := driver.Open(dsn, zorder.Register)
if err != nil {
t.Fatal(err)
}
defer db.Close()
var got int64
err = db.QueryRow(`SELECT unzorder(-1, 2, 0)`).Scan(&got)
if err == nil {
t.Error("want error")
}
err = db.QueryRow(`SELECT unzorder(0, 2, 2)`).Scan(&got)
if err == nil {
t.Error("want error")
}
err = db.QueryRow(`SELECT unzorder(0, 25, 2)`).Scan(&got)
if err == nil {
t.Error("want error")
}
}

14
func.go
View File

@@ -59,7 +59,7 @@ func (c *Conn) CreateCollation(name string, fn CollatingFunction) error {
return c.error(rc)
}
// Collating function is the type of a collation callback.
// CollatingFunction is the type of a collation callback.
// Implementations must not retain a or b.
type CollatingFunction func(a, b []byte) int
@@ -132,7 +132,7 @@ func (c *Conn) CreateWindowFunction(name string, nArg int, flag FunctionFlag, fn
if win, ok := agg.(WindowFunction); ok {
return win
}
return windowFunc{agg, name}
return agg
}))
}
rc := res_t(c.call("sqlite3_create_window_function_go",
@@ -307,13 +307,3 @@ func (a *aggregateFunc) Close() error {
a.stop()
return nil
}
type windowFunc struct {
AggregateFunction
name string
}
func (w windowFunc) Inverse(ctx Context, arg ...Value) {
// Implementing inverse allows certain queries that don't really need it to succeed.
ctx.ResultError(util.ErrorString(w.name + ": may not be used as a window function"))
}

15
go.mod
View File

@@ -1,23 +1,22 @@
module github.com/ncruces/go-sqlite3
go 1.23.0
toolchain go1.24.0
go 1.24.0
require (
github.com/ncruces/julianday v1.0.0
github.com/ncruces/sort v0.1.5
github.com/ncruces/sort v0.1.6
github.com/ncruces/wbt v0.2.0
github.com/tetratelabs/wazero v1.9.0
golang.org/x/crypto v0.40.0
golang.org/x/sys v0.34.0
golang.org/x/sys v0.36.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.16.0 // test
golang.org/x/text v0.27.0 // ext/unicode
golang.org/x/crypto v0.42.0 // vfs/adiantum vfs/xts
golang.org/x/sync v0.17.0 // test
golang.org/x/text v0.29.0 // ext/unicode
lukechampine.com/adiantum v1.1.1 // vfs/adiantum
)

22
go.sum
View File

@@ -4,19 +4,21 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
github.com/ncruces/sort v0.1.5 h1:fiFWXXAqKI8QckPf/6hu/bGFwcEPrirIOFaJqWujs4k=
github.com/ncruces/sort v0.1.5/go.mod h1:obJToO4rYr6VWP0Uw5FYymgYGt3Br4RXcs/JdKaXAPk=
github.com/ncruces/sort v0.1.6 h1:TrsJfGRH1AoWoaeB4/+gCohot9+cA6u/INaH5agIhNk=
github.com/ncruces/sort v0.1.6/go.mod h1:obJToO4rYr6VWP0Uw5FYymgYGt3Br4RXcs/JdKaXAPk=
github.com/ncruces/wbt v0.2.0 h1:Q9zlKOBSZc7Yy/R2cGa35g6RKUUE3BjNIW3tfGC4F04=
github.com/ncruces/wbt v0.2.0/go.mod h1:DtF92amvMxH69EmBFUSFWRDAlo6hOEfoNQnClxj9C/c=
github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE=
github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE=
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
lukechampine.com/adiantum v1.1.1 h1:4fp6gTxWCqpEbLy40ExiYDDED3oUNWx5cTqBCtPdZqA=
lukechampine.com/adiantum v1.1.1/go.mod h1:LrAYVnTYLnUtE/yMp5bQr0HstAf060YUF8nM0B6+rUw=

View File

@@ -1,12 +1,10 @@
module github.com/ncruces/go-sqlite3/gormlite
go 1.23.0
toolchain go1.24.0
go 1.24.0
require (
github.com/ncruces/go-sqlite3 v0.26.3
gorm.io/gorm v1.30.0
github.com/ncruces/go-sqlite3 v0.29.0
gorm.io/gorm v1.30.5
)
require (
@@ -14,6 +12,6 @@ require (
github.com/jinzhu/now v1.1.5 // indirect
github.com/ncruces/julianday v1.0.0 // indirect
github.com/tetratelabs/wazero v1.9.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.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.26.3 h1:WFkQj4KNMhbqiBPGDrVpK74w1DzcxQu3wYpmdWAvfYM=
github.com/ncruces/go-sqlite3 v0.26.3/go.mod h1:XFTPtFIo1DmGCh+XVP8KGn9b/o2f+z0WZuT09x2N6eo=
github.com/ncruces/go-sqlite3 v0.29.0 h1:1tsLiagCoqZEfcHDeKsNSv5jvrY/Iu393pAnw2wLNJU=
github.com/ncruces/go-sqlite3 v0.29.0/go.mod h1:r1hSvYKPNJ+OlUA1O3r8o9LAawzPAlqeZiIdxTBBBJ0=
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.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
gorm.io/gorm v1.30.5 h1:dvEfYwxL+i+xgCNSGGBT1lDjCzfELK8fHZxL3Ee9X0s=
gorm.io/gorm v1.30.5/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=

View File

@@ -14,10 +14,10 @@ import (
)
func TestDialector(t *testing.T) {
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
// Custom connection with a custom function called "my_custom_function".
db, err := driver.Open(tmp, func(conn *sqlite3.Conn) error {
db, err := driver.Open(dsn, func(conn *sqlite3.Conn) error {
return conn.CreateFunction("my_custom_function", 0, sqlite3.DETERMINISTIC,
func(ctx sqlite3.Context, arg ...sqlite3.Value) {
ctx.ResultText("my-result")
@@ -36,14 +36,14 @@ func TestDialector(t *testing.T) {
}{
{
description: "Default driver",
dialector: Open(tmp),
dialector: Open(dsn),
openSuccess: true,
query: "SELECT 1",
querySuccess: true,
},
{
description: "Custom function",
dialector: Open(tmp),
dialector: Open(dsn),
openSuccess: true,
query: "SELECT my_custom_function()",
querySuccess: false,

View File

@@ -7,7 +7,7 @@ rm -rf gorm/ tests/
go work use -r .
go test
git clone --branch v1.30.0 --filter=blob:none https://github.com/go-gorm/gorm.git
git clone --branch v1.30.5 --filter=blob:none https://github.com/go-gorm/gorm.git
mv gorm/tests tests
rm -rf gorm/

View File

@@ -7,13 +7,14 @@ diff --git a/tests/.gitignore b/tests/.gitignore
diff --git a/tests/tests_test.go b/tests/tests_test.go
--- a/tests/tests_test.go
+++ b/tests/tests_test.go
@@ -7,9 +7,11 @@ import (
@@ -8,9 +8,11 @@ import (
"path/filepath"
"time"
+ _ "github.com/ncruces/go-sqlite3/embed"
+ sqlite "github.com/ncruces/go-sqlite3/gormlite"
+
"gorm.io/driver/gaussdb"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
- "gorm.io/driver/sqlite"

View File

@@ -9,24 +9,31 @@ import (
"golang.org/x/sys/unix"
)
func NewMemory(_, max uint64) experimental.LinearMemory {
func NewMemory(cap, max uint64) experimental.LinearMemory {
// Round up to the page size.
rnd := uint64(unix.Getpagesize() - 1)
max = (max + rnd) &^ rnd
res := (max + rnd) &^ rnd
if max > math.MaxInt {
// This ensures int(max) overflows to a negative value,
if res > math.MaxInt {
// This ensures int(res) overflows to a negative value,
// and unix.Mmap returns EINVAL.
max = math.MaxUint64
res = math.MaxUint64
}
// Reserve max bytes of address space, to ensure we won't need to move it.
com := res
prot := unix.PROT_READ | unix.PROT_WRITE
if cap < max { // Commit memory only if cap=max.
com = 0
prot = unix.PROT_NONE
}
// Reserve res bytes of address space, to ensure we won't need to move it.
// A protected, private, anonymous mapping should not commit memory.
b, err := unix.Mmap(-1, 0, int(max), unix.PROT_NONE, unix.MAP_PRIVATE|unix.MAP_ANON)
b, err := unix.Mmap(-1, 0, int(res), prot, unix.MAP_PRIVATE|unix.MAP_ANON)
if err != nil {
panic(err)
}
return &mmappedMemory{buf: b[:0]}
return &mmappedMemory{buf: b[:com]}
}
// The slice covers the entire mmapped memory:
@@ -40,9 +47,11 @@ func (m *mmappedMemory) Reallocate(size uint64) []byte {
com := uint64(len(m.buf))
res := uint64(cap(m.buf))
if com < size && size <= res {
// Round up to the page size.
// Grow geometrically, round up to the page size.
rnd := uint64(unix.Getpagesize() - 1)
new := (size + rnd) &^ rnd
new := com + com>>3
new = min(max(size, new), res)
new = (new + rnd) &^ rnd
// Commit additional memory up to new bytes.
err := unix.Mprotect(m.buf[com:new], unix.PROT_READ|unix.PROT_WRITE)
@@ -50,8 +59,7 @@ func (m *mmappedMemory) Reallocate(size uint64) []byte {
return nil
}
// Update committed memory.
m.buf = m.buf[:new]
m.buf = m.buf[:new] // Update committed memory.
}
// Limit returned capacity because bytes beyond
// len(m.buf) have not yet been committed.

View File

@@ -9,20 +9,26 @@ import (
"golang.org/x/sys/windows"
)
func NewMemory(_, max uint64) experimental.LinearMemory {
func NewMemory(cap, max uint64) experimental.LinearMemory {
// Round up to the page size.
rnd := uint64(windows.Getpagesize() - 1)
max = (max + rnd) &^ rnd
res := (max + rnd) &^ rnd
if max > math.MaxInt {
// This ensures uintptr(max) overflows to a large value,
if res > math.MaxInt {
// This ensures uintptr(res) overflows to a large value,
// and windows.VirtualAlloc returns an error.
max = math.MaxUint64
res = math.MaxUint64
}
// Reserve max bytes of address space, to ensure we won't need to move it.
// This does not commit memory.
r, err := windows.VirtualAlloc(0, uintptr(max), windows.MEM_RESERVE, windows.PAGE_READWRITE)
com := res
kind := windows.MEM_COMMIT
if cap < max { // Commit memory only if cap=max.
com = 0
kind = windows.MEM_RESERVE
}
// Reserve res bytes of address space, to ensure we won't need to move it.
r, err := windows.VirtualAlloc(0, uintptr(res), uint32(kind), windows.PAGE_READWRITE)
if err != nil {
panic(err)
}
@@ -30,8 +36,9 @@ func NewMemory(_, max uint64) experimental.LinearMemory {
mem := virtualMemory{addr: r}
// SliceHeader, although deprecated, avoids a go vet warning.
sh := (*reflect.SliceHeader)(unsafe.Pointer(&mem.buf))
sh.Cap = int(max)
sh.Data = r
sh.Len = int(com)
sh.Cap = int(res)
return &mem
}
@@ -47,9 +54,11 @@ func (m *virtualMemory) Reallocate(size uint64) []byte {
com := uint64(len(m.buf))
res := uint64(cap(m.buf))
if com < size && size <= res {
// Round up to the page size.
// Grow geometrically, round up to the page size.
rnd := uint64(windows.Getpagesize() - 1)
new := (size + rnd) &^ rnd
new := com + com>>3
new = min(max(size, new), res)
new = (new + rnd) &^ rnd
// Commit additional memory up to new bytes.
_, err := windows.VirtualAlloc(m.addr, uintptr(new), windows.MEM_COMMIT, windows.PAGE_READWRITE)
@@ -57,8 +66,7 @@ func (m *virtualMemory) Reallocate(size uint64) []byte {
return nil
}
// Update committed memory.
m.buf = m.buf[:new]
m.buf = m.buf[:new] // Update committed memory.
}
// Limit returned capacity because bytes beyond
// len(m.buf) have not yet been committed.

View File

@@ -20,20 +20,6 @@ func ExportFuncVI[T0 i32](mod wazero.HostModuleBuilder, name string, fn func(con
Export(name)
}
type funcVII[T0, T1 i32] func(context.Context, api.Module, T0, T1)
func (fn funcVII[T0, T1]) Call(ctx context.Context, mod api.Module, stack []uint64) {
_ = stack[1] // prevent bounds check on every slice access
fn(ctx, mod, T0(stack[0]), T1(stack[1]))
}
func ExportFuncVII[T0, T1 i32](mod wazero.HostModuleBuilder, name string, fn func(context.Context, api.Module, T0, T1)) {
mod.NewFunctionBuilder().
WithGoModuleFunction(funcVII[T0, T1](fn),
[]api.ValueType{api.ValueTypeI32, api.ValueTypeI32}, nil).
Export(name)
}
type funcVIII[T0, T1, T2 i32] func(context.Context, api.Module, T0, T1, T2)
func (fn funcVIII[T0, T1, T2]) Call(ctx context.Context, mod api.Module, stack []uint64) {

View File

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

52
internal/util/json_v2.go Normal file
View File

@@ -0,0 +1,52 @@
//go:build goexperiment.jsonv2
package util
import (
"encoding/json/v2"
"math"
"strconv"
"time"
"unsafe"
)
type JSON struct{ Value any }
func (j JSON) Scan(value any) error {
var buf []byte
switch v := value.(type) {
case []byte:
buf = v
case string:
buf = unsafe.Slice(unsafe.StringData(v), len(v))
case int64:
buf = strconv.AppendInt(nil, v, 10)
case float64:
buf = AppendNumber(nil, v)
case time.Time:
buf = append(buf, '"')
buf = v.AppendFormat(buf, time.RFC3339Nano)
buf = append(buf, '"')
case nil:
buf = []byte("null")
default:
panic(AssertErr())
}
return json.Unmarshal(buf, j.Value)
}
func AppendNumber(dst []byte, f float64) []byte {
switch {
case math.IsNaN(f):
dst = append(dst, "null"...)
case math.IsInf(f, 1):
dst = append(dst, "9.0e999"...)
case math.IsInf(f, -1):
dst = append(dst, "-9.0e999"...)
default:
return strconv.AppendFloat(dst, f, 'g', -1, 64)
}
return dst
}

83
json.go
View File

@@ -1,6 +1,13 @@
//go:build !goexperiment.jsonv2
package sqlite3
import "github.com/ncruces/go-sqlite3/internal/util"
import (
"encoding/json"
"strconv"
"github.com/ncruces/go-sqlite3/internal/util"
)
// JSON returns a value that can be used as an argument to
// [database/sql.DB.Exec], [database/sql.Row.Scan] and similar methods to
@@ -10,3 +17,77 @@ import "github.com/ncruces/go-sqlite3/internal/util"
func JSON(value any) any {
return util.JSON{Value: value}
}
// ResultJSON sets the result of the function to the JSON encoding of value.
//
// https://sqlite.org/c3ref/result_blob.html
func (ctx Context) ResultJSON(value any) {
err := json.NewEncoder(callbackWriter(func(p []byte) (int, error) {
ctx.ResultRawText(p[:len(p)-1]) // remove the newline
return 0, nil
})).Encode(value)
if err != nil {
ctx.ResultError(err)
return // notest
}
}
// BindJSON binds the JSON encoding of value to the prepared statement.
// The leftmost SQL parameter has an index of 1.
//
// https://sqlite.org/c3ref/bind_blob.html
func (s *Stmt) BindJSON(param int, value any) error {
return json.NewEncoder(callbackWriter(func(p []byte) (int, error) {
return 0, s.BindRawText(param, p[:len(p)-1]) // remove the newline
})).Encode(value)
}
// ColumnJSON parses the JSON-encoded value of the result column
// and stores it in the value pointed to by ptr.
// The leftmost column of the result set has the index 0.
//
// https://sqlite.org/c3ref/column_blob.html
func (s *Stmt) ColumnJSON(col int, ptr any) error {
var data []byte
switch s.ColumnType(col) {
case NULL:
data = []byte("null")
case TEXT:
data = s.ColumnRawText(col)
case BLOB:
data = s.ColumnRawBlob(col)
case INTEGER:
data = strconv.AppendInt(nil, s.ColumnInt64(col), 10)
case FLOAT:
data = util.AppendNumber(nil, s.ColumnFloat(col))
default:
panic(util.AssertErr())
}
return json.Unmarshal(data, ptr)
}
// JSON parses a JSON-encoded value
// and stores the result in the value pointed to by ptr.
func (v Value) JSON(ptr any) error {
var data []byte
switch v.Type() {
case NULL:
data = []byte("null")
case TEXT:
data = v.RawText()
case BLOB:
data = v.RawBlob()
case INTEGER:
data = strconv.AppendInt(nil, v.Int64(), 10)
case FLOAT:
data = util.AppendNumber(nil, v.Float())
default:
panic(util.AssertErr())
}
return json.Unmarshal(data, ptr)
}
type callbackWriter func(p []byte) (int, error)
func (fn callbackWriter) Write(p []byte) (int, error) { return fn(p) }

113
json_v2.go Normal file
View File

@@ -0,0 +1,113 @@
//go:build goexperiment.jsonv2
package sqlite3
import (
"encoding/json/v2"
"strconv"
"github.com/ncruces/go-sqlite3/internal/util"
)
// JSON returns a value that can be used as an argument to
// [database/sql.DB.Exec], [database/sql.Row.Scan] and similar methods to
// store value as JSON, or decode JSON into value.
// JSON should NOT be used with [Stmt.BindJSON], [Stmt.ColumnJSON],
// [Value.JSON], or [Context.ResultJSON].
func JSON(value any) any {
return util.JSON{Value: value}
}
// ResultJSON sets the result of the function to the JSON encoding of value.
//
// https://sqlite.org/c3ref/result_blob.html
func (ctx Context) ResultJSON(value any) {
w := bytesWriter{sqlite: ctx.c.sqlite}
if err := json.MarshalWrite(&w, value); err != nil {
ctx.c.free(w.ptr)
ctx.ResultError(err)
return // notest
}
ctx.c.call("sqlite3_result_text_go",
stk_t(ctx.handle), stk_t(w.ptr), stk_t(len(w.buf)))
}
// BindJSON binds the JSON encoding of value to the prepared statement.
// The leftmost SQL parameter has an index of 1.
//
// https://sqlite.org/c3ref/bind_blob.html
func (s *Stmt) BindJSON(param int, value any) error {
w := bytesWriter{sqlite: s.c.sqlite}
if err := json.MarshalWrite(&w, value); err != nil {
s.c.free(w.ptr)
return err
}
rc := res_t(s.c.call("sqlite3_bind_text_go",
stk_t(s.handle), stk_t(param),
stk_t(w.ptr), stk_t(len(w.buf))))
return s.c.error(rc)
}
// ColumnJSON parses the JSON-encoded value of the result column
// and stores it in the value pointed to by ptr.
// The leftmost column of the result set has the index 0.
//
// https://sqlite.org/c3ref/column_blob.html
func (s *Stmt) ColumnJSON(col int, ptr any) error {
var data []byte
switch s.ColumnType(col) {
case NULL:
data = []byte("null")
case TEXT:
data = s.ColumnRawText(col)
case BLOB:
data = s.ColumnRawBlob(col)
case INTEGER:
data = strconv.AppendInt(nil, s.ColumnInt64(col), 10)
case FLOAT:
data = util.AppendNumber(nil, s.ColumnFloat(col))
default:
panic(util.AssertErr())
}
return json.Unmarshal(data, ptr)
}
// JSON parses a JSON-encoded value
// and stores the result in the value pointed to by ptr.
func (v Value) JSON(ptr any) error {
var data []byte
switch v.Type() {
case NULL:
data = []byte("null")
case TEXT:
data = v.RawText()
case BLOB:
data = v.RawBlob()
case INTEGER:
data = strconv.AppendInt(nil, v.Int64(), 10)
case FLOAT:
data = util.AppendNumber(nil, v.Float())
default:
panic(util.AssertErr())
}
return json.Unmarshal(data, ptr)
}
type bytesWriter struct {
*sqlite
buf []byte
ptr ptr_t
}
func (b *bytesWriter) Write(p []byte) (n int, err error) {
if len(p) > cap(b.buf)-len(b.buf) {
want := int64(len(b.buf)) + int64(len(p))
grow := int64(cap(b.buf))
grow += grow >> 1
want = max(want, grow)
b.ptr = b.realloc(b.ptr, want)
b.buf = util.View(b.mod, b.ptr, want)[:len(b.buf)]
}
b.buf = append(b.buf, p...)
return len(p), nil
}

View File

@@ -5,6 +5,7 @@ import (
"context"
"math/bits"
"os"
"strings"
"sync"
"unsafe"
@@ -128,11 +129,10 @@ func (sqlt *sqlite) error(rc res_t, handle ptr_t, sql ...string) error {
var msg, query string
if ptr := ptr_t(sqlt.call("sqlite3_errmsg", stk_t(handle))); ptr != 0 {
msg = util.ReadString(sqlt.mod, ptr, _MAX_LENGTH)
switch {
case msg == "not an error":
msg = ""
case msg == util.ErrorCodeString(uint32(rc))[len("sqlite3: "):]:
if msg == "not an error" {
msg = ""
} else {
msg = strings.TrimPrefix(msg, util.ErrorCodeString(uint32(rc))[len("sqlite3: "):])
}
}

View File

@@ -2,7 +2,7 @@
# handle, and interrupt, sqlite3_busy_timeout.
--- sqlite3.c.orig
+++ sqlite3.c
@@ -184433,7 +184433,7 @@
@@ -184474,7 +184474,7 @@
if( !sqlite3SafetyCheckOk(db) ) return SQLITE_MISUSE_BKPT;
#endif
if( ms>0 ){

View File

@@ -3,7 +3,16 @@ set -euo pipefail
cd -P -- "$(dirname -- "$0")"
curl -#OL "https://sqlite.org/2025/sqlite-amalgamation-3500200.zip"
curl -#OL "https://sqlite.org/2025/sqlite-amalgamation-3500400.zip"
# Verify download.
if hash=$(openssl dgst -sha3-256 sqlite-amalgamation-*.zip); then
if ! [[ $hash =~ f131b68e6ba5fb891cc13ebb5ff9555054c77294cb92d8d1268bad5dba4fa2a1 ]]; then
echo $hash
exit 1
fi
fi 2> /dev/null
unzip -d . sqlite-amalgamation-*.zip
mv sqlite-amalgamation-*/sqlite3.c .
mv sqlite-amalgamation-*/sqlite3.h .
@@ -19,30 +28,30 @@ rm -rf sqlite-amalgamation-*
mkdir -p ext/
cd ext/
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.2/ext/misc/anycollseq.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.2/ext/misc/base64.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.2/ext/misc/decimal.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.2/ext/misc/ieee754.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.2/ext/misc/regexp.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.2/ext/misc/series.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.2/ext/misc/spellfix.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.2/ext/misc/uint.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.4/ext/misc/anycollseq.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.4/ext/misc/base64.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.4/ext/misc/decimal.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.4/ext/misc/ieee754.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.4/ext/misc/regexp.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.4/ext/misc/series.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.4/ext/misc/spellfix.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.4/ext/misc/uint.c"
cd ~-
cd ../vfs/tests/mptest/testdata/
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.2/mptest/config01.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.2/mptest/config02.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.2/mptest/crash01.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.2/mptest/crash02.subtest"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.2/mptest/multiwrite01.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.4/mptest/config01.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.4/mptest/config02.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.4/mptest/crash01.test"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.4/mptest/crash02.subtest"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.4/mptest/multiwrite01.test"
cd ~-
cd ../vfs/tests/mptest/wasm/
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.2/mptest/mptest.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.4/mptest/mptest.c"
cd ~-
cd ../vfs/tests/speedtest1/wasm/
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.2/test/speedtest1.c"
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.4/test/speedtest1.c"
cd ~-
cat *.patch | patch -p0 --no-backup-if-mismatch

View File

@@ -22,7 +22,8 @@ EOF
-mmutable-globals -mnontrapping-fptoint \
-msimd128 -mbulk-memory -msign-ext \
-mreference-types -mmultivalue \
-fno-stack-protector -fno-stack-clash-protection \
-mno-extended-const \
-fno-stack-protector \
-Wl,-z,stack-size=4096 \
-Wl,--stack-first \
-Wl,--import-undefined \
@@ -42,10 +43,11 @@ EOF
-Wl,--export=qsort
"$BINARYEN/wasm-ctor-eval" -g -c _initialize libc.wasm -o libc.tmp
"$BINARYEN/wasm-opt" -g --strip --strip-producers -c -O3 \
libc.tmp -o libc.wasm \
"$BINARYEN/wasm-opt" -g libc.tmp -o libc.wasm \
--low-memory-unused --generate-global-effects --converge -O3 \
--enable-mutable-globals --enable-nontrapping-float-to-int \
--enable-simd --enable-bulk-memory --enable-sign-ext \
--enable-reference-types --enable-multivalue
--enable-reference-types --enable-multivalue \
--strip --strip-debug --strip-producers
"$BINARYEN/wasm-dis" -o libc.wat libc.wasm

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,6 @@ import (
"os"
"strings"
"testing"
"unicode/utf8"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
@@ -76,8 +75,7 @@ func Benchmark_memset(b *testing.B) {
clear(memory)
b.SetBytes(size)
b.ResetTimer()
for range b.N {
for b.Loop() {
call(memset, ptr1, 3, size)
}
}
@@ -87,8 +85,7 @@ func Benchmark_memcpy(b *testing.B) {
fill(memory[ptr2:ptr2+size], 5)
b.SetBytes(size)
b.ResetTimer()
for range b.N {
for b.Loop() {
call(memcpy, ptr1, ptr2, size)
}
}
@@ -98,8 +95,7 @@ func Benchmark_strlen(b *testing.B) {
fill(memory[ptr1:ptr1+size-1], 5)
b.SetBytes(size)
b.ResetTimer()
for range b.N {
for b.Loop() {
call(strlen, ptr1)
}
}
@@ -110,8 +106,7 @@ func Benchmark_memchr(b *testing.B) {
fill(memory[ptr1+size/2:ptr1+size], 5)
b.SetBytes(size/2 + 1)
b.ResetTimer()
for range b.N {
for b.Loop() {
call(memchr, ptr1, 5, size)
}
}
@@ -122,8 +117,7 @@ func Benchmark_strchr(b *testing.B) {
fill(memory[ptr1+size/2:ptr1+size-1], 5)
b.SetBytes(size/2 + 1)
b.ResetTimer()
for range b.N {
for b.Loop() {
call(strchr, ptr1, 5)
}
}
@@ -134,8 +128,7 @@ func Benchmark_strrchr(b *testing.B) {
fill(memory[ptr1+size/2:ptr1+size-1], 7)
b.SetBytes(size/2 + 1)
b.ResetTimer()
for range b.N {
for b.Loop() {
call(strrchr, ptr1, 5)
}
}
@@ -147,8 +140,7 @@ func Benchmark_memcmp(b *testing.B) {
fill(memory[ptr2+size/2:ptr2+size], 5)
b.SetBytes(size/2 + 1)
b.ResetTimer()
for range b.N {
for b.Loop() {
call(memcmp, ptr1, ptr2, size)
}
}
@@ -163,8 +155,7 @@ func Benchmark_strspn(b *testing.B) {
memory[ptr2+3] = 9
b.SetBytes(size)
b.ResetTimer()
for range b.N {
for b.Loop() {
call(strspn, ptr1, ptr2)
}
}
@@ -177,8 +168,7 @@ func Benchmark_strcspn(b *testing.B) {
memory[ptr2+1] = 9
b.SetBytes(size)
b.ResetTimer()
for range b.N {
for b.Loop() {
call(strcspn, ptr1, ptr2)
}
}
@@ -750,15 +740,7 @@ func Fuzz_strspn(f *testing.F) {
s = term(s)
chars = term(chars)
want := strings.IndexFunc(s, func(r rune) bool {
if uint32(r) >= utf8.RuneSelf {
t.Skip()
}
return strings.IndexByte(chars, byte(r)) < 0
})
if want < 0 {
want = len(s)
}
want := indexNotByte(s, chars)
if uint32(got) != uint32(want) {
t.Errorf("strspn(%v, %v) = %d, want %d",
@@ -778,11 +760,6 @@ func Fuzz_strcspn(f *testing.F) {
if len(s) > 128 || len(chars) > 128 {
t.SkipNow()
}
if strings.ContainsFunc(chars, func(r rune) bool {
return uint32(r) >= utf8.RuneSelf
}) {
t.SkipNow()
}
copy(memory[ptr1:], s)
copy(memory[ptr2:], chars)
memory[ptr1+len(s)] = 0
@@ -792,10 +769,7 @@ func Fuzz_strcspn(f *testing.F) {
s = term(s)
chars = term(chars)
want := strings.IndexAny(s, chars)
if want < 0 {
want = len(s)
}
want := indexAnyByte(s, chars)
if uint32(got) != uint32(want) {
t.Errorf("strcspn(%q, %q) = %d, want %d",
@@ -838,3 +812,21 @@ func term1[T interface{ []byte | string }](s T) T {
}
return s
}
func indexNotByte(s, chars string) int {
for i, c := range []byte(s) {
if strings.IndexByte(chars, c) < 0 {
return i
}
}
return len(s)
}
func indexAnyByte(s, chars string) int {
for i, c := range []byte(s) {
if strings.IndexByte(chars, c) >= 0 {
return i
}
}
return len(s)
}

View File

@@ -242,17 +242,21 @@ char *strrchr(const char *s, int c) {
// SIMDized check which bytes are in a set (Geoff Langdale)
// http://0x80.pl/notesen/2018-10-18-simd-byte-lookup.html
// This is the same algorithm as truffle from Hyperscan:
// https://github.com/intel/hyperscan/blob/v5.4.2/src/nfa/truffle.c#L64-L81
// https://github.com/intel/hyperscan/blob/v5.4.2/src/nfa/trufflecompile.cpp
typedef struct {
__u8x16 lo;
__u8x16 hi;
} __wasm_v128_bitmap256_t;
__attribute__((always_inline))
static void __wasm_v128_setbit(__wasm_v128_bitmap256_t *bitmap, int i) {
uint8_t hi_nibble = (uint8_t)i >> 4;
uint8_t lo_nibble = (uint8_t)i & 0xf;
bitmap->lo[lo_nibble] |= (uint8_t)((uint32_t)1 << (hi_nibble - 0));
bitmap->hi[lo_nibble] |= (uint8_t)((uint32_t)1 << (hi_nibble - 8));
static void __wasm_v128_setbit(__wasm_v128_bitmap256_t *bitmap, uint8_t i) {
uint8_t hi_nibble = i >> 4;
uint8_t lo_nibble = i & 0xf;
bitmap->lo[lo_nibble] |= (uint8_t)(1u << (hi_nibble - 0));
bitmap->hi[lo_nibble] |= (uint8_t)(1u << (hi_nibble - 8));
}
#ifndef __wasm_relaxed_simd__
@@ -264,18 +268,17 @@ static void __wasm_v128_setbit(__wasm_v128_bitmap256_t *bitmap, int i) {
__attribute__((always_inline))
static v128_t __wasm_v128_chkbits(__wasm_v128_bitmap256_t bitmap, v128_t v) {
v128_t hi_nibbles = wasm_u8x16_shr(v, 4);
v128_t bitmask_lookup = wasm_u8x16_const(1, 2, 4, 8, 16, 32, 64, 128, //
1, 2, 4, 8, 16, 32, 64, 128);
v128_t bitmask_lookup = wasm_u64x2_const_splat(0x8040201008040201);
v128_t bitmask = wasm_i8x16_relaxed_swizzle(bitmask_lookup, hi_nibbles);
v128_t indices_0_7 = v & wasm_u8x16_const_splat(0x8f);
v128_t indices_8_15 = indices_0_7 ^ wasm_u8x16_const_splat(0x80);
v128_t row_0_7 = wasm_i8x16_swizzle(bitmap.lo, indices_0_7);
v128_t row_8_15 = wasm_i8x16_swizzle(bitmap.hi, indices_8_15);
v128_t row_0_7 = wasm_i8x16_swizzle((v128_t)bitmap.lo, indices_0_7);
v128_t row_8_15 = wasm_i8x16_swizzle((v128_t)bitmap.hi, indices_8_15);
v128_t bitsets = row_0_7 | row_8_15;
return wasm_i8x16_eq(bitsets & bitmask, bitmask);
return bitsets & bitmask;
}
#undef wasm_i8x16_relaxed_swizzle
@@ -317,17 +320,18 @@ size_t strspn(const char *s, const char *c) {
for (; *c; c++) {
// Terminator IS NOT on the bitmap.
__wasm_v128_setbit(&bitmap, *c);
__wasm_v128_setbit(&bitmap, (uint8_t)*c);
}
for (;;) {
v128_t v = *(v128_t *)addr;
v128_t cmp = __wasm_v128_chkbits(bitmap, v);
v128_t found = __wasm_v128_chkbits(bitmap, v);
// Bitmask is slow on AArch64, all_true is much faster.
if (!wasm_i8x16_all_true(cmp)) {
if (!wasm_i8x16_all_true(found)) {
v128_t cmp = wasm_i8x16_eq(found, (v128_t){});
// Clear the bits corresponding to align (little-endian)
// so we can count trailing zeros.
int mask = (uint16_t)~wasm_i8x16_bitmask(cmp) >> align << align;
int mask = wasm_i8x16_bitmask(cmp) >> align << align;
// At least one bit will be set, unless align cleared them.
// Knowing this helps the compiler if it unrolls the loop.
__builtin_assume(mask || align);
@@ -356,17 +360,18 @@ size_t strcspn(const char *s, const char *c) {
do {
// Terminator IS on the bitmap.
__wasm_v128_setbit(&bitmap, *c);
__wasm_v128_setbit(&bitmap, (uint8_t)*c);
} while (*c++);
for (;;) {
v128_t v = *(v128_t *)addr;
v128_t cmp = __wasm_v128_chkbits(bitmap, v);
v128_t found = __wasm_v128_chkbits(bitmap, v);
// Bitmask is slow on AArch64, any_true is much faster.
if (wasm_v128_any_true(cmp)) {
if (wasm_v128_any_true(found)) {
v128_t cmp = wasm_i8x16_eq(found, (v128_t){});
// Clear the bits corresponding to align (little-endian)
// so we can count trailing zeros.
int mask = wasm_i8x16_bitmask(cmp) >> align << align;
int mask = (uint16_t)~wasm_i8x16_bitmask(cmp) >> align << align;
// At least one bit will be set, unless align cleared them.
// Knowing this helps the compiler if it unrolls the loop.
__builtin_assume(mask || align);

View File

@@ -26,8 +26,8 @@ elif [[ "$OSTYPE" == "darwin"* ]]; then
fi
fi
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-$WASI_SDK.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_123/binaryen-version_123-$BINARYEN.tar.gz"
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-27/wasi-sdk-27.0-$WASI_SDK.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-$BINARYEN.tar.gz"
# Download tools
mkdir -p "$ROOT/tools"

View File

@@ -1,7 +1,7 @@
# Remove VFS registration. Go handles it.
--- sqlite3.c.orig
+++ sqlite3.c
@@ -26883,7 +26883,7 @@
@@ -26884,7 +26884,7 @@
sqlite3_free(p);
return sqlite3_os_init();
}
@@ -10,7 +10,7 @@
/*
** The list of all registered VFS implementations.
*/
@@ -26980,7 +26980,7 @@
@@ -26981,7 +26981,7 @@
sqlite3_mutex_leave(mutex);
return SQLITE_OK;
}

40
stmt.go
View File

@@ -1,9 +1,7 @@
package sqlite3
import (
"encoding/json"
"math"
"strconv"
"time"
"github.com/ncruces/go-sqlite3/internal/util"
@@ -362,16 +360,6 @@ func (s *Stmt) BindPointer(param int, ptr any) error {
return s.c.error(rc)
}
// BindJSON binds the JSON encoding of value to the prepared statement.
// The leftmost SQL parameter has an index of 1.
//
// https://sqlite.org/c3ref/bind_blob.html
func (s *Stmt) BindJSON(param int, value any) error {
return json.NewEncoder(callbackWriter(func(p []byte) (int, error) {
return 0, s.BindRawText(param, p[:len(p)-1]) // remove the newline
})).Encode(value)
}
// BindValue binds a copy of value to the prepared statement.
// The leftmost SQL parameter has an index of 1.
//
@@ -598,30 +586,6 @@ func (s *Stmt) columnRawBytes(col int, ptr ptr_t, nul int32) []byte {
return util.View(s.c.mod, ptr, int64(n+nul))[:n]
}
// ColumnJSON parses the JSON-encoded value of the result column
// and stores it in the value pointed to by ptr.
// The leftmost column of the result set has the index 0.
//
// https://sqlite.org/c3ref/column_blob.html
func (s *Stmt) ColumnJSON(col int, ptr any) error {
var data []byte
switch s.ColumnType(col) {
case NULL:
data = []byte("null")
case TEXT:
data = s.ColumnRawText(col)
case BLOB:
data = s.ColumnRawBlob(col)
case INTEGER:
data = strconv.AppendInt(nil, s.ColumnInt64(col), 10)
case FLOAT:
data = util.AppendNumber(nil, s.ColumnFloat(col))
default:
panic(util.AssertErr())
}
return json.Unmarshal(data, ptr)
}
// ColumnValue returns the unprotected value of the result column.
// The leftmost column of the result set has the index 0.
//
@@ -748,7 +712,3 @@ func (s *Stmt) columns(count int64) ([]byte, ptr_t, error) {
return util.View(s.c.mod, typePtr, count), dataPtr, nil
}
type callbackWriter func(p []byte) (int, error)
func (fn callbackWriter) Write(p []byte) (int, error) { return fn(p) }

View File

@@ -45,10 +45,7 @@ func TestBlob(t *testing.T) {
}
var data [1280]byte
_, err = rand.Read(data[:])
if err != nil {
t.Fatal(err)
}
rand.Read(data[:])
_, err = blob.Write(data[:size/2])
if err != nil {

View File

@@ -35,7 +35,7 @@ type params struct {
*sql.DB
}
func (t params) mustExec(sql string, args ...interface{}) sql.Result {
func (t params) mustExec(sql string, args ...any) sql.Result {
res, err := t.DB.Exec(sql, args...)
if err != nil {
t.Fatalf("Error running %q: %v", sql, err)

View File

@@ -388,11 +388,11 @@ func TestConn_Trace(t *testing.T) {
func TestConn_AutoVacuumPages(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t, url.Values{
dsn := memdb.TestDB(t, url.Values{
"_pragma": {"auto_vacuum(full)"},
})
db, err := sqlite3.Open(tmp)
db, err := sqlite3.Open(dsn)
if err != nil {
t.Fatal(err)
}

View File

@@ -12,7 +12,7 @@ import (
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/embed"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
_ "github.com/ncruces/go-sqlite3/vfs/memdb"
"github.com/ncruces/go-sqlite3/vfs/memdb"
)
func TestConn_Open_dir(t *testing.T) {
@@ -112,6 +112,48 @@ func TestConn_Close_BUSY(t *testing.T) {
}
}
func TestConn_BusyHandler(t *testing.T) {
t.Parallel()
dsn := memdb.TestDB(t)
db1, err := sqlite3.Open(dsn)
if err != nil {
t.Fatal(err)
}
defer db1.Close()
db2, err := sqlite3.Open(dsn)
if err != nil {
t.Fatal(err)
}
defer db2.Close()
var called bool
err = db2.BusyHandler(func(ctx context.Context, count int) (retry bool) {
called = true
return count < 1
})
if err != nil {
t.Fatal(err)
}
tx, err := db1.BeginExclusive()
if err != nil {
t.Fatal(err)
}
defer tx.End(&err)
_, err = db2.BeginExclusive()
if !errors.Is(err, sqlite3.BUSY) {
t.Errorf("got %v, want sqlite3.BUSY", err)
}
if !called {
t.Error("busy handler not called")
}
}
func TestConn_SetInterrupt(t *testing.T) {
t.Parallel()
@@ -121,7 +163,7 @@ func TestConn_SetInterrupt(t *testing.T) {
}
defer db.Close()
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
db.SetInterrupt(ctx)
// Interrupt doesn't interrupt this.
@@ -160,9 +202,7 @@ func TestConn_SetInterrupt(t *testing.T) {
t.Errorf("got %v, want sqlite3.INTERRUPT", err)
}
ctx, cancel = context.WithCancel(context.Background())
defer cancel()
db.SetInterrupt(ctx)
db.SetInterrupt(t.Context())
// Interrupting can be cleared.
err = db.Exec(`SELECT 1`)
@@ -170,9 +210,8 @@ func TestConn_SetInterrupt(t *testing.T) {
t.Fatal(err)
}
db.SetInterrupt(ctx)
if got := db.GetInterrupt(); got != ctx {
t.Errorf("got %v, want %v", got, ctx)
if got := db.GetInterrupt(); got != t.Context() {
t.Errorf("got %v, want %v", got, t.Context())
}
}

View File

@@ -1,7 +1,6 @@
package tests
import (
"context"
"testing"
"github.com/ncruces/go-sqlite3"
@@ -13,12 +12,9 @@ import (
func TestDriver(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
db, err := driver.Open(tmp, nil, func(c *sqlite3.Conn) error {
db, err := driver.Open(dsn, nil, func(c *sqlite3.Conn) error {
return c.Exec(`PRAGMA optimize`)
})
if err != nil {
@@ -26,13 +22,13 @@ func TestDriver(t *testing.T) {
}
defer db.Close()
conn, err := db.Conn(ctx)
conn, err := db.Conn(t.Context())
if err != nil {
t.Fatal(err)
}
defer conn.Close()
res, err := conn.ExecContext(ctx,
res, err := conn.ExecContext(t.Context(),
`CREATE TABLE users (id INTEGER PRIMARY KEY NOT NULL, name VARCHAR(10))`)
if err != nil {
t.Fatal(err)
@@ -52,7 +48,7 @@ func TestDriver(t *testing.T) {
t.Errorf("got %d want 0", changes)
}
res, err = conn.ExecContext(ctx,
res, err = conn.ExecContext(t.Context(),
`INSERT INTO users (id, name) VALUES (0, 'go'), (1, 'zig'), (2, 'whatever')`)
if err != nil {
t.Fatal(err)
@@ -65,7 +61,7 @@ func TestDriver(t *testing.T) {
t.Errorf("got %d want 3", changes)
}
stmt, err := conn.PrepareContext(context.Background(),
stmt, err := conn.PrepareContext(t.Context(),
`SELECT id, name FROM users`)
if err != nil {
t.Fatal(err)

View File

@@ -1,7 +1,6 @@
package tests
import (
"context"
"encoding/json"
"math"
"testing"
@@ -17,31 +16,28 @@ import (
func TestJSON(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
db, err := driver.Open(tmp)
db, err := driver.Open(dsn)
if err != nil {
t.Fatal(err)
}
defer db.Close()
conn, err := db.Conn(ctx)
conn, err := db.Conn(t.Context())
if err != nil {
t.Fatal(err)
}
defer conn.Close()
_, err = conn.ExecContext(ctx, `CREATE TABLE test (col)`)
_, err = conn.ExecContext(t.Context(), `CREATE TABLE test (col)`)
if err != nil {
t.Fatal(err)
}
reference := time.Date(2013, 10, 7, 4, 23, 19, 120_000_000, time.FixedZone("", -4*3600))
_, err = conn.ExecContext(ctx,
_, err = conn.ExecContext(t.Context(),
`INSERT INTO test (col) VALUES (?), (?), (?), (?)`,
nil, 1, math.Pi, reference,
)
@@ -49,7 +45,7 @@ func TestJSON(t *testing.T) {
t.Fatal(err)
}
_, err = conn.ExecContext(ctx,
_, err = conn.ExecContext(t.Context(),
`INSERT INTO test (col) VALUES (?), (?), (?), (?)`,
sqlite3.JSON(math.Pi), sqlite3.JSON(false),
julianday.Format(reference), sqlite3.JSON([]string{}))
@@ -57,7 +53,7 @@ func TestJSON(t *testing.T) {
t.Fatal(err)
}
rows, err := conn.QueryContext(ctx, "SELECT * FROM test")
rows, err := conn.QueryContext(t.Context(), "SELECT * FROM test")
if err != nil {
t.Fatal(err)
}

View File

@@ -18,6 +18,7 @@ import (
"github.com/ncruces/go-sqlite3/vfs"
_ "github.com/ncruces/go-sqlite3/vfs/adiantum"
"github.com/ncruces/go-sqlite3/vfs/memdb"
"github.com/ncruces/go-sqlite3/vfs/mvcc"
_ "github.com/ncruces/go-sqlite3/vfs/xts"
)
@@ -98,6 +99,22 @@ func Test_memdb(t *testing.T) {
testIntegrity(t, name)
}
func Test_mvcc(t *testing.T) {
var iter int
if testing.Short() {
iter = 1000
} else {
iter = 5000
}
name := mvcc.TestDB(t, mvcc.Snapshot{}, url.Values{
"_pragma": {"busy_timeout(10000)"},
})
createDB(t, name)
testParallel(t, name, iter)
testIntegrity(t, name)
}
func Test_adiantum(t *testing.T) {
if !vfs.SupportsFileLocking {
t.Skip("skipping without locks")
@@ -312,6 +329,16 @@ func Benchmark_memdb(b *testing.B) {
testParallel(b, name, b.N)
}
func Benchmark_mvcc(b *testing.B) {
name := mvcc.TestDB(b, mvcc.Snapshot{}, url.Values{
"_pragma": {"busy_timeout(10000)"},
})
createDB(b, name)
b.ResetTimer()
testParallel(b, name, b.N)
}
func createDB(t testing.TB, name string) {
db, err := sqlite3.Open(name)
if err != nil {

View File

@@ -496,7 +496,7 @@ func TestStmt(t *testing.T) {
if got := stmt.ColumnBlob(0, nil); string(got) != "true" {
t.Errorf("got %q, want true", got)
}
var got any = 1
var got any
if err := stmt.ColumnJSON(0, &got); err != nil {
t.Error(err)
} else if got != true {

View File

@@ -1,7 +1,6 @@
package tests
import (
"context"
"fmt"
"reflect"
"testing"
@@ -134,37 +133,36 @@ func TestTimeFormat_Decode(t *testing.T) {
func TestTimeFormat_Scanner(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
db, err := driver.Open(tmp)
db, err := driver.Open(dsn)
if err != nil {
t.Fatal(err)
}
defer db.Close()
conn, err := db.Conn(ctx)
conn, err := db.Conn(t.Context())
if err != nil {
t.Fatal(err)
}
defer conn.Close()
_, err = conn.ExecContext(ctx, `CREATE TABLE test (col)`)
_, err = conn.ExecContext(t.Context(), `CREATE TABLE test (col)`)
if err != nil {
t.Fatal(err)
}
reference := time.Date(2013, 10, 7, 4, 23, 19, 120_000_000, time.FixedZone("", -4*3600))
_, err = conn.ExecContext(ctx, `INSERT INTO test VALUES (?)`, sqlite3.TimeFormat7TZ.Encode(reference))
_, err = conn.ExecContext(t.Context(), `INSERT INTO test VALUES (?)`,
sqlite3.TimeFormat7TZ.Encode(reference))
if err != nil {
t.Fatal(err)
}
var got time.Time
err = conn.QueryRowContext(ctx, "SELECT * FROM test").Scan(sqlite3.TimeFormatAuto.Scanner(&got))
err = conn.QueryRowContext(t.Context(), "SELECT * FROM test").
Scan(sqlite3.TimeFormatAuto.Scanner(&got))
if err != nil {
t.Fatal(err)
}

View File

@@ -170,7 +170,7 @@ func TestConn_Transaction_interrupt(t *testing.T) {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
db.SetInterrupt(ctx)
tx, err = db.BeginExclusive()
@@ -203,7 +203,7 @@ func TestConn_Transaction_interrupt(t *testing.T) {
t.Errorf("got %v, want sqlite3.INTERRUPT", err)
}
db.SetInterrupt(context.Background())
db.SetInterrupt(t.Context())
stmt, _, err := db.Prepare(`SELECT count(*) FROM test`)
if err != nil {
t.Fatal(err)
@@ -226,7 +226,7 @@ func TestConn_Transaction_interrupted(t *testing.T) {
}
defer db.Close()
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
db.SetInterrupt(ctx)
cancel()
@@ -246,15 +246,15 @@ func TestConn_Transaction_interrupted(t *testing.T) {
func TestConn_Transaction_busy(t *testing.T) {
t.Parallel()
tmp := memdb.TestDB(t)
dsn := memdb.TestDB(t)
db1, err := sqlite3.Open(tmp)
db1, err := sqlite3.Open(dsn)
if err != nil {
t.Fatal(err)
}
defer db1.Close()
db2, err := sqlite3.Open(tmp + "&_pragma=busy_timeout(10000)")
db2, err := sqlite3.Open(dsn + "&_pragma=busy_timeout(10000)")
if err != nil {
t.Fatal(err)
}
@@ -274,7 +274,7 @@ func TestConn_Transaction_busy(t *testing.T) {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
db2.SetInterrupt(ctx)
go cancel()
@@ -493,7 +493,7 @@ func TestConn_Savepoint_interrupt(t *testing.T) {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
db.SetInterrupt(ctx)
savept1 := db.Savepoint()
@@ -530,7 +530,7 @@ func TestConn_Savepoint_interrupt(t *testing.T) {
t.Errorf("got %v, want sqlite3.INTERRUPT", err)
}
db.SetInterrupt(context.Background())
db.SetInterrupt(t.Context())
stmt, _, err := db.Prepare(`SELECT count(*) FROM test`)
if err != nil {
t.Fatal(err)

View File

@@ -94,7 +94,7 @@ func (f TimeFormat) Encode(t time.Time) any {
case TimeFormatUnix:
return t.Unix()
case TimeFormatUnixFrac:
return float64(t.Unix()) + float64(t.Nanosecond())*1e-9
return math.FMA(1e-9, float64(t.Nanosecond()), float64(t.Unix()))
case TimeFormatUnixMilli:
return t.UnixMilli()
case TimeFormatUnixMicro:

View File

@@ -49,7 +49,7 @@ func (s *SeekingReaderAt) Size() (int64, error) {
return s.r.Seek(0, io.SeekEnd)
}
// ReadAt implements [io.Closer].
// Close implements [io.Closer].
func (s *SeekingReaderAt) Close() error {
s.l.Lock()
defer s.l.Unlock()

View File

@@ -23,7 +23,7 @@ func (FS) Open(name string) (fs.File, error) {
return os.OpenFile(name, os.O_RDONLY, 0)
}
// ReadFileFS implements [fs.StatFS].
// Stat implements [fs.StatFS].
func (FS) Stat(name string) (fs.FileInfo, error) {
return os.Stat(name)
}

View File

@@ -17,14 +17,16 @@ trap 'rm -f sql3parse_table.tmp' EXIT
-mmutable-globals -mnontrapping-fptoint \
-msimd128 -mbulk-memory -msign-ext \
-mreference-types -mmultivalue \
-fno-stack-protector -fno-stack-clash-protection \
-mno-extended-const \
-fno-stack-protector \
-Wl,--stack-first \
-Wl,--import-undefined \
-Wl,--export=sql3parse_table
"$BINARYEN/wasm-ctor-eval" -c _initialize sql3parse_table.wasm -o sql3parse_table.tmp
"$BINARYEN/wasm-opt" --strip --strip-debug --strip-producers -c -Oz \
sql3parse_table.tmp -o sql3parse_table.wasm --low-memory-unused \
"$BINARYEN/wasm-opt" sql3parse_table.tmp -o sql3parse_table.wasm \
--low-memory-unused --gufa --generate-global-effects --converge -Oz \
--enable-mutable-globals --enable-nontrapping-float-to-int \
--enable-simd --enable-bulk-memory --enable-sign-ext \
--enable-reference-types --enable-multivalue
--enable-reference-types --enable-multivalue \
--strip --strip-debug --strip-producers

102
util/vfsutil/slice.go Normal file
View File

@@ -0,0 +1,102 @@
package vfsutil
import (
"io"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/vfs"
)
// SliceFile implements [vfs.File] with a byte slice.
// It is suitable for temporary files (such as [vfs.OPEN_TEMP_JOURNAL]),
// but not concurrency safe.
type SliceFile []byte
var (
// Ensure these interfaces are implemented:
_ vfs.FileSizeHint = &SliceFile{}
)
// ReadAt implements [io.ReaderAt].
func (f *SliceFile) ReadAt(b []byte, off int64) (n int, err error) {
if d := *f; off < int64(len(d)) {
n = copy(b, d[off:])
}
if n < len(b) {
err = io.EOF
}
return
}
// WriteAt implements [io.WriterAt].
func (f *SliceFile) WriteAt(b []byte, off int64) (n int, err error) {
d := *f
if off > int64(len(d)) {
d = append(d, make([]byte, off-int64(len(d)))...)
}
d = append(d[:off], b...)
if len(d) > len(*f) {
*f = d
}
return len(b), nil
}
// Size implements [vfs.File].
func (f *SliceFile) Size() (int64, error) {
return int64(len(*f)), nil
}
// Truncate implements [vfs.File].
func (f *SliceFile) Truncate(size int64) error {
if d := *f; size < int64(len(d)) {
*f = d[:size]
}
return nil
}
// SizeHint implements [vfs.FileSizeHint].
func (f *SliceFile) SizeHint(size int64) error {
if d := *f; size > int64(len(d)) {
*f = append(d, make([]byte, size-int64(len(d)))...)
}
return nil
}
// Close implements [io.Closer].
func (*SliceFile) Close() error { return nil }
// Sync implements [vfs.File].
func (*SliceFile) Sync(flags vfs.SyncFlag) error { return nil }
// Lock implements [vfs.File].
func (*SliceFile) Lock(lock vfs.LockLevel) error {
// notest // not concurrency safe
return sqlite3.IOERR_LOCK
}
// Unlock implements [vfs.File].
func (*SliceFile) Unlock(lock vfs.LockLevel) error {
// notest // not concurrency safe
return sqlite3.IOERR_UNLOCK
}
// CheckReservedLock implements [vfs.File].
func (*SliceFile) CheckReservedLock() (bool, error) {
// notest // not concurrency safe
return false, sqlite3.IOERR_CHECKRESERVEDLOCK
}
// SectorSize implements [vfs.File].
func (*SliceFile) SectorSize() int {
// notest // safe default
return 0
}
// DeviceCharacteristics implements [vfs.File].
func (*SliceFile) DeviceCharacteristics() vfs.DeviceCharacteristic {
return vfs.IOCAP_ATOMIC |
vfs.IOCAP_SEQUENTIAL |
vfs.IOCAP_SAFE_APPEND |
vfs.IOCAP_POWERSAFE_OVERWRITE |
vfs.IOCAP_SUBPAGE_READ
}

View File

@@ -22,6 +22,14 @@ func UnwrapFile[T vfs.File](f vfs.File) (_ T, _ bool) {
}
}
// WrapOpen helps wrap [vfs.VFS].
func WrapOpen(f vfs.VFS, name string, flags vfs.OpenFlag) (file vfs.File, _ vfs.OpenFlag, err error) {
if f, ok := f.(vfs.VFSFilename); name == "" && ok {
return f.OpenFilename(nil, flags)
}
return f.Open(name, flags)
}
// WrapOpenFilename helps wrap [vfs.VFSFilename].
func WrapOpenFilename(f vfs.VFS, name *vfs.Filename, flags vfs.OpenFlag) (file vfs.File, _ vfs.OpenFlag, err error) {
if f, ok := f.(vfs.VFSFilename); ok {

330
util/vfsutil/wrap_test.go Normal file
View File

@@ -0,0 +1,330 @@
// Package vfsutil implements virtual filesystem utilities.
package vfsutil
import (
"testing"
"github.com/ncruces/go-sqlite3/vfs"
)
func TestWrapOpen(t *testing.T) {
called := 0
WrapOpen(mockVFS{open: func(name string, flags vfs.OpenFlag) (vfs.File, vfs.OpenFlag, error) {
called++
return nil, flags, nil
}}, "", 0)
if called != 1 {
t.Error("open not called")
}
WrapOpenFilename(mockVFS{open: func(name string, flags vfs.OpenFlag) (vfs.File, vfs.OpenFlag, error) {
called++
return nil, flags, nil
}}, nil, 0)
if called != 2 {
t.Error("open not called")
}
}
func TestWrapOpenFilename(t *testing.T) {
called := 0
WrapOpen(mockVFSFilename{openFilename: func(name *vfs.Filename, flags vfs.OpenFlag) (vfs.File, vfs.OpenFlag, error) {
called++
return nil, flags, nil
}}, "", 0)
if called != 1 {
t.Error("openFilename not called")
}
WrapOpenFilename(mockVFSFilename{openFilename: func(name *vfs.Filename, flags vfs.OpenFlag) (vfs.File, vfs.OpenFlag, error) {
called++
return nil, flags, nil
}}, nil, 0)
if called != 2 {
t.Error("openFilename not called")
}
}
func TestWrapLockState(t *testing.T) {
called := 0
WrapLockState(mockFile{lockState: func() vfs.LockLevel {
called++
return 0
}})
if called != 1 {
t.Error("lockState not called")
}
}
func TestWrapPersistWAL(t *testing.T) {
persist := false
WrapSetPersistWAL(mockFile{setPersistWAL: func(b bool) { persist = b }}, true)
if !persist {
t.Error("setPersistWAL not called")
}
called := 0
WrapPersistWAL(mockFile{persistWAL: func() bool { called++; return persist }})
if !persist {
t.Error("persistWAL not called")
}
if called != 1 {
}
}
func TestWrapPowersafeOverwrite(t *testing.T) {
persist := false
WrapSetPowersafeOverwrite(mockFile{setPowersafeOverwrite: func(b bool) { persist = b }}, true)
if !persist {
t.Error("setPowersafeOverwrite not called")
}
called := 0
WrapPowersafeOverwrite(mockFile{powersafeOverwrite: func() bool { called++; return persist }})
if !persist {
t.Error("powersafeOverwrite not called")
}
if called != 1 {
}
}
func TestWrapChunkSize(t *testing.T) {
var chunk int
WrapChunkSize(mockFile{chunkSize: func(size int) {
chunk = size
}}, 5)
if chunk != 5 {
t.Error("chunkSize not called")
}
}
func TestWrapSizeHint(t *testing.T) {
var hint int64
WrapSizeHint(mockFile{sizeHint: func(size int64) error {
hint = size
return nil
}}, 5)
if hint != 5 {
t.Error("sizeHint not called")
}
}
func TestWrapHasMoved(t *testing.T) {
called := 0
WrapHasMoved(mockFile{hasMoved: func() (bool, error) {
called++
return false, nil
}})
if called != 1 {
t.Error("hasMoved not called")
}
}
func TestWrapOverwrite(t *testing.T) {
called := 0
WrapOverwrite(mockFile{overwrite: func() error {
called++
return nil
}})
if called != 1 {
t.Error("overwrite not called")
}
}
func TestWrapSyncSuper(t *testing.T) {
called := 0
WrapSyncSuper(mockFile{syncSuper: func(super string) error {
called++
return nil
}}, "")
if called != 1 {
t.Error("syncSuper not called")
}
}
func TestWrapCommitPhaseTwo(t *testing.T) {
called := 0
WrapCommitPhaseTwo(mockFile{commitPhaseTwo: func() error {
called++
return nil
}})
if called != 1 {
t.Error("commitPhaseTwo not called")
}
}
func TestWrapBatchAtomicWrite(t *testing.T) {
calledBegin := 0
calledCommit := 0
calledRollback := 0
f := mockFile{
begin: func() error { calledBegin++; return nil },
commit: func() error { calledCommit++; return nil },
rollback: func() error { calledRollback++; return nil },
}
WrapBeginAtomicWrite(f)
WrapCommitAtomicWrite(f)
WrapRollbackAtomicWrite(f)
if calledBegin != 1 {
t.Error("beginAtomicWrite not called")
}
if calledCommit != 1 {
t.Error("commitAtomicWrite not called")
}
if calledRollback != 1 {
t.Error("rollbackAtomicWrite not called")
}
}
func TestWrapCheckpoint(t *testing.T) {
calledStart := 0
calledDone := 0
f := mockFile{
ckptStart: func() { calledStart++ },
ckptDone: func() { calledDone++ },
}
WrapCheckpointStart(f)
WrapCheckpointDone(f)
if calledStart != 1 {
t.Error("checkpointStart not called")
}
if calledDone != 1 {
t.Error("checkpointDone not called")
}
}
func TestWrapPragma(t *testing.T) {
called := 0
val, err := WrapPragma(mockFile{
pragma: func(name, value string) (string, error) {
called++
if name != "foo" || value != "bar" {
t.Error("wrong pragma arguments")
}
return "baz", nil
},
}, "foo", "bar")
if called != 1 {
t.Error("pragma not called")
}
if err != nil {
t.Error(err)
}
if val != "baz" {
t.Error("unexpected pragma return value")
}
}
func TestWrapBusyHandler(t *testing.T) {
called := 0
WrapBusyHandler(mockFile{
busyHandler: func(handler func() bool) {
handler()
called++
},
}, func() bool { return true })
if called != 1 {
t.Error("busyHandler not called")
}
}
type mockVFS struct {
open func(name string, flags vfs.OpenFlag) (vfs.File, vfs.OpenFlag, error)
}
func (m mockVFS) Open(name string, flags vfs.OpenFlag) (vfs.File, vfs.OpenFlag, error) {
return m.open(name, flags)
}
func (m mockVFS) Delete(name string, syncDir bool) error { panic("unimplemented") }
func (m mockVFS) FullPathname(name string) (string, error) { panic("unimplemented") }
func (m mockVFS) Access(name string, flags vfs.AccessFlag) (bool, error) { panic("unimplemented") }
type mockVFSFilename struct {
mockVFS
openFilename func(name *vfs.Filename, flags vfs.OpenFlag) (vfs.File, vfs.OpenFlag, error)
}
func (m mockVFSFilename) OpenFilename(name *vfs.Filename, flags vfs.OpenFlag) (vfs.File, vfs.OpenFlag, error) {
return m.openFilename(name, flags)
}
type mockFile struct {
lockState func() vfs.LockLevel
persistWAL func() bool
setPersistWAL func(bool)
powersafeOverwrite func() bool
setPowersafeOverwrite func(bool)
chunkSize func(int)
sizeHint func(int64) error
hasMoved func() (bool, error)
overwrite func() error
syncSuper func(super string) error
commitPhaseTwo func() error
begin func() error
commit func() error
rollback func() error
ckptStart func()
ckptDone func()
busyHandler func(func() bool)
pragma func(name, value string) (string, error)
}
func (m mockFile) LockState() vfs.LockLevel { return m.lockState() }
func (m mockFile) PersistWAL() bool { return m.persistWAL() }
func (m mockFile) SetPersistWAL(v bool) { m.setPersistWAL(v) }
func (m mockFile) PowersafeOverwrite() bool { return m.powersafeOverwrite() }
func (m mockFile) SetPowersafeOverwrite(v bool) { m.setPowersafeOverwrite(v) }
func (m mockFile) ChunkSize(s int) { m.chunkSize(s) }
func (m mockFile) SizeHint(s int64) error { return m.sizeHint(s) }
func (m mockFile) HasMoved() (bool, error) { return m.hasMoved() }
func (m mockFile) Overwrite() error { return m.overwrite() }
func (m mockFile) SyncSuper(s string) error { return m.syncSuper(s) }
func (m mockFile) CommitPhaseTwo() error { return m.commitPhaseTwo() }
func (m mockFile) BeginAtomicWrite() error { return m.begin() }
func (m mockFile) CommitAtomicWrite() error { return m.commit() }
func (m mockFile) RollbackAtomicWrite() error { return m.rollback() }
func (m mockFile) CheckpointStart() { m.ckptStart() }
func (m mockFile) CheckpointDone() { m.ckptDone() }
func (m mockFile) BusyHandler(f func() bool) { m.busyHandler(f) }
func (m mockFile) Pragma(n, v string) (string, error) { return m.pragma(n, v) }
func (m mockFile) Close() error { panic("unimplemented") }
func (m mockFile) ReadAt(p []byte, off int64) (n int, err error) { panic("unimplemented") }
func (m mockFile) WriteAt(p []byte, off int64) (n int, err error) { panic("unimplemented") }
func (m mockFile) Truncate(size int64) error { panic("unimplemented") }
func (m mockFile) Sync(flags vfs.SyncFlag) error { panic("unimplemented") }
func (m mockFile) Size() (int64, error) { panic("unimplemented") }
func (m mockFile) Lock(lock vfs.LockLevel) error { panic("unimplemented") }
func (m mockFile) Unlock(lock vfs.LockLevel) error { panic("unimplemented") }
func (m mockFile) CheckReservedLock() (bool, error) { panic("unimplemented") }
func (m mockFile) SectorSize() int { panic("unimplemented") }
func (m mockFile) DeviceCharacteristics() vfs.DeviceCharacteristic { panic("unimplemented") }

View File

@@ -1,9 +1,7 @@
package sqlite3
import (
"encoding/json"
"math"
"strconv"
"time"
"github.com/ncruces/go-sqlite3/internal/util"
@@ -31,9 +29,9 @@ func (v Value) Dup() *Value {
// Close frees an SQL value previously obtained by [Value.Dup].
//
// https://sqlite.org/c3ref/value_dup.html
func (dup *Value) Close() error {
dup.c.call("sqlite3_value_free", stk_t(dup.handle))
dup.handle = 0
func (v *Value) Close() error {
v.c.call("sqlite3_value_free", stk_t(v.handle))
v.handle = 0
return nil
}
@@ -162,27 +160,6 @@ func (v Value) Pointer() any {
return util.GetHandle(v.c.ctx, ptr)
}
// JSON parses a JSON-encoded value
// and stores the result in the value pointed to by ptr.
func (v Value) JSON(ptr any) error {
var data []byte
switch v.Type() {
case NULL:
data = []byte("null")
case TEXT:
data = v.RawText()
case BLOB:
data = v.RawBlob()
case INTEGER:
data = strconv.AppendInt(nil, v.Int64(), 10)
case FLOAT:
data = util.AppendNumber(nil, v.Float())
default:
panic(util.AssertErr())
}
return json.Unmarshal(data, ptr)
}
// NoChange returns true if and only if the value is unchanged
// in a virtual table update operatiom.
//

View File

@@ -25,8 +25,8 @@ func (adiantumCreator) HBSH(key []byte) *hbsh.HBSH {
func (adiantumCreator) KDF(text string) []byte {
if text == "" {
key := make([]byte, 32)
n, _ := rand.Read(key)
return key[:n]
rand.Read(key)
return key
}
return argon2.IDKey([]byte(text), []byte(pepper), 3, 64*1024, 4, 32)
}

View File

@@ -54,9 +54,8 @@ func Test_fileformat(t *testing.T) {
func Benchmark_nokey(b *testing.B) {
tmp := filepath.Join(b.TempDir(), "test.db")
sqlite3.Initialize()
b.ResetTimer()
for range b.N {
for b.Loop() {
db, err := sqlite3.Open("file:" + filepath.ToSlash(tmp) + "?nolock=1")
if err != nil {
b.Fatal(err)
@@ -68,9 +67,8 @@ func Benchmark_nokey(b *testing.B) {
func Benchmark_hexkey(b *testing.B) {
tmp := filepath.Join(b.TempDir(), "test.db")
sqlite3.Initialize()
b.ResetTimer()
for range b.N {
for b.Loop() {
db, err := sqlite3.Open("file:" + filepath.ToSlash(tmp) + "?nolock=1" +
"&vfs=adiantum&hexkey=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
if err != nil {
@@ -83,9 +81,8 @@ func Benchmark_hexkey(b *testing.B) {
func Benchmark_textkey(b *testing.B) {
tmp := filepath.Join(b.TempDir(), "test.db")
sqlite3.Initialize()
b.ResetTimer()
for range b.N {
for b.Loop() {
db, err := sqlite3.Open("file:" + filepath.ToSlash(tmp) + "?nolock=1" +
"&vfs=adiantum&textkey=correct+horse+battery+staple")
if err != nil {

View File

@@ -45,8 +45,8 @@ func (hpolycCreator) KDF(secret string) []byte {
if secret == "" {
// No secret is given, generate a random key.
key := make([]byte, 32)
n, _ := rand.Read(key)
return key[:n]
rand.Read(key)
return key
}
// Hash the secret with a KDF.
return argon2.IDKey([]byte(secret), []byte("hpolyc"), 3, 64*1024, 4, 32)

View File

@@ -20,7 +20,10 @@ type hbshVFS struct {
func (h *hbshVFS) Open(name string, flags vfs.OpenFlag) (vfs.File, vfs.OpenFlag, error) {
// notest // OpenFilename is called instead
return nil, 0, sqlite3.CANTOPEN
if name == "" {
return h.OpenFilename(nil, flags)
}
return nil, flags, sqlite3.CANTOPEN
}
func (h *hbshVFS) OpenFilename(name *vfs.Filename, flags vfs.OpenFlag) (file vfs.File, _ vfs.OpenFlag, err error) {
@@ -160,10 +163,11 @@ func (h *hbshFile) WriteAt(p []byte, off int64) (n int, err error) {
if off > min || len(p[n:]) < blockSize {
// Partial block write: read-update-write.
m, err := h.File.ReadAt(h.block[:], min)
if m != blockSize {
if err != io.EOF {
return n, err
}
if m == blockSize {
data = h.hbsh.Decrypt(h.block[:], h.tweak[:])
} else if err != io.EOF {
return n, err
} else {
// Writing past the EOF.
// We're either appending an entirely new block,
// or the final block was only partially written.
@@ -171,8 +175,6 @@ func (h *hbshFile) WriteAt(p []byte, off int64) (n int, err error) {
// and is as good as corrupt.
// Either way, zero pad the file to the next block size.
clear(data)
} else {
data = h.hbsh.Decrypt(h.block[:], h.tweak[:])
}
if off > min {
data = data[off-min:]
@@ -223,16 +225,16 @@ func (h *hbshFile) SizeHint(size int64) error {
return vfsutil.WrapSizeHint(h.File, roundUp(size))
}
// Wrap optional methods.
func (h *hbshFile) Unwrap() vfs.File {
return h.File
return h.File // notest
}
func (h *hbshFile) SharedMemory() vfs.SharedMemory {
return vfsutil.WrapSharedMemory(h.File)
return vfsutil.WrapSharedMemory(h.File) // notest
}
// Wrap optional methods.
func (h *hbshFile) LockState() vfs.LockLevel {
return vfsutil.WrapLockState(h.File) // notest
}

View File

@@ -36,9 +36,9 @@ type VFSFilename interface {
//
// https://sqlite.org/c3ref/io_methods.html
type File interface {
Close() error
ReadAt(p []byte, off int64) (n int, err error)
WriteAt(p []byte, off int64) (n int, err error)
io.Closer
io.ReaderAt
io.WriterAt
Truncate(size int64) error
Sync(flags SyncFlag) error
Size() (int64, error)

View File

@@ -5,7 +5,6 @@ import (
"context"
_ "embed"
"encoding/binary"
"strconv"
"github.com/tetratelabs/wazero/api"
@@ -13,48 +12,30 @@ import (
"github.com/ncruces/go-sqlite3/util/sql3util"
)
func cksmWrapFile(name *Filename, flags OpenFlag, file File) File {
// Checksum only main databases and WALs.
if flags&(OPEN_MAIN_DB|OPEN_WAL) == 0 {
func cksmWrapFile(file File, flags OpenFlag) File {
// Checksum only main databases.
if flags&OPEN_MAIN_DB == 0 {
return file
}
cksm := cksmFile{File: file}
if flags&OPEN_WAL != 0 {
main, _ := name.DatabaseFile().(cksmFile)
cksm.cksmFlags = main.cksmFlags
} else {
cksm.cksmFlags = new(cksmFlags)
cksm.isDB = true
}
return cksm
return &cksmFile{File: file}
}
type cksmFile struct {
File
*cksmFlags
isDB bool
}
type cksmFlags struct {
computeCksm bool
verifyCksm bool
inCkpt bool
pageSize int
computeCksm bool
}
func (c cksmFile) ReadAt(p []byte, off int64) (n int, err error) {
func (c *cksmFile) ReadAt(p []byte, off int64) (n int, err error) {
n, err = c.File.ReadAt(p, off)
p = p[:n]
if isHeader(c.isDB, p, off) {
if isHeader(p, off) {
c.init((*[100]byte)(p))
}
// Verify checksums.
if c.verifyCksm && !c.inCkpt && len(p) == c.pageSize {
if c.verifyCksm && sql3util.ValidPageSize(len(p)) {
cksm1 := cksmCompute(p[:len(p)-8])
cksm2 := *(*[8]byte)(p[len(p)-8:])
if cksm1 != cksm2 {
@@ -64,20 +45,20 @@ func (c cksmFile) ReadAt(p []byte, off int64) (n int, err error) {
return n, err
}
func (c cksmFile) WriteAt(p []byte, off int64) (n int, err error) {
if isHeader(c.isDB, p, off) {
func (c *cksmFile) WriteAt(p []byte, off int64) (n int, err error) {
if isHeader(p, off) {
c.init((*[100]byte)(p))
}
// Compute checksums.
if c.computeCksm && !c.inCkpt && len(p) == c.pageSize {
if c.computeCksm && sql3util.ValidPageSize(len(p)) {
*(*[8]byte)(p[len(p)-8:]) = cksmCompute(p[:len(p)-8])
}
return c.File.WriteAt(p, off)
}
func (c cksmFile) Pragma(name string, value string) (string, error) {
func (c *cksmFile) Pragma(name string, value string) (string, error) {
switch name {
case "checksum_verification":
b, ok := sql3util.ParseBool(value)
@@ -90,15 +71,15 @@ func (c cksmFile) Pragma(name string, value string) (string, error) {
return "1", nil
case "page_size":
if c.computeCksm {
if c.computeCksm && value != "" {
// Do not allow page size changes on a checksum database.
return strconv.Itoa(c.pageSize), nil
return "", nil
}
}
return "", _NOTFOUND
}
func (c cksmFile) DeviceCharacteristics() DeviceCharacteristic {
func (c *cksmFile) DeviceCharacteristics() DeviceCharacteristic {
ret := c.File.DeviceCharacteristics()
if c.verifyCksm {
ret &^= IOCAP_SUBPAGE_READ
@@ -106,13 +87,8 @@ func (c cksmFile) DeviceCharacteristics() DeviceCharacteristic {
return ret
}
func (c cksmFile) fileControl(ctx context.Context, mod api.Module, op _FcntlOpcode, pArg ptr_t) _ErrorCode {
switch op {
case _FCNTL_CKPT_START:
c.inCkpt = true
case _FCNTL_CKPT_DONE:
c.inCkpt = false
case _FCNTL_PRAGMA:
func (c *cksmFile) fileControl(ctx context.Context, mod api.Module, op _FcntlOpcode, pArg ptr_t) _ErrorCode {
if op == _FCNTL_PRAGMA {
rc := vfsFileControlImpl(ctx, mod, c, op, pArg)
if rc != _NOTFOUND {
return rc
@@ -121,24 +97,26 @@ func (c cksmFile) fileControl(ctx context.Context, mod api.Module, op _FcntlOpco
return vfsFileControlImpl(ctx, mod, c.File, op, pArg)
}
func (f *cksmFlags) init(header *[100]byte) {
f.pageSize = 256 * int(binary.LittleEndian.Uint16(header[16:18]))
if r := header[20] == 8; r != f.computeCksm {
f.computeCksm = r
f.verifyCksm = r
}
if !sql3util.ValidPageSize(f.pageSize) {
f.computeCksm = false
f.verifyCksm = false
func (c *cksmFile) init(header *[100]byte) {
if r := header[20] == 8; r != c.computeCksm {
c.computeCksm = r
c.verifyCksm = r
}
}
func isHeader(isDB bool, p []byte, off int64) bool {
check := sql3util.ValidPageSize(len(p))
if isDB {
check = off == 0 && len(p) >= 100
func (c *cksmFile) SharedMemory() SharedMemory {
if f, ok := c.File.(FileSharedMemory); ok {
return f.SharedMemory()
}
return check && bytes.HasPrefix(p, []byte("SQLite format 3\000"))
return nil
}
func (c *cksmFile) Unwrap() File {
return c.File
}
func isHeader(p []byte, off int64) bool {
return off == 0 && len(p) >= 100 && bytes.HasPrefix(p, []byte("SQLite format 3\000"))
}
func cksmCompute(a []byte) (cksm [8]byte) {
@@ -155,14 +133,3 @@ func cksmCompute(a []byte) (cksm [8]byte) {
binary.LittleEndian.PutUint32(cksm[4:8], s2)
return
}
func (c cksmFile) SharedMemory() SharedMemory {
if f, ok := c.File.(FileSharedMemory); ok {
return f.SharedMemory()
}
return nil
}
func (c cksmFile) Unwrap() File {
return c.File
}

View File

@@ -94,6 +94,10 @@ const (
OPEN_PRIVATECACHE OpenFlag = 0x00040000 /* Ok for sqlite3_open_v2() */
OPEN_WAL OpenFlag = 0x00080000 /* VFS only */
OPEN_NOFOLLOW OpenFlag = 0x01000000 /* Ok for sqlite3_open_v2() */
_FLAG_ATOMIC OpenFlag = 0x10000000
_FLAG_KEEP_WAL OpenFlag = 0x20000000
_FLAG_PSOW OpenFlag = 0x40000000
_FLAG_SYNC_DIR OpenFlag = 0x80000000
)
// AccessFlag is a flag for the [VFS] Access method.

View File

@@ -51,7 +51,7 @@ func (vfsOS) Delete(path string, syncDir bool) error {
return _OK
}
defer f.Close()
err = osSync(f, false, false)
err = osSync(f, 0, SYNC_FULL)
if err != nil {
return _IOERR_DIR_FSYNC
}
@@ -75,6 +75,9 @@ func (vfsOS) Access(name string, flags AccessFlag) (bool, error) {
func (vfsOS) Open(name string, flags OpenFlag) (File, OpenFlag, error) {
// notest // OpenFilename is called instead
if name == "" {
return vfsOS{}.OpenFilename(nil, flags)
}
return nil, 0, _CANTOPEN
}
@@ -128,27 +131,24 @@ func (vfsOS) OpenFilename(name *Filename, flags OpenFlag) (File, OpenFlag, error
}
file := vfsFile{
File: f,
psow: true,
atomic: osBatchAtomic(f),
readOnly: flags&OPEN_READONLY != 0,
syncDir: isUnix && isCreate && isJournl,
delete: !isUnix && flags&OPEN_DELETEONCLOSE != 0,
shm: NewSharedMemory(name.String()+"-shm", flags),
File: f,
flags: flags | _FLAG_PSOW,
shm: NewSharedMemory(name.String()+"-shm", flags),
}
if osBatchAtomic(f) {
file.flags |= _FLAG_ATOMIC
}
if isUnix && isCreate && isJournl {
file.flags |= _FLAG_SYNC_DIR
}
return &file, flags, nil
}
type vfsFile struct {
*os.File
shm SharedMemory
lock LockLevel
readOnly bool
keepWAL bool
syncDir bool
atomic bool
delete bool
psow bool
shm SharedMemory
lock LockLevel
flags OpenFlag
}
var (
@@ -161,7 +161,7 @@ var (
)
func (f *vfsFile) Close() error {
if f.delete {
if !isUnix && f.flags&OPEN_DELETEONCLOSE != 0 {
defer os.Remove(f.Name())
}
if f.shm != nil {
@@ -180,21 +180,18 @@ func (f *vfsFile) WriteAt(p []byte, off int64) (n int, err error) {
}
func (f *vfsFile) Sync(flags SyncFlag) error {
dataonly := (flags & SYNC_DATAONLY) != 0
fullsync := (flags & 0x0f) == SYNC_FULL
err := osSync(f.File, fullsync, dataonly)
err := osSync(f.File, f.flags, flags)
if err != nil {
return err
}
if isUnix && f.syncDir {
f.syncDir = false
if isUnix && f.flags&_FLAG_SYNC_DIR != 0 {
f.flags ^= _FLAG_SYNC_DIR
d, err := os.Open(filepath.Dir(f.File.Name()))
if err != nil {
return nil
}
defer d.Close()
err = osSync(d, false, false)
err = osSync(f.File, f.flags, flags)
if err != nil {
return _IOERR_DIR_FSYNC
}
@@ -212,10 +209,10 @@ func (f *vfsFile) SectorSize() int {
func (f *vfsFile) DeviceCharacteristics() DeviceCharacteristic {
ret := IOCAP_SUBPAGE_READ
if f.atomic {
if f.flags&_FLAG_ATOMIC != 0 {
ret |= IOCAP_BATCH_ATOMIC
}
if f.psow {
if f.flags&_FLAG_PSOW != 0 {
ret |= IOCAP_POWERSAFE_OVERWRITE
}
if runtime.GOOS == "windows" {
@@ -246,8 +243,20 @@ func (f *vfsFile) HasMoved() (bool, error) {
return !os.SameFile(fi, pi), nil
}
func (f *vfsFile) LockState() LockLevel { return f.lock }
func (f *vfsFile) PowersafeOverwrite() bool { return f.psow }
func (f *vfsFile) PersistWAL() bool { return f.keepWAL }
func (f *vfsFile) SetPowersafeOverwrite(psow bool) { f.psow = psow }
func (f *vfsFile) SetPersistWAL(keepWAL bool) { f.keepWAL = keepWAL }
func (f *vfsFile) LockState() LockLevel { return f.lock }
func (f *vfsFile) PowersafeOverwrite() bool { return f.flags&_FLAG_PSOW != 0 }
func (f *vfsFile) PersistWAL() bool { return f.flags&_FLAG_KEEP_WAL != 0 }
func (f *vfsFile) SetPowersafeOverwrite(psow bool) {
f.flags &^= _FLAG_PSOW
if psow {
f.flags |= _FLAG_PSOW
}
}
func (f *vfsFile) SetPersistWAL(keepWAL bool) {
f.flags &^= _FLAG_KEEP_WAL
if keepWAL {
f.flags |= _FLAG_KEEP_WAL
}
}

View File

@@ -56,7 +56,7 @@ func (n *Filename) Journal() string {
return n.path("sqlite3_filename_journal")
}
// Journal returns the name of the corresponding WAL file.
// WAL returns the name of the corresponding WAL file.
//
// https://sqlite.org/c3ref/filename_database.html
func (n *Filename) WAL() string {

View File

@@ -41,7 +41,7 @@ func (f *vfsFile) Lock(lock LockLevel) error {
}
// Do not allow any kind of write-lock on a read-only database.
if f.readOnly && lock >= LOCK_RESERVED {
if lock >= LOCK_RESERVED && f.flags&OPEN_READONLY != 0 {
return _IOERR_LOCK
}

View File

@@ -6,4 +6,7 @@ SQLite VFS in pure Go.
It has some benefits over the C version:
- the memory backing the database needs not be contiguous,
- the database can grow/shrink incrementally without copying,
- reader-writer concurrency is slightly improved.
- reader-writer concurrency is slightly improved.
[`memdb.TestDB`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/memdb#TestDB)
is the preferred way to setup an in-memory database for testing.

View File

@@ -10,6 +10,7 @@
package memdb
import (
"crypto/rand"
"fmt"
"net/url"
"sync"
@@ -74,11 +75,27 @@ func Delete(name string) {
// TestDB creates an empty shared memory database for the test to use.
// The database is automatically deleted when the test and all its subtests complete.
// Returns a URI filename appropriate to call Open with.
// Each subsequent call to TestDB returns a unique database.
//
// func Test_something(t *testing.T) {
// t.Parallel()
// dsn := memdb.TestDB(t, url.Values{
// "_pragma": {"busy_timeout(1000)"},
// })
//
// db, err := sql.Open("sqlite3", dsn)
// if err != nil {
// t.Fatal(err)
// }
// defer db.Close()
//
// // ...
// }
func TestDB(tb testing.TB, params ...url.Values) string {
tb.Helper()
name := fmt.Sprintf("%s_%p", tb.Name(), tb)
name := fmt.Sprintf("%s_%s", tb.Name(), rand.Text())
tb.Cleanup(func() { Delete(name) })
Create(name, nil)

View File

@@ -2,40 +2,39 @@ package memdb
import (
"io"
"strings"
"sync"
"time"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/util/vfsutil"
"github.com/ncruces/go-sqlite3/vfs"
)
const sectorSize = 65536
// Ensure sectorSize is a multiple of 64K (the largest page size).
var _ [0]struct{} = [sectorSize & 65535]struct{}{}
type memVFS struct{}
func (memVFS) Open(name string, flags vfs.OpenFlag) (vfs.File, vfs.OpenFlag, error) {
// For simplicity, we do not support reading or writing data
// across "sector" boundaries.
//
// This is not a problem for most SQLite file types:
// - databases, which only do page aligned reads/writes;
// - temp journals, as used by the sorter, which does the same:
// https://github.com/sqlite/sqlite/blob/b74eb0/src/vdbesort.c#L409-L412
//
// We refuse to open all other file types,
// but returning OPEN_MEMORY means SQLite won't ask us to.
const types = vfs.OPEN_MAIN_DB | vfs.OPEN_TEMP_DB |
vfs.OPEN_TRANSIENT_DB | vfs.OPEN_TEMP_JOURNAL
if flags&types == 0 {
// This is not a problem for SQLite database files.
const databases = vfs.OPEN_MAIN_DB | vfs.OPEN_TEMP_DB | vfs.OPEN_TRANSIENT_DB
// Temp journals, as used by the sorter, use SliceFile.
if flags&vfs.OPEN_TEMP_JOURNAL != 0 {
return &vfsutil.SliceFile{}, flags | vfs.OPEN_MEMORY, nil
}
// Refuse to open all other file types.
// Returning OPEN_MEMORY means SQLite won't ask us to.
if flags&databases == 0 {
// notest // OPEN_MEMORY
return nil, flags, sqlite3.CANTOPEN
}
// A shared database has a name that begins with "/".
shared := len(name) > 1 && name[0] == '/'
shared := strings.HasPrefix(name, "/")
var db *memDB
if shared {
@@ -76,18 +75,16 @@ func (memVFS) FullPathname(name string) (string, error) {
type memDB struct {
name string
// +checklocks:lockMtx
waiter *sync.Cond
// +checklocks:dataMtx
data []*[sectorSize]byte
// +checklocks:dataMtx
size int64
// +checklocks:memoryMtx
refs int32
shared int32 // +checklocks:lockMtx
pending bool // +checklocks:lockMtx
reserved bool // +checklocks:lockMtx
waiter *sync.Cond // +checklocks:lockMtx
size int64 // +checklocks:dataMtx
refs int32 // +checklocks:memoryMtx
shared int32 // +checklocks:lockMtx
pending bool // +checklocks:lockMtx
reserved bool // +checklocks:lockMtx
lockMtx sync.Mutex
dataMtx sync.RWMutex
@@ -129,7 +126,7 @@ func (m *memFile) ReadAt(b []byte, off int64) (n int, err error) {
base := off / sectorSize
rest := off % sectorSize
have := int64(sectorSize)
if base == int64(len(m.data))-1 {
if m.size < off+int64(len(b)) {
have = modRoundUp(m.size, sectorSize)
}
n = copy(b, (*m.data[base])[rest:have])
@@ -150,22 +147,37 @@ func (m *memFile) WriteAt(b []byte, off int64) (n int, err error) {
m.data = append(m.data, new([sectorSize]byte))
}
n = copy((*m.data[base])[rest:], b)
if size := off + int64(n); size > m.size {
m.size = size
}
if n < len(b) {
// notest // assume writes are page aligned
return n, io.ErrShortWrite
}
if size := off + int64(len(b)); size > m.size {
m.size = size
}
return n, nil
}
func (m *memFile) Size() (int64, error) {
m.dataMtx.RLock()
defer m.dataMtx.RUnlock()
return m.size, nil
}
func (m *memFile) Truncate(size int64) error {
m.dataMtx.Lock()
defer m.dataMtx.Unlock()
return m.truncate(size)
}
func (m *memFile) SizeHint(size int64) error {
m.dataMtx.Lock()
defer m.dataMtx.Unlock()
if size > m.size {
return m.truncate(size)
}
return nil
}
// +checklocks:m.dataMtx
func (m *memFile) truncate(size int64) error {
if size < m.size {
@@ -185,16 +197,6 @@ func (m *memFile) truncate(size int64) error {
return nil
}
func (m *memFile) Sync(flag vfs.SyncFlag) error {
return nil
}
func (m *memFile) Size() (int64, error) {
m.dataMtx.RLock()
defer m.dataMtx.RUnlock()
return m.size, nil
}
func (m *memFile) Lock(lock vfs.LockLevel) error {
if m.lock >= lock {
return nil
@@ -278,31 +280,24 @@ func (m *memFile) CheckReservedLock() (bool, error) {
return m.reserved, nil
}
func (m *memFile) SectorSize() int {
func (m *memFile) LockState() vfs.LockLevel {
return m.lock
}
func (*memFile) Sync(flag vfs.SyncFlag) error { return nil }
func (*memFile) SectorSize() int {
// notest // IOCAP_POWERSAFE_OVERWRITE
return sectorSize
}
func (m *memFile) DeviceCharacteristics() vfs.DeviceCharacteristic {
func (*memFile) DeviceCharacteristics() vfs.DeviceCharacteristic {
return vfs.IOCAP_ATOMIC |
vfs.IOCAP_SEQUENTIAL |
vfs.IOCAP_SAFE_APPEND |
vfs.IOCAP_POWERSAFE_OVERWRITE
}
func (m *memFile) SizeHint(size int64) error {
m.dataMtx.Lock()
defer m.dataMtx.Unlock()
if size > m.size {
return m.truncate(size)
}
return nil
}
func (m *memFile) LockState() vfs.LockLevel {
return m.lock
}
func divRoundUp(a, b int64) int64 {
return (a + b - 1) / b
}

15
vfs/mvcc/README.md Normal file
View File

@@ -0,0 +1,15 @@
# Go `mvcc` SQLite VFS
This package implements the **EXPERIMENTAL** `"mvcc"` in-memory SQLite VFS.
It has some benefits over the [`"memdb"`](../memdb/README.md) VFS:
- panics do not corrupt a shared database,
- single-writer not blocked by readers,
- readers never block,
- instant snapshots.
[`mvcc.TestDB`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/mvcc#TestDB)
is the preferred way to setup an in-memory database for testing
when you intend to leverage snapshots,
e.g. to setup many independent copies of a database,
such as one for each subtest.

149
vfs/mvcc/api.go Normal file
View File

@@ -0,0 +1,149 @@
// Package mvcc implements the "mvcc" SQLite VFS.
//
// The "mvcc" [vfs.VFS] allows the same in-memory database to be shared
// among multiple database connections in the same process,
// as long as the database name begins with "/".
//
// Importing package mvcc registers the VFS:
//
// import _ "github.com/ncruces/go-sqlite3/vfs/mvcc"
package mvcc
import (
"crypto/rand"
"fmt"
"net/url"
"strings"
"sync"
"testing"
"github.com/ncruces/go-sqlite3/vfs"
"github.com/ncruces/wbt"
)
func init() {
vfs.Register("mvcc", mvccVFS{})
}
var (
memoryMtx sync.Mutex
// +checklocks:memoryMtx
memoryDBs = map[string]*mvccDB{}
)
// Create creates a shared memory database,
// using a snapshot as its initial contents.
func Create(name string, snapshot Snapshot) {
memoryMtx.Lock()
defer memoryMtx.Unlock()
memoryDBs[name] = &mvccDB{
refs: 1,
name: name,
data: snapshot.Tree,
}
}
// Delete deletes a shared memory database.
func Delete(name string) {
name = getName(name)
memoryMtx.Lock()
defer memoryMtx.Unlock()
delete(memoryDBs, name)
}
// Snapshot represents a database snapshot.
type Snapshot struct {
*wbt.Tree[int64, string]
}
// NewSnapshot creates a snapshot from data.
func NewSnapshot(data string) Snapshot {
var tree *wbt.Tree[int64, string]
// Convert data from WAL/2 to rollback journal.
if len(data) >= 20 && (false ||
data[18] == 2 && data[19] == 2 ||
data[18] == 3 && data[19] == 3) {
tree = tree.
Put(0, data[:18]).
Put(18, "\001\001").
Put(20, data[20:])
} else if len(data) > 0 {
tree = tree.Put(0, data)
}
return Snapshot{tree}
}
// TakeSnapshot takes a snapshot of a database.
// Name may be a URI filename.
func TakeSnapshot(name string) Snapshot {
name = getName(name)
memoryMtx.Lock()
defer memoryMtx.Unlock()
db := memoryDBs[name]
if db == nil {
return Snapshot{}
}
db.mtx.Lock()
defer db.mtx.Unlock()
return Snapshot{db.data}
}
// TestDB creates a shared database from a snapshot for the test to use.
// The database is automatically deleted when the test and all its subtests complete.
// Returns a URI filename appropriate to call Open with.
// Each subsequent call to TestDB returns a unique database.
//
// func Test_something(t *testing.T) {
// t.Parallel()
// dsn := mvcc.TestDB(t, snapshot, url.Values{
// "_pragma": {"busy_timeout(1000)"},
// })
//
// db, err := sql.Open("sqlite3", dsn)
// if err != nil {
// t.Fatal(err)
// }
// defer db.Close()
//
// // ...
// }
func TestDB(tb testing.TB, snapshot Snapshot, params ...url.Values) string {
tb.Helper()
name := fmt.Sprintf("%s_%s", tb.Name(), rand.Text())
tb.Cleanup(func() { Delete(name) })
Create(name, snapshot)
p := url.Values{"vfs": {"mvcc"}}
for _, v := range params {
for k, v := range v {
for _, v := range v {
p.Add(k, v)
}
}
}
return (&url.URL{
Scheme: "file",
OmitHost: true,
Path: "/" + name,
RawQuery: p.Encode(),
}).String()
}
func getName(dsn string) string {
u, err := url.Parse(dsn)
if err == nil &&
u.Scheme == "file" &&
strings.HasPrefix(u.Path, "/") &&
u.Query().Get("vfs") == "mvcc" {
return u.Path[1:]
}
return dsn
}

50
vfs/mvcc/example_test.go Normal file
View File

@@ -0,0 +1,50 @@
package mvcc_test
import (
"database/sql"
_ "embed"
"fmt"
"log"
_ "github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/vfs/mvcc"
)
//go:embed testdata/test.db
var testDB string
func Example() {
mvcc.Create("test.db", mvcc.NewSnapshot(testDB))
db, err := sql.Open("sqlite3", "file:/test.db?vfs=mvcc")
if err != nil {
log.Fatal(err)
}
defer db.Close()
_, err = db.Exec(`INSERT INTO users (id, name) VALUES (3, 'rust')`)
if err != nil {
log.Fatal(err)
}
rows, err := db.Query(`SELECT id, name FROM users`)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var id, name string
err = rows.Scan(&id, &name)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s %s\n", id, name)
}
// Output:
// 0 go
// 1 zig
// 2 whatever
// 3 rust
}

323
vfs/mvcc/mvcc.go Normal file
View File

@@ -0,0 +1,323 @@
package mvcc
import (
"io"
"strings"
"sync"
"time"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/util/vfsutil"
"github.com/ncruces/go-sqlite3/vfs"
"github.com/ncruces/wbt"
)
type mvccVFS struct{}
func (mvccVFS) Open(name string, flags vfs.OpenFlag) (vfs.File, vfs.OpenFlag, error) {
// Temporary files use SliceFile.
if name == "" || flags&vfs.OPEN_DELETEONCLOSE != 0 {
return &vfsutil.SliceFile{}, flags | vfs.OPEN_MEMORY, nil
}
// Only main databases benefit from multiversion concurrency control.
// Refuse to open all other file types.
// Returning OPEN_MEMORY means SQLite won't ask us to.
if flags&vfs.OPEN_MAIN_DB == 0 {
// notest // OPEN_MEMORY
return nil, flags, sqlite3.CANTOPEN
}
// A shared database has a name that begins with "/".
shared := strings.HasPrefix(name, "/")
var db *mvccDB
if shared {
name = name[1:]
memoryMtx.Lock()
defer memoryMtx.Unlock()
db = memoryDBs[name]
}
if db == nil {
if flags&vfs.OPEN_CREATE == 0 {
return nil, flags, sqlite3.CANTOPEN
}
db = &mvccDB{name: name}
}
if shared {
db.refs++ // +checklocksforce: memoryMtx is held
memoryDBs[name] = db
}
return &mvccFile{
mvccDB: db,
readOnly: flags&vfs.OPEN_READONLY != 0,
}, flags | vfs.OPEN_MEMORY, nil
}
func (mvccVFS) Delete(name string, dirSync bool) error {
return sqlite3.IOERR_DELETE_NOENT // used to delete journals
}
func (mvccVFS) Access(name string, flag vfs.AccessFlag) (bool, error) {
return false, nil // used to check for journals
}
func (mvccVFS) FullPathname(name string) (string, error) {
return name, nil
}
type mvccDB struct {
data *wbt.Tree[int64, string] // +checklocks:mtx
owner *mvccFile // +checklocks:mtx
waiter *sync.Cond // +checklocks:mtx
name string
refs int // +checklocks:memoryMtx
mtx sync.Mutex
}
func (m *mvccDB) release() {
memoryMtx.Lock()
defer memoryMtx.Unlock()
if m.refs--; m.refs == 0 && m == memoryDBs[m.name] {
delete(memoryDBs, m.name)
}
}
type mvccFile struct {
*mvccDB
data *wbt.Tree[int64, string]
lock vfs.LockLevel
readOnly bool
}
var (
// Ensure these interfaces are implemented:
_ vfs.FileLockState = &mvccFile{}
_ vfs.FileCommitPhaseTwo = &mvccFile{}
)
func (m *mvccFile) Close() error {
// Relase ownership, discard changes.
m.release()
m.data = nil
m.lock = vfs.LOCK_NONE
m.mtx.Lock()
defer m.mtx.Unlock()
if m.owner == m {
m.owner = nil
}
return nil
}
func (m *mvccFile) ReadAt(b []byte, off int64) (n int, err error) {
// If unlocked, use a snapshot of the database.
data := m.data
if m.lock == vfs.LOCK_NONE {
m.mtx.Lock()
defer m.mtx.Unlock()
data = m.mvccDB.data
}
for k, v := range data.AscendFloor(off) {
if i := k - off; i >= 0 {
if +i > int64(n) {
// Missing data.
clear(b[n:])
}
if +i < int64(len(b)) {
// Copy prefix.
n = copy(b[+i:], v) + int(i)
}
} else {
if -i < int64(len(v)) {
// Copy suffix.
n = copy(b, v[-i:])
}
}
if n >= len(b) {
return n, nil
}
}
return n, io.EOF
}
func (m *mvccFile) WriteAt(b []byte, off int64) (n int, err error) {
// If unlocked, take a snapshot of the database.
data := m.data
if m.lock == vfs.LOCK_NONE {
m.mtx.Lock()
defer m.mtx.Unlock()
data = m.mvccDB.data
m.lock = vfs.LOCK_EXCLUSIVE + 1 // UNKNOWN_LOCK
}
next := off + int64(len(b))
for k, v := range data.AscendFloor(off) {
if k >= next {
break
}
switch {
case k > off:
// Delete overlap.
data = data.Delete(k)
case k < off && off < k+int64(len(v)):
// Reinsert prefix.
data = data.Put(k, v[:off-k])
}
if k+int64(len(v)) > next {
// Reinsert suffix.
data = data.Put(next, v[next-k:])
}
}
m.data = data.Put(off, string(b))
return len(b), nil
}
func (m *mvccFile) Size() (int64, error) {
// If unlocked, use a snapshot of the database.
data := m.data
if m.lock == vfs.LOCK_NONE {
m.mtx.Lock()
defer m.mtx.Unlock()
data = m.mvccDB.data
}
if data == nil {
return 0, nil
}
data = data.Max()
return data.Key() + int64(len(data.Value())), nil
}
func (m *mvccFile) Truncate(size int64) error {
// If unlocked, take a snapshot of the database.
data := m.data
if m.lock == vfs.LOCK_NONE {
m.mtx.Lock()
defer m.mtx.Unlock()
data = m.mvccDB.data
m.lock = vfs.LOCK_EXCLUSIVE + 1 // UNKNOWN_LOCK
}
for data != nil && data.Key() >= size {
data = data.Left()
}
for k := range data.AscendCeil(size) {
data = data.Delete(k)
}
m.data = data
return nil
}
func (m *mvccFile) Lock(lock vfs.LockLevel) error {
if m.lock >= lock {
return nil
}
if m.readOnly && lock >= vfs.LOCK_RESERVED {
return sqlite3.IOERR_LOCK
}
m.mtx.Lock()
defer m.mtx.Unlock()
// Take a snapshot of the database.
if lock == vfs.LOCK_SHARED {
m.data = m.mvccDB.data
m.lock = lock
return nil
}
// We are the owners.
if m.owner == m {
m.lock = lock
return nil
}
// Someone else is the owner.
if m.owner != nil {
before := time.Now()
if m.waiter == nil {
m.waiter = sync.NewCond(&m.mtx)
}
defer time.AfterFunc(time.Millisecond, m.waiter.Broadcast).Stop()
for m.owner != nil {
// Our snapshot is invalid.
if m.data != m.mvccDB.data {
return sqlite3.BUSY_SNAPSHOT
}
if time.Since(before) > time.Millisecond {
return sqlite3.BUSY
}
m.waiter.Wait()
}
}
// Our snapshot is invalid.
if m.data != m.mvccDB.data {
return sqlite3.BUSY_SNAPSHOT
}
// Take ownership.
m.lock = lock
m.owner = m
return nil
}
func (m *mvccFile) Unlock(lock vfs.LockLevel) error {
if m.lock <= lock {
return nil
}
m.mtx.Lock()
defer m.mtx.Unlock()
// Relase ownership, commit changes.
if m.owner == m {
m.owner = nil
m.mvccDB.data = m.data
if m.waiter != nil {
m.waiter.Broadcast()
}
}
m.lock = lock
return nil
}
func (m *mvccFile) CheckReservedLock() (bool, error) {
// notest // OPEN_MEMORY
if m.lock >= vfs.LOCK_RESERVED {
return true, nil
}
m.mtx.Lock()
defer m.mtx.Unlock()
return m.owner != nil, nil
}
func (m *mvccFile) CommitPhaseTwo() error {
// Modified without lock, commit changes.
if m.lock > vfs.LOCK_EXCLUSIVE {
m.mtx.Lock()
defer m.mtx.Unlock()
m.mvccDB.data = m.data
}
return nil
}
func (m *mvccFile) LockState() vfs.LockLevel {
return m.lock
}
func (*mvccFile) Sync(flag vfs.SyncFlag) error { return nil }
func (*mvccFile) SectorSize() int {
// notest // safe default
return 0
}
func (*mvccFile) DeviceCharacteristics() vfs.DeviceCharacteristic {
return vfs.IOCAP_ATOMIC |
vfs.IOCAP_SEQUENTIAL |
vfs.IOCAP_SAFE_APPEND |
vfs.IOCAP_POWERSAFE_OVERWRITE |
vfs.IOCAP_SUBPAGE_READ
}

29
vfs/mvcc/mvcc_test.go Normal file
View File

@@ -0,0 +1,29 @@
package mvcc
import (
_ "embed"
"testing"
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/embed"
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
)
//go:embed testdata/wal.db
var walDB string
func Test_wal(t *testing.T) {
t.Parallel()
dsn := TestDB(t, NewSnapshot(walDB))
db, err := sqlite3.Open(dsn)
if err != nil {
t.Fatal(err)
}
defer db.Close()
err = db.Exec(`CREATE TABLE users (id INT, name VARCHAR(10))`)
if err != nil {
t.Fatal(err)
}
}

BIN
vfs/mvcc/testdata/test.db vendored Normal file

Binary file not shown.

BIN
vfs/mvcc/testdata/wal.db vendored Normal file

Binary file not shown.

View File

@@ -23,12 +23,26 @@ type flocktimeout_t struct {
timeout unix.Timespec
}
func osSync(file *os.File, fullsync, _ /*dataonly*/ bool) error {
if fullsync {
return file.Sync()
func osSync(file *os.File, open OpenFlag, sync SyncFlag) error {
var cmd int
if sync&SYNC_FULL == SYNC_FULL {
// For rollback journals all we really need is a barrier.
if open&OPEN_MAIN_JOURNAL != 0 {
cmd = unix.F_BARRIERFSYNC
} else {
cmd = unix.F_FULLFSYNC
}
}
fd := file.Fd()
for {
err := unix.Fsync(int(file.Fd()))
err := error(unix.ENOTSUP)
if cmd != 0 {
_, err = unix.FcntlInt(fd, cmd, 0)
}
if err == unix.ENOTSUP {
err = unix.Fsync(int(fd))
}
if err != unix.EINTR {
return err
}

Some files were not shown because too many files have changed in this diff Show More