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