Files
bun.sh/test/js/sql/adapter-env-var-precedence.test.ts
Alistair Smith e63608fced Fix: Make SQL connection string parsing more sensible (#22260)
This PR makes connection string parsing more sensible in Bun.SQL,
without breaking the default fallback of postgres

Added some tests checking for connection string precedence

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Ciro Spaciari <ciro.spaciari@gmail.com>
2025-09-08 20:59:24 -07:00

476 lines
18 KiB
TypeScript

import { SQL } from "bun";
import { afterAll, beforeEach, describe, expect, test } from "bun:test";
import { isWindows } from "harness";
import { unlinkSync } from "js/node/fs/export-star-from";
declare module "bun" {
namespace SQL {
export interface PostgresOrMySQLOptions {
sslMode?: number;
}
}
}
describe("SQL adapter environment variable precedence", () => {
const originalEnv = { ...process.env };
// prettier-ignore
const SQL_ENV_VARS = [
'DATABASE_URL', 'DATABASEURL',
'TLS_DATABASE_URL',
'POSTGRES_URL', 'PGURL', 'PG_URL',
'TLS_POSTGRES_DATABASE_URL',
'MYSQL_URL', 'MYSQLURL',
'TLS_MYSQL_DATABASE_URL',
'MARIADB_URL', 'MARIADBURL',
'TLS_MARIADB_DATABASE_URL',
'SQLITE_URL', 'SQLITEURL',
'PGHOST', 'PGUSER', 'PGPASSWORD', 'PGDATABASE', 'PGPORT',
'MYSQL_HOST', 'MYSQL_USER', 'MYSQL_PASSWORD', 'MYSQL_DATABASE', 'MYSQL_PORT'
];
beforeEach(() => {
for (const key of Object.keys(process.env).concat(...Object.keys(Bun.env), ...Object.keys(import.meta.env))) {
delete process.env[key];
delete Bun.env[key];
delete import.meta.env[key];
}
for (const key in originalEnv) {
process.env[key] = originalEnv[key];
Bun.env[key] = originalEnv[key];
import.meta.env[key] = originalEnv[key];
}
for (const key of SQL_ENV_VARS) {
delete process.env[key];
delete Bun.env[key];
delete import.meta.env[key];
}
});
afterAll(() => {
for (const key of Object.keys(process.env).concat(...Object.keys(Bun.env), ...Object.keys(import.meta.env))) {
delete process.env[key];
delete Bun.env[key];
delete import.meta.env[key];
}
for (const key in originalEnv) {
process.env[key] = originalEnv[key];
Bun.env[key] = originalEnv[key];
import.meta.env[key] = originalEnv[key];
}
for (const key of SQL_ENV_VARS) {
delete process.env[key];
delete Bun.env[key];
delete import.meta.env[key];
}
});
test("should not prioritize DATABASE_URL over explicit options (issue #22147)", () => {
process.env.DATABASE_URL = "foo_url";
const options = new SQL({
hostname: "bar_url",
username: "postgres",
password: "postgres",
port: 5432,
});
expect(options.options.adapter).toBe("postgres");
expect(options.options.hostname).toBe("bar_url");
expect(options.options.port).toBe(5432);
expect(options.options.username).toBe("postgres");
});
test("should only read PostgreSQL env vars when adapter is postgres", () => {
process.env.PGHOST = "pg-host";
process.env.PGUSER = "pg-user";
process.env.PGPASSWORD = "pg-pass";
process.env.MYSQL_URL = "mysql://mysql-host/db";
const options = new SQL({
adapter: "postgres",
});
expect(options.options.hostname).toBe("pg-host");
expect(options.options.username).toBe("pg-user");
expect(options.options.password).toBe("pg-pass");
// Should not use MYSQL_URL
expect(options.options.hostname).not.toBe("mysql-host");
});
test("should only read MySQL env vars when adapter is mysql", () => {
process.env.PGHOST = "pg-host";
process.env.PGUSER = "pg-user";
process.env.MYSQL_URL = "mysql://mysql-host/db";
const options = new SQL({
adapter: "mysql",
});
// Should use MYSQL_URL and not read PostgreSQL env vars
expect(options.options.hostname).toBe("mysql-host");
expect(options.options.username).not.toBe("pg-user");
});
test("should infer postgres adapter from postgres:// protocol", () => {
const options = new SQL("postgres://user:pass@host:5432/db");
expect(options.options.adapter).toBe("postgres");
});
test("should infer mysql adapter from mysql:// protocol", () => {
const options = new SQL("mysql://user:pass@host:3306/db");
expect(options.options.adapter).toBe("mysql");
});
test("should default to postgres when no protocol specified", () => {
const options = new SQL("user:pass@host/db");
expect(options.options.adapter).toBe("postgres");
});
test("adapter-specific env vars should take precedence over generic ones", () => {
process.env.USER = "generic-user";
process.env.PGUSER = "postgres-user";
const options = new SQL({
adapter: "postgres",
});
expect(options.options.username).toBe("postgres-user");
});
test("should infer mysql adapter from MYSQL_URL env var", () => {
process.env.MYSQL_URL = "mysql://user:pass@host:3306/db";
const options = new SQL();
expect(options.options.adapter).toBe("mysql");
expect(options.options.hostname).toBe("host");
expect(options.options.port).toBe(3306);
});
test("should default to port 3306 for MySQL when no port specified", () => {
process.env.MYSQL_URL = "mysql://user:pass@host/db";
const options = new SQL();
expect(options.options.adapter).toBe("mysql");
expect(options.options.hostname).toBe("host");
expect(options.options.port).toBe(3306); // Should default to MySQL port
});
test("should default to port 3306 for explicit MySQL adapter", () => {
const options = new SQL({
adapter: "mysql",
hostname: "localhost",
});
expect(options.options.adapter).toBe("mysql");
expect(options.options.port).toBe(3306); // Should default to MySQL port
});
test("should infer postgres adapter from POSTGRES_URL env var", () => {
process.env.POSTGRES_URL = "postgres://user:pass@host:5432/db";
const options = new SQL();
expect(options.options.adapter).toBe("postgres");
expect(options.options.hostname).toBe("host");
expect(options.options.port).toBe(5432);
});
test("POSTGRES_URL should take precedence over MYSQL_URL", () => {
process.env.POSTGRES_URL = "postgres://pg-host:5432/pgdb";
process.env.MYSQL_URL = "mysql://mysql-host:3306/mysqldb";
const options = new SQL();
expect(options.options.adapter).toBe("postgres");
expect(options.options.hostname).toBe("pg-host");
expect(options.options.port).toBe(5432);
});
test("should infer mysql from MYSQL_URL even without protocol", () => {
process.env.MYSQL_URL = "root@localhost:3306/test";
const options = new SQL();
expect(options.options.adapter).toBe("mysql");
expect(options.options.hostname).toBe("localhost");
expect(options.options.port).toBe(3306);
expect(options.options.username).toBe("root");
});
test("should infer postgres from POSTGRES_URL even without protocol", () => {
process.env.POSTGRES_URL = "user@localhost:5432/test";
const options = new SQL();
expect(options.options.adapter).toBe("postgres");
expect(options.options.hostname).toBe("localhost");
expect(options.options.port).toBe(5432);
expect(options.options.username).toBe("user");
});
test("environment variable name should override protocol (PGURL with mysql protocol should be postgres)", () => {
process.env.PGURL = "mysql://host:3306/db";
const options = new SQL();
expect(options.options.adapter).toBe("postgres");
expect(options.options.hostname).toBe("host");
expect(options.options.port).toBe(3306);
});
test("environment variable name should override protocol (MYSQL_URL with postgres protocol should be mysql)", () => {
process.env.MYSQL_URL = "postgres://host:5432/db";
const options = new SQL();
expect(options.options.adapter).toBe("mysql");
expect(options.options.hostname).toBe("host");
expect(options.options.port).toBe(5432);
});
test("should use MySQL-specific environment variables", () => {
process.env.MYSQL_HOST = "mysql-server";
process.env.MYSQL_PORT = "3307";
process.env.MYSQL_USER = "admin";
process.env.MYSQL_PASSWORD = "secret";
process.env.MYSQL_DATABASE = "production";
const options = new SQL({ adapter: "mysql" });
expect(options.options.adapter).toBe("mysql");
expect(options.options.hostname).toBe("mysql-server");
expect(options.options.port).toBe(3307);
expect(options.options.username).toBe("admin");
expect(options.options.password).toBe("secret");
expect(options.options.database).toBe("production");
});
test("MySQL-specific env vars should take precedence over generic ones", () => {
process.env.USER = "generic-user";
process.env.MYSQL_USER = "mysql-user";
const options = new SQL({ adapter: "mysql" });
expect(options.options.username).toBe("mysql-user");
});
test("should default to database name 'mysql' for MySQL adapter", () => {
const options = new SQL({ adapter: "mysql", hostname: "localhost" });
expect(options.options.adapter).toBe("mysql");
expect(options.options.database).toBe("mysql");
});
test("should default to username as database name for PostgreSQL adapter", () => {
const options = new SQL({ adapter: "postgres", hostname: "localhost", username: "testuser" });
expect(options.options.adapter).toBe("postgres");
expect(options.options.database).toBe("testuser");
});
test("should infer mysql adapter from TLS_MYSQL_DATABASE_URL", () => {
process.env.TLS_MYSQL_DATABASE_URL = "mysql://user:pass@host:3306/db";
const options = new SQL();
expect(options.options.adapter).toBe("mysql");
expect(options.options.hostname).toBe("host");
expect(options.options.port).toBe(3306);
expect(options.options.sslMode).toBe(2); // SSLMode.require
});
test("should infer postgres adapter from TLS_POSTGRES_DATABASE_URL", () => {
process.env.TLS_POSTGRES_DATABASE_URL = "postgres://user:pass@host:5432/db";
const options = new SQL();
expect(options.options.adapter).toBe("postgres");
expect(options.options.hostname).toBe("host");
expect(options.options.port).toBe(5432);
expect(options.options.sslMode).toBe(2); // SSLMode.require
});
test("should infer adapter from TLS_DATABASE_URL using protocol", () => {
process.env.TLS_DATABASE_URL = "mysql://user:pass@host:3306/db";
const options = new SQL();
expect(options.options.adapter).toBe("mysql");
expect(options.options.hostname).toBe("host");
expect(options.options.port).toBe(3306);
expect(options.options.sslMode).toBe(2); // SSLMode.require
});
describe("Adapter-Protocol Validation", () => {
test("should work with explicit adapter and URL without protocol", () => {
const options = new SQL("user:pass@host:3306/db", { adapter: "mysql" });
expect(options.options.adapter).toBe("mysql");
expect(options.options.hostname).toBe("host");
expect(options.options.port).toBe(3306);
});
test("should work with explicit adapter and matching protocol", () => {
const options = new SQL("mysql://user:pass@host:3306/db", { adapter: "mysql" });
expect(options.options.adapter).toBe("mysql");
expect(options.options.hostname).toBe("host");
expect(options.options.port).toBe(3306);
});
test.skipIf(isWindows)("should work with unix:// protocol and explicit adapter", () => {
using sock = Bun.listen({
unix: "/tmp/thisisacoolmysql.sock",
socket: {
data: console.log,
},
});
const options = new SQL(`unix://${sock.unix}`, { adapter: "mysql" });
expect(options.options.adapter).toBe("mysql");
expect(options.options.path).toBe("/tmp/thisisacoolmysql.sock");
unlinkSync(sock.unix);
});
test("should work with sqlite:// protocol and sqlite adapter", () => {
const options = new SQL("sqlite:///tmp/test.db", { adapter: "sqlite" });
expect(options.options.adapter).toBe("sqlite");
expect(options.options.filename).toBe("/tmp/test.db");
});
test("should work with sqlite:// protocol without adapter", () => {
const options = new SQL("sqlite:///tmp/test.db");
expect(options.options.adapter).toBe("sqlite");
expect(options.options.filename).toBe("/tmp/test.db");
});
describe("Explicit options override URL parameters", () => {
test("explicit hostname should override URL hostname", () => {
const options = new SQL("postgres://urluser:urlpass@urlhost:1234/urldb", {
hostname: "explicithost",
});
expect(options.options.hostname).toBe("explicithost");
expect(options.options.port).toBe(1234); // URL port should remain
expect(options.options.username).toBe("urluser"); // URL username should remain
expect(options.options.database).toBe("urldb"); // URL database should remain
});
test("explicit port should override URL port", () => {
const options = new SQL("postgres://urluser:urlpass@urlhost:1234/urldb", {
port: 5432,
});
expect(options.options.hostname).toBe("urlhost"); // URL hostname should remain
expect(options.options.port).toBe(5432);
expect(options.options.username).toBe("urluser"); // URL username should remain
expect(options.options.database).toBe("urldb"); // URL database should remain
});
test("explicit username should override URL username", () => {
const options = new SQL("postgres://urluser:urlpass@urlhost:1234/urldb", {
username: "explicituser",
});
expect(options.options.hostname).toBe("urlhost"); // URL hostname should remain
expect(options.options.port).toBe(1234); // URL port should remain
expect(options.options.username).toBe("explicituser");
expect(options.options.database).toBe("urldb"); // URL database should remain
});
test("explicit password should override URL password", () => {
const options = new SQL("postgres://urluser:urlpass@urlhost:1234/urldb", {
password: "explicitpass",
});
expect(options.options.hostname).toBe("urlhost"); // URL hostname should remain
expect(options.options.port).toBe(1234); // URL port should remain
expect(options.options.username).toBe("urluser"); // URL username should remain
expect(options.options.password).toBe("explicitpass");
expect(options.options.database).toBe("urldb"); // URL database should remain
});
test("explicit database should override URL database", () => {
const options = new SQL("postgres://urluser:urlpass@urlhost:1234/urldb", {
database: "explicitdb",
});
expect(options.options.hostname).toBe("urlhost"); // URL hostname should remain
expect(options.options.port).toBe(1234); // URL port should remain
expect(options.options.username).toBe("urluser"); // URL username should remain
expect(options.options.database).toBe("explicitdb");
});
test("multiple explicit options should override corresponding URL parameters", () => {
const options = new SQL("postgres://urluser:urlpass@urlhost:1234/urldb", {
hostname: "explicithost",
port: 5432,
username: "explicituser",
password: "explicitpass",
database: "explicitdb",
});
expect(options.options.hostname).toBe("explicithost");
expect(options.options.port).toBe(5432);
expect(options.options.username).toBe("explicituser");
expect(options.options.password).toBe("explicitpass");
expect(options.options.database).toBe("explicitdb");
});
test("should work with MySQL URLs and explicit options", () => {
const options = new SQL("mysql://urluser:urlpass@urlhost:3306/urldb", {
hostname: "explicithost",
port: 3307,
username: "explicituser",
});
expect(options.options.adapter).toBe("mysql");
expect(options.options.hostname).toBe("explicithost");
expect(options.options.port).toBe(3307);
expect(options.options.username).toBe("explicituser");
expect(options.options.password).toBe("urlpass"); // URL password should remain
expect(options.options.database).toBe("urldb"); // URL database should remain
});
test("should work with alternative option names (user, pass, db, host)", () => {
const options = new SQL("postgres://urluser:urlpass@urlhost:1234/urldb", {
host: "explicithost",
user: "explicituser",
pass: "explicitpass",
db: "explicitdb",
});
expect(options.options.hostname).toBe("explicithost");
expect(options.options.username).toBe("explicituser");
expect(options.options.password).toBe("explicitpass");
expect(options.options.database).toBe("explicitdb");
});
test("explicit options should override URL even when environment variables are present", () => {
process.env.PGHOST = "envhost";
process.env.PGPORT = "9999";
process.env.PGUSER = "envuser";
const options = new SQL("postgres://urluser:urlpass@urlhost:1234/urldb", {
hostname: "explicithost",
port: 5432,
username: "explicituser",
});
expect(options.options.hostname).toBe("explicithost");
expect(options.options.port).toBe(5432);
expect(options.options.username).toBe("explicituser");
expect(options.options.password).toBe("urlpass"); // URL password should remain since no explicit password
expect(options.options.database).toBe("urldb"); // URL database should remain
});
test("explicit options should have higher precedence than environment-specific variables", () => {
process.env.MYSQL_HOST = "mysqlhost";
process.env.MYSQL_USER = "mysqluser";
process.env.MYSQL_PASSWORD = "mysqlpass";
const options = new SQL("mysql://urluser:urlpass@urlhost:3306/urldb", {
hostname: "explicithost",
username: "explicituser",
});
expect(options.options.adapter).toBe("mysql");
expect(options.options.hostname).toBe("explicithost");
expect(options.options.username).toBe("explicituser");
expect(options.options.password).toBe("urlpass"); // URL password (not env)
expect(options.options.database).toBe("urldb"); // URL database should remain
});
});
});
});