Compare commits

..

44 Commits

Author SHA1 Message Date
Nuno Cruces
e6b2d2aef5 Bulk copies. 2025-11-24 13:42:12 +00:00
Nuno Cruces
0286e50e25 Experimental file control opcode for write transaction (#339) 2025-11-21 12:48:31 +00:00
dependabot[bot]
8ac10eb8b4 Bump actions/checkout from 5 to 6 (#338)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [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/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  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-11-20 22:48:29 +00:00
Nuno Cruces
0ff41bb966 Deps. 2025-11-20 18:47:59 +00:00
Nuno Cruces
ba9caf0405 Shared page cache. 2025-11-20 18:30:51 +00:00
Nuno Cruces
2c167dd116 Avoid polling intermediate levels. 2025-11-19 16:22:10 +00:00
Nuno Cruces
ce0da893b4 Fix #335. 2025-11-19 11:27:42 +00:00
Nuno Cruces
9bbbab77f6 Fix define. 2025-11-18 17:58:56 +00:00
Nuno Cruces
bab2d26652 Deps. 2025-11-11 23:03:31 +00:00
Nuno Cruces
3132b272de Test more, log less. 2025-11-10 11:26:28 +00:00
Nuno Cruces
8f9a6ca4c1 Litestream lightweight read-replicas. (#328) 2025-11-09 13:16:08 +00:00
Nuno Cruces
99b097de3b Windows ARM runners. 2025-11-09 12:44:32 +00:00
Nuno Cruces
4a956e80a2 wasi-sdk-28. 2025-11-09 01:32:25 +00:00
Nuno Cruces
5f4ff03f6f wazero v1.10.0. 2025-11-09 01:32:15 +00:00
Nuno Cruces
5890049488 Shim modernc. 2025-11-09 01:32:14 +00:00
Nuno Cruces
5e73c5d714 Issue #330. 2025-11-06 12:07:37 +00:00
Nuno Cruces
6d92aa16ef SQLite 3.51.0. 2025-11-05 12:30:09 +00:00
Nuno Cruces
191d1337e7 Gorm v1.31.1. 2025-11-05 12:30:09 +00:00
Nuno Cruces
b65e894849 Improve error reporting. (#327) 2025-10-17 16:40:15 +01:00
Nuno Cruces
0b040d3f09 Prepare 3.51.0. 2025-10-16 15:18:44 +01:00
Nuno Cruces
1db4366226 VFS error handling. 2025-10-16 15:18:40 +01:00
Nuno Cruces
9e1cbfb5bb Release snapshot. 2025-10-10 17:37:44 +01:00
dependabot[bot]
7f2d70a0f3 Bump golang.org/x/crypto from 0.42.0 to 0.43.0 (#324)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.42.0 to 0.43.0.
- [Commits](https://github.com/golang/crypto/compare/v0.42.0...v0.43.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.43.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-10-08 23:11:06 +01:00
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
133 changed files with 2762 additions and 1385 deletions

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
echo 'set -eu' > test.sh
echo 'set -eux' > test.sh
for p in $(go list ./...); do
dir=".${p#github.com/ncruces/go-sqlite3}"

View File

@@ -10,12 +10,12 @@ jobs:
test:
strategy:
matrix:
os: [ubuntu-24.04, ubuntu-24.04-arm, macos-13, macos-15]
os: [ubuntu-24.04, ubuntu-24.04-arm, macos-15, macos-15-intel]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v5
- uses: actions/checkout@v6
- 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@v5
- uses: actions/checkout@v6
- 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@v5
- uses: actions/setup-go@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with: { go-version: stable }
- name: Format
@@ -51,17 +51,17 @@ jobs:
run: go vet ./...
- name: Build
run: go build -v ./...
run: go build ./...
- name: Test
run: go test -v ./... -bench . -benchtime=1x
run: go test ./... -bench . -benchtime=1x
- name: Test BSD locks
run: go test -v -tags sqlite3_flock ./...
run: go test -tags sqlite3_flock ./...
if: matrix.os != 'windows-latest'
- name: Test dot locks
run: go test -v -tags sqlite3_dotlk ./...
run: go test -tags sqlite3_dotlk ./...
if: matrix.os != 'windows-latest'
- name: Test modules
@@ -69,12 +69,12 @@ jobs:
run: |
go work init .
go work use -r embed gormlite
go test -v ./embed/bcw2/...
go test ./embed/bcw2/...
- name: Test GORM
shell: bash
run: gormlite/test.sh
if: matrix.os != 'windows-latest'
if: matrix.os == 'ubuntu-latest'
- name: Collect coverage
run: |
@@ -99,36 +99,34 @@ jobs:
os:
- name: freebsd
version: '14.3'
flags: '-test.v'
- name: netbsd
version: '10.1'
flags: '-test.v'
- name: freebsd
arch: arm64
version: '14.3'
flags: '-test.v -test.short'
tflags: '-test.short'
- name: netbsd
arch: arm64
version: '10.1'
flags: '-test.v -test.short'
tflags: '-test.short'
- name: openbsd
version: '7.7'
flags: '-test.v -test.short'
version: '7.8'
tflags: '-test.short'
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Build
env:
GOOS: ${{ matrix.os.name }}
GOARCH: ${{ matrix.os.arch }}
TESTFLAGS: ${{ matrix.os.flags }}
TESTFLAGS: ${{ matrix.os.tflags }}
run: .github/workflows/build-test.sh
- name: Test
uses: cross-platform-actions/action@v0.29.0
uses: cross-platform-actions/action@v0.30.0
with:
operating_system: ${{ matrix.os.name }}
architecture: ${{ matrix.os.arch }}
@@ -143,19 +141,16 @@ jobs:
os:
- name: dragonfly
action: 'vmactions/dragonflybsd-vm@v1'
tflags: '-test.v'
- name: illumos
action: 'vmactions/omnios-vm@v1'
tflags: '-test.v'
- name: solaris
action: 'vmactions/solaris-vm@v1'
bflags: '-tags sqlite3_dotlk'
tflags: '-test.v'
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Build
env:
@@ -174,8 +169,8 @@ jobs:
steps:
- uses: bytecodealliance/actions/wasmtime/setup@v1
- uses: actions/checkout@v5
- uses: actions/setup-go@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with: { go-version: stable }
- name: Set path
@@ -187,7 +182,7 @@ jobs:
GOARCH: wasm
GOWASIRUNTIME: wasmtime
GOWASIRUNTIMEARGS: '--env CI=true'
run: go test -v -short -tags sqlite3_dotlk -skip Example ./...
run: go test -short -tags sqlite3_dotlk -skip Example ./...
test-qemu:
runs-on: ubuntu-latest
@@ -195,42 +190,60 @@ jobs:
steps:
- uses: docker/setup-qemu-action@v3
- uses: actions/checkout@v5
- uses: actions/setup-go@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with: { go-version: stable }
- name: Test 386 (32-bit)
run: GOARCH=386 go test -v -short ./...
run: GOARCH=386 go test -short ./...
- name: Test riscv64 (interpreter)
run: GOARCH=riscv64 go test -v -short ./...
run: GOARCH=riscv64 go test -short ./...
- name: Test ppc64le (interpreter)
run: GOARCH=ppc64le go test -v -short ./...
run: GOARCH=ppc64le go test -short ./...
- name: Test loong64 (interpreter)
run: GOARCH=loong64 go test -short ./...
- name: Test s390x (big-endian)
run: GOARCH=s390x go test -v -short -tags sqlite3_dotlk ./...
run: GOARCH=s390x go test -short -tags sqlite3_dotlk ./...
test-linuxarm:
runs-on: ubuntu-24.04-arm
needs: test
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with: { go-version: stable }
- name: Test
run: go test -v ./...
run: go test ./...
- name: Test arm (32-bit)
run: GOARCH=arm GOARM=7 go test -short ./...
test-macintel:
runs-on: macos-13
runs-on: macos-15-intel
needs: test
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with: { go-version: stable }
- name: Test
run: go test -v ./...
run: go test ./...
test-winarm:
runs-on: windows-11-arm
needs: test
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with: { go-version: stable }
- name: Test
run: go test ./...

View File

@@ -84,14 +84,14 @@ It also benefits greatly from [SQLite's](https://sqlite.org/testing.html) and
thorough testing.
Every commit is tested on:
* Linux: amd64, arm64, 386, riscv64, ppc64le, s390x
* Linux: amd64, arm64, 386, arm, riscv64, ppc64le, loong64, s390x
* macOS: amd64, arm64
* Windows: amd64
* Windows: amd64, arm64
* BSD:
* FreeBSD: amd64, arm64
* OpenBSD: amd64
* NetBSD: amd64, arm64
* DragonFly BSD: amd64
* OpenBSD: amd64
* illumos: amd64
* Solaris: amd64

View File

@@ -265,7 +265,7 @@ func traceCallback(ctx context.Context, mod api.Module, evt TraceEvent, pDB, pAr
}
}
if arg1 != nil {
_, rc = errorCode(c.trace(evt, arg1, arg2), ERROR)
_ = c.trace(evt, arg1, arg2)
}
}
return rc

39
conn.go
View File

@@ -420,21 +420,21 @@ func busyCallback(ctx context.Context, mod api.Module, pDB ptr_t, count int32) (
// Status retrieves runtime status information about a database connection.
//
// https://sqlite.org/c3ref/db_status.html
func (c *Conn) Status(op DBStatus, reset bool) (current, highwater int, err error) {
func (c *Conn) Status(op DBStatus, reset bool) (current, highwater int64, err error) {
defer c.arena.mark()()
hiPtr := c.arena.new(intlen)
curPtr := c.arena.new(intlen)
hiPtr := c.arena.new(8)
curPtr := c.arena.new(8)
var i int32
if reset {
i = 1
}
rc := res_t(c.call("sqlite3_db_status", stk_t(c.handle),
rc := res_t(c.call("sqlite3_db_status64", stk_t(c.handle),
stk_t(op), stk_t(curPtr), stk_t(hiPtr), stk_t(i)))
if err = c.error(rc); err == nil {
current = int(util.Read32[int32](c.mod, curPtr))
highwater = int(util.Read32[int32](c.mod, hiPtr))
current = util.Read64[int64](c.mod, curPtr)
highwater = util.Read64[int64](c.mod, hiPtr)
}
return
}
@@ -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

@@ -73,6 +73,9 @@ const (
ERROR_MISSING_COLLSEQ ExtendedErrorCode = xErrorCode(ERROR) | (1 << 8)
ERROR_RETRY ExtendedErrorCode = xErrorCode(ERROR) | (2 << 8)
ERROR_SNAPSHOT ExtendedErrorCode = xErrorCode(ERROR) | (3 << 8)
ERROR_RESERVESIZE ExtendedErrorCode = xErrorCode(ERROR) | (4 << 8)
ERROR_KEY ExtendedErrorCode = xErrorCode(ERROR) | (5 << 8)
ERROR_UNABLE ExtendedErrorCode = xErrorCode(ERROR) | (6 << 8)
IOERR_READ ExtendedErrorCode = xErrorCode(IOERR) | (1 << 8)
IOERR_SHORT_READ ExtendedErrorCode = xErrorCode(IOERR) | (2 << 8)
IOERR_WRITE ExtendedErrorCode = xErrorCode(IOERR) | (3 << 8)
@@ -107,6 +110,8 @@ const (
IOERR_DATA ExtendedErrorCode = xErrorCode(IOERR) | (32 << 8)
IOERR_CORRUPTFS ExtendedErrorCode = xErrorCode(IOERR) | (33 << 8)
IOERR_IN_PAGE ExtendedErrorCode = xErrorCode(IOERR) | (34 << 8)
IOERR_BADKEY ExtendedErrorCode = xErrorCode(IOERR) | (35 << 8)
IOERR_CODEC ExtendedErrorCode = xErrorCode(IOERR) | (36 << 8)
LOCKED_SHAREDCACHE ExtendedErrorCode = xErrorCode(LOCKED) | (1 << 8)
LOCKED_VTAB ExtendedErrorCode = xErrorCode(LOCKED) | (2 << 8)
BUSY_RECOVERY ExtendedErrorCode = xErrorCode(BUSY) | (1 << 8)
@@ -229,7 +234,8 @@ const (
DBSTATUS_DEFERRED_FKS DBStatus = 10
DBSTATUS_CACHE_USED_SHARED DBStatus = 11
DBSTATUS_CACHE_SPILL DBStatus = 12
// DBSTATUS_MAX DBStatus = 12
DBSTATUS_TEMPBUF_SPILL DBStatus = 13
// DBSTATUS_MAX DBStatus = 13
)
// DBConfig are the available database connection configuration options.
@@ -362,13 +368,14 @@ const (
// CheckpointMode are all the checkpoint mode values.
//
// https://sqlite.org/c3ref/c_checkpoint_full.html
type CheckpointMode uint32
type CheckpointMode int32
const (
CHECKPOINT_PASSIVE CheckpointMode = 0 /* Do as much as possible w/o blocking */
CHECKPOINT_FULL CheckpointMode = 1 /* Wait for writers, then checkpoint */
CHECKPOINT_RESTART CheckpointMode = 2 /* Like FULL but wait for readers */
CHECKPOINT_TRUNCATE CheckpointMode = 3 /* Like RESTART but also truncate WAL */
CHECKPOINT_NOOP CheckpointMode = -1 /* Do no work at all */
CHECKPOINT_PASSIVE CheckpointMode = 0 /* Do as much as possible w/o blocking */
CHECKPOINT_FULL CheckpointMode = 1 /* Wait for writers, then checkpoint */
CHECKPOINT_RESTART CheckpointMode = 2 /* Like FULL but wait for readers */
CHECKPOINT_TRUNCATE CheckpointMode = 3 /* Like RESTART but also truncate WAL */
)
// TxnState are the allowed return values from [Conn.TxnState].

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
@@ -214,14 +198,14 @@ func (ctx Context) ResultError(err error) {
return
}
msg, code := errorCode(err, _OK)
msg, code := errorCode(err, ERROR)
if msg != "" {
defer ctx.c.arena.mark()()
ptr := ctx.c.arena.string(msg)
ctx.c.call("sqlite3_result_error",
stk_t(ctx.handle), stk_t(ptr), stk_t(len(msg)))
}
if code != _OK {
if code != res_t(ERROR) {
ctx.c.call("sqlite3_result_error_code",
stk_t(ctx.handle), stk_t(code))
}

View File

@@ -263,10 +263,8 @@ func (n *connector) Connect(ctx context.Context) (ret driver.Conn, err error) {
return nil, err
}
defer s.Close()
if s.Step() && s.ColumnBool(0) {
c.readOnly = '1'
} else {
c.readOnly = '0'
if s.Step() {
c.readOnly = s.ColumnBool(0)
}
err = s.Close()
if err != nil {
@@ -322,7 +320,7 @@ type conn struct {
txReset string
tmRead sqlite3.TimeFormat
tmWrite sqlite3.TimeFormat
readOnly byte
readOnly bool
}
var (
@@ -358,9 +356,9 @@ func (c *conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, e
c.txReset = ``
txBegin := `BEGIN ` + txLock
if opts.ReadOnly {
if opts.ReadOnly && !c.readOnly {
txBegin += ` ; PRAGMA query_only=on`
c.txReset = `; PRAGMA query_only=` + string(c.readOnly)
c.txReset = `; PRAGMA query_only=off`
}
if old := c.Conn.SetInterrupt(ctx); old != ctx {
@@ -442,6 +440,22 @@ func (c *conn) CheckNamedValue(arg *driver.NamedValue) error {
return nil
}
// Deprecated: for Litestream use only; may be removed at any time.
func (c *conn) FileControlPersistWAL(schema string, mode int) (int, error) {
// notest
arg := make([]any, 1)
if mode >= 0 {
arg[0] = mode > 0
} else {
arg = arg[:0]
}
res, err := c.Conn.FileControl(schema, sqlite3.FCNTL_PERSIST_WAL, arg...)
if res == true {
return 1, err
}
return 0, err
}
type stmt struct {
*sqlite3.Stmt
tmWrite sqlite3.TimeFormat
@@ -607,14 +621,24 @@ func (r resultRowsAffected) RowsAffected() (int64, error) {
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 {
@@ -644,8 +668,8 @@ type rows struct {
*stmt
names []string
types []string
nulls []bool
scans []scantype
dest []driver.Value
}
var (
@@ -675,34 +699,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
}
@@ -721,15 +747,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.
@@ -772,6 +796,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)
@@ -790,18 +815,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
}
@@ -816,38 +830,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) error {
func (r *rows) ScanColumn(dest any, index int) (err error) {
// notest // Go 1.26
var ptr *time.Time
var tm *time.Time
var ok *bool
switch d := dest.(type) {
case *time.Time:
ptr = d
tm = d
case *sql.NullTime:
ptr = &d.Time
tm = &d.Time
ok = &d.Valid
case *sql.Null[time.Time]:
ptr = &d.V
tm = &d.V
ok = &d.Valid
default:
return driver.ErrSkip
}
if t := r.Stmt.ColumnTime(index, r.tmRead); !t.IsZero() {
*ptr = t
return nil
value := r.dest[index]
*tm, err = r.tmRead.Decode(value)
if ok != nil {
*ok = err == nil
if value == nil {
return nil
}
}
return driver.ErrSkip
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)
}
@@ -43,18 +44,18 @@ func Test_Open_dir(t *testing.T) {
if err == nil {
t.Fatal("want error")
}
if !errors.Is(err, sqlite3.CANTOPEN) {
t.Errorf("got %v, want sqlite3.CANTOPEN", err)
if !errors.Is(err, sqlite3.CANTOPEN_ISDIR) {
t.Errorf("got %v, want sqlite3.CANTOPEN_ISDIR", err)
}
}
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)
}

Binary file not shown.

View File

@@ -53,7 +53,7 @@ func Test_bcw2(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if version != "3.51.0" {
if version != "3.52.0" {
t.Error(version)
}
}

View File

@@ -15,9 +15,9 @@ cp "$ROOT"/sqlite3/*.[ch] build/
cp "$ROOT"/sqlite3/*.patch build/
cd sqlite/
# 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
# https://sqlite.org/src/info/352b363a5d727047
curl -#L https://github.com/sqlite/sqlite/archive/dbd613c.tar.gz | tar xz --strip-components=1
# curl -#L https://sqlite.org/src/tarball/sqlite.tar.gz?r=352b363a5d | tar xz --strip-components=1
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"
@@ -58,12 +58,15 @@ cd ~-
-Wl,--initial-memory=327680 \
-D_HAVE_SQLITE_CONFIG_H \
-DSQLITE_ENABLE_UPDATE_DELETE_LIMIT \
-DSQLITE_ENABLE_ORDERED_SET_AGGREGATES \
-DSQLITE_EXPERIMENTAL_PRAGMA_20251114 \
-DSQLITE_CUSTOM_INCLUDE=sqlite_opt.h \
$(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.27.1
require github.com/ncruces/go-sqlite3 v0.30.1
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.35.0 // indirect
github.com/ncruces/sort v0.1.6 // indirect
github.com/tetratelabs/wazero v1.10.1 // indirect
golang.org/x/sys v0.38.0 // indirect
)

View File

@@ -1,12 +1,12 @@
github.com/ncruces/go-sqlite3 v0.27.1 h1:suqlM7xhSyDVMV9RgX99MCPqt9mB6YOCzHZuiI36K34=
github.com/ncruces/go-sqlite3 v0.27.1/go.mod h1:gpF5s+92aw2MbDmZK0ZOnCdFlpe11BH20CTspVqri0c=
github.com/ncruces/go-sqlite3 v0.30.1 h1:pHC3YsyRdJv4pCMB4MO1Q2BXw/CAa+Hoj7GSaKtVk+g=
github.com/ncruces/go-sqlite3 v0.30.1/go.mod h1:UVsWrQaq1qkcal5/vT5lOJnZCVlR5rsThKdwidjFsKc=
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.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.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=
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/tetratelabs/wazero v1.10.1 h1:2DugeJf6VVk58KTPszlNfeeN8AhhpwcZqkJj2wwFuH8=
github.com/tetratelabs/wazero v1.10.1/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=

View File

@@ -27,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

@@ -59,7 +59,7 @@ sqlite3_db_filename
sqlite3_db_name
sqlite3_db_readonly
sqlite3_db_release_memory
sqlite3_db_status
sqlite3_db_status64
sqlite3_declare_vtab
sqlite3_errcode
sqlite3_errmsg

View File

@@ -17,5 +17,7 @@ import (
var binary string
func init() {
sqlite3.Binary = unsafe.Slice(unsafe.StringData(binary), len(binary))
if sqlite3.Binary == nil {
sqlite3.Binary = unsafe.Slice(unsafe.StringData(binary), len(binary))
}
}

View File

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

Binary file not shown.

View File

@@ -11,6 +11,7 @@ import (
//
// https://sqlite.org/c3ref/errcode.html
type Error struct {
sys error
msg string
sql string
code res_t
@@ -33,16 +34,28 @@ func (e *Error) ExtendedCode() ExtendedErrorCode {
// Error implements the error interface.
func (e *Error) Error() string {
var b strings.Builder
b.WriteString(util.ErrorCodeString(uint32(e.code)))
b.WriteString(util.ErrorCodeString(e.code))
if e.msg != "" {
b.WriteString(": ")
b.WriteString(e.msg)
}
if e.sys != nil {
b.WriteString(": ")
b.WriteString(e.sys.Error())
}
return b.String()
}
// Unwrap returns the underlying operating system error
// that caused the I/O error or failure to open a file.
//
// https://sqlite.org/c3ref/system_errno.html
func (e *Error) Unwrap() error {
return e.sys
}
// Is tests whether this error matches a given [ErrorCode] or [ExtendedErrorCode].
//
// It makes it possible to do:
@@ -90,7 +103,16 @@ func (e *Error) SQL() string {
// Error implements the error interface.
func (e ErrorCode) Error() string {
return util.ErrorCodeString(uint32(e))
return util.ErrorCodeString(e)
}
// As converts this error to an [ExtendedErrorCode].
func (e ErrorCode) As(err any) bool {
c, ok := err.(*xErrorCode)
if ok {
*c = xErrorCode(e)
}
return ok
}
// Temporary returns true for [BUSY] errors.
@@ -105,7 +127,7 @@ func (e ErrorCode) ExtendedCode() ExtendedErrorCode {
// Error implements the error interface.
func (e ExtendedErrorCode) Error() string {
return util.ErrorCodeString(uint32(e))
return util.ErrorCodeString(e)
}
// Is tests whether this error matches a given [ErrorCode].
@@ -150,14 +172,10 @@ func errorCode(err error, def ErrorCode) (msg string, code res_t) {
return code.msg, res_t(code.code)
}
var ecode ErrorCode
var xcode xErrorCode
switch {
case errors.As(err, &xcode):
if errors.As(err, &xcode) {
code = res_t(xcode)
case errors.As(err, &ecode):
code = res_t(ecode)
default:
} else {
code = res_t(def)
}
return err.Error(), code

View File

@@ -59,7 +59,8 @@ func (c *cursor) Next() error {
}
func (c *cursor) RowID() (int64, error) {
return int64(c.rowID), nil
// One-based RowID for consistency with carray and other tables.
return int64(c.rowID) + 1, nil
}
func (c *cursor) Column(ctx sqlite3.Context, n int) error {

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

@@ -154,6 +154,7 @@ func (c *closure) BestIndex(idx *sqlite3.IndexInfo) error {
return sqlite3.CONSTRAINT
}
idx.IdxFlags = sqlite3.INDEX_SCAN_HEX
idx.EstimatedCost = cost
idx.IdxNum = plan
return nil

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

@@ -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

@@ -14,9 +14,9 @@ import (
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)
}
@@ -59,9 +59,9 @@ func Test_zorder(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)
}
@@ -87,9 +87,9 @@ func Test_unzorder(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)
}
@@ -116,9 +116,9 @@ func Test_zorder_error(t *testing.T) {
func Test_unzorder_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)
}

18
go.mod
View File

@@ -1,24 +1,22 @@
module github.com/ncruces/go-sqlite3
go 1.23.0
toolchain go1.24.0
go 1.24.0
require (
github.com/ncruces/aa v0.3.0
github.com/ncruces/julianday v1.0.0
github.com/ncruces/sort v0.1.5
github.com/tetratelabs/wazero v1.9.0
golang.org/x/crypto v0.41.0
golang.org/x/sys v0.35.0
github.com/ncruces/sort v0.1.6
github.com/ncruces/wbt v0.2.0
github.com/tetratelabs/wazero v1.10.1
golang.org/x/sys v0.38.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.28.0 // ext/unicode
golang.org/x/crypto v0.45.0 // vfs/adiantum vfs/xts
golang.org/x/sync v0.18.0 // test
golang.org/x/text v0.31.0 // ext/unicode
lukechampine.com/adiantum v1.1.1 // vfs/adiantum
)

28
go.sum
View File

@@ -2,23 +2,23 @@ github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA=
github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc=
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/aa v0.3.0 h1:6NPcK3jsyPWRZBZWCyF7c4IzEQX4eJtkKJBA+IRKTkQ=
github.com/ncruces/aa v0.3.0/go.mod h1:ctOw1LVqfuqzqg2S9LlR045bLAiXtaTiPMCL3zzl7Ik=
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.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
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.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
github.com/tetratelabs/wazero v1.10.1 h1:2DugeJf6VVk58KTPszlNfeeN8AhhpwcZqkJj2wwFuH8=
github.com/tetratelabs/wazero v1.10.1/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
lukechampine.com/adiantum v1.1.1 h1:4fp6gTxWCqpEbLy40ExiYDDED3oUNWx5cTqBCtPdZqA=
lukechampine.com/adiantum v1.1.1/go.mod h1:LrAYVnTYLnUtE/yMp5bQr0HstAf060YUF8nM0B6+rUw=

View File

@@ -1,19 +1,17 @@
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.27.1
gorm.io/gorm v1.30.1
github.com/ncruces/go-sqlite3 v0.30.1
gorm.io/gorm v1.31.1
)
require (
github.com/jinzhu/inflection v1.0.0 // indirect
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.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
github.com/tetratelabs/wazero v1.10.1 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.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.27.1 h1:suqlM7xhSyDVMV9RgX99MCPqt9mB6YOCzHZuiI36K34=
github.com/ncruces/go-sqlite3 v0.27.1/go.mod h1:gpF5s+92aw2MbDmZK0ZOnCdFlpe11BH20CTspVqri0c=
github.com/ncruces/go-sqlite3 v0.30.1 h1:pHC3YsyRdJv4pCMB4MO1Q2BXw/CAa+Hoj7GSaKtVk+g=
github.com/ncruces/go-sqlite3 v0.30.1/go.mod h1:UVsWrQaq1qkcal5/vT5lOJnZCVlR5rsThKdwidjFsKc=
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.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4=
gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
github.com/tetratelabs/wazero v1.10.1 h1:2DugeJf6VVk58KTPszlNfeeN8AhhpwcZqkJj2wwFuH8=
github.com/tetratelabs/wazero v1.10.1/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=

View File

@@ -339,7 +339,7 @@ func (m _Migrator) GetIndexes(value interface{}) ([]gorm.Index, error) {
indexes := make([]gorm.Index, 0)
err := m.RunWithValue(value, func(stmt *gorm.Statement) error {
rst := make([]*_Index, 0)
if err := m.DB.Debug().Raw("SELECT * FROM PRAGMA_index_list(?)", stmt.Table).Scan(&rst).Error; err != nil { // alias `PRAGMA index_list(?)`
if err := m.DB.Raw("SELECT * FROM PRAGMA_index_list(?)", stmt.Table).Scan(&rst).Error; err != nil { // alias `PRAGMA index_list(?)`
return err
}
for _, index := range rst {

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.1 --filter=blob:none https://github.com/go-gorm/gorm.git
git clone --branch v1.31.1 --filter=blob:none https://github.com/go-gorm/gorm.git
mv gorm/tests tests
rm -rf gorm/

View File

@@ -5,28 +5,36 @@ package alloc
import (
"math"
"github.com/tetratelabs/wazero/experimental"
"golang.org/x/sys/unix"
"github.com/tetratelabs/wazero/experimental"
)
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 +48,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 +60,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

@@ -5,24 +5,31 @@ import (
"reflect"
"unsafe"
"github.com/tetratelabs/wazero/experimental"
"golang.org/x/sys/windows"
"github.com/tetratelabs/wazero/experimental"
)
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 +37,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 +55,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 +67,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

@@ -39,6 +39,9 @@ const (
ERROR_MISSING_COLLSEQ = ERROR | (1 << 8)
ERROR_RETRY = ERROR | (2 << 8)
ERROR_SNAPSHOT = ERROR | (3 << 8)
ERROR_RESERVESIZE = ERROR | (4 << 8)
ERROR_KEY = ERROR | (5 << 8)
ERROR_UNABLE = ERROR | (6 << 8)
IOERR_READ = IOERR | (1 << 8)
IOERR_SHORT_READ = IOERR | (2 << 8)
IOERR_WRITE = IOERR | (3 << 8)
@@ -73,6 +76,8 @@ const (
IOERR_DATA = IOERR | (32 << 8)
IOERR_CORRUPTFS = IOERR | (33 << 8)
IOERR_IN_PAGE = IOERR | (34 << 8)
IOERR_BADKEY = IOERR | (35 << 8)
IOERR_CODEC = IOERR | (36 << 8)
LOCKED_SHAREDCACHE = LOCKED | (1 << 8)
LOCKED_VTAB = LOCKED | (2 << 8)
BUSY_RECOVERY = BUSY | (1 << 8)

View File

@@ -33,8 +33,12 @@ func AssertErr() ErrorString {
return ErrorString(msg)
}
func ErrorCodeString(rc uint32) string {
switch rc {
type errorCode interface {
~uint8 | ~uint16 | ~uint32 | ~int32
}
func ErrorCodeString[T errorCode](rc T) string {
switch uint32(rc) {
case ABORT_ROLLBACK:
return "sqlite3: abort due to ROLLBACK"
case ROW:
@@ -42,7 +46,7 @@ func ErrorCodeString(rc uint32) string {
case DONE:
return "sqlite3: no more rows available"
}
switch rc & 0xff {
switch uint8(rc) {
case OK:
return "sqlite3: not an error"
case ERROR:

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
}

View File

@@ -7,8 +7,9 @@ import (
"os"
"unsafe"
"github.com/tetratelabs/wazero/api"
"golang.org/x/sys/unix"
"github.com/tetratelabs/wazero/api"
)
type mmapState struct {

View File

@@ -1,12 +1,10 @@
package util
import (
"context"
"os"
"reflect"
"unsafe"
"github.com/tetratelabs/wazero/api"
"golang.org/x/sys/windows"
)
@@ -16,14 +14,21 @@ type MappedRegion struct {
addr uintptr
}
func MapRegion(ctx context.Context, mod api.Module, f *os.File, offset int64, size int32) (*MappedRegion, error) {
h, err := windows.CreateFileMapping(windows.Handle(f.Fd()), nil, windows.PAGE_READWRITE, 0, 0, nil)
func MapRegion(f *os.File, offset int64, size int32) (*MappedRegion, error) {
maxSize := offset + int64(size)
h, err := windows.CreateFileMapping(
windows.Handle(f.Fd()), nil, windows.PAGE_READWRITE,
uint32(maxSize>>32), uint32(maxSize), nil)
if h == 0 {
return nil, err
}
const allocationGranularity = 64 * 1024
align := offset % allocationGranularity
offset -= align
a, err := windows.MapViewOfFile(h, windows.FILE_MAP_WRITE,
uint32(offset>>32), uint32(offset), uintptr(size))
uint32(offset>>32), uint32(offset), uintptr(size)+uintptr(align))
if a == 0 {
windows.CloseHandle(h)
return nil, err
@@ -32,9 +37,9 @@ func MapRegion(ctx context.Context, mod api.Module, f *os.File, offset int64, si
ret := &MappedRegion{Handle: h, addr: a}
// SliceHeader, although deprecated, avoids a go vet warning.
sh := (*reflect.SliceHeader)(unsafe.Pointer(&ret.Data))
sh.Data = a + uintptr(align)
sh.Len = int(size)
sh.Cap = int(size)
sh.Data = a
return ret, nil
}

View File

@@ -12,6 +12,7 @@ type ConnKey struct{}
type moduleKey struct{}
type moduleState struct {
sysError error
mmapState
handleState
}
@@ -23,3 +24,20 @@ func NewContext(ctx context.Context) context.Context {
ctx = context.WithValue(ctx, moduleKey{}, state)
return ctx
}
func GetSystemError(ctx context.Context) error {
// Test needed to simplify testing.
s, ok := ctx.Value(moduleKey{}).(*moduleState)
if ok {
return s.sysError
}
return nil
}
func SetSystemError(ctx context.Context, err error) {
// Test needed to simplify testing.
s, ok := ctx.Value(moduleKey{}).(*moduleState)
if ok {
s.sysError = err
}
}

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
}

11
litestream/README.md Normal file
View File

@@ -0,0 +1,11 @@
# Litestream lightweight read-replicas
This package implements the **EXPERIMENTAL** `"litestream"` SQLite VFS
that offers Litestream [lightweight read-replicas](https://fly.io/blog/litestream-revamped/#lightweight-read-replicas).
See the [example](vfs_test.go) for how to use.
To improve performance,
increase `PollInterval` (and `MinLevel`) as much as you can,
and set [`PRAGMA cache_size=N`](https://www.sqlite.org/pragma.html#pragma_cache_size)
(or use `_pragma=cache_size(N)`).

78
litestream/api.go Normal file
View File

@@ -0,0 +1,78 @@
// Package litestream implements a Litestream lightweight read-replica VFS.
package litestream
import (
"log/slog"
"sync"
"time"
"github.com/benbjohnson/litestream"
"github.com/ncruces/go-sqlite3/vfs"
)
const (
// The default poll interval.
DefaultPollInterval = 1 * time.Second
// The default cache size: 10 MiB.
DefaultCacheSize = 10 * 1024 * 1024
)
func init() {
vfs.Register("litestream", liteVFS{})
}
var (
liteMtx sync.RWMutex
// +checklocks:liteMtx
liteDBs = map[string]*liteDB{}
)
// ReplicaOptions represents options for [NewReplica].
type ReplicaOptions struct {
// Where to log error messages. May be nil.
Logger *slog.Logger
// Replica poll interval.
// Should be less than the compaction interval
// used by the replica at MinLevel+1.
PollInterval time.Duration
// Minimum compaction level to track.
MinLevel int
// CacheSize is the maximum size of the page cache in bytes.
// Zero means DefaultCacheSize, negative disables caching.
CacheSize int
}
// NewReplica creates a read-replica from a Litestream client.
func NewReplica(name string, client litestream.ReplicaClient, options ReplicaOptions) {
if options.Logger != nil {
options.Logger = options.Logger.With("name", name)
} else {
options.Logger = slog.New(slog.DiscardHandler)
}
if options.PollInterval <= 0 {
options.PollInterval = DefaultPollInterval
}
if options.CacheSize == 0 {
options.CacheSize = DefaultCacheSize
}
liteMtx.Lock()
defer liteMtx.Unlock()
liteDBs[name] = &liteDB{
client: client,
opts: options,
cache: pageCache{size: options.CacheSize},
}
}
// RemoveReplica removes a replica by name.
func RemoveReplica(name string) {
liteMtx.Lock()
defer liteMtx.Unlock()
delete(liteDBs, name)
}

72
litestream/cache.go Normal file
View File

@@ -0,0 +1,72 @@
package litestream
import (
"encoding/binary"
"sync"
"golang.org/x/sync/singleflight"
"github.com/superfly/ltx"
)
type pageCache struct {
single singleflight.Group
pages map[uint32]cachedPage // +checklocks:mtx
size int
mtx sync.Mutex
}
type cachedPage struct {
data []byte
txid ltx.TXID
}
func (c *pageCache) getOrFetch(pgno uint32, maxTXID ltx.TXID, fetch func() (any, error)) ([]byte, error) {
if c.size >= 0 {
c.mtx.Lock()
if c.pages == nil {
c.pages = map[uint32]cachedPage{}
}
page := c.pages[pgno]
c.mtx.Unlock()
if page.txid == maxTXID {
return page.data, nil
}
}
var key [12]byte
binary.LittleEndian.PutUint32(key[0:], pgno)
binary.LittleEndian.PutUint64(key[4:], uint64(maxTXID))
v, err, _ := c.single.Do(string(key[:]), fetch)
if err != nil {
return nil, err
}
page := cachedPage{v.([]byte), maxTXID}
if c.size >= 0 {
c.mtx.Lock()
c.evict(len(page.data))
c.pages[pgno] = page
c.mtx.Unlock()
}
return page.data, nil
}
// +checklocks:c.mtx
func (c *pageCache) evict(pageSize int) {
// Evict random keys until we're under the maximum size.
// SQLite has its own page cache, which it will use for each connection.
// Since this is a second layer of shared cache,
// random eviction is probably good enough.
if pageSize*len(c.pages) < c.size {
return
}
for key := range c.pages {
delete(c.pages, key)
if pageSize*len(c.pages) < c.size {
return
}
}
}

View File

@@ -0,0 +1,48 @@
package litestream_test
import (
"log"
"time"
"github.com/benbjohnson/litestream/s3"
"github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/litestream"
)
func ExampleNewReplica() {
client := s3.NewReplicaClient()
client.Bucket = "test-bucket"
client.Path = "fruits.db"
litestream.NewReplica("fruits.db", client, litestream.ReplicaOptions{
PollInterval: 5 * time.Second,
})
db, err := driver.Open("file:fruits.db?vfs=litestream")
if err != nil {
log.Fatalln(err)
}
defer db.Close()
for {
time.Sleep(time.Second)
rows, err := db.Query("SELECT * FROM fruits")
if err != nil {
log.Fatalln(err)
}
for rows.Next() {
var name, color string
err := rows.Scan(&name, &color)
if err != nil {
log.Fatalln(err)
}
log.Println(name, color)
}
log.Println("===")
rows.Close()
}
}

63
litestream/go.mod Normal file
View File

@@ -0,0 +1,63 @@
module github.com/ncruces/go-sqlite3/litestream
go 1.24.4
require (
github.com/benbjohnson/litestream v0.5.2
github.com/ncruces/go-sqlite3 v0.30.1
github.com/ncruces/wbt v0.2.0
github.com/superfly/ltx v0.5.0
)
// github.com/ncruces/go-sqlite3
require (
github.com/ncruces/julianday v1.0.0 // indirect
github.com/tetratelabs/wazero v1.10.1 // indirect
golang.org/x/sys v0.38.0 // indirect
)
// github.com/superfly/ltx
require github.com/pierrec/lz4/v4 v4.1.22 // indirect
// github.com/benbjohnson/litestream
require (
filippo.io/age v1.2.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.3 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/psanford/sqlite3vfs v0.0.0-20240315230605-24e1d98cf361 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.45.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
modernc.org/sqlite v1.40.1 // indirect
)
// github.com/benbjohnson/litestream/s3
require (
github.com/aws/aws-sdk-go-v2 v1.37.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0 // indirect
github.com/aws/aws-sdk-go-v2/config v1.30.2 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.18.2 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.1 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.18.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.1 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.85.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.26.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.31.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.35.1 // indirect
github.com/aws/smithy-go v1.22.5 // indirect
)
replace modernc.org/sqlite => github.com/ncruces/go-sqlite3/litestream/modernc v0.0.0-20251109124432-99b097de3b79

197
litestream/go.sum Normal file
View File

@@ -0,0 +1,197 @@
c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3IqwfuN5kgDfo5MLzpNM0=
c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w=
cloud.google.com/go v0.111.0 h1:YHLKNupSD1KqjDbQ3+LVdQ81h/UJbJyZG203cEfnQgM=
cloud.google.com/go v0.111.0/go.mod h1:0mibmpKP1TyOOFYQY5izo0LnT+ecvOQ0Sg3OdmMiNRU=
cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI=
cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8=
cloud.google.com/go/storage v1.36.0 h1:P0mOkAcaJxhCTvAkMhxMfrTKiNcub4YmmPBtlhAyTr8=
cloud.google.com/go/storage v1.36.0/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8=
filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o=
filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.2 h1:Hr5FTipp7SL07o2FvoVOX9HRiRH3CR3Mj8pxqCcdD5A=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.2/go.mod h1:QyVsSSN64v5TGltphKLQ2sQxe4OBQg0J1eKRcVBnfgE=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0 h1:MhRfI58HblXzCtWEZCO0feHs8LweePB3s90r7WaR1KU=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0/go.mod h1:okZ+ZURbArNdlJ+ptXoyHNuOETzOl1Oww19rm8I2WLA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2 h1:FwladfywkNirM+FZYLBR2kBz5C8Tg0fw5w5Y7meRXWI=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2/go.mod h1:vv5Ad0RrIoT1lJFdWBZwt4mB1+j+V8DUroixmKDTCdk=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/aws/aws-sdk-go-v2 v1.37.1 h1:SMUxeNz3Z6nqGsXv0JuJXc8w5YMtrQMuIBmDx//bBDY=
github.com/aws/aws-sdk-go-v2 v1.37.1/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0 h1:6GMWV6CNpA/6fbFHnoAjrv4+LGfyTqZz2LtCHnspgDg=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0/go.mod h1:/mXlTIVG9jbxkqDnr5UQNQxW1HRYxeGklkM9vAFeabg=
github.com/aws/aws-sdk-go-v2/config v1.30.2 h1:YE1BmSc4fFYqFgN1mN8uzrtc7R9x+7oSWeX8ckoltAw=
github.com/aws/aws-sdk-go-v2/config v1.30.2/go.mod h1:UNrLGZ6jfAVjgVJpkIxjLufRJqTXCVYOpkeVf83kwBo=
github.com/aws/aws-sdk-go-v2/credentials v1.18.2 h1:mfm0GKY/PHLhs7KO0sUaOtFnIQ15Qqxt+wXbO/5fIfs=
github.com/aws/aws-sdk-go-v2/credentials v1.18.2/go.mod h1:v0SdJX6ayPeZFQxgXUKw5RhLpAoZUuynxWDfh8+Eknc=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.1 h1:owmNBboeA0kHKDcdF8KiSXmrIuXZustfMGGytv6OMkM=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.1/go.mod h1:Bg1miN59SGxrZqlP8vJZSmXW+1N8Y1MjQDq1OfuNod8=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.18.2 h1:YFX4DvH1CPQXgQR8935b46Om+L7+6jus4aTdKqyDR84=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.18.2/go.mod h1:DgMPy7GqxcV0RSyaITnI3rw8HC3lIHB87U3KPQKDxHg=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.1 h1:ksZXBYv80EFTcgc8OJO48aQ8XDWXIQL7gGasPeCoTzI=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.1/go.mod h1:HSksQyyJETVZS7uM54cir0IgxttTD+8aEoJMPGepHBI=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.1 h1:+dn/xF/05utS7tUhjIcndbuaPjfll2LhbH1cCDGLYUQ=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.1/go.mod h1:hyAGz30LHdm5KBZDI58MXx5lDVZ5CUfvfTZvMu4HCZo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.1 h1:4HbnOGE9491a9zYJ9VpPh1ApgEq6ZlD4Kuv1PJenFpc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.1/go.mod h1:Z6QnHC6TmpJWUxAy8FI4JzA7rTwl6EIANkyK9OR5z5w=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 h1:6+lZi2JeGKtCraAj1rpoZfKqnQ9SptseRZioejfUOLM=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.1 h1:ps3nrmBWdWwakZBydGX1CxeYFK80HsQ79JLMwm7Y4/c=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.1/go.mod h1:bAdfrfxENre68Hh2swNaGEVuFYE74o0SaSCAlaG9E74=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.1 h1:ky79ysLMxhwk5rxJtS+ILd3Mc8kC5fhsLBrP27r6h4I=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.1/go.mod h1:+2MmkvFvPYM1vsozBWduoLJUi5maxFk5B7KJFECujhY=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.1 h1:MdVYlN5pcQu1t1OYx4Ajo3fKl1IEhzgdPQbYFCRjYS8=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.1/go.mod h1:iikmNLrvHm2p4a3/4BPeix2S9P+nW8yM1IZW73x8bFA=
github.com/aws/aws-sdk-go-v2/service/s3 v1.85.1 h1:Hsqo8+dFxSdDvv9B2PgIx1AJAnDpqgS0znVI+R+MoGY=
github.com/aws/aws-sdk-go-v2/service/s3 v1.85.1/go.mod h1:8Q0TAPXD68Z8YqlcIGHs/UNIDHsxErV9H4dl4vJEpgw=
github.com/aws/aws-sdk-go-v2/service/sso v1.26.1 h1:uWaz3DoNK9MNhm7i6UGxqufwu3BEuJZm72WlpGwyVtY=
github.com/aws/aws-sdk-go-v2/service/sso v1.26.1/go.mod h1:ILpVNjL0BO+Z3Mm0SbEeUoYS9e0eJWV1BxNppp0fcb8=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.31.1 h1:XdG6/o1/ZDmn3wJU5SRAejHaWgKS4zHv0jBamuKuS2k=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.31.1/go.mod h1:oiotGTKadCOCl3vg/tYh4k45JlDF81Ka8rdumNhEnIQ=
github.com/aws/aws-sdk-go-v2/service/sts v1.35.1 h1:iF4Xxkc0H9c/K2dS0zZw3SCkj0Z7n6AMnUiiyoJND+I=
github.com/aws/aws-sdk-go-v2/service/sts v1.35.1/go.mod h1:0bxIatfN0aLq4mjoLDeBpOjOke68OsFlXPDFJ7V0MYw=
github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw=
github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
github.com/benbjohnson/litestream v0.5.2 h1:uD9I17n6RgUgyCwPM/Sw2YXNmMGixecUB5kmJ4FL08o=
github.com/benbjohnson/litestream v0.5.2/go.mod h1:jSW6AGqbxmJnEXGjMHchlZclGphzbJ6jGrGo5fYIDhU=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
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/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.44.0 h1:ECKVrDLdh/kDPV1g0gAQ+2+m2KprqZK5O/eJAyAnH2M=
github.com/nats-io/nats.go v1.44.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/ncruces/go-sqlite3 v0.30.1 h1:pHC3YsyRdJv4pCMB4MO1Q2BXw/CAa+Hoj7GSaKtVk+g=
github.com/ncruces/go-sqlite3 v0.30.1/go.mod h1:UVsWrQaq1qkcal5/vT5lOJnZCVlR5rsThKdwidjFsKc=
github.com/ncruces/go-sqlite3/litestream/modernc v0.0.0-20251109124432-99b097de3b79 h1:evpQceUV2vRbOe84U/QhBBchfqFERRHTx1JOadFFMLE=
github.com/ncruces/go-sqlite3/litestream/modernc v0.0.0-20251109124432-99b097de3b79/go.mod h1:GSM2gXEOb9HIFFtsl0IUtnpvpDmVi7Kbp8z5GzwA0Tw=
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
github.com/ncruces/wbt v0.2.0 h1:Q9zlKOBSZc7Yy/R2cGa35g6RKUUE3BjNIW3tfGC4F04=
github.com/ncruces/wbt v0.2.0/go.mod h1:DtF92amvMxH69EmBFUSFWRDAlo6hOEfoNQnClxj9C/c=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q=
github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/psanford/sqlite3vfs v0.0.0-20240315230605-24e1d98cf361 h1:vAKifIJuYY306ZJSrwDgKonWcJGELijdaenABqbV03E=
github.com/psanford/sqlite3vfs v0.0.0-20240315230605-24e1d98cf361/go.mod h1:iW4cSew5PAb1sMZiTEkVJAIBNrepaB6jTYjeP47WtI0=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/superfly/ltx v0.5.0 h1:dXNrcT3ZtMb6iKZopIV7z5UBscnapg0b0F02loQsk5o=
github.com/superfly/ltx v0.5.0/go.mod h1:Nf50QAIXU/ET4ua3AuQ2fh31MbgNQZA7r/DYx6Os77s=
github.com/tetratelabs/wazero v1.10.1 h1:2DugeJf6VVk58KTPszlNfeeN8AhhpwcZqkJj2wwFuH8=
github.com/tetratelabs/wazero v1.10.1/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.154.0 h1:X7QkVKZBskztmpPKWQXgjJRPA2dJYrL6r+sYPRLj050=
google.golang.org/api v0.154.0/go.mod h1:qhSMkM85hgqiokIYsrRyKxrjfBeIhgl4Z2JmeRkYylc=
google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 h1:YJ5pD9rF8o9Qtta0Cmy9rdBwkSjrTCT6XTiUQVOtIos=
google.golang.org/genproto v0.0.0-20231212172506-995d672761c0/go.mod h1:l/k7rMz0vFTBPy+tFSGvXEd3z+BcoG1k7EHbqm+YBsY=
google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0 h1:s1w3X6gQxwrLEpxnLd/qXTVLgQE2yXwaOaoa6IlY/+o=
google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0/go.mod h1:CAny0tYF+0/9rmDB9fahA9YLzX3+AEVl1qXbv5hhj6c=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 h1:/jFB8jK5R3Sq3i/lmeZO0cATSzFfZaJq1J2Euan3XKU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0/go.mod h1:FUoWkonphQm3RhTS+kOEhF8h0iDpm4tdXolVCeZ9KKA=
google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

11
litestream/modernc/go.mod Normal file
View File

@@ -0,0 +1,11 @@
module modernc.org/sqlite
go 1.24.0
require github.com/ncruces/go-sqlite3 v0.30.1
require (
github.com/ncruces/julianday v1.0.0 // indirect
github.com/tetratelabs/wazero v1.10.1 // indirect
golang.org/x/sys v0.38.0 // indirect
)

10
litestream/modernc/go.sum Normal file
View File

@@ -0,0 +1,10 @@
github.com/ncruces/go-sqlite3 v0.30.1 h1:pHC3YsyRdJv4pCMB4MO1Q2BXw/CAa+Hoj7GSaKtVk+g=
github.com/ncruces/go-sqlite3 v0.30.1/go.mod h1:UVsWrQaq1qkcal5/vT5lOJnZCVlR5rsThKdwidjFsKc=
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.10.1 h1:2DugeJf6VVk58KTPszlNfeeN8AhhpwcZqkJj2wwFuH8=
github.com/tetratelabs/wazero v1.10.1/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=

View File

@@ -0,0 +1,20 @@
// Package sqlite provides a shim that allows Litestream to work with the ncruces SQLite driver.
package sqlite
import (
"database/sql"
"slices"
"github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
)
func init() {
if !slices.Contains(sql.Drivers(), "sqlite") {
sql.Register("sqlite", &driver.SQLite{})
}
}
type FileControl interface {
FileControlPersistWAL(string, int) (int, error)
}

309
litestream/vfs.go Normal file
View File

@@ -0,0 +1,309 @@
package litestream
import (
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"sync"
"time"
"github.com/benbjohnson/litestream"
"github.com/superfly/ltx"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/util/vfsutil"
"github.com/ncruces/go-sqlite3/vfs"
"github.com/ncruces/wbt"
)
type liteVFS struct{}
func (liteVFS) Open(name string, flags vfs.OpenFlag) (vfs.File, vfs.OpenFlag, error) {
// 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.
if flags&vfs.OPEN_MAIN_DB == 0 {
return nil, flags, sqlite3.CANTOPEN
}
liteMtx.RLock()
defer liteMtx.RUnlock()
if db, ok := liteDBs[name]; ok {
// Build the page index so we can lookup individual pages.
if err := db.buildIndex(context.Background()); err != nil {
db.opts.Logger.Error("build index", "error", err)
return nil, 0, err
}
return &liteFile{db: db}, flags | vfs.OPEN_READONLY, nil
}
return nil, flags, sqlite3.CANTOPEN
}
func (liteVFS) Delete(name string, dirSync bool) error {
// notest // used to delete journals
return sqlite3.IOERR_DELETE_NOENT
}
func (liteVFS) Access(name string, flag vfs.AccessFlag) (bool, error) {
// notest // used to check for journals
return false, nil
}
func (liteVFS) FullPathname(name string) (string, error) {
return name, nil
}
type liteFile struct {
db *liteDB
conn *sqlite3.Conn
pages *pageIndex
txid ltx.TXID
pageSize uint32
}
func (f *liteFile) Close() error { return nil }
func (f *liteFile) ReadAt(p []byte, off int64) (n int, err error) {
ctx := f.context()
pages, txid := f.pages, f.txid
if pages == nil {
pages, txid, err = f.db.pollReplica(ctx)
}
if err != nil {
return 0, err
}
pgno := uint32(1)
if off >= 512 {
pgno += uint32(off / int64(f.pageSize))
}
elem, ok := pages.Get(pgno)
if !ok {
return 0, io.EOF
}
data, err := f.db.cache.getOrFetch(pgno, elem.MaxTXID, func() (any, error) {
_, data, err := litestream.FetchPage(ctx, f.db.client, elem.Level, elem.MinTXID, elem.MaxTXID, elem.Offset, elem.Size)
return data, err
})
if err != nil {
f.db.opts.Logger.Error("fetch page", "error", err)
return 0, err
}
// Update the first page to pretend we are in journal mode,
// load the page size and track changes to the database.
if pgno == 1 && len(data) >= 100 &&
data[18] >= 1 && data[19] >= 1 &&
data[18] <= 3 && data[19] <= 3 {
data[18], data[19] = 0x01, 0x01
binary.BigEndian.PutUint32(data[24:28], uint32(txid))
f.pageSize = uint32(256 * binary.LittleEndian.Uint16(data[16:18]))
}
n = copy(p, data[off%int64(len(data)):])
return n, nil
}
func (f *liteFile) WriteAt(b []byte, off int64) (n int, err error) {
// notest // OPEN_READONLY
return 0, sqlite3.IOERR_WRITE
}
func (f *liteFile) Truncate(size int64) error {
// notest // OPEN_READONLY
return sqlite3.IOERR_TRUNCATE
}
func (f *liteFile) Sync(flag vfs.SyncFlag) error {
// notest // OPEN_READONLY
return sqlite3.IOERR_FSYNC
}
func (f *liteFile) Size() (size int64, err error) {
if max := f.pages.Max(); max != nil {
size = int64(max.Key()) * int64(f.pageSize)
}
return
}
func (f *liteFile) Lock(lock vfs.LockLevel) (err error) {
if lock >= vfs.LOCK_RESERVED {
return sqlite3.IOERR_LOCK
}
f.pages, f.txid, err = f.db.pollReplica(f.context())
return err
}
func (f *liteFile) Unlock(lock vfs.LockLevel) error {
f.pages, f.txid = nil, 0
return nil
}
func (f *liteFile) CheckReservedLock() (bool, error) {
// notest // used to check for hot journals
return false, nil
}
func (f *liteFile) SectorSize() int {
// notest // safe default
return 0
}
func (f *liteFile) DeviceCharacteristics() vfs.DeviceCharacteristic {
// notest // safe default
return 0
}
func (f *liteFile) SetDB(conn any) {
f.conn = conn.(*sqlite3.Conn)
}
func (f *liteFile) context() context.Context {
if f.conn != nil {
return f.conn.GetInterrupt()
}
return context.Background()
}
type liteDB struct {
client litestream.ReplicaClient
opts ReplicaOptions
cache pageCache
pages *pageIndex // +checklocks:mtx
lastPoll time.Time // +checklocks:mtx
txids levelTXIDs // +checklocks:mtx
mtx sync.Mutex
}
func (f *liteDB) buildIndex(ctx context.Context) error {
f.mtx.Lock()
defer f.mtx.Unlock()
// Skip if we already have an index.
if f.pages != nil {
return nil
}
// Build the index from scratch from a Litestream restore plan.
infos, err := litestream.CalcRestorePlan(ctx, f.client, 0, time.Time{}, f.opts.Logger)
if err != nil {
if !errors.Is(err, litestream.ErrTxNotAvailable) {
return fmt.Errorf("calc restore plan: %w", err)
}
return nil
}
for _, info := range infos {
err := f.updateInfo(ctx, info)
if err != nil {
return err
}
}
f.lastPoll = time.Now()
return nil
}
func (f *liteDB) pollReplica(ctx context.Context) (*pageIndex, ltx.TXID, error) {
f.mtx.Lock()
defer f.mtx.Unlock()
// Limit polling interval.
if time.Since(f.lastPoll) < f.opts.PollInterval {
return f.pages, f.txids[0], nil
}
for level := range pollLevels(f.opts.MinLevel) {
if err := f.updateLevel(ctx, level); err != nil {
f.opts.Logger.Error("cannot poll replica", "error", err)
return nil, 0, err
}
}
f.lastPoll = time.Now()
return f.pages, f.txids[0], nil
}
// +checklocks:f.mtx
func (f *liteDB) updateLevel(ctx context.Context, level int) error {
var nextTXID ltx.TXID
// Snapshots must start from scratch,
// other levels can start from where they were left.
if level != litestream.SnapshotLevel {
nextTXID = f.txids[level] + 1
}
// Start reading from the next LTX file after the current position.
itr, err := f.client.LTXFiles(ctx, level, nextTXID, false)
if err != nil {
return fmt.Errorf("ltx files: %w", err)
}
defer itr.Close()
// Build an update across all new LTX files.
for itr.Next() {
info := itr.Item()
// Skip LTX files already fully loaded into the index.
if info.MaxTXID <= f.txids[level] {
continue
}
err := f.updateInfo(ctx, info)
if err != nil {
return err
}
}
if err := itr.Err(); err != nil {
return err
}
return itr.Close()
}
// +checklocks:f.mtx
func (f *liteDB) updateInfo(ctx context.Context, info *ltx.FileInfo) error {
idx, err := litestream.FetchPageIndex(ctx, f.client, info)
if err != nil {
return fmt.Errorf("fetch page index: %w", err)
}
// Replace pages in the index with new pages.
for k, v := range idx {
// Patch avoids mutating the index for an unmodified page.
f.pages = f.pages.Patch(k, func(node *pageIndex) (ltx.PageIndexElem, bool) {
return v, node == nil || v != node.Value()
})
}
// Track the MaxTXID for each level.
maxTXID := &f.txids[info.Level]
*maxTXID = max(*maxTXID, info.MaxTXID)
return nil
}
func pollLevels(minLevel int) (r []int) {
// Updating from lower to upper levels is non-racy,
// since LTX files are compacted into higher levels
// before the lower level LTX files are deleted.
// Also, only level 0 compactions and snapshots delete files,
// so the intermediate levels never need to be updated.
if minLevel <= 0 {
return append(r, 0, 1, litestream.SnapshotLevel)
}
if minLevel >= litestream.SnapshotLevel {
return append(r, litestream.SnapshotLevel)
}
return append(r, minLevel, litestream.SnapshotLevel)
}
// Type aliases; these are a mouthful.
type pageIndex = wbt.Tree[uint32, ltx.PageIndexElem]
type levelTXIDs = [litestream.SnapshotLevel + 1]ltx.TXID

34
litestream/vfs_test.go Normal file
View File

@@ -0,0 +1,34 @@
package litestream
import (
"slices"
"strconv"
"testing"
"github.com/benbjohnson/litestream"
_ "github.com/ncruces/go-sqlite3/embed"
)
func Test_pollLevels(t *testing.T) {
tests := []struct {
minLevel int
want []int
}{
{minLevel: -1, want: []int{0, 1, litestream.SnapshotLevel}},
{minLevel: 0, want: []int{0, 1, litestream.SnapshotLevel}},
{minLevel: 1, want: []int{1, litestream.SnapshotLevel}},
{minLevel: 2, want: []int{2, litestream.SnapshotLevel}},
{minLevel: 3, want: []int{3, litestream.SnapshotLevel}},
{minLevel: litestream.SnapshotLevel, want: []int{litestream.SnapshotLevel}},
{minLevel: litestream.SnapshotLevel + 1, want: []int{litestream.SnapshotLevel}},
}
for _, tt := range tests {
t.Run(strconv.Itoa(tt.minLevel), func(t *testing.T) {
got := pollLevels(tt.minLevel)
if !slices.Equal(got, tt.want) {
t.Errorf("pollLevels() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -5,11 +5,14 @@ import (
"context"
"math/bits"
"os"
"runtime"
"strings"
"sync"
"unsafe"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental"
"github.com/ncruces/go-sqlite3/internal/util"
"github.com/ncruces/go-sqlite3/vfs"
@@ -78,7 +81,9 @@ func compileSQLite() {
return
}
instance.compiled, instance.err = instance.runtime.CompileModule(ctx, bin)
instance.compiled, instance.err = instance.runtime.CompileModule(
experimental.WithCompilationWorkers(ctx, runtime.GOMAXPROCS(0)/4),
bin)
}
type sqlite struct {
@@ -124,14 +129,14 @@ func (sqlt *sqlite) error(rc res_t, handle ptr_t, sql ...string) error {
panic(util.OOMErr)
}
var msg, query string
if handle != 0 {
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: "):]:
msg = strings.TrimPrefix(msg, "sqlite3: ")
msg = strings.TrimPrefix(msg, util.ErrorCodeString(rc)[len("sqlite3: "):])
msg = strings.TrimPrefix(msg, ": ")
if msg == "" || msg == "not an error" {
msg = ""
}
}
@@ -141,10 +146,16 @@ func (sqlt *sqlite) error(rc res_t, handle ptr_t, sql ...string) error {
query = sql[0][i:]
}
}
}
if msg != "" || query != "" {
return &Error{code: rc, msg: msg, sql: query}
}
var sys error
switch ErrorCode(rc) {
case CANTOPEN, IOERR:
sys = util.GetSystemError(sqlt.ctx)
}
if sys != nil || msg != "" || query != "" {
return &Error{code: rc, sys: sys, msg: msg, sql: query}
}
return xErrorCode(rc)
}

View File

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

View File

@@ -3,46 +3,54 @@ set -euo pipefail
cd -P -- "$(dirname -- "$0")"
curl -#OL "https://sqlite.org/2025/sqlite-amalgamation-3500400.zip"
unzip -d . sqlite-amalgamation-*.zip
mv sqlite-amalgamation-*/sqlite3.c .
mv sqlite-amalgamation-*/sqlite3.h .
mv sqlite-amalgamation-*/sqlite3ext.h .
rm -rf sqlite-amalgamation-*
curl -#OL "https://sqlite.org/2025/sqlite-autoconf-3510000.tar.gz"
# To test a snapshot:
# Verify download.
if hash=$(openssl dgst -sha3-256 sqlite-autoconf-*.tar.gz); then
if ! [[ $hash =~ fa52f9cc74dbca004aa650ae698036a3350611f672649e165078f4eae21d6a2e ]]; then
echo $hash
exit 1
fi
fi 2> /dev/null
tar xzf sqlite-autoconf-*.tar.gz
# To test a snapshot instead:
# curl -# https://sqlite.org/snapshot/sqlite-snapshot-202410081727.tar.gz | tar xz
# mv sqlite-snapshot-*/sqlite3.c .
# mv sqlite-snapshot-*/sqlite3.h .
# mv sqlite-snapshot-*/sqlite3ext.h .
# rm -rf sqlite-snapshot-*
mv sqlite-*/sqlite3.c .
mv sqlite-*/sqlite3.h .
mv sqlite-*/sqlite3ext.h .
rm -r sqlite-*
GITHUB_TAG="https://github.com/sqlite/sqlite/raw/version-3.51.0"
mkdir -p ext/
cd ext/
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"
curl -#OL "$GITHUB_TAG/ext/misc/anycollseq.c"
curl -#OL "$GITHUB_TAG/ext/misc/base64.c"
curl -#OL "$GITHUB_TAG/ext/misc/decimal.c"
curl -#OL "$GITHUB_TAG/ext/misc/ieee754.c"
curl -#OL "$GITHUB_TAG/ext/misc/regexp.c"
curl -#OL "$GITHUB_TAG/ext/misc/series.c"
curl -#OL "$GITHUB_TAG/ext/misc/spellfix.c"
curl -#OL "$GITHUB_TAG/ext/misc/uint.c"
cd ~-
cd ../vfs/tests/mptest/testdata/
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"
curl -#OL "$GITHUB_TAG/mptest/config01.test"
curl -#OL "$GITHUB_TAG/mptest/config02.test"
curl -#OL "$GITHUB_TAG/mptest/crash01.test"
curl -#OL "$GITHUB_TAG/mptest/crash02.subtest"
curl -#OL "$GITHUB_TAG/mptest/multiwrite01.test"
cd ~-
cd ../vfs/tests/mptest/wasm/
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.4/mptest/mptest.c"
curl -#OL "$GITHUB_TAG/mptest/mptest.c"
cd ~-
cd ../vfs/tests/speedtest1/wasm/
curl -#OL "https://github.com/sqlite/sqlite/raw/version-3.50.4/test/speedtest1.c"
curl -#OL "$GITHUB_TAG/test/speedtest1.c"
cd ~-
cat *.patch | patch -p0 --no-backup-if-mismatch
cat *.patch | patch -p0 --no-backup-if-mismatch

View File

@@ -43,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

@@ -75,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)
}
}
@@ -86,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)
}
}
@@ -97,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)
}
}
@@ -109,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)
}
}
@@ -121,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)
}
}
@@ -133,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)
}
}
@@ -146,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)
}
}
@@ -162,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)
}
}
@@ -176,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)
}
}

