Compare commits

..

59 Commits

Author SHA1 Message Date
Nuno Cruces
0e55451a0b Deps. 2025-12-19 12:28:57 +00:00
Nuno Cruces
ea9a58ab19 Remove singleflight. 2025-12-17 13:17:50 +00:00
Nuno Cruces
0b46e74ea6 Bump cross-platform-actions/action from 0.30.0 to 0.31.0 2025-12-16 12:34:55 +00:00
Nuno Cruces
8dca850bee Stricter floats. 2025-12-12 17:21:33 +00:00
Nuno Cruces
5b78823416 ScanColumn dropped. 2025-12-12 13:06:37 +00:00
Nuno Cruces
1764a571da Litestream v0.5.3. 2025-12-12 12:44:57 +00:00
Nuno Cruces
9837310af7 Fuzz time shift. 2025-12-10 16:15:44 +00:00
dependabot[bot]
ec8961a621 Bump golang.org/x/crypto from 0.45.0 to 0.46.0 (#341)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.45.0 to 0.46.0.
- [Commits](https://github.com/golang/crypto/compare/v0.45.0...v0.46.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.46.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-12-09 00:49:00 +00:00
Nuno Cruces
8c37aa2d97 Remove inprocess primary. 2025-12-04 16:31:41 +00:00
Nuno Cruces
ca93c498e7 Relative time, fixes. 2025-12-04 15:55:39 +00:00
Nuno Cruces
15e9087fa8 Time travel pragma. 2025-12-03 15:01:04 +00:00
Nuno Cruces
7028e3a5b9 SQLite 3.51.1. 2025-11-30 10:24:34 +00:00
Nuno Cruces
03bb20de6e Lock tweaks. 2025-11-29 16:12:29 +00:00
Nuno Cruces
20a51a344e Remove MinLevel. 2025-11-27 17:29:47 +00:00
Nuno Cruces
2dbcc480f7 Initial pragma support. 2025-11-27 16:28:16 +00:00
Nuno Cruces
0f0716c438 Inprocess Litestream primary. 2025-11-26 14:07:44 +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
137 changed files with 3240 additions and 1544 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: |
@@ -98,37 +98,35 @@ jobs:
matrix:
os:
- name: freebsd
version: '14.3'
flags: '-test.v'
version: '15.0'
- name: netbsd
version: '10.1'
flags: '-test.v'
- name: freebsd
arch: arm64
version: '14.3'
flags: '-test.v -test.short'
version: '15.0'
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.31.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 {
@@ -607,14 +605,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,7 +652,6 @@ type rows struct {
*stmt
names []string
types []string
nulls []bool
scans []scantype
}
@@ -652,7 +659,6 @@ var (
// Ensure these interfaces are implemented:
_ driver.RowsColumnTypeDatabaseTypeName = &rows{}
_ driver.RowsColumnTypeNullable = &rows{}
// _ driver.RowsColumnScanner = &rows{}
)
func (r *rows) Close() error {
@@ -675,34 +681,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 +729,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.
@@ -790,18 +796,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 +811,21 @@ 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
}
}
}
return nil
}
func (r *rows) ScanColumn(dest any, index int) error {
// notest // Go 1.26
var ptr *time.Time
switch d := dest.(type) {
case *time.Time:
ptr = d
case *sql.NullTime:
ptr = &d.Time
case *sql.Null[time.Time]:
ptr = &d.V
default:
return driver.ErrSkip
}
if t := r.Stmt.ColumnTime(index, r.tmRead); !t.IsZero() {
*ptr = t
return nil
}
return driver.ErrSkip
}

View File

