diff --git a/src/js/internal/sql/shared.ts b/src/js/internal/sql/shared.ts index 8f5d302aa0..540fdf1a3b 100644 --- a/src/js/internal/sql/shared.ts +++ b/src/js/internal/sql/shared.ts @@ -422,6 +422,28 @@ function parseOptions( // Step 2: Determine the adapter (without reading environment variables yet) const adapter = determineAdapter(options, inputUrl, env); + // Step 2.5: Validate adapter matches protocol if URL is provided + if (inputUrl) { + let urlToValidate: URL; + try { + if (typeof inputUrl === "string") { + // Parse the URL for validation - handle SQLite URLs specially + if (parseDefinitelySqliteUrl(inputUrl) !== null) { + // Create a fake URL for SQLite validation + urlToValidate = new URL("sqlite:///" + encodeURIComponent(inputUrl)); + } else { + urlToValidate = parseUrlForAdapter(inputUrl, adapter); + } + } else { + urlToValidate = inputUrl; + } + validateAdapterProtocolMatch(adapter, urlToValidate); + } catch (error) { + // If URL parsing fails or validation fails, throw the error + throw error; + } + } + // Handle SQLite early since it has different logic if (adapter === "sqlite") { return handleSQLiteOptions(options, inputUrl, env); @@ -467,12 +489,7 @@ function parseOptions( } } - // Step 4: Validate adapter matches protocol if URL is provided - if (finalUrl && inputUrl) { - validateAdapterProtocolMatch(adapter, finalUrl); - } - - // Step 5: Normalize and validate options for the specific adapter + // Step 4: Normalize and validate options for the specific adapter return normalizeOptionsForAdapter(adapter, options, finalUrl, env, sslMode); } diff --git a/test/js/sql/adapter-env-var-precedence.test.ts b/test/js/sql/adapter-env-var-precedence.test.ts index 6fa25dd003..c94f7e5e7f 100644 --- a/test/js/sql/adapter-env-var-precedence.test.ts +++ b/test/js/sql/adapter-env-var-precedence.test.ts @@ -304,4 +304,80 @@ describe("SQL adapter environment variable precedence", () => { expect(options.options.sslMode).toBe(2); // SSLMode.require restoreEnv(); }); + + describe("Adapter-Protocol Validation", () => { + test("should work with explicit adapter and URL without protocol", () => { + cleanEnv(); + + 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); + restoreEnv(); + }); + + test("should work with explicit adapter and matching protocol", () => { + cleanEnv(); + + 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); + restoreEnv(); + }); + + test("should throw error when adapter conflicts with protocol (mysql adapter with postgres protocol)", () => { + cleanEnv(); + + expect(() => { + new SQL("postgres://user:pass@host:5432/db", { adapter: "mysql" }); + }).toThrow(/Protocol 'postgres' is not compatible with adapter 'mysql'/); + restoreEnv(); + }); + + test("should throw error when adapter conflicts with protocol (postgres adapter with mysql protocol)", () => { + cleanEnv(); + + expect(() => { + new SQL("mysql://user:pass@host:3306/db", { adapter: "postgres" }); + }).toThrow(/Protocol 'mysql' is not compatible with adapter 'postgres'/); + restoreEnv(); + }); + + test("should throw error when sqlite adapter used with mysql protocol", () => { + cleanEnv(); + + expect(() => { + new SQL("mysql://user:pass@host:3306/db", { adapter: "sqlite" }); + }).toThrow(/Protocol 'mysql' is not compatible with adapter 'sqlite'/); + restoreEnv(); + }); + + test("should throw error when mysql adapter used with postgres protocol", () => { + cleanEnv(); + + expect(() => { + new SQL("postgres://user:pass@host:5432/db", { adapter: "mysql" }); + }).toThrow(/Protocol 'postgres' is not compatible with adapter 'mysql'/); + restoreEnv(); + }); + + test("should work with unix:// protocol and explicit adapter", () => { + cleanEnv(); + + const options = new SQL("unix:///tmp/mysql.sock", { adapter: "mysql" }); + expect(options.options.adapter).toBe("mysql"); + expect(options.options.path).toBe("/tmp/mysql.sock"); + restoreEnv(); + }); + + test("should work with sqlite:// protocol and sqlite adapter", () => { + cleanEnv(); + + const options = new SQL("sqlite:///tmp/test.db", { adapter: "sqlite" }); + expect(options.options.adapter).toBe("sqlite"); + expect(options.options.filename).toBe("/tmp/test.db"); + restoreEnv(); + }); + }); });