fix(db): handle camelCase postgres metadata keys in migration schema detection (#8092)

This commit is contained in:
Taesu
2026-02-22 13:01:51 +09:00
committed by GitHub
parent 3ec7d41a91
commit cbd86131d5
2 changed files with 75 additions and 13 deletions

View File

@@ -1,4 +1,5 @@
import type { BetterAuthOptions } from "@better-auth/core";
import { CamelCasePlugin, Kysely, PostgresDialect } from "kysely";
import { Pool } from "pg";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { betterAuth } from "../auth/full";
@@ -33,6 +34,10 @@ describe.runIf(isPostgresAvailable)(
const customSchemaPool = new Pool({
connectionString: `${CONNECTION_STRING}?options=-c search_path=${customSchema}`,
});
const customSchemaKysely = new Kysely({
dialect: new PostgresDialect({ pool: customSchemaPool }),
plugins: [new CamelCasePlugin()],
});
beforeAll(async () => {
// Setup: Create custom schema and a table in public schema
@@ -84,6 +89,54 @@ describe.runIf(isPostgresAvailable)(
expect(userTableCreated?.fields).toHaveProperty("emailVerified");
});
/**
* @see https://github.com/better-auth/better-auth/issues/7926
*/
it("should detect custom schema with CamelCasePlugin enabled", async () => {
// Create a user table in the custom schema so it should be detected as existing
await customSchemaPool.query(`
CREATE TABLE IF NOT EXISTS ${customSchema}.user (
id TEXT PRIMARY KEY NOT NULL,
email TEXT NOT NULL,
name TEXT NOT NULL,
"emailVerified" BOOLEAN NOT NULL,
image TEXT,
"createdAt" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL,
"updatedAt" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL
);
`);
try {
const config: BetterAuthOptions = {
database: {
db: customSchemaKysely,
type: "postgres",
},
emailAndPassword: {
enabled: true,
},
};
const { toBeCreated, toBeAdded } = await getMigrations(config);
// user table exists in custom schema, so it should NOT be in toBeCreated
const userTableCreated = toBeCreated.find((t) => t.table === "user");
const userTableToBeAdded = toBeAdded.find((t) => t.table === "user");
expect(userTableCreated).toBeUndefined();
expect(userTableToBeAdded).toBeUndefined();
// Other tables should still need to be created
const sessionTable = toBeCreated.find((t) => t.table === "session");
expect(sessionTable).toBeDefined();
} finally {
// Cleanup: drop the user table so subsequent tests are not affected
await customSchemaPool.query(
`DROP TABLE IF EXISTS ${customSchema}.user CASCADE`,
);
}
});
it("should not be affected by tables in public schema when using custom schema", async () => {
// Even though there's a 'user' table in public schema,
// when we use custom schema, migrations should create a new user table

View File

@@ -98,14 +98,17 @@ export function matchType(
*/
async function getPostgresSchema(db: Kysely<unknown>): Promise<string> {
try {
const result = await sql<{ search_path: string }>`SHOW search_path`.execute(
db,
);
if (result.rows[0]?.search_path) {
const result = await sql<{
search_path?: string;
searchPath?: string;
}>`SHOW search_path`.execute(db);
const searchPath =
result.rows[0]?.search_path ?? result.rows[0]?.searchPath;
if (searchPath) {
// search_path can be a comma-separated list like "$user, public" or '"$user", public'
// Supabase may return escaped format like '"\$user", public'
// We want the first non-variable schema
const schemas = result.rows[0].search_path
const schemas = searchPath
.split(",")
.map((s) => s.trim())
// Remove quotes and filter out variables like $user
@@ -150,13 +153,18 @@ export async function getMigrations(config: BetterAuthOptions) {
// Verify the schema exists
try {
const schemaCheck = await sql<{ schema_name: string }>`
SELECT schema_name
FROM information_schema.schemata
const schemaCheck = await sql<{
schema_name?: string;
schemaName?: string;
}>`
SELECT schema_name
FROM information_schema.schemata
WHERE schema_name = ${currentSchema}
`.execute(db);
if (!schemaCheck.rows[0]) {
const schemaExists =
schemaCheck.rows[0]?.schema_name ?? schemaCheck.rows[0]?.schemaName;
if (!schemaExists) {
logger.warn(
`Schema '${currentSchema}' does not exist. Tables will be inspected from available schemas. Consider creating the schema first or checking your database configuration.`,
);
@@ -176,16 +184,17 @@ export async function getMigrations(config: BetterAuthOptions) {
// Get tables with their schema information
try {
const tablesInSchema = await sql<{
table_name: string;
table_name?: string;
tableName?: string;
}>`
SELECT table_name
FROM information_schema.tables
SELECT table_name
FROM information_schema.tables
WHERE table_schema = ${currentSchema}
AND table_type = 'BASE TABLE'
`.execute(db);
const tableNamesInSchema = new Set(
tablesInSchema.rows.map((row) => row.table_name),
tablesInSchema.rows.map((row) => row.table_name ?? row.tableName),
);
// Filter to only tables that exist in the target schema