postgresai 0.16.0-dev.1 → 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.
- package/CHANGELOG.md +11 -0
- package/bin/postgres-ai.ts +408 -25
- package/dist/bin/postgres-ai.js +940 -90
- package/dist/sql/06.helpers.sql +0 -122
- package/dist/sql/sql/06.helpers.sql +0 -122
- package/lib/checkup-summary.ts +25 -0
- package/lib/checkup.ts +88 -2
- package/lib/init.ts +29 -8
- package/lib/joe.ts +703 -0
- package/lib/supabase.ts +0 -18
- package/lib/util.ts +241 -18
- package/package.json +1 -1
- package/sql/06.helpers.sql +0 -122
- package/test/auth.test.ts +30 -1
- package/test/checkup.integration.test.ts +31 -21
- package/test/checkup.test.ts +48 -3
- package/test/init.integration.test.ts +9 -79
- package/test/init.test.ts +35 -0
- package/test/joe.cli.test.ts +628 -0
- package/test/joe.test.ts +855 -0
- package/test/monitoring.test.ts +54 -3
- package/test/schema-validation.test.ts +40 -0
- package/test/test-utils.ts +5 -0
- package/test/util.test.ts +227 -1
package/dist/sql/06.helpers.sql
CHANGED
|
@@ -2,126 +2,6 @@
|
|
|
2
2
|
-- These functions use SECURITY DEFINER to allow the monitoring user to perform
|
|
3
3
|
-- operations they don't have direct permissions for.
|
|
4
4
|
|
|
5
|
-
/*
|
|
6
|
-
* explain_generic
|
|
7
|
-
*
|
|
8
|
-
* Function to get generic explain plans with optional HypoPG index testing.
|
|
9
|
-
* Requires: PostgreSQL 16+ (for generic_plan option), HypoPG extension (optional).
|
|
10
|
-
*
|
|
11
|
-
* Security notes:
|
|
12
|
-
* - EXPLAIN without ANALYZE is read-only (plans but doesn't execute the query)
|
|
13
|
-
* - PostgreSQL's EXPLAIN only accepts a single statement (primary protection)
|
|
14
|
-
* - Input validation uses a simple heuristic to detect multiple statements
|
|
15
|
-
* (Note: may reject valid queries containing semicolons in string literals)
|
|
16
|
-
*
|
|
17
|
-
* Usage examples:
|
|
18
|
-
* -- Basic generic plan
|
|
19
|
-
* select postgres_ai.explain_generic('select * from users where id = $1');
|
|
20
|
-
*
|
|
21
|
-
* -- JSON format
|
|
22
|
-
* select postgres_ai.explain_generic('select * from users where id = $1', 'json');
|
|
23
|
-
*
|
|
24
|
-
* -- Test a hypothetical index
|
|
25
|
-
* select postgres_ai.explain_generic(
|
|
26
|
-
* 'select * from users where email = $1',
|
|
27
|
-
* 'text',
|
|
28
|
-
* 'create index on users (email)'
|
|
29
|
-
* );
|
|
30
|
-
*/
|
|
31
|
-
create or replace function postgres_ai.explain_generic(
|
|
32
|
-
in query text,
|
|
33
|
-
in format text default 'text',
|
|
34
|
-
in hypopg_index text default null,
|
|
35
|
-
out result text
|
|
36
|
-
)
|
|
37
|
-
language plpgsql
|
|
38
|
-
security definer
|
|
39
|
-
set search_path = pg_catalog, public
|
|
40
|
-
as $$
|
|
41
|
-
declare
|
|
42
|
-
v_line record;
|
|
43
|
-
v_lines text[] := '{}';
|
|
44
|
-
v_explain_query text;
|
|
45
|
-
v_hypo_result record;
|
|
46
|
-
v_version int;
|
|
47
|
-
v_hypopg_available boolean;
|
|
48
|
-
v_clean_query text;
|
|
49
|
-
begin
|
|
50
|
-
-- Check PostgreSQL version (generic_plan requires 16+)
|
|
51
|
-
select current_setting('server_version_num')::int into v_version;
|
|
52
|
-
|
|
53
|
-
if v_version < 160000 then
|
|
54
|
-
raise exception 'generic_plan requires PostgreSQL 16+, current version: %',
|
|
55
|
-
current_setting('server_version');
|
|
56
|
-
end if;
|
|
57
|
-
|
|
58
|
-
-- Input validation: reject empty queries
|
|
59
|
-
if query is null or trim(query) = '' then
|
|
60
|
-
raise exception 'query cannot be empty';
|
|
61
|
-
end if;
|
|
62
|
-
|
|
63
|
-
-- Input validation: detect multiple statements (defense-in-depth)
|
|
64
|
-
-- Note: This is a simple heuristic - EXPLAIN itself only accepts single statements
|
|
65
|
-
-- Limitation: Queries with semicolons inside string literals will be rejected
|
|
66
|
-
v_clean_query := trim(query);
|
|
67
|
-
if v_clean_query like '%;%' then
|
|
68
|
-
-- Strip trailing semicolon if present (common user convenience)
|
|
69
|
-
v_clean_query := regexp_replace(v_clean_query, ';\s*$', '');
|
|
70
|
-
-- If there's still a semicolon, reject (likely multiple statements or semicolon in string)
|
|
71
|
-
if v_clean_query like '%;%' then
|
|
72
|
-
raise exception 'query contains semicolon (multiple statements not allowed; note: semicolons in string literals are also not supported)';
|
|
73
|
-
end if;
|
|
74
|
-
end if;
|
|
75
|
-
|
|
76
|
-
-- Check if HypoPG extension is available
|
|
77
|
-
if hypopg_index is not null then
|
|
78
|
-
select exists(
|
|
79
|
-
select 1 from pg_extension where extname = 'hypopg'
|
|
80
|
-
) into v_hypopg_available;
|
|
81
|
-
|
|
82
|
-
if not v_hypopg_available then
|
|
83
|
-
raise exception 'HypoPG extension is required for hypothetical index testing but is not installed';
|
|
84
|
-
end if;
|
|
85
|
-
|
|
86
|
-
-- Create hypothetical index
|
|
87
|
-
select * into v_hypo_result from hypopg_create_index(hypopg_index);
|
|
88
|
-
raise notice 'Created hypothetical index: % (oid: %)',
|
|
89
|
-
v_hypo_result.indexname, v_hypo_result.indexrelid;
|
|
90
|
-
end if;
|
|
91
|
-
|
|
92
|
-
-- Build and execute EXPLAIN query
|
|
93
|
-
-- Note: EXPLAIN is read-only (plans but doesn't execute), making this safe
|
|
94
|
-
begin
|
|
95
|
-
if lower(format) = 'json' then
|
|
96
|
-
execute 'explain (verbose, settings, generic_plan, format json) ' || v_clean_query
|
|
97
|
-
into result;
|
|
98
|
-
else
|
|
99
|
-
for v_line in execute 'explain (verbose, settings, generic_plan) ' || v_clean_query loop
|
|
100
|
-
v_lines := array_append(v_lines, v_line."QUERY PLAN");
|
|
101
|
-
end loop;
|
|
102
|
-
result := array_to_string(v_lines, e'\n');
|
|
103
|
-
end if;
|
|
104
|
-
exception when others then
|
|
105
|
-
-- Clean up hypothetical index before re-raising
|
|
106
|
-
if hypopg_index is not null then
|
|
107
|
-
perform hypopg_reset();
|
|
108
|
-
end if;
|
|
109
|
-
raise;
|
|
110
|
-
end;
|
|
111
|
-
|
|
112
|
-
-- Clean up hypothetical index
|
|
113
|
-
if hypopg_index is not null then
|
|
114
|
-
perform hypopg_reset();
|
|
115
|
-
end if;
|
|
116
|
-
end;
|
|
117
|
-
$$;
|
|
118
|
-
|
|
119
|
-
comment on function postgres_ai.explain_generic(text, text, text) is
|
|
120
|
-
'Returns generic EXPLAIN plan with optional HypoPG index testing (requires PG16+)';
|
|
121
|
-
|
|
122
|
-
-- Grant execute to the monitoring user
|
|
123
|
-
grant execute on function postgres_ai.explain_generic(text, text, text) to {{ROLE_IDENT}};
|
|
124
|
-
|
|
125
5
|
/*
|
|
126
6
|
* table_describe
|
|
127
7
|
*
|
|
@@ -435,5 +315,3 @@ comment on function postgres_ai.table_describe(text) is
|
|
|
435
315
|
'Returns comprehensive table information in compact text format for LLM analysis';
|
|
436
316
|
|
|
437
317
|
grant execute on function postgres_ai.table_describe(text) to {{ROLE_IDENT}};
|
|
438
|
-
|
|
439
|
-
|
|
@@ -2,126 +2,6 @@
|
|
|
2
2
|
-- These functions use SECURITY DEFINER to allow the monitoring user to perform
|
|
3
3
|
-- operations they don't have direct permissions for.
|
|
4
4
|
|
|
5
|
-
/*
|
|
6
|
-
* explain_generic
|
|
7
|
-
*
|
|
8
|
-
* Function to get generic explain plans with optional HypoPG index testing.
|
|
9
|
-
* Requires: PostgreSQL 16+ (for generic_plan option), HypoPG extension (optional).
|
|
10
|
-
*
|
|
11
|
-
* Security notes:
|
|
12
|
-
* - EXPLAIN without ANALYZE is read-only (plans but doesn't execute the query)
|
|
13
|
-
* - PostgreSQL's EXPLAIN only accepts a single statement (primary protection)
|
|
14
|
-
* - Input validation uses a simple heuristic to detect multiple statements
|
|
15
|
-
* (Note: may reject valid queries containing semicolons in string literals)
|
|
16
|
-
*
|
|
17
|
-
* Usage examples:
|
|
18
|
-
* -- Basic generic plan
|
|
19
|
-
* select postgres_ai.explain_generic('select * from users where id = $1');
|
|
20
|
-
*
|
|
21
|
-
* -- JSON format
|
|
22
|
-
* select postgres_ai.explain_generic('select * from users where id = $1', 'json');
|
|
23
|
-
*
|
|
24
|
-
* -- Test a hypothetical index
|
|
25
|
-
* select postgres_ai.explain_generic(
|
|
26
|
-
* 'select * from users where email = $1',
|
|
27
|
-
* 'text',
|
|
28
|
-
* 'create index on users (email)'
|
|
29
|
-
* );
|
|
30
|
-
*/
|
|
31
|
-
create or replace function postgres_ai.explain_generic(
|
|
32
|
-
in query text,
|
|
33
|
-
in format text default 'text',
|
|
34
|
-
in hypopg_index text default null,
|
|
35
|
-
out result text
|
|
36
|
-
)
|
|
37
|
-
language plpgsql
|
|
38
|
-
security definer
|
|
39
|
-
set search_path = pg_catalog, public
|
|
40
|
-
as $$
|
|
41
|
-
declare
|
|
42
|
-
v_line record;
|
|
43
|
-
v_lines text[] := '{}';
|
|
44
|
-
v_explain_query text;
|
|
45
|
-
v_hypo_result record;
|
|
46
|
-
v_version int;
|
|
47
|
-
v_hypopg_available boolean;
|
|
48
|
-
v_clean_query text;
|
|
49
|
-
begin
|
|
50
|
-
-- Check PostgreSQL version (generic_plan requires 16+)
|
|
51
|
-
select current_setting('server_version_num')::int into v_version;
|
|
52
|
-
|
|
53
|
-
if v_version < 160000 then
|
|
54
|
-
raise exception 'generic_plan requires PostgreSQL 16+, current version: %',
|
|
55
|
-
current_setting('server_version');
|
|
56
|
-
end if;
|
|
57
|
-
|
|
58
|
-
-- Input validation: reject empty queries
|
|
59
|
-
if query is null or trim(query) = '' then
|
|
60
|
-
raise exception 'query cannot be empty';
|
|
61
|
-
end if;
|
|
62
|
-
|
|
63
|
-
-- Input validation: detect multiple statements (defense-in-depth)
|
|
64
|
-
-- Note: This is a simple heuristic - EXPLAIN itself only accepts single statements
|
|
65
|
-
-- Limitation: Queries with semicolons inside string literals will be rejected
|
|
66
|
-
v_clean_query := trim(query);
|
|
67
|
-
if v_clean_query like '%;%' then
|
|
68
|
-
-- Strip trailing semicolon if present (common user convenience)
|
|
69
|
-
v_clean_query := regexp_replace(v_clean_query, ';\s*$', '');
|
|
70
|
-
-- If there's still a semicolon, reject (likely multiple statements or semicolon in string)
|
|
71
|
-
if v_clean_query like '%;%' then
|
|
72
|
-
raise exception 'query contains semicolon (multiple statements not allowed; note: semicolons in string literals are also not supported)';
|
|
73
|
-
end if;
|
|
74
|
-
end if;
|
|
75
|
-
|
|
76
|
-
-- Check if HypoPG extension is available
|
|
77
|
-
if hypopg_index is not null then
|
|
78
|
-
select exists(
|
|
79
|
-
select 1 from pg_extension where extname = 'hypopg'
|
|
80
|
-
) into v_hypopg_available;
|
|
81
|
-
|
|
82
|
-
if not v_hypopg_available then
|
|
83
|
-
raise exception 'HypoPG extension is required for hypothetical index testing but is not installed';
|
|
84
|
-
end if;
|
|
85
|
-
|
|
86
|
-
-- Create hypothetical index
|
|
87
|
-
select * into v_hypo_result from hypopg_create_index(hypopg_index);
|
|
88
|
-
raise notice 'Created hypothetical index: % (oid: %)',
|
|
89
|
-
v_hypo_result.indexname, v_hypo_result.indexrelid;
|
|
90
|
-
end if;
|
|
91
|
-
|
|
92
|
-
-- Build and execute EXPLAIN query
|
|
93
|
-
-- Note: EXPLAIN is read-only (plans but doesn't execute), making this safe
|
|
94
|
-
begin
|
|
95
|
-
if lower(format) = 'json' then
|
|
96
|
-
execute 'explain (verbose, settings, generic_plan, format json) ' || v_clean_query
|
|
97
|
-
into result;
|
|
98
|
-
else
|
|
99
|
-
for v_line in execute 'explain (verbose, settings, generic_plan) ' || v_clean_query loop
|
|
100
|
-
v_lines := array_append(v_lines, v_line."QUERY PLAN");
|
|
101
|
-
end loop;
|
|
102
|
-
result := array_to_string(v_lines, e'\n');
|
|
103
|
-
end if;
|
|
104
|
-
exception when others then
|
|
105
|
-
-- Clean up hypothetical index before re-raising
|
|
106
|
-
if hypopg_index is not null then
|
|
107
|
-
perform hypopg_reset();
|
|
108
|
-
end if;
|
|
109
|
-
raise;
|
|
110
|
-
end;
|
|
111
|
-
|
|
112
|
-
-- Clean up hypothetical index
|
|
113
|
-
if hypopg_index is not null then
|
|
114
|
-
perform hypopg_reset();
|
|
115
|
-
end if;
|
|
116
|
-
end;
|
|
117
|
-
$$;
|
|
118
|
-
|
|
119
|
-
comment on function postgres_ai.explain_generic(text, text, text) is
|
|
120
|
-
'Returns generic EXPLAIN plan with optional HypoPG index testing (requires PG16+)';
|
|
121
|
-
|
|
122
|
-
-- Grant execute to the monitoring user
|
|
123
|
-
grant execute on function postgres_ai.explain_generic(text, text, text) to {{ROLE_IDENT}};
|
|
124
|
-
|
|
125
5
|
/*
|
|
126
6
|
* table_describe
|
|
127
7
|
*
|
|
@@ -435,5 +315,3 @@ comment on function postgres_ai.table_describe(text) is
|
|
|
435
315
|
'Returns comprehensive table information in compact text format for LLM analysis';
|
|
436
316
|
|
|
437
317
|
grant execute on function postgres_ai.table_describe(text) to {{ROLE_IDENT}};
|
|
438
|
-
|
|
439
|
-
|
package/lib/checkup-summary.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
@@ -924,14 +924,6 @@ export async function verifyInitSetup(params: {
|
|
|
924
924
|
}
|
|
925
925
|
|
|
926
926
|
// Check for helper functions
|
|
927
|
-
const explainFnRes = await params.client.query(
|
|
928
|
-
"select has_function_privilege($1, 'postgres_ai.explain_generic(text, text, text)', 'EXECUTE') as ok",
|
|
929
|
-
[role]
|
|
930
|
-
);
|
|
931
|
-
if (!explainFnRes.rows?.[0]?.ok) {
|
|
932
|
-
missingRequired.push("EXECUTE on postgres_ai.explain_generic(text, text, text)");
|
|
933
|
-
}
|
|
934
|
-
|
|
935
927
|
const tableDescribeFnRes = await params.client.query(
|
|
936
928
|
"select has_function_privilege($1, 'postgres_ai.table_describe(text)', 'EXECUTE') as ok",
|
|
937
929
|
[role]
|
|
@@ -1027,10 +1019,28 @@ export async function checkCurrentUserPermissions(
|
|
|
1027
1019
|
|
|
1028
1020
|
union all
|
|
1029
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
|
+
|
|
1030
1039
|
select
|
|
1031
1040
|
'postgres_ai.pg_statistic view exists' as permission_name,
|
|
1032
1041
|
'optional' as status,
|
|
1033
1042
|
case
|
|
1043
|
+
when to_regnamespace('postgres_ai') is null then null
|
|
1034
1044
|
when not has_schema_privilege(current_user, 'postgres_ai', 'USAGE') then null
|
|
1035
1045
|
else to_regclass('postgres_ai.pg_statistic') is not null
|
|
1036
1046
|
end as granted
|
|
@@ -1041,6 +1051,7 @@ export async function checkCurrentUserPermissions(
|
|
|
1041
1051
|
'select on postgres_ai.pg_statistic' as permission_name,
|
|
1042
1052
|
'optional' as status,
|
|
1043
1053
|
case
|
|
1054
|
+
when to_regnamespace('postgres_ai') is null then null
|
|
1044
1055
|
when not has_schema_privilege(current_user, 'postgres_ai', 'USAGE') then null
|
|
1045
1056
|
when to_regclass('postgres_ai.pg_statistic') is null then null
|
|
1046
1057
|
else has_table_privilege(current_user, 'postgres_ai.pg_statistic', 'select')
|
|
@@ -1060,6 +1071,10 @@ export async function checkCurrentUserPermissions(
|
|
|
1060
1071
|
when permission_name like 'select on pg_catalog.pg_index' then
|
|
1061
1072
|
format('grant select on pg_catalog.pg_index to %I;', current_user)
|
|
1062
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)
|
|
1063
1078
|
when permission_name = 'postgres_ai.pg_statistic view exists' and granted = false then
|
|
1064
1079
|
'-- create postgres_ai.pg_statistic view (see setup script)'
|
|
1065
1080
|
when permission_name = 'select on postgres_ai.pg_statistic' and granted = false then
|
|
@@ -1105,6 +1120,12 @@ export function formatPermissionCheckMessages(result: PreflightPermissionResult)
|
|
|
1105
1120
|
const errors: string[] = [];
|
|
1106
1121
|
|
|
1107
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
|
+
}
|
|
1108
1129
|
const fix = row.fix_command ? ` Fix: ${row.fix_command}` : "";
|
|
1109
1130
|
warnings.push(`Warning: optional permission missing — ${row.permission_name}.${fix}`);
|
|
1110
1131
|
}
|