postgresai 0.16.0-dev.10 → 0.16.0-dev.11

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.
@@ -41,6 +41,8 @@ export function generateCheckSummary(checkId: string, report: any): CheckSummary
41
41
  case 'D004': return summarizeD004(nodeData);
42
42
  case 'F001': return summarizeF001(nodeData);
43
43
  case 'F003': return summarizeF003(nodeData);
44
+ case 'F004': return summarizeBloat(nodeData, 'table');
45
+ case 'F005': return summarizeBloat(nodeData, 'index');
44
46
  case 'G001': return summarizeG001(nodeData);
45
47
  case 'G003': return summarizeG003(nodeData);
46
48
  default:
@@ -273,6 +275,29 @@ function summarizeF003(nodeData: any): CheckSummary {
273
275
  return { status: 'warning', message: parts.join(', ') };
274
276
  }
275
277
 
278
+ function summarizeBloat(nodeData: any, kind: 'table' | 'index'): CheckSummary {
279
+ const data = nodeData?.data || {};
280
+ let totalCount = 0;
281
+
282
+ for (const dbData of Object.values(data)) {
283
+ const dbEntry = dbData as any;
284
+ if (dbEntry?.status?.ok === false) {
285
+ const reason = String(dbEntry.status.reason || 'query_error').replaceAll('_', ' ');
286
+ return { status: 'warning', message: `Bloat estimate degraded: ${reason}` };
287
+ }
288
+ totalCount += dbEntry?.total_count || 0;
289
+ }
290
+
291
+ if (totalCount === 0) {
292
+ return { status: 'ok', message: `No bloated ${kind}${kind === 'index' ? 'es' : 's'} found` };
293
+ }
294
+
295
+ return {
296
+ status: 'warning',
297
+ message: `Found ${totalCount} bloated ${kind}${totalCount === 1 ? '' : kind === 'index' ? 'es' : 's'}`,
298
+ };
299
+ }
300
+
276
301
  function summarizeG001(nodeData: any): CheckSummary {
277
302
  const data = nodeData?.data || {};
278
303
  const settingsCount = Object.keys(data).length;
package/lib/checkup.ts CHANGED
@@ -1498,6 +1498,74 @@ async function generateF003(client: Client, nodeName: string): Promise<Report> {
1498
1498
  * Uses pg_stats for column statistics to estimate row sizes.
1499
1499
  * SQL loaded from config/pgwatch-prometheus/metrics.yml (pg_table_bloat metric).
1500
1500
  */
1501
+ type BloatCheckReason = "missing_schema" | "missing_view" | "missing_grant" | "query_error";
1502
+
1503
+ interface BloatCheckStatus {
1504
+ ok: boolean;
1505
+ reason: BloatCheckReason | null;
1506
+ error: string | null;
1507
+ }
1508
+
1509
+ function bloatErrorStatus(err: unknown): BloatCheckStatus {
1510
+ const error = err instanceof Error ? err.message : String(err);
1511
+ const code = typeof err === "object" && err !== null && "code" in err
1512
+ ? String((err as { code?: unknown }).code || "")
1513
+ : "";
1514
+ const normalized = error.toLowerCase();
1515
+
1516
+ let reason: BloatCheckReason = "query_error";
1517
+ if (code === "3F000" || normalized.includes('schema "postgres_ai" does not exist')) {
1518
+ reason = "missing_schema";
1519
+ } else if (code === "42P01" || normalized.includes('relation "postgres_ai.pg_statistic" does not exist')) {
1520
+ reason = "missing_view";
1521
+ } else if (code === "42501" || normalized.includes("permission denied")) {
1522
+ reason = "missing_grant";
1523
+ }
1524
+
1525
+ return { ok: false, reason, error };
1526
+ }
1527
+
1528
+ async function getBloatCheckStatus(client: Client): Promise<BloatCheckStatus> {
1529
+ try {
1530
+ const result = await client.query(`
1531
+ select
1532
+ to_regnamespace('postgres_ai') is not null as schema_exists,
1533
+ case
1534
+ when to_regnamespace('postgres_ai') is null then false
1535
+ else has_schema_privilege(current_user, 'postgres_ai', 'USAGE')
1536
+ end as schema_usage,
1537
+ case
1538
+ when to_regnamespace('postgres_ai') is null then false
1539
+ when not has_schema_privilege(current_user, 'postgres_ai', 'USAGE') then false
1540
+ else to_regclass('postgres_ai.pg_statistic') is not null
1541
+ end as view_exists,
1542
+ case
1543
+ when to_regnamespace('postgres_ai') is null then false
1544
+ when not has_schema_privilege(current_user, 'postgres_ai', 'USAGE') then false
1545
+ when to_regclass('postgres_ai.pg_statistic') is null then false
1546
+ else has_table_privilege(current_user, 'postgres_ai.pg_statistic', 'SELECT')
1547
+ end as view_select
1548
+ `);
1549
+ const capability = result.rows[0] || {};
1550
+
1551
+ if (!capability.schema_exists) {
1552
+ return { ok: false, reason: "missing_schema", error: 'schema "postgres_ai" does not exist' };
1553
+ }
1554
+ if (!capability.schema_usage) {
1555
+ return { ok: false, reason: "missing_grant", error: "permission denied for schema postgres_ai" };
1556
+ }
1557
+ if (!capability.view_exists) {
1558
+ return { ok: false, reason: "missing_view", error: 'relation "postgres_ai.pg_statistic" does not exist' };
1559
+ }
1560
+ if (!capability.view_select) {
1561
+ return { ok: false, reason: "missing_grant", error: "permission denied for relation postgres_ai.pg_statistic" };
1562
+ }
1563
+ return { ok: true, reason: null, error: null };
1564
+ } catch (err) {
1565
+ return bloatErrorStatus(err);
1566
+ }
1567
+ }
1568
+
1501
1569
  async function generateF004(client: Client, nodeName: string): Promise<Report> {
1502
1570
  const report = createBaseReport("F004", "Autovacuum: heap bloat (estimated)", nodeName);
1503
1571
  const postgresVersion = await getPostgresVersion(client);
@@ -1520,8 +1588,15 @@ async function generateF004(client: Client, nodeName: string): Promise<Report> {
1520
1588
  }
1521
1589
 
1522
1590
  let bloatedTables: BloatedTable[] = [];
1591
+ let status = await getBloatCheckStatus(client);
1523
1592
 
1524
1593
  try {
1594
+ if (!status.ok) throw Object.assign(new Error(status.error || "Bloat prerequisites unavailable"), {
1595
+ code: status.reason === "missing_schema" ? "3F000"
1596
+ : status.reason === "missing_view" ? "42P01"
1597
+ : status.reason === "missing_grant" ? "42501"
1598
+ : undefined,
1599
+ });
1525
1600
  // Get bloat data
1526
1601
  const sql = getMetricSql(METRIC_NAMES.F004, pgMajorVersion);
1527
1602
  const bloatResult = await client.query(sql);
@@ -1572,7 +1647,8 @@ async function generateF004(client: Client, nodeName: string): Promise<Report> {
1572
1647
  };
1573
1648
  });
1574
1649
  } catch (err) {
1575
- const errorMsg = err instanceof Error ? err.message : String(err);
1650
+ status = bloatErrorStatus(err);
1651
+ const errorMsg = status.error || "Unknown error";
1576
1652
  console.error(`[F004] Error estimating table bloat: ${errorMsg}`);
1577
1653
  if (errorMsg.includes("postgres_ai.")) {
1578
1654
  console.error(` Hint: Run "postgresai prepare-db <connection>" to create required objects.`);
@@ -1587,6 +1663,7 @@ async function generateF004(client: Client, nodeName: string): Promise<Report> {
1587
1663
  const totalBloatSizeBytes = bloatedTables.reduce((sum, t) => sum + t.bloat_size, 0);
1588
1664
 
1589
1665
  const dbEntry = {
1666
+ status,
1590
1667
  bloated_tables: bloatedTables,
1591
1668
  total_count: totalCount,
1592
1669
  total_bloat_size_bytes: totalBloatSizeBytes,
@@ -1634,8 +1711,15 @@ async function generateF005(client: Client, nodeName: string): Promise<Report> {
1634
1711
  }
1635
1712
 
1636
1713
  let bloatedIndexes: BloatedIndex[] = [];
1714
+ let status = await getBloatCheckStatus(client);
1637
1715
 
1638
1716
  try {
1717
+ if (!status.ok) throw Object.assign(new Error(status.error || "Bloat prerequisites unavailable"), {
1718
+ code: status.reason === "missing_schema" ? "3F000"
1719
+ : status.reason === "missing_view" ? "42P01"
1720
+ : status.reason === "missing_grant" ? "42501"
1721
+ : undefined,
1722
+ });
1639
1723
  // Get bloat data
1640
1724
  const sql = getMetricSql(METRIC_NAMES.F005, pgMajorVersion);
1641
1725
  const bloatResult = await client.query(sql);
@@ -1690,7 +1774,8 @@ async function generateF005(client: Client, nodeName: string): Promise<Report> {
1690
1774
  };
1691
1775
  });
1692
1776
  } catch (err) {
1693
- const errorMsg = err instanceof Error ? err.message : String(err);
1777
+ status = bloatErrorStatus(err);
1778
+ const errorMsg = status.error || "Unknown error";
1694
1779
  console.error(`[F005] Error estimating index bloat: ${errorMsg}`);
1695
1780
  if (errorMsg.includes("postgres_ai.")) {
1696
1781
  console.error(` Hint: Run "postgresai prepare-db <connection>" to create required objects.`);
@@ -1705,6 +1790,7 @@ async function generateF005(client: Client, nodeName: string): Promise<Report> {
1705
1790
  const totalBloatSizeBytes = bloatedIndexes.reduce((sum, idx) => sum + idx.bloat_size, 0);
1706
1791
 
1707
1792
  const dbEntry = {
1793
+ status,
1708
1794
  bloated_indexes: bloatedIndexes,
1709
1795
  total_count: totalCount,
1710
1796
  total_bloat_size_bytes: totalBloatSizeBytes,
package/lib/init.ts CHANGED
@@ -1019,10 +1019,28 @@ export async function checkCurrentUserPermissions(
1019
1019
 
1020
1020
  union all
1021
1021
 
1022
+ select
1023
+ 'postgres_ai schema exists' as permission_name,
1024
+ 'optional' as status,
1025
+ to_regnamespace('postgres_ai') is not null as granted
1026
+
1027
+ union all
1028
+
1029
+ select
1030
+ 'usage on postgres_ai schema' as permission_name,
1031
+ 'optional' as status,
1032
+ case
1033
+ when to_regnamespace('postgres_ai') is null then null
1034
+ else has_schema_privilege(current_user, 'postgres_ai', 'USAGE')
1035
+ end as granted
1036
+
1037
+ union all
1038
+
1022
1039
  select
1023
1040
  'postgres_ai.pg_statistic view exists' as permission_name,
1024
1041
  'optional' as status,
1025
1042
  case
1043
+ when to_regnamespace('postgres_ai') is null then null
1026
1044
  when not has_schema_privilege(current_user, 'postgres_ai', 'USAGE') then null
1027
1045
  else to_regclass('postgres_ai.pg_statistic') is not null
1028
1046
  end as granted
@@ -1033,6 +1051,7 @@ export async function checkCurrentUserPermissions(
1033
1051
  'select on postgres_ai.pg_statistic' as permission_name,
1034
1052
  'optional' as status,
1035
1053
  case
1054
+ when to_regnamespace('postgres_ai') is null then null
1036
1055
  when not has_schema_privilege(current_user, 'postgres_ai', 'USAGE') then null
1037
1056
  when to_regclass('postgres_ai.pg_statistic') is null then null
1038
1057
  else has_table_privilege(current_user, 'postgres_ai.pg_statistic', 'select')
@@ -1052,6 +1071,10 @@ export async function checkCurrentUserPermissions(
1052
1071
  when permission_name like 'select on pg_catalog.pg_index' then
1053
1072
  format('grant select on pg_catalog.pg_index to %I;', current_user)
1054
1073
  end
1074
+ when permission_name = 'postgres_ai schema exists' and granted = false then
1075
+ '-- run postgresai prepare-db or create the postgres_ai schema and pg_statistic view manually'
1076
+ when permission_name = 'usage on postgres_ai schema' and granted = false then
1077
+ format('grant usage on schema postgres_ai to %I;', current_user)
1055
1078
  when permission_name = 'postgres_ai.pg_statistic view exists' and granted = false then
1056
1079
  '-- create postgres_ai.pg_statistic view (see setup script)'
1057
1080
  when permission_name = 'select on postgres_ai.pg_statistic' and granted = false then
@@ -1097,6 +1120,12 @@ export function formatPermissionCheckMessages(result: PreflightPermissionResult)
1097
1120
  const errors: string[] = [];
1098
1121
 
1099
1122
  for (const row of result.missingOptional) {
1123
+ if (row.permission_name === "postgres_ai schema exists") {
1124
+ warnings.push(
1125
+ "Warning: optional: postgres_ai schema not found — F004/F005 (bloat estimates) will be skipped; run prepare-db or create the view manually to enable them."
1126
+ );
1127
+ continue;
1128
+ }
1100
1129
  const fix = row.fix_command ? ` Fix: ${row.fix_command}` : "";
1101
1130
  warnings.push(`Warning: optional permission missing — ${row.permission_name}.${fix}`);
1102
1131
  }