Skip to content

Commit b82cf86

Browse files
committed
fix(prepare-db): address REV review feedback
- Fix ALTER USER filtering to preserve comments and handle edge cases - Add provider validation with warning for unknown providers - Fix help text to only mention implemented providers (supabase) - Add comprehensive tests: - verifyInitSetup skips search_path check for supabase - buildInitPlan preserves comments when filtering ALTER USER - validateProvider returns correct results - CLI --provider supabase skips role step - CLI warns about unknown provider
1 parent e4647b3 commit b82cf86

File tree

3 files changed

+126
-3
lines changed

3 files changed

+126
-3
lines changed

cli/bin/postgres-ai.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { Client } from "pg";
1212
import { startMcpServer } from "../lib/mcp-server";
1313
import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue, createIssue, updateIssue, updateIssueComment } from "../lib/issues";
1414
import { resolveBaseUrls } from "../lib/util";
15-
import { applyInitPlan, buildInitPlan, connectWithSslFallback, DEFAULT_MONITORING_USER, redactPasswordsInSql, resolveAdminConnection, resolveMonitoringPassword, verifyInitSetup } from "../lib/init";
15+
import { applyInitPlan, buildInitPlan, connectWithSslFallback, DEFAULT_MONITORING_USER, KNOWN_PROVIDERS, redactPasswordsInSql, resolveAdminConnection, resolveMonitoringPassword, validateProvider, verifyInitSetup } from "../lib/init";
1616
import * as pkce from "../lib/pkce";
1717
import * as authServer from "../lib/auth-server";
1818
import { maskSecret } from "../lib/util";
@@ -564,7 +564,7 @@ program
564564
.option("--monitoring-user <name>", "Monitoring role name to create/update", DEFAULT_MONITORING_USER)
565565
.option("--password <password>", "Monitoring role password (overrides PGAI_MON_PASSWORD)")
566566
.option("--skip-optional-permissions", "Skip optional permissions (RDS/self-managed extras)", false)
567-
.option("--provider <provider>", "Database provider (e.g., supabase, rds, aurora). Affects which steps are executed.")
567+
.option("--provider <provider>", "Database provider (e.g., supabase). Affects which steps are executed.")
568568
.option("--verify", "Verify that monitoring role/permissions are in place (no changes)", false)
569569
.option("--reset-password", "Reset monitoring role password only (no other changes)", false)
570570
.option("--print-sql", "Print SQL plan and exit (no changes applied)", false)
@@ -644,6 +644,12 @@ program
644644
const shouldPrintSql = !!opts.printSql;
645645
const redactPasswords = (sql: string): string => redactPasswordsInSql(sql);
646646

