trickle-observe 0.2.93 → 0.2.95

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Database query observer — patches popular database drivers to capture
3
+ * SQL queries, execution time, and result shapes.
4
+ *
5
+ * Currently supports:
6
+ * - pg (node-postgres) — used by Prisma, Knex, Sequelize, TypeORM
7
+ *
8
+ * Captured data is written to .trickle/queries.jsonl as:
9
+ * { query: "SELECT ...", params: [...], durationMs: 2.5, rowCount: 42, columns: [...] }
10
+ */
11
+ /**
12
+ * Patch pg (node-postgres) to capture queries.
13
+ * Called from observe-register when pg is required.
14
+ */
15
+ export declare function patchPg(pgModule: any, debug: boolean): void;
16
+ /**
17
+ * Patch mysql2 to capture queries.
18
+ */
19
+ export declare function patchMysql2(mysqlModule: any, debug: boolean): void;
20
+ /**
21
+ * Patch better-sqlite3 to capture queries.
22
+ */
23
+ export declare function patchBetterSqlite3(dbConstructor: any, debug: boolean): void;
@@ -0,0 +1,292 @@
1
+ "use strict";
2
+ /**
3
+ * Database query observer — patches popular database drivers to capture
4
+ * SQL queries, execution time, and result shapes.
5
+ *
6
+ * Currently supports:
7
+ * - pg (node-postgres) — used by Prisma, Knex, Sequelize, TypeORM
8
+ *
9
+ * Captured data is written to .trickle/queries.jsonl as:
10
+ * { query: "SELECT ...", params: [...], durationMs: 2.5, rowCount: 42, columns: [...] }
11
+ */
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || (function () {
29
+ var ownKeys = function(o) {
30
+ ownKeys = Object.getOwnPropertyNames || function (o) {
31
+ var ar = [];
32
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
+ return ar;
34
+ };
35
+ return ownKeys(o);
36
+ };
37
+ return function (mod) {
38
+ if (mod && mod.__esModule) return mod;
39
+ var result = {};
40
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
+ __setModuleDefault(result, mod);
42
+ return result;
43
+ };
44
+ })();
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.patchPg = patchPg;
47
+ exports.patchMysql2 = patchMysql2;
48
+ exports.patchBetterSqlite3 = patchBetterSqlite3;
49
+ const fs = __importStar(require("fs"));
50
+ const path = __importStar(require("path"));
51
+ let queriesFile = null;
52
+ let debugMode = false;
53
+ const MAX_QUERY_LENGTH = 500;
54
+ const MAX_QUERIES = 100;
55
+ let queryCount = 0;
56
+ function getQueriesFile() {
57
+ if (queriesFile)
58
+ return queriesFile;
59
+ const dir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
60
+ try {
61
+ fs.mkdirSync(dir, { recursive: true });
62
+ }
63
+ catch { }
64
+ queriesFile = path.join(dir, 'queries.jsonl');
65
+ // Clear previous
66
+ try {
67
+ fs.writeFileSync(queriesFile, '');
68
+ }
69
+ catch { }
70
+ return queriesFile;
71
+ }
72
+ function writeQuery(record) {
73
+ if (queryCount >= MAX_QUERIES)
74
+ return;
75
+ queryCount++;
76
+ try {
77
+ fs.appendFileSync(getQueriesFile(), JSON.stringify(record) + '\n');
78
+ }
79
+ catch { }
80
+ }
81
+ /**
82
+ * Patch pg (node-postgres) to capture queries.
83
+ * Called from observe-register when pg is required.
84
+ */
85
+ function patchPg(pgModule, debug) {
86
+ debugMode = debug;
87
+ // Patch Client.prototype.query
88
+ const Client = pgModule.Client;
89
+ if (!Client || !Client.prototype)
90
+ return;
91
+ const originalQuery = Client.prototype.query;
92
+ if (originalQuery.__trickle_patched)
93
+ return;
94
+ Client.prototype.query = function patchedQuery(...args) {
95
+ const startTime = performance.now();
96
+ // Extract query text and params
97
+ let queryText = '';
98
+ let params;
99
+ if (typeof args[0] === 'string') {
100
+ queryText = args[0];
101
+ params = Array.isArray(args[1]) ? args[1] : undefined;
102
+ }
103
+ else if (args[0] && typeof args[0] === 'object' && args[0].text) {
104
+ queryText = args[0].text;
105
+ params = args[0].values;
106
+ }
107
+ const truncatedQuery = queryText.length > MAX_QUERY_LENGTH
108
+ ? queryText.substring(0, MAX_QUERY_LENGTH) + '...'
109
+ : queryText;
110
+ // Call original
111
+ const result = originalQuery.apply(this, args);
112
+ // Handle promise-based queries
113
+ if (result && typeof result.then === 'function') {
114
+ return result.then((res) => {
115
+ const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
116
+ const columns = res.fields?.map((f) => f.name) || [];
117
+ writeQuery({
118
+ kind: 'query',
119
+ query: truncatedQuery,
120
+ params: params?.slice(0, 5),
121
+ durationMs,
122
+ rowCount: res.rowCount || 0,
123
+ columns: columns.length > 0 ? columns : undefined,
124
+ timestamp: Date.now(),
125
+ });
126
+ if (debugMode) {
127
+ console.log(`[trickle/db] ${truncatedQuery.substring(0, 60)}... (${durationMs}ms, ${res.rowCount} rows)`);
128
+ }
129
+ return res;
130
+ }, (err) => {
131
+ const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
132
+ writeQuery({
133
+ kind: 'query',
134
+ query: truncatedQuery,
135
+ params: params?.slice(0, 5),
136
+ durationMs,
137
+ rowCount: 0,
138
+ error: err.message?.substring(0, 200),
139
+ timestamp: Date.now(),
140
+ });
141
+ throw err;
142
+ });
143
+ }
144
+ return result;
145
+ };
146
+ Client.prototype.query.__trickle_patched = true;
147
+ // Also patch Pool if available
148
+ if (pgModule.Pool) {
149
+ const Pool = pgModule.Pool;
150
+ const origPoolQuery = Pool.prototype.query;
151
+ if (origPoolQuery && !origPoolQuery.__trickle_patched) {
152
+ Pool.prototype.query = function patchedPoolQuery(...args) {
153
+ const startTime = performance.now();
154
+ let queryText = '';
155
+ let params;
156
+ if (typeof args[0] === 'string') {
157
+ queryText = args[0];
158
+ params = Array.isArray(args[1]) ? args[1] : undefined;
159
+ }
160
+ else if (args[0] && typeof args[0] === 'object' && args[0].text) {
161
+ queryText = args[0].text;
162
+ params = args[0].values;
163
+ }
164
+ const truncatedQuery = queryText.length > MAX_QUERY_LENGTH
165
+ ? queryText.substring(0, MAX_QUERY_LENGTH) + '...'
166
+ : queryText;
167
+ const result = origPoolQuery.apply(this, args);
168
+ if (result && typeof result.then === 'function') {
169
+ return result.then((res) => {
170
+ const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
171
+ writeQuery({
172
+ kind: 'query',
173
+ query: truncatedQuery,
174
+ params: params?.slice(0, 5),
175
+ durationMs,
176
+ rowCount: res.rowCount || 0,
177
+ columns: res.fields?.map((f) => f.name),
178
+ timestamp: Date.now(),
179
+ });
180
+ return res;
181
+ }, (err) => {
182
+ writeQuery({
183
+ kind: 'query',
184
+ query: truncatedQuery,
185
+ durationMs: Math.round((performance.now() - startTime) * 100) / 100,
186
+ rowCount: 0,
187
+ error: err.message?.substring(0, 200),
188
+ timestamp: Date.now(),
189
+ });
190
+ throw err;
191
+ });
192
+ }
193
+ return result;
194
+ };
195
+ Pool.prototype.query.__trickle_patched = true;
196
+ }
197
+ }
198
+ if (debugMode) {
199
+ console.log('[trickle/db] PostgreSQL query tracing enabled');
200
+ }
201
+ }
202
+ /**
203
+ * Patch mysql2 to capture queries.
204
+ */
205
+ function patchMysql2(mysqlModule, debug) {
206
+ debugMode = debug;
207
+ // Patch Connection.prototype.query and .execute
208
+ const Connection = mysqlModule.Connection;
209
+ if (!Connection || !Connection.prototype)
210
+ return;
211
+ for (const method of ['query', 'execute']) {
212
+ const original = Connection.prototype[method];
213
+ if (!original || original.__trickle_patched)
214
+ continue;
215
+ Connection.prototype[method] = function patchedMethod(...args) {
216
+ const startTime = performance.now();
217
+ let queryText = typeof args[0] === 'string' ? args[0] : args[0]?.sql || '';
218
+ const truncated = queryText.length > MAX_QUERY_LENGTH ? queryText.substring(0, MAX_QUERY_LENGTH) + '...' : queryText;
219
+ const result = original.apply(this, args);
220
+ if (result && typeof result.then === 'function') {
221
+ return result.then((res) => {
222
+ const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
223
+ const rows = Array.isArray(res) ? res[0] : res;
224
+ writeQuery({
225
+ kind: 'query', query: truncated, durationMs,
226
+ rowCount: Array.isArray(rows) ? rows.length : 0,
227
+ columns: Array.isArray(rows) && rows[0] ? Object.keys(rows[0]) : undefined,
228
+ timestamp: Date.now(),
229
+ });
230
+ return res;
231
+ }, (err) => {
232
+ writeQuery({
233
+ kind: 'query', query: truncated,
234
+ durationMs: Math.round((performance.now() - startTime) * 100) / 100,
235
+ rowCount: 0, error: err.message?.substring(0, 200), timestamp: Date.now(),
236
+ });
237
+ throw err;
238
+ });
239
+ }
240
+ return result;
241
+ };
242
+ Connection.prototype[method].__trickle_patched = true;
243
+ }
244
+ if (debug)
245
+ console.log('[trickle/db] MySQL query tracing enabled');
246
+ }
247
+ /**
248
+ * Patch better-sqlite3 to capture queries.
249
+ */
250
+ function patchBetterSqlite3(dbConstructor, debug) {
251
+ debugMode = debug;
252
+ // better-sqlite3 returns a Database constructor — patch its prototype
253
+ const origPrepare = dbConstructor.prototype?.prepare;
254
+ if (!origPrepare || origPrepare.__trickle_patched)
255
+ return;
256
+ dbConstructor.prototype.prepare = function patchedPrepare(sql) {
257
+ const stmt = origPrepare.call(this, sql);
258
+ const truncated = sql.length > MAX_QUERY_LENGTH ? sql.substring(0, MAX_QUERY_LENGTH) + '...' : sql;
259
+ // Patch run, get, all methods on the statement
260
+ for (const method of ['run', 'get', 'all']) {
261
+ const origMethod = stmt[method];
262
+ if (!origMethod)
263
+ continue;
264
+ stmt[method] = function (...args) {
265
+ const startTime = performance.now();
266
+ try {
267
+ const result = origMethod.apply(this, args);
268
+ const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
269
+ const rowCount = method === 'all' ? (Array.isArray(result) ? result.length : 0)
270
+ : method === 'get' ? (result ? 1 : 0)
271
+ : (result?.changes || 0);
272
+ writeQuery({
273
+ kind: 'query', query: truncated, durationMs, rowCount, timestamp: Date.now(),
274
+ });
275
+ return result;
276
+ }
277
+ catch (err) {
278
+ writeQuery({
279
+ kind: 'query', query: truncated,
280
+ durationMs: Math.round((performance.now() - startTime) * 100) / 100,
281
+ rowCount: 0, error: err.message?.substring(0, 200), timestamp: Date.now(),
282
+ });
283
+ throw err;
284
+ }
285
+ };
286
+ }
287
+ return stmt;
288
+ };
289
+ dbConstructor.prototype.prepare.__trickle_patched = true;
290
+ if (debug)
291
+ console.log('[trickle/db] SQLite query tracing enabled');
292
+ }
@@ -1123,6 +1123,31 @@ if (enabled) {
1123
1123
  }
