Skip to content

Commit 884b7a0

Browse files
✨ Add CI workflow and tests for EnhancedQueryLogger; update bun.lock
1 parent 4e0ce0a commit 884b7a0

File tree

4 files changed

+363
-0
lines changed

4 files changed

+363
-0
lines changed

.github/workflows/ci.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: CI - Run Tests
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
run-tests:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Checkout code
15+
uses: actions/checkout@v5
16+
17+
- name: Setup Bun
18+
uses: oven-sh/setup-bun@v2
19+
with:
20+
bun-version: latest
21+
22+
- name: Install dependencies
23+
run: bun install --frozen-lockfile
24+
25+
- name: Run tests
26+
run: bun test
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
import { afterEach, beforeEach, describe, expect, it, jest } from "bun:test";
2+
import { EnhancedQueryLogger } from "../src";
3+
4+
describe("EnhancedQueryLogger", () => {
5+
let logger: EnhancedQueryLogger;
6+
let mockLog: ReturnType<typeof jest.fn<(...args: string[]) => void>>;
7+
8+
beforeEach(() => {
9+
mockLog = jest.fn();
10+
logger = new EnhancedQueryLogger({
11+
log: mockLog,
12+
});
13+
});
14+
15+
afterEach(() => {
16+
jest.clearAllMocks();
17+
});
18+
19+
describe("Query Type Detection", () => {
20+
it("should detect SELECT queries", () => {
21+
logger.logQuery("SELECT * FROM users", []);
22+
expect(mockLog).toHaveBeenCalled();
23+
const logCall = mockLog.mock.calls.find(call =>
24+
call[0].includes("SELECT")
25+
);
26+
expect(logCall).toBeDefined();
27+
});
28+
});
29+
30+
it("should detect INSERT queries", () => {
31+
logger.logQuery("INSERT INTO users (name) VALUES (?)", ["John Doe"]);
32+
expect(mockLog).toHaveBeenCalled();
33+
const logCall = mockLog.mock.calls.find(
34+
call => call[0].includes("📝") && call[0].includes("INSERT")
35+
);
36+
expect(logCall).toBeDefined();
37+
});
38+
39+
it("should detect UPDATE queries", () => {
40+
logger.logQuery("UPDATE users SET name = ? WHERE id = ?", ["Jane", 1]);
41+
const logCall = mockLog.mock.calls.find(
42+
call => call[0].includes("✏️") && call[0].includes("UPDATE")
43+
);
44+
expect(logCall).toBeDefined();
45+
});
46+
47+
it("should detect DELETE queries", () => {
48+
logger.logQuery("DELETE FROM users WHERE id = ?", [1]);
49+
const logCall = mockLog.mock.calls.find(
50+
call => call[0].includes("🗑️") && call[0].includes("DELETE")
51+
);
52+
expect(logCall).toBeDefined();
53+
});
54+
55+
it("should detect CREATE queries", () => {
56+
logger.logQuery("CREATE TABLE users (id INT)", []);
57+
const logCall = mockLog.mock.calls.find(
58+
call => call[0].includes("🏗️") && call[0].includes("CREATE")
59+
);
60+
expect(logCall).toBeDefined();
61+
});
62+
63+
it("should detect DROP queries", () => {
64+
logger.logQuery("DROP TABLE users", []);
65+
const logCall = mockLog.mock.calls.find(
66+
call => call[0].includes("💥") && call[0].includes("DROP")
67+
);
68+
expect(logCall).toBeDefined();
69+
});
70+
71+
it("should detect ALTER queries", () => {
72+
logger.logQuery("ALTER TABLE users ADD COLUMN email VARCHAR(255)", []);
73+
const logCall = mockLog.mock.calls.find(
74+
call => call[0].includes("🔧") && call[0].includes("ALTER")
75+
);
76+
expect(logCall).toBeDefined();
77+
});
78+
79+
it("should classify unknown queries as OTHER", () => {
80+
logger.logQuery("EXPLAIN SELECT * FROM users", []);
81+
const logCall = mockLog.mock.calls.find(
82+
call => call[0].includes("⚡") && call[0].includes("OTHER")
83+
);
84+
expect(logCall).toBeDefined();
85+
});
86+
87+
describe("Table Name Extraction", () => {
88+
it("should extract table name from SELECT queries", () => {
89+
logger.logQuery("SELECT * FROM users WHERE id = 1", []);
90+
const logCall = mockLog.mock.calls.find(call =>
91+
call[0].includes("users")
92+
);
93+
expect(logCall).toBeDefined();
94+
});
95+
96+
it("should extract table name from INSERT queries", () => {
97+
logger.logQuery("INSERT INTO products (name) VALUES (?)", ["Test"]);
98+
const logCall = mockLog.mock.calls.find(call =>
99+
call[0].includes("products")
100+
);
101+
expect(logCall).toBeDefined();
102+
});
103+
104+
it("should extract table name from UPDATE queries", () => {
105+
logger.logQuery("UPDATE orders SET status = ? WHERE id = ?", [
106+
"completed",
107+
1,
108+
]);
109+
const logCall = mockLog.mock.calls.find(call =>
110+
call[0].includes("orders")
111+
);
112+
expect(logCall).toBeDefined();
113+
});
114+
115+
it("should extract table name from CREATE TABLE queries", () => {
116+
logger.logQuery("CREATE TABLE new_table (id INT)", []);
117+
const logCall = mockLog.mock.calls.find(call =>
118+
call[0].includes("new_table")
119+
);
120+
expect(logCall).toBeDefined();
121+
});
122+
123+
it("should handle queries with quoted table names", () => {
124+
logger.logQuery("SELECT * FROM `users` WHERE id = 1", []);
125+
const logCall = mockLog.mock.calls.find(call =>
126+
call[0].includes("users")
127+
);
128+
expect(logCall).toBeDefined();
129+
});
130+
131+
it("should handle queries without identifiable table names", () => {
132+
logger.logQuery("SHOW TABLES", []);
133+
// Should not crash and should log the query
134+
expect(mockLog).toHaveBeenCalled();
135+
});
136+
});
137+
describe("Parameter Formatting", () => {
138+
it("should format string parameters", () => {
139+
logger.logQuery("SELECT * FROM users WHERE name = ?", ["John Doe"]);
140+
const logCall = mockLog.mock.calls.find(call =>
141+
call[0].includes("John Doe")
142+
);
143+
expect(logCall).toBeDefined();
144+
});
145+
146+
it("should format number parameters", () => {
147+
logger.logQuery("SELECT * FROM users WHERE id = ?", [123]);
148+
const logCall = mockLog.mock.calls.find(call => call[0].includes("123"));
149+
expect(logCall).toBeDefined();
150+
});
151+
152+
it("should format boolean parameters", () => {
153+
logger.logQuery("SELECT * FROM users WHERE active = ?", [true]);
154+
const logCall = mockLog.mock.calls.find(call => call[0].includes("true"));
155+
expect(logCall).toBeDefined();
156+
});
157+
158+
it("should format null parameters", () => {
159+
logger.logQuery("SELECT * FROM users WHERE email = ?", [null]);
160+
const logCall = mockLog.mock.calls.find(call => call[0].includes("null"));
161+
expect(logCall).toBeDefined();
162+
});
163+
164+
it("should format object parameters", () => {
165+
logger.logQuery("SELECT * FROM users WHERE metadata = ?", [
166+
{ key: "value" },
167+
]);
168+
const logCall = mockLog.mock.calls.find(
169+
call => call[0].includes("key") && call[0].includes("value")
170+
);
171+
expect(logCall).toBeDefined();
172+
});
173+
174+
it("should handle empty parameter arrays", () => {
175+
logger.logQuery("SELECT * FROM users", []);
176+
expect(mockLog).toHaveBeenCalled();
177+
// Should not include parameter section
178+
const parameterCall = mockLog.mock.calls.find(call =>
179+
call[0].includes("Parameters:")
180+
);
181+
expect(parameterCall).toBeUndefined();
182+
});
183+
});
184+
185+
describe("Query Formatting", () => {
186+
it("should highlight SQL keywords", () => {
187+
logger.logQuery("SELECT name FROM users WHERE id = 1", []);
188+
const logCall = mockLog.mock.calls.find(
189+
call =>
190+
call[0].includes("SELECT") ||
191+
call[0].includes("FROM") ||
192+
call[0].includes("WHERE")
193+
);
194+
expect(logCall).toBeDefined();
195+
});
196+
197+
it("should highlight string literals", () => {
198+
logger.logQuery("SELECT * FROM users WHERE name = 'John'", []);
199+
const logCall = mockLog.mock.calls.find(call =>
200+
call[0].includes("'John'")
201+
);
202+
expect(logCall).toBeDefined();
203+
});
204+
205+
it("should highlight numbers", () => {
206+
logger.logQuery("SELECT * FROM users WHERE id = 123", []);
207+
const logCall = mockLog.mock.calls.find(call => call[0].includes("123"));
208+
expect(logCall).toBeDefined();
209+
});
210+
});
211+
212+
describe("Logging Format", () => {
213+
it("should include query count in header", () => {
214+
logger.logQuery("SELECT * FROM users", []);
215+
const headerCall = mockLog.mock.calls.find(call =>
216+
call[0].includes("#1")
217+
);
218+
expect(headerCall).toBeDefined();
219+
});
220+
221+
it("should increment query count", () => {
222+
logger.logQuery("SELECT * FROM users", []);
223+
logger.logQuery("SELECT * FROM products", []);
224+
225+
const firstQueryCall = mockLog.mock.calls.find(call =>
226+
call[0].includes("#1")
227+
);
228+
const secondQueryCall = mockLog.mock.calls.find(call =>
229+
call[0].includes("#2")
230+
);
231+
232+
expect(firstQueryCall).toBeDefined();
233+
expect(secondQueryCall).toBeDefined();
234+
});
235+
236+
it("should include timestamp", () => {
237+
logger.logQuery("SELECT * FROM users", []);
238+
const timeCall = mockLog.mock.calls.find(call =>
239+
call[0].includes("Time:")
240+
);
241+
expect(timeCall).toBeDefined();
242+
});
243+
244+
it("should include SQL section", () => {
245+
logger.logQuery("SELECT * FROM users", []);
246+
const sqlCall = mockLog.mock.calls.find(call => call[0].includes("SQL:"));
247+
expect(sqlCall).toBeDefined();
248+
});
249+
250+
it("should call log function multiple times for complete output", () => {
251+
logger.logQuery("SELECT * FROM users WHERE id = ?", [1]);
252+
// Should have header, time, query info, SQL line, parameters, and footer
253+
expect(mockLog).toHaveBeenCalledTimes(6);
254+
});
255+
});
256+
257+
describe("Constructor Options", () => {
258+
it("should use console.log by default", () => {
259+
const defaultLogger = new EnhancedQueryLogger();
260+
// Should not throw when logging
261+
expect(() => {
262+
defaultLogger.logQuery("SELECT 1", []);
263+
}).not.toThrow();
264+
});
265+
266+
it("should accept custom log function", () => {
267+
const customLog = jest.fn();
268+
const customLogger = new EnhancedQueryLogger({ log: customLog });
269+
270+
customLogger.logQuery("SELECT 1", []);
271+
expect(customLog).toHaveBeenCalled();
272+
});
273+
});
274+
275+
describe("Edge Cases", () => {
276+
it("should handle very long queries", () => {
277+
const longQuery = "SELECT " + "column,".repeat(100) + " id FROM users";
278+
expect(() => {
279+
logger.logQuery(longQuery, []);
280+
}).not.toThrow();
281+
});
282+
283+
it("should handle queries with special characters", () => {
284+
const specialQuery =
285+
"SELECT * FROM users WHERE name = 'O''Reilly' AND email LIKE '%@%.com'";
286+
expect(() => {
287+
logger.logQuery(specialQuery, []);
288+
}).not.toThrow();
289+
});
290+
291+
it("should handle empty queries", () => {
292+
expect(() => {
293+
logger.logQuery("", []);
294+
}).not.toThrow();
295+
});
296+
297+
it("should handle queries with only whitespace", () => {
298+
expect(() => {
299+
logger.logQuery(" \n\t ", []);
300+
}).not.toThrow();
301+
});
302+
303+
it("should handle malformed parameters that throw JSON.stringify errors", () => {
304+
const circularObj: any = { prop: "value" };
305+
circularObj.circular = circularObj;
306+
307+
expect(() => {
308+
logger.logQuery("SELECT * FROM users WHERE data = ?", [circularObj]);
309+
}).not.toThrow();
310+
});
311+
});
312+
});

