postgresai 0.14.0-dev.46 → 0.14.0-dev.49
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/bun.lock +1 -1
- package/dist/bin/postgres-ai.js +436 -2718
- package/lib/metrics-loader.ts +449 -97
- package/package.json +2 -2
- package/dist/sql/01.role.sql +0 -16
- package/dist/sql/02.permissions.sql +0 -37
- package/dist/sql/03.optional_rds.sql +0 -6
- package/dist/sql/04.optional_self_managed.sql +0 -8
- package/dist/sql/05.helpers.sql +0 -415
package/lib/metrics-loader.ts
CHANGED
|
@@ -1,119 +1,467 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Embedded SQL queries for express checkup reports
|
|
3
3
|
*
|
|
4
|
-
* IMPORTANT:
|
|
5
|
-
*
|
|
4
|
+
* IMPORTANT: These SQL queries are extracted from config/pgwatch-prometheus/metrics.yml
|
|
5
|
+
* and embedded here for the CLI npm package to work without external dependencies.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* When updating queries, ensure both this file AND metrics.yml are kept in sync.
|
|
8
|
+
* The metrics.yml remains the source of truth for the monitoring stack.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
// When running from dist: cli/dist/lib/metrics-loader.js -> config/pgwatch-prometheus/metrics.yml
|
|
19
|
-
const currentDir = typeof __dirname !== "undefined"
|
|
20
|
-
? __dirname
|
|
21
|
-
: dirname(fileURLToPath(import.meta.url));
|
|
22
|
-
|
|
23
|
-
// Try multiple possible locations
|
|
24
|
-
const possiblePaths = [
|
|
25
|
-
resolve(currentDir, "../../config/pgwatch-prometheus/metrics.yml"), // from cli/lib
|
|
26
|
-
resolve(currentDir, "../../../config/pgwatch-prometheus/metrics.yml"), // from cli/dist/lib
|
|
27
|
-
resolve(currentDir, "../../../../config/pgwatch-prometheus/metrics.yml"), // deeper nesting
|
|
28
|
-
];
|
|
29
|
-
|
|
30
|
-
for (const path of possiblePaths) {
|
|
31
|
-
try {
|
|
32
|
-
readFileSync(path);
|
|
33
|
-
return path;
|
|
34
|
-
} catch {
|
|
35
|
-
// Try next path
|
|
36
|
-
}
|
|
37
|
-
}
|
|
11
|
+
/**
|
|
12
|
+
* Embedded SQL queries for each metric.
|
|
13
|
+
* Keys are metric names, values are SQL query strings.
|
|
14
|
+
*/
|
|
15
|
+
const EMBEDDED_SQL: Record<string, string> = {
|
|
16
|
+
// =========================================================================
|
|
17
|
+
// EXPRESS REPORTS - Simple settings and version queries
|
|
18
|
+
// =========================================================================
|
|
38
19
|
|
|
39
|
-
|
|
40
|
-
|
|
20
|
+
express_version: `
|
|
21
|
+
select
|
|
22
|
+
name,
|
|
23
|
+
setting
|
|
24
|
+
from pg_settings
|
|
25
|
+
where name in ('server_version', 'server_version_num');
|
|
26
|
+
`,
|
|
41
27
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
28
|
+
express_settings: `
|
|
29
|
+
select
|
|
30
|
+
name,
|
|
31
|
+
setting,
|
|
32
|
+
unit,
|
|
33
|
+
category,
|
|
34
|
+
context,
|
|
35
|
+
vartype,
|
|
36
|
+
case when (source <> 'default') then 0 else 1 end as is_default,
|
|
37
|
+
case
|
|
38
|
+
when unit = '8kB' then pg_size_pretty(setting::bigint * 8192)
|
|
39
|
+
when unit = 'kB' then pg_size_pretty(setting::bigint * 1024)
|
|
40
|
+
when unit = 'MB' then pg_size_pretty(setting::bigint * 1024 * 1024)
|
|
41
|
+
when unit = 'B' then pg_size_pretty(setting::bigint)
|
|
42
|
+
when unit = 'ms' then setting || ' ms'
|
|
43
|
+
when unit = 's' then setting || ' s'
|
|
44
|
+
when unit = 'min' then setting || ' min'
|
|
45
|
+
else setting
|
|
46
|
+
end as pretty_value
|
|
47
|
+
from pg_settings
|
|
48
|
+
order by name;
|
|
49
|
+
`,
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
express_altered_settings: `
|
|
52
|
+
select
|
|
53
|
+
name,
|
|
54
|
+
setting,
|
|
55
|
+
unit,
|
|
56
|
+
category,
|
|
57
|
+
case
|
|
58
|
+
when unit = '8kB' then pg_size_pretty(setting::bigint * 8192)
|
|
59
|
+
when unit = 'kB' then pg_size_pretty(setting::bigint * 1024)
|
|
60
|
+
when unit = 'MB' then pg_size_pretty(setting::bigint * 1024 * 1024)
|
|
61
|
+
when unit = 'B' then pg_size_pretty(setting::bigint)
|
|
62
|
+
when unit = 'ms' then setting || ' ms'
|
|
63
|
+
when unit = 's' then setting || ' s'
|
|
64
|
+
when unit = 'min' then setting || ' min'
|
|
65
|
+
else setting
|
|
66
|
+
end as pretty_value
|
|
67
|
+
from pg_settings
|
|
68
|
+
where source <> 'default'
|
|
69
|
+
order by name;
|
|
70
|
+
`,
|
|
56
71
|
|
|
57
|
-
|
|
72
|
+
express_database_sizes: `
|
|
73
|
+
select
|
|
74
|
+
datname,
|
|
75
|
+
pg_database_size(datname) as size_bytes
|
|
76
|
+
from pg_database
|
|
77
|
+
where datistemplate = false
|
|
78
|
+
order by size_bytes desc;
|
|
79
|
+
`,
|
|
58
80
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
81
|
+
express_cluster_stats: `
|
|
82
|
+
select
|
|
83
|
+
sum(numbackends) as total_connections,
|
|
84
|
+
sum(xact_commit) as total_commits,
|
|
85
|
+
sum(xact_rollback) as total_rollbacks,
|
|
86
|
+
sum(blks_read) as blocks_read,
|
|
87
|
+
sum(blks_hit) as blocks_hit,
|
|
88
|
+
sum(tup_returned) as tuples_returned,
|
|
89
|
+
sum(tup_fetched) as tuples_fetched,
|
|
90
|
+
sum(tup_inserted) as tuples_inserted,
|
|
91
|
+
sum(tup_updated) as tuples_updated,
|
|
92
|
+
sum(tup_deleted) as tuples_deleted,
|
|
93
|
+
sum(deadlocks) as total_deadlocks,
|
|
94
|
+
sum(temp_files) as temp_files_created,
|
|
95
|
+
sum(temp_bytes) as temp_bytes_written
|
|
96
|
+
from pg_stat_database
|
|
97
|
+
where datname is not null;
|
|
98
|
+
`,
|
|
99
|
+
|
|
100
|
+
express_connection_states: `
|
|
101
|
+
select
|
|
102
|
+
coalesce(state, 'null') as state,
|
|
103
|
+
count(*) as count
|
|
104
|
+
from pg_stat_activity
|
|
105
|
+
group by state;
|
|
106
|
+
`,
|
|
107
|
+
|
|
108
|
+
express_uptime: `
|
|
109
|
+
select
|
|
110
|
+
pg_postmaster_start_time() as start_time,
|
|
111
|
+
current_timestamp - pg_postmaster_start_time() as uptime;
|
|
112
|
+
`,
|
|
113
|
+
|
|
114
|
+
express_stats_reset: `
|
|
115
|
+
select
|
|
116
|
+
extract(epoch from stats_reset) as stats_reset_epoch,
|
|
117
|
+
stats_reset::text as stats_reset_time,
|
|
118
|
+
extract(day from (now() - stats_reset))::integer as days_since_reset,
|
|
119
|
+
extract(epoch from pg_postmaster_start_time()) as postmaster_startup_epoch,
|
|
120
|
+
pg_postmaster_start_time()::text as postmaster_startup_time
|
|
121
|
+
from pg_stat_database
|
|
122
|
+
where datname = current_database();
|
|
123
|
+
`,
|
|
124
|
+
|
|
125
|
+
express_current_database: `
|
|
126
|
+
select
|
|
127
|
+
current_database() as datname,
|
|
128
|
+
pg_database_size(current_database()) as size_bytes;
|
|
129
|
+
`,
|
|
130
|
+
|
|
131
|
+
// =========================================================================
|
|
132
|
+
// INDEX HEALTH REPORTS - H001, H002, H004
|
|
133
|
+
// =========================================================================
|
|
134
|
+
|
|
135
|
+
pg_invalid_indexes: `
|
|
136
|
+
with fk_indexes as (
|
|
137
|
+
select
|
|
138
|
+
schemaname as tag_schema_name,
|
|
139
|
+
(indexrelid::regclass)::text as tag_index_name,
|
|
140
|
+
(relid::regclass)::text as tag_table_name,
|
|
141
|
+
(confrelid::regclass)::text as tag_fk_table_ref,
|
|
142
|
+
array_to_string(indclass, ', ') as tag_opclasses
|
|
143
|
+
from
|
|
144
|
+
pg_stat_all_indexes
|
|
145
|
+
join pg_index using (indexrelid)
|
|
146
|
+
left join pg_constraint
|
|
147
|
+
on array_to_string(indkey, ',') = array_to_string(conkey, ',')
|
|
148
|
+
and schemaname = (connamespace::regnamespace)::text
|
|
149
|
+
and conrelid = relid
|
|
150
|
+
and contype = 'f'
|
|
151
|
+
where idx_scan = 0
|
|
152
|
+
and indisunique is false
|
|
153
|
+
and conkey is not null
|
|
154
|
+
), data as (
|
|
155
|
+
select
|
|
156
|
+
pci.relname as tag_index_name,
|
|
157
|
+
pn.nspname as tag_schema_name,
|
|
158
|
+
pct.relname as tag_table_name,
|
|
159
|
+
quote_ident(pn.nspname) as tag_schema_name,
|
|
160
|
+
quote_ident(pci.relname) as tag_index_name,
|
|
161
|
+
quote_ident(pct.relname) as tag_table_name,
|
|
162
|
+
coalesce(nullif(quote_ident(pn.nspname), 'public') || '.', '') || quote_ident(pct.relname) as tag_relation_name,
|
|
163
|
+
pg_relation_size(pidx.indexrelid) index_size_bytes,
|
|
164
|
+
((
|
|
165
|
+
select count(1)
|
|
166
|
+
from fk_indexes fi
|
|
167
|
+
where
|
|
168
|
+
fi.tag_fk_table_ref = pct.relname
|
|
169
|
+
and fi.tag_opclasses like (array_to_string(pidx.indclass, ', ') || '%')
|
|
170
|
+
) > 0)::int as supports_fk
|
|
171
|
+
from pg_index pidx
|
|
172
|
+
join pg_class as pci on pci.oid = pidx.indexrelid
|
|
173
|
+
join pg_class as pct on pct.oid = pidx.indrelid
|
|
174
|
+
left join pg_namespace pn on pn.oid = pct.relnamespace
|
|
175
|
+
where pidx.indisvalid = false
|
|
176
|
+
), data_total as (
|
|
177
|
+
select
|
|
178
|
+
sum(index_size_bytes) as index_size_bytes_sum
|
|
179
|
+
from data
|
|
180
|
+
), num_data as (
|
|
181
|
+
select
|
|
182
|
+
row_number() over () num,
|
|
183
|
+
data.*
|
|
184
|
+
from data
|
|
185
|
+
)
|
|
186
|
+
select
|
|
187
|
+
(extract(epoch from now()) * 1e9)::int8 as epoch_ns,
|
|
188
|
+
current_database() as tag_datname,
|
|
189
|
+
num_data.*
|
|
190
|
+
from num_data
|
|
191
|
+
limit 1000;
|
|
192
|
+
`,
|
|
193
|
+
|
|
194
|
+
unused_indexes: `
|
|
195
|
+
with fk_indexes as (
|
|
196
|
+
select
|
|
197
|
+
n.nspname as schema_name,
|
|
198
|
+
ci.relname as index_name,
|
|
199
|
+
cr.relname as table_name,
|
|
200
|
+
(confrelid::regclass)::text as fk_table_ref,
|
|
201
|
+
array_to_string(indclass, ', ') as opclasses
|
|
202
|
+
from pg_index i
|
|
203
|
+
join pg_class ci on ci.oid = i.indexrelid and ci.relkind = 'i'
|
|
204
|
+
join pg_class cr on cr.oid = i.indrelid and cr.relkind = 'r'
|
|
205
|
+
join pg_namespace n on n.oid = ci.relnamespace
|
|
206
|
+
join pg_constraint cn on cn.conrelid = cr.oid
|
|
207
|
+
left join pg_stat_all_indexes as si on si.indexrelid = i.indexrelid
|
|
208
|
+
where
|
|
209
|
+
contype = 'f'
|
|
210
|
+
and i.indisunique is false
|
|
211
|
+
and conkey is not null
|
|
212
|
+
and ci.relpages > 5
|
|
213
|
+
and si.idx_scan < 10
|
|
214
|
+
), table_scans as (
|
|
215
|
+
select relid,
|
|
216
|
+
tables.idx_scan + tables.seq_scan as all_scans,
|
|
217
|
+
( tables.n_tup_ins + tables.n_tup_upd + tables.n_tup_del ) as writes,
|
|
218
|
+
pg_relation_size(relid) as table_size
|
|
219
|
+
from pg_stat_all_tables as tables
|
|
220
|
+
join pg_class c on c.oid = relid
|
|
221
|
+
where c.relpages > 5
|
|
222
|
+
), indexes as (
|
|
223
|
+
select
|
|
224
|
+
i.indrelid,
|
|
225
|
+
i.indexrelid,
|
|
226
|
+
n.nspname as schema_name,
|
|
227
|
+
cr.relname as table_name,
|
|
228
|
+
ci.relname as index_name,
|
|
229
|
+
si.idx_scan,
|
|
230
|
+
pg_relation_size(i.indexrelid) as index_bytes,
|
|
231
|
+
ci.relpages,
|
|
232
|
+
(case when a.amname = 'btree' then true else false end) as idx_is_btree,
|
|
233
|
+
array_to_string(i.indclass, ', ') as opclasses
|
|
234
|
+
from pg_index i
|
|
235
|
+
join pg_class ci on ci.oid = i.indexrelid and ci.relkind = 'i'
|
|
236
|
+
join pg_class cr on cr.oid = i.indrelid and cr.relkind = 'r'
|
|
237
|
+
join pg_namespace n on n.oid = ci.relnamespace
|
|
238
|
+
join pg_am a on ci.relam = a.oid
|
|
239
|
+
left join pg_stat_all_indexes as si on si.indexrelid = i.indexrelid
|
|
240
|
+
where
|
|
241
|
+
i.indisunique = false
|
|
242
|
+
and i.indisvalid = true
|
|
243
|
+
and ci.relpages > 5
|
|
244
|
+
), index_ratios as (
|
|
245
|
+
select
|
|
246
|
+
i.indexrelid as index_id,
|
|
247
|
+
i.schema_name,
|
|
248
|
+
i.table_name,
|
|
249
|
+
i.index_name,
|
|
250
|
+
idx_scan,
|
|
251
|
+
all_scans,
|
|
252
|
+
round(( case when all_scans = 0 then 0.0::numeric
|
|
253
|
+
else idx_scan::numeric/all_scans * 100 end), 2) as index_scan_pct,
|
|
254
|
+
writes,
|
|
255
|
+
round((case when writes = 0 then idx_scan::numeric else idx_scan::numeric/writes end), 2)
|
|
256
|
+
as scans_per_write,
|
|
257
|
+
index_bytes as index_size_bytes,
|
|
258
|
+
table_size as table_size_bytes,
|
|
259
|
+
i.relpages,
|
|
260
|
+
idx_is_btree,
|
|
261
|
+
i.opclasses,
|
|
262
|
+
(
|
|
263
|
+
select count(1)
|
|
264
|
+
from fk_indexes fi
|
|
265
|
+
where fi.fk_table_ref = i.table_name
|
|
266
|
+
and fi.schema_name = i.schema_name
|
|
267
|
+
and fi.opclasses like (i.opclasses || '%')
|
|
268
|
+
) > 0 as supports_fk
|
|
269
|
+
from indexes i
|
|
270
|
+
join table_scans ts on ts.relid = i.indrelid
|
|
271
|
+
)
|
|
272
|
+
select
|
|
273
|
+
'Never Used Indexes' as tag_reason,
|
|
274
|
+
current_database() as tag_datname,
|
|
275
|
+
index_id,
|
|
276
|
+
schema_name as tag_schema_name,
|
|
277
|
+
table_name as tag_table_name,
|
|
278
|
+
index_name as tag_index_name,
|
|
279
|
+
pg_get_indexdef(index_id) as index_definition,
|
|
280
|
+
idx_scan,
|
|
281
|
+
all_scans,
|
|
282
|
+
index_scan_pct,
|
|
283
|
+
writes,
|
|
284
|
+
scans_per_write,
|
|
285
|
+
index_size_bytes,
|
|
286
|
+
table_size_bytes,
|
|
287
|
+
relpages,
|
|
288
|
+
idx_is_btree,
|
|
289
|
+
opclasses as tag_opclasses,
|
|
290
|
+
supports_fk
|
|
291
|
+
from index_ratios
|
|
292
|
+
where
|
|
293
|
+
idx_scan = 0
|
|
294
|
+
and idx_is_btree
|
|
295
|
+
order by index_size_bytes desc
|
|
296
|
+
limit 1000;
|
|
297
|
+
`,
|
|
298
|
+
|
|
299
|
+
redundant_indexes: `
|
|
300
|
+
with fk_indexes as (
|
|
301
|
+
select
|
|
302
|
+
n.nspname as schema_name,
|
|
303
|
+
ci.relname as index_name,
|
|
304
|
+
cr.relname as table_name,
|
|
305
|
+
(confrelid::regclass)::text as fk_table_ref,
|
|
306
|
+
array_to_string(indclass, ', ') as opclasses
|
|
307
|
+
from pg_index i
|
|
308
|
+
join pg_class ci on ci.oid = i.indexrelid and ci.relkind = 'i'
|
|
309
|
+
join pg_class cr on cr.oid = i.indrelid and cr.relkind = 'r'
|
|
310
|
+
join pg_namespace n on n.oid = ci.relnamespace
|
|
311
|
+
join pg_constraint cn on cn.conrelid = cr.oid
|
|
312
|
+
left join pg_stat_all_indexes as si on si.indexrelid = i.indexrelid
|
|
313
|
+
where
|
|
314
|
+
contype = 'f'
|
|
315
|
+
and i.indisunique is false
|
|
316
|
+
and conkey is not null
|
|
317
|
+
and ci.relpages > 5
|
|
318
|
+
and si.idx_scan < 10
|
|
319
|
+
),
|
|
320
|
+
index_data as (
|
|
321
|
+
select
|
|
322
|
+
*,
|
|
323
|
+
indkey::text as columns,
|
|
324
|
+
array_to_string(indclass, ', ') as opclasses
|
|
325
|
+
from pg_index i
|
|
326
|
+
join pg_class ci on ci.oid = i.indexrelid and ci.relkind = 'i'
|
|
327
|
+
where indisvalid = true and ci.relpages > 5
|
|
328
|
+
), redundant_indexes as (
|
|
329
|
+
select
|
|
330
|
+
i2.indexrelid as index_id,
|
|
331
|
+
tnsp.nspname as schema_name,
|
|
332
|
+
trel.relname as table_name,
|
|
333
|
+
pg_relation_size(trel.oid) as table_size_bytes,
|
|
334
|
+
irel.relname as index_name,
|
|
335
|
+
am1.amname as access_method,
|
|
336
|
+
(i1.indexrelid::regclass)::text as reason,
|
|
337
|
+
i1.indexrelid as reason_index_id,
|
|
338
|
+
pg_get_indexdef(i1.indexrelid) main_index_def,
|
|
339
|
+
pg_size_pretty(pg_relation_size(i1.indexrelid)) main_index_size,
|
|
340
|
+
pg_get_indexdef(i2.indexrelid) index_def,
|
|
341
|
+
pg_relation_size(i2.indexrelid) index_size_bytes,
|
|
342
|
+
s.idx_scan as index_usage,
|
|
343
|
+
quote_ident(tnsp.nspname) as formated_schema_name,
|
|
344
|
+
coalesce(nullif(quote_ident(tnsp.nspname), 'public') || '.', '') || quote_ident(irel.relname) as formated_index_name,
|
|
345
|
+
quote_ident(trel.relname) as formated_table_name,
|
|
346
|
+
coalesce(nullif(quote_ident(tnsp.nspname), 'public') || '.', '') || quote_ident(trel.relname) as formated_relation_name,
|
|
347
|
+
i2.opclasses
|
|
348
|
+
from (
|
|
349
|
+
select indrelid, indexrelid, opclasses, indclass, indexprs, indpred, indisprimary, indisunique, columns
|
|
350
|
+
from index_data
|
|
351
|
+
order by indexrelid
|
|
352
|
+
) as i1
|
|
353
|
+
join index_data as i2 on (
|
|
354
|
+
i1.indrelid = i2.indrelid
|
|
355
|
+
and i1.indexrelid <> i2.indexrelid
|
|
356
|
+
)
|
|
357
|
+
inner join pg_opclass op1 on i1.indclass[0] = op1.oid
|
|
358
|
+
inner join pg_opclass op2 on i2.indclass[0] = op2.oid
|
|
359
|
+
inner join pg_am am1 on op1.opcmethod = am1.oid
|
|
360
|
+
inner join pg_am am2 on op2.opcmethod = am2.oid
|
|
361
|
+
join pg_stat_all_indexes as s on s.indexrelid = i2.indexrelid
|
|
362
|
+
join pg_class as trel on trel.oid = i2.indrelid
|
|
363
|
+
join pg_namespace as tnsp on trel.relnamespace = tnsp.oid
|
|
364
|
+
join pg_class as irel on irel.oid = i2.indexrelid
|
|
365
|
+
where
|
|
366
|
+
not i2.indisprimary
|
|
367
|
+
and not i2.indisunique
|
|
368
|
+
and am1.amname = am2.amname
|
|
369
|
+
and i1.columns like (i2.columns || '%')
|
|
370
|
+
and i1.opclasses like (i2.opclasses || '%')
|
|
371
|
+
and pg_get_expr(i1.indexprs, i1.indrelid) is not distinct from pg_get_expr(i2.indexprs, i2.indrelid)
|
|
372
|
+
and pg_get_expr(i1.indpred, i1.indrelid) is not distinct from pg_get_expr(i2.indpred, i2.indrelid)
|
|
373
|
+
), redundant_indexes_fk as (
|
|
374
|
+
select
|
|
375
|
+
ri.*,
|
|
376
|
+
((
|
|
377
|
+
select count(1)
|
|
378
|
+
from fk_indexes fi
|
|
379
|
+
where
|
|
380
|
+
fi.fk_table_ref = ri.table_name
|
|
381
|
+
and fi.opclasses like (ri.opclasses || '%')
|
|
382
|
+
) > 0)::int as supports_fk
|
|
383
|
+
from redundant_indexes ri
|
|
384
|
+
),
|
|
385
|
+
redundant_indexes_tmp_num as (
|
|
386
|
+
select row_number() over () num, rig.*
|
|
387
|
+
from redundant_indexes_fk rig
|
|
388
|
+
), redundant_indexes_tmp_links as (
|
|
389
|
+
select
|
|
390
|
+
ri1.*,
|
|
391
|
+
ri2.num as r_num
|
|
392
|
+
from redundant_indexes_tmp_num ri1
|
|
393
|
+
left join redundant_indexes_tmp_num ri2 on ri2.reason_index_id = ri1.index_id and ri1.reason_index_id = ri2.index_id
|
|
394
|
+
), redundant_indexes_tmp_cut as (
|
|
395
|
+
select
|
|
396
|
+
*
|
|
397
|
+
from redundant_indexes_tmp_links
|
|
398
|
+
where num < r_num or r_num is null
|
|
399
|
+
), redundant_indexes_cut_grouped as (
|
|
400
|
+
select
|
|
401
|
+
distinct(num),
|
|
402
|
+
*
|
|
403
|
+
from redundant_indexes_tmp_cut
|
|
404
|
+
order by index_size_bytes desc
|
|
405
|
+
), redundant_indexes_grouped as (
|
|
406
|
+
select
|
|
407
|
+
index_id,
|
|
408
|
+
schema_name as tag_schema_name,
|
|
409
|
+
table_name,
|
|
410
|
+
table_size_bytes,
|
|
411
|
+
index_name as tag_index_name,
|
|
412
|
+
access_method as tag_access_method,
|
|
413
|
+
string_agg(distinct reason, ', ') as tag_reason,
|
|
414
|
+
index_size_bytes,
|
|
415
|
+
index_usage,
|
|
416
|
+
index_def as index_definition,
|
|
417
|
+
formated_index_name as tag_index_name,
|
|
418
|
+
formated_schema_name as tag_schema_name,
|
|
419
|
+
formated_table_name as tag_table_name,
|
|
420
|
+
formated_relation_name as tag_relation_name,
|
|
421
|
+
supports_fk::int as supports_fk
|
|
422
|
+
from redundant_indexes_cut_grouped
|
|
423
|
+
group by
|
|
424
|
+
index_id,
|
|
425
|
+
table_size_bytes,
|
|
426
|
+
schema_name,
|
|
427
|
+
table_name,
|
|
428
|
+
index_name,
|
|
429
|
+
access_method,
|
|
430
|
+
index_def,
|
|
431
|
+
index_size_bytes,
|
|
432
|
+
index_usage,
|
|
433
|
+
formated_index_name,
|
|
434
|
+
formated_schema_name,
|
|
435
|
+
formated_table_name,
|
|
436
|
+
formated_relation_name,
|
|
437
|
+
supports_fk
|
|
438
|
+
order by index_size_bytes desc
|
|
439
|
+
)
|
|
440
|
+
select * from redundant_indexes_grouped
|
|
441
|
+
limit 1000;
|
|
442
|
+
`,
|
|
443
|
+
};
|
|
72
444
|
|
|
73
445
|
/**
|
|
74
|
-
* Get SQL query for a specific metric
|
|
75
|
-
* Falls back to lower versions if exact version not found.
|
|
446
|
+
* Get SQL query for a specific metric.
|
|
76
447
|
*
|
|
77
|
-
* @param metricName - Name of the metric
|
|
78
|
-
* @param
|
|
448
|
+
* @param metricName - Name of the metric (e.g., "pg_invalid_indexes", "express_version")
|
|
449
|
+
* @param _pgMajorVersion - PostgreSQL major version (currently unused, for future version-specific queries)
|
|
79
450
|
* @returns SQL query string
|
|
80
451
|
*/
|
|
81
|
-
export function getMetricSql(metricName: string,
|
|
82
|
-
const
|
|
83
|
-
const metric = root.metrics[metricName];
|
|
84
|
-
|
|
85
|
-
if (!metric) {
|
|
86
|
-
throw new Error(`Metric "${metricName}" not found in metrics.yml`);
|
|
87
|
-
}
|
|
452
|
+
export function getMetricSql(metricName: string, _pgMajorVersion: number = 16): string {
|
|
453
|
+
const sql = EMBEDDED_SQL[metricName];
|
|
88
454
|
|
|
89
|
-
if (!
|
|
90
|
-
throw new Error(`Metric "${metricName}"
|
|
455
|
+
if (!sql) {
|
|
456
|
+
throw new Error(`Metric "${metricName}" not found. Available metrics: ${Object.keys(EMBEDDED_SQL).join(", ")}`);
|
|
91
457
|
}
|
|
92
458
|
|
|
93
|
-
|
|
94
|
-
const versions = Object.keys(metric.sqls)
|
|
95
|
-
.map(Number)
|
|
96
|
-
.filter(v => !isNaN(v))
|
|
97
|
-
.sort((a, b) => b - a); // Sort descending
|
|
98
|
-
|
|
99
|
-
for (const version of versions) {
|
|
100
|
-
if (version <= pgMajorVersion) {
|
|
101
|
-
return metric.sqls[version.toString()];
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// If no matching version, use the lowest available
|
|
106
|
-
const lowestVersion = versions[versions.length - 1];
|
|
107
|
-
if (lowestVersion !== undefined) {
|
|
108
|
-
return metric.sqls[lowestVersion.toString()];
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
throw new Error(`No SQL query found for metric "${metricName}"`);
|
|
459
|
+
return sql;
|
|
112
460
|
}
|
|
113
461
|
|
|
114
462
|
/**
|
|
115
|
-
* Metric names
|
|
116
|
-
* These map check IDs to metric names in
|
|
463
|
+
* Metric names that correspond to express report checks.
|
|
464
|
+
* These map check IDs to metric names in the EMBEDDED_SQL object.
|
|
117
465
|
*/
|
|
118
466
|
export const METRIC_NAMES = {
|
|
119
467
|
// Index health checks
|
|
@@ -133,8 +481,8 @@ export const METRIC_NAMES = {
|
|
|
133
481
|
} as const;
|
|
134
482
|
|
|
135
483
|
/**
|
|
136
|
-
* Transform a row from metrics
|
|
137
|
-
* Metrics
|
|
484
|
+
* Transform a row from metrics query output to JSON report format.
|
|
485
|
+
* Metrics use `tag_` prefix for dimensions; we strip it for JSON reports.
|
|
138
486
|
* Also removes Prometheus-specific fields like epoch_ns, num.
|
|
139
487
|
*/
|
|
140
488
|
export function transformMetricRow(row: Record<string, unknown>): Record<string, unknown> {
|
|
@@ -154,3 +502,7 @@ export function transformMetricRow(row: Record<string, unknown>): Record<string,
|
|
|
154
502
|
return result;
|
|
155
503
|
}
|
|
156
504
|
|
|
505
|
+
// Legacy export for backward compatibility (no longer loads from file)
|
|
506
|
+
export function loadMetricsYml(): { metrics: Record<string, unknown> } {
|
|
507
|
+
return { metrics: EMBEDDED_SQL };
|
|
508
|
+
}
|
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.49",
|
|
4
4
|
"description": "postgres_ai CLI",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"private": false,
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"node": ">=18"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
|
-
"build": "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'))\"
|
|
25
|
+
"build": "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'))\"",
|
|
26
26
|
"prepublishOnly": "npm run build",
|
|
27
27
|
"start": "bun ./bin/postgres-ai.ts --help",
|
|
28
28
|
"start:node": "node ./dist/bin/postgres-ai.js --help",
|
package/dist/sql/01.role.sql
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
-- Role creation / password update (template-filled by cli/lib/init.ts)
|
|
2
|
-
--
|
|
3
|
-
-- Always uses a race-safe pattern (create if missing, then always alter to set the password):
|
|
4
|
-
-- do $$ begin
|
|
5
|
-
-- if not exists (select 1 from pg_catalog.pg_roles where rolname = '...') then
|
|
6
|
-
-- begin
|
|
7
|
-
-- create user "..." with password '...';
|
|
8
|
-
-- exception when duplicate_object then
|
|
9
|
-
-- null;
|
|
10
|
-
-- end;
|
|
11
|
-
-- end if;
|
|
12
|
-
-- alter user "..." with password '...';
|
|
13
|
-
-- end $$;
|
|
14
|
-
{{ROLE_STMT}}
|
|
15
|
-
|
|
16
|
-
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
-- Required permissions for postgres_ai monitoring user (template-filled by cli/lib/init.ts)
|
|
2
|
-
|
|
3
|
-
-- Allow connect
|
|
4
|
-
grant connect on database {{DB_IDENT}} to {{ROLE_IDENT}};
|
|
5
|
-
|
|
6
|
-
-- Standard monitoring privileges
|
|
7
|
-
grant pg_monitor to {{ROLE_IDENT}};
|
|
8
|
-
grant select on pg_catalog.pg_index to {{ROLE_IDENT}};
|
|
9
|
-
|
|
10
|
-
-- Create postgres_ai schema for our objects
|
|
11
|
-
create schema if not exists postgres_ai;
|
|
12
|
-
grant usage on schema postgres_ai to {{ROLE_IDENT}};
|
|
13
|
-
|
|
14
|
-
-- For bloat analysis: expose pg_statistic via a view
|
|
15
|
-
create or replace view postgres_ai.pg_statistic as
|
|
16
|
-
select
|
|
17
|
-
n.nspname as schemaname,
|
|
18
|
-
c.relname as tablename,
|
|
19
|
-
a.attname,
|
|
20
|
-
s.stanullfrac as null_frac,
|
|
21
|
-
s.stawidth as avg_width,
|
|
22
|
-
false as inherited
|
|
23
|
-
from pg_catalog.pg_statistic s
|
|
24
|
-
join pg_catalog.pg_class c on c.oid = s.starelid
|
|
25
|
-
join pg_catalog.pg_namespace n on n.oid = c.relnamespace
|
|
26
|
-
join pg_catalog.pg_attribute a on a.attrelid = s.starelid and a.attnum = s.staattnum
|
|
27
|
-
where a.attnum > 0 and not a.attisdropped;
|
|
28
|
-
|
|
29
|
-
grant select on postgres_ai.pg_statistic to {{ROLE_IDENT}};
|
|
30
|
-
|
|
31
|
-
-- Hardened clusters sometimes revoke PUBLIC on schema public
|
|
32
|
-
grant usage on schema public to {{ROLE_IDENT}};
|
|
33
|
-
|
|
34
|
-
-- Keep search_path predictable; postgres_ai first so our objects are found
|
|
35
|
-
alter user {{ROLE_IDENT}} set search_path = postgres_ai, "$user", public, pg_catalog;
|
|
36
|
-
|
|
37
|
-
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
-- Optional permissions for self-managed Postgres (best effort)
|
|
2
|
-
|
|
3
|
-
grant execute on function pg_catalog.pg_stat_file(text) to {{ROLE_IDENT}};
|
|
4
|
-
grant execute on function pg_catalog.pg_stat_file(text, boolean) to {{ROLE_IDENT}};
|
|
5
|
-
grant execute on function pg_catalog.pg_ls_dir(text) to {{ROLE_IDENT}};
|
|
6
|
-
grant execute on function pg_catalog.pg_ls_dir(text, boolean, boolean) to {{ROLE_IDENT}};
|
|
7
|
-
|
|
8
|
-
|