Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { mkdir, unlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";

import {
liveMigrationVersion,
queryLiveDb,
requireLiveSuccess,
test,
throwWithCleanup,
} from "../../../../../tests/helpers/live.ts";

test("amends the migration history status on the remote database", async ({
cli,
project,
workspace,
}) => {
const version = liveMigrationVersion();
const migrations = join(workspace.path, "supabase", "migrations");
await mkdir(migrations, { recursive: true });
const migrationFile = join(migrations, `${version}_e2e_repair.sql`);
// `repair --status applied` records the file's statements in migration
// history without executing them, so this table is never actually created.
await writeFile(migrationFile, `create table if not exists e2e_repair_${version} (id int);\n`);
Comment thread
7ttp marked this conversation as resolved.

let targetError: unknown;
let versionReverted = false;
const cleanupErrors: Array<unknown> = [];
try {
const applied = await cli([
"migration",
"repair",
version,
"--status",
"applied",
"--db-url",
project.dbUrl,
]);
expect(applied.exitCode, applied.stderr).toBe(0);
expect(applied.stderr, applied.stdout).toContain("=> applied");
await unlink(migrationFile);

const recorded = await queryLiveDb(
project.dbUrl,
"select version from supabase_migrations.schema_migrations where version = $1",
[version],
);
expect(recorded).toHaveLength(1);

const reverted = await cli([
"migration",
"repair",
version,
"--status",
"reverted",
"--db-url",
project.dbUrl,
]);
expect(reverted.exitCode, reverted.stderr).toBe(0);
expect(reverted.stderr, reverted.stdout).toContain("=> reverted");

const remaining = await queryLiveDb(
project.dbUrl,
"select version from supabase_migrations.schema_migrations where version = $1",
[version],
);
expect(remaining).toHaveLength(0);
// Only skip the teardown revert once the row is verifiably gone.
versionReverted = true;
} catch (error) {
targetError = error;
} finally {
if (!versionReverted) {
try {
const cleanup = await cli([
"migration",
"repair",
version,
"--status",
"reverted",
"--db-url",
project.dbUrl,
]);
requireLiveSuccess(cleanup, "migration repair cleanup");
} catch (error) {
cleanupErrors.push(error);
}
}
}
throwWithCleanup(targetError, cleanupErrors);
});
102 changes: 102 additions & 0 deletions apps/cli/src/legacy/commands/migration/up/up.live.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { mkdir, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";

import {
liveMigrationVersion,
queryLiveDb,
requireLiveSuccess,
test,
throwWithCleanup,
} from "../../../../../tests/helpers/live.ts";

test("applies a test-written migration to the remote database", async ({
cli,
project,
workspace,
}) => {
const version = liveMigrationVersion();
const migrations = join(workspace.path, "supabase", "migrations");
await mkdir(migrations, { recursive: true });

// The serial suite shares one remote project, so seed a local stub for every
// version already in remote history — otherwise `migration up` rejects them
// as missing locally. The history table may not exist yet on a fresh project.
let remoteVersions: Array<{ version: string }> = [];
try {
remoteVersions = await queryLiveDb(
project.dbUrl,
"select version from supabase_migrations.schema_migrations order by version",
);
} catch (error) {
// 42P01 (undefined relation) covers the fresh-project case where the
// history table or its schema does not exist yet; anything else is a real
// failure the test must surface.
if ((error as { code?: string }).code !== "42P01") throw error;
remoteVersions = [];
}
for (const row of remoteVersions) {
await writeFile(
join(migrations, `${row.version}_preexisting_remote.sql`),
"-- stub for a version already in remote history\n",
);
}

const migrationFile = join(migrations, `${version}_e2e_up.sql`);
await writeFile(migrationFile, `create table if not exists e2e_up_${version} (id int);\n`);

let targetError: unknown;
const cleanupErrors: Array<unknown> = [];
try {
const applied = await cli(["migration", "up", "--db-url", project.dbUrl]);
expect(applied.exitCode, applied.stderr).toBe(0);
expect(applied.stderr, applied.stdout).toContain("Applying migration");
Comment thread
7ttp marked this conversation as resolved.

const history = await queryLiveDb(
project.dbUrl,
"select version from supabase_migrations.schema_migrations where version = $1",
[version],
);
expect(history).toHaveLength(1);

const created = await queryLiveDb(project.dbUrl, "select to_regclass($1) as table_oid", [
`public.e2e_up_${version}`,
]);
expect(created[0]?.["table_oid"], "migration up must execute the migration sql").not.toBeNull();
} catch (error) {
targetError = error;
} finally {
try {
await rm(migrationFile, { force: true });
} catch (error) {
cleanupErrors.push(error);
}
try {
const dropped = await cli([
"db",
"query",
`drop table if exists e2e_up_${version}`,
"--db-url",
project.dbUrl,
]);
requireLiveSuccess(dropped, "db query cleanup after migration up");
} catch (error) {
cleanupErrors.push(error);
}
try {
const reverted = await cli([
"migration",
"repair",
version,
"--status",
"reverted",
"--db-url",
project.dbUrl,
]);
requireLiveSuccess(reverted, "migration repair cleanup after migration up");
} catch (error) {
cleanupErrors.push(error);
}
}
throwWithCleanup(targetError, cleanupErrors);
});
36 changes: 36 additions & 0 deletions apps/cli/tests/helpers/live.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";

import pg from "pg";
import { inject, test as vitestTest } from "vitest";

import { makeTempHome, runSupabase } from "./cli.ts";
Expand Down Expand Up @@ -144,6 +145,41 @@ export async function removeStorageLiveObject(
}
}

/**
* Unique migration version for a live test: a sortable `YYYYMMDDHHMMSS` UTC
* stamp plus four random digits, so it always orders after any conventional
* timestamp version already in the shared project's migration history.
*/
export function liveMigrationVersion(): string {
const stamp = new Date()
.toISOString()
.replaceAll(/[-:TZ.]/gu, "")
.slice(0, 14);
return `${stamp}${Math.floor(Math.random() * 10_000)
.toString()
.padStart(4, "0")}`;
Comment thread
7ttp marked this conversation as resolved.
}

/**
* Runs one query against the live project over a direct pg connection, so
* live assertions can verify database state without invoking another CLI
* command.
*/
export async function queryLiveDb<T extends Record<string, unknown>>(
dbUrl: string,
query: string,
values?: ReadonlyArray<unknown>,
): Promise<T[]> {
const client = new pg.Client({ connectionString: dbUrl });
await client.connect();
try {
const result = await client.query(query, values === undefined ? undefined : [...values]);
return result.rows as T[];
} finally {
await client.end();
}
}

/** Rethrow a target failure without discarding failures from exact cleanup. */
export function throwWithCleanup(primary: unknown, cleanup: ReadonlyArray<unknown>): void {
if (primary !== undefined) {
Expand Down
Loading