diff --git a/docs/api/sql.md b/docs/api/sql.md index 1fd30db32d..2b25208aa2 100644 --- a/docs/api/sql.md +++ b/docs/api/sql.md @@ -274,6 +274,23 @@ If no connection URL is provided, the system checks for the following individual | `PGPASSWORD` | - | (empty) | Database password | | `PGDATABASE` | - | username | Database name | +## Runtime Preconnection + +Bun can preconnect to PostgreSQL at startup to improve performance by establishing database connections before your application code runs. This is useful for reducing connection latency on the first database query. + +```bash +# Enable PostgreSQL preconnection +bun --sql-preconnect index.js + +# Works with DATABASE_URL environment variable +DATABASE_URL=postgres://user:pass@localhost:5432/db bun --sql-preconnect index.js + +# Can be combined with other runtime flags +bun --sql-preconnect --hot index.js +``` + +The `--sql-preconnect` flag will automatically establish a PostgreSQL connection using your configured environment variables at startup. If the connection fails, it won't crash your application - the error will be handled gracefully. + ## Connection Options You can configure your database connection manually by passing options to the SQL constructor: diff --git a/src/bun_js.zig b/src/bun_js.zig index b9ee1001a7..d23bb5bf50 100644 --- a/src/bun_js.zig +++ b/src/bun_js.zig @@ -308,6 +308,28 @@ pub const Run = struct { } } + do_postgres_preconnect: { + if (this.ctx.runtime_options.sql_preconnect) { + const global = vm.global; + const bun_object = vm.global.toJSValue().get(global, "Bun") catch |err| { + global.reportActiveExceptionAsUnhandled(err); + break :do_postgres_preconnect; + } orelse break :do_postgres_preconnect; + const sql_object = bun_object.get(global, "sql") catch |err| { + global.reportActiveExceptionAsUnhandled(err); + break :do_postgres_preconnect; + } orelse break :do_postgres_preconnect; + const connect_fn = sql_object.get(global, "connect") catch |err| { + global.reportActiveExceptionAsUnhandled(err); + break :do_postgres_preconnect; + } orelse break :do_postgres_preconnect; + _ = connect_fn.call(global, sql_object, &.{}) catch |err| { + global.reportActiveExceptionAsUnhandled(err); + break :do_postgres_preconnect; + }; + } + } + switch (this.ctx.debug.hot_reload) { .hot => JSC.hot_reloader.HotReloader.enableHotModuleReloading(vm), .watch => JSC.hot_reloader.WatchReloader.enableHotModuleReloading(vm), diff --git a/src/cli.zig b/src/cli.zig index 85c5199069..969e4ce2ce 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -378,6 +378,7 @@ pub const Command = struct { debugger: Debugger = .{ .unspecified = {} }, if_present: bool = false, redis_preconnect: bool = false, + sql_preconnect: bool = false, eval: struct { script: []const u8 = "", eval_and_print: bool = false, diff --git a/src/cli/Arguments.zig b/src/cli/Arguments.zig index 19650aedfc..183ee061cf 100644 --- a/src/cli/Arguments.zig +++ b/src/cli/Arguments.zig @@ -106,6 +106,7 @@ pub const runtime_params_ = [_]ParamType{ clap.parseParam("--title Set the process title") catch unreachable, clap.parseParam("--zero-fill-buffers Boolean to force Buffer.allocUnsafe(size) to be zero-filled.") catch unreachable, clap.parseParam("--redis-preconnect Preconnect to $REDIS_URL at startup") catch unreachable, + clap.parseParam("--sql-preconnect Preconnect to PostgreSQL at startup") catch unreachable, clap.parseParam("--no-addons Throw an error if process.dlopen is called, and disable export condition \"node-addons\"") catch unreachable, clap.parseParam("--unhandled-rejections One of \"strict\", \"throw\", \"warn\", \"none\", or \"warn-with-error-code\"") catch unreachable, clap.parseParam("--console-depth Set the default depth for console.log object inspection (default: 2)") catch unreachable, @@ -589,6 +590,10 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C ctx.runtime_options.redis_preconnect = true; } + if (args.flag("--sql-preconnect")) { + ctx.runtime_options.sql_preconnect = true; + } + if (args.flag("--no-addons")) { // used for disabling process.dlopen and // for disabling export condition "node-addons" diff --git a/test/cli/run/sql-preconnect.test.ts b/test/cli/run/sql-preconnect.test.ts new file mode 100644 index 0000000000..1ffd72af0f --- /dev/null +++ b/test/cli/run/sql-preconnect.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDirWithFiles } from "harness"; + +describe("--sql-preconnect", () => { + test("should attempt to preconnect to PostgreSQL on startup", async () => { + let connectionAttempts = 0; + const { promise, resolve } = Promise.withResolvers(); + + await using server = Bun.listen({ + port: 0, + hostname: "127.0.0.1", + socket: { + open(socket) { + connectionAttempts++; + socket.end(); + if (connectionAttempts >= 1) { + resolve(); + } + }, + data() {}, + close() {}, + }, + }); + + const testDir = tempDirWithFiles("sql-preconnect-test", { + "index.js": `console.log("Script executed");`, + }); + + const proc = Bun.spawn({ + cmd: [bunExe(), "--sql-preconnect", "index.js"], + env: { + ...bunEnv, + DATABASE_URL: `postgres://127.0.0.1:${server.port}/MY_DATABASE`, + }, + cwd: testDir, + }); + + await promise; + proc.kill(); + await proc.exited; + + expect(connectionAttempts).toBeGreaterThan(0); + }); + + test("should not connect when flag is not used", async () => { + let connectionAttempts = 0; + + await using server = Bun.listen({ + port: 0, + hostname: "127.0.0.1", + socket: { + open(socket) { + connectionAttempts++; + socket.end(); + }, + data() {}, + close() {}, + }, + }); + + const testDir = tempDirWithFiles("sql-no-preconnect", { + "index.js": `console.log("Normal script executed");`, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "index.js"], + env: { + ...bunEnv, + DATABASE_URL: `postgres://127.0.0.1:${server.port}/MY_DATABASE`, + }, + cwd: testDir, + }); + + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]); + + expect(exitCode).toBe(0); + expect(stdout).toContain("Normal script executed"); + expect(connectionAttempts).toBe(0); // No connection should be attempted without the flag + }); +}); diff --git a/test/no-validate-exceptions.txt b/test/no-validate-exceptions.txt index b4b0bb8870..fc4d24346b 100644 --- a/test/no-validate-exceptions.txt +++ b/test/no-validate-exceptions.txt @@ -98,6 +98,7 @@ test/integration/sass/sass.test.ts test/integration/sharp/sharp.test.ts test/integration/svelte/client-side.test.ts test/integration/typegraphql/src/typegraphql.test.ts +test/cli/run/sql-preconnect.test.ts test/internal/bindgen.test.ts test/js/bun/bun-object/deep-match.spec.ts test/js/bun/bun-object/write.spec.ts