diff --git a/packages/bun-types/sql.d.ts b/packages/bun-types/sql.d.ts index b792170381..f86847a1b2 100644 --- a/packages/bun-types/sql.d.ts +++ b/packages/bun-types/sql.d.ts @@ -122,6 +122,12 @@ declare module "bun" { */ filename?: URL | ":memory:" | (string & {}) | undefined; + /** + * Enable query logging with ActiveRecord-style output + * @default false + */ + log?: boolean | undefined; + /** * Callback executed when a connection attempt completes (SQLite) * Receives an Error on failure, or null on success. @@ -315,6 +321,12 @@ declare module "bun" { * @default true */ prepare?: boolean | undefined; + + /** + * Enable query logging with ActiveRecord-style output + * @default false + */ + log?: boolean | undefined; } /** diff --git a/src/js/internal/sql/mysql.ts b/src/js/internal/sql/mysql.ts index 4d121f84b9..7934c3b5d8 100644 --- a/src/js/internal/sql/mysql.ts +++ b/src/js/internal/sql/mysql.ts @@ -114,6 +114,7 @@ export interface MySQLDotZig { connectionTimeout: number, maxLifetime: number, useUnnamedPreparedStatements: boolean, + log: boolean, ) => $ZigGeneratedClasses.MySQLConnection; createQuery: ( sql: string, @@ -324,6 +325,7 @@ class PooledMySQLConnection { connectionTimeout, maxLifetime, !prepare, + options.log ?? false, ); } catch (e) { onClose(e as Error); diff --git a/src/js/internal/sql/postgres.ts b/src/js/internal/sql/postgres.ts index 73f17dbb0e..06cd6901ee 100644 --- a/src/js/internal/sql/postgres.ts +++ b/src/js/internal/sql/postgres.ts @@ -138,6 +138,7 @@ export interface PostgresDotZig { connectionTimeout: number, maxLifetime: number, useUnnamedPreparedStatements: boolean, + log: boolean, ) => $ZigGeneratedClasses.PostgresSQLConnection; createQuery: ( sql: string, @@ -348,6 +349,7 @@ class PooledPostgresConnection { connectionTimeout, maxLifetime, !prepare, + options.log ?? false, ); } catch (e) { onClose(e as Error); diff --git a/src/js/internal/sql/shared.ts b/src/js/internal/sql/shared.ts index 8db0c57ee7..6bf7300bf6 100644 --- a/src/js/internal/sql/shared.ts +++ b/src/js/internal/sql/shared.ts @@ -480,6 +480,8 @@ function parseOptions( prepare = false; } + const log = options.log ?? false; + onconnect ??= options.onconnect; onclose ??= options.onclose; if (onconnect !== undefined) { @@ -564,6 +566,7 @@ function parseOptions( tls, prepare, bigint, + log, sslMode, query, max: max || 10, diff --git a/src/js/internal/sql/sqlite.ts b/src/js/internal/sql/sqlite.ts index 11304a7e87..1c81fd9daf 100644 --- a/src/js/internal/sql/sqlite.ts +++ b/src/js/internal/sql/sqlite.ts @@ -319,6 +319,10 @@ export class SQLiteQueryHandle implements BaseQueryHandle 0) { + const duration = performance.now() - startTime; + const valuesStr = values && values.length > 0 + ? ` [${values.map(v => v === null ? "null" : typeof v === "string" ? `"${v}"` : String(v)).join(", ")}]` + : ""; + console.log(`[\x1b[1;36m**SQLITE**\x1b[0m] \x1b[33m(${duration.toFixed(1)}ms)\x1b[0m ${sql}${valuesStr}`); + } } catch (err) { + // Log failed query + if (shouldLog && startTime > 0) { + const duration = performance.now() - startTime; + const valuesStr = values && values.length > 0 + ? ` [${values.map(v => v === null ? "null" : typeof v === "string" ? `"${v}"` : String(v)).join(", ")}]` + : ""; + console.log(`[\x1b[1;36m**SQLITE**\x1b[0m] \x1b[33m(${duration.toFixed(1)}ms)\x1b[0m ${sql}${valuesStr} \x1b[31mERROR: ${err instanceof Error ? err.message : String(err)}\x1b[0m`); + } // Convert bun:sqlite errors to SQLiteError if (err && typeof err === "object" && "name" in err && err.name === "SQLiteError") { // Extract SQLite error properties @@ -417,6 +438,11 @@ export class SQLiteAdapter } this.db = new SQLiteModule.Database(filename, options); + + // Set logging flag on the database instance + if (this.connectionInfo.log) { + (this.db as any).log_enabled = true; + } try { const onconnect = this.connectionInfo.onconnect; diff --git a/src/sql/mysql/MySQLConnection.zig b/src/sql/mysql/MySQLConnection.zig index 040c49b7a7..d8de8ace77 100644 --- a/src/sql/mysql/MySQLConnection.zig +++ b/src/sql/mysql/MySQLConnection.zig @@ -869,6 +869,7 @@ pub fn call(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JS const connection_timeout = arguments[12].toInt32(); const max_lifetime = arguments[13].toInt32(); const use_unnamed_prepared_statements = arguments[14].asBoolean(); + const log_enabled = if (arguments.len > 15) arguments[15].asBoolean() else false; var ptr = try bun.default_allocator.create(MySQLConnection); @@ -893,6 +894,7 @@ pub fn call(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JS .character_set = CharacterSet.default, .flags = .{ .use_unnamed_prepared_statements = use_unnamed_prepared_statements, + .log_enabled = log_enabled, }, }; diff --git a/src/sql/mysql/MySQLQuery.zig b/src/sql/mysql/MySQLQuery.zig index 292922afd1..81f1700de7 100644 --- a/src/sql/mysql/MySQLQuery.zig +++ b/src/sql/mysql/MySQLQuery.zig @@ -7,6 +7,7 @@ cursor_name: bun.String = bun.String.empty, thisValue: JSRef = JSRef.empty(), status: Status = Status.pending, +start_time: i128 = 0, ref_count: RefCount = RefCount.init(), @@ -81,6 +82,18 @@ pub fn onWriteFail( queries_array: JSValue, ) void { this.status = .fail; + + // Log query failure if enabled + if (this.start_time > 0) { + const end_time = std.time.nanoTimestamp(); + const duration_ms = @as(f64, @floatFromInt(end_time - this.start_time)) / std.time.ns_per_ms; + + var query_str = this.query.toUTF8(bun.default_allocator); + defer query_str.deinit(); + + bun.Output.prettyln("[**MYSQL**] ({d:.1}ms) {s} ERROR", .{ duration_ms, query_str.slice() }); + } + const thisValue = this.thisValue.get(); defer this.thisValue.deinit(); const targetValue = this.getTarget(globalObject, true); @@ -216,6 +229,17 @@ pub fn onResult(this: *@This(), result_count: u64, globalObject: *jsc.JSGlobalOb const targetValue = this.getTarget(globalObject, is_last); if (is_last) { this.status = .success; + + // Log query completion if enabled + if (this.start_time > 0) { + const end_time = std.time.nanoTimestamp(); + const duration_ms = @as(f64, @floatFromInt(end_time - this.start_time)) / std.time.ns_per_ms; + + var query_str = this.query.toUTF8(bun.default_allocator); + defer query_str.deinit(); + + bun.Output.prettyln("[**MYSQL**] ({d:.1}ms) {s}", .{ duration_ms, query_str.slice() }); + } } else { this.status = .partial_response; } @@ -355,6 +379,11 @@ pub fn doRun(this: *MySQLQuery, globalObject: *jsc.JSGlobalObject, callframe: *j return globalObject.throw("connection must be a MySQLConnection", .{}); }; + // Record start time for logging + if (connection.flags.log_enabled) { + this.start_time = std.time.nanoTimestamp(); + } + connection.poll_ref.ref(globalObject.bunVM()); var query = arguments[1]; diff --git a/src/sql/postgres/PostgresSQLConnection.zig b/src/sql/postgres/PostgresSQLConnection.zig index 0ddbd7c13e..baff8807f0 100644 --- a/src/sql/postgres/PostgresSQLConnection.zig +++ b/src/sql/postgres/PostgresSQLConnection.zig @@ -695,6 +695,7 @@ pub fn call(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JS const connection_timeout = arguments[12].toInt32(); const max_lifetime = arguments[13].toInt32(); const use_unnamed_prepared_statements = arguments[14].asBoolean(); + const log_enabled = if (arguments.len > 15) arguments[15].asBoolean() else false; const ptr: *PostgresSQLConnection = try bun.default_allocator.create(PostgresSQLConnection); @@ -719,6 +720,7 @@ pub fn call(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JS .max_lifetime_interval_ms = @intCast(max_lifetime), .flags = .{ .use_unnamed_prepared_statements = use_unnamed_prepared_statements, + .log_enabled = log_enabled, }, }; diff --git a/src/sql/postgres/PostgresSQLQuery.zig b/src/sql/postgres/PostgresSQLQuery.zig index 35b1af4906..3877aa49a2 100644 --- a/src/sql/postgres/PostgresSQLQuery.zig +++ b/src/sql/postgres/PostgresSQLQuery.zig @@ -7,6 +7,7 @@ cursor_name: bun.String = bun.String.empty, thisValue: JSRef = JSRef.empty(), status: Status = Status.pending, +start_time: i128 = 0, ref_count: RefCount = RefCount.init(), @@ -84,6 +85,18 @@ pub fn onWriteFail( this.ref(); defer this.deref(); this.status = .fail; + + // Log query failure if enabled + if (this.start_time > 0) { + const end_time = std.time.nanoTimestamp(); + const duration_ms = @as(f64, @floatFromInt(end_time - this.start_time)) / std.time.ns_per_ms; + + var query_str = this.query.toUTF8(bun.default_allocator); + defer query_str.deinit(); + + bun.Output.prettyln("[**POSTGRES**] ({d:.1}ms) {s} ERROR", .{ duration_ms, query_str.slice() }); + } + const thisValue = this.thisValue.get(); defer this.thisValue.deinit(); const targetValue = this.getTarget(globalObject, true); @@ -149,6 +162,17 @@ pub fn onResult(this: *@This(), command_tag_str: []const u8, globalObject: *jsc. const targetValue = this.getTarget(globalObject, is_last); if (is_last) { this.status = .success; + + // Log query completion if enabled + if (this.start_time > 0) { + const end_time = std.time.nanoTimestamp(); + const duration_ms = @as(f64, @floatFromInt(end_time - this.start_time)) / std.time.ns_per_ms; + + var query_str = this.query.toUTF8(bun.default_allocator); + defer query_str.deinit(); + + bun.Output.prettyln("[**POSTGRES**] ({d:.1}ms) {s}", .{ duration_ms, query_str.slice() }); + } } else { this.status = .partial_response; } @@ -281,6 +305,11 @@ pub fn doRun(this: *PostgresSQLQuery, globalObject: *jsc.JSGlobalObject, callfra return globalObject.throw("connection must be a PostgresSQLConnection", .{}); }; + // Record start time for logging + if (connection.flags.log_enabled) { + this.start_time = std.time.nanoTimestamp(); + } + connection.poll_ref.ref(globalObject.bunVM()); var query = arguments[1]; diff --git a/src/sql/shared/ConnectionFlags.zig b/src/sql/shared/ConnectionFlags.zig index 52ca043b39..99cb0044f8 100644 --- a/src/sql/shared/ConnectionFlags.zig +++ b/src/sql/shared/ConnectionFlags.zig @@ -4,4 +4,5 @@ pub const ConnectionFlags = packed struct { use_unnamed_prepared_statements: bool = false, waiting_to_prepare: bool = false, has_backpressure: bool = false, + log_enabled: bool = false, }; diff --git a/test_sql_logging.js b/test_sql_logging.js new file mode 100644 index 0000000000..d9185e1326 --- /dev/null +++ b/test_sql_logging.js @@ -0,0 +1,36 @@ +// Test SQL logging functionality +import { expect, test } from "bun:test"; + +test("MySQL logging", async () => { + try { + const sql = new Bun.SQL("mysql://user:password@localhost:3306/test", { log: true }); + // This will fail to connect but should demonstrate the logging option being passed + } catch (e) { + // Connection will fail but that's expected for this test + console.log("MySQL logging test completed (connection expected to fail)"); + } +}); + +test("PostgreSQL logging", async () => { + try { + const sql = new Bun.SQL("postgres://user:password@localhost:5432/test", { log: true }); + // This will fail to connect but should demonstrate the logging option being passed + } catch (e) { + // Connection will fail but that's expected for this test + console.log("PostgreSQL logging test completed (connection expected to fail)"); + } +}); + +test("SQLite logging", async () => { + try { + const sql = new Bun.SQL(":memory:", { log: true }); + await sql`CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)`; + await sql`INSERT INTO users (name) VALUES ('Alice')`; + const users = await sql`SELECT * FROM users`; + expect(users.length).toBe(1); + expect(users[0].name).toBe('Alice'); + console.log("SQLite logging test completed successfully"); + } catch (e) { + console.error("SQLite test failed:", e); + } +}); \ No newline at end of file diff --git a/test_sql_simple.js b/test_sql_simple.js new file mode 100644 index 0000000000..c26481f8b6 --- /dev/null +++ b/test_sql_simple.js @@ -0,0 +1,17 @@ +// Simple test for SQL logging +console.log("Testing SQL logging..."); + +try { + const sql = new Bun.SQL(":memory:", { log: true }); + console.log("SQLite connection created with logging enabled"); + + // Test queries + console.log("Running SQLite queries..."); + await sql`CREATE TABLE test (id INTEGER, name TEXT)`; + await sql`INSERT INTO test VALUES (1, 'Alice')`; + const result = await sql`SELECT * FROM test`; + console.log("Query result:", result); + +} catch (e) { + console.error("Error:", e.message); +} \ No newline at end of file