@kysera/debug 0.7.2 → 0.7.4

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.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Kysely } from 'kysely';
2
- import { KyseraLogger } from '@kysera/core';
2
+ import { KyseraLogger, QueryMetrics } from '@kysera/core';
3
+ export { QueryMetrics } from '@kysera/core';
3
4
 
4
5
  /**
5
6
  * Debug plugin for Kysely.
@@ -7,19 +8,6 @@ import { KyseraLogger } from '@kysera/core';
7
8
  * @module @kysera/debug
8
9
  */
9
10
 
10
- /**
11
- * Query metrics data.
12
- */
13
- interface QueryMetrics {
14
- /** SQL query string */
15
- sql: string;
16
- /** Query parameters */
17
- params?: unknown[];
18
- /** Query execution duration in milliseconds */
19
- duration: number;
20
- /** Timestamp when query was executed */
21
- timestamp: number;
22
- }
23
11
  /**
24
12
  * Options for debug plugin.
25
13
  */
@@ -84,7 +72,7 @@ interface DebugDatabase<DB> extends Kysely<DB> {
84
72
  *
85
73
  * // Get collected metrics
86
74
  * const metrics = debugDb.getMetrics();
87
- * console.log(`Total queries: ${metrics.length}`);
75
+ * console.log('Total queries: ' + metrics.length);
88
76
  * ```
89
77
  *
90
78
  * @example With custom options
@@ -97,7 +85,7 @@ interface DebugDatabase<DB> extends Kysely<DB> {
97
85
  * slowQueryThreshold: 50,
98
86
  * maxMetrics: 500,
99
87
  * onSlowQuery: (sql, duration) => {
100
- * alertService.notify(`Slow query: ${duration}ms`);
88
+ * alertService.notify('Slow query: ' + duration + 'ms');
101
89
  * },
102
90
  * });
103
91
  * ```
@@ -158,16 +146,15 @@ interface ProfilerOptions {
158
146
  *
159
147
  * // Get summary
160
148
  * const summary = profiler.getSummary();
161
- * console.log(`Total queries: ${summary.totalQueries}`);
162
- * console.log(`Average duration: ${summary.averageDuration.toFixed(2)}ms`);
149
+ * console.log('Total queries: ' + summary.totalQueries);
150
+ * console.log('Average duration: ' + summary.averageDuration.toFixed(2) + 'ms');
163
151
  *
164
152
  * // Clear recorded queries
165
153
  * profiler.clear();
166
154
  * ```
167
155
  */
168
156
  declare class QueryProfiler {
169
- private queries;
170
- private readonly maxQueries;
157
+ private readonly queriesBuffer;
171
158
  /**
172
159
  * Create a new query profiler.
173
160
  *
@@ -177,6 +164,9 @@ declare class QueryProfiler {
177
164
  /**
178
165
  * Record a query metric.
179
166
  *
167
+ * Uses O(1) circular buffer to maintain bounded memory usage.
168
+ * When the buffer is full, oldest entries are overwritten.
169
+ *
180
170
  * @param metric - Query metrics to record
181
171
  */
182
172
  record(metric: QueryMetrics): void;
@@ -208,6 +198,13 @@ declare class QueryProfiler {
208
198
  * Get the number of recorded queries.
209
199
  */
210
200
  get count(): number;
201
+ /**
202
+ * Get queries in chronological order.
203
+ * Handles circular buffer wrap-around correctly.
204
+ *
205
+ * @returns Array of queries in chronological order
206
+ */
207
+ private getOrderedQueries;
211
208
  }
212
209
 
213
210
  /**
@@ -294,4 +291,85 @@ declare function minifySQL(sql: string): string;
294
291
  */
295
292
  declare function highlightSQL(sql: string): string;
296
293
 
297
- export { type DebugDatabase, type DebugOptions, type ProfilerOptions, type ProfilerSummary, type QueryMetrics, QueryProfiler, formatSQL, formatSQLPretty, highlightSQL, minifySQL, withDebug };
294
+ /**
295
+ * Circular buffer utility for bounded memory usage.
296
+ *
297
+ * @module @kysera/debug
298
+ */
299
+ /**
300
+ * Circular buffer with O(1) operations.
301
+ *
302
+ * Maintains a fixed-size buffer where oldest entries are overwritten
303
+ * when capacity is reached. Provides efficient add and retrieval
304
+ * operations without reallocation.
305
+ *
306
+ * @template T - Type of items stored in buffer
307
+ *
308
+ * @example
309
+ * ```typescript
310
+ * const buffer = new CircularBuffer<number>(3);
311
+ *
312
+ * buffer.add(1);
313
+ * buffer.add(2);
314
+ * buffer.add(3);
315
+ * buffer.add(4); // Overwrites oldest (1)
316
+ *
317
+ * const items = buffer.getOrdered(); // [2, 3, 4]
318
+ * console.log(buffer.size); // 3
319
+ * ```
320
+ */
321
+ declare class CircularBuffer<T> {
322
+ private items;
323
+ private writeIndex;
324
+ private readonly maxSize;
325
+ /**
326
+ * Create a new circular buffer.
327
+ *
328
+ * @param maxSize - Maximum number of items to store
329
+ */
330
+ constructor(maxSize: number);
331
+ /**
332
+ * Add an item to the buffer.
333
+ *
334
+ * Uses O(1) operation. When buffer is full, overwrites oldest entry.
335
+ *
336
+ * @param item - Item to add
337
+ */
338
+ add(item: T): void;
339
+ /**
340
+ * Get all items in chronological order.
341
+ *
342
+ * Returns items from oldest to newest. Handles buffer wrap-around correctly.
343
+ *
344
+ * @returns Array of items in chronological order
345
+ */
346
+ getOrdered(): T[];
347
+ /**
348
+ * Get all items in storage order (not chronological).
349
+ *
350
+ * @returns Array of items in storage order
351
+ */
352
+ getRaw(): T[];
353
+ /**
354
+ * Clear all items from buffer.
355
+ */
356
+ clear(): void;
357
+ /**
358
+ * Get current number of items in buffer.
359
+ */
360
+ get size(): number;
361
+ /**
362
+ * Get maximum capacity of buffer.
363
+ */
364
+ get capacity(): number;
365
+ /**
366
+ * Check if buffer is full.
367
+ */
368
+ get isFull(): boolean;
369
+ /**
370
+ * Check if buffer is empty.
371
+ */
372
+ get isEmpty(): boolean;
373
+ }
374
+
375
+ export { CircularBuffer, type DebugDatabase, type DebugOptions, type ProfilerOptions, type ProfilerSummary, QueryProfiler, formatSQL, formatSQLPretty, highlightSQL, minifySQL, withDebug };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import {DefaultQueryCompiler}from'kysely';import {consoleLogger}from'@kysera/core';var n=class{metrics=[];queryData=new WeakMap;maxMetrics;logger;options;onSlowQuery;constructor(e={}){this.logger=e.logger??consoleLogger,this.maxMetrics=e.maxMetrics??1e3,this.onSlowQuery=e.onSlowQuery,this.options={logQuery:e.logQuery??true,logParams:e.logParams??false,slowQueryThreshold:e.slowQueryThreshold??100};}transformQuery(e){let r=performance.now(),s=new DefaultQueryCompiler().compileQuery(e.node,e.queryId);return this.queryData.set(e.queryId,{startTime:r,sql:s.sql,params:s.parameters}),e.node}transformResult(e){let r=this.queryData.get(e.queryId);if(r){let s=performance.now()-r.startTime;this.queryData.delete(e.queryId);let o={sql:r.sql,params:[...r.params],duration:s,timestamp:Date.now()};if(this.metrics.push(o),this.metrics.length>this.maxMetrics&&this.metrics.shift(),this.options.logQuery){let m=this.options.logParams?`[SQL] ${r.sql}
2
- [Params] ${JSON.stringify(r.params)}`:`[SQL] ${r.sql}`;this.logger.debug(m),this.logger.debug(`[Duration] ${s.toFixed(2)}ms`);}s>this.options.slowQueryThreshold&&(this.onSlowQuery?this.onSlowQuery(r.sql,s):this.logger.warn(`[SLOW QUERY] ${s.toFixed(2)}ms: ${r.sql}`));}return Promise.resolve(e.result)}getMetrics(){return [...this.metrics]}clearMetrics(){this.metrics=[];}};function p(i,e={}){let r=new n(e),t=i.withPlugin(r);return t.getMetrics=()=>r.getMetrics(),t.clearMetrics=()=>{r.clearMetrics();},t}var u=class{queries=[];maxQueries;constructor(e={}){this.maxQueries=e.maxQueries??1e3;}record(e){this.queries.push(e),this.queries.length>this.maxQueries&&this.queries.shift();}getSummary(){if(this.queries.length===0)return {totalQueries:0,totalDuration:0,averageDuration:0,slowestQuery:null,fastestQuery:null,queries:[]};let e=this.queries.reduce((t,s)=>t+s.duration,0),r=[...this.queries].sort((t,s)=>s.duration-t.duration);return {totalQueries:this.queries.length,totalDuration:e,averageDuration:e/this.queries.length,slowestQuery:r[0]??null,fastestQuery:r[r.length-1]??null,queries:[...this.queries]}}getSlowestQueries(e){return [...this.queries].sort((r,t)=>t.duration-r.duration).slice(0,e)}getSlowQueries(e){return this.queries.filter(r=>r.duration>e)}clear(){this.queries=[];}get count(){return this.queries.length}};function a(i){return i.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}var l=["SELECT","FROM","WHERE","JOIN","LEFT JOIN","RIGHT JOIN","INNER JOIN","OUTER JOIN","CROSS JOIN","ORDER BY","GROUP BY","HAVING","LIMIT","OFFSET","INSERT INTO","VALUES","UPDATE","SET","DELETE FROM","UNION","EXCEPT","INTERSECT","ON","AND","OR","RETURNING"];function g(i){let e=i;for(let r of l){let t=new RegExp(`(\\s+)(${a(r)})\\s+`,"gi");e=e.replace(t,`
3
- $2 `);}return e.trim()}function h(i,e=2){let r=g(i),t=" ".repeat(e),s=0;return r=r.replace(/\(/g,()=>(s++,`(
4
- `+t.repeat(s))),r=r.replace(/\)/g,()=>(s=Math.max(0,s-1),`
5
- `+t.repeat(s)+")")),r=r.replace(/\n\s*\n/g,`
6
- `),r.trim()}function Q(i){return i.replace(/\s+/g," ").replace(/\s*,\s*/g,", ").replace(/\s*\(\s*/g," (").replace(/\s*\)\s*/g,") ").trim()}function f(i){let e="\x1B[34m",r="\x1B[0m",t=i;for(let s of l){let o=new RegExp(`\\b(${a(s)})\\b`,"gi");t=t.replace(o,`${e}$1${r}`);}return t}export{u as QueryProfiler,g as formatSQL,h as formatSQLPretty,f as highlightSQL,Q as minifySQL,p as withDebug};//# sourceMappingURL=index.js.map
1
+ import {DefaultQueryCompiler}from'kysely';import {consoleLogger}from'@kysera/core';var o=class{items=[];writeIndex=0;maxSize;constructor(e){if(e<=0)throw new Error("CircularBuffer maxSize must be positive");this.maxSize=e;}add(e){this.items.length<this.maxSize?this.items.push(e):this.items[this.writeIndex%this.maxSize]=e,this.writeIndex++;}getOrdered(){if(this.items.length<this.maxSize)return [...this.items];let e=this.writeIndex%this.maxSize;return [...this.items.slice(e),...this.items.slice(0,e)]}getRaw(){return [...this.items]}clear(){this.items=[],this.writeIndex=0;}get size(){return this.items.length}get capacity(){return this.maxSize}get isFull(){return this.items.length>=this.maxSize}get isEmpty(){return this.items.length===0}};var u=class{metricsBuffer;queryData=new WeakMap;logger;options;onSlowQuery;constructor(e={}){this.logger=e.logger??consoleLogger,this.metricsBuffer=new o(e.maxMetrics??1e3),this.onSlowQuery=e.onSlowQuery,this.options={logQuery:e.logQuery??true,logParams:e.logParams??false,slowQueryThreshold:e.slowQueryThreshold??100};}transformQuery(e){let r=performance.now(),t=new DefaultQueryCompiler().compileQuery(e.node,e.queryId);return this.queryData.set(e.queryId,{startTime:r,sql:t.sql,params:t.parameters}),e.node}transformResult(e){let r=this.queryData.get(e.queryId);if(r){let t=performance.now()-r.startTime;this.queryData.delete(e.queryId);let n={sql:r.sql,params:[...r.params],duration:t,timestamp:Date.now()};if(this.metricsBuffer.add(n),this.options.logQuery){let c=this.options.logParams?"[SQL] "+r.sql+`
2
+ [Params] `+JSON.stringify(r.params):"[SQL] "+r.sql;this.logger.debug(c),this.logger.debug("[Duration] "+t.toFixed(2)+"ms");}t>this.options.slowQueryThreshold&&(this.onSlowQuery?this.onSlowQuery(r.sql,t):this.logger.warn("[SLOW QUERY] "+t.toFixed(2)+"ms: "+r.sql));}return Promise.resolve(e.result)}getMetrics(){return this.metricsBuffer.getOrdered()}clearMetrics(){this.metricsBuffer.clear();}};function p(s,e={}){let r=new u(e),i=s.withPlugin(r);return i.getMetrics=()=>r.getMetrics(),i.clearMetrics=()=>{r.clearMetrics();},i}var a=class{queriesBuffer;constructor(e={}){this.queriesBuffer=new o(e.maxQueries??1e3);}record(e){this.queriesBuffer.add(e);}getSummary(){let e=this.getOrderedQueries();if(e.length===0)return {totalQueries:0,totalDuration:0,averageDuration:0,slowestQuery:null,fastestQuery:null,queries:[]};let r=e.reduce((t,n)=>t+n.duration,0),i=[...e].sort((t,n)=>n.duration-t.duration);return {totalQueries:e.length,totalDuration:r,averageDuration:r/e.length,slowestQuery:i[0]??null,fastestQuery:i[i.length-1]??null,queries:e}}getSlowestQueries(e){return [...this.getOrderedQueries()].sort((r,i)=>i.duration-r.duration).slice(0,e)}getSlowQueries(e){return this.getOrderedQueries().filter(r=>r.duration>e)}clear(){this.queriesBuffer.clear();}get count(){return this.queriesBuffer.size}getOrderedQueries(){return this.queriesBuffer.getOrdered()}};function l(s){return s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}var m=["SELECT","FROM","WHERE","JOIN","LEFT JOIN","RIGHT JOIN","INNER JOIN","OUTER JOIN","CROSS JOIN","ORDER BY","GROUP BY","HAVING","LIMIT","OFFSET","INSERT INTO","VALUES","UPDATE","SET","DELETE FROM","UNION","EXCEPT","INTERSECT","ON","AND","OR","RETURNING"],d=m.map(s=>new RegExp(`(\\s+)(${l(s)})\\s+`,"gi")),h=m.map(s=>({regex:new RegExp(`\\b(${l(s)})\\b`,"gi"),keyword:s}));function g(s){let e=s;for(let r of d)r.lastIndex=0,e=e.replace(r,`
3
+ $2 `);return e.trim()}function Q(s,e=2){let r=g(s),i=" ".repeat(e),t=0;return r=r.replace(/\(/g,()=>(t++,`(
4
+ `+i.repeat(t))),r=r.replace(/\)/g,()=>(t=Math.max(0,t-1),`
5
+ `+i.repeat(t)+")")),r=r.replace(/\n\s*\n/g,`
6
+ `),r.trim()}function x(s){return s.replace(/\s+/g," ").replace(/\s*,\s*/g,", ").replace(/\s*\(\s*/g," (").replace(/\s*\)\s*/g,") ").trim()}function S(s){let e="\x1B[34m",r="\x1B[0m",i=s;for(let{regex:t}of h)t.lastIndex=0,i=i.replace(t,e+"$1"+r);return i}export{o as CircularBuffer,a as QueryProfiler,g as formatSQL,Q as formatSQLPretty,S as highlightSQL,x as minifySQL,p as withDebug};//# sourceMappingURL=index.js.map
7
7
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/plugin.ts","../src/profiler.ts","../src/format.ts"],"names":["DebugPlugin","options","consoleLogger","args","startTime","compiled","DefaultQueryCompiler","data","duration","metric","message","withDebug","db","plugin","debugDb","QueryProfiler","totalDuration","sum","q","sorted","a","b","count","thresholdMs","escapeRegex","str","SQL_KEYWORDS","formatSQL","sql","formatted","keyword","regex","formatSQLPretty","indentSize","indent","level","minifySQL","highlightSQL","BLUE","RESET","highlighted"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Debug plugin for Kysely.\n *\n * @module @kysera/debug\n */\n\nimport type {\n Kysely,\n PluginTransformQueryArgs,\n PluginTransformResultArgs,\n QueryResult,\n UnknownRow,\n KyselyPlugin,\n RootOperationNode,\n} from 'kysely';\nimport { DefaultQueryCompiler } from 'kysely';\nimport { consoleLogger, type KyseraLogger } from '@kysera/core';\n\n/**\n * Query metrics data.\n */\nexport interface QueryMetrics {\n /** SQL query string */\n sql: string;\n /** Query parameters */\n params?: unknown[];\n /** Query execution duration in milliseconds */\n duration: number;\n /** Timestamp when query was executed */\n timestamp: number;\n}\n\n/**\n * Options for debug plugin.\n */\nexport interface DebugOptions {\n /**\n * Log query SQL.\n * @default true\n */\n logQuery?: boolean;\n\n /**\n * Log query parameters.\n * @default false\n */\n logParams?: boolean;\n\n /**\n * Duration threshold (ms) to consider a query slow.\n * @default 100\n */\n slowQueryThreshold?: number;\n\n /**\n * Callback for slow queries.\n */\n onSlowQuery?: (sql: string, duration: number) => void;\n\n /**\n * Logger for debug messages.\n * @default consoleLogger\n */\n logger?: KyseraLogger;\n\n /**\n * Maximum number of metrics to keep in memory.\n * When limit is reached, oldest metrics are removed (circular buffer).\n * @default 1000\n */\n maxMetrics?: number;\n}\n\n/**\n * Internal query data for tracking execution.\n * @internal\n */\ninterface QueryData {\n startTime: number;\n sql: string;\n params: readonly unknown[];\n}\n\n/**\n * Debug plugin implementation.\n * @internal\n */\nclass DebugPlugin implements KyselyPlugin {\n private metrics: QueryMetrics[] = [];\n private queryData = new WeakMap<object, QueryData>();\n private readonly maxMetrics: number;\n private readonly logger: KyseraLogger;\n private readonly options: Required<Pick<DebugOptions, 'logQuery' | 'logParams' | 'slowQueryThreshold'>>;\n private readonly onSlowQuery: ((sql: string, duration: number) => void) | undefined;\n\n constructor(options: DebugOptions = {}) {\n this.logger = options.logger ?? consoleLogger;\n this.maxMetrics = options.maxMetrics ?? 1000;\n this.onSlowQuery = options.onSlowQuery;\n this.options = {\n logQuery: options.logQuery ?? true,\n logParams: options.logParams ?? false,\n slowQueryThreshold: options.slowQueryThreshold ?? 100,\n };\n }\n\n transformQuery(args: PluginTransformQueryArgs): RootOperationNode {\n const startTime = performance.now();\n\n // Compile the query to get SQL and parameters\n const compiler = new DefaultQueryCompiler();\n const compiled = compiler.compileQuery(args.node, args.queryId);\n\n // Store query data for later use in transformResult\n this.queryData.set(args.queryId, {\n startTime,\n sql: compiled.sql,\n params: compiled.parameters,\n });\n\n return args.node;\n }\n\n transformResult(args: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>> {\n const data = this.queryData.get(args.queryId);\n\n if (data) {\n const endTime = performance.now();\n const duration = endTime - data.startTime;\n this.queryData.delete(args.queryId);\n\n const metric: QueryMetrics = {\n sql: data.sql,\n params: [...data.params],\n duration,\n timestamp: Date.now(),\n };\n\n // Circular buffer: keep only last N metrics\n this.metrics.push(metric);\n if (this.metrics.length > this.maxMetrics) {\n this.metrics.shift();\n }\n\n if (this.options.logQuery) {\n const message = this.options.logParams\n ? `[SQL] ${data.sql}\\n[Params] ${JSON.stringify(data.params)}`\n : `[SQL] ${data.sql}`;\n this.logger.debug(message);\n this.logger.debug(`[Duration] ${duration.toFixed(2)}ms`);\n }\n\n // Check for slow query\n if (duration > this.options.slowQueryThreshold) {\n if (this.onSlowQuery) {\n this.onSlowQuery(data.sql, duration);\n } else {\n this.logger.warn(`[SLOW QUERY] ${duration.toFixed(2)}ms: ${data.sql}`);\n }\n }\n }\n\n return Promise.resolve(args.result);\n }\n\n getMetrics(): QueryMetrics[] {\n return [...this.metrics];\n }\n\n clearMetrics(): void {\n this.metrics = [];\n }\n}\n\n/**\n * Database with debug capabilities.\n */\nexport interface DebugDatabase<DB> extends Kysely<DB> {\n /** Get all collected query metrics */\n getMetrics(): QueryMetrics[];\n /** Clear all collected metrics */\n clearMetrics(): void;\n}\n\n/**\n * Wrap a Kysely database with debug capabilities.\n *\n * Adds query logging, metrics collection, and slow query detection.\n *\n * @param db - Kysely database instance\n * @param options - Debug options\n * @returns Database with debug capabilities\n *\n * @example Basic usage\n * ```typescript\n * import { withDebug } from '@kysera/debug';\n *\n * const debugDb = withDebug(db);\n *\n * // Queries are now logged and timed\n * await debugDb.selectFrom('users').selectAll().execute();\n *\n * // Get collected metrics\n * const metrics = debugDb.getMetrics();\n * console.log(`Total queries: ${metrics.length}`);\n * ```\n *\n * @example With custom options\n * ```typescript\n * import { withDebug } from '@kysera/debug';\n *\n * const debugDb = withDebug(db, {\n * logQuery: true,\n * logParams: true,\n * slowQueryThreshold: 50,\n * maxMetrics: 500,\n * onSlowQuery: (sql, duration) => {\n * alertService.notify(`Slow query: ${duration}ms`);\n * },\n * });\n * ```\n */\nexport function withDebug<DB>(\n db: Kysely<DB>,\n options: DebugOptions = {}\n): DebugDatabase<DB> {\n const plugin = new DebugPlugin(options);\n const debugDb = db.withPlugin(plugin) as DebugDatabase<DB>;\n\n // Attach metrics methods\n debugDb.getMetrics = (): QueryMetrics[] => plugin.getMetrics();\n debugDb.clearMetrics = (): void => {\n plugin.clearMetrics();\n };\n\n return debugDb;\n}\n","/**\n * Query profiler for performance analysis.\n *\n * @module @kysera/debug\n */\n\nimport type { QueryMetrics } from './plugin.js';\n\n/**\n * Query profiler summary.\n */\nexport interface ProfilerSummary {\n /** Total number of recorded queries */\n totalQueries: number;\n /** Sum of all query durations */\n totalDuration: number;\n /** Average query duration */\n averageDuration: number;\n /** Slowest recorded query */\n slowestQuery: QueryMetrics | null;\n /** Fastest recorded query */\n fastestQuery: QueryMetrics | null;\n /** All recorded queries */\n queries: QueryMetrics[];\n}\n\n/**\n * Options for QueryProfiler.\n */\nexport interface ProfilerOptions {\n /**\n * Maximum number of queries to keep in memory.\n * @default 1000\n */\n maxQueries?: number;\n}\n\n/**\n * Query profiler for collecting and analyzing query performance.\n *\n * Provides detailed statistics about query execution times\n * including average, min, max, and query counts.\n *\n * @example\n * ```typescript\n * import { QueryProfiler } from '@kysera/debug';\n *\n * const profiler = new QueryProfiler({ maxQueries: 500 });\n *\n * // Record queries manually\n * profiler.record({\n * sql: 'SELECT * FROM users',\n * duration: 10,\n * timestamp: Date.now(),\n * });\n *\n * // Get summary\n * const summary = profiler.getSummary();\n * console.log(`Total queries: ${summary.totalQueries}`);\n * console.log(`Average duration: ${summary.averageDuration.toFixed(2)}ms`);\n *\n * // Clear recorded queries\n * profiler.clear();\n * ```\n */\nexport class QueryProfiler {\n private queries: QueryMetrics[] = [];\n private readonly maxQueries: number;\n\n /**\n * Create a new query profiler.\n *\n * @param options - Profiler options\n */\n constructor(options: ProfilerOptions = {}) {\n this.maxQueries = options.maxQueries ?? 1000;\n }\n\n /**\n * Record a query metric.\n *\n * @param metric - Query metrics to record\n */\n record(metric: QueryMetrics): void {\n this.queries.push(metric);\n // Circular buffer: keep only last N queries\n if (this.queries.length > this.maxQueries) {\n this.queries.shift();\n }\n }\n\n /**\n * Get profiling summary.\n *\n * @returns Summary of all recorded queries\n */\n getSummary(): ProfilerSummary {\n if (this.queries.length === 0) {\n return {\n totalQueries: 0,\n totalDuration: 0,\n averageDuration: 0,\n slowestQuery: null,\n fastestQuery: null,\n queries: [],\n };\n }\n\n const totalDuration = this.queries.reduce((sum, q) => sum + q.duration, 0);\n const sorted = [...this.queries].sort((a, b) => b.duration - a.duration);\n\n return {\n totalQueries: this.queries.length,\n totalDuration,\n averageDuration: totalDuration / this.queries.length,\n slowestQuery: sorted[0] ?? null,\n fastestQuery: sorted[sorted.length - 1] ?? null,\n queries: [...this.queries],\n };\n }\n\n /**\n * Get the slowest N queries.\n *\n * @param count - Number of queries to return\n * @returns Array of slowest queries\n */\n getSlowestQueries(count: number): QueryMetrics[] {\n return [...this.queries]\n .sort((a, b) => b.duration - a.duration)\n .slice(0, count);\n }\n\n /**\n * Get queries slower than a threshold.\n *\n * @param thresholdMs - Duration threshold in milliseconds\n * @returns Array of slow queries\n */\n getSlowQueries(thresholdMs: number): QueryMetrics[] {\n return this.queries.filter((q) => q.duration > thresholdMs);\n }\n\n /**\n * Clear all recorded queries.\n */\n clear(): void {\n this.queries = [];\n }\n\n /**\n * Get the number of recorded queries.\n */\n get count(): number {\n return this.queries.length;\n }\n}\n","/**\n * SQL formatting utilities.\n *\n * @module @kysera/debug\n */\n\n/**\n * Escape special regex characters in a string.\n * @internal\n */\nfunction escapeRegex(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/**\n * SQL keywords that should start on a new line.\n */\nconst SQL_KEYWORDS = [\n 'SELECT',\n 'FROM',\n 'WHERE',\n 'JOIN',\n 'LEFT JOIN',\n 'RIGHT JOIN',\n 'INNER JOIN',\n 'OUTER JOIN',\n 'CROSS JOIN',\n 'ORDER BY',\n 'GROUP BY',\n 'HAVING',\n 'LIMIT',\n 'OFFSET',\n 'INSERT INTO',\n 'VALUES',\n 'UPDATE',\n 'SET',\n 'DELETE FROM',\n 'UNION',\n 'EXCEPT',\n 'INTERSECT',\n 'ON',\n 'AND',\n 'OR',\n 'RETURNING',\n];\n\n/**\n * Format SQL for better readability.\n *\n * Adds newlines before major SQL keywords to make\n * complex queries easier to read.\n *\n * @param sql - SQL string to format\n * @returns Formatted SQL string\n *\n * @example\n * ```typescript\n * import { formatSQL } from '@kysera/debug';\n *\n * const sql = 'SELECT id, name FROM users WHERE active = true ORDER BY name';\n * console.log(formatSQL(sql));\n * // SELECT id, name\n * // FROM users\n * // WHERE active = true\n * // ORDER BY name\n * ```\n */\nexport function formatSQL(sql: string): string {\n let formatted = sql;\n\n // Add newlines before SQL keywords\n for (const keyword of SQL_KEYWORDS) {\n const regex = new RegExp(`(\\\\s+)(${escapeRegex(keyword)})\\\\s+`, 'gi');\n formatted = formatted.replace(regex, `\\n$2 `);\n }\n\n return formatted.trim();\n}\n\n/**\n * Format SQL with indentation for nested queries.\n *\n * More advanced formatting with proper indentation\n * for subqueries and nested structures.\n *\n * @param sql - SQL string to format\n * @param indentSize - Number of spaces for indentation\n * @returns Formatted SQL string\n *\n * @example\n * ```typescript\n * import { formatSQLPretty } from '@kysera/debug';\n *\n * const sql = 'SELECT * FROM users WHERE id IN (SELECT user_id FROM orders)';\n * console.log(formatSQLPretty(sql));\n * ```\n */\nexport function formatSQLPretty(sql: string, indentSize = 2): string {\n let formatted = formatSQL(sql);\n const indent = ' '.repeat(indentSize);\n let level = 0;\n\n // Handle parentheses for subqueries\n formatted = formatted.replace(/\\(/g, () => {\n level++;\n return '(\\n' + indent.repeat(level);\n });\n\n formatted = formatted.replace(/\\)/g, () => {\n level = Math.max(0, level - 1);\n return '\\n' + indent.repeat(level) + ')';\n });\n\n // Clean up excessive newlines\n formatted = formatted.replace(/\\n\\s*\\n/g, '\\n');\n\n return formatted.trim();\n}\n\n/**\n * Minify SQL by removing unnecessary whitespace.\n *\n * @param sql - SQL string to minify\n * @returns Minified SQL string\n *\n * @example\n * ```typescript\n * import { minifySQL } from '@kysera/debug';\n *\n * const sql = `\n * SELECT id, name\n * FROM users\n * WHERE active = true\n * `;\n * console.log(minifySQL(sql));\n * // SELECT id, name FROM users WHERE active = true\n * ```\n */\nexport function minifySQL(sql: string): string {\n return sql\n .replace(/\\s+/g, ' ') // Collapse whitespace\n .replace(/\\s*,\\s*/g, ', ') // Normalize commas\n .replace(/\\s*\\(\\s*/g, ' (') // Normalize opening parens\n .replace(/\\s*\\)\\s*/g, ') ') // Normalize closing parens\n .trim();\n}\n\n/**\n * Highlight SQL keywords in a query.\n *\n * Returns SQL with ANSI color codes for terminal output.\n *\n * @param sql - SQL string to highlight\n * @returns SQL with ANSI color codes\n *\n * @example\n * ```typescript\n * import { highlightSQL } from '@kysera/debug';\n *\n * console.log(highlightSQL('SELECT * FROM users'));\n * // Keywords will be highlighted in blue\n * ```\n */\nexport function highlightSQL(sql: string): string {\n const BLUE = '\\x1b[34m';\n const RESET = '\\x1b[0m';\n\n let highlighted = sql;\n\n for (const keyword of SQL_KEYWORDS) {\n const regex = new RegExp(`\\\\b(${escapeRegex(keyword)})\\\\b`, 'gi');\n highlighted = highlighted.replace(regex, `${BLUE}$1${RESET}`);\n }\n\n return highlighted;\n}\n"]}
1
+ {"version":3,"sources":["../src/circular-buffer.ts","../src/plugin.ts","../src/profiler.ts","../src/format.ts"],"names":["CircularBuffer","maxSize","item","start","DebugPlugin","options","consoleLogger","args","startTime","compiled","DefaultQueryCompiler","data","duration","metric","message","withDebug","db","plugin","debugDb","QueryProfiler","orderedQueries","totalDuration","sum","q","sorted","a","b","count","thresholdMs","escapeRegex","str","SQL_KEYWORDS","FORMAT_PATTERNS","keyword","HIGHLIGHT_PATTERNS","formatSQL","sql","formatted","pattern","formatSQLPretty","indentSize","indent","level","minifySQL","highlightSQL","BLUE","RESET","highlighted","regex"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Circular buffer utility for bounded memory usage.\n *\n * @module @kysera/debug\n */\n\n/**\n * Circular buffer with O(1) operations.\n *\n * Maintains a fixed-size buffer where oldest entries are overwritten\n * when capacity is reached. Provides efficient add and retrieval\n * operations without reallocation.\n *\n * @template T - Type of items stored in buffer\n *\n * @example\n * ```typescript\n * const buffer = new CircularBuffer<number>(3);\n *\n * buffer.add(1);\n * buffer.add(2);\n * buffer.add(3);\n * buffer.add(4); // Overwrites oldest (1)\n *\n * const items = buffer.getOrdered(); // [2, 3, 4]\n * console.log(buffer.size); // 3\n * ```\n */\nexport class CircularBuffer<T> {\n private items: T[] = []\n private writeIndex = 0\n private readonly maxSize: number\n\n /**\n * Create a new circular buffer.\n *\n * @param maxSize - Maximum number of items to store\n */\n constructor(maxSize: number) {\n if (maxSize <= 0) {\n throw new Error('CircularBuffer maxSize must be positive')\n }\n this.maxSize = maxSize\n }\n\n /**\n * Add an item to the buffer.\n *\n * Uses O(1) operation. When buffer is full, overwrites oldest entry.\n *\n * @param item - Item to add\n */\n add(item: T): void {\n if (this.items.length < this.maxSize) {\n this.items.push(item)\n } else {\n this.items[this.writeIndex % this.maxSize] = item\n }\n this.writeIndex++\n }\n\n /**\n * Get all items in chronological order.\n *\n * Returns items from oldest to newest. Handles buffer wrap-around correctly.\n *\n * @returns Array of items in chronological order\n */\n getOrdered(): T[] {\n if (this.items.length < this.maxSize) {\n return [...this.items]\n }\n // Buffer is full, reconstruct chronological order\n const start = this.writeIndex % this.maxSize\n return [...this.items.slice(start), ...this.items.slice(0, start)]\n }\n\n /**\n * Get all items in storage order (not chronological).\n *\n * @returns Array of items in storage order\n */\n getRaw(): T[] {\n return [...this.items]\n }\n\n /**\n * Clear all items from buffer.\n */\n clear(): void {\n this.items = []\n this.writeIndex = 0\n }\n\n /**\n * Get current number of items in buffer.\n */\n get size(): number {\n return this.items.length\n }\n\n /**\n * Get maximum capacity of buffer.\n */\n get capacity(): number {\n return this.maxSize\n }\n\n /**\n * Check if buffer is full.\n */\n get isFull(): boolean {\n return this.items.length >= this.maxSize\n }\n\n /**\n * Check if buffer is empty.\n */\n get isEmpty(): boolean {\n return this.items.length === 0\n }\n}\n","/**\n * Debug plugin for Kysely.\n *\n * @module @kysera/debug\n */\n\nimport type {\n Kysely,\n PluginTransformQueryArgs,\n PluginTransformResultArgs,\n QueryResult,\n UnknownRow,\n KyselyPlugin,\n RootOperationNode\n} from 'kysely'\nimport { DefaultQueryCompiler } from 'kysely'\nimport { consoleLogger, type KyseraLogger, type QueryMetrics } from '@kysera/core'\nimport { CircularBuffer } from './circular-buffer.js'\n\n// Re-export QueryMetrics for backwards compatibility\nexport type { QueryMetrics }\n\n/**\n * Options for debug plugin.\n */\nexport interface DebugOptions {\n /**\n * Log query SQL.\n * @default true\n */\n logQuery?: boolean\n\n /**\n * Log query parameters.\n * @default false\n */\n logParams?: boolean\n\n /**\n * Duration threshold (ms) to consider a query slow.\n * @default 100\n */\n slowQueryThreshold?: number\n\n /**\n * Callback for slow queries.\n */\n onSlowQuery?: (sql: string, duration: number) => void\n\n /**\n * Logger for debug messages.\n * @default consoleLogger\n */\n logger?: KyseraLogger\n\n /**\n * Maximum number of metrics to keep in memory.\n * When limit is reached, oldest metrics are removed (circular buffer).\n * @default 1000\n */\n maxMetrics?: number\n}\n\n/**\n * Internal query data for tracking execution.\n * @internal\n */\ninterface QueryData {\n startTime: number\n sql: string\n params: readonly unknown[]\n}\n\n/**\n * Debug plugin implementation.\n * @internal\n */\nclass DebugPlugin implements KyselyPlugin {\n private readonly metricsBuffer: CircularBuffer<QueryMetrics>\n private queryData = new WeakMap<object, QueryData>()\n private readonly logger: KyseraLogger\n private readonly options: Required<\n Pick<DebugOptions, 'logQuery' | 'logParams' | 'slowQueryThreshold'>\n >\n private readonly onSlowQuery: ((sql: string, duration: number) => void) | undefined\n\n constructor(options: DebugOptions = {}) {\n this.logger = options.logger ?? consoleLogger\n this.metricsBuffer = new CircularBuffer<QueryMetrics>(options.maxMetrics ?? 1000)\n this.onSlowQuery = options.onSlowQuery\n this.options = {\n logQuery: options.logQuery ?? true,\n logParams: options.logParams ?? false,\n slowQueryThreshold: options.slowQueryThreshold ?? 100\n }\n }\n\n transformQuery(args: PluginTransformQueryArgs): RootOperationNode {\n const startTime = performance.now()\n\n // Compile the query to get SQL and parameters\n const compiler = new DefaultQueryCompiler()\n const compiled = compiler.compileQuery(args.node, args.queryId)\n\n // Store query data for later use in transformResult\n this.queryData.set(args.queryId, {\n startTime,\n sql: compiled.sql,\n params: compiled.parameters\n })\n\n return args.node\n }\n\n transformResult(args: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>> {\n const data = this.queryData.get(args.queryId)\n\n if (data) {\n const endTime = performance.now()\n const duration = endTime - data.startTime\n this.queryData.delete(args.queryId)\n\n const metric: QueryMetrics = {\n sql: data.sql,\n params: [...data.params],\n duration,\n timestamp: Date.now()\n }\n\n // Add to circular buffer (O(1) operation)\n this.metricsBuffer.add(metric)\n\n if (this.options.logQuery) {\n const message = this.options.logParams\n ? '[SQL] ' + data.sql + '\\n[Params] ' + JSON.stringify(data.params)\n : '[SQL] ' + data.sql\n this.logger.debug(message)\n this.logger.debug('[Duration] ' + duration.toFixed(2) + 'ms')\n }\n\n // Check for slow query\n if (duration > this.options.slowQueryThreshold) {\n if (this.onSlowQuery) {\n this.onSlowQuery(data.sql, duration)\n } else {\n this.logger.warn('[SLOW QUERY] ' + duration.toFixed(2) + 'ms: ' + data.sql)\n }\n }\n }\n\n return Promise.resolve(args.result)\n }\n\n getMetrics(): QueryMetrics[] {\n return this.metricsBuffer.getOrdered()\n }\n\n clearMetrics(): void {\n this.metricsBuffer.clear()\n }\n}\n\n/**\n * Database with debug capabilities.\n */\nexport interface DebugDatabase<DB> extends Kysely<DB> {\n /** Get all collected query metrics */\n getMetrics(): QueryMetrics[]\n /** Clear all collected metrics */\n clearMetrics(): void\n}\n\n/**\n * Wrap a Kysely database with debug capabilities.\n *\n * Adds query logging, metrics collection, and slow query detection.\n *\n * @param db - Kysely database instance\n * @param options - Debug options\n * @returns Database with debug capabilities\n *\n * @example Basic usage\n * ```typescript\n * import { withDebug } from '@kysera/debug';\n *\n * const debugDb = withDebug(db);\n *\n * // Queries are now logged and timed\n * await debugDb.selectFrom('users').selectAll().execute();\n *\n * // Get collected metrics\n * const metrics = debugDb.getMetrics();\n * console.log('Total queries: ' + metrics.length);\n * ```\n *\n * @example With custom options\n * ```typescript\n * import { withDebug } from '@kysera/debug';\n *\n * const debugDb = withDebug(db, {\n * logQuery: true,\n * logParams: true,\n * slowQueryThreshold: 50,\n * maxMetrics: 500,\n * onSlowQuery: (sql, duration) => {\n * alertService.notify('Slow query: ' + duration + 'ms');\n * },\n * });\n * ```\n */\nexport function withDebug<DB>(db: Kysely<DB>, options: DebugOptions = {}): DebugDatabase<DB> {\n const plugin = new DebugPlugin(options)\n const debugDb = db.withPlugin(plugin) as DebugDatabase<DB>\n\n // Attach metrics methods\n debugDb.getMetrics = (): QueryMetrics[] => plugin.getMetrics()\n debugDb.clearMetrics = (): void => {\n plugin.clearMetrics()\n }\n\n return debugDb\n}\n","/**\n * Query profiler for performance analysis.\n *\n * @module @kysera/debug\n */\n\nimport type { QueryMetrics } from '@kysera/core'\nimport { CircularBuffer } from './circular-buffer.js'\n\n/**\n * Query profiler summary.\n */\nexport interface ProfilerSummary {\n /** Total number of recorded queries */\n totalQueries: number\n /** Sum of all query durations */\n totalDuration: number\n /** Average query duration */\n averageDuration: number\n /** Slowest recorded query */\n slowestQuery: QueryMetrics | null\n /** Fastest recorded query */\n fastestQuery: QueryMetrics | null\n /** All recorded queries */\n queries: QueryMetrics[]\n}\n\n/**\n * Options for QueryProfiler.\n */\nexport interface ProfilerOptions {\n /**\n * Maximum number of queries to keep in memory.\n * @default 1000\n */\n maxQueries?: number\n}\n\n/**\n * Query profiler for collecting and analyzing query performance.\n *\n * Provides detailed statistics about query execution times\n * including average, min, max, and query counts.\n *\n * @example\n * ```typescript\n * import { QueryProfiler } from '@kysera/debug';\n *\n * const profiler = new QueryProfiler({ maxQueries: 500 });\n *\n * // Record queries manually\n * profiler.record({\n * sql: 'SELECT * FROM users',\n * duration: 10,\n * timestamp: Date.now(),\n * });\n *\n * // Get summary\n * const summary = profiler.getSummary();\n * console.log('Total queries: ' + summary.totalQueries);\n * console.log('Average duration: ' + summary.averageDuration.toFixed(2) + 'ms');\n *\n * // Clear recorded queries\n * profiler.clear();\n * ```\n */\nexport class QueryProfiler {\n private readonly queriesBuffer: CircularBuffer<QueryMetrics>\n\n /**\n * Create a new query profiler.\n *\n * @param options - Profiler options\n */\n constructor(options: ProfilerOptions = {}) {\n this.queriesBuffer = new CircularBuffer<QueryMetrics>(options.maxQueries ?? 1000)\n }\n\n /**\n * Record a query metric.\n *\n * Uses O(1) circular buffer to maintain bounded memory usage.\n * When the buffer is full, oldest entries are overwritten.\n *\n * @param metric - Query metrics to record\n */\n record(metric: QueryMetrics): void {\n this.queriesBuffer.add(metric)\n }\n\n /**\n * Get profiling summary.\n *\n * @returns Summary of all recorded queries\n */\n getSummary(): ProfilerSummary {\n const orderedQueries = this.getOrderedQueries()\n\n if (orderedQueries.length === 0) {\n return {\n totalQueries: 0,\n totalDuration: 0,\n averageDuration: 0,\n slowestQuery: null,\n fastestQuery: null,\n queries: []\n }\n }\n\n const totalDuration = orderedQueries.reduce((sum, q) => sum + q.duration, 0)\n const sorted = [...orderedQueries].sort((a, b) => b.duration - a.duration)\n\n return {\n totalQueries: orderedQueries.length,\n totalDuration,\n averageDuration: totalDuration / orderedQueries.length,\n slowestQuery: sorted[0] ?? null,\n fastestQuery: sorted[sorted.length - 1] ?? null,\n queries: orderedQueries\n }\n }\n\n /**\n * Get the slowest N queries.\n *\n * @param count - Number of queries to return\n * @returns Array of slowest queries\n */\n getSlowestQueries(count: number): QueryMetrics[] {\n return [...this.getOrderedQueries()].sort((a, b) => b.duration - a.duration).slice(0, count)\n }\n\n /**\n * Get queries slower than a threshold.\n *\n * @param thresholdMs - Duration threshold in milliseconds\n * @returns Array of slow queries\n */\n getSlowQueries(thresholdMs: number): QueryMetrics[] {\n return this.getOrderedQueries().filter(q => q.duration > thresholdMs)\n }\n\n /**\n * Clear all recorded queries.\n */\n clear(): void {\n this.queriesBuffer.clear()\n }\n\n /**\n * Get the number of recorded queries.\n */\n get count(): number {\n return this.queriesBuffer.size\n }\n\n /**\n * Get queries in chronological order.\n * Handles circular buffer wrap-around correctly.\n *\n * @returns Array of queries in chronological order\n */\n private getOrderedQueries(): QueryMetrics[] {\n return this.queriesBuffer.getOrdered()\n }\n}\n","/**\n * SQL formatting utilities.\n *\n * @module @kysera/debug\n */\n\n/**\n * Escape special regex characters in a string.\n * @internal\n */\nfunction escapeRegex(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/**\n * SQL keywords that should start on a new line.\n */\nconst SQL_KEYWORDS = [\n 'SELECT',\n 'FROM',\n 'WHERE',\n 'JOIN',\n 'LEFT JOIN',\n 'RIGHT JOIN',\n 'INNER JOIN',\n 'OUTER JOIN',\n 'CROSS JOIN',\n 'ORDER BY',\n 'GROUP BY',\n 'HAVING',\n 'LIMIT',\n 'OFFSET',\n 'INSERT INTO',\n 'VALUES',\n 'UPDATE',\n 'SET',\n 'DELETE FROM',\n 'UNION',\n 'EXCEPT',\n 'INTERSECT',\n 'ON',\n 'AND',\n 'OR',\n 'RETURNING'\n]\n\n/**\n * Pre-compiled regex patterns for SQL formatting.\n * Cached at module load time for O(1) lookup.\n * @internal\n */\nconst FORMAT_PATTERNS: RegExp[] = SQL_KEYWORDS.map(\n keyword => new RegExp(`(\\\\s+)(${escapeRegex(keyword)})\\\\s+`, 'gi')\n)\n\n/**\n * Pre-compiled regex patterns for SQL highlighting.\n * Cached at module load time for O(1) lookup.\n * @internal\n */\nconst HIGHLIGHT_PATTERNS: { regex: RegExp; keyword: string }[] = SQL_KEYWORDS.map(keyword => ({\n regex: new RegExp(`\\\\b(${escapeRegex(keyword)})\\\\b`, 'gi'),\n keyword\n}))\n\n/**\n * Format SQL for better readability.\n *\n * Adds newlines before major SQL keywords to make\n * complex queries easier to read.\n *\n * @param sql - SQL string to format\n * @returns Formatted SQL string\n *\n * @example\n * ```typescript\n * import { formatSQL } from '@kysera/debug';\n *\n * const sql = 'SELECT id, name FROM users WHERE active = true ORDER BY name';\n * console.log(formatSQL(sql));\n * // SELECT id, name\n * // FROM users\n * // WHERE active = true\n * // ORDER BY name\n * ```\n */\nexport function formatSQL(sql: string): string {\n let formatted = sql\n\n // Add newlines before SQL keywords using pre-compiled patterns\n for (const pattern of FORMAT_PATTERNS) {\n // Reset lastIndex for global regexes to ensure consistent behavior\n pattern.lastIndex = 0\n formatted = formatted.replace(pattern, '\\n$2 ')\n }\n\n return formatted.trim()\n}\n\n/**\n * Format SQL with indentation for nested queries.\n *\n * More advanced formatting with proper indentation\n * for subqueries and nested structures.\n *\n * @param sql - SQL string to format\n * @param indentSize - Number of spaces for indentation\n * @returns Formatted SQL string\n *\n * @example\n * ```typescript\n * import { formatSQLPretty } from '@kysera/debug';\n *\n * const sql = 'SELECT * FROM users WHERE id IN (SELECT user_id FROM orders)';\n * console.log(formatSQLPretty(sql));\n * ```\n */\nexport function formatSQLPretty(sql: string, indentSize = 2): string {\n let formatted = formatSQL(sql)\n const indent = ' '.repeat(indentSize)\n let level = 0\n\n // Handle parentheses for subqueries\n formatted = formatted.replace(/\\(/g, () => {\n level++\n return '(\\n' + indent.repeat(level)\n })\n\n formatted = formatted.replace(/\\)/g, () => {\n level = Math.max(0, level - 1)\n return '\\n' + indent.repeat(level) + ')'\n })\n\n // Clean up excessive newlines\n formatted = formatted.replace(/\\n\\s*\\n/g, '\\n')\n\n return formatted.trim()\n}\n\n/**\n * Minify SQL by removing unnecessary whitespace.\n *\n * @param sql - SQL string to minify\n * @returns Minified SQL string\n *\n * @example\n * ```typescript\n * import { minifySQL } from '@kysera/debug';\n *\n * const sql = `\n * SELECT id, name\n * FROM users\n * WHERE active = true\n * `;\n * console.log(minifySQL(sql));\n * // SELECT id, name FROM users WHERE active = true\n * ```\n */\nexport function minifySQL(sql: string): string {\n return sql\n .replace(/\\s+/g, ' ') // Collapse whitespace\n .replace(/\\s*,\\s*/g, ', ') // Normalize commas\n .replace(/\\s*\\(\\s*/g, ' (') // Normalize opening parens\n .replace(/\\s*\\)\\s*/g, ') ') // Normalize closing parens\n .trim()\n}\n\n/**\n * Highlight SQL keywords in a query.\n *\n * Returns SQL with ANSI color codes for terminal output.\n *\n * @param sql - SQL string to highlight\n * @returns SQL with ANSI color codes\n *\n * @example\n * ```typescript\n * import { highlightSQL } from '@kysera/debug';\n *\n * console.log(highlightSQL('SELECT * FROM users'));\n * // Keywords will be highlighted in blue\n * ```\n */\nexport function highlightSQL(sql: string): string {\n const BLUE = '\\x1b[34m'\n const RESET = '\\x1b[0m'\n\n let highlighted = sql\n\n for (const { regex } of HIGHLIGHT_PATTERNS) {\n // Reset lastIndex for global regexes to ensure consistent behavior\n regex.lastIndex = 0\n highlighted = highlighted.replace(regex, BLUE + '$1' + RESET)\n }\n\n return highlighted\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kysera/debug",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "description": "Debug utilities for Kysely - query logging, profiling, SQL formatting",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -19,15 +19,15 @@
19
19
  "kysely": ">=0.28.8"
20
20
  },
21
21
  "dependencies": {
22
- "@kysera/core": "0.7.2"
22
+ "@kysera/core": "0.7.4"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/node": "^24.10.1",
26
- "@vitest/coverage-v8": "^4.0.15",
26
+ "@vitest/coverage-v8": "^4.0.16",
27
27
  "kysely": "^0.28.9",
28
28
  "tsup": "^8.5.1",
29
29
  "typescript": "^5.9.3",
30
- "vitest": "^4.0.15"
30
+ "vitest": "^4.0.16"
31
31
  },
32
32
  "keywords": [
33
33
  "kysely",
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Circular buffer utility for bounded memory usage.
3
+ *
4
+ * @module @kysera/debug
5
+ */
6
+
7
+ /**
8
+ * Circular buffer with O(1) operations.
9
+ *
10
+ * Maintains a fixed-size buffer where oldest entries are overwritten
11
+ * when capacity is reached. Provides efficient add and retrieval
12
+ * operations without reallocation.
13
+ *
14
+ * @template T - Type of items stored in buffer
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * const buffer = new CircularBuffer<number>(3);
19
+ *
20
+ * buffer.add(1);
21
+ * buffer.add(2);
22
+ * buffer.add(3);
23
+ * buffer.add(4); // Overwrites oldest (1)
24
+ *
25
+ * const items = buffer.getOrdered(); // [2, 3, 4]
26
+ * console.log(buffer.size); // 3
27
+ * ```
28
+ */
29
+ export class CircularBuffer<T> {
30
+ private items: T[] = []
31
+ private writeIndex = 0
32
+ private readonly maxSize: number
33
+
34
+ /**
35
+ * Create a new circular buffer.
36
+ *
37
+ * @param maxSize - Maximum number of items to store
38
+ */
39
+ constructor(maxSize: number) {
40
+ if (maxSize <= 0) {
41
+ throw new Error('CircularBuffer maxSize must be positive')
42
+ }
43
+ this.maxSize = maxSize
44
+ }
45
+
46
+ /**
47
+ * Add an item to the buffer.
48
+ *
49
+ * Uses O(1) operation. When buffer is full, overwrites oldest entry.
50
+ *
51
+ * @param item - Item to add
52
+ */
53
+ add(item: T): void {
54
+ if (this.items.length < this.maxSize) {
55
+ this.items.push(item)
56
+ } else {
57
+ this.items[this.writeIndex % this.maxSize] = item
58
+ }
59
+ this.writeIndex++
60
+ }
61
+
62
+ /**
63
+ * Get all items in chronological order.
64
+ *
65
+ * Returns items from oldest to newest. Handles buffer wrap-around correctly.
66
+ *
67
+ * @returns Array of items in chronological order
68
+ */
69
+ getOrdered(): T[] {
70
+ if (this.items.length < this.maxSize) {
71
+ return [...this.items]
72
+ }
73
+ // Buffer is full, reconstruct chronological order
74
+ const start = this.writeIndex % this.maxSize
75
+ return [...this.items.slice(start), ...this.items.slice(0, start)]
76
+ }
77
+
78
+ /**
79
+ * Get all items in storage order (not chronological).
80
+ *
81
+ * @returns Array of items in storage order
82
+ */
83
+ getRaw(): T[] {
84
+ return [...this.items]
85
+ }
86
+
87
+ /**
88
+ * Clear all items from buffer.
89
+ */
90
+ clear(): void {
91
+ this.items = []
92
+ this.writeIndex = 0
93
+ }
94
+
95
+ /**
96
+ * Get current number of items in buffer.
97
+ */
98
+ get size(): number {
99
+ return this.items.length
100
+ }
101
+
102
+ /**
103
+ * Get maximum capacity of buffer.
104
+ */
105
+ get capacity(): number {
106
+ return this.maxSize
107
+ }
108
+
109
+ /**
110
+ * Check if buffer is full.
111
+ */
112
+ get isFull(): boolean {
113
+ return this.items.length >= this.maxSize
114
+ }
115
+
116
+ /**
117
+ * Check if buffer is empty.
118
+ */
119
+ get isEmpty(): boolean {
120
+ return this.items.length === 0
121
+ }
122
+ }
package/src/format.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  * @internal
10
10
  */
11
11
  function escapeRegex(str: string): string {
12
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
12
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
13
13
  }
14
14
 
15
15
  /**
@@ -41,8 +41,27 @@ const SQL_KEYWORDS = [
41
41
  'ON',
42
42
  'AND',
43
43
  'OR',
44
- 'RETURNING',
45
- ];
44
+ 'RETURNING'
45
+ ]
46
+
47
+ /**
48
+ * Pre-compiled regex patterns for SQL formatting.
49
+ * Cached at module load time for O(1) lookup.
50
+ * @internal
51
+ */
52
+ const FORMAT_PATTERNS: RegExp[] = SQL_KEYWORDS.map(
53
+ keyword => new RegExp(`(\\s+)(${escapeRegex(keyword)})\\s+`, 'gi')
54
+ )
55
+
56
+ /**
57
+ * Pre-compiled regex patterns for SQL highlighting.
58
+ * Cached at module load time for O(1) lookup.
59
+ * @internal
60
+ */
61
+ const HIGHLIGHT_PATTERNS: { regex: RegExp; keyword: string }[] = SQL_KEYWORDS.map(keyword => ({
62
+ regex: new RegExp(`\\b(${escapeRegex(keyword)})\\b`, 'gi'),
63
+ keyword
64
+ }))
46
65
 
47
66
  /**
48
67
  * Format SQL for better readability.
@@ -66,15 +85,16 @@ const SQL_KEYWORDS = [
66
85
  * ```
67
86
  */
68
87
  export function formatSQL(sql: string): string {
69
- let formatted = sql;
88
+ let formatted = sql
70
89
 
71
- // Add newlines before SQL keywords
72
- for (const keyword of SQL_KEYWORDS) {
73
- const regex = new RegExp(`(\\s+)(${escapeRegex(keyword)})\\s+`, 'gi');
74
- formatted = formatted.replace(regex, `\n$2 `);
90
+ // Add newlines before SQL keywords using pre-compiled patterns
91
+ for (const pattern of FORMAT_PATTERNS) {
92
+ // Reset lastIndex for global regexes to ensure consistent behavior
93
+ pattern.lastIndex = 0
94
+ formatted = formatted.replace(pattern, '\n$2 ')
75
95
  }
76
96
 
77
- return formatted.trim();
97
+ return formatted.trim()
78
98
  }
79
99
 
80
100
  /**
@@ -96,25 +116,25 @@ export function formatSQL(sql: string): string {
96
116
  * ```
97
117
  */
98
118
  export function formatSQLPretty(sql: string, indentSize = 2): string {
99
- let formatted = formatSQL(sql);
100
- const indent = ' '.repeat(indentSize);
101
- let level = 0;
119
+ let formatted = formatSQL(sql)
120
+ const indent = ' '.repeat(indentSize)
121
+ let level = 0
102
122
 
103
123
  // Handle parentheses for subqueries
104
124
  formatted = formatted.replace(/\(/g, () => {
105
- level++;
106
- return '(\n' + indent.repeat(level);
107
- });
125
+ level++
126
+ return '(\n' + indent.repeat(level)
127
+ })
108
128
 
109
129
  formatted = formatted.replace(/\)/g, () => {
110
- level = Math.max(0, level - 1);
111
- return '\n' + indent.repeat(level) + ')';
112
- });
130
+ level = Math.max(0, level - 1)
131
+ return '\n' + indent.repeat(level) + ')'
132
+ })
113
133
 
114
134
  // Clean up excessive newlines
115
- formatted = formatted.replace(/\n\s*\n/g, '\n');
135
+ formatted = formatted.replace(/\n\s*\n/g, '\n')
116
136
 
117
- return formatted.trim();
137
+ return formatted.trim()
118
138
  }
119
139
 
120
140
  /**
@@ -142,7 +162,7 @@ export function minifySQL(sql: string): string {
142
162
  .replace(/\s*,\s*/g, ', ') // Normalize commas
143
163
  .replace(/\s*\(\s*/g, ' (') // Normalize opening parens
144
164
  .replace(/\s*\)\s*/g, ') ') // Normalize closing parens
145
- .trim();
165
+ .trim()
146
166
  }
147
167
 
148
168
  /**
@@ -162,15 +182,16 @@ export function minifySQL(sql: string): string {
162
182
  * ```
163
183
  */
164
184
  export function highlightSQL(sql: string): string {
165
- const BLUE = '\x1b[34m';
166
- const RESET = '\x1b[0m';
185
+ const BLUE = '\x1b[34m'
186
+ const RESET = '\x1b[0m'
167
187
 
168
- let highlighted = sql;
188
+ let highlighted = sql
169
189
 
170
- for (const keyword of SQL_KEYWORDS) {
171
- const regex = new RegExp(`\\b(${escapeRegex(keyword)})\\b`, 'gi');
172
- highlighted = highlighted.replace(regex, `${BLUE}$1${RESET}`);
190
+ for (const { regex } of HIGHLIGHT_PATTERNS) {
191
+ // Reset lastIndex for global regexes to ensure consistent behavior
192
+ regex.lastIndex = 0
193
+ highlighted = highlighted.replace(regex, BLUE + '$1' + RESET)
173
194
  }
174
195
 
175
- return highlighted;
196
+ return highlighted
176
197
  }
package/src/index.ts CHANGED
@@ -29,24 +29,13 @@
29
29
  */
30
30
 
31
31
  // Plugin
32
- export {
33
- type QueryMetrics,
34
- type DebugOptions,
35
- type DebugDatabase,
36
- withDebug,
37
- } from './plugin.js';
32
+ export { type QueryMetrics, type DebugOptions, type DebugDatabase, withDebug } from './plugin.js'
38
33
 
39
34
  // Profiler
40
- export {
41
- type ProfilerSummary,
42
- type ProfilerOptions,
43
- QueryProfiler,
44
- } from './profiler.js';
35
+ export { type ProfilerSummary, type ProfilerOptions, QueryProfiler } from './profiler.js'
45
36
 
46
37
  // Formatting
47
- export {
48
- formatSQL,
49
- formatSQLPretty,
50
- minifySQL,
51
- highlightSQL,
52
- } from './format.js';
38
+ export { formatSQL, formatSQLPretty, minifySQL, highlightSQL } from './format.js'
39
+
40
+ // Utilities
41
+ export { CircularBuffer } from './circular-buffer.js'