1124
1124
  catch { /* fall through to normal processing */ }
1125
1125
  }
1126
+ // ── Database auto-detection: patch database drivers to capture SQL queries ──
1127
+ if (request === 'pg' && !expressPatched.has('pg')) {
1128
+ expressPatched.add('pg');
1129
+ try {
1130
+ const { patchPg } = require(path_1.default.join(__dirname, 'db-observer.js'));
1131
+ patchPg(exports, debug);
1132
+ }
1133
+ catch { /* not critical */ }
1134
+ }
1135
+ if ((request === 'mysql2' || request === 'mysql2/promise') && !expressPatched.has('mysql2')) {
1136
+ expressPatched.add('mysql2');
1137
+ try {
1138
+ const { patchMysql2 } = require(path_1.default.join(__dirname, 'db-observer.js'));
1139
+ patchMysql2(exports, debug);
1140
+ }
1141
+ catch { /* not critical */ }
1142
+ }
1143
+ if (request === 'better-sqlite3' && !expressPatched.has('better-sqlite3')) {
1144
+ expressPatched.add('better-sqlite3');
1145
+ try {
1146
+ const { patchBetterSqlite3 } = require(path_1.default.join(__dirname, 'db-observer.js'));
1147
+ patchBetterSqlite3(exports, debug);
1148
+ }
1149
+ catch { /* not critical */ }
1150
+ }
1126
1151
  // Resolve to absolute path for dedup — do this FIRST since bundlers like