View File

@@ -26,8 +26,8 @@ elif [[ "$OSTYPE" == "darwin"* ]]; then
fi
fi
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_123/binaryen-version_123-$BINARYEN.tar.gz"
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-29/wasi-sdk-29.0-$WASI_SDK.tar.gz"
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_125/binaryen-version_125-$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
@@ -26884,7 +26884,7 @@
@@ -27176,7 +27176,7 @@
sqlite3_free(p);
return sqlite3_os_init();
}
@@ -10,7 +10,7 @@
/*
** The list of all registered VFS implementations.
*/
@@ -26981,7 +26981,7 @@
@@ -27273,7 +27273,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

@@ -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

@@ -22,8 +22,8 @@ func TestConn_Open_dir(t *testing.T) {
if err == nil {
t.Fatal("want error")
}
if !errors.Is(err, sqlite3.CANTOPEN) {
t.Errorf("got %v, want sqlite3.CANTOPEN", err)
if !errors.Is(err, sqlite3.CANTOPEN_ISDIR) {
t.Errorf("got %v, want sqlite3.CANTOPEN_ISDIR", err)
}
}
@@ -163,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.
@@ -202,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`)
@@ -212,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

@@ -107,9 +107,9 @@ func Test_mvcc(t *testing.T) {
iter = 5000
}
mvcc.Create("test.db", "")
name := "file:/test.db?vfs=mvcc" +
"&_pragma=busy_timeout(10000)"
name := mvcc.TestDB(t, mvcc.Snapshot{}, url.Values{
"_pragma": {"busy_timeout(10000)"},
})
createDB(t, name)
testParallel(t, name, iter)
testIntegrity(t, name)
@@ -330,9 +330,9 @@ func Benchmark_memdb(b *testing.B) {
}
func Benchmark_mvcc(b *testing.B) {
mvcc.Create("test.db", "")
name := "file:/test.db?vfs=mvcc" +
"&_pragma=busy_timeout(10000)"
name := mvcc.TestDB(b, mvcc.Snapshot{}, url.Values{
"_pragma": {"busy_timeout(10000)"},
})
createDB(b, name)
b.ResetTimer()

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

@@ -24,8 +24,9 @@ trap 'rm -f sql3parse_table.tmp' EXIT
-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

View File

@@ -1,9 +1,7 @@
package sqlite3
import (
"encoding/json"
"math"
"strconv"
"time"
"github.com/ncruces/go-sqlite3/internal/util"
@@ -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

@@ -20,11 +20,12 @@ The main differences to be aware of are
### File Locking
POSIX advisory locks,
which SQLite uses on [Unix](https://github.com/sqlite/sqlite/blob/5d60f4/src/os_unix.c#L13-L14),
are [broken by design](https://github.com/sqlite/sqlite/blob/5d60f4/src/os_unix.c#L1074-L1162).
which SQLite uses on [Unix](https://github.com/sqlite/sqlite/blob/41fda52/src/os_unix.c#L13-L14),
are [broken by design](https://github.com/sqlite/sqlite/blob/41fda52/src/os_unix.c#L1188-L1276).
Instead, on Linux and macOS, this package uses
[OFD locks](https://gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html)
[OFD locks](https://sourceware.org/glibc/manual/2.25/html_node/Open-File-Description-Locks.html)
to synchronize access to database files.
OFD locks require [Linux 3.15](https://lwn.net/Articles/586904/).
This package can also use
[BSD locks](https://man.freebsd.org/cgi/man.cgi?query=flock&sektion=2),
@@ -73,7 +74,7 @@ to check if your build supports shared memory.
### Blocking Locks
On Windows and macOS, this package implements
On macOS, this package implements
[Wal-mode blocking locks](https://sqlite.org/src/doc/tip/doc/wal-lock.md).
### Batch-Atomic Write
@@ -116,9 +117,13 @@ The VFS can be customized with a few build tags:
- [`github.com/ncruces/go-sqlite3/vfs/memdb`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/memdb)
implements an in-memory VFS.
- [`github.com/ncruces/go-sqlite3/vfs/mvcc`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/mvcc)
implements an in-memory MVCC VFS.
- [`github.com/ncruces/go-sqlite3/vfs/readervfs`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/readervfs)
implements a VFS for immutable databases.
- [`github.com/ncruces/go-sqlite3/vfs/adiantum`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/adiantum)
wraps a VFS to offer encryption at rest.
- [`github.com/ncruces/go-sqlite3/vfs/xts`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/xts)
wraps a VFS to offer encryption at rest.
- [`github.com/ncruces/go-sqlite3/litestream`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/litestream)
implements Litestream [lightweight read-replicas](https://fly.io/blog/litestream-revamped/#lightweight-read-replicas).

View File

@@ -4,6 +4,7 @@ import (
"crypto/rand"
"golang.org/x/crypto/argon2"
"lukechampine.com/adiantum"
"lukechampine.com/adiantum/hbsh"
)
@@ -25,8 +26,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

@@ -8,6 +8,7 @@ import (
"os"
"golang.org/x/crypto/argon2"
"lukechampine.com/adiantum/hbsh"
"lukechampine.com/adiantum/hpolyc"
@@ -45,8 +46,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

@@ -56,7 +56,7 @@ func (h *hbshVFS) OpenFilename(name *vfs.Filename, flags vfs.OpenFlag) (file vfs
if hbsh == nil {
file.Close()
return nil, flags, sqlite3.CANTOPEN
return nil, flags, sqlite3.IOERR_BADKEY
}
return &hbshFile{File: file, hbsh: hbsh, init: h.init}, flags, nil
}
@@ -108,7 +108,7 @@ func (h *hbshFile) Pragma(name string, value string) (string, error) {
if h.hbsh = h.init.HBSH(key); h.hbsh != nil {
return "ok", nil
}
return "", sqlite3.CANTOPEN
return "", sqlite3.IOERR_BADKEY
}
func (h *hbshFile) ReadAt(p []byte, off int64) (n int, err error) {

View File

@@ -10,7 +10,8 @@ import (
// A VFS defines the interface between the SQLite core and the underlying operating system.
//
// Use sqlite3.ErrorCode or sqlite3.ExtendedErrorCode to return specific error codes to SQLite.
// Use [SystemError], sqlite3.ErrorCode, or sqlite3.ExtendedErrorCode
// to return specific error codes to SQLite.
//
// https://sqlite.org/c3ref/vfs.html
type VFS interface {
@@ -31,8 +32,9 @@ type VFSFilename interface {
// A File represents an open file in the OS interface layer.
//
// Use sqlite3.ErrorCode or sqlite3.ExtendedErrorCode to return specific error codes to SQLite.
// In particular, sqlite3.BUSY is necessary to correctly implement lock methods.
// Use [SystemError], sqlite3.ErrorCode, or sqlite3.ExtendedErrorCode
// to return specific error codes to SQLite.
// In particular, sqlite3.BUSY is needed to correctly implement lock methods.
//
// https://sqlite.org/c3ref/io_methods.html
type File interface {
@@ -193,8 +195,8 @@ type FileSharedMemory interface {
// SharedMemory is a shared-memory WAL-index implementation.
// Use [NewSharedMemory] to create a shared-memory.
type SharedMemory interface {
shmMap(context.Context, api.Module, int32, int32, bool) (ptr_t, _ErrorCode)
shmLock(int32, int32, _ShmFlag) _ErrorCode
shmMap(context.Context, api.Module, int32, int32, bool) (ptr_t, error)
shmLock(int32, int32, _ShmFlag) error
shmUnmap(bool)
shmBarrier()
io.Closer
@@ -205,6 +207,10 @@ type blockingSharedMemory interface {
shmEnableBlocking(block bool)
}
// FileControl makes it easy to forward all fileControl methods,
// which we want to do for the checksum VFS.
// However, this is not a safe default, and other VFSes
// should explicitly wrap the methods they want to wrap.
type fileControl interface {
File
fileControl(ctx context.Context, mod api.Module, op _FcntlOpcode, pArg ptr_t) _ErrorCode

View File

@@ -20,11 +20,11 @@ type (
type _ErrorCode uint32
func (e _ErrorCode) Error() string {
return util.ErrorCodeString(uint32(e))
return util.ErrorCodeString(e)
}
const (
_OK _ErrorCode = util.OK
_OK = util.OK
_ERROR _ErrorCode = util.ERROR
_PERM _ErrorCode = util.PERM
_BUSY _ErrorCode = util.BUSY
@@ -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

@@ -40,7 +40,7 @@ func evalSymlinks(path string) (string, error) {
func (vfsOS) Delete(path string, syncDir bool) error {
err := os.Remove(path)
if errors.Is(err, fs.ErrNotExist) {
return _IOERR_DELETE_NOENT
return sysError{err, _IOERR_DELETE_NOENT}
}
if err != nil {
return err
@@ -48,12 +48,12 @@ func (vfsOS) Delete(path string, syncDir bool) error {
if isUnix && syncDir {
f, err := os.Open(filepath.Dir(path))
if err != nil {
return _OK
return nil
}
defer f.Close()
err = osSync(f, false, false)
err = osSync(f, 0, SYNC_FULL)
if err != nil {
return _IOERR_DIR_FSYNC
return sysError{err, _IOERR_DIR_FSYNC}
}
}
return nil
@@ -108,14 +108,14 @@ func (vfsOS) OpenFilename(name *Filename, flags OpenFlag) (File, OpenFlag, error
}
if err != nil {
if name == nil {
return nil, flags, _IOERR_GETTEMPPATH
return nil, flags, sysError{err, _IOERR_GETTEMPPATH}
}
if errors.Is(err, syscall.EISDIR) {
return nil, flags, _CANTOPEN_ISDIR
return nil, flags, sysError{err, _CANTOPEN_ISDIR}
}
if isCreate && isJournl && errors.Is(err, fs.ErrPermission) &&
osAccess(name.String(), ACCESS_EXISTS) != nil {
return nil, flags, _READONLY_DIRECTORY
return nil, flags, sysError{err, _READONLY_DIRECTORY}
}
return nil, flags, err
}
@@ -123,7 +123,7 @@ func (vfsOS) OpenFilename(name *Filename, flags OpenFlag) (File, OpenFlag, error
if modeof := name.URIParameter("modeof"); modeof != "" {
if err = osSetMode(f, modeof); err != nil {
f.Close()
return nil, flags, _IOERR_FSTAT
return nil, flags, sysError{err, _IOERR_FSTAT}
}
}
if isUnix && flags&OPEN_DELETEONCLOSE != 0 {
@@ -131,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 (
@@ -164,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 {
@@ -183,23 +180,20 @@ 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
return sysError{err, _IOERR_DIR_FSYNC}
}
}
return nil
@@ -215,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" {
@@ -249,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

@@ -120,7 +120,7 @@ func (n *Filename) URIParameter(key string) string {
}
// Parse the format from:
// https://github.com/sqlite/sqlite/blob/b74eb0/src/pager.c#L4797-L4840
// https://github.com/sqlite/sqlite/blob/41fda52/src/pager.c#L4821-L4864
// This avoids having to alloc/free the key just to find a value.
for {
k := util.ReadString(n.mod, ptr, _MAX_NAME)
@@ -160,7 +160,7 @@ func (n *Filename) URIParameters() url.Values {
var params url.Values
// Parse the format from:
// https://github.com/sqlite/sqlite/blob/b74eb0/src/pager.c#L4797-L4840
// https://github.com/sqlite/sqlite/blob/41fda52/src/pager.c#L4821-L4864
// This is the only way to support multiple valued keys.
for {
k := util.ReadString(n.mod, ptr, _MAX_NAME)

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
}
@@ -51,8 +51,8 @@ func (f *vfsFile) Lock(lock LockLevel) error {
if f.lock != LOCK_NONE {
panic(util.AssertErr())
}
if rc := osGetSharedLock(f.File); rc != _OK {
return rc
if err := osGetSharedLock(f.File); err != nil {
return err
}
f.lock = LOCK_SHARED
return nil
@@ -62,8 +62,8 @@ func (f *vfsFile) Lock(lock LockLevel) error {
if f.lock != LOCK_SHARED {
panic(util.AssertErr())
}
if rc := osGetReservedLock(f.File); rc != _OK {
return rc
if err := osGetReservedLock(f.File); err != nil {
return err
}
f.lock = LOCK_RESERVED
return nil
@@ -73,8 +73,8 @@ func (f *vfsFile) Lock(lock LockLevel) error {
if f.lock <= LOCK_NONE || f.lock >= LOCK_EXCLUSIVE {
panic(util.AssertErr())
}
if rc := osGetExclusiveLock(f.File, &f.lock); rc != _OK {
return rc
if err := osGetExclusiveLock(f.File, &f.lock); err != nil {
return err
}
f.lock = LOCK_EXCLUSIVE
return nil
@@ -101,14 +101,14 @@ func (f *vfsFile) Unlock(lock LockLevel) error {
switch lock {
case LOCK_SHARED:
rc := osDowngradeLock(f.File, f.lock)
err := osDowngradeLock(f.File, f.lock)
f.lock = LOCK_SHARED
return rc
return err
case LOCK_NONE:
rc := osReleaseLock(f.File, f.lock)
err := osReleaseLock(f.File, f.lock)
f.lock = LOCK_NONE
return rc
return err
default:
panic(util.AssertErr())

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)

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