// Copyright (C) 2016 Yasuhiro Matsumoto <mattn.jp@gmail.com>. // TODO: add "Gimpl do foo" team? // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. // +build trace package sqlite3 /* #ifndef USE_LIBSQLITE3 #include <sqlite3-binding.h> #else #include <sqlite3.h> #endif #include <stdlib.h> void stepTrampoline(sqlite3_context*, int, sqlite3_value**); void doneTrampoline(sqlite3_context*); void traceCallbackTrampoline(unsigned traceEventCode, void *ctx, void *p, void *x); */ import "C" import ( "errors" "fmt" "reflect" "strings" "sync" "unsafe" ) // Trace... constants identify the possible events causing callback invocation. // Values are same as the corresponding SQLite Trace Event Codes. const ( TraceStmt = C.SQLITE_TRACE_STMT TraceProfile = C.SQLITE_TRACE_PROFILE TraceRow = C.SQLITE_TRACE_ROW TraceClose = C.SQLITE_TRACE_CLOSE ) type TraceInfo struct { // Pack together the shorter fields, to keep the struct smaller. // On a 64-bit machine there would be padding // between EventCode and ConnHandle; having AutoCommit here is "free": EventCode uint32 AutoCommit bool ConnHandle uintptr // Usually filled, unless EventCode = TraceClose = SQLITE_TRACE_CLOSE: // identifier for a prepared statement: StmtHandle uintptr // Two strings filled when EventCode = TraceStmt = SQLITE_TRACE_STMT: // (1) either the unexpanded SQL text of the prepared statement, or // an SQL comment that indicates the invocation of a trigger; // (2) expanded SQL, if requested and if (1) is not an SQL comment. StmtOrTrigger string ExpandedSQL string // only if requested (TraceConfig.WantExpandedSQL = true) // filled when EventCode = TraceProfile = SQLITE_TRACE_PROFILE: // estimated number of nanoseconds that the prepared statement took to run: RunTimeNanosec int64 DBError Error } // TraceUserCallback gives the signature for a trace function // provided by the user (Go application programmer). // SQLite 3.14 documentation (as of September 2, 2016) // for SQL Trace Hook = sqlite3_trace_v2(): // The integer return value from the callback is currently ignored, // though this may change in future releases. Callback implementations // should return zero to ensure future compatibility. type TraceUserCallback func(TraceInfo) int type TraceConfig struct { Callback TraceUserCallback EventMask uint WantExpandedSQL bool } func fillDBError(dbErr *Error, db *C.sqlite3) { // See SQLiteConn.lastError(), in file 'sqlite3.go' at the time of writing (Sept 5, 2016) dbErr.Code = ErrNo(C.sqlite3_errcode(db)) dbErr.ExtendedCode = ErrNoExtended(C.sqlite3_extended_errcode(db)) dbErr.err = C.GoString(C.sqlite3_errmsg(db)) } func fillExpandedSQL(info *TraceInfo, db *C.sqlite3, pStmt unsafe.Pointer) { if pStmt == nil { panic("No SQLite statement pointer in P arg of trace_v2 callback") } expSQLiteCStr := C.sqlite3_expanded_sql((*C.sqlite3_stmt)(pStmt)) if expSQLiteCStr == nil { fillDBError(&info.DBError, db) return } info.ExpandedSQL = C.GoString(expSQLiteCStr) } //export traceCallbackTrampoline func traceCallbackTrampoline( traceEventCode uint, // Parameter named 'C' in SQLite docs = Context given at registration: ctx unsafe.Pointer, // Parameter named 'P' in SQLite docs (Primary event data?): p unsafe.Pointer, // Parameter named 'X' in SQLite docs (eXtra event data?): xValue unsafe.Pointer) int { if ctx == nil { panic(fmt.Sprintf("No context (ev 0x%x)", traceEventCode)) } contextDB := (*C.sqlite3)(ctx) connHandle := uintptr(ctx) var traceConf TraceConfig var found bool if traceEventCode == TraceClose { // clean up traceMap: 'pop' means get and delete traceConf, found = popTraceMapping(connHandle) } else { traceConf, found = lookupTraceMapping(connHandle) } if !found { panic(fmt.Sprintf("Mapping not found for handle 0x%x (ev 0x%x)", connHandle, traceEventCode)) } var info TraceInfo info.EventCode = uint32(traceEventCode) info.AutoCommit = (int(C.sqlite3_get_autocommit(contextDB)) != 0) info.ConnHandle = connHandle switch traceEventCode { case TraceStmt: info.StmtHandle = uintptr(p) var xStr string if xValue != nil { xStr = C.GoString((*C.char)(xValue)) } info.StmtOrTrigger = xStr if !strings.HasPrefix(xStr, "--") { // Not SQL comment, therefore the current event // is not related to a trigger. // The user might want to receive the expanded SQL; // let's check: if traceConf.WantExpandedSQL { fillExpandedSQL(&info, contextDB, p) } } case TraceProfile: info.StmtHandle = uintptr(p) if xValue == nil { panic("NULL pointer in X arg of trace_v2 callback for SQLITE_TRACE_PROFILE event") } info.RunTimeNanosec = *(*int64)(xValue) // sample the error //TODO: is it safe? is it useful? fillDBError(&info.DBError, contextDB) case TraceRow: info.StmtHandle = uintptr(p) case TraceClose: handle := uintptr(p) if handle != info.ConnHandle { panic(fmt.Sprintf("Different conn handle 0x%x (expected 0x%x) in SQLITE_TRACE_CLOSE event.", handle, info.ConnHandle)) } default: // Pass unsupported events to the user callback (if configured); // let the user callback decide whether to panic or ignore them. } // Do not execute user callback when the event was not requested by user! // Remember that the Close event is always selected when // registering this callback trampoline with SQLite --- for cleanup. // In the future there may be more events forced to "selected" in SQLite // for the driver's needs. if traceConf.EventMask&traceEventCode == 0 { return 0 } r := 0 if traceConf.Callback != nil { r = traceConf.Callback(info) } return r } type traceMapEntry struct { config TraceConfig } var traceMapLock sync.Mutex var traceMap = make(map[uintptr]traceMapEntry) func addTraceMapping(connHandle uintptr, traceConf TraceConfig) { traceMapLock.Lock() defer traceMapLock.Unlock() oldEntryCopy, found := traceMap[connHandle] if found { panic(fmt.Sprintf("Adding trace config %v: handle 0x%x already registered (%v).", traceConf, connHandle, oldEntryCopy.config)) } traceMap[connHandle] = traceMapEntry{config: traceConf} fmt.Printf("Added trace config %v: handle 0x%x.\n", traceConf, connHandle) } func lookupTraceMapping(connHandle uintptr) (TraceConfig, bool) { traceMapLock.Lock() defer traceMapLock.Unlock() entryCopy, found := traceMap[connHandle] return entryCopy.config, found } // 'pop' = get and delete from map before returning the value to the caller func popTraceMapping(connHandle uintptr) (TraceConfig, bool) { traceMapLock.Lock() defer traceMapLock.Unlock() entryCopy, found := traceMap[connHandle] if found { delete(traceMap, connHandle) fmt.Printf("Pop handle 0x%x: deleted trace config %v.\n", connHandle, entryCopy.config) } return entryCopy.config, found } // RegisterAggregator makes a Go type available as a SQLite aggregation function. // // Because aggregation is incremental, it's implemented in Go with a // type that has 2 methods: func Step(values) accumulates one row of // data into the accumulator, and func Done() ret finalizes and // returns the aggregate value. "values" and "ret" may be any type // supported by RegisterFunc. // // RegisterAggregator takes as implementation a constructor function // that constructs an instance of the aggregator type each time an // aggregation begins. The constructor must return a pointer to a // type, or an interface that implements Step() and Done(). // // The constructor function and the Step/Done methods may optionally // return an error in addition to their other return values. // // See _example/go_custom_funcs for a detailed example. func (c *SQLiteConn) RegisterAggregator(name string, impl interface{}, pure bool) error { var ai aggInfo ai.constructor = reflect.ValueOf(impl) t := ai.constructor.Type() if t.Kind() != reflect.Func { return errors.New("non-function passed to RegisterAggregator") } if t.NumOut() != 1 && t.NumOut() != 2 { return errors.New("SQLite aggregator constructors must return 1 or 2 values") } if t.NumOut() == 2 && !t.Out(1).Implements(reflect.TypeOf((*error)(nil)).Elem()) { return errors.New("Second return value of SQLite function must be error") } if t.NumIn() != 0 { return errors.New("SQLite aggregator constructors must not have arguments") } agg := t.Out(0) switch agg.Kind() { case reflect.Ptr, reflect.Interface: default: return errors.New("SQlite aggregator constructor must return a pointer object") } stepFn, found := agg.MethodByName("Step") if !found { return errors.New("SQlite aggregator doesn't have a Step() function") } step := stepFn.Type if step.NumOut() != 0 && step.NumOut() != 1 { return errors.New("SQlite aggregator Step() function must return 0 or 1 values") } if step.NumOut() == 1 && !step.Out(0).Implements(reflect.TypeOf((*error)(nil)).Elem()) { return errors.New("type of SQlite aggregator Step() return value must be error") } stepNArgs := step.NumIn() start := 0 if agg.Kind() == reflect.Ptr { // Skip over the method receiver stepNArgs-- start++ } if step.IsVariadic() { stepNArgs-- } for i := start; i < start+stepNArgs; i++ { conv, err := callbackArg(step.In(i)) if err != nil { return err } ai.stepArgConverters = append(ai.stepArgConverters, conv) } if step.IsVariadic() { conv, err := callbackArg(t.In(start + stepNArgs).Elem()) if err != nil { return err } ai.stepVariadicConverter = conv // Pass -1 to sqlite so that it allows any number of // arguments. The call helper verifies that the minimum number // of arguments is present for variadic functions. stepNArgs = -1 } doneFn, found := agg.MethodByName("Done") if !found { return errors.New("SQlite aggregator doesn't have a Done() function") } done := doneFn.Type doneNArgs := done.NumIn() if agg.Kind() == reflect.Ptr { // Skip over the method receiver doneNArgs-- } if doneNArgs != 0 { return errors.New("SQlite aggregator Done() function must have no arguments") } if done.NumOut() != 1 && done.NumOut() != 2 { return errors.New("SQLite aggregator Done() function must return 1 or 2 values") } if done.NumOut() == 2 && !done.Out(1).Implements(reflect.TypeOf((*error)(nil)).Elem()) { return errors.New("second return value of SQLite aggregator Done() function must be error") } conv, err := callbackRet(done.Out(0)) if err != nil { return err } ai.doneRetConverter = conv ai.active = make(map[int64]reflect.Value) ai.next = 1 // ai must outlast the database connection, or we'll have dangling pointers. c.aggregators = append(c.aggregators, &ai) cname := C.CString(name) defer C.free(unsafe.Pointer(cname)) opts := C.SQLITE_UTF8 if pure { opts |= C.SQLITE_DETERMINISTIC } rv := C._sqlite3_create_function(c.db, cname, C.int(stepNArgs), C.int(opts), C.uintptr_t(newHandle(c, &ai)), nil, (*[0]byte)(unsafe.Pointer(C.stepTrampoline)), (*[0]byte)(unsafe.Pointer(C.doneTrampoline))) if rv != C.SQLITE_OK { return c.lastError() } return nil } // SetTrace installs or removes the trace callback for the given database connection. // It's not named 'RegisterTrace' because only one callback can be kept and called. // Calling SetTrace a second time on same database connection // overrides (cancels) any prior callback and all its settings: // event mask, etc. func (c *SQLiteConn) SetTrace(requested *TraceConfig) error { connHandle := uintptr(unsafe.Pointer(c.db)) _, _ = popTraceMapping(connHandle) if requested == nil { // The traceMap entry was deleted already by popTraceMapping(): // can disable all events now, no need to watch for TraceClose. err := c.setSQLiteTrace(0) return err } reqCopy := *requested // Disable potentially expensive operations // if their result will not be used. We are doing this // just in case the caller provided nonsensical input. if reqCopy.EventMask&TraceStmt == 0 { reqCopy.WantExpandedSQL = false } addTraceMapping(connHandle, reqCopy) // The callback trampoline function does cleanup on Close event, // regardless of the presence or absence of the user callback. // Therefore it needs the Close event to be selected: actualEventMask := reqCopy.EventMask | TraceClose err := c.setSQLiteTrace(actualEventMask) return err } func (c *SQLiteConn) setSQLiteTrace(sqliteEventMask uint) error { rv := C.sqlite3_trace_v2(c.db, C.uint(sqliteEventMask), (*[0]byte)(unsafe.Pointer(C.traceCallbackTrampoline)), unsafe.Pointer(c.db)) // Fourth arg is same as first: we are // passing the database connection handle as callback context. if rv != C.SQLITE_OK { return c.lastError() } return nil }