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 }