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