postgresai 0.14.0-beta.13 → 0.14.0-beta.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/bin/postgres-ai.ts +324 -12
  2. package/dist/bin/postgres-ai.js +1009 -46
  3. package/dist/sql/02.extensions.sql +8 -0
  4. package/dist/sql/{02.permissions.sql → 03.permissions.sql} +1 -0
  5. package/dist/sql/sql/02.extensions.sql +8 -0
  6. package/dist/sql/sql/{02.permissions.sql → 03.permissions.sql} +1 -0
  7. package/dist/sql/sql/uninit/01.helpers.sql +5 -0
  8. package/dist/sql/sql/uninit/02.permissions.sql +30 -0
  9. package/dist/sql/sql/uninit/03.role.sql +27 -0
  10. package/dist/sql/uninit/01.helpers.sql +5 -0
  11. package/dist/sql/uninit/02.permissions.sql +30 -0
  12. package/dist/sql/uninit/03.role.sql +27 -0
  13. package/lib/checkup-dictionary.ts +114 -0
  14. package/lib/checkup.ts +130 -14
  15. package/lib/init.ts +109 -8
  16. package/package.json +9 -7
  17. package/scripts/embed-checkup-dictionary.ts +106 -0
  18. package/sql/02.extensions.sql +8 -0
  19. package/sql/{02.permissions.sql → 03.permissions.sql} +1 -0
  20. package/sql/uninit/01.helpers.sql +5 -0
  21. package/sql/uninit/02.permissions.sql +30 -0
  22. package/sql/uninit/03.role.sql +27 -0
  23. package/test/checkup.test.ts +17 -18
  24. package/test/init.test.ts +245 -11
  25. package/lib/metrics-embedded.ts +0 -79
  26. /package/dist/sql/{03.optional_rds.sql → 04.optional_rds.sql} +0 -0
  27. /package/dist/sql/{04.optional_self_managed.sql → 05.optional_self_managed.sql} +0 -0
  28. /package/dist/sql/{05.helpers.sql → 06.helpers.sql} +0 -0
  29. /package/dist/sql/sql/{03.optional_rds.sql → 04.optional_rds.sql} +0 -0
  30. /package/dist/sql/sql/{04.optional_self_managed.sql → 05.optional_self_managed.sql} +0 -0
  31. /package/dist/sql/sql/{05.helpers.sql → 06.helpers.sql} +0 -0
  32. /package/sql/{03.optional_rds.sql → 04.optional_rds.sql} +0 -0
  33. /package/sql/{04.optional_self_managed.sql → 05.optional_self_managed.sql} +0 -0
  34. /package/sql/{05.helpers.sql → 06.helpers.sql} +0 -0
@@ -0,0 +1,8 @@
1
+ -- Extensions required for postgres_ai monitoring
2
+
3
+ -- Enable pg_stat_statements for query performance monitoring
4
+ -- Note: Uses IF NOT EXISTS because extension may already be installed.
5
+ -- We do NOT drop this extension in unprepare-db since it may have been pre-existing.
6
+ create extension if not exists pg_stat_statements;
7
+
8
+
@@ -8,6 +8,7 @@ grant pg_monitor to {{ROLE_IDENT}};
8
8
  grant select on pg_catalog.pg_index to {{ROLE_IDENT}};
9
9
 
10
10
  -- Create postgres_ai schema for our objects
11
+ -- Using IF NOT EXISTS for idempotency - prepare-db can be run multiple times
11
12
  create schema if not exists postgres_ai;
12
13
  grant usage on schema postgres_ai to {{ROLE_IDENT}};
13
14
 
@@ -0,0 +1,8 @@
1
+ -- Extensions required for postgres_ai monitoring
2
+
3
+ -- Enable pg_stat_statements for query performance monitoring
4
+ -- Note: Uses IF NOT EXISTS because extension may already be installed.
5
+ -- We do NOT drop this extension in unprepare-db since it may have been pre-existing.
6
+ create extension if not exists pg_stat_statements;
7
+
8
+
@@ -8,6 +8,7 @@ grant pg_monitor to {{ROLE_IDENT}};
8
8
  grant select on pg_catalog.pg_index to {{ROLE_IDENT}};
