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:
starslingdev[bot]
2026-10-19 11:15:24 +09:00
committed by GitHub
co-authored by starslingdev[bot] <248995740+starslingdev[bot]@users.noreply.github.com> Taesu
parent 460425da01
commit f9ac706602
4 changed files with 117 additions and 22 deletions
@@ -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) {