gemini-search

A simple search engine for Geminispace
git clone git://git.laack.co/gemini-search.git
Log | Files | Refs | README

sqlite3_trace.go (8370B)


      1 // Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
      2 //
      3 // Use of this source code is governed by an MIT-style
      4 // license that can be found in the LICENSE file.
      5 
      6 //go:build sqlite_trace || trace
      7 // +build sqlite_trace trace
      8 
      9 package sqlite3
     10 
     11 /*
     12 #ifndef USE_LIBSQLITE3
     13 #include "sqlite3-binding.h"
     14 #else
     15 #include <sqlite3.h>
     16 #endif
     17 #include <stdlib.h>
     18 
     19 int traceCallbackTrampoline(unsigned int traceEventCode, void *ctx, void *p, void *x);
     20 */
     21 import "C"
     22 
     23 import (
     24 	"fmt"
     25 	"strings"
     26 	"sync"
     27 	"unsafe"
     28 )
     29 
     30 // Trace... constants identify the possible events causing callback invocation.
     31 // Values are same as the corresponding SQLite Trace Event Codes.
     32 const (
     33 	TraceStmt    = uint32(C.SQLITE_TRACE_STMT)
     34 	TraceProfile = uint32(C.SQLITE_TRACE_PROFILE)
     35 	TraceRow     = uint32(C.SQLITE_TRACE_ROW)
     36 	TraceClose   = uint32(C.SQLITE_TRACE_CLOSE)
     37 )
     38 
     39 type TraceInfo struct {
     40 	// Pack together the shorter fields, to keep the struct smaller.
     41 	// On a 64-bit machine there would be padding
     42 	// between EventCode and ConnHandle; having AutoCommit here is "free":
     43 	EventCode  uint32
     44 	AutoCommit bool
     45 	ConnHandle uintptr
     46 
     47 	// Usually filled, unless EventCode = TraceClose = SQLITE_TRACE_CLOSE:
     48 	// identifier for a prepared statement:
     49 	StmtHandle uintptr
     50 
     51 	// Two strings filled when EventCode = TraceStmt = SQLITE_TRACE_STMT:
     52 	// (1) either the unexpanded SQL text of the prepared statement, or
     53 	//     an SQL comment that indicates the invocation of a trigger;
     54 	// (2) expanded SQL, if requested and if (1) is not an SQL comment.
     55 	StmtOrTrigger string
     56 	ExpandedSQL   string // only if requested (TraceConfig.WantExpandedSQL = true)
     57 
     58 	// filled when EventCode = TraceProfile = SQLITE_TRACE_PROFILE:
     59 	// estimated number of nanoseconds that the prepared statement took to run:
     60 	RunTimeNanosec int64
     61 
     62 	DBError Error
     63 }
     64 
     65 // TraceUserCallback gives the signature for a trace function
     66 // provided by the user (Go application programmer).
     67 // SQLite 3.14 documentation (as of September 2, 2016)
     68 // for SQL Trace Hook = sqlite3_trace_v2():
     69 // The integer return value from the callback is currently ignored,
     70 // though this may change in future releases. Callback implementations
     71 // should return zero to ensure future compatibility.
     72 type TraceUserCallback func(TraceInfo) int
     73 
     74 type TraceConfig struct {
     75 	Callback        TraceUserCallback
     76 	EventMask       uint32
     77 	WantExpandedSQL bool
     78 }
     79 
     80 func fillDBError(dbErr *Error, db *C.sqlite3) {
     81 	// See SQLiteConn.lastError(), in file 'sqlite3.go' at the time of writing (Sept 5, 2016)
     82 	dbErr.Code = ErrNo(C.sqlite3_errcode(db))
     83 	dbErr.ExtendedCode = ErrNoExtended(C.sqlite3_extended_errcode(db))
     84 	dbErr.err = C.GoString(C.sqlite3_errmsg(db))
     85 }
     86 
     87 func fillExpandedSQL(info *TraceInfo, db *C.sqlite3, pStmt unsafe.Pointer) {
     88 	if pStmt == nil {
     89 		panic("No SQLite statement pointer in P arg of trace_v2 callback")
     90 	}
     91 
     92 	expSQLiteCStr := C.sqlite3_expanded_sql((*C.sqlite3_stmt)(pStmt))
     93 	defer C.sqlite3_free(unsafe.Pointer(expSQLiteCStr))
     94 	if expSQLiteCStr == nil {
     95 		fillDBError(&info.DBError, db)
     96 		return
     97 	}
     98 	info.ExpandedSQL = C.GoString(expSQLiteCStr)
     99 }
    100 
    101 //export traceCallbackTrampoline
    102 func traceCallbackTrampoline(
    103 	traceEventCode C.uint,
    104 	// Parameter named 'C' in SQLite docs = Context given at registration:
    105 	ctx unsafe.Pointer,
    106 	// Parameter named 'P' in SQLite docs (Primary event data?):
    107 	p unsafe.Pointer,
    108 	// Parameter named 'X' in SQLite docs (eXtra event data?):
    109 	xValue unsafe.Pointer) C.int {
    110 
    111 	eventCode := uint32(traceEventCode)
    112 
    113 	if ctx == nil {
    114 		panic(fmt.Sprintf("No context (ev 0x%x)", traceEventCode))
    115 	}
    116 
    117 	contextDB := (*C.sqlite3)(ctx)
    118 	connHandle := uintptr(ctx)
    119 
    120 	var traceConf TraceConfig
    121 	var found bool
    122 	if eventCode == TraceClose {
    123 		// clean up traceMap: 'pop' means get and delete
    124 		traceConf, found = popTraceMapping(connHandle)
    125 	} else {
    126 		traceConf, found = lookupTraceMapping(connHandle)
    127 	}
    128 
    129 	if !found {
    130 		panic(fmt.Sprintf("Mapping not found for handle 0x%x (ev 0x%x)",
    131 			connHandle, eventCode))
    132 	}
    133 
    134 	var info TraceInfo
    135 
    136 	info.EventCode = eventCode
    137 	info.AutoCommit = (int(C.sqlite3_get_autocommit(contextDB)) != 0)
    138 	info.ConnHandle = connHandle
    139 
    140 	switch eventCode {
    141 	case TraceStmt:
    142 		info.StmtHandle = uintptr(p)
    143 
    144 		var xStr string
    145 		if xValue != nil {
    146 			xStr = C.GoString((*C.char)(xValue))
    147 		}
    148 		info.StmtOrTrigger = xStr
    149 		if !strings.HasPrefix(xStr, "--") {
    150 			// Not SQL comment, therefore the current event
    151 			// is not related to a trigger.
    152 			// The user might want to receive the expanded SQL;
    153 			// let's check:
    154 			if traceConf.WantExpandedSQL {
    155 				fillExpandedSQL(&info, contextDB, p)
    156 			}
    157 		}
    158 
    159 	case TraceProfile:
    160 		info.StmtHandle = uintptr(p)
    161 
    162 		if xValue == nil {
    163 			panic("NULL pointer in X arg of trace_v2 callback for SQLITE_TRACE_PROFILE event")
    164 		}
    165 
    166 		info.RunTimeNanosec = *(*int64)(xValue)
    167 
    168 		// sample the error //TODO: is it safe? is it useful?
    169 		fillDBError(&info.DBError, contextDB)
    170 
    171 	case TraceRow:
    172 		info.StmtHandle = uintptr(p)
    173 
    174 	case TraceClose:
    175 		handle := uintptr(p)
    176 		if handle != info.ConnHandle {
    177 			panic(fmt.Sprintf("Different conn handle 0x%x (expected 0x%x) in SQLITE_TRACE_CLOSE event.",
    178 				handle, info.ConnHandle))
    179 		}
    180 
    181 	default:
    182 		// Pass unsupported events to the user callback (if configured);
    183 		// let the user callback decide whether to panic or ignore them.
    184 	}
    185 
    186 	// Do not execute user callback when the event was not requested by user!
    187 	// Remember that the Close event is always selected when
    188 	// registering this callback trampoline with SQLite --- for cleanup.
    189 	// In the future there may be more events forced to "selected" in SQLite
    190 	// for the driver's needs.
    191 	if traceConf.EventMask&eventCode == 0 {
    192 		return 0
    193 	}
    194 
    195 	r := 0
    196 	if traceConf.Callback != nil {
    197 		r = traceConf.Callback(info)
    198 	}
    199 	return C.int(r)
    200 }
    201 
    202 type traceMapEntry struct {
    203 	config TraceConfig
    204 }
    205 
    206 var traceMapLock sync.Mutex
    207 var traceMap = make(map[uintptr]traceMapEntry)
    208 
    209 func addTraceMapping(connHandle uintptr, traceConf TraceConfig) {
    210 	traceMapLock.Lock()
    211 	defer traceMapLock.Unlock()
    212 
    213 	oldEntryCopy, found := traceMap[connHandle]
    214 	if found {
    215 		panic(fmt.Sprintf("Adding trace config %v: handle 0x%x already registered (%v).",
    216 			traceConf, connHandle, oldEntryCopy.config))
    217 	}
    218 	traceMap[connHandle] = traceMapEntry{config: traceConf}
    219 }
    220 
    221 func lookupTraceMapping(connHandle uintptr) (TraceConfig, bool) {
    222 	traceMapLock.Lock()
    223 	defer traceMapLock.Unlock()
    224 
    225 	entryCopy, found := traceMap[connHandle]
    226 	return entryCopy.config, found
    227 }
    228 
    229 // 'pop' = get and delete from map before returning the value to the caller
    230 func popTraceMapping(connHandle uintptr) (TraceConfig, bool) {
    231 	traceMapLock.Lock()
    232 	defer traceMapLock.Unlock()
    233 
    234 	entryCopy, found := traceMap[connHandle]
    235 	if found {
    236 		delete(traceMap, connHandle)
    237 	}
    238 	return entryCopy.config, found
    239 }
    240 
    241 // SetTrace installs or removes the trace callback for the given database connection.
    242 // It's not named 'RegisterTrace' because only one callback can be kept and called.
    243 // Calling SetTrace a second time on same database connection
    244 // overrides (cancels) any prior callback and all its settings:
    245 // event mask, etc.
    246 func (c *SQLiteConn) SetTrace(requested *TraceConfig) error {
    247 	connHandle := uintptr(unsafe.Pointer(c.db))
    248 
    249 	_, _ = popTraceMapping(connHandle)
    250 
    251 	if requested == nil {
    252 		// The traceMap entry was deleted already by popTraceMapping():
    253 		// can disable all events now, no need to watch for TraceClose.
    254 		err := c.setSQLiteTrace(0)
    255 		return err
    256 	}
    257 
    258 	reqCopy := *requested
    259 
    260 	// Disable potentially expensive operations
    261 	// if their result will not be used. We are doing this
    262 	// just in case the caller provided nonsensical input.
    263 	if reqCopy.EventMask&TraceStmt == 0 {
    264 		reqCopy.WantExpandedSQL = false
    265 	}
    266 
    267 	addTraceMapping(connHandle, reqCopy)
    268 
    269 	// The callback trampoline function does cleanup on Close event,
    270 	// regardless of the presence or absence of the user callback.
    271 	// Therefore it needs the Close event to be selected:
    272 	actualEventMask := uint(reqCopy.EventMask | TraceClose)
    273 	err := c.setSQLiteTrace(actualEventMask)
    274 	return err
    275 }
    276 
    277 func (c *SQLiteConn) setSQLiteTrace(sqliteEventMask uint) error {
    278 	rv := C.sqlite3_trace_v2(c.db,
    279 		C.uint(sqliteEventMask),
    280 		(*[0]byte)(unsafe.Pointer(C.traceCallbackTrampoline)),
    281 		unsafe.Pointer(c.db)) // Fourth arg is same as first: we are
    282 	// passing the database connection handle as callback context.
    283 
    284 	if rv != C.SQLITE_OK {
    285 		return c.lastError()
    286 	}
    287 	return nil
    288 }