9
9
 
10
10
  -- Create postgres_ai schema for our objects
11
+ -- Using IF NOT EXISTS for idempotency - prepare-db can be run multiple times
11
12
  create schema if not exists postgres_ai;
12
13
  grant usage on schema postgres_ai to {{ROLE_IDENT}};
13
14
 
@@ -0,0 +1,5 @@
1
+ -- Drop helper functions created by prepare-db (template-filled by cli/lib/init.ts)
2
+ -- Run before dropping the postgres_ai schema.
3
+
4
+ drop function if exists postgres_ai.explain_generic(text, text, text);
5
+ drop function if exists postgres_ai.table_describe(text);
@@ -0,0 +1,30 @@
1
+ -- Revoke permissions and drop objects created by prepare-db (template-filled by cli/lib/init.ts)
2
+
3
+ -- Drop the postgres_ai.pg_statistic view
4
+ drop view if exists postgres_ai.pg_statistic;
5
+
6
+ -- Drop the postgres_ai schema (CASCADE to handle any remaining objects)
7
+ drop schema if exists postgres_ai cascade;
8
+
9
+ -- Revoke permissions from the monitoring role
10
+ -- Use a DO block to handle the case where the role doesn't exist
11
+ do $$ begin
12
+ revoke pg_monitor from {{ROLE_IDENT}};
13
+ exception when undefined_object then
14
+ null; -- Role doesn't exist, nothing to revoke
15
+ end $$;
16
+
17
+ do $$ begin
18
+ revoke select on pg_catalog.pg_index from {{ROLE_IDENT}};
19
+ exception when undefined_object then
20
+ null; -- Role doesn't exist
21
+ end $$;
22
+
23
+ do $$ begin
24
+ revoke connect on database {{DB_IDENT}} from {{ROLE_IDENT}};
25
+ exception when undefined_object then
26
+ null; -- Role doesn't exist
27
+ end $$;
28
+
29
+ -- Note: USAGE on public is typically granted by default; we don't revoke it
30
+ -- to avoid breaking other applications that may rely on it.
@@ -0,0 +1,27 @@
1
+ -- Drop the monitoring role created by prepare-db (template-filled by cli/lib/init.ts)
2
+ -- This must run after revoking all permissions from the role.
3
+
4
+ -- Use a DO block to handle the case where the role doesn't exist
5
+ do $$ begin
6
+ -- Reassign owned objects to current user before dropping
7
+ -- This handles any objects that might have been created by the role
8
+ begin
9
+ execute format('reassign owned by %I to current_user', {{ROLE_LITERAL}});
10
+ exception when undefined_object then
11
+ null; -- Role doesn't exist, nothing to reassign
12
+ end;
13
+
14
+ -- Drop owned objects (in case reassign didn't work for some objects)
15
+ begin
16
+ execute format('drop owned by %I', {{ROLE_LITERAL}});
17
+ exception when undefined_object then
18
+ null; -- Role doesn't exist
19
+ end;
20
+
21
+ -- Drop the role
22
+ begin
23
+ execute format('drop role %I', {{ROLE_LITERAL}});
24
+ exception when undefined_object then
25
+ null; -- Role doesn't exist, that's fine
26
+ end;
27
+ end $$;
@@ -0,0 +1,5 @@
1
+ -- Drop helper functions created by prepare-db (template-filled by cli/lib/init.ts)
2
+ -- Run before dropping the postgres_ai schema.
3
+
4
+ drop function if exists postgres_ai.explain_generic(text, text, text);
5
+ drop function if exists postgres_ai.table_describe(text);
@@ -0,0 +1,30 @@
1
+ -- Revoke permissions and drop objects created by prepare-db (template-filled by cli/lib/init.ts)
2
+
3
+ -- Drop the postgres_ai.pg_statistic view
4
+ drop view if exists postgres_ai.pg_statistic;
5
+
6
+ -- Drop the postgres_ai schema (CASCADE to handle any remaining objects)
7
+ drop schema if exists postgres_ai cascade;
8
+
9
+ -- Revoke permissions from the monitoring role
10
+ -- Use a DO block to handle the case where the role doesn't exist
11
+ do $$ begin
12
+ revoke pg_monitor from {{ROLE_IDENT}};
13
+ exception when undefined_object then
14
+ null; -- Role doesn't exist, nothing to revoke
15
+ end $$;
16
+
17
+ do $$ begin
18
+ revoke select on pg_catalog.pg_index from {{ROLE_IDENT}};
19
+ exception when undefined_object then
20
+ null; -- Role doesn't exist
21
+ end $$;
22
+
23
+ do $$ begin
24
+ revoke connect on database {{DB_IDENT}} from {{ROLE_IDENT}};
25
+ exception when undefined_object then
26
+ null; -- Role doesn't exist
27
+ end $$;
28
+
29
+ -- Note: USAGE on public is typically granted by default; we don't revoke it
30
+ -- to avoid breaking other applications that may rely on it.
@@ -0,0 +1,27 @@
1
+ -- Drop the monitoring role created by prepare-db (template-filled by cli/lib/init.ts)
2
+ -- This must run after revoking all permissions from the role.
3
+
4
+ -- Use a DO block to handle the case where the role doesn't exist
5
+ do $$ begin
6
+ -- Reassign owned objects to current user before dropping
7
+ -- This handles any objects that might have been created by the role
8
+ begin
9
+ execute format('reassign owned by %I to current_user', {{ROLE_LITERAL}});
10
+ exception when undefined_object then
11
+ null; -- Role doesn't exist, nothing to reassign
12
+ end;
13
+
14
+ -- Drop owned objects (in case reassign didn't work for some objects)
15
+ begin
16
+ execute format('drop owned by %I', {{ROLE_LITERAL}});
17
+ exception when undefined_object then
18
+ null; -- Role doesn't exist
19
+ end;
20
+
21
+ -- Drop the role
22
+ begin
23
+ execute format('drop role %I', {{ROLE_LITERAL}});
24
+ exception when undefined_object then
25
+ null; -- Role doesn't exist, that's fine
26
+ end;
27
+ end $$;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Checkup Dictionary Module
3
+ * =========================
4
+ * Provides access to the checkup report dictionary data embedded at build time.
5
+ *
6
+ * The dictionary is fetched from https://postgres.ai/api/general/checkup_dictionary
7
+ * during the build process and embedded into checkup-dictionary-embedded.ts.
8
+ *
9
+ * This ensures no API calls are made at runtime while keeping the data up-to-date.
10
+ */
11
+
12
+ import { CHECKUP_DICTIONARY_DATA } from "./checkup-dictionary-embedded";
13
+
14
+ /**
15
+ * A checkup dictionary entry describing a single check type.
16
+ */
17
+ export interface CheckupDictionaryEntry {
18
+ /** Unique check code (e.g., "A001", "H002") */
19
+ code: string;
20
+ /** Human-readable title for the check */
21
+ title: string;
22
+ /** Brief description of what the check covers */
23
+ description: string;
24
+ /** Category grouping (e.g., "system", "indexes", "vacuum") */
25
+ category: string;
26
+ /** Optional sort order within category */
27
+ sort_order: number | null;
28
+ /** Whether this is a system-level report */
29
+ is_system_report: boolean;
30
+ }
31
+
32
+ /**
33
+ * Module-level cache for O(1) lookups by code.
34
+ * Initialized at module load time from embedded data.
35
+ * Keys are normalized to uppercase for case-insensitive lookups.
36
+ */
37
+ const dictionaryByCode: Map<string, CheckupDictionaryEntry> = new Map(
38
+ CHECKUP_DICTIONARY_DATA.map((entry) => [entry.code.toUpperCase(), entry])
39
+ );
40
+
41
+ /**
42
+ * Get all checkup dictionary entries.
43
+ *
44
+ * @returns Array of all checkup dictionary entries
45
+ */
46
+ export function getAllCheckupEntries(): CheckupDictionaryEntry[] {
47
+ return CHECKUP_DICTIONARY_DATA;
48
+ }
49
+
50
+ /**
51
+ * Get a checkup dictionary entry by its code.
52
+ *
53
+ * @param code - The check code (e.g., "A001", "H002"). Lookup is case-insensitive.
54
+ * @returns The dictionary entry or null if not found
55
+ */
56
+ export function getCheckupEntry(code: string): CheckupDictionaryEntry | null {
57
+ return dictionaryByCode.get(code.toUpperCase()) ?? null;
58
+ }
59
+
60
+ /**
61
+ * Get the title for a checkup code.
62
+ *
63
+ * @param code - The check code (e.g., "A001", "H002")
64
+ * @returns The title or the code itself if not found
65
+ */
66
+ export function getCheckupTitle(code: string): string {
67
+ const entry = getCheckupEntry(code);
68
+ return entry?.title ?? code;
69
+ }
70
+
71
+ /**
72
+ * Check if a code exists in the dictionary.
73
+ *
74
+ * @param code - The check code to validate
75
+ * @returns True if the code exists in the dictionary
76
+ */
77
+ export function isValidCheckupCode(code: string): boolean {
78
+ return dictionaryByCode.has(code.toUpperCase());
79
+ }
80
+
81
+ /**
82
+ * Get all check codes as an array.
83
+ *
84
+ * @returns Array of all check codes (e.g., ["A001", "A002", ...])
85
+ */
86
+ export function getAllCheckupCodes(): string[] {
87
+ return CHECKUP_DICTIONARY_DATA.map((entry) => entry.code);
88
+ }
89
+
90
+ /**
91
+ * Get checkup entries filtered by category.
92
+ *
93
+ * @param category - The category to filter by (e.g., "indexes", "vacuum")
94
+ * @returns Array of entries in the specified category
95
+ */
96
+ export function getCheckupEntriesByCategory(category: string): CheckupDictionaryEntry[] {
97
+ return CHECKUP_DICTIONARY_DATA.filter(
98
+ (entry) => entry.category.toLowerCase() === category.toLowerCase()
99
+ );
100
+ }
101
+
102
+ /**
103
+ * Build a code-to-title mapping object.
104
+ * Useful for backwards compatibility with CHECK_INFO style usage.
105
+ *
106
+ * @returns Object mapping check codes to titles (e.g., { "A001": "System information", ... })
107
+ */
108
+ export function buildCheckInfoMap(): Record<string, string> {
109
+ const result: Record<string, string> = {};
110
+ for (const entry of CHECKUP_DICTIONARY_DATA) {
111
+ result[entry.code] = entry.title;
112
+ }
113
+ return result;
114
+ }
package/lib/checkup.ts CHANGED
@@ -51,6 +51,7 @@ import * as fs from "fs";
51
51
  import * as path from "path";
52
52
  import * as pkg from "../package.json";
53
53
  import { getMetricSql, transformMetricRow, METRIC_NAMES } from "./metrics-loader";
54
+ import { getCheckupTitle, buildCheckInfoMap } from "./checkup-dictionary";
54
55
 
55
56
  // Time constants
56
57
  const SECONDS_PER_DAY = 86400;
@@ -1185,6 +1186,37 @@ async function generateD004(client: Client, nodeName: string): Promise<Report> {
1185
1186
  return report;
1186
1187
  }
1187
1188
 
1189
+ /**
1190
+ * Generate D001 report - Logging settings
1191
+ *
1192
+ * Collects all PostgreSQL logging-related settings including:
1193
+ * - Log destination and collector settings
1194
+ * - Log file rotation and naming
1195
+ * - Log verbosity and filtering
1196
+ * - Statement and duration logging
1197
+ */
1198
+ async function generateD001(client: Client, nodeName: string): Promise<Report> {
1199
+ const report = createBaseReport("D001", "Logging settings", nodeName);
1200
+ const postgresVersion = await getPostgresVersion(client);
1201
+ const pgMajorVersion = parseInt(postgresVersion.server_major_ver, 10) || 16;
1202
+ const allSettings = await getSettings(client, pgMajorVersion);
1203
+
1204
+ // Filter logging-related settings (log_* and logging_*)
1205
+ const loggingSettings: Record<string, SettingInfo> = {};
1206
+ for (const [name, setting] of Object.entries(allSettings)) {
1207
+ if (name.startsWith("log_") || name.startsWith("logging_")) {
1208
+ loggingSettings[name] = setting;
1209
+ }
1210
+ }
1211
+
1212
+ report.results[nodeName] = {
1213
+ data: loggingSettings,
1214
+ postgres_version: postgresVersion,
1215
+ };
1216
+
1217
+ return report;
1218
+ }
1219
+
1188
1220
  /**
1189
1221
  * Generate F001 report - Autovacuum: current settings
1190
1222
  */
@@ -1326,6 +1358,82 @@ async function generateG001(client: Client, nodeName: string): Promise<Report> {
1326
1358
  return report;
1327
1359
  }
1328
1360
 
1361
+ /**
1362
+ * Generate G003 report - Timeouts, locks, deadlocks
1363
+ *
1364
+ * Collects timeout and lock-related settings, plus deadlock statistics.
1365
+ */
1366
+ async function generateG003(client: Client, nodeName: string): Promise<Report> {
1367
+ const report = createBaseReport("G003", "Timeouts, locks, deadlocks", nodeName);
1368
+ const postgresVersion = await getPostgresVersion(client);
1369
+ const pgMajorVersion = parseInt(postgresVersion.server_major_ver, 10) || 16;
1370
+ const allSettings = await getSettings(client, pgMajorVersion);
1371
+
1372
+ // Timeout and lock-related setting names
1373
+ const lockTimeoutSettingNames = [
1374
+ "lock_timeout",
1375
+ "statement_timeout",
1376
+ "idle_in_transaction_session_timeout",
1377
+ "idle_session_timeout",
1378
+ "deadlock_timeout",
1379
+ "max_locks_per_transaction",
1380
+ "max_pred_locks_per_transaction",
1381
+ "max_pred_locks_per_relation",
1382
+ "max_pred_locks_per_page",
1383
+ "log_lock_waits",
1384
+ "transaction_timeout",
1385
+ ];
1386
+
1387
+ const lockSettings: Record<string, SettingInfo> = {};
1388
+ for (const name of lockTimeoutSettingNames) {
1389
+ if (allSettings[name]) {
1390
+ lockSettings[name] = allSettings[name];
1391
+ }
1392
+ }
1393
+
1394
+ // Get deadlock statistics from pg_stat_database
1395
+ let deadlockStats: {
1396
+ deadlocks: number;
1397
+ conflicts: number;
1398
+ stats_reset: string | null;
1399
+ } | null = null;
1400
+ let deadlockError: string | null = null;
1401
+
1402
+ try {
1403
+ const statsResult = await client.query(`
1404
+ select
1405
+ coalesce(sum(deadlocks), 0)::bigint as deadlocks,
1406
+ coalesce(sum(conflicts), 0)::bigint as conflicts,
1407
+ min(stats_reset)::text as stats_reset
1408
+ from pg_stat_database
1409
+ where datname = current_database()
1410
+ `);
1411
+ if (statsResult.rows.length > 0) {
1412
+ const row = statsResult.rows[0];
1413
+ deadlockStats = {
1414
+ deadlocks: parseInt(row.deadlocks, 10),
1415
+ conflicts: parseInt(row.conflicts, 10),
1416
+ stats_reset: row.stats_reset || null,
1417
+ };
1418
+ }
1419
+ } catch (err) {
1420
+ const errorMsg = err instanceof Error ? err.message : String(err);
1421
+ console.log(`[G003] Error querying deadlock stats: ${errorMsg}`);
1422
+ deadlockError = errorMsg;
1423
+ }
1424
+
1425
+ report.results[nodeName] = {
1426
+ data: {
1427
+ settings: lockSettings,
1428
+ deadlock_stats: deadlockStats,
1429
+ ...(deadlockError && { deadlock_stats_error: deadlockError }),
1430
+ },
1431
+ postgres_version: postgresVersion,
1432
+ };
1433
+
1434
+ return report;
1435
+ }
1436
+
1329
1437
  /**
1330
1438
  * Available report generators
1331
1439
  */
@@ -1335,30 +1443,38 @@ export const REPORT_GENERATORS: Record<string, (client: Client, nodeName: string
1335
1443
  A004: generateA004,
1336
1444
  A007: generateA007,
1337
1445
  A013: generateA013,
1446
+ D001: generateD001,
1338
1447
  D004: generateD004,
1339
1448
  F001: generateF001,
1340
1449
  G001: generateG001,
1450
+ G003: generateG003,
1341
1451
  H001: generateH001,
1342
1452
  H002: generateH002,
1343
1453
  H004: generateH004,
1344
1454
  };
1345
1455
 
1346
1456
  /**
1347
- * Check IDs and titles
1457
+ * Check IDs and titles.
1458
+ *
1459
+ * This mapping is built from the embedded checkup dictionary, which is
1460
+ * fetched from https://postgres.ai/api/general/checkup_dictionary at build time.
1461
+ *
1462
+ * For the full dictionary (all available checks), use the checkup-dictionary module.
1463
+ * CHECK_INFO is filtered to only include checks that have express-mode generators.
1348
1464
  */
1349
- export const CHECK_INFO: Record<string, string> = {
1350
- A002: "Postgres major version",
1351
- A003: "Postgres settings",
1352
- A004: "Cluster information",
1353
- A007: "Altered settings",
1354
- A013: "Postgres minor version",
1355
- D004: "pg_stat_statements and pg_stat_kcache settings",
1356
- F001: "Autovacuum: current settings",
1357
- G001: "Memory-related settings",
1358
- H001: "Invalid indexes",
1359
- H002: "Unused indexes",
1360
- H004: "Redundant indexes",
1361
- };
1465
+ export const CHECK_INFO: Record<string, string> = (() => {
1466
+ // Build the full dictionary map
1467
+ const fullMap = buildCheckInfoMap();
1468
+
1469
+ // Filter to only include checks that have express-mode generators
1470
+ const expressCheckIds = Object.keys(REPORT_GENERATORS);
1471
+ const filtered: Record<string, string> = {};
1472
+ for (const checkId of expressCheckIds) {
1473
+ // Use dictionary title if available, otherwise use a fallback
1474
+ filtered[checkId] = fullMap[checkId] || checkId;
1475
+ }
1476
+ return filtered;
1477
+ })();
1362
1478
 
1363
1479
  /**
1364
1480
  * Generate all available health check reports.
package/lib/init.ts CHANGED
@@ -527,7 +527,13 @@ end $$;`;
527
527
  steps.push({ name: "01.role", sql: roleSql });
528
528
  }
529
529
 
530
- let permissionsSql = applyTemplate(loadSqlTemplate("02.permissions.sql"), vars);
530
+ // Extensions should be created before permissions (so we can grant permissions on them)
531
+ steps.push({
532
+ name: "02.extensions",
533
+ sql: loadSqlTemplate("02.extensions.sql"),
534
+ });
535
+
536
+ let permissionsSql = applyTemplate(loadSqlTemplate("03.permissions.sql"), vars);
531
537
 
532
538
  // Some providers restrict ALTER USER - remove those statements.
533
539
  // TODO: Make this more flexible by allowing users to specify which statements to skip via config.
@@ -545,26 +551,26 @@ end $$;`;
545
551
  }
546
552
 
547
553
  steps.push({
548
- name: "02.permissions",
554
+ name: "03.permissions",
549
555
  sql: permissionsSql,
550
556
  });
551
557
 
552
558
  // Helper functions (SECURITY DEFINER) for plan analysis and table info
553
559
  steps.push({
554
- name: "05.helpers",
555
- sql: applyTemplate(loadSqlTemplate("05.helpers.sql"), vars),
560
+ name: "06.helpers",
561
+ sql: applyTemplate(loadSqlTemplate("06.helpers.sql"), vars),
556
562
  });
557
563
 
558
564
  if (params.includeOptionalPermissions) {
559
565
  steps.push(
560
566
  {
561
- name: "03.optional_rds",
562
- sql: applyTemplate(loadSqlTemplate("03.optional_rds.sql"), vars),
567
+ name: "04.optional_rds",
568
+ sql: applyTemplate(loadSqlTemplate("04.optional_rds.sql"), vars),
563
569
  optional: true,
564
570
  },
565
571
  {
566
- name: "04.optional_self_managed",
567
- sql: applyTemplate(loadSqlTemplate("04.optional_self_managed.sql"), vars),
572
+ name: "05.optional_self_managed",
573
+ sql: applyTemplate(loadSqlTemplate("05.optional_self_managed.sql"), vars),
568
574
  optional: true,
569
575
  }
570
576
  );
@@ -657,6 +663,101 @@ export type VerifyInitResult = {
657
663
  missingOptional: string[];
658
664
  };
659
665
 
666
+ export type UninitPlan = {
667
+ monitoringUser: string;
668
+ database: string;
669
+ steps: InitStep[];
670
+ /** If true, also drop the monitoring role. If false, only revoke permissions. */
671
+ dropRole: boolean;
672
+ };
673
+
674
+ export async function buildUninitPlan(params: {
675
+ database: string;
676
+ monitoringUser?: string;
677
+ /** If true, drop the role entirely. If false, only revoke permissions/drop objects. */
678
+ dropRole?: boolean;
679
+ /** Provider type. Affects which steps are included. Defaults to "self-managed". */
680
+ provider?: DbProvider;
681
+ }): Promise<UninitPlan> {
682
+ const monitoringUser = params.monitoringUser || DEFAULT_MONITORING_USER;
683
+ const database = params.database;
684
+ const provider = params.provider ?? "self-managed";
685
+ const dropRole = params.dropRole ?? true;
686
+
687
+ const qRole = quoteIdent(monitoringUser);
688
+ const qDb = quoteIdent(database);
689
+ const qRoleLiteral = quoteLiteral(monitoringUser);
690
+
691
+ const steps: InitStep[] = [];
692
+
693
+ const vars: Record<string, string> = {
694
+ ROLE_IDENT: qRole,
695
+ DB_IDENT: qDb,
696
+ ROLE_LITERAL: qRoleLiteral,
697
+ };
698
+
699
+ // Step 1: Drop helper functions
700
+ steps.push({
701
+ name: "01.drop_helpers",
702
+ sql: applyTemplate(loadSqlTemplate("uninit/01.helpers.sql"), vars),
703
+ });
704
+
705
+ // Step 2: Drop view, revoke permissions, drop schema
706
+ steps.push({
707
+ name: "02.revoke_permissions",
708
+ sql: applyTemplate(loadSqlTemplate("uninit/02.permissions.sql"), vars),
709
+ });
710
+
711
+ // Step 3: Drop the role (only if requested and provider allows it)
712
+ if (dropRole && !SKIP_ROLE_CREATION_PROVIDERS.includes(provider)) {
713
+ steps.push({
714
+ name: "03.drop_role",
715
+ sql: applyTemplate(loadSqlTemplate("uninit/03.role.sql"), vars),
716
+ });
717
+ }
718
+
719
+ return { monitoringUser, database, steps, dropRole };
720
+ }
721
+
722
+ export async function applyUninitPlan(params: {
723
+ client: PgClient;
724
+ plan: UninitPlan;
725
+ }): Promise<{ applied: string[]; errors: string[] }> {
726
+ const applied: string[] = [];
727
+ const errors: string[] = [];
728
+
729
+ // Helper to wrap a step execution in begin/commit
730
+ const executeStep = async (step: InitStep): Promise<void> => {
731
+ await params.client.query("begin;");
732
+ try {
733
+ await params.client.query(step.sql, step.params as any);
734
+ await params.client.query("commit;");
735
+ } catch (e) {
736
+ try {
737
+ await params.client.query("rollback;");
738
+ } catch {
739
+ // ignore
740
+ }
741
+ throw e;
742
+ }
743
+ };
744
+
745
+ // Apply steps in order - unlike init, uninit steps are not optional
746
+ // but we continue on errors to clean up as much as possible
747
+ for (const step of params.plan.steps) {
748
+ try {
749
+ await executeStep(step);
750
+ applied.push(step.name);
751
+ } catch (e) {
752
+ const msg = e instanceof Error ? e.message : String(e);
753
+ errors.push(`${step.name}: ${msg}`);
754
+ // Continue to try other steps
755
+ }
756
+ }
757
+
758
+ return { applied, errors };
759
+ }
760
+
660
761
  export async function verifyInitSetup(params: {
661
762
  client: PgClient;
662
763
  database: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresai",
3
- "version": "0.14.0-beta.13",
3
+ "version": "0.14.0-beta.15",
4
4
  "description": "postgres_ai CLI",
5
5
  "license": "Apache-2.0",
6
6
  "private": false,
@@ -26,15 +26,17 @@
26
26
  },
27
27
  "scripts": {
28
28
  "embed-metrics": "bun run scripts/embed-metrics.ts",
29
- "build": "bun run embed-metrics && bun build ./bin/postgres-ai.ts --outdir ./dist/bin --target node && node -e \"const fs=require('fs');const f='./dist/bin/postgres-ai.js';fs.writeFileSync(f,fs.readFileSync(f,'utf8').replace('#!/usr/bin/env bun','#!/usr/bin/env node'))\" && cp -r ./sql ./dist/sql",
29
+ "embed-checkup-dictionary": "bun run scripts/embed-checkup-dictionary.ts",
30
+ "embed-all": "bun run embed-metrics && bun run embed-checkup-dictionary",
31
+ "build": "bun run embed-all && bun build ./bin/postgres-ai.ts --outdir ./dist/bin --target node && node -e \"const fs=require('fs');const f='./dist/bin/postgres-ai.js';fs.writeFileSync(f,fs.readFileSync(f,'utf8').replace('#!/usr/bin/env bun','#!/usr/bin/env node'))\" && cp -r ./sql ./dist/sql",
30
32
  "prepublishOnly": "npm run build",
31
33
  "start": "bun ./bin/postgres-ai.ts --help",
32
34
  "start:node": "node ./dist/bin/postgres-ai.js --help",
33
- "dev": "bun run embed-metrics && bun --watch ./bin/postgres-ai.ts",
34
- "test": "bun run embed-metrics && bun test",
35
- "test:fast": "bun run embed-metrics && bun test --coverage=false",
36
- "test:coverage": "bun run embed-metrics && bun test --coverage && echo 'Coverage report: cli/coverage/lcov-report/index.html'",
37
- "typecheck": "bun run embed-metrics && bunx tsc --noEmit"
35
+ "dev": "bun run embed-all && bun --watch ./bin/postgres-ai.ts",
36
+ "test": "bun run embed-all && bun test",
37
+ "test:fast": "bun run embed-all && bun test --coverage=false",
38
+ "test:coverage": "bun run embed-all && bun test --coverage && echo 'Coverage report: cli/coverage/lcov-report/index.html'",
39
+ "typecheck": "bun run embed-all && bunx tsc --noEmit"
38
40
  },
39
41
  "dependencies": {
40
42
  "@modelcontextprotocol/sdk": "^1.20.2",