__tests__/index.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { describe, expect, it } from "bun:test";
2+
import { EnhancedQueryLogger } from "../src";
3+
4+
describe("Package Exports", () => {
5+
it("should export EnhancedQueryLogger class", () => {
6+
expect(EnhancedQueryLogger).toBeDefined();
7+
expect(typeof EnhancedQueryLogger).toBe("function");
8+
});
9+
10+
it("shouldbe able to instantiate EnhancedQueryLogger", () => {
11+
const logger = new EnhancedQueryLogger();
12+
expect(logger).toBeInstanceOf(EnhancedQueryLogger);
13+
});
14+
15+
it("should implement Logger interface methods", () => {
16+
const logger = new EnhancedQueryLogger();
17+
expect(typeof logger.logQuery).toBe("function");
18+
});
19+
});

bun.lock

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,8 @@
210210

211211
"mz": ["[email protected]", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
212212

213+
"nanoid": ["[email protected]", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
214+
213215
"object-assign": ["[email protected]", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
214216

215217
"package-json-from-dist": ["[email protected]", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
@@ -228,6 +230,8 @@
228230

229231
"pkg-types": ["[email protected]", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
230232

233+
"postcss": ["[email protected]", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
234+
231235
"postcss-load-config": ["[email protected]", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="],
232236

233237
"punycode": ["[email protected]", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
@@ -246,6 +250,8 @@
246250

247251
"source-map": ["[email protected]", "", { "dependencies": { "whatwg-url": "^7.0.0" } }, "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA=="],
248252

253+
"source-map-js": ["[email protected]", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
254+
249255
"string-width": ["[email protected]", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
250256

251257
"string-width-cjs": ["[email protected]", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],

0 commit comments

Comments
 (0)