1127
1152
  // tsx/esbuild may use path aliases (e.g., @config/env) that don't start
1128
1153
  // with './' or '/'. We need the resolved path to decide if it's user code.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-observe",
3
- "version": "0.2.93",
3
+ "version": "0.2.95",
4
4
  "description": "Runtime type observability for JavaScript applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Database query observer — patches popular database drivers to capture
3
+ * SQL queries, execution time, and result shapes.
4
+ *
5
+ * Currently supports:
6
+ * - pg (node-postgres) — used by Prisma, Knex, Sequelize, TypeORM
7
+ *
8
+ * Captured data is written to .trickle/queries.jsonl as:
9
+ * { query: "SELECT ...", params: [...], durationMs: 2.5, rowCount: 42, columns: [...] }
10
+ */
11
+
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+
15
+ interface QueryRecord {
16
+ kind: 'query';
17
+ query: string;
18
+ params?: unknown[];
19
+ durationMs: number;
20
+ rowCount: number;
21
+ columns?: string[];
22
+ error?: string;
23
+ timestamp: number;
24
+ }
25
+
26
+ let queriesFile: string | null = null;
27
+ let debugMode = false;
28
+ const MAX_QUERY_LENGTH = 500;
29
+ const MAX_QUERIES = 100;
30
+ let queryCount = 0;
31
+
32
+ function getQueriesFile(): string {
33
+ if (queriesFile) return queriesFile;
34
+ const dir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
35
+ try { fs.mkdirSync(dir, { recursive: true }); } catch {}
36
+ queriesFile = path.join(dir, 'queries.jsonl');
37
+ // Clear previous
38
+ try { fs.writeFileSync(queriesFile, ''); } catch {}
39
+ return queriesFile;
40
+ }
41
+
42
+ function writeQuery(record: QueryRecord): void {
43
+ if (queryCount >= MAX_QUERIES) return;
44
+ queryCount++;
45
+ try {
46
+ fs.appendFileSync(getQueriesFile(), JSON.stringify(record) + '\n');
47
+ } catch {}
48
+ }
49
+
50
+ /**
51
+ * Patch pg (node-postgres) to capture queries.
52
+ * Called from observe-register when pg is required.
53
+ */
54
+ export function patchPg(pgModule: any, debug: boolean): void {
55
+ debugMode = debug;
56
+
57
+ // Patch Client.prototype.query
58
+ const Client = pgModule.Client;
59
+ if (!Client || !Client.prototype) return;
60
+
61
+ const originalQuery = Client.prototype.query;
62
+ if ((originalQuery as any).__trickle_patched) return;
63
+
64
+ Client.prototype.query = function patchedQuery(...args: any[]): any {
65
+ const startTime = performance.now();
66
+
67
+ // Extract query text and params
68
+ let queryText = '';
69
+ let params: unknown[] | undefined;
70
+ if (typeof args[0] === 'string') {
71
+ queryText = args[0];
72
+ params = Array.isArray(args[1]) ? args[1] : undefined;
73
+ } else if (args[0] && typeof args[0] === 'object' && args[0].text) {
74
+ queryText = args[0].text;
75
+ params = args[0].values;
76
+ }
77
+
78
+ const truncatedQuery = queryText.length > MAX_QUERY_LENGTH
79
+ ? queryText.substring(0, MAX_QUERY_LENGTH) + '...'
80
+ : queryText;
81
+
82
+ // Call original
83
+ const result = originalQuery.apply(this, args);
84
+
85
+ // Handle promise-based queries
86
+ if (result && typeof result.then === 'function') {
87
+ return result.then(
88
+ (res: any) => {
89
+ const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
90
+ const columns = res.fields?.map((f: any) => f.name) || [];
91
+ writeQuery({
92
+ kind: 'query',
93
+ query: truncatedQuery,
94
+ params: params?.slice(0, 5),
95
+ durationMs,
96
+ rowCount: res.rowCount || 0,
97
+ columns: columns.length > 0 ? columns : undefined,
98
+ timestamp: Date.now(),
99
+ });
100
+ if (debugMode) {
101
+ console.log(`[trickle/db] ${truncatedQuery.substring(0, 60)}... (${durationMs}ms, ${res.rowCount} rows)`);
102
+ }
103
+ return res;
104
+ },
105
+ (err: any) => {
106
+ const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
107
+ writeQuery({
108
+ kind: 'query',
109
+ query: truncatedQuery,
110
+ params: params?.slice(0, 5),
111
+ durationMs,
112
+ rowCount: 0,
113
+ error: err.message?.substring(0, 200),
114
+ timestamp: Date.now(),
115
+ });
116
+ throw err;
117
+ },
118
+ );
119
+ }
120
+
121
+ return result;
122
+ };
123
+
124
+ (Client.prototype.query as any).__trickle_patched = true;
125
+
126
+ // Also patch Pool if available
127
+ if (pgModule.Pool) {
128
+ const Pool = pgModule.Pool;
129
+ const origPoolQuery = Pool.prototype.query;
130
+ if (origPoolQuery && !(origPoolQuery as any).__trickle_patched) {
131
+ Pool.prototype.query = function patchedPoolQuery(...args: any[]): any {
132
+ const startTime = performance.now();
133
+ let queryText = '';
134
+ let params: unknown[] | undefined;
135
+ if (typeof args[0] === 'string') {
136
+ queryText = args[0];
137
+ params = Array.isArray(args[1]) ? args[1] : undefined;
138
+ } else if (args[0] && typeof args[0] === 'object' && args[0].text) {
139
+ queryText = args[0].text;
140
+ params = args[0].values;
141
+ }
142
+
143
+ const truncatedQuery = queryText.length > MAX_QUERY_LENGTH
144
+ ? queryText.substring(0, MAX_QUERY_LENGTH) + '...'
145
+ : queryText;
146
+
147
+ const result = origPoolQuery.apply(this, args);
148
+ if (result && typeof result.then === 'function') {
149
+ return result.then(
150
+ (res: any) => {
151
+ const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
152
+ writeQuery({
153
+ kind: 'query',
154
+ query: truncatedQuery,
155
+ params: params?.slice(0, 5),
156
+ durationMs,
157
+ rowCount: res.rowCount || 0,
158
+ columns: res.fields?.map((f: any) => f.name),
159
+ timestamp: Date.now(),
160
+ });
161
+ return res;
162
+ },
163
+ (err: any) => {
164
+ writeQuery({
165
+ kind: 'query',
166
+ query: truncatedQuery,
167
+ durationMs: Math.round((performance.now() - startTime) * 100) / 100,
168
+ rowCount: 0,
169
+ error: err.message?.substring(0, 200),
170
+ timestamp: Date.now(),
171
+ });
172
+ throw err;
173
+ },
174
+ );
175
+ }
176
+ return result;
177
+ };
178
+ (Pool.prototype.query as any).__trickle_patched = true;
179
+ }
180
+ }
181
+
182
+ if (debugMode) {
183
+ console.log('[trickle/db] PostgreSQL query tracing enabled');
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Patch mysql2 to capture queries.
189
+ */
190
+ export function patchMysql2(mysqlModule: any, debug: boolean): void {
191
+ debugMode = debug;
192
+
193
+ // Patch Connection.prototype.query and .execute
194
+ const Connection = mysqlModule.Connection;
195
+ if (!Connection || !Connection.prototype) return;
196
+
197
+ for (const method of ['query', 'execute'] as const) {
198
+ const original = Connection.prototype[method];
199
+ if (!original || (original as any).__trickle_patched) continue;
200
+
201
+ Connection.prototype[method] = function patchedMethod(...args: any[]): any {
202
+ const startTime = performance.now();
203
+ let queryText = typeof args[0] === 'string' ? args[0] : args[0]?.sql || '';
204
+ const truncated = queryText.length > MAX_QUERY_LENGTH ? queryText.substring(0, MAX_QUERY_LENGTH) + '...' : queryText;
205
+
206
+ const result = original.apply(this, args);
207
+ if (result && typeof result.then === 'function') {
208
+ return result.then(
209
+ (res: any) => {
210
+ const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
211
+ const rows = Array.isArray(res) ? res[0] : res;
212
+ writeQuery({
213
+ kind: 'query', query: truncated, durationMs,
214
+ rowCount: Array.isArray(rows) ? rows.length : 0,
215
+ columns: Array.isArray(rows) && rows[0] ? Object.keys(rows[0]) : undefined,
216
+ timestamp: Date.now(),
217
+ });
218
+ return res;
219
+ },
220
+ (err: any) => {
221
+ writeQuery({
222
+ kind: 'query', query: truncated,
223
+ durationMs: Math.round((performance.now() - startTime) * 100) / 100,
224
+ rowCount: 0, error: err.message?.substring(0, 200), timestamp: Date.now(),
225
+ });
226
+ throw err;
227
+ },
228
+ );
229
+ }
230
+ return result;
231
+ };
232
+ (Connection.prototype[method] as any).__trickle_patched = true;
233
+ }
234
+
235
+ if (debug) console.log('[trickle/db] MySQL query tracing enabled');
236
+ }
237
+
238
+ /**
239
+ * Patch better-sqlite3 to capture queries.
240
+ */
241
+ export function patchBetterSqlite3(dbConstructor: any, debug: boolean): void {
242
+ debugMode = debug;
243
+
244
+ // better-sqlite3 returns a Database constructor — patch its prototype
245
+ const origPrepare = dbConstructor.prototype?.prepare;
246
+ if (!origPrepare || (origPrepare as any).__trickle_patched) return;
247
+
248
+ dbConstructor.prototype.prepare = function patchedPrepare(sql: string): any {
249
+ const stmt = origPrepare.call(this, sql);
250
+ const truncated = sql.length > MAX_QUERY_LENGTH ? sql.substring(0, MAX_QUERY_LENGTH) + '...' : sql;
251
+
252
+ // Patch run, get, all methods on the statement
253
+ for (const method of ['run', 'get', 'all'] as const) {
254
+ const origMethod = stmt[method];
255
+ if (!origMethod) continue;
256
+ stmt[method] = function (...args: any[]): any {
257
+ const startTime = performance.now();
258
+ try {
259
+ const result = origMethod.apply(this, args);
260
+ const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
261
+ const rowCount = method === 'all' ? (Array.isArray(result) ? result.length : 0)
262
+ : method === 'get' ? (result ? 1 : 0)
263
+ : (result?.changes || 0);
264
+ writeQuery({
265
+ kind: 'query', query: truncated, durationMs, rowCount, timestamp: Date.now(),
266
+ });
267
+ return result;
268
+ } catch (err: any) {
269
+ writeQuery({
270
+ kind: 'query', query: truncated,
271
+ durationMs: Math.round((performance.now() - startTime) * 100) / 100,
272
+ rowCount: 0, error: err.message?.substring(0, 200), timestamp: Date.now(),
273
+ });
274
+ throw err;
275
+ }
276
+ };
277
+ }
278
+ return stmt;
279
+ };
280
+ (dbConstructor.prototype.prepare as any).__trickle_patched = true;
281
+
282
+ if (debug) console.log('[trickle/db] SQLite query tracing enabled');
283
+ }
@@ -1113,6 +1113,29 @@ if (enabled) {
1113
1113
  } catch { /* fall through to normal processing */ }
1114
1114
  }
1115
1115
 
1116
+ // ── Database auto-detection: patch database drivers to capture SQL queries ──
1117
+ if (request === 'pg' && !expressPatched.has('pg')) {
1118
+ expressPatched.add('pg');
1119
+ try {
1120
+ const { patchPg } = require(path.join(__dirname, 'db-observer.js'));
1121
+ patchPg(exports, debug);
1122
+ } catch { /* not critical */ }
1123
+ }
1124
+ if ((request === 'mysql2' || request === 'mysql2/promise') && !expressPatched.has('mysql2')) {
1125
+ expressPatched.add('mysql2');
1126
+ try {
1127
+ const { patchMysql2 } = require(path.join(__dirname, 'db-observer.js'));
1128
+ patchMysql2(exports, debug);
1129
+ } catch { /* not critical */ }
1130
+ }
1131
+ if (request === 'better-sqlite3' && !expressPatched.has('better-sqlite3')) {
1132
+ expressPatched.add('better-sqlite3');
1133
+ try {
1134
+ const { patchBetterSqlite3 } = require(path.join(__dirname, 'db-observer.js'));
1135
+ patchBetterSqlite3(exports, debug);
1136
+ } catch { /* not critical */ }
1137
+ }
1138
+
1116
1139
  // Resolve to absolute path for dedup — do this FIRST since bundlers like
1117
1140
  // tsx/esbuild may use path aliases (e.g., @config/env) that don't start
1118
1141
  // with './' or '/'. We need the resolved path to decide if it's user code.