647+
// Validate provider and warn if unknown
648+
const providerWarning = validateProvider(opts.provider);
649+
if (providerWarning) {
650+
console.warn(`⚠ ${providerWarning}`);
651+
}
652+
647653
// Offline mode: allow printing SQL without providing/using an admin connection.
648654
// Useful for audits/reviews; caller can provide -d/PGDATABASE.
649655
if (!conn && !opts.dbUrl && !opts.host && !opts.port && !opts.username && !opts.adminPassword) {

cli/lib/init.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ export const DEFAULT_MONITORING_USER = "postgres_ai_mon";
1515
*/
1616
export type DbProvider = string;
1717

18+
/** Known providers with special handling. Unknown providers are treated as self-managed. */
19+
export const KNOWN_PROVIDERS = ["self-managed", "supabase"] as const;
20+
1821
/** Providers where we skip role creation (users managed externally). */
1922
const SKIP_ROLE_CREATION_PROVIDERS = ["supabase"];
2023

@@ -24,6 +27,12 @@ const SKIP_ALTER_USER_PROVIDERS = ["supabase"];
2427
/** Providers where we skip search_path verification (not set via ALTER USER). */
2528
const SKIP_SEARCH_PATH_CHECK_PROVIDERS = ["supabase"];
2629

30+
/** Check if a provider is known and return a warning message if not. */
31+
export function validateProvider(provider: string | undefined): string | null {
32+
if (!provider || KNOWN_PROVIDERS.includes(provider as any)) return null;
33+
return `Unknown provider "${provider}". Known providers: ${KNOWN_PROVIDERS.join(", ")}. Treating as self-managed.`;
34+
}
35+
2736
export type PgClientConfig = {
2837
connectionString?: string;
2938
host?: string;
@@ -525,7 +534,13 @@ end $$;`;
525534
if (SKIP_ALTER_USER_PROVIDERS.includes(provider)) {
526535
permissionsSql = permissionsSql
527536
.split("\n")
528-
.filter((line) => !line.toLowerCase().includes("alter user"))
537+
.filter((line) => {
538+
const trimmed = line.trim();
539+
// Keep comments and empty lines
540+
if (trimmed.startsWith("--") || trimmed === "") return true;
541+
// Filter out ALTER USER statements (case-insensitive, flexible whitespace)
542+
return !/^\s*alter\s+user\s+/i.test(line);
543+
})
529544
.join("\n");
530545
}
531546

cli/test/init.test.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,91 @@ describe("init module", () => {
326326
expect(calls[calls.length - 1].toLowerCase()).toBe("rollback;");
327327
});
328328

329+
test("verifyInitSetup skips search_path check for supabase provider", async () => {
330+
const calls: string[] = [];
331+
const client = {
332+
query: async (sql: string, params?: any) => {
333+
calls.push(String(sql));
334+
335+
if (String(sql).toLowerCase().startsWith("begin isolation level repeatable read")) {
336+
return { rowCount: 1, rows: [] };
337+
}
338+
if (String(sql).toLowerCase() === "rollback;") {
339+
return { rowCount: 1, rows: [] };
340+
}
341+
// Return empty rolconfig - would fail without provider=supabase
342+
if (String(sql).includes("select rolconfig")) {
343+
return { rowCount: 1, rows: [{ rolconfig: null }] };
344+
}
345+
if (String(sql).includes("from pg_catalog.pg_roles")) {
346+
return { rowCount: 1, rows: [{ rolname: DEFAULT_MONITORING_USER }] };
347+
}
348+
if (String(sql).includes("has_database_privilege")) {
349+
return { rowCount: 1, rows: [{ ok: true }] };
350+
}
351+
if (String(sql).includes("pg_has_role")) {
352+
return { rowCount: 1, rows: [{ ok: true }] };
353+
}
354+
if (String(sql).includes("has_table_privilege")) {
355+
return { rowCount: 1, rows: [{ ok: true }] };
356+
}
357+
if (String(sql).includes("to_regclass")) {
358+
return { rowCount: 1, rows: [{ ok: true }] };
359+
}
360+
if (String(sql).includes("has_function_privilege")) {
361+
return { rowCount: 1, rows: [{ ok: true }] };
362+
}
363+
if (String(sql).includes("has_schema_privilege")) {
364+
return { rowCount: 1, rows: [{ ok: true }] };
365+
}
366+
367+
throw new Error(`unexpected sql: ${sql} params=${JSON.stringify(params)}`);
368+
},
369+
};
370+
371+
// With provider=supabase, should pass even without search_path
372+
const r = await init.verifyInitSetup({
373+
client: client as any,
374+
database: "mydb",
375+
monitoringUser: DEFAULT_MONITORING_USER,
376+
includeOptionalPermissions: false,
377+
provider: "supabase",
378+
});
379+
expect(r.ok).toBe(true);
380+
expect(r.missingRequired.length).toBe(0);
381+
// Should not have queried for rolconfig since we skip search_path check
382+
expect(calls.some((c) => c.includes("select rolconfig"))).toBe(false);
383+
});
384+
385+
test("buildInitPlan preserves comments when filtering ALTER USER", async () => {
386+
const plan = await init.buildInitPlan({
387+
database: "mydb",
388+
monitoringUser: DEFAULT_MONITORING_USER,
389+
monitoringPassword: "pw",
390+
includeOptionalPermissions: false,
391+
provider: "supabase",
392+
});
393+
const permStep = plan.steps.find((s) => s.name === "02.permissions");
394+
expect(permStep).toBeDefined();
395+
// Should have removed ALTER USER but kept comments
396+
expect(permStep!.sql.toLowerCase()).not.toMatch(/^\s*alter\s+user/m);
397+
// Should still have comment lines
398+
expect(permStep!.sql).toMatch(/^--/m);
399+
});
400+
401+
test("validateProvider returns null for known providers", () => {
402+
expect(init.validateProvider(undefined)).toBe(null);
403+
expect(init.validateProvider("self-managed")).toBe(null);
404+
expect(init.validateProvider("supabase")).toBe(null);
405+
});
406+
407+
test("validateProvider returns warning for unknown providers", () => {
408+
const warning = init.validateProvider("unknown-provider");
409+
expect(warning).not.toBe(null);
410+
expect(warning).toMatch(/Unknown provider/);
411+
expect(warning).toMatch(/unknown-provider/);
412+
});
413+
329414
test("redactPasswordsInSql redacts password literals with embedded quotes", async () => {
330415
const plan = await init.buildInitPlan({
331416
database: "mydb",
@@ -355,6 +440,23 @@ describe("CLI commands", () => {
355440
expect(r.stdout).toMatch(new RegExp(`grant connect on database "mydb" to "${DEFAULT_MONITORING_USER}"`, "i"));
356441
});
357442

443+
test("cli: prepare-db --print-sql with --provider supabase skips role step", () => {
444+
const r = runCli(["prepare-db", "--print-sql", "-d", "mydb", "--password", "monpw", "--provider", "supabase"]);
445+
expect(r.status).toBe(0);
446+
expect(r.stdout).toMatch(/provider: supabase/);
447+
// Should not have 01.role step
448+
expect(r.stdout).not.toMatch(/-- 01\.role/);
449+
// Should have 02.permissions step
450+
expect(r.stdout).toMatch(/-- 02\.permissions/);
451+
});
452+
453+
test("cli: prepare-db warns about unknown provider", () => {
454+
const r = runCli(["prepare-db", "--print-sql", "-d", "mydb", "--password", "monpw", "--provider", "unknown-cloud"]);
455+
expect(r.status).toBe(0);
456+
// Should warn about unknown provider
457+
expect(r.stderr).toMatch(/Unknown provider.*unknown-cloud/);
458+
});
459+
358460
test("pgai wrapper forwards to postgresai CLI", () => {
359461
const r = runPgai(["--help"]);
360462
expect(r.status).toBe(0);

0 commit comments

Comments
 (0)