postgresai 0.14.0-dev.45 → 0.14.0-dev.48

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.
@@ -1,119 +1,467 @@
1
1
  /**
2
- * Load SQL queries from metrics.yml
2
+ * Embedded SQL queries for express checkup reports
3
3
  *
4
- * IMPORTANT: This module loads SQL queries directly from config/pgwatch-prometheus/metrics.yml
5
- * to avoid code duplication. The metrics.yml is the single source of truth for metric extraction logic.
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
- * DO NOT copy-paste SQL queries into TypeScript code. Always load them from metrics.yml.
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
- import { readFileSync } from "fs";
11
- import { resolve, dirname } from "path";
12
- import { fileURLToPath } from "url";
13
- import * as yaml from "js-yaml";
14
-
15
- // Get the path to metrics.yml relative to this file
16
- function getMetricsYmlPath(): string {
17
- // When running from source: cli/lib/metrics-loader.ts -> config/pgwatch-prometheus/metrics.yml
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
- throw new Error(`Cannot find metrics.yml. Tried: ${possiblePaths.join(", ")}`);
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
- interface MetricDefinition {
43
- description?: string;
44
- sqls?: {
45
- [pgVersion: string]: string;
46
- };
47
- gauges?: string[];
48
- statement_timeout_seconds?: number;
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
- interface MetricsYmlRoot {
52
- metrics: {
53
- [metricName: string]: MetricDefinition;
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
- let cachedMetrics: MetricsYmlRoot | null = null;
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
- * Load and parse metrics.yml (cached after first load)
61
- */
62
- export function loadMetricsYml(): MetricsYmlRoot {
63
- if (cachedMetrics) {
64
- return cachedMetrics;
65
- }
66
-
67
- const metricsPath = getMetricsYmlPath();
68
- const content = readFileSync(metricsPath, "utf8");
69
- cachedMetrics = yaml.load(content) as MetricsYmlRoot;
70
- return cachedMetrics;
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 and PostgreSQL version.
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 in metrics.yml (e.g., "pg_invalid_indexes")
78
- * @param pgMajorVersion - PostgreSQL major version (e.g., 16)
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, pgMajorVersion: number = 16): string {
82
- const root = loadMetricsYml();
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 (!metric.sqls) {
90
- throw new Error(`Metric "${metricName}" has no SQL queries defined`);
455
+ if (!sql) {
456
+ throw new Error(`Metric "${metricName}" not found. Available metrics: ${Object.keys(EMBEDDED_SQL).join(", ")}`);
91
457
  }
92
458
 
93
- // Try exact version first, then fall back to lower versions
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 in metrics.yml that correspond to express report checks.
116
- * These map check IDs to metric names in config/pgwatch-prometheus/metrics.yml.
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.yml query output to JSON report format.
137
- * Metrics.yml uses `tag_` prefix for dimensions; we strip it for JSON reports.
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.45",
3
+ "version": "0.14.0-dev.48",
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'))\" && cp -r sql dist/",
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",
@@ -99,6 +99,25 @@ function createMockClient(versionRows: any[], settingsRows: any[], options: Mock
99
99
  if (sql.includes("redundant_indexes") && sql.includes("columns like")) {
100
100
  return { rows: redundantIndexesRows };
101
101
  }
102
+ // D004: pg_stat_statements extension check
103
+ if (sql.includes("pg_extension") && sql.includes("pg_stat_statements")) {
104
+ return { rows: [] }; // Extension not installed by default
105
+ }
106
+ // D004: pg_stat_kcache extension check
107
+ if (sql.includes("pg_extension") && sql.includes("pg_stat_kcache")) {
108
+ return { rows: [] }; // Extension not installed by default
109
+ }
110
+ // G001: Memory settings query
111
+ if (sql.includes("pg_size_bytes") && sql.includes("shared_buffers") && sql.includes("work_mem")) {
112
+ return { rows: [{
113
+ shared_buffers_bytes: "134217728",
114
+ wal_buffers_bytes: "4194304",
115
+ work_mem_bytes: "4194304",
116
+ maintenance_work_mem_bytes: "67108864",
117
+ effective_cache_size_bytes: "4294967296",
118
+ max_connections: 100,
119
+ }] };
120
+ }
102
121
  throw new Error(`Unexpected query: ${sql}`);
103
122
  },
104
123
  };
@@ -208,6 +227,21 @@ describe("CHECK_INFO", () => {
208
227
  expect("H004" in checkup.CHECK_INFO).toBe(true);
209
228
  expect(checkup.CHECK_INFO.H004).toBe("Redundant indexes");
210
229
  });
230
+
231
+ test("contains D004", () => {
232
+ expect("D004" in checkup.CHECK_INFO).toBe(true);
233
+ expect(checkup.CHECK_INFO.D004).toBe("pg_stat_statements and pg_stat_kcache settings");
234
+ });
235
+
236
+ test("contains F001", () => {
237
+ expect("F001" in checkup.CHECK_INFO).toBe(true);
238
+ expect(checkup.CHECK_INFO.F001).toBe("Autovacuum: current settings");
239
+ });
240
+
241
+ test("contains G001", () => {
242
+ expect("G001" in checkup.CHECK_INFO).toBe(true);
243
+ expect(checkup.CHECK_INFO.G001).toBe("Memory-related settings");
244
+ });
211
245
  });
212
246
 
213
247
  // Tests for REPORT_GENERATORS
@@ -252,6 +286,21 @@ describe("REPORT_GENERATORS", () => {
252
286
  expect(typeof checkup.REPORT_GENERATORS.H004).toBe("function");
253
287
  });
254
288
 
289
+ test("has generator for D004", () => {
290
+ expect("D004" in checkup.REPORT_GENERATORS).toBe(true);
291
+ expect(typeof checkup.REPORT_GENERATORS.D004).toBe("function");
292
+ });
293
+
294
+ test("has generator for F001", () => {
295
+ expect("F001" in checkup.REPORT_GENERATORS).toBe(true);
296
+ expect(typeof checkup.REPORT_GENERATORS.F001).toBe("function");
297
+ });
298
+
299
+ test("has generator for G001", () => {
300
+ expect("G001" in checkup.REPORT_GENERATORS).toBe(true);
301
+ expect(typeof checkup.REPORT_GENERATORS.G001).toBe("function");
302
+ });
303
+
255
304
  test("REPORT_GENERATORS and CHECK_INFO have same keys", () => {
256
305
  const generatorKeys = Object.keys(checkup.REPORT_GENERATORS).sort();
257
306
  const infoKeys = Object.keys(checkup.CHECK_INFO).sort();