diff --git a/config.go b/config.go index 2f82d08..0342be7 100644 --- a/config.go +++ b/config.go @@ -82,7 +82,7 @@ func (c *Conn) SetAuthorizer(cb func(action AuthorizerActionCode, name3rd, name4 } -func authorizerCallback(ctx context.Context, mod api.Module, pDB uint32, action AuthorizerActionCode, zName3rd, zName4th, zSchema, zNameInner uint32) AuthorizerReturnCode { +func authorizerCallback(ctx context.Context, mod api.Module, pDB uint32, action AuthorizerActionCode, zName3rd, zName4th, zSchema, zNameInner uint32) (rc AuthorizerReturnCode) { if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB && c.authorizer != nil { var name3rd, name4th, schema, nameInner string if zName3rd != 0 { @@ -97,7 +97,68 @@ func authorizerCallback(ctx context.Context, mod api.Module, pDB uint32, action if zNameInner != 0 { nameInner = util.ReadString(mod, zNameInner, _MAX_NAME) } - return c.authorizer(action, name3rd, name4th, schema, nameInner) + rc = c.authorizer(action, name3rd, name4th, schema, nameInner) } - return AUTH_OK + return rc +} + +// WalCheckpoint checkpoints a WAL database. +// +// https://sqlite.org/c3ref/wal_checkpoint_v2.html +func (c *Conn) WalCheckpoint(schema string, mode CheckpointMode) (nLog, nCkpt int, err error) { + defer c.arena.mark()() + nLogPtr := c.arena.new(ptrlen) + nCkptPtr := c.arena.new(ptrlen) + schemaPtr := c.arena.string(schema) + r := c.call("sqlite3_wal_checkpoint_v2", + uint64(c.handle), uint64(schemaPtr), uint64(mode), + uint64(nLogPtr), uint64(nCkptPtr)) + nLog = int(int32(util.ReadUint32(c.mod, nLogPtr))) + nCkpt = int(int32(util.ReadUint32(c.mod, nCkptPtr))) + return nLog, nCkpt, c.error(r) +} + +// WalAutoCheckpoint configures WAL auto-checkpoints. +// +// https://sqlite.org/c3ref/wal_autocheckpoint.html +func (c *Conn) WalAutoCheckpoint(pages int) error { + r := c.call("sqlite3_wal_autocheckpoint", uint64(c.handle), uint64(pages)) + return c.error(r) +} + +// WalHook registers a callback function to be invoked +// each time data is committed to a database in WAL mode. +// +// https://sqlite.org/c3ref/wal_hook.html +func (c *Conn) WalHook(cb func(db *Conn, schema string, pages int) error) { + var enable uint64 + if cb != nil { + enable = 1 + } + c.call("sqlite3_wal_hook_go", uint64(c.handle), enable) + c.wal = cb +} + +func walCallback(ctx context.Context, mod api.Module, _, pDB, zSchema uint32, pages int32) (rc uint32) { + if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB && c.wal != nil { + schema := util.ReadString(mod, zSchema, _MAX_NAME) + err := c.wal(c, schema, int(pages)) + _, rc = errorCode(err, ERROR) + } + return rc +} + +// AutoVacuumPages registers a autovacuum compaction amount callback. +// +// https://sqlite.org/c3ref/autovacuum_pages.html +func (c *Conn) AutoVacuumPages(cb func(schema string, dbPages, freePages, bytesPerPage uint) uint) error { + funcPtr := util.AddHandle(c.ctx, cb) + r := c.call("sqlite3_autovacuum_pages_go", uint64(c.handle), uint64(funcPtr)) + return c.error(r) +} + +func autoVacuumCallback(ctx context.Context, mod api.Module, pApp, zSchema, nDbPage, nFreePage, nBytePerPage uint32) uint32 { + fn := util.GetHandle(ctx, pApp).(func(schema string, dbPages, freePages, bytesPerPage uint) uint) + schema := util.ReadString(mod, zSchema, _MAX_NAME) + return uint32(fn(schema, uint(nDbPage), uint(nFreePage), uint(nBytePerPage))) } diff --git a/conn.go b/conn.go index 626669a..bb4cae2 100644 --- a/conn.go +++ b/conn.go @@ -29,6 +29,7 @@ type Conn struct { update func(AuthorizerActionCode, string, string, int64) commit func() bool rollback func() + wal func(*Conn, string, int) error arena arena handle uint32 @@ -281,6 +282,12 @@ func (c *Conn) ReleaseMemory() error { return c.error(r) } +// GetInterrupt gets the context set with [Conn.SetInterrupt], +// or nil if none was set. +func (c *Conn) GetInterrupt() context.Context { + return c.interrupt +} + // SetInterrupt interrupts a long-running query when a context is done. // // Subsequent uses of the connection will return [INTERRUPT] @@ -325,13 +332,13 @@ func (c *Conn) checkInterrupt() { } } -func progressCallback(ctx context.Context, mod api.Module, pDB uint32) uint32 { +func progressCallback(ctx context.Context, mod api.Module, pDB uint32) (interrupt uint32) { if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB && c.commit != nil { if c.interrupt != nil && c.interrupt.Err() != nil { - return 1 + interrupt = 1 } } - return 0 + return interrupt } // BusyTimeout sets a busy timeout. @@ -359,13 +366,13 @@ func (c *Conn) BusyHandler(cb func(count int) (retry bool)) error { return nil } -func busyCallback(ctx context.Context, mod api.Module, pDB, count uint32) uint32 { +func busyCallback(ctx context.Context, mod api.Module, pDB uint32, count int32) (retry uint32) { if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB && c.busy != nil { - if retry := c.busy(int(count)); retry { - return 1 + if c.busy(int(count)) { + retry = 1 } } - return 0 + return retry } func (c *Conn) error(rc uint64, sql ...string) error { diff --git a/const.go b/const.go index 5b7e412..2bb5365 100644 --- a/const.go +++ b/const.go @@ -305,6 +305,18 @@ const ( AUTH_IGNORE AuthorizerReturnCode = 2 /* Don't allow access, but don't generate an error */ ) +// CheckpointMode are all the checkpoint mode values. +// +// https://sqlite.org/c3ref/c_checkpoint_full.html +type CheckpointMode uint32 + +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 */ +) + // TxnState are the allowed return values from [Conn.TxnState]. // // https://sqlite.org/c3ref/c_txn_none.html diff --git a/embed/README.md b/embed/README.md index a9367c4..043f73c 100644 --- a/embed/README.md +++ b/embed/README.md @@ -1,6 +1,6 @@ # Embeddable Wasm build of SQLite -This folder includes an embeddable Wasm build of SQLite 3.45.1 for use with +This folder includes an embeddable Wasm build of SQLite 3.45.2 for use with [`github.com/ncruces/go-sqlite3`](https://pkg.go.dev/github.com/ncruces/go-sqlite3). The following optional features are compiled in: diff --git a/embed/exports.txt b/embed/exports.txt index 4af10dc..0f9fc46 100644 --- a/embed/exports.txt +++ b/embed/exports.txt @@ -1,8 +1,9 @@ +aligned_alloc free malloc malloc_destructor -aligned_alloc sqlite3_anycollseq_init +sqlite3_autovacuum_pages_go sqlite3_backup_finish sqlite3_backup_init sqlite3_backup_pagecount @@ -115,4 +116,7 @@ sqlite3_vtab_in_first sqlite3_vtab_in_next sqlite3_vtab_nochange sqlite3_vtab_on_conflict -sqlite3_vtab_rhs_value \ No newline at end of file +sqlite3_vtab_rhs_value +sqlite3_wal_autocheckpoint +sqlite3_wal_checkpoint_v2 +sqlite3_wal_hook_go \ No newline at end of file diff --git a/embed/sqlite3.wasm b/embed/sqlite3.wasm index c36b3c8..da51855 100755 Binary files a/embed/sqlite3.wasm and b/embed/sqlite3.wasm differ diff --git a/gormlite/migrator.go b/gormlite/migrator.go index 5a3b343..c54c536 100644 --- a/gormlite/migrator.go +++ b/gormlite/migrator.go @@ -334,7 +334,7 @@ type _Index struct { // GetIndexes return Indexes []gorm.Index and execErr error, // See the [doc] // -// [doc]: https://www.sqlite.org/pragma.html#pragma_index_list +// [doc]: https://sqlite.org/pragma.html#pragma_index_list func (m _Migrator) GetIndexes(value interface{}) ([]gorm.Index, error) { indexes := make([]gorm.Index, 0) err := m.RunWithValue(value, func(stmt *gorm.Statement) error { diff --git a/sqlite.go b/sqlite.go index a7f3f3b..05c3234 100644 --- a/sqlite.go +++ b/sqlite.go @@ -294,6 +294,8 @@ func exportCallbacks(env wazero.HostModuleBuilder) wazero.HostModuleBuilder { util.ExportFuncII(env, "go_commit_hook", commitCallback) util.ExportFuncVI(env, "go_rollback_hook", rollbackCallback) util.ExportFuncVIIIIJ(env, "go_update_hook", updateCallback) + util.ExportFuncIIIII(env, "go_wal_hook", walCallback) + util.ExportFuncIIIIII(env, "go_autovacuum_pages", autoVacuumCallback) util.ExportFuncIIIIIII(env, "go_authorizer", authorizerCallback) util.ExportFuncVIII(env, "go_log", logCallback) util.ExportFuncVI(env, "go_destroy", destroyCallback) diff --git a/sqlite3/hooks.c b/sqlite3/hooks.c index 445d661..ee9b735 100644 --- a/sqlite3/hooks.c +++ b/sqlite3/hooks.c @@ -8,12 +8,16 @@ int go_busy_handler(void *, int); int go_commit_hook(void *); void go_rollback_hook(void *); void go_update_hook(void *, int, char const *, char const *, sqlite3_int64); +int go_wal_hook(void *, sqlite3 *, const char *, int); int go_authorizer(void *, int, const char *, const char *, const char *, const char *); void go_log(void *, int, const char *); +unsigned int go_autovacuum_pages(void *, const char *, unsigned int, + unsigned int, unsigned int); + void sqlite3_progress_handler_go(sqlite3 *db, int n) { sqlite3_progress_handler(db, n, go_progress_handler, /*arg=*/db); } @@ -34,6 +38,10 @@ void sqlite3_update_hook_go(sqlite3 *db, bool enable) { sqlite3_update_hook(db, enable ? go_update_hook : NULL, /*arg=*/db); } +void sqlite3_wal_hook_go(sqlite3 *db, bool enable) { + sqlite3_wal_hook(db, enable ? go_wal_hook : NULL, /*arg=*/NULL); +} + int sqlite3_set_authorizer_go(sqlite3 *db, bool enable) { return sqlite3_set_authorizer(db, enable ? go_authorizer : NULL, /*arg=*/db); } @@ -41,4 +49,10 @@ int sqlite3_set_authorizer_go(sqlite3 *db, bool enable) { int sqlite3_config_log_go(bool enable) { return sqlite3_config(SQLITE_CONFIG_LOG, enable ? go_log : NULL, /*arg=*/NULL); +} + +int sqlite3_autovacuum_pages_go(sqlite3 *db, go_handle app) { + int rc = sqlite3_autovacuum_pages(db, go_autovacuum_pages, app, go_destroy); + if (rc) go_destroy(app); + return rc; } \ No newline at end of file diff --git a/stmt.go b/stmt.go index 34fdafb..fc26b1e 100644 --- a/stmt.go +++ b/stmt.go @@ -120,7 +120,7 @@ func (s *Stmt) Status(op StmtStatus, reset bool) int { } r := s.c.call("sqlite3_stmt_status", uint64(s.handle), uint64(op), i) - return int(r) + return int(int32(r)) } // ClearBindings resets all bindings on the prepared statement. @@ -137,7 +137,7 @@ func (s *Stmt) ClearBindings() error { func (s *Stmt) BindCount() int { r := s.c.call("sqlite3_bind_parameter_count", uint64(s.handle)) - return int(r) + return int(int32(r)) } // BindIndex returns the index of a parameter in the prepared statement @@ -149,7 +149,7 @@ func (s *Stmt) BindIndex(name string) int { namePtr := s.c.arena.string(name) r := s.c.call("sqlite3_bind_parameter_index", uint64(s.handle), uint64(namePtr)) - return int(r) + return int(int32(r)) } // BindName returns the name of a parameter in the prepared statement. @@ -357,7 +357,7 @@ func (s *Stmt) BindValue(param int, value Value) error { func (s *Stmt) ColumnCount() int { r := s.c.call("sqlite3_column_count", uint64(s.handle)) - return int(r) + return int(int32(r)) } // ColumnName returns the name of the result column. diff --git a/tests/conn_test.go b/tests/conn_test.go index 2e3dbea..0fd5978 100644 --- a/tests/conn_test.go +++ b/tests/conn_test.go @@ -11,6 +11,7 @@ import ( "github.com/ncruces/go-sqlite3" _ "github.com/ncruces/go-sqlite3/embed" + _ "github.com/ncruces/go-sqlite3/vfs/memdb" ) func TestConn_Open_dir(t *testing.T) { @@ -449,3 +450,35 @@ func TestConn_DBName(t *testing.T) { t.Errorf("got %s", name) } } + +func TestConn_AutoVacuumPages(t *testing.T) { + t.Parallel() + + db, err := sqlite3.Open("file:test.db?vfs=memdb&_pragma=auto_vacuum(FULL)") + if err != nil { + t.Fatal(err) + } + defer db.Close() + + err = db.AutoVacuumPages(func(schema string, dbPages, freePages, bytesPerPage uint) uint { + return freePages + }) + if err != nil { + t.Fatal(err) + } + + err = db.Exec(`CREATE TABLE test (col)`) + if err != nil { + t.Fatal(err) + } + + err = db.Exec(`INSERT INTO test VALUES (zeroblob(1024*1024))`) + if err != nil { + t.Fatal(err) + } + + err = db.Exec(`DROP TABLE test`) + if err != nil { + t.Fatal(err) + } +} diff --git a/tests/wal_test.go b/tests/wal_test.go index 3076b64..4ed6d17 100644 --- a/tests/wal_test.go +++ b/tests/wal_test.go @@ -31,3 +31,34 @@ func TestWAL_enter_exit(t *testing.T) { t.Fatal(err) } } + +func TestConn_WalCheckpoint(t *testing.T) { + t.Parallel() + + file := filepath.Join(t.TempDir(), "test.db") + + db, err := sqlite3.Open(file) + if err != nil { + t.Fatal(err) + } + defer db.Close() + + err = db.WalAutoCheckpoint(1000) + if err != nil { + t.Fatal(err) + } + + db.WalHook(func(db *sqlite3.Conn, schema string, pages int) error { + log, ckpt, err := db.WalCheckpoint(schema, sqlite3.CHECKPOINT_FULL) + t.Log(log, ckpt, err) + return err + }) + + err = db.Exec(` + PRAGMA journal_mode=WAL; + CREATE TABLE test (col); + `) + if err != nil { + t.Fatal(err) + } +} diff --git a/txn.go b/txn.go index 0f96f3b..0efbc2d 100644 --- a/txn.go +++ b/txn.go @@ -257,7 +257,7 @@ func (c *Conn) RollbackHook(cb func()) { c.rollback = cb } -// RollbackHook registers a callback function to be invoked +// UpdateHook registers a callback function to be invoked // whenever a row is updated, inserted or deleted in a rowid table. // // https://sqlite.org/c3ref/update_hook.html @@ -270,13 +270,13 @@ func (c *Conn) UpdateHook(cb func(action AuthorizerActionCode, schema, table str c.update = cb } -func commitCallback(ctx context.Context, mod api.Module, pDB uint32) uint32 { +func commitCallback(ctx context.Context, mod api.Module, pDB uint32) (rollback uint32) { if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.handle == pDB && c.commit != nil { - if ok := c.commit(); !ok { - return 1 + if !c.commit() { + rollback = 1 } } - return 0 + return rollback } func rollbackCallback(ctx context.Context, mod api.Module, pDB uint32) { diff --git a/vfs/lock.go b/vfs/lock.go index 9332c9f..57bc5f9 100644 --- a/vfs/lock.go +++ b/vfs/lock.go @@ -117,11 +117,9 @@ func (f *vfsFile) Unlock(lock LockLevel) error { switch lock { case LOCK_SHARED: - if rc := osDowngradeLock(f.File, f.lock); rc != _OK { - return rc - } + rc := osDowngradeLock(f.File, f.lock) f.lock = LOCK_SHARED - return nil + return rc case LOCK_NONE: rc := osReleaseLock(f.File, f.lock) diff --git a/vfs/tests/mptest/testdata/mptest.wasm.bz2 b/vfs/tests/mptest/testdata/mptest.wasm.bz2 index 0b753ef..fc887dc 100644 --- a/vfs/tests/mptest/testdata/mptest.wasm.bz2 +++ b/vfs/tests/mptest/testdata/mptest.wasm.bz2 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:523b3640c6de9accf3f6b01e7da2098faba12eec0168b67a78c56fef0716c7ae -size 469261 +oid sha256:40b56dfd62584e32da7191c664ecfdd28dca419de05b9e46f4b36f7919a36d3e +size 468861 diff --git a/vfs/tests/speedtest1/testdata/speedtest1.wasm.bz2 b/vfs/tests/speedtest1/testdata/speedtest1.wasm.bz2 index e178f88..ec64efa 100644 --- a/vfs/tests/speedtest1/testdata/speedtest1.wasm.bz2 +++ b/vfs/tests/speedtest1/testdata/speedtest1.wasm.bz2 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:855720cce2881c98d09c15eddf9cab0d5974a2a82f7f67987a28b97414629345 -size 483463 +oid sha256:6be5a0accea1a62ecdaf2055388f02ab52ea13323b90c36dd5833d8a4717c7c1 +size 483476 diff --git a/vtab.go b/vtab.go index 982b1d4..a330c98 100644 --- a/vtab.go +++ b/vtab.go @@ -489,7 +489,7 @@ func vtabRenameCallback(ctx context.Context, mod api.Module, pVTab, zNew uint32) return vtabError(ctx, mod, pVTab, _VTAB_ERROR, err) } -func vtabFindFuncCallback(ctx context.Context, mod api.Module, pVTab, nArg, zName, pxFunc uint32) uint32 { +func vtabFindFuncCallback(ctx context.Context, mod api.Module, pVTab uint32, nArg int32, zName, pxFunc uint32) uint32 { vtab := vtabGetHandle(ctx, mod, pVTab).(VTabOverloader) f, op := vtab.FindFunction(int(nArg), util.ReadString(mod, zName, _MAX_NAME)) if op != 0 { @@ -539,19 +539,19 @@ func vtabRollbackCallback(ctx context.Context, mod api.Module, pVTab uint32) uin return vtabError(ctx, mod, pVTab, _VTAB_ERROR, err) } -func vtabSavepointCallback(ctx context.Context, mod api.Module, pVTab, id uint32) uint32 { +func vtabSavepointCallback(ctx context.Context, mod api.Module, pVTab uint32, id int32) uint32 { vtab := vtabGetHandle(ctx, mod, pVTab).(VTabSavepointer) err := vtab.Savepoint(int(id)) return vtabError(ctx, mod, pVTab, _VTAB_ERROR, err) } -func vtabReleaseCallback(ctx context.Context, mod api.Module, pVTab, id uint32) uint32 { +func vtabReleaseCallback(ctx context.Context, mod api.Module, pVTab uint32, id int32) uint32 { vtab := vtabGetHandle(ctx, mod, pVTab).(VTabSavepointer) err := vtab.Release(int(id)) return vtabError(ctx, mod, pVTab, _VTAB_ERROR, err) } -func vtabRollbackToCallback(ctx context.Context, mod api.Module, pVTab, id uint32) uint32 { +func vtabRollbackToCallback(ctx context.Context, mod api.Module, pVTab uint32, id int32) uint32 { vtab := vtabGetHandle(ctx, mod, pVTab).(VTabSavepointer) err := vtab.RollbackTo(int(id)) return vtabError(ctx, mod, pVTab, _VTAB_ERROR, err) @@ -573,7 +573,7 @@ func cursorCloseCallback(ctx context.Context, mod api.Module, pCur uint32) uint3 return vtabError(ctx, mod, 0, _VTAB_ERROR, err) } -func cursorFilterCallback(ctx context.Context, mod api.Module, pCur, idxNum, idxStr, nArg, pArg uint32) uint32 { +func cursorFilterCallback(ctx context.Context, mod api.Module, pCur uint32, idxNum int32, idxStr, nArg, pArg uint32) uint32 { cursor := vtabGetHandle(ctx, mod, pCur).(VTabCursor) db := ctx.Value(connKey{}).(*Conn) args := make([]Value, nArg) @@ -600,7 +600,7 @@ func cursorNextCallback(ctx context.Context, mod api.Module, pCur uint32) uint32 return vtabError(ctx, mod, pCur, _CURSOR_ERROR, err) } -func cursorColumnCallback(ctx context.Context, mod api.Module, pCur, pCtx, n uint32) uint32 { +func cursorColumnCallback(ctx context.Context, mod api.Module, pCur, pCtx uint32, n int32) uint32 { cursor := vtabGetHandle(ctx, mod, pCur).(VTabCursor) db := ctx.Value(connKey{}).(*Conn) err := cursor.Column(&Context{db, pCtx}, int(n))