@@ -33,7 +33,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 +43,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 +72,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 +100,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 +136,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 +151,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 +198,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 +233,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 +256,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 +297,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 +347,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 +384,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 +431,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)
}
@@ -539,12 +533,8 @@ func Benchmark_loop(b *testing.B) {
b.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
b.Cleanup(cancel)
b.ResetTimer()
for range b.N {
_, err := db.ExecContext(ctx,
for b.Loop() {
_, err := db.ExecContext(b.Context(),
`WITH RECURSIVE c(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM c WHERE x < 1000000) SELECT x FROM c;`)
if err != nil {
b.Fatal(err)

View File

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

View File

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

Binary file not shown.

View File

@@ -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,14 +15,15 @@ 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/f273f6b8245c5dca
curl -#L https://github.com/sqlite/sqlite/archive/7c126d7.tar.gz | tar xz --strip-components=1
# curl -#L https://sqlite.org/src/tarball/sqlite.tar.gz?r=f273f6b824 | 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"
else
sh configure --enable-update-limit
make verify-source
OPTS=-DSQLITE_ENABLE_ORDERED_SET_AGGREGATES make sqlite3.c
fi
cd ~-
@@ -58,12 +59,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.3
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.11.0 // indirect
golang.org/x/sys v0.39.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.3 h1:X/CgWW9GzmIAkEPrifhKqf0cC15DuOVxAJaHFTTAURQ=
github.com/ncruces/go-sqlite3 v0.30.3/go.mod h1:AxKu9sRxkludimFocbktlY6LiYSkxiI5gTA8r+os/Nw=
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.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.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=

View File

@@ -23,12 +23,14 @@ trap 'rm -f sqlite3.tmp' EXIT
-Wl,--import-undefined \
-Wl,--initial-memory=327680 \
-D_HAVE_SQLITE_CONFIG_H \
-DSQLITE_EXPERIMENTAL_PRAGMA_20251114 \
-DSQLITE_CUSTOM_INCLUDE=sqlite_opt.h \
$(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.1" {
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

@@ -16,6 +16,7 @@ import (
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/internal/util"
"github.com/ncruces/go-sqlite3/util/sql3util"
)
// Register registers the bloom_filter virtual table:
@@ -55,11 +56,9 @@ func create(db *sqlite3.Conn, _, schema, table string, arg ...string) (_ *bloom,
}
if len(arg) > 1 {
b.prob, err = strconv.ParseFloat(arg[1], 64)
if err != nil {
return nil, err
}
if b.prob <= 0 || b.prob >= 1 {
var ok bool
b.prob, ok = sql3util.ParseFloat(arg[1])
if !ok || b.prob <= 0 || b.prob >= 1 {
return nil, util.ErrorString("bloom: probability must be in the range (0,1)")
}
} else {

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

@@ -254,19 +254,15 @@ func (c *cursor) Column(ctx sqlite3.Context, col int) error {
switch typ {
case numeric, integer:
if strings.TrimLeft(txt, "+-0123456789") == "" {
if i, err := strconv.ParseInt(txt, 10, 64); err == nil {
ctx.ResultInt64(i)
return nil
}
if i, err := strconv.ParseInt(txt, 10, 64); err == nil {
ctx.ResultInt64(i)
return nil
}
fallthrough
case real:
if strings.TrimLeft(txt, "+-.0123456789Ee") == "" {
if f, err := strconv.ParseFloat(txt, 64); err == nil {
ctx.ResultFloat(f)
return nil
}
if f, ok := sql3util.ParseFloat(txt); ok {
ctx.ResultFloat(f)
return nil
}
fallthrough
default:

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.11.0
golang.org/x/sys v0.39.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.46.0 // vfs/adiantum vfs/xts
golang.org/x/sync v0.19.0 // test
golang.org/x/text v0.32.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.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
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.3
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.11.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.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.3 h1:X/CgWW9GzmIAkEPrifhKqf0cC15DuOVxAJaHFTTAURQ=
github.com/ncruces/go-sqlite3 v0.30.3/go.mod h1:AxKu9sRxkludimFocbktlY6LiYSkxiI5gTA8r+os/Nw=
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.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
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](example_test.go) for how to use.
Our `PRAGMA litestream_time` accepts:
- Go [duration strings](https://pkg.go.dev/time#ParseDuration)
- SQLite [time values](https://sqlite.org/lang_datefunc.html#time_values)
- SQLite [time modifiers 1 through 13](https://sqlite.org/lang_datefunc.html#modifiers)

77
litestream/api.go Normal file
View File

@@ -0,0 +1,77 @@
// 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
// 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 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)
}
type ReplicaClient = litestream.ReplicaClient

70
litestream/cache.go Normal file
View File

@@ -0,0 +1,70 @@
package litestream
import (
"context"
"fmt"
"sync"
"github.com/benbjohnson/litestream"
"github.com/superfly/ltx"
)
type pageCache struct {
pages map[uint32]cachedPage // +checklocks:mtx
size int
mtx sync.Mutex
}
type cachedPage struct {
data []byte
txid ltx.TXID
}
func (c *pageCache) getOrFetch(ctx context.Context, client ReplicaClient, pgno uint32, elem ltx.PageIndexElem) ([]byte, error) {
if c.size > 0 {
c.mtx.Lock()
page := c.pages[pgno]
c.mtx.Unlock()
if page.txid == elem.MaxTXID {
return page.data, nil
}
}
h, data, err := litestream.FetchPage(ctx, client, elem.Level, elem.MinTXID, elem.MaxTXID, elem.Offset, elem.Size)
if err != nil {
return nil, fmt.Errorf("fetch page: %w", err)
}
if pgno != h.Pgno {
return nil, fmt.Errorf("fetch page: want %d, got %d", pgno, h.Pgno)
}
if c.size > 0 {
c.mtx.Lock()
if c.pages != nil {
c.evict(len(data))
} else {
c.pages = map[uint32]cachedPage{}
}
c.pages[pgno] = cachedPage{data, elem.MaxTXID}
c.mtx.Unlock()
}
return 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()
}
}

62
litestream/go.mod Normal file
View File

@@ -0,0 +1,62 @@
module github.com/ncruces/go-sqlite3/litestream
go 1.24.4
require (
github.com/benbjohnson/litestream v0.5.5
github.com/ncruces/go-sqlite3 v0.30.4-0.20251216123455-0b46e74ea69b
github.com/ncruces/wbt v0.2.0
github.com/superfly/ltx v0.5.1
)
// github.com/ncruces/go-sqlite3
require (
github.com/ncruces/julianday v1.0.0 // indirect
github.com/tetratelabs/wazero v1.11.0 // indirect
golang.org/x/sys v0.39.0 // indirect
)
// github.com/superfly/ltx
require github.com/pierrec/lz4/v4 v4.1.22 // indirect
// github.com/benbjohnson/litestream
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/kr/text v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // 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.4 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
google.golang.org/protobuf v1.36.11 // 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 github.com/benbjohnson/litestream => github.com/ncruces/litestream v0.5.5

190
litestream/go.sum Normal file
View File

@@ -0,0 +1,190 @@
cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y=
cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic=
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=
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/aliyun/alibabacloud-oss-go-sdk-v2 v1.3.0 h1:wQlqotpyjYPjJz+Noh5bRu7Snmydk8SKC5Z6u1CR20Y=
github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.3.0/go.mod h1:FTzydeQVmR24FI0D6XWUOMKckjXehM/jgMn1xC+DA9M=
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/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.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/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.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/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.4-0.20251216123455-0b46e74ea69b h1:0HG7ul3Q1d/E/jrZpBTpzx4xhxJwMKuq5J4nuZIogm8=
github.com/ncruces/go-sqlite3 v0.30.4-0.20251216123455-0b46e74ea69b/go.mod h1:wz6IQnveXfqaXZozfhM8ciIJi2LRnnifBuBQarPDYo0=
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
github.com/ncruces/litestream v0.5.5 h1:LUoyorC+Xx0TtiuEjwd0+GIusCK5IIZwTPsO1+se55g=
github.com/ncruces/litestream v0.5.5/go.mod h1:yLedOf7Gj4ZQQB6tCB06zm9wpNc2d8TdemfWjjc6fNk=
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.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/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.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
github.com/prometheus/common v0.67.4/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/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/studio-b12/gowebdav v0.11.0 h1:qbQzq4USxY28ZYsGJUfO5jR+xkFtcnwWgitp4Zp1irU=
github.com/studio-b12/gowebdav v0.11.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
github.com/superfly/ltx v0.5.1 h1:vGUeBhKvBKZ2s2TsTrOSahz+m0PswfqOWDAD+ICiiYY=
github.com/superfly/ltx v0.5.1/go.mod h1:Nf50QAIXU/ET4ua3AuQ2fh31MbgNQZA7r/DYx6Os77s=
github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
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.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
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-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
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-20231120223509-83a465c0220f h1:Vn+VyHU5guc9KjB5KrjI2q0wCOWEOIh0OEsleqakHJg=
google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f/go.mod h1:nWSwAFPb+qfNJXsoeO3Io7zf4tMSfN8EA8RlDA04GhY=
google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f h1:2yNACc1O40tTnrsbk9Cv6oxiW8pxI/pXj0wRtdlYmgY=
google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f/go.mod h1:Uy9bTZJqmfrw2rIBxgGLnamc78euZULUBrLZ9XTITKI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 h1:DC7wcm+i+P1rN3Ff07vL+OndGg5OhNddHyTA+ocPqYE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4/go.mod h1:eJVxU6o+4G1PSczBr85xmyvSNYAKvAYgkub40YGomFM=
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/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=

63
litestream/time.go Normal file
View File

@@ -0,0 +1,63 @@
package litestream
import (
"math"
"strings"
"time"
"github.com/ncruces/go-sqlite3/util/sql3util"
)
func parseTimeDelta(s string) (years, months, days int, duration time.Duration, ok bool) {
duration, err := time.ParseDuration(s)
if err == nil {
return 0, 0, 0, duration, true
}
if strings.EqualFold(s, "now") {
return 0, 0, 0, 0, true
}
ss := strings.TrimSuffix(strings.ToLower(s), "s")
switch {
case strings.HasSuffix(ss, " year"):
years, duration, ok = parseDateUnit(ss, " year", 365*86400)
case strings.HasSuffix(ss, " month"):
months, duration, ok = parseDateUnit(ss, " month", 30*86400)
case strings.HasSuffix(ss, " day"):
months, duration, ok = parseDateUnit(ss, " day", 86400)
case strings.HasSuffix(ss, " hour"):
duration, ok = parseTimeUnit(ss, " hour", time.Hour)
case strings.HasSuffix(ss, " minute"):
duration, ok = parseTimeUnit(ss, " minute", time.Minute)
case strings.HasSuffix(ss, " second"):
duration, ok = parseTimeUnit(ss, " second", time.Second)
default:
return sql3util.ParseTimeShift(s)
}
return
}
func parseDateUnit(s, unit string, seconds float64) (int, time.Duration, bool) {
f, ok := sql3util.ParseFloat(s[:len(s)-len(unit)])
if !ok {
return 0, 0, false
}
i, f := math.Modf(f)
if math.MinInt <= i && i <= math.MaxInt {
return int(i), time.Duration(f * seconds * float64(time.Second)), true
}
return 0, 0, false
}
func parseTimeUnit(s, unit string, scale time.Duration) (time.Duration, bool) {
f, ok := sql3util.ParseFloat(s[:len(s)-len(unit)])
return time.Duration(f * float64(scale)), ok
}

406
litestream/vfs.go Normal file
View File

@@ -0,0 +1,406 @@
package litestream
import (
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"strconv"
"strings"
"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
syncTime time.Time
txid ltx.TXID
pageSize uint32
locked bool
}
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 && f.syncTime.IsZero() {
pages, txid, err = f.db.pollReplica(ctx)
}
if err != nil {
f.db.opts.Logger.Error("poll replica", "error", err)
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(ctx, f.db.client, pgno, elem)
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 {
// notest // OPEN_READONLY
return sqlite3.IOERR_LOCK
}
if f.syncTime.IsZero() {
f.pages, f.txid, err = f.db.pollReplica(f.context())
}
if err != nil {
f.db.opts.Logger.Error("poll replica", "error", err)
} else {
f.locked = true
}
return err
}
func (f *liteFile) Unlock(lock vfs.LockLevel) error {
if f.syncTime.IsZero() {
f.pages, f.txid = nil, 0
}
f.locked = false
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) Pragma(name, value string) (string, error) {
switch name {
case "litestream_txid":
txid := f.txid
if txid == 0 {
f.db.mtx.Lock()
txid = f.db.txids[0]
f.db.mtx.Unlock()
}
return txid.String(), nil
case "litestream_lag":
f.db.mtx.Lock()
lastPoll := f.db.lastPoll
f.db.mtx.Unlock()
if lastPoll.IsZero() {
return "-1", nil
}
lag := time.Since(lastPoll) / time.Second
return strconv.FormatInt(int64(lag), 10), nil
case "litestream_time":
if value == "" {
syncTime := f.syncTime
if syncTime.IsZero() {
f.db.mtx.Lock()
syncTime = f.db.lastInfo
f.db.mtx.Unlock()
}
if syncTime.IsZero() {
return "latest", nil
}
return syncTime.Format(time.RFC3339Nano), nil
}
if f.locked {
return "", sqlite3.MISUSE
}
if strings.EqualFold(value, "latest") {
f.syncTime = time.Time{}
f.pages, f.txid = nil, 0
return "", nil
}
var syncTime time.Time
if years, months, days, duration, ok := parseTimeDelta(value); ok {
syncTime = time.Now().AddDate(years, months, days).Add(duration)
} else {
syncTime, _ = sqlite3.TimeFormatAuto.Decode(value)
}
if syncTime.IsZero() {
return "", sqlite3.MISUSE
}
err := f.buildIndex(f.context(), syncTime)
if err != nil {
f.db.opts.Logger.Error("build index", "error", err)
}
return "", err
}
return "", sqlite3.NOTFOUND
}
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()
}
func (f *liteFile) buildIndex(ctx context.Context, syncTime time.Time) error {
// Build the index from scratch from a Litestream restore plan.
infos, err := litestream.CalcRestorePlan(ctx, f.db.client, 0, syncTime, f.db.opts.Logger)
if err != nil && !errors.Is(err, litestream.ErrTxNotAvailable) {
return fmt.Errorf("calc restore plan: %w", err)
}
var txid ltx.TXID
var pages *pageIndex
for _, info := range infos {
pages, err = fetchPageIndex(ctx, pages, f.db.client, info)
if err != nil {
return err
}
txid = max(txid, info.MaxTXID)
}
f.syncTime = syncTime
f.pages = pages
f.txid = txid
return nil
}
type liteDB struct {
client litestream.ReplicaClient
opts ReplicaOptions
cache pageCache
pages *pageIndex // +checklocks:mtx
lastPoll time.Time // +checklocks:mtx
lastInfo time.Time // +checklocks:mtx
txids levelTXIDs // +checklocks:mtx
mtx sync.Mutex
}
func (d *liteDB) buildIndex(ctx context.Context) error {
d.mtx.Lock()
defer d.mtx.Unlock()
// Skip if we already have an index.
if !d.lastPoll.IsZero() {
return nil
}
// Build the index from scratch from a Litestream restore plan.
infos, err := litestream.CalcRestorePlan(ctx, d.client, 0, time.Time{}, d.opts.Logger)
if err != nil && !errors.Is(err, litestream.ErrTxNotAvailable) {
return fmt.Errorf("calc restore plan: %w", err)
}
for _, info := range infos {
err := d.updateInfo(ctx, info)
if err != nil {
return err
}
}
d.lastPoll = time.Now()
return nil
}
func (d *liteDB) pollReplica(ctx context.Context) (*pageIndex, ltx.TXID, error) {
d.mtx.Lock()
defer d.mtx.Unlock()
// Limit polling interval.
if time.Since(d.lastPoll) < d.opts.PollInterval {
return d.pages, d.txids[0], nil
}
for level := range []int{0, 1, litestream.SnapshotLevel} {
if err := d.updateLevel(ctx, level); err != nil {
return nil, 0, err
}
}
d.lastPoll = time.Now()
return d.pages, d.txids[0], nil
}
// +checklocks:d.mtx
func (d *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 = d.txids[level] + 1
}
// Start reading from the next LTX file after the current position.
itr, err := d.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 <= d.txids[level] {
continue
}
err := d.updateInfo(ctx, info)
if err != nil {
return err
}
}
if err := itr.Err(); err != nil {
return err
}
return itr.Close()
}
// +checklocks:d.mtx
func (d *liteDB) updateInfo(ctx context.Context, info *ltx.FileInfo) error {
pages, err := fetchPageIndex(ctx, d.pages, d.client, info)
if err != nil {
return err
}
// Track the MaxTXID for each level.
maxTXID := &d.txids[info.Level]
*maxTXID = max(*maxTXID, info.MaxTXID)
d.txids[0] = max(d.txids[0], *maxTXID)
if d.lastInfo.Before(info.CreatedAt) {
d.lastInfo = info.CreatedAt
}
d.pages = pages
return nil
}
func fetchPageIndex(
ctx context.Context, pages *pageIndex,
client litestream.ReplicaClient, info *ltx.FileInfo) (*pageIndex, error) {
idx, err := litestream.FetchPageIndex(ctx, client, info)
if err != nil {
return nil, 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.
pages = pages.Patch(k, func(node *pageIndex) (ltx.PageIndexElem, bool) {
return v, node == nil || v != node.Value()
})
}
return pages, nil
}
// Type aliases; these are a mouthful.
type pageIndex = wbt.Tree[uint32, ltx.PageIndexElem]
type levelTXIDs = [litestream.SnapshotLevel + 1]ltx.TXID

133
litestream/vfs_test.go Normal file
View File

@@ -0,0 +1,133 @@
package litestream
import (
"path/filepath"
"testing"
"time"
"github.com/benbjohnson/litestream"
"github.com/benbjohnson/litestream/file"
"github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
)
func Test_integration(t *testing.T) {
dir := t.TempDir()
dbpath := filepath.Join(dir, "test.db")
backup := filepath.Join(dir, "backup", "test.db")
db, err := driver.Open(dbpath)
if err != nil {
t.Fatal(err)
}
defer db.Close()
client := file.NewReplicaClient(backup)
setupReplication(t, dbpath, client)
NewReplica("test.db", client, ReplicaOptions{})
replica, err := driver.Open("file:test.db?vfs=litestream")
if err != nil {
t.Fatal(err)
}
defer replica.Close()
_, err = db.ExecContext(t.Context(), `CREATE TABLE users (id INT, name VARCHAR(10))`)
if err != nil {
t.Fatal(err)
}
_, err = db.ExecContext(t.Context(),
`INSERT INTO users (id, name) VALUES (0, 'go'), (1, 'zig'), (2, 'whatever')`)
if err != nil {
t.Fatal(err)
}
time.Sleep(DefaultPollInterval + litestream.DefaultMonitorInterval)
rows, err := replica.QueryContext(t.Context(), `SELECT id, name FROM users`)
if err != nil {
t.Fatal(err)
}
defer rows.Close()
row := 0
ids := []int{0, 1, 2}
names := []string{"go", "zig", "whatever"}
for ; rows.Next(); row++ {
var id int
var name string
err := rows.Scan(&id, &name)
if err != nil {
t.Fatal(err)
}
if id != ids[row] {
t.Errorf("got %d, want %d", id, ids[row])
}
if name != names[row] {
t.Errorf("got %q, want %q", name, names[row])
}
}
if row != 3 {
t.Errorf("got %d, want %d", row, len(ids))
}
var lag int
err = replica.QueryRowContext(t.Context(), `PRAGMA litestream_lag`).Scan(&lag)
if err != nil {
t.Fatal(err)
}
if lag < 0 || lag > 2 {
t.Errorf("got %d", lag)
}
var txid string
err = replica.QueryRowContext(t.Context(), `PRAGMA litestream_txid`).Scan(&txid)
if err != nil {
t.Fatal(err)
}
if txid != "0000000000000001" {
t.Errorf("got %q", txid)
}
_, err = replica.ExecContext(t.Context(), `PRAGMA litestream_time='-1.5h'`)
if err != nil {
t.Fatal(err)
}
_, err = replica.ExecContext(t.Context(), `PRAGMA litestream_time='-00:01'`)
if err != nil {
t.Fatal(err)
}
_, err = replica.ExecContext(t.Context(), `PRAGMA litestream_time='-2.5 years'`)
if err != nil {
t.Fatal(err)
}
_, err = replica.ExecContext(t.Context(), `PRAGMA litestream_time='1970-01-01'`)
if err != nil {
t.Fatal(err)
}
var sync time.Time
err = replica.QueryRowContext(t.Context(), `PRAGMA litestream_time`).Scan(&sync)
if err != nil {
t.Fatal(err)
}
if !sync.Equal(time.Unix(0, 0)) {
t.Errorf("got %v", sync)
}
}
func setupReplication(tb testing.TB, path string, client ReplicaClient) {
lsdb := litestream.NewDB(path)
lsdb.Replica = litestream.NewReplicaWithClient(lsdb, client)
err := lsdb.Open()
if err != nil {
tb.Fatal(err)
}
tb.Cleanup(func() { lsdb.Close(tb.Context()) })
}

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-3510100.tar.gz"
# To test a snapshot:
# Verify download.
if hash=$(openssl dgst -sha3-256 sqlite-autoconf-*.tar.gz); then
if ! [[ $hash =~ 9b2b1e73f577def1d5b75c5541555a7f42e6e073ad19f7a9118478389c9bbd9b ]]; 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.1"
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)

17
time.go
View File

@@ -7,6 +7,7 @@ import (
"time"
"github.com/ncruces/go-sqlite3/internal/util"
"github.com/ncruces/go-sqlite3/util/sql3util"
"github.com/ncruces/julianday"
)
@@ -94,7 +95,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:
@@ -157,11 +158,13 @@ func (f TimeFormat) Decode(v any) (time.Time, error) {
case TimeFormatUnix, TimeFormatUnixFrac:
if s, ok := v.(string); ok {
f, err := strconv.ParseFloat(s, 64)
if err != nil {
return time.Time{}, err
if i, err := strconv.ParseInt(s, 10, 64); err == nil {
v = i
} else if f, ok := sql3util.ParseFloat(s); ok {
v = f
} else {
return time.Time{}, util.TimeErr
}
v = f
}
switch v := v.(type) {
case float64:
@@ -234,8 +237,8 @@ func (f TimeFormat) Decode(v any) (time.Time, error) {
v = i
break
}
f, err := strconv.ParseFloat(s, 64)
if err == nil {
f, ok := sql3util.ParseFloat(s)
if ok {
v = f
break
}

View File

@@ -1,6 +1,10 @@
package sql3util
import "strings"
import (
"strconv"
"strings"
"time"
)
// NamedArg splits an named arg into a key and value,
// around an equals sign.
@@ -63,3 +67,124 @@ func ParseBool(s string) (b, ok bool) {
}
return false, false
}
// ParseFloat parses a decimal floating point number.
func ParseFloat(s string) (f float64, ok bool) {
if strings.TrimLeft(s, "+-.0123456789Ee") != "" {
return
}
f, err := strconv.ParseFloat(s, 64)
return f, err == nil
}
// ParseTimeShift parses a time shift modifier,
// also the output of timediff.
//
// https://sqlite.org/lang_datefunc.html
func ParseTimeShift(s string) (years, months, days int, duration time.Duration, ok bool) {
// Sign part: ±
neg := strings.HasPrefix(s, "-")
sign := neg || strings.HasPrefix(s, "+")
if sign {
s = s[1:]
}
if ok = len(s) >= 5; !ok {
return // !ok
}
defer func() {
if neg {
years = -years
months = -months
days = -days
duration = -duration
}
}()
// Date part: YYYY-MM-DD
if s[4] == '-' {
if ok = sign && len(s) >= 10 && s[7] == '-'; !ok {
return // !ok
}
if years, ok = parseInt(s[0:4], 0); !ok {
return // !ok
}
if months, ok = parseInt(s[5:7], 12); !ok {
return // !ok
}
if days, ok = parseInt(s[8:10], 31); !ok {
return // !ok
}
if len(s) == 10 {
return
}
if ok = s[10] == ' '; !ok {
return // !ok
}
s = s[11:]
}
// Time part: HH:MM
if ok = len(s) >= 5 && s[2] == ':'; !ok {
return // !ok
}
var hours, minutes int
if hours, ok = parseInt(s[0:2], 24); !ok {
return
}
if minutes, ok = parseInt(s[3:5], 60); !ok {
return
}
duration = time.Duration(hours)*time.Hour + time.Duration(minutes)*time.Minute
if len(s) == 5 {
return
}
if ok = len(s) >= 8 && s[5] == ':'; !ok {
return // !ok
}
// Seconds part: HH:MM:SS
var seconds int
if seconds, ok = parseInt(s[6:8], 60); !ok {
return
}
duration += time.Duration(seconds) * time.Second
if len(s) == 8 {
return
}
if ok = len(s) >= 10 && s[8] == '.'; !ok {
return // !ok
}
s = s[9:]
// Nanosecond part: HH:MM:SS.SSS
var nanos int
if nanos, ok = parseInt(s[0:min(9, len(s))], 0); !ok {
return
}
for i := len(s); i < 9; i++ {
nanos *= 10
}
duration += time.Duration(nanos)
// Subnanosecond part.
if len(s) > 9 {
_, ok = parseInt(s[9:], 0)
}
return
}
func parseInt(s string, max int) (i int, _ bool) {
for _, r := range []byte(s) {
r -= '0'
if r > 9 {
return
}
i = i*10 + int(r)
}
return i, max == 0 || i < max
}

View File

@@ -2,7 +2,10 @@ package sql3util_test
import (
"testing"
"time"
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/util/sql3util"
)
@@ -53,3 +56,112 @@ func TestParseBool(t *testing.T) {
})
}
}
func TestParseTimeShift(t *testing.T) {
epoch := time.Unix(0, 0)
tests := []struct {
str string
val time.Time
ok bool
}{
{"", epoch, false},
{"0001-11-30", epoch, false},
{"+_001-11-30", epoch, false},
{"+0001-_1-30", epoch.AddDate(1, 0, 0), false},
{"+0001-11-_0", epoch.AddDate(1, 11, 0), false},
{"+0001-11-30", epoch.AddDate(1, 11, 30), true},
{"-0001-11-30", epoch.AddDate(-1, -11, -30), true},
{"+0001-11-30T", epoch.AddDate(1, 11, 30), false},
{"+0001-11-30 12", epoch.AddDate(1, 11, 30), false},
{"+0001-11-30 _2:30", epoch.AddDate(1, 11, 30), false},
{"+0001-11-30 12:_0", epoch.AddDate(1, 11, 30), false},
{"+0001-11-30 12:30", epoch.AddDate(1, 11, 30).Add(12*time.Hour + 30*time.Minute), true},
{"+0001-11-30 12:30:", epoch.AddDate(1, 11, 30).Add(12*time.Hour + 30*time.Minute), false},
{"+0001-11-30 12:30:_0", epoch.AddDate(1, 11, 30).Add(12*time.Hour + 30*time.Minute), false},
{"+0001-11-30 12:30:59", epoch.AddDate(1, 11, 30).Add(12*time.Hour + 30*time.Minute + 59*time.Second), true},
{"+0001-11-30 12:30:59.", epoch.AddDate(1, 11, 30).Add(12*time.Hour + 30*time.Minute + 59*time.Second), false},
{"+0001-11-30 12:30:59._", epoch.AddDate(1, 11, 30).Add(12*time.Hour + 30*time.Minute + 59*time.Second), false},
{"+0001-11-30 12:30:59.1", epoch.AddDate(1, 11, 30).Add(12*time.Hour + 30*time.Minute + 59*time.Second + 100*time.Millisecond), true},
{"+0001-11-30 12:30:59.123456789_", epoch.AddDate(1, 11, 30).Add(12*time.Hour + 30*time.Minute + 59*time.Second + 123456789), false},
{"+0001-11-30 12:30:59.1234567890", epoch.AddDate(1, 11, 30).Add(12*time.Hour + 30*time.Minute + 59*time.Second + 123456789), true},
{"-12:30:59.1234567890", epoch.Add(-12*time.Hour - 30*time.Minute - 59*time.Second - 123456789), true},
}
for _, tt := range tests {
t.Run(tt.str, func(t *testing.T) {
years, months, days, duration, gotOK := sql3util.ParseTimeShift(tt.str)
gotVal := epoch.AddDate(years, months, days).Add(duration)
if !gotVal.Equal(tt.val) || gotOK != tt.ok {
t.Errorf("ParseTimeShift(%q) = (%v, %v) want (%v, %v)", tt.str, gotVal, gotOK, tt.val, tt.ok)
}
})
}
}
func FuzzParseTimeShift(f *testing.F) {
f.Add("")
f.Add("0001-12-30")
f.Add("+_001-12-30")
f.Add("+0001-_2-30")
f.Add("+0001-12-_0")
f.Add("+0001-12-30")
f.Add("-0001-12-30")
f.Add("+0001-12-30T")
f.Add("+0001-12-30 12")
f.Add("+0001-12-30 _2:30")
f.Add("+0001-12-30 12:_0")
f.Add("+0001-12-30 12:30")
f.Add("+0001-12-30 12:30:")
f.Add("+0001-12-30 12:30:_0")
f.Add("+0001-12-30 12:30:60")
f.Add("+0001-12-30 12:30:60.")
f.Add("+0001-12-30 12:30:60._")
f.Add("+0001-12-30 12:30:60.1")
f.Add("+0001-12-30 12:30:60.123456789_")
f.Add("+0001-12-30 12:30:60.1234567890")
f.Add("-12:30:60.1234567890")
c, err := sqlite3.Open(":memory:")
if err != nil {
f.Fatal(err)
}
defer c.Close()
s, _, err := c.Prepare(`SELECT julianday('00:00', ?)`)
if err != nil {
f.Fatal(err)
}
defer s.Close()
// Default SQLite date.
epoch := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
f.Fuzz(func(t *testing.T, str string) {
years, months, days, duration, ok := sql3util.ParseTimeShift(str)
// Account for a full 400 year cycle.
if years < -200 || years > +200 {
t.Skip()
}
// SQLite only tracks milliseconds.
if duration != duration.Truncate(time.Millisecond) {
t.Skip()
}
if ok {
s.Reset()
s.BindText(1, str)
if !s.Step() {
t.Fail()
}
got := epoch.AddDate(years, months, days).Add(duration)
// Julian day introduces floating point inaccuracy.
want := s.ColumnTime(0, sqlite3.TimeFormatJulianDay)
want = want.Round(time.Millisecond)
if !got.Equal(want) {
t.Fatalf("with %q, got %v, want %v", str, got, want)
}
}
})
}

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)

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