mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-22 15:59:56 -05:00
perf(e2e): skip redundant prisma schema pushes (#9882)
Co-authored-by: starslingdev[bot] <248995740+starslingdev[bot]@users.noreply.github.com> Co-authored-by: Taesu <bytaesu@gmail.com>
This commit is contained in:
co-authored by
starslingdev[bot] <248995740+starslingdev[bot]@users.noreply.github.com>
Taesu
parent
460425da01
commit
f9ac706602
@@ -33,7 +33,7 @@ const { execute } = await testAdapter({
|
||||
const migrationCount = incrementMigrationCount();
|
||||
await generateAuthConfigFile(options);
|
||||
await generatePrismaSchema(options, db, migrationCount, dialect);
|
||||
await pushPrismaSchema(dialect);
|
||||
await pushPrismaSchema(dialect, db);
|
||||
destroyPrismaClient({ migrationCount: migrationCount - 1, dialect });
|
||||
},
|
||||
tests: [
|
||||
|
||||
@@ -35,7 +35,7 @@ const { execute } = await testAdapter({
|
||||
const migrationCount = incrementMigrationCount();
|
||||
await generateAuthConfigFile(options);
|
||||
await generatePrismaSchema(options, db, migrationCount, dialect);
|
||||
await pushPrismaSchema(dialect);
|
||||
await pushPrismaSchema(dialect, db);
|
||||
destroyPrismaClient({ migrationCount: migrationCount - 1, dialect });
|
||||
},
|
||||
tests: [
|
||||
|
||||
@@ -33,7 +33,7 @@ const { execute } = await testAdapter({
|
||||
const migrationCount = incrementMigrationCount();
|
||||
await generateAuthConfigFile(options);
|
||||
await generatePrismaSchema(options, db, migrationCount, dialect);
|
||||
await pushPrismaSchema(dialect);
|
||||
await pushPrismaSchema(dialect, db);
|
||||
destroyPrismaClient({ migrationCount: migrationCount - 1, dialect });
|
||||
},
|
||||
tests: [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, join } from "node:path";
|
||||
@@ -9,6 +9,27 @@ import { DATABASE_URLS } from "./constants";
|
||||
// so we can copy instead of running `prisma generate` again.
|
||||
const lastGeneratedDir = new Map<string, string>();
|
||||
|
||||
// Track the schema last pushed per dialect. Migrations run on every
|
||||
// better-auth options change, but most of them produce a schema that is
|
||||
// already in the database. Then a full `prisma db push --force-reset` (a CLI
|
||||
// subprocess that drops and recreates every table) is not needed, and clearing
|
||||
// the data in-process is enough for test isolation.
|
||||
const lastPushedSchema = new Map<string, string>();
|
||||
|
||||
// The raw query methods used for the data reset. Prisma omits `$transaction`
|
||||
// from the client passed to a transaction callback.
|
||||
type RawQueryClient = {
|
||||
$queryRawUnsafe: <T = unknown>(
|
||||
query: string,
|
||||
...values: unknown[]
|
||||
) => Promise<T>;
|
||||
$executeRawUnsafe: (query: string, ...values: unknown[]) => Promise<number>;
|
||||
};
|
||||
|
||||
type RawClient = RawQueryClient & {
|
||||
$transaction: <T>(fn: (tx: RawQueryClient) => Promise<T>) => Promise<T>;
|
||||
};
|
||||
|
||||
function resolvePrismaCli() {
|
||||
const require = createRequire(import.meta.url);
|
||||
return join(
|
||||
@@ -18,9 +39,51 @@ function resolvePrismaCli() {
|
||||
);
|
||||
}
|
||||
|
||||
export async function pushPrismaSchema(dialect: Dialect) {
|
||||
// Empties every table and resets its identity counter, which leaves the same
|
||||
// state as `db push --force-reset` when the schema is unchanged. Throws on any
|
||||
// failure so the caller can fall back to a full push.
|
||||
async function truncateAllTables(
|
||||
dialect: Dialect,
|
||||
db: RawClient,
|
||||
): Promise<void> {
|
||||
if (dialect === "postgresql") {
|
||||
const rows = await db.$queryRawUnsafe<Array<{ tablename: string }>>(
|
||||
`SELECT tablename FROM pg_tables WHERE schemaname = 'public'`,
|
||||
);
|
||||
const tables = rows.map((r) => `"${r.tablename}"`);
|
||||
if (tables.length > 0) {
|
||||
await db.$executeRawUnsafe(
|
||||
`TRUNCATE TABLE ${tables.join(", ")} RESTART IDENTITY CASCADE`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// The mariadb adapter pools connections, so the session-scoped
|
||||
// FOREIGN_KEY_CHECKS=0 only holds inside an interactive transaction. TRUNCATE
|
||||
// commits implicitly and would end that transaction, so rows are deleted
|
||||
// with DELETE and the counters are reset after the transaction.
|
||||
const tables = await db.$transaction(async (tx) => {
|
||||
await tx.$executeRawUnsafe(`SET FOREIGN_KEY_CHECKS = 0`);
|
||||
// The mariadb driver returns information_schema text columns as binary
|
||||
// buffers, so the name is cast to CHAR.
|
||||
const rows = await tx.$queryRawUnsafe<Array<{ name: unknown }>>(
|
||||
`SELECT CAST(table_name AS CHAR) AS name FROM information_schema.tables WHERE table_schema = DATABASE()`,
|
||||
);
|
||||
const names = rows.map((r) => String(r.name));
|
||||
for (const table of names) {
|
||||
await tx.$executeRawUnsafe(`DELETE FROM \`${table}\``);
|
||||
}
|
||||
await tx.$executeRawUnsafe(`SET FOREIGN_KEY_CHECKS = 1`);
|
||||
return names;
|
||||
});
|
||||
for (const table of tables) {
|
||||
await db.$executeRawUnsafe(`ALTER TABLE \`${table}\` AUTO_INCREMENT = 1`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function pushPrismaSchema(dialect: Dialect, db?: RawClient) {
|
||||
const cwd = import.meta.dirname;
|
||||
const cli = `${process.execPath} ${resolvePrismaCli()}`;
|
||||
const prismaCli = resolvePrismaCli();
|
||||
|
||||
// Write a per-dialect prisma config file (Prisma v7 requires datasource url here)
|
||||
const configPath = join(cwd, `prisma-config-${dialect}.ts`);
|
||||
@@ -48,29 +111,61 @@ export default defineConfig({
|
||||
}
|
||||
const outputDir = join(cwd, outputMatch[1]);
|
||||
|
||||
// When the schema is unchanged since the last push, only clear the data.
|
||||
// sqlite pushes are cheap, so sqlite always does a full push.
|
||||
let didReset = false;
|
||||
if (
|
||||
db != null &&
|
||||
dialect !== "sqlite" &&
|
||||
lastPushedSchema.get(dialect) === schemaKey
|
||||
) {
|
||||
try {
|
||||
await truncateAllTables(dialect, db);
|
||||
didReset = true;
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[pushPrismaSchema] data-reset fast-path failed for ${dialect}, falling back to full db push:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
execSync(
|
||||
`${cli} db push --force-reset --accept-data-loss --config ${configPath}`,
|
||||
{
|
||||
stdio: "pipe",
|
||||
cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
// Prisma v7 blocks --force-reset when it detects an AI agent; this env var grants consent.
|
||||
PRISMA_USER_CONSENT_FOR_DANGEROUS_AI_ACTION:
|
||||
"I am running tests in a local development environment",
|
||||
if (!didReset) {
|
||||
execFileSync(
|
||||
process.execPath,
|
||||
[
|
||||
prismaCli,
|
||||
"db",
|
||||
"push",
|
||||
"--force-reset",
|
||||
"--accept-data-loss",
|
||||
"--config",
|
||||
configPath,
|
||||
],
|
||||
{
|
||||
stdio: "pipe",
|
||||
cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
// Prisma v7 blocks --force-reset when it detects an AI agent; this env var grants consent.
|
||||
PRISMA_USER_CONSENT_FOR_DANGEROUS_AI_ACTION:
|
||||
"I am running tests in a local development environment",
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
);
|
||||
lastPushedSchema.set(dialect, schemaKey);
|
||||
}
|
||||
|
||||
const prevDir = lastGeneratedDir.get(schemaKey);
|
||||
if (prevDir && fs.existsSync(prevDir)) {
|
||||
fs.cpSync(prevDir, outputDir, { recursive: true });
|
||||
} else {
|
||||
execSync(`${cli} generate --config ${configPath}`, {
|
||||
stdio: "pipe",
|
||||
cwd,
|
||||
});
|
||||
execFileSync(
|
||||
process.execPath,
|
||||
[prismaCli, "generate", "--config", configPath],
|
||||
{ stdio: "pipe", cwd },
|
||||
);
|
||||
lastGeneratedDir.set(schemaKey, outputDir);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user