Pre-update hook.

This commit is contained in:
Nuno Cruces
2026-06-08 12:59:01 +01:00
parent dc662cff13
commit 25266ab966
11 changed files with 142 additions and 69 deletions
+2 -4
View File
@@ -120,8 +120,7 @@ func (b *Backup) Step(nPage int) (done bool, err error) {
//
// https://sqlite.org/c3ref/backup_finish.html#sqlite3backupremaining
func (b *Backup) Remaining() int {
n := int32(b.c.wrp.Xsqlite3_backup_remaining(int32(b.handle)))
return int(n)
return int(b.c.wrp.Xsqlite3_backup_remaining(int32(b.handle)))
}
// PageCount returns the total number of pages in the source database
@@ -129,6 +128,5 @@ func (b *Backup) Remaining() int {
//
// https://sqlite.org/c3ref/backup_finish.html#sqlite3backuppagecount
func (b *Backup) PageCount() int {
n := int32(b.c.wrp.Xsqlite3_backup_pagecount(int32(b.handle)))
return int(n)
return int(b.c.wrp.Xsqlite3_backup_pagecount(int32(b.handle)))
}
+2 -3
View File
@@ -146,7 +146,7 @@ func (c *Conn) FileControl(schema string, op FcntlOpcode, arg ...any) (any, erro
rc = res_t(c.wrp.Xsqlite3_file_control(
int32(c.handle), int32(schemaPtr),
int32(op), int32(ptr)))
ret = uint32(c.wrp.Read32(ptr))
ret = c.wrp.Read32(ptr)
case FCNTL_LOCKSTATE:
rc = res_t(c.wrp.Xsqlite3_file_control(
@@ -193,8 +193,7 @@ func (c *Conn) FileControl(schema string, op FcntlOpcode, arg ...any) (any, erro
//
// https://sqlite.org/c3ref/limit.html
func (c *Conn) Limit(id LimitCategory, value int) int {
v := int32(c.wrp.Xsqlite3_limit(int32(c.handle), int32(id), int32(value)))
return int(v)
return int(c.wrp.Xsqlite3_limit(int32(c.handle), int32(id), int32(value)))
}
// SetAuthorizer registers an authorizer callback with the database connection.
+2 -1
View File
@@ -34,6 +34,7 @@ type Conn struct {
update func(AuthorizerActionCode, string, string, int64)
commit func() bool
rollback func()
preupdate func(PreUpdateData)
busy1st time.Time
busylst time.Time
@@ -506,7 +507,7 @@ func (c *Conn) errorFor(handle ptr_t, rc res_t, sql ...string) error {
}
if len(sql) != 0 {
if i := int32(c.wrp.Xsqlite3_error_offset(int32(handle))); i != -1 {
if i := c.wrp.Xsqlite3_error_offset(int32(handle)); i != -1 {
query = sql[0][i:]
}
}
+1 -1
View File
@@ -217,6 +217,6 @@ func (ctx Context) ResultSubtype(t uint) {
//
// https://sqlite.org/c3ref/vtab_nochange.html
func (ctx Context) VTabNoChange() bool {
b := int32(ctx.c.wrp.Xsqlite3_vtab_nochange(int32(ctx.handle)))
b := ctx.c.wrp.Xsqlite3_vtab_nochange(int32(ctx.handle))
return b != 0
}
+1 -1
View File
@@ -3,7 +3,7 @@ module github.com/ncruces/go-sqlite3
go 1.25.0
require (
github.com/ncruces/go-sqlite3-wasm/v3 v3.0.35302
github.com/ncruces/go-sqlite3-wasm/v3 v3.0.35303-0.20260608094803-e16536efb9dc
github.com/ncruces/julianday v1.0.0
github.com/ncruces/sort v1.0.0
github.com/ncruces/wbt v1.0.0
+2 -2
View File
@@ -2,8 +2,8 @@ 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/go-sqlite3-wasm/v3 v3.0.35302 h1:pNeiMr2t0AIXxmiJ04C8JunmlNaPFLN9YoKF9LustUQ=
github.com/ncruces/go-sqlite3-wasm/v3 v3.0.35302/go.mod h1:WwFR2jWecdTSNSADwPfY34FeZ6OJIpDEzi6QZAoXd4w=
github.com/ncruces/go-sqlite3-wasm/v3 v3.0.35303-0.20260608094803-e16536efb9dc h1:rERx6R19XjRPF6LWou0KpsEPE5OkyQtZQwTMU7t8TBA=
github.com/ncruces/go-sqlite3-wasm/v3 v3.0.35303-0.20260608094803-e16536efb9dc/go.mod h1:WwFR2jWecdTSNSADwPfY34FeZ6OJIpDEzi6QZAoXd4w=
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
github.com/ncruces/sort v1.0.0 h1:tXeitUzE3B8I2z+rsqbKUBThBlJJ34glqCDP0XReJmk=
+7 -18
View File
@@ -72,7 +72,7 @@ func (s *Stmt) ExpandedSQL() string {
//
// https://sqlite.org/c3ref/stmt_readonly.html
func (s *Stmt) ReadOnly() bool {
b := int32(s.c.wrp.Xsqlite3_stmt_readonly(int32(s.handle)))
b := s.c.wrp.Xsqlite3_stmt_readonly(int32(s.handle))
return b != 0
}
@@ -151,9 +151,7 @@ func (s *Stmt) Status(op StmtStatus, reset bool) int {
if reset {
i = 1
}
n := int32(s.c.wrp.Xsqlite3_stmt_status(int32(s.handle),
int32(op), i))
return int(n)
return int(s.c.wrp.Xsqlite3_stmt_status(int32(s.handle), int32(op), i))
}
// ClearBindings resets all bindings on the prepared statement.
@@ -168,9 +166,7 @@ func (s *Stmt) ClearBindings() error {
//
// https://sqlite.org/c3ref/bind_parameter_count.html
func (s *Stmt) BindCount() int {
n := int32(s.c.wrp.Xsqlite3_bind_parameter_count(
int32(s.handle)))
return int(n)
return int(s.c.wrp.Xsqlite3_bind_parameter_count(int32(s.handle)))
}
// BindIndex returns the index of a parameter in the prepared statement
@@ -180,9 +176,7 @@ func (s *Stmt) BindCount() int {
func (s *Stmt) BindIndex(name string) int {
defer s.c.arena.Mark()()
namePtr := s.c.arena.String(name)
i := int32(s.c.wrp.Xsqlite3_bind_parameter_index(
int32(s.handle), int32(namePtr)))
return int(i)
return int(s.c.wrp.Xsqlite3_bind_parameter_index(int32(s.handle), int32(namePtr)))
}
// BindName returns the name of a parameter in the prepared statement.
@@ -365,18 +359,14 @@ func (s *Stmt) BindValue(param int, value Value) error {
//
// https://sqlite.org/c3ref/data_count.html
func (s *Stmt) DataCount() int {
n := int32(s.c.wrp.Xsqlite3_data_count(
int32(s.handle)))
return int(n)
return int(s.c.wrp.Xsqlite3_data_count(int32(s.handle)))
}
// ColumnCount returns the number of columns in a result set.
//
// https://sqlite.org/c3ref/column_count.html
func (s *Stmt) ColumnCount() int {
n := int32(s.c.wrp.Xsqlite3_column_count(
int32(s.handle)))
return int(n)
return int(s.c.wrp.Xsqlite3_column_count(int32(s.handle)))
}
// ColumnName returns the name of the result column.
@@ -568,8 +558,7 @@ func (s *Stmt) columnRawBytes(col int, ptr ptr_t, nul int32) []byte {
return nil
}
n := int32(s.c.wrp.Xsqlite3_column_bytes(
int32(s.handle), int32(col)))
n := s.c.wrp.Xsqlite3_column_bytes(int32(s.handle), int32(col))
return s.c.wrp.Bytes(ptr, int64(n+nul))[:n]
}
+17
View File
@@ -22,6 +22,23 @@ func TestConn_Transaction_exec(t *testing.T) {
db.RollbackHook(func() {})
db.CommitHook(func() bool { return true })
db.UpdateHook(func(sqlite3.AuthorizerActionCode, string, string, int64) {})
db.PreUpdateHook(func(pud sqlite3.PreUpdateData) {
if got := pud.Count(); got != 1 {
t.Errorf("got %d, want 1", got)
}
if got := pud.Depth(); got != 0 {
t.Errorf("got %d, want 0", got)
}
if got := pud.BlobWrite(); got != -1 {
t.Errorf("got %d, want -1", got)
}
if _, err := pud.New(0); err != nil {
t.Error(err)
}
if _, err := pud.Old(0); !errors.Is(err, sqlite3.MISUSE) {
t.Error(err)
}
})
err = db.Exec(`CREATE TABLE test (col)`)
if err != nil {
+81 -3
View File
@@ -255,7 +255,7 @@ func (c *Conn) RollbackHook(cb func()) {
// whenever a row is updated, inserted or deleted in a rowid table.
//
// https://sqlite.org/c3ref/update_hook.html
func (c *Conn) UpdateHook(cb func(action AuthorizerActionCode, schema, table string, rowid int64)) {
func (c *Conn) UpdateHook(cb func(op AuthorizerActionCode, schema, table string, rowid int64)) {
var enable int32
if cb != nil {
enable = 1
@@ -264,6 +264,19 @@ func (c *Conn) UpdateHook(cb func(action AuthorizerActionCode, schema, table str
c.update = cb
}
// PreUpdateHook registers a callback function that is invoked prior
// to each INSERT, UPDATE, and DELETE operation on a database table.
//
// https://sqlite.org/c3ref/preupdate_blobwrite.html
func (c *Conn) PreUpdateHook(cb func(PreUpdateData)) {
var enable int32
if cb != nil {
enable = 1
}
c.wrp.Xsqlite3_preupdate_hook_go(int32(c.handle), enable)
c.preupdate = cb
}
func (e *env) Xgo_commit_hook(pDB int32) (rollback int32) {
if c, ok := e.DB.(*Conn); ok && c.handle == ptr_t(pDB) && c.commit != nil {
if !c.commit() {
@@ -279,11 +292,24 @@ func (e *env) Xgo_rollback_hook(pDB int32) {
}
}
func (e *env) Xgo_update_hook(pDB, action, zSchema, zTabName int32, rowid int64) {
func (e *env) Xgo_update_hook(pDB, op, zSchema, zTabName int32, rowid int64) {
if c, ok := e.DB.(*Conn); ok && c.handle == ptr_t(pDB) && c.update != nil {
schema := e.ReadString(ptr_t(zSchema), _MAX_NAME)
table := e.ReadString(ptr_t(zTabName), _MAX_NAME)
c.update(AuthorizerActionCode(action), schema, table, rowid)
c.update(AuthorizerActionCode(op), schema, table, rowid)
}
}
func (e *env) Xgo_preupdate_hook(_, pDB, op, zSchema, zTabName int32, oldRowID, newRowID int64) {
if c, ok := e.DB.(*Conn); ok && c.handle == ptr_t(pDB) && c.preupdate != nil {
c.preupdate(PreUpdateData{
c: c,
Op: AuthorizerActionCode(op),
Schema: e.ReadString(ptr_t(zSchema), _MAX_NAME),
Table: e.ReadString(ptr_t(zTabName), _MAX_NAME),
OldRowID: oldRowID,
NewRowID: newRowID,
})
}
}
@@ -294,3 +320,55 @@ func (c *Conn) CacheFlush() error {
rc := res_t(c.wrp.Xsqlite3_db_cacheflush(int32(c.handle)))
return c.error(rc)
}
// PreUpdateData provides information about a preupdate event.
//
// https://sqlite.org/c3ref/preupdate_blobwrite.html
type PreUpdateData struct {
c *Conn
Op AuthorizerActionCode
Schema string
Table string
OldRowID int64
NewRowID int64
}
// Conn returns the database connection associated with the preupdate event.
func (pud *PreUpdateData) Conn() *Conn {
return pud.c
}
// Count returns the number of columns in the row that is being inserted, updated, or deleted.
//
// https://sqlite.org/c3ref/preupdate_blobwrite.html
func (pud *PreUpdateData) Count() int {
return int(pud.c.wrp.Xsqlite3_preupdate_count(int32(pud.c.handle)))
}
// Depth returns the trigger depth of the insert, update, or delete operation.
//
// https://sqlite.org/c3ref/preupdate_blobwrite.html
func (pud *PreUpdateData) Depth() int {
return int(pud.c.wrp.Xsqlite3_preupdate_depth(int32(pud.c.handle)))
}
// BlobWrite returns the index of the column being written to using [Blob].
//
// https://sqlite.org/c3ref/preupdate_blobwrite.html
func (pud *PreUpdateData) BlobWrite() int {
return int(pud.c.wrp.Xsqlite3_preupdate_blobwrite(int32(pud.c.handle)))
}
// Old returns the value of a column of the table row before it is updated.
//
// https://sqlite.org/c3ref/preupdate_blobwrite.html
func (pud *PreUpdateData) Old(column int) (Value, error) {
return pud.c.columnValue(pud.c.wrp.Xsqlite3_preupdate_old, pud.c.handle, column)
}
// New returns the value of a column of the table row after it is updated.
//
// https://sqlite.org/c3ref/preupdate_blobwrite.html
func (pud *PreUpdateData) New(column int) (Value, error) {
return pud.c.columnValue(pud.c.wrp.Xsqlite3_preupdate_new, pud.c.handle, column)
}
+23 -21
View File
@@ -147,7 +147,7 @@ func (v Value) rawBytes(ptr ptr_t, nul int32) []byte {
return nil
}
n := int32(v.c.wrp.Xsqlite3_value_bytes(int32(v.handle)))
n := v.c.wrp.Xsqlite3_value_bytes(int32(v.handle))
return v.c.wrp.Bytes(ptr, int64(n+nul))[:n]
}
@@ -163,7 +163,7 @@ func (v Value) Pointer() any {
//
// https://sqlite.org/c3ref/value_blob.html
func (v Value) NoChange() bool {
b := int32(v.c.wrp.Xsqlite3_value_nochange(int32(v.handle)))
b := v.c.wrp.Xsqlite3_value_nochange(int32(v.handle))
return b != 0
}
@@ -171,7 +171,7 @@ func (v Value) NoChange() bool {
//
// https://sqlite.org/c3ref/value_blob.html
func (v Value) FromBind() bool {
b := int32(v.c.wrp.Xsqlite3_value_frombind(int32(v.handle)))
b := v.c.wrp.Xsqlite3_value_frombind(int32(v.handle))
return b != 0
}
@@ -180,16 +180,7 @@ func (v Value) FromBind() bool {
//
// https://sqlite.org/c3ref/vtab_in_first.html
func (v Value) InFirst() (Value, error) {
defer v.c.arena.Mark()()
valPtr := v.c.arena.New(ptrlen)
rc := res_t(v.c.wrp.Xsqlite3_vtab_in_first(int32(v.handle), int32(valPtr)))
if err := v.c.error(rc); err != nil {
return Value{}, err
}
return Value{
c: v.c,
handle: ptr_t(v.c.wrp.Read32(valPtr)),
}, nil
return v.c.returnValue(v.c.wrp.Xsqlite3_vtab_in_first, v.handle)
}
// InNext returns the next element
@@ -197,14 +188,25 @@ func (v Value) InFirst() (Value, error) {
//
// https://sqlite.org/c3ref/vtab_in_first.html
func (v Value) InNext() (Value, error) {
defer v.c.arena.Mark()()
valPtr := v.c.arena.New(ptrlen)
rc := res_t(v.c.wrp.Xsqlite3_vtab_in_next(int32(v.handle), int32(valPtr)))
if err := v.c.error(rc); err != nil {
return v.c.returnValue(v.c.wrp.Xsqlite3_vtab_in_next, v.handle)
}
func (c *Conn) returnValue(fn func(_, _ int32) int32, handle ptr_t) (Value, error) {
defer c.arena.Mark()()
valPtr := c.arena.New(ptrlen)
rc := res_t(fn(int32(handle), int32(valPtr)))
if err := c.error(rc); err != nil {
return Value{}, err
}
return Value{
c: v.c,
handle: ptr_t(v.c.wrp.Read32(valPtr)),
}, nil
return Value{c: c, handle: ptr_t(c.wrp.Read32(valPtr))}, nil
}
func (c *Conn) columnValue(fn func(_, _, _ int32) int32, handle ptr_t, column int) (Value, error) {
defer c.arena.Mark()()
valPtr := c.arena.New(ptrlen)
rc := res_t(fn(int32(handle), int32(column), int32(valPtr)))
if err := c.error(rc); err != nil {
return Value{}, err
}
return Value{c: c, handle: ptr_t(c.wrp.Read32(valPtr))}, nil
}
+4 -15
View File
@@ -299,17 +299,7 @@ type IndexConstraintUsage struct {
//
// https://sqlite.org/c3ref/vtab_rhs_value.html
func (idx *IndexInfo) RHSValue(column int) (Value, error) {
defer idx.c.arena.Mark()()
valPtr := idx.c.arena.New(ptrlen)
rc := res_t(idx.c.wrp.Xsqlite3_vtab_rhs_value(int32(idx.handle),
int32(column), int32(valPtr)))
if err := idx.c.error(rc); err != nil {
return Value{}, err
}
return Value{
c: idx.c,
handle: ptr_t(idx.c.wrp.Read32(valPtr)),
}, nil
return idx.c.columnValue(idx.c.wrp.Xsqlite3_vtab_rhs_value, idx.handle, column)
}
// Collation returns the name of the collation for a virtual table constraint.
@@ -325,16 +315,15 @@ func (idx *IndexInfo) Collation(column int) string {
//
// https://sqlite.org/c3ref/vtab_distinct.html
func (idx *IndexInfo) Distinct() int {
i := int32(idx.c.wrp.Xsqlite3_vtab_distinct(int32(idx.handle)))
return int(i)
return int(idx.c.wrp.Xsqlite3_vtab_distinct(int32(idx.handle)))
}
// In identifies and handles IN constraints.
//
// https://sqlite.org/c3ref/vtab_in.html
func (idx *IndexInfo) In(column, handle int) bool {
b := int32(idx.c.wrp.Xsqlite3_vtab_in(int32(idx.handle),
int32(column), int32(handle)))
b := idx.c.wrp.Xsqlite3_vtab_in(int32(idx.handle),
int32(column), int32(handle))
return b != 0
}