postgresai 0.14.0-dev.44 → 0.14.0-dev.46

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.
@@ -13064,7 +13064,7 @@ var {
13064
13064
  // package.json
13065
13065
  var package_default = {
13066
13066
  name: "postgresai",
13067
- version: "0.14.0-dev.44",
13067
+ version: "0.14.0-dev.46",
13068
13068
  description: "postgres_ai CLI",
13069
13069
  license: "Apache-2.0",
13070
13070
  private: false,
@@ -15881,7 +15881,7 @@ var Result = import_lib.default.Result;
15881
15881
  var TypeOverrides = import_lib.default.TypeOverrides;
15882
15882
  var defaults = import_lib.default.defaults;
15883
15883
  // package.json
15884
- var version = "0.14.0-dev.44";
15884
+ var version = "0.14.0-dev.46";
15885
15885
  var package_default2 = {
15886
15886
  name: "postgresai",
15887
15887
  version,
@@ -26956,7 +26956,7 @@ function parseVersionNum(versionNum) {
26956
26956
  function formatBytes(bytes) {
26957
26957
  if (bytes === 0)
26958
26958
  return "0 B";
26959
- const units = ["B", "kB", "MB", "GB", "TB", "PB"];
26959
+ const units = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
26960
26960
  const i3 = Math.floor(Math.log(bytes) / Math.log(1024));
26961
26961
  return `${(bytes / Math.pow(1024, i3)).toFixed(2)} ${units[i3]}`;
26962
26962
  }
@@ -27375,12 +27375,216 @@ async function generateH004(client, nodeName = "node-01") {
27375
27375
  };
27376
27376
  return report;
27377
27377
  }
27378
+ async function generateD004(client, nodeName) {
27379
+ const report = createBaseReport("D004", "pg_stat_statements and pg_stat_kcache settings", nodeName);
27380
+ const postgresVersion = await getPostgresVersion(client);
27381
+ const allSettings = await getSettings(client);
27382
+ const pgssSettings = {};
27383
+ for (const [name, setting] of Object.entries(allSettings)) {
27384
+ if (name.startsWith("pg_stat_statements") || name.startsWith("pg_stat_kcache")) {
27385
+ pgssSettings[name] = setting;
27386
+ }
27387
+ }
27388
+ let pgssAvailable = false;
27389
+ let pgssMetricsCount = 0;
27390
+ let pgssTotalCalls = 0;
27391
+ const pgssSampleQueries = [];
27392
+ try {
27393
+ const extCheck = await client.query("select 1 from pg_extension where extname = 'pg_stat_statements'");
27394
+ if (extCheck.rows.length > 0) {
27395
+ pgssAvailable = true;
27396
+ const statsResult = await client.query(`
27397
+ select count(*) as cnt, coalesce(sum(calls), 0) as total_calls
27398
+ from pg_stat_statements
27399
+ `);
27400
+ pgssMetricsCount = parseInt(statsResult.rows[0]?.cnt || "0", 10);
27401
+ pgssTotalCalls = parseInt(statsResult.rows[0]?.total_calls || "0", 10);
27402
+ const sampleResult = await client.query(`
27403
+ select
27404
+ queryid::text as queryid,
27405
+ coalesce(usename, 'unknown') as "user",
27406
+ coalesce(datname, 'unknown') as database,
27407
+ calls
27408
+ from pg_stat_statements s
27409
+ left join pg_database d on s.dbid = d.oid
27410
+ left join pg_user u on s.userid = u.usesysid
27411
+ order by calls desc
27412
+ limit 5
27413
+ `);
27414
+ for (const row of sampleResult.rows) {
27415
+ pgssSampleQueries.push({
27416
+ queryid: row.queryid,
27417
+ user: row.user,
27418
+ database: row.database,
27419
+ calls: parseInt(row.calls, 10)
27420
+ });
27421
+ }
27422
+ }
27423
+ } catch {}
27424
+ let kcacheAvailable = false;
27425
+ let kcacheMetricsCount = 0;
27426
+ let kcacheTotalExecTime = 0;
27427
+ let kcacheTotalUserTime = 0;
27428
+ let kcacheTotalSystemTime = 0;
27429
+ const kcacheSampleQueries = [];
27430
+ try {
27431
+ const extCheck = await client.query("select 1 from pg_extension where extname = 'pg_stat_kcache'");
27432
+ if (extCheck.rows.length > 0) {
27433
+ kcacheAvailable = true;
27434
+ const statsResult = await client.query(`
27435
+ select
27436
+ count(*) as cnt,
27437
+ coalesce(sum(exec_user_time + exec_system_time), 0) as total_exec_time,
27438
+ coalesce(sum(exec_user_time), 0) as total_user_time,
27439
+ coalesce(sum(exec_system_time), 0) as total_system_time
27440
+ from pg_stat_kcache
27441
+ `);
27442
+ kcacheMetricsCount = parseInt(statsResult.rows[0]?.cnt || "0", 10);
27443
+ kcacheTotalExecTime = parseFloat(statsResult.rows[0]?.total_exec_time || "0");
27444
+ kcacheTotalUserTime = parseFloat(statsResult.rows[0]?.total_user_time || "0");
27445
+ kcacheTotalSystemTime = parseFloat(statsResult.rows[0]?.total_system_time || "0");
27446
+ const sampleResult = await client.query(`
27447
+ select
27448
+ queryid::text as queryid,
27449
+ coalesce(usename, 'unknown') as "user",
27450
+ (exec_user_time + exec_system_time) as exec_total_time
27451
+ from pg_stat_kcache k
27452
+ left join pg_user u on k.userid = u.usesysid
27453
+ order by (exec_user_time + exec_system_time) desc
27454
+ limit 5
27455
+ `);
27456
+ for (const row of sampleResult.rows) {
27457
+ kcacheSampleQueries.push({
27458
+ queryid: row.queryid,
27459
+ user: row.user,
27460
+ exec_total_time: parseFloat(row.exec_total_time)
27461
+ });
27462
+ }
27463
+ }
27464
+ } catch {}
27465
+ report.results[nodeName] = {
27466
+ data: {
27467
+ settings: pgssSettings,
27468
+ pg_stat_statements_status: {
27469
+ extension_available: pgssAvailable,
27470
+ metrics_count: pgssMetricsCount,
27471
+ total_calls: pgssTotalCalls,
27472
+ sample_queries: pgssSampleQueries
27473
+ },
27474
+ pg_stat_kcache_status: {
27475
+ extension_available: kcacheAvailable,
27476
+ metrics_count: kcacheMetricsCount,
27477
+ total_exec_time: kcacheTotalExecTime,
27478
+ total_user_time: kcacheTotalUserTime,
27479
+ total_system_time: kcacheTotalSystemTime,
27480
+ sample_queries: kcacheSampleQueries
27481
+ }
27482
+ },
27483
+ postgres_version: postgresVersion
27484
+ };
27485
+ return report;
27486
+ }
27487
+ async function generateF001(client, nodeName) {
27488
+ const report = createBaseReport("F001", "Autovacuum: current settings", nodeName);
27489
+ const postgresVersion = await getPostgresVersion(client);
27490
+ const allSettings = await getSettings(client);
27491
+ const autovacuumSettings = {};
27492
+ for (const [name, setting] of Object.entries(allSettings)) {
27493
+ if (name.includes("autovacuum") || name.includes("vacuum")) {
27494
+ autovacuumSettings[name] = setting;
27495
+ }
27496
+ }
27497
+ report.results[nodeName] = {
27498
+ data: autovacuumSettings,
27499
+ postgres_version: postgresVersion
27500
+ };
27501
+ return report;
27502
+ }
27503
+ async function generateG001(client, nodeName) {
27504
+ const report = createBaseReport("G001", "Memory-related settings", nodeName);
27505
+ const postgresVersion = await getPostgresVersion(client);
27506
+ const allSettings = await getSettings(client);
27507
+ const memorySettingNames = [
27508
+ "shared_buffers",
27509
+ "work_mem",
27510
+ "maintenance_work_mem",
27511
+ "effective_cache_size",
27512
+ "wal_buffers",
27513
+ "temp_buffers",
27514
+ "max_connections",
27515
+ "autovacuum_work_mem",
27516
+ "hash_mem_multiplier",
27517
+ "logical_decoding_work_mem",
27518
+ "max_stack_depth",
27519
+ "max_prepared_transactions",
27520
+ "max_locks_per_transaction",
27521
+ "max_pred_locks_per_transaction"
27522
+ ];
27523
+ const memorySettings = {};
27524
+ for (const name of memorySettingNames) {
27525
+ if (allSettings[name]) {
27526
+ memorySettings[name] = allSettings[name];
27527
+ }
27528
+ }
27529
+ let memoryUsage = {};
27530
+ try {
27531
+ const memQuery = await client.query(`
27532
+ select
27533
+ pg_size_bytes(current_setting('shared_buffers')) as shared_buffers_bytes,
27534
+ pg_size_bytes(current_setting('wal_buffers')) as wal_buffers_bytes,
27535
+ pg_size_bytes(current_setting('work_mem')) as work_mem_bytes,
27536
+ pg_size_bytes(current_setting('maintenance_work_mem')) as maintenance_work_mem_bytes,
27537
+ pg_size_bytes(current_setting('effective_cache_size')) as effective_cache_size_bytes,
27538
+ current_setting('max_connections')::int as max_connections
27539
+ `);
27540
+ if (memQuery.rows.length > 0) {
27541
+ const row = memQuery.rows[0];
27542
+ const sharedBuffersBytes = parseInt(row.shared_buffers_bytes, 10);
27543
+ const walBuffersBytes = parseInt(row.wal_buffers_bytes, 10);
27544
+ const workMemBytes = parseInt(row.work_mem_bytes, 10);
27545
+ const maintenanceWorkMemBytes = parseInt(row.maintenance_work_mem_bytes, 10);
27546
+ const effectiveCacheSizeBytes = parseInt(row.effective_cache_size_bytes, 10);
27547
+ const maxConnections = row.max_connections;
27548
+ const sharedMemoryTotal = sharedBuffersBytes + walBuffersBytes;
27549
+ const maxWorkMemUsage = workMemBytes * maxConnections;
27550
+ memoryUsage = {
27551
+ shared_buffers_bytes: sharedBuffersBytes,
27552
+ shared_buffers_pretty: formatBytes(sharedBuffersBytes),
27553
+ wal_buffers_bytes: walBuffersBytes,
27554
+ wal_buffers_pretty: formatBytes(walBuffersBytes),
27555
+ shared_memory_total_bytes: sharedMemoryTotal,
27556
+ shared_memory_total_pretty: formatBytes(sharedMemoryTotal),
27557
+ work_mem_per_connection_bytes: workMemBytes,
27558
+ work_mem_per_connection_pretty: formatBytes(workMemBytes),
27559
+ max_work_mem_usage_bytes: maxWorkMemUsage,
27560
+ max_work_mem_usage_pretty: formatBytes(maxWorkMemUsage),
27561
+ maintenance_work_mem_bytes: maintenanceWorkMemBytes,
27562
+ maintenance_work_mem_pretty: formatBytes(maintenanceWorkMemBytes),
27563
+ effective_cache_size_bytes: effectiveCacheSizeBytes,
27564
+ effective_cache_size_pretty: formatBytes(effectiveCacheSizeBytes)
27565
+ };
27566
+ }
27567
+ } catch {}
27568
+ report.results[nodeName] = {
27569
+ data: {
27570
+ settings: memorySettings,
27571
+ analysis: {
27572
+ estimated_total_memory_usage: memoryUsage
27573
+ }
27574
+ },
27575
+ postgres_version: postgresVersion
27576
+ };
27577
+ return report;
27578
+ }
27378
27579
  var REPORT_GENERATORS = {
27379
27580
  A002: generateA002,
27380
27581
  A003: generateA003,
27381
27582
  A004: generateA004,
27382
27583
  A007: generateA007,
27383
27584
  A013: generateA013,
27585
+ D004: generateD004,
27586
+ F001: generateF001,
27587
+ G001: generateG001,
27384
27588
  H001: generateH001,
27385
27589
  H002: generateH002,
27386
27590
  H004: generateH004
@@ -27391,6 +27595,9 @@ var CHECK_INFO = {
27391
27595
  A004: "Cluster information",
27392
27596
  A007: "Altered settings",
27393
27597
  A013: "Postgres minor version",
27598
+ D004: "pg_stat_statements and pg_stat_kcache settings",
27599
+ F001: "Autovacuum: current settings",
27600
+ G001: "Memory-related settings",
27394
27601
  H001: "Invalid indexes",
27395
27602
  H002: "Unused indexes",
27396
27603
  H004: "Redundant indexes"
package/lib/checkup.ts CHANGED
@@ -186,11 +186,15 @@ export function parseVersionNum(versionNum: string): { major: string; minor: str
186
186
  }
187
187
 
188
188
  /**
189
- * Format bytes to human readable string
189
+ * Format bytes to human readable string using binary units (1024-based).
190
+ * Uses IEC standard: KiB, MiB, GiB, etc.
191
+ *
192
+ * Note: PostgreSQL's pg_size_pretty() uses kB/MB/GB with 1024 base (technically
193
+ * incorrect SI usage), but we follow IEC binary units per project style guide.
190
194
  */
191
195
  export function formatBytes(bytes: number): string {
192
196
  if (bytes === 0) return "0 B";
193
- const units = ["B", "kB", "MB", "GB", "TB", "PB"];
197
+ const units = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
194
198
  const i = Math.floor(Math.log(bytes) / Math.log(1024));
195
199
  return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`;
196
200
  }
@@ -776,6 +780,276 @@ export async function generateH004(client: Client, nodeName: string = "node-01")
776
780
  return report;
777
781
  }
778
782
 
783
+ /**
784
+ * Generate D004 report - pg_stat_statements and pg_stat_kcache settings
785
+ */
786
+ async function generateD004(client: Client, nodeName: string): Promise<Report> {
787
+ const report = createBaseReport("D004", "pg_stat_statements and pg_stat_kcache settings", nodeName);
788
+ const postgresVersion = await getPostgresVersion(client);
789
+ const allSettings = await getSettings(client);
790
+
791
+ // Filter settings related to pg_stat_statements and pg_stat_kcache
792
+ const pgssSettings: Record<string, SettingInfo> = {};
793
+ for (const [name, setting] of Object.entries(allSettings)) {
794
+ if (name.startsWith("pg_stat_statements") || name.startsWith("pg_stat_kcache")) {
795
+ pgssSettings[name] = setting;
796
+ }
797
+ }
798
+
799
+ // Check pg_stat_statements extension
800
+ let pgssAvailable = false;
801
+ let pgssMetricsCount = 0;
802
+ let pgssTotalCalls = 0;
803
+ const pgssSampleQueries: Array<{ queryid: string; user: string; database: string; calls: number }> = [];
804
+
805
+ try {
806
+ const extCheck = await client.query(
807
+ "select 1 from pg_extension where extname = 'pg_stat_statements'"
808
+ );
809
+ if (extCheck.rows.length > 0) {
810
+ pgssAvailable = true;
811
+ const statsResult = await client.query(`
812
+ select count(*) as cnt, coalesce(sum(calls), 0) as total_calls
813
+ from pg_stat_statements
814
+ `);
815
+ pgssMetricsCount = parseInt(statsResult.rows[0]?.cnt || "0", 10);
816
+ pgssTotalCalls = parseInt(statsResult.rows[0]?.total_calls || "0", 10);
817
+
818
+ // Get sample queries (top 5 by calls)
819
+ const sampleResult = await client.query(`
820
+ select
821
+ queryid::text as queryid,
822
+ coalesce(usename, 'unknown') as "user",
823
+ coalesce(datname, 'unknown') as database,
824
+ calls
825
+ from pg_stat_statements s
826
+ left join pg_database d on s.dbid = d.oid
827
+ left join pg_user u on s.userid = u.usesysid
828
+ order by calls desc
829
+ limit 5
830
+ `);
831
+ for (const row of sampleResult.rows) {
832
+ pgssSampleQueries.push({
833
+ queryid: row.queryid,
834
+ user: row.user,
835
+ database: row.database,
836
+ calls: parseInt(row.calls, 10),
837
+ });
838
+ }
839
+ }
840
+ } catch {
841
+ // Extension not available or accessible
842
+ }
843
+
844
+ // Check pg_stat_kcache extension
845
+ let kcacheAvailable = false;
846
+ let kcacheMetricsCount = 0;
847
+ let kcacheTotalExecTime = 0;
848
+ let kcacheTotalUserTime = 0;
849
+ let kcacheTotalSystemTime = 0;
850
+ const kcacheSampleQueries: Array<{ queryid: string; user: string; exec_total_time: number }> = [];
851
+
852
+ try {
853
+ const extCheck = await client.query(
854
+ "select 1 from pg_extension where extname = 'pg_stat_kcache'"
855
+ );
856
+ if (extCheck.rows.length > 0) {
857
+ kcacheAvailable = true;
858
+ const statsResult = await client.query(`
859
+ select
860
+ count(*) as cnt,
861
+ coalesce(sum(exec_user_time + exec_system_time), 0) as total_exec_time,
862
+ coalesce(sum(exec_user_time), 0) as total_user_time,
863
+ coalesce(sum(exec_system_time), 0) as total_system_time
864
+ from pg_stat_kcache
865
+ `);
866
+ kcacheMetricsCount = parseInt(statsResult.rows[0]?.cnt || "0", 10);
867
+ kcacheTotalExecTime = parseFloat(statsResult.rows[0]?.total_exec_time || "0");
868
+ kcacheTotalUserTime = parseFloat(statsResult.rows[0]?.total_user_time || "0");
869
+ kcacheTotalSystemTime = parseFloat(statsResult.rows[0]?.total_system_time || "0");
870
+
871
+ // Get sample queries (top 5 by exec time)
872
+ const sampleResult = await client.query(`
873
+ select
874
+ queryid::text as queryid,
875
+ coalesce(usename, 'unknown') as "user",
876
+ (exec_user_time + exec_system_time) as exec_total_time
877
+ from pg_stat_kcache k
878
+ left join pg_user u on k.userid = u.usesysid
879
+ order by (exec_user_time + exec_system_time) desc
880
+ limit 5
881
+ `);
882
+ for (const row of sampleResult.rows) {
883
+ kcacheSampleQueries.push({
884
+ queryid: row.queryid,
885
+ user: row.user,
886
+ exec_total_time: parseFloat(row.exec_total_time),
887
+ });
888
+ }
889
+ }
890
+ } catch {
891
+ // Extension not available or accessible
892
+ }
893
+
894
+ report.results[nodeName] = {
895
+ data: {
896
+ settings: pgssSettings,
897
+ pg_stat_statements_status: {
898
+ extension_available: pgssAvailable,
899
+ metrics_count: pgssMetricsCount,
900
+ total_calls: pgssTotalCalls,
901
+ sample_queries: pgssSampleQueries,
902
+ },
903
+ pg_stat_kcache_status: {
904
+ extension_available: kcacheAvailable,
905
+ metrics_count: kcacheMetricsCount,
906
+ total_exec_time: kcacheTotalExecTime,
907
+ total_user_time: kcacheTotalUserTime,
908
+ total_system_time: kcacheTotalSystemTime,
909
+ sample_queries: kcacheSampleQueries,
910
+ },
911
+ },
912
+ postgres_version: postgresVersion,
913
+ };
914
+
915
+ return report;
916
+ }
917
+
918
+ /**
919
+ * Generate F001 report - Autovacuum: current settings
920
+ */
921
+ async function generateF001(client: Client, nodeName: string): Promise<Report> {
922
+ const report = createBaseReport("F001", "Autovacuum: current settings", nodeName);
923
+ const postgresVersion = await getPostgresVersion(client);
924
+ const allSettings = await getSettings(client);
925
+
926
+ // Filter autovacuum-related settings
927
+ const autovacuumSettings: Record<string, SettingInfo> = {};
928
+ for (const [name, setting] of Object.entries(allSettings)) {
929
+ if (name.includes("autovacuum") || name.includes("vacuum")) {
930
+ autovacuumSettings[name] = setting;
931
+ }
932
+ }
933
+
934
+ report.results[nodeName] = {
935
+ data: autovacuumSettings,
936
+ postgres_version: postgresVersion,
937
+ };
938
+
939
+ return report;
940
+ }
941
+
942
+ /**
943
+ * Generate G001 report - Memory-related settings
944
+ */
945
+ async function generateG001(client: Client, nodeName: string): Promise<Report> {
946
+ const report = createBaseReport("G001", "Memory-related settings", nodeName);
947
+ const postgresVersion = await getPostgresVersion(client);
948
+ const allSettings = await getSettings(client);
949
+
950
+ // Memory-related setting names
951
+ const memorySettingNames = [
952
+ "shared_buffers",
953
+ "work_mem",
954
+ "maintenance_work_mem",
955
+ "effective_cache_size",
956
+ "wal_buffers",
957
+ "temp_buffers",
958
+ "max_connections",
959
+ "autovacuum_work_mem",
960
+ "hash_mem_multiplier",
961
+ "logical_decoding_work_mem",
962
+ "max_stack_depth",
963
+ "max_prepared_transactions",
964
+ "max_locks_per_transaction",
965
+ "max_pred_locks_per_transaction",
966
+ ];
967
+
968
+ const memorySettings: Record<string, SettingInfo> = {};
969
+ for (const name of memorySettingNames) {
970
+ if (allSettings[name]) {
971
+ memorySettings[name] = allSettings[name];
972
+ }
973
+ }
974
+
975
+ // Calculate memory usage estimates
976
+ interface MemoryUsage {
977
+ shared_buffers_bytes: number;
978
+ shared_buffers_pretty: string;
979
+ wal_buffers_bytes: number;
980
+ wal_buffers_pretty: string;
981
+ shared_memory_total_bytes: number;
982
+ shared_memory_total_pretty: string;
983
+ work_mem_per_connection_bytes: number;
984
+ work_mem_per_connection_pretty: string;
985
+ max_work_mem_usage_bytes: number;
986
+ max_work_mem_usage_pretty: string;
987
+ maintenance_work_mem_bytes: number;
988
+ maintenance_work_mem_pretty: string;
989
+ effective_cache_size_bytes: number;
990
+ effective_cache_size_pretty: string;
991
+ }
992
+
993
+ let memoryUsage: MemoryUsage | Record<string, never> = {};
994
+
995
+ try {
996
+ // Get actual byte values from PostgreSQL
997
+ const memQuery = await client.query(`
998
+ select
999
+ pg_size_bytes(current_setting('shared_buffers')) as shared_buffers_bytes,
1000
+ pg_size_bytes(current_setting('wal_buffers')) as wal_buffers_bytes,
1001
+ pg_size_bytes(current_setting('work_mem')) as work_mem_bytes,
1002
+ pg_size_bytes(current_setting('maintenance_work_mem')) as maintenance_work_mem_bytes,
1003
+ pg_size_bytes(current_setting('effective_cache_size')) as effective_cache_size_bytes,
1004
+ current_setting('max_connections')::int as max_connections
1005
+ `);
1006
+
1007
+ if (memQuery.rows.length > 0) {
1008
+ const row = memQuery.rows[0];
1009
+ const sharedBuffersBytes = parseInt(row.shared_buffers_bytes, 10);
1010
+ const walBuffersBytes = parseInt(row.wal_buffers_bytes, 10);
1011
+ const workMemBytes = parseInt(row.work_mem_bytes, 10);
1012
+ const maintenanceWorkMemBytes = parseInt(row.maintenance_work_mem_bytes, 10);
1013
+ const effectiveCacheSizeBytes = parseInt(row.effective_cache_size_bytes, 10);
1014
+ const maxConnections = row.max_connections;
1015
+
1016
+ const sharedMemoryTotal = sharedBuffersBytes + walBuffersBytes;
1017
+ const maxWorkMemUsage = workMemBytes * maxConnections;
1018
+
1019
+ memoryUsage = {
1020
+ shared_buffers_bytes: sharedBuffersBytes,
1021
+ shared_buffers_pretty: formatBytes(sharedBuffersBytes),
1022
+ wal_buffers_bytes: walBuffersBytes,
1023
+ wal_buffers_pretty: formatBytes(walBuffersBytes),
1024
+ shared_memory_total_bytes: sharedMemoryTotal,
1025
+ shared_memory_total_pretty: formatBytes(sharedMemoryTotal),
1026
+ work_mem_per_connection_bytes: workMemBytes,
1027
+ work_mem_per_connection_pretty: formatBytes(workMemBytes),
1028
+ max_work_mem_usage_bytes: maxWorkMemUsage,
1029
+ max_work_mem_usage_pretty: formatBytes(maxWorkMemUsage),
1030
+ maintenance_work_mem_bytes: maintenanceWorkMemBytes,
1031
+ maintenance_work_mem_pretty: formatBytes(maintenanceWorkMemBytes),
1032
+ effective_cache_size_bytes: effectiveCacheSizeBytes,
1033
+ effective_cache_size_pretty: formatBytes(effectiveCacheSizeBytes),
1034
+ };
1035
+ }
1036
+ } catch {
1037
+ // If we can't calculate, leave empty object (schema allows this)
1038
+ }
1039
+
1040
+ report.results[nodeName] = {
1041
+ data: {
1042
+ settings: memorySettings,
1043
+ analysis: {
1044
+ estimated_total_memory_usage: memoryUsage,
1045
+ },
1046
+ },
1047
+ postgres_version: postgresVersion,
1048
+ };
1049
+
1050
+ return report;
1051
+ }
1052
+
779
1053
  /**
780
1054
  * Available report generators
781
1055
  */
@@ -785,6 +1059,9 @@ export const REPORT_GENERATORS: Record<string, (client: Client, nodeName: string
785
1059
  A004: generateA004,
786
1060
  A007: generateA007,
787
1061
  A013: generateA013,
1062
+ D004: generateD004,
1063
+ F001: generateF001,
1064
+ G001: generateG001,
788
1065
  H001: generateH001,
789
1066
  H002: generateH002,
790
1067
  H004: generateH004,
@@ -799,6 +1076,9 @@ export const CHECK_INFO: Record<string, string> = {
799
1076
  A004: "Cluster information",
800
1077
  A007: "Altered settings",
801
1078
  A013: "Postgres minor version",
1079
+ D004: "pg_stat_statements and pg_stat_kcache settings",
1080
+ F001: "Autovacuum: current settings",
1081
+ G001: "Memory-related settings",
802
1082
  H001: "Invalid indexes",
803
1083
  H002: "Unused indexes",
804
1084
  H004: "Redundant indexes",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresai",
3
- "version": "0.14.0-dev.44",
3
+ "version": "0.14.0-dev.46",
4
4
  "description": "postgres_ai CLI",
5
5
  "license": "Apache-2.0",
6
6
  "private": false,
@@ -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();
@@ -269,17 +318,17 @@ describe("formatBytes", () => {
269
318
  expect(checkup.formatBytes(500)).toBe("500.00 B");
270
319
  });
271
320
 
272
- test("formats kilobytes", () => {
273
- expect(checkup.formatBytes(1024)).toBe("1.00 kB");
274
- expect(checkup.formatBytes(1536)).toBe("1.50 kB");
321
+ test("formats kibibytes", () => {
322
+ expect(checkup.formatBytes(1024)).toBe("1.00 KiB");
323
+ expect(checkup.formatBytes(1536)).toBe("1.50 KiB");
275
324
  });
276
325
 
277
- test("formats megabytes", () => {
278
- expect(checkup.formatBytes(1048576)).toBe("1.00 MB");
326
+ test("formats mebibytes", () => {
327
+ expect(checkup.formatBytes(1048576)).toBe("1.00 MiB");
279
328
  });
280
329
 
281
- test("formats gigabytes", () => {
282
- expect(checkup.formatBytes(1073741824)).toBe("1.00 GB");
330
+ test("formats gibibytes", () => {
331
+ expect(checkup.formatBytes(1073741824)).toBe("1.00 GiB");
283
332
  });
284
333
  });
285
334
 
@@ -213,7 +213,7 @@ describe.skipIf(skipTests)("integration: prepare-db", () => {
213
213
  } finally {
214
214
  await pg.cleanup();
215
215
  }
216
- });
216
+ }, { timeout: 30000 });
217
217
 
218
218
  test("requires explicit monitoring password in non-interactive mode", async () => {
219
219
  pg = await createTempPostgres();
@@ -237,7 +237,7 @@ describe.skipIf(skipTests)("integration: prepare-db", () => {
237
237
  } finally {
238
238
  await pg.cleanup();
239
239
  }
240
- });
240
+ }, { timeout: 30000 });
241
241
 
242
242
  test("fixes slightly-off permissions idempotently", async () => {
243
243
  pg = await createTempPostgres();
@@ -291,7 +291,7 @@ describe.skipIf(skipTests)("integration: prepare-db", () => {
291
291
  } finally {
292
292
  await pg.cleanup();
293
293
  }
294
- });
294
+ }, { timeout: 30000 });
295
295
 
296
296
  test("reports nicely when lacking permissions", async () => {
297
297
  pg = await createTempPostgres();
@@ -324,7 +324,7 @@ describe.skipIf(skipTests)("integration: prepare-db", () => {
324
324
  } finally {
325
325
  await pg.cleanup();
326
326
  }
327
- });
327
+ }, { timeout: 30000 });
328
328
 
329
329
  test("--verify returns 0 when ok and non-zero when missing", async () => {
330
330
  pg = await createTempPostgres();
@@ -360,7 +360,7 @@ describe.skipIf(skipTests)("integration: prepare-db", () => {
360
360
  } finally {
361
361
  await pg.cleanup();
362
362
  }
363
- });
363
+ }, { timeout: 30000 });
364
364
 
365
365
  test("--reset-password updates the monitoring role login password", async () => {
366
366
  pg = await createTempPostgres();
@@ -392,5 +392,5 @@ describe.skipIf(skipTests)("integration: prepare-db", () => {
392
392
  } finally {
393
393
  await pg.cleanup();
394
394
  }
395
- });
395
+ }, { timeout: 30000 });
396
396
  });
@@ -28,6 +28,7 @@ function validateReport(report: any, checkId: string): { valid: boolean; errors:
28
28
  // Mock client for testing
29
29
  function createMockClient(options: {
30
30
  versionRows?: any[];
31
+ settingsRows?: any[];
31
32
  invalidIndexesRows?: any[];
32
33
  unusedIndexesRows?: any[];
33
34
  redundantIndexesRows?: any[];
@@ -37,6 +38,12 @@ function createMockClient(options: {
37
38
  { name: "server_version", setting: "16.3" },
38
39
  { name: "server_version_num", setting: "160003" },
39
40
  ],
41
+ settingsRows = [
42
+ { name: "shared_buffers", setting: "128MB", unit: "", category: "Resource Usage / Memory", context: "postmaster", vartype: "string", pretty_value: "128 MB" },
43
+ { name: "work_mem", setting: "4MB", unit: "", category: "Resource Usage / Memory", context: "user", vartype: "string", pretty_value: "4 MB" },
44
+ { name: "autovacuum", setting: "on", unit: "", category: "Autovacuum", context: "sighup", vartype: "bool", pretty_value: "on" },
45
+ { name: "pg_stat_statements.max", setting: "5000", unit: "", category: "Custom", context: "superuser", vartype: "integer", pretty_value: "5000" },
46
+ ],
40
47
  invalidIndexesRows = [],
41
48
  unusedIndexesRows = [],
42
49
  redundantIndexesRows = [],
@@ -47,6 +54,10 @@ function createMockClient(options: {
47
54
  if (sql.includes("server_version") && sql.includes("server_version_num") && !sql.includes("order by")) {
48
55
  return { rows: versionRows };
49
56
  }
57
+ // Full settings query
58
+ if (sql.includes("pg_settings") && sql.includes("order by") && sql.includes("is_default")) {
59
+ return { rows: settingsRows };
60
+ }
50
61
  if (sql.includes("current_database()") && sql.includes("pg_database_size")) {
51
62
  return { rows: [{ datname: "testdb", size_bytes: "1073741824" }] };
52
63
  }
@@ -68,6 +79,25 @@ function createMockClient(options: {
68
79
  if (sql.includes("redundant_indexes") && sql.includes("columns like")) {
69
80
  return { rows: redundantIndexesRows };
70
81
  }
82
+ // D004: pg_stat_statements extension check
83
+ if (sql.includes("pg_extension") && sql.includes("pg_stat_statements")) {
84
+ return { rows: [] }; // Extension not installed
85
+ }
86
+ // D004: pg_stat_kcache extension check
87
+ if (sql.includes("pg_extension") && sql.includes("pg_stat_kcache")) {
88
+ return { rows: [] }; // Extension not installed
89
+ }
90
+ // G001: Memory settings query
91
+ if (sql.includes("pg_size_bytes") && sql.includes("shared_buffers") && sql.includes("work_mem")) {
92
+ return { rows: [{
93
+ shared_buffers_bytes: "134217728",
94
+ wal_buffers_bytes: "4194304",
95
+ work_mem_bytes: "4194304",
96
+ maintenance_work_mem_bytes: "67108864",
97
+ effective_cache_size_bytes: "4294967296",
98
+ max_connections: 100,
99
+ }] };
100
+ }
71
101
  throw new Error(`Unexpected query: ${sql}`);
72
102
  },
73
103
  };
@@ -186,3 +216,42 @@ describe("H004 schema validation", () => {
186
216
  });
187
217
  });
188
218
 
219
+ describe("D004 schema validation", () => {
220
+ test("D004 report validates against schema (extensions not installed)", async () => {
221
+ const mockClient = createMockClient();
222
+ const report = await checkup.REPORT_GENERATORS.D004(mockClient as any, "node-01");
223
+
224
+ const result = validateReport(report, "D004");
225
+ if (!result.valid) {
226
+ console.error("D004 validation errors:", result.errors);
227
+ }
228
+ expect(result.valid).toBe(true);
229
+ });
230
+ });
231
+
232
+ describe("F001 schema validation", () => {
233
+ test("F001 report validates against schema", async () => {
234
+ const mockClient = createMockClient();
235
+ const report = await checkup.REPORT_GENERATORS.F001(mockClient as any, "node-01");
236
+
237
+ const result = validateReport(report, "F001");
238
+ if (!result.valid) {
239
+ console.error("F001 validation errors:", result.errors);
240
+ }
241
+ expect(result.valid).toBe(true);
242
+ });
243
+ });
244
+
245
+ describe("G001 schema validation", () => {
246
+ test("G001 report validates against schema", async () => {
247
+ const mockClient = createMockClient();
248
+ const report = await checkup.REPORT_GENERATORS.G001(mockClient as any, "node-01");
249
+
250
+ const result = validateReport(report, "G001");
251
+ if (!result.valid) {
252
+ console.error("G001 validation errors:", result.errors);
253
+ }
254
+ expect(result.valid).toBe(true);
255
+ });
256
+ });
257
+