postgresai 0.14.0-dev.75 → 0.14.0-dev.77
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/bin/postgres-ai.ts +312 -6
- package/dist/bin/postgres-ai.js +916 -40
- package/dist/sql/02.extensions.sql +8 -0
- package/dist/sql/{02.permissions.sql → 03.permissions.sql} +1 -0
- package/dist/sql/sql/02.extensions.sql +8 -0
- package/dist/sql/sql/{02.permissions.sql → 03.permissions.sql} +1 -0
- package/dist/sql/sql/uninit/01.helpers.sql +5 -0
- package/dist/sql/sql/uninit/02.permissions.sql +30 -0
- package/dist/sql/sql/uninit/03.role.sql +27 -0
- package/dist/sql/uninit/01.helpers.sql +5 -0
- package/dist/sql/uninit/02.permissions.sql +30 -0
- package/dist/sql/uninit/03.role.sql +27 -0
- package/lib/checkup-dictionary.ts +113 -0
- package/lib/checkup.ts +21 -14
- package/lib/init.ts +109 -8
- package/package.json +9 -7
- package/scripts/embed-checkup-dictionary.ts +106 -0
- package/sql/02.extensions.sql +8 -0
- package/sql/{02.permissions.sql → 03.permissions.sql} +1 -0
- package/sql/uninit/01.helpers.sql +5 -0
- package/sql/uninit/02.permissions.sql +30 -0
- package/sql/uninit/03.role.sql +27 -0
- package/test/checkup.test.ts +17 -18
- package/test/init.test.ts +245 -11
- package/lib/metrics-embedded.ts +0 -79
- /package/dist/sql/{03.optional_rds.sql → 04.optional_rds.sql} +0 -0
- /package/dist/sql/{04.optional_self_managed.sql → 05.optional_self_managed.sql} +0 -0
- /package/dist/sql/{05.helpers.sql → 06.helpers.sql} +0 -0
- /package/dist/sql/sql/{03.optional_rds.sql → 04.optional_rds.sql} +0 -0
- /package/dist/sql/sql/{04.optional_self_managed.sql → 05.optional_self_managed.sql} +0 -0
- /package/dist/sql/sql/{05.helpers.sql → 06.helpers.sql} +0 -0
- /package/sql/{03.optional_rds.sql → 04.optional_rds.sql} +0 -0
- /package/sql/{04.optional_self_managed.sql → 05.optional_self_managed.sql} +0 -0
- /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,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,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,113 @@
|
|
|
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
|
+
*/
|
|
36
|
+
const dictionaryByCode: Map<string, CheckupDictionaryEntry> = new Map(
|
|
37
|
+
CHECKUP_DICTIONARY_DATA.map((entry) => [entry.code, entry])
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get all checkup dictionary entries.
|
|
42
|
+
*
|
|
43
|
+
* @returns Array of all checkup dictionary entries
|
|
44
|
+
*/
|
|
45
|
+
export function getAllCheckupEntries(): CheckupDictionaryEntry[] {
|
|
46
|
+
return CHECKUP_DICTIONARY_DATA;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get a checkup dictionary entry by its code.
|
|
51
|
+
*
|
|
52
|
+
* @param code - The check code (e.g., "A001", "H002")
|
|
53
|
+
* @returns The dictionary entry or null if not found
|
|
54
|
+
*/
|
|
55
|
+
export function getCheckupEntry(code: string): CheckupDictionaryEntry | null {
|
|
56
|
+
return dictionaryByCode.get(code.toUpperCase()) ?? null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get the title for a checkup code.
|
|
61
|
+
*
|
|
62
|
+
* @param code - The check code (e.g., "A001", "H002")
|
|
63
|
+
* @returns The title or the code itself if not found
|
|
64
|
+
*/
|
|
65
|
+
export function getCheckupTitle(code: string): string {
|
|
66
|
+
const entry = getCheckupEntry(code);
|
|
67
|
+
return entry?.title ?? code;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if a code exists in the dictionary.
|
|
72
|
+
*
|
|
73
|
+
* @param code - The check code to validate
|
|
74
|
+
* @returns True if the code exists in the dictionary
|
|
75
|
+
*/
|
|
76
|
+
export function isValidCheckupCode(code: string): boolean {
|
|
77
|
+
return dictionaryByCode.has(code.toUpperCase());
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get all check codes as an array.
|
|
82
|
+
*
|
|
83
|
+
* @returns Array of all check codes (e.g., ["A001", "A002", ...])
|
|
84
|
+
*/
|
|
85
|
+
export function getAllCheckupCodes(): string[] {
|
|
86
|
+
return CHECKUP_DICTIONARY_DATA.map((entry) => entry.code);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get checkup entries filtered by category.
|
|
91
|
+
*
|
|
92
|
+
* @param category - The category to filter by (e.g., "indexes", "vacuum")
|
|
93
|
+
* @returns Array of entries in the specified category
|
|
94
|
+
*/
|
|
95
|
+
export function getCheckupEntriesByCategory(category: string): CheckupDictionaryEntry[] {
|
|
96
|
+
return CHECKUP_DICTIONARY_DATA.filter(
|
|
97
|
+
(entry) => entry.category.toLowerCase() === category.toLowerCase()
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Build a code-to-title mapping object.
|
|
103
|
+
* Useful for backwards compatibility with CHECK_INFO style usage.
|
|
104
|
+
*
|
|
105
|
+
* @returns Object mapping check codes to titles (e.g., { "A001": "System information", ... })
|
|
106
|
+
*/
|
|
107
|
+
export function buildCheckInfoMap(): Record<string, string> {
|
|
108
|
+
const result: Record<string, string> = {};
|
|
109
|
+
for (const entry of CHECKUP_DICTIONARY_DATA) {
|
|
110
|
+
result[entry.code] = entry.title;
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
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;
|
|
@@ -1344,21 +1345,27 @@ export const REPORT_GENERATORS: Record<string, (client: Client, nodeName: string
|
|
|
1344
1345
|
};
|
|
1345
1346
|
|
|
1346
1347
|
/**
|
|
1347
|
-
* Check IDs and titles
|
|
1348
|
+
* Check IDs and titles.
|
|
1349
|
+
*
|
|
1350
|
+
* This mapping is built from the embedded checkup dictionary, which is
|
|
1351
|
+
* fetched from https://postgres.ai/api/general/checkup_dictionary at build time.
|
|
1352
|
+
*
|
|
1353
|
+
* For the full dictionary (all available checks), use the checkup-dictionary module.
|
|
1354
|
+
* CHECK_INFO is filtered to only include checks that have express-mode generators.
|
|
1348
1355
|
*/
|
|
1349
|
-
export const CHECK_INFO: Record<string, string> = {
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
};
|
|
1356
|
+
export const CHECK_INFO: Record<string, string> = (() => {
|
|
1357
|
+
// Build the full dictionary map
|
|
1358
|
+
const fullMap = buildCheckInfoMap();
|
|
1359
|
+
|
|
1360
|
+
// Filter to only include checks that have express-mode generators
|
|
1361
|
+
const expressCheckIds = Object.keys(REPORT_GENERATORS);
|
|
1362
|
+
const filtered: Record<string, string> = {};
|
|
1363
|
+
for (const checkId of expressCheckIds) {
|
|
1364
|
+
// Use dictionary title if available, otherwise use a fallback
|
|
1365
|
+
filtered[checkId] = fullMap[checkId] || checkId;
|
|
1366
|
+
}
|
|
1367
|
+
return filtered;
|
|
1368
|
+
})();
|
|
1362
1369
|
|
|
1363
1370
|
/**
|
|
1364
1371
|
* 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
|
-
|
|
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: "
|
|
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: "
|
|
555
|
-
sql: applyTemplate(loadSqlTemplate("
|
|
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: "
|
|
562
|
-
sql: applyTemplate(loadSqlTemplate("
|
|
567
|
+
name: "04.optional_rds",
|
|
568
|
+
sql: applyTemplate(loadSqlTemplate("04.optional_rds.sql"), vars),
|
|
563
569
|
optional: true,
|
|
564
570
|
},
|
|
565
571
|
{
|
|
566
|
-
name: "
|
|
567
|
-
sql: applyTemplate(loadSqlTemplate("
|
|
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-dev.
|
|
3
|
+
"version": "0.14.0-dev.77",
|
|
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
|
-
"
|
|
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-
|
|
34
|
-
"test": "bun run embed-
|
|
35
|
-
"test:fast": "bun run embed-
|
|
36
|
-
"test:coverage": "bun run embed-
|
|
37
|
-
"typecheck": "bun run embed-
|
|
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",
|
|
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",
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Build script to fetch checkup dictionary from API and embed it.
|
|
4
|
+
*
|
|
5
|
+
* This script fetches from https://postgres.ai/api/general/checkup_dictionary
|
|
6
|
+
* and generates cli/lib/checkup-dictionary-embedded.ts with the data embedded.
|
|
7
|
+
*
|
|
8
|
+
* The generated file is NOT committed to git - it's regenerated at build time.
|
|
9
|
+
*
|
|
10
|
+
* Usage: bun run scripts/embed-checkup-dictionary.ts
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as fs from "fs";
|
|
14
|
+
import * as path from "path";
|
|
15
|
+
|
|
16
|
+
// API endpoint - always available without auth
|
|
17
|
+
const DICTIONARY_URL = "https://postgres.ai/api/general/checkup_dictionary";
|
|
18
|
+
|
|
19
|
+
// Output path relative to cli/ directory
|
|
20
|
+
const CLI_DIR = path.resolve(__dirname, "..");
|
|
21
|
+
const OUTPUT_PATH = path.resolve(CLI_DIR, "lib/checkup-dictionary-embedded.ts");
|
|
22
|
+
|
|
23
|
+
// Request timeout (10 seconds)
|
|
24
|
+
const FETCH_TIMEOUT_MS = 10_000;
|
|
25
|
+
|
|
26
|
+
interface CheckupDictionaryEntry {
|
|
27
|
+
code: string;
|
|
28
|
+
title: string;
|
|
29
|
+
description: string;
|
|
30
|
+
category: string;
|
|
31
|
+
sort_order: number | null;
|
|
32
|
+
is_system_report: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function generateTypeScript(data: CheckupDictionaryEntry[], sourceUrl: string): string {
|
|
36
|
+
const lines: string[] = [
|
|
37
|
+
"// AUTO-GENERATED FILE - DO NOT EDIT",
|
|
38
|
+
`// Generated from: ${sourceUrl}`,
|
|
39
|
+
`// Generated at: ${new Date().toISOString()}`,
|
|
40
|
+
"// To regenerate: bun run embed-checkup-dictionary",
|
|
41
|
+
"",
|
|
42
|
+
'import { CheckupDictionaryEntry } from "./checkup-dictionary";',
|
|
43
|
+
"",
|
|
44
|
+
"/**",
|
|
45
|
+
" * Embedded checkup dictionary data fetched from API at build time.",
|
|
46
|
+
" * Contains all available checkup report codes, titles, and metadata.",
|
|
47
|
+
" */",
|
|
48
|
+
`export const CHECKUP_DICTIONARY_DATA: CheckupDictionaryEntry[] = ${JSON.stringify(data, null, 2)};`,
|
|
49
|
+
"",
|
|
50
|
+
];
|
|
51
|
+
return lines.join("\n");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function fetchWithTimeout(url: string, timeoutMs: number): Promise<Response> {
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
60
|
+
return response;
|
|
61
|
+
} finally {
|
|
62
|
+
clearTimeout(timeoutId);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function main() {
|
|
67
|
+
console.log(`Fetching checkup dictionary from: ${DICTIONARY_URL}`);
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const response = await fetchWithTimeout(DICTIONARY_URL, FETCH_TIMEOUT_MS);
|
|
71
|
+
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const data: CheckupDictionaryEntry[] = await response.json();
|
|
77
|
+
|
|
78
|
+
if (!Array.isArray(data)) {
|
|
79
|
+
throw new Error("Expected array response from API");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Validate entries have required fields
|
|
83
|
+
for (const entry of data) {
|
|
84
|
+
if (!entry.code || !entry.title) {
|
|
85
|
+
throw new Error(`Invalid entry missing code or title: ${JSON.stringify(entry)}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const tsCode = generateTypeScript(data, DICTIONARY_URL);
|
|
90
|
+
fs.writeFileSync(OUTPUT_PATH, tsCode, "utf8");
|
|
91
|
+
|
|
92
|
+
console.log(`Generated: ${OUTPUT_PATH}`);
|
|
93
|
+
console.log(`Dictionary contains ${data.length} entries`);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
96
|
+
console.warn(`Warning: Failed to fetch checkup dictionary: ${errorMsg}`);
|
|
97
|
+
console.warn("Generating empty dictionary as fallback");
|
|
98
|
+
|
|
99
|
+
// Generate empty dictionary to allow build to proceed
|
|
100
|
+
const fallbackTs = generateTypeScript([], `N/A (fetch failed: ${errorMsg})`);
|
|
101
|
+
fs.writeFileSync(OUTPUT_PATH, fallbackTs, "utf8");
|
|
102
|
+
console.log(`Generated fallback dictionary at ${OUTPUT_PATH}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
main();
|
|
@@ -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
|
|