@@ -4,6 +4,7 @@ import { resolve } from "path";
44// Import from source directly since we're using Bun
55import * as checkup from "../lib/checkup" ;
66import * as api from "../lib/checkup-api" ;
7+ import * as metricsLoader from "../lib/metrics-loader" ;
78import { createMockClient } from "./test-utils" ;
89
910
@@ -40,6 +41,24 @@ describe("parseVersionNum", () => {
4041 expect ( result . minor ) . toBe ( "12" ) ;
4142 } ) ;
4243
44+ test ( "parses PG 13.16 version number" , ( ) => {
45+ const result = checkup . parseVersionNum ( "130016" ) ;
46+ expect ( result . major ) . toBe ( "13" ) ;
47+ expect ( result . minor ) . toBe ( "16" ) ;
48+ } ) ;
49+
50+ test ( "parses PG 17.2 version number" , ( ) => {
51+ const result = checkup . parseVersionNum ( "170002" ) ;
52+ expect ( result . major ) . toBe ( "17" ) ;
53+ expect ( result . minor ) . toBe ( "2" ) ;
54+ } ) ;
55+
56+ test ( "parses PG 18.0 version number" , ( ) => {
57+ const result = checkup . parseVersionNum ( "180000" ) ;
58+ expect ( result . major ) . toBe ( "18" ) ;
59+ expect ( result . minor ) . toBe ( "0" ) ;
60+ } ) ;
61+
4362 test ( "handles empty string" , ( ) => {
4463 const result = checkup . parseVersionNum ( "" ) ;
4564 expect ( result . major ) . toBe ( "" ) ;
@@ -888,4 +907,292 @@ describe("checkup-api", () => {
888907 } ) ;
889908} ) ;
890909
910+ // PostgreSQL version compatibility tests (PG13-PG18)
911+ describe ( "PostgreSQL version compatibility (PG13-PG18)" , ( ) => {
912+ // Helper to create version-specific mock data
913+ const createVersionMockData = ( major : number , minor : number ) => ( {
914+ versionRows : [
915+ { name : "server_version" , setting : `${ major } .${ minor } ` } ,
916+ { name : "server_version_num" , setting : `${ major } ${ String ( minor ) . padStart ( 4 , "0" ) } ` } ,
917+ ] ,
918+ settingsRows : [
919+ {
920+ tag_setting_name : "shared_buffers" ,
921+ tag_setting_value : "16384" ,
922+ tag_unit : "8kB" ,
923+ tag_category : "Resource Usage / Memory" ,
924+ tag_vartype : "integer" ,
925+ is_default : 0 ,
926+ setting_normalized : "134217728" ,
927+ unit_normalized : "bytes" ,
928+ } ,
929+ ] ,
930+ databaseSizesRows : [ { datname : "postgres" , size_bytes : "1073741824" } ] ,
931+ dbStatsRows : [ {
932+ numbackends : 5 ,
933+ xact_commit : 100 ,
934+ xact_rollback : 1 ,
935+ blks_read : 100 ,
936+ blks_hit : 900 ,
937+ tup_returned : 500 ,
938+ tup_fetched : 400 ,
939+ tup_inserted : 50 ,
940+ tup_updated : 30 ,
941+ tup_deleted : 10 ,
942+ deadlocks : 0 ,
943+ temp_files : 0 ,
944+ temp_bytes : 0 ,
945+ postmaster_uptime_s : 864000 ,
946+ } ] ,
947+ connectionStatesRows : [ { state : "active" , count : 2 } , { state : "idle" , count : 3 } ] ,
948+ uptimeRows : [ { start_time : new Date ( "2024-01-01T00:00:00Z" ) , uptime : "10 days" } ] ,
949+ invalidIndexesRows : [ ] ,
950+ unusedIndexesRows : [ ] ,
951+ redundantIndexesRows : [ ] ,
952+ } ) ;
953+
954+ // Test matrix for all supported PostgreSQL versions
955+ const pgVersions = [
956+ { major : 13 , minor : 16 , versionNum : "130016" } ,
957+ { major : 14 , minor : 12 , versionNum : "140012" } ,
958+ { major : 15 , minor : 7 , versionNum : "150007" } ,
959+ { major : 16 , minor : 3 , versionNum : "160003" } ,
960+ { major : 17 , minor : 2 , versionNum : "170002" } ,
961+ { major : 18 , minor : 0 , versionNum : "180000" } ,
962+ ] ;
963+
964+ describe ( "getPostgresVersion extracts correct version for each PG version" , ( ) => {
965+ for ( const { major, minor, versionNum } of pgVersions ) {
966+ test ( `PG ${ major } .${ minor } ` , async ( ) => {
967+ const mockClient = createMockClient ( createVersionMockData ( major , minor ) ) ;
968+ const version = await checkup . getPostgresVersion ( mockClient as any ) ;
969+
970+ expect ( version . version ) . toBe ( `${ major } .${ minor } ` ) ;
971+ expect ( version . server_version_num ) . toBe ( versionNum ) ;
972+ expect ( version . server_major_ver ) . toBe ( String ( major ) ) ;
973+ expect ( version . server_minor_ver ) . toBe ( String ( minor ) ) ;
974+ } ) ;
975+ }
976+ } ) ;
977+
978+ describe ( "generateA002 (major version) works for each PG version" , ( ) => {
979+ for ( const { major, minor } of pgVersions ) {
980+ test ( `PG ${ major } .${ minor } ` , async ( ) => {
981+ const mockClient = createMockClient ( createVersionMockData ( major , minor ) ) ;
982+ const report = await checkup . generateA002 ( mockClient as any , "test-node" ) ;
983+
984+ expect ( report . checkId ) . toBe ( "A002" ) ;
985+ expect ( report . checkTitle ) . toBe ( "Postgres major version" ) ;
986+ expect ( report . results [ "test-node" ] . data . version . version ) . toBe ( `${ major } .${ minor } ` ) ;
987+ expect ( report . results [ "test-node" ] . data . version . server_major_ver ) . toBe ( String ( major ) ) ;
988+ expect ( report . results [ "test-node" ] . data . version . server_minor_ver ) . toBe ( String ( minor ) ) ;
989+ } ) ;
990+ }
991+ } ) ;
992+
993+ describe ( "generateA013 (minor version) works for each PG version" , ( ) => {
994+ for ( const { major, minor } of pgVersions ) {
995+ test ( `PG ${ major } .${ minor } ` , async ( ) => {
996+ const mockClient = createMockClient ( createVersionMockData ( major , minor ) ) ;
997+ const report = await checkup . generateA013 ( mockClient as any , "test-node" ) ;
998+
999+ expect ( report . checkId ) . toBe ( "A013" ) ;
1000+ expect ( report . checkTitle ) . toBe ( "Postgres minor version" ) ;
1001+ expect ( report . results [ "test-node" ] . data . version . server_minor_ver ) . toBe ( String ( minor ) ) ;
1002+ expect ( report . results [ "test-node" ] . data . version . version ) . toBe ( `${ major } .${ minor } ` ) ;
1003+ } ) ;
1004+ }
1005+ } ) ;
1006+
1007+ describe ( "generateA003 (settings) works for each PG version" , ( ) => {
1008+ for ( const { major, minor } of pgVersions ) {
1009+ test ( `PG ${ major } .${ minor } ` , async ( ) => {
1010+ const mockClient = createMockClient ( createVersionMockData ( major , minor ) ) ;
1011+ const report = await checkup . generateA003 ( mockClient as any , "test-node" ) ;
1012+
1013+ expect ( report . checkId ) . toBe ( "A003" ) ;
1014+ expect ( report . checkTitle ) . toBe ( "Postgres settings" ) ;
1015+ expect ( "shared_buffers" in report . results [ "test-node" ] . data ) . toBe ( true ) ;
1016+ expect ( report . results [ "test-node" ] . postgres_version ?. version ) . toBe ( `${ major } .${ minor } ` ) ;
1017+ expect ( report . results [ "test-node" ] . postgres_version ?. server_major_ver ) . toBe ( String ( major ) ) ;
1018+ } ) ;
1019+ }
1020+ } ) ;
1021+
1022+ describe ( "generateA007 (altered settings) works for each PG version" , ( ) => {
1023+ for ( const { major, minor } of pgVersions ) {
1024+ test ( `PG ${ major } .${ minor } ` , async ( ) => {
1025+ const mockClient = createMockClient ( createVersionMockData ( major , minor ) ) ;
1026+ const report = await checkup . generateA007 ( mockClient as any , "test-node" ) ;
1027+
1028+ expect ( report . checkId ) . toBe ( "A007" ) ;
1029+ expect ( report . checkTitle ) . toBe ( "Altered settings" ) ;
1030+ expect ( "shared_buffers" in report . results [ "test-node" ] . data ) . toBe ( true ) ;
1031+ expect ( report . results [ "test-node" ] . postgres_version ?. version ) . toBe ( `${ major } .${ minor } ` ) ;
1032+ } ) ;
1033+ }
1034+ } ) ;
1035+
1036+ describe ( "generateA004 (cluster info) works for each PG version" , ( ) => {
1037+ for ( const { major, minor } of pgVersions ) {
1038+ test ( `PG ${ major } .${ minor } ` , async ( ) => {
1039+ const mockClient = createMockClient ( createVersionMockData ( major , minor ) ) ;
1040+ const report = await checkup . generateA004 ( mockClient as any , "test-node" ) ;
1041+
1042+ expect ( report . checkId ) . toBe ( "A004" ) ;
1043+ expect ( report . checkTitle ) . toBe ( "Cluster information" ) ;
1044+ expect ( "general_info" in report . results [ "test-node" ] . data ) . toBe ( true ) ;
1045+ expect ( "database_sizes" in report . results [ "test-node" ] . data ) . toBe ( true ) ;
1046+ expect ( report . results [ "test-node" ] . postgres_version ?. version ) . toBe ( `${ major } .${ minor } ` ) ;
1047+ } ) ;
1048+ }
1049+ } ) ;
1050+
1051+ describe ( "generateH001 (invalid indexes) works for each PG version" , ( ) => {
1052+ for ( const { major, minor } of pgVersions ) {
1053+ test ( `PG ${ major } .${ minor } ` , async ( ) => {
1054+ const mockClient = createMockClient ( createVersionMockData ( major , minor ) ) ;
1055+ const report = await checkup . generateH001 ( mockClient as any , "test-node" ) ;
1056+
1057+ expect ( report . checkId ) . toBe ( "H001" ) ;
1058+ expect ( report . checkTitle ) . toBe ( "Invalid indexes" ) ;
1059+ expect ( "test-node" in report . results ) . toBe ( true ) ;
1060+ expect ( report . results [ "test-node" ] . postgres_version ?. version ) . toBe ( `${ major } .${ minor } ` ) ;
1061+ } ) ;
1062+ }
1063+ } ) ;
1064+
1065+ describe ( "generateH002 (unused indexes) works for each PG version" , ( ) => {
1066+ for ( const { major, minor } of pgVersions ) {
1067+ test ( `PG ${ major } .${ minor } ` , async ( ) => {
1068+ const mockClient = createMockClient ( createVersionMockData ( major , minor ) ) ;
1069+ const report = await checkup . generateH002 ( mockClient as any , "test-node" ) ;
1070+
1071+ expect ( report . checkId ) . toBe ( "H002" ) ;
1072+ expect ( report . checkTitle ) . toBe ( "Unused indexes" ) ;
1073+ expect ( "test-node" in report . results ) . toBe ( true ) ;
1074+ expect ( report . results [ "test-node" ] . postgres_version ?. version ) . toBe ( `${ major } .${ minor } ` ) ;
1075+ } ) ;
1076+ }
1077+ } ) ;
1078+
1079+ describe ( "generateH004 (redundant indexes) works for each PG version" , ( ) => {
1080+ for ( const { major, minor } of pgVersions ) {
1081+ test ( `PG ${ major } .${ minor } ` , async ( ) => {
1082+ const mockClient = createMockClient ( createVersionMockData ( major , minor ) ) ;
1083+ const report = await checkup . generateH004 ( mockClient as any , "test-node" ) ;
1084+
1085+ expect ( report . checkId ) . toBe ( "H004" ) ;
1086+ expect ( report . checkTitle ) . toBe ( "Redundant indexes" ) ;
1087+ expect ( "test-node" in report . results ) . toBe ( true ) ;
1088+ expect ( report . results [ "test-node" ] . postgres_version ?. version ) . toBe ( `${ major } .${ minor } ` ) ;
1089+ } ) ;
1090+ }
1091+ } ) ;
1092+
1093+ describe ( "generateAllReports works for each PG version" , ( ) => {
1094+ for ( const { major, minor } of pgVersions ) {
1095+ test ( `PG ${ major } .${ minor } ` , async ( ) => {
1096+ const mockClient = createMockClient ( createVersionMockData ( major , minor ) ) ;
1097+ const reports = await checkup . generateAllReports ( mockClient as any , "test-node" ) ;
1098+
1099+ // Verify all expected checks are generated
1100+ const expectedChecks = [ "A002" , "A003" , "A004" , "A007" , "A013" , "H001" , "H002" , "H004" ] ;
1101+ for ( const checkId of expectedChecks ) {
1102+ expect ( checkId in reports ) . toBe ( true ) ;
1103+ expect ( reports [ checkId ] . checkId ) . toBe ( checkId ) ;
1104+ }
1105+
1106+ // Verify postgres version is correctly set in A002 report (via data.version)
1107+ expect ( reports . A002 . results [ "test-node" ] . data . version . version ) . toBe ( `${ major } .${ minor } ` ) ;
1108+ expect ( reports . A002 . results [ "test-node" ] . data . version . server_major_ver ) . toBe ( String ( major ) ) ;
1109+
1110+ // Verify postgres_version is set in reports that include it (A003, A004, H001, etc.)
1111+ expect ( reports . A003 . results [ "test-node" ] . postgres_version ?. version ) . toBe ( `${ major } .${ minor } ` ) ;
1112+ expect ( reports . A004 . results [ "test-node" ] . postgres_version ?. version ) . toBe ( `${ major } .${ minor } ` ) ;
1113+ } ) ;
1114+ }
1115+ } ) ;
1116+ } ) ;
1117+
1118+ // Tests for version-aware SQL query selection
1119+ describe ( "Version-aware SQL query selection (PG13-PG18)" , ( ) => {
1120+ const pgVersions = [ 13 , 14 , 15 , 16 , 17 , 18 ] ;
1121+
1122+ // Core metrics that should be available for all versions
1123+ const coreMetrics = [
1124+ "settings" ,
1125+ "db_stats" ,
1126+ "db_size" ,
1127+ "stats_reset" ,
1128+ "pg_invalid_indexes" ,
1129+ "unused_indexes" ,
1130+ "redundant_indexes" ,
1131+ ] ;
1132+
1133+ describe ( "getMetricSql returns SQL for all PG versions" , ( ) => {
1134+ for ( const pgVersion of pgVersions ) {
1135+ for ( const metric of coreMetrics ) {
1136+ test ( `${ metric } for PG${ pgVersion } ` , ( ) => {
1137+ const sql = metricsLoader . getMetricSql ( metric , pgVersion ) ;
1138+ expect ( typeof sql ) . toBe ( "string" ) ;
1139+ expect ( sql . length ) . toBeGreaterThan ( 0 ) ;
1140+ // Verify it's actually SQL
1141+ expect ( sql . toLowerCase ( ) ) . toMatch ( / s e l e c t / ) ;
1142+ } ) ;
1143+ }
1144+ }
1145+ } ) ;
1146+
1147+ describe ( "getMetricSql selects appropriate version for each PG major version" , ( ) => {
1148+ for ( const pgVersion of pgVersions ) {
1149+ test ( `PG${ pgVersion } gets compatible SQL version` , ( ) => {
1150+ // Settings metric should return SQL for all supported versions
1151+ const sql = metricsLoader . getMetricSql ( "settings" , pgVersion ) ;
1152+ expect ( sql ) . toBeTruthy ( ) ;
1153+ // SQL should be valid (not throw an error)
1154+ expect ( ( ) => metricsLoader . getMetricSql ( "settings" , pgVersion ) ) . not . toThrow ( ) ;
1155+ } ) ;
1156+ }
1157+ } ) ;
1158+
1159+ describe ( "getMetricDefinition returns metadata for all metrics" , ( ) => {
1160+ for ( const metric of coreMetrics ) {
1161+ test ( `${ metric } has definition` , ( ) => {
1162+ const definition = metricsLoader . getMetricDefinition ( metric ) ;
1163+ expect ( definition ) . toBeTruthy ( ) ;
1164+ expect ( definition ?. sqls ) . toBeTruthy ( ) ;
1165+ expect ( typeof definition ?. sqls ) . toBe ( "object" ) ;
1166+ } ) ;
1167+ }
1168+ } ) ;
1169+
1170+ test ( "listMetricNames returns all expected metrics" , ( ) => {
1171+ const names = metricsLoader . listMetricNames ( ) ;
1172+ expect ( Array . isArray ( names ) ) . toBe ( true ) ;
1173+ // Should include core metrics
1174+ for ( const metric of coreMetrics ) {
1175+ expect ( names ) . toContain ( metric ) ;
1176+ }
1177+ } ) ;
1178+
1179+ describe ( "METRIC_NAMES maps check IDs correctly" , ( ) => {
1180+ test ( "H001 maps to pg_invalid_indexes" , ( ) => {
1181+ expect ( metricsLoader . METRIC_NAMES . H001 ) . toBe ( "pg_invalid_indexes" ) ;
1182+ } ) ;
1183+
1184+ test ( "H002 maps to unused_indexes" , ( ) => {
1185+ expect ( metricsLoader . METRIC_NAMES . H002 ) . toBe ( "unused_indexes" ) ;
1186+ } ) ;
1187+
1188+ test ( "H004 maps to redundant_indexes" , ( ) => {
1189+ expect ( metricsLoader . METRIC_NAMES . H004 ) . toBe ( "redundant_indexes" ) ;
1190+ } ) ;
1191+
1192+ test ( "settings metric exists" , ( ) => {
1193+ expect ( metricsLoader . METRIC_NAMES . settings ) . toBe ( "settings" ) ;
1194+ } ) ;
1195+ } ) ;
1196+ } ) ;
1197+
8911198
0 commit comments