Skip to content

Commit 195efce

Browse files
committed
feat: add L003 to CLI express path implementation
Implement L003 (Integer out-of-range risks in PKs) in the CLI TypeScript code for the "express path" - direct SQL execution against PostgreSQL. Changes: - cli/scripts/embed-metrics.ts: Add sequence_overflow to REQUIRED_METRICS - cli/lib/metrics-loader.ts: Add L003 metric name mapping - cli/lib/checkup.ts: Add SequenceOverflowRisk interface, getSequenceOverflowRisks helper function, generateL003 report generator, and register in REPORT_GENERATORS - cli/test/test-utils.ts: Add mock support for sequence_overflow queries The express path allows running checkups directly against PostgreSQL without requiring the full pgwatch/Prometheus infrastructure. All 297 CLI tests pass.
1 parent 38e7e40 commit 195efce

File tree

4 files changed

+117
-1
lines changed

4 files changed

+117
-1
lines changed

cli/lib/checkup.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,24 @@ export interface RedundantIndex {
242242
redundant_to_parse_error?: string;
243243
}
244244

245+
/**
246+
* Sequence overflow risk entry (L003)
247+
* Monitors sequence-generated columns for potential integer overflow
248+
*/
249+
export interface SequenceOverflowRisk {
250+
schema_name: string;
251+
table_name: string;
252+
column_name: string;
253+
sequence_name: string;
254+
sequence_data_type: string;
255+
column_data_type: string;
256+
current_value: number;
257+
max_value: number;
258+
sequence_percent_used: number;
259+
column_percent_used: number;
260+
capacity_used_pretty: string;
261+
}
262+
245263
/**
246264
* Node result for reports
247265
*/
@@ -839,6 +857,49 @@ export async function getRedundantIndexes(client: Client, pgMajorVersion: number
839857
});
840858
}
841859

860+
/**
861+
* Get sequence overflow risks from database (L003)
862+
* Monitors sequence-generated columns (serial/identity) for potential integer overflow
863+
* by checking how close the current sequence value is to the max value for the data type.
864+
*
865+
* @param client - Connected PostgreSQL client
866+
* @param pgMajorVersion - PostgreSQL major version (default: 16)
867+
* @returns Array of sequence overflow risk entries
868+
*/
869+
export async function getSequenceOverflowRisks(
870+
client: Client,
871+
pgMajorVersion: number = 16
872+
): Promise<SequenceOverflowRisk[]> {
873+
const sql = getMetricSql(METRIC_NAMES.L003, pgMajorVersion);
874+
const result = await client.query(sql);
875+
return result.rows.map((row) => {
876+
const transformed = transformMetricRow(row);
877+
const currentValue = parseInt(String(transformed.current_value || 0), 10);
878+
const sequencePercentUsed = parseFloat(String(transformed.sequence_percent_used || 0));
879+
const columnPercentUsed = parseFloat(String(transformed.column_percent_used || 0));
880+
const capacityPercent = Math.max(sequencePercentUsed, columnPercentUsed);
881+
882+
// Use the smaller of the two max values (column type is the constraint)
883+
const sequenceMaxValue = parseInt(String(transformed.sequence_max_value || 0), 10);
884+
const columnMaxValue = parseInt(String(transformed.column_max_value || 0), 10);
885+
const effectiveMaxValue = Math.min(sequenceMaxValue, columnMaxValue) || columnMaxValue || sequenceMaxValue;
886+
887+
return {
888+
schema_name: String(transformed.schema_name || ""),
889+
table_name: String(transformed.table_name || ""),
890+
column_name: String(transformed.column_name || ""),
891+
sequence_name: String(transformed.sequence_name || ""),
892+
sequence_data_type: String(transformed.sequence_data_type || ""),
893+
column_data_type: String(transformed.column_data_type || ""),
894+
current_value: currentValue,
895+
max_value: effectiveMaxValue,
896+
sequence_percent_used: Math.round(sequencePercentUsed * 100) / 100,
897+
column_percent_used: Math.round(columnPercentUsed * 100) / 100,
898+
capacity_used_pretty: `${capacityPercent.toFixed(2)}%`,
899+
};
900+
});
901+
}
902+
842903
/**
843904
* Create base report structure
844905
*/
@@ -1326,6 +1387,49 @@ async function generateG001(client: Client, nodeName: string): Promise<Report> {
13261387
return report;
13271388
}
13281389

