mirror of https://github.com/gogits/gogs.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
415 lines
12 KiB
415 lines
12 KiB
// 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*); |
|
int traceCallbackTrampoline(unsigned int 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 C.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 C.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) C.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 C.int(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 := sqlite3_create_function(c.db, cname, C.int(stepNArgs), C.int(opts), newHandle(c, &ai), nil, C.stepTrampoline, 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 := uint(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 |
|
}
|
|
|