1390+
/**
1391+
* Generate L003 report - Integer out-of-range risks in PKs
1392+
* Monitors sequence-generated columns (serial/identity) for potential integer overflow.
1393+
*/
1394+
export async function generateL003(client: Client, nodeName: string = "node-01"): Promise<Report> {
1395+
const report = createBaseReport("L003", "Integer out-of-range risks in PKs", nodeName);
1396+
const postgresVersion = await getPostgresVersion(client);
1397+
const pgMajorVersion = parseInt(postgresVersion.server_major_ver, 10) || 16;
1398+
const overflowRisks = await getSequenceOverflowRisks(client, pgMajorVersion);
1399+
const { datname: dbName } = await getCurrentDatabaseInfo(client, pgMajorVersion);
1400+
1401+
// Default thresholds matching schema
1402+
const warningThreshold = 50;
1403+
const criticalThreshold = 75;
1404+
1405+
// Count high-risk items (above warning threshold)
1406+
const highRiskCount = overflowRisks.filter(
1407+
(risk) => Math.max(risk.sequence_percent_used, risk.column_percent_used) >= warningThreshold
1408+
).length;
1409+
1410+
// Sort by highest capacity usage (descending)
1411+
overflowRisks.sort(
1412+
(a, b) =>
1413+
Math.max(b.sequence_percent_used, b.column_percent_used) -
1414+
Math.max(a.sequence_percent_used, a.column_percent_used)
1415+
);
1416+
1417+
report.results[nodeName] = {
1418+
data: {
1419+
[dbName]: {
1420+
overflow_risks: overflowRisks,
1421+
total_count: overflowRisks.length,
1422+
high_risk_count: highRiskCount,
1423+
warning_threshold_pct: warningThreshold,
1424+
critical_threshold_pct: criticalThreshold,
1425+
},
1426+
},
1427+
postgres_version: postgresVersion,
1428+
};
1429+
1430+
return report;
1431+
}
1432+
13291433
/**
13301434
* Available report generators
13311435
*/
@@ -1341,6 +1445,7 @@ export const REPORT_GENERATORS: Record<string, (client: Client, nodeName: string
13411445
H001: generateH001,
13421446
H002: generateH002,
13431447
H004: generateH004,
1448+
L003: generateL003,
13441449
};
13451450

13461451
/**
@@ -1358,6 +1463,7 @@ export const CHECK_INFO: Record<string, string> = {
13581463
H001: "Invalid indexes",
13591464
H002: "Unused indexes",
13601465
H004: "Redundant indexes",
1466+
L003: "Integer out-of-range risks in PKs",
13611467
};
13621468

13631469
/**

cli/lib/metrics-loader.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,10 @@ export function listMetricNames(): string[] {
6363
export const METRIC_NAMES = {
6464
// Index health checks
6565
H001: "pg_invalid_indexes",
66-
H002: "unused_indexes",
66+
H002: "unused_indexes",
6767
H004: "redundant_indexes",
68+
// Sequence overflow risks (L003)
69+
L003: "sequence_overflow",
6870
// Settings and version info (A002, A003, A007, A013)
6971
settings: "settings",
7072
// Database statistics (A004)

cli/scripts/embed-metrics.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ const REQUIRED_METRICS = [
4646
"redundant_indexes",
4747
// Stats reset info (H002)
4848
"stats_reset",
49+
// Sequence overflow risks (L003)
50+
"sequence_overflow",
4951
];
5052

5153
function main() {

cli/test/test-utils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface MockClientOptions {
1616
unusedIndexesRows?: any[];
1717
redundantIndexesRows?: any[];
1818
sensitiveColumnsRows?: any[];
19+
sequenceOverflowRows?: any[];
1920
}
2021

2122
const DEFAULT_VERSION_ROWS = [
@@ -47,6 +48,7 @@ export function createMockClient(options: MockClientOptions = {}) {
4748
unusedIndexesRows = [],
4849
redundantIndexesRows = [],
4950
sensitiveColumnsRows = [],
51+
sequenceOverflowRows = [],
5052
} = options;
5153

5254
return {
@@ -99,6 +101,10 @@ export function createMockClient(options: MockClientOptions = {}) {
99101
if (sql.includes("redundant_indexes_grouped") && sql.includes("columns like")) {
100102
return { rows: redundantIndexesRows };
101103
}
104+
// Sequence overflow (L003) - from metrics.yml
105+
if (sql.includes("pg_sequence_last_value") && sql.includes("pg_depend") && sql.includes("sequence_percent_used")) {
106+
return { rows: sequenceOverflowRows };
107+
}
102108
// D004: pg_stat_statements extension check
103109
if (sql.includes("pg_extension") && sql.includes("pg_stat_statements")) {
104110
return { rows: [] };

0 commit comments

Comments
 (0)