import type { Pool } from "pg" export type PartitionKind = "day" | "week" | "month" export interface PartitionSpec { parent: string kind: PartitionKind keepDays: number } /** Leaf-only: PG forbids storage params on partitioned parents (SQLSTATE 42809). */ const FACT_LEAF_STORAGE = "fillfactor = 70, autovacuum_vacuum_scale_factor = 0.05, autovacuum_vacuum_cost_limit = 2000" const FACT_PARENTS = new Set(["flow_hour_facts", "flow_daily_facts"]) export const PARTITION_SPECS: PartitionSpec[] = [ { parent: "flow_buckets", kind: "day", keepDays: 4 }, { parent: "flow_minute_stats", kind: "day", keepDays: 4 }, { parent: "flow_minute_dims", kind: "day", keepDays: 4 }, { parent: "flow_daily_dims", kind: "month", keepDays: 420 }, { parent: "flow_hour_facts", kind: "day", keepDays: 3 }, { parent: "flow_daily_facts", kind: "month", keepDays: 420 }, { parent: "traffic_samples", kind: "week", keepDays: 21 }, { parent: "servers_rest_ping_samples", kind: "week", keepDays: 35 }, { parent: "uptime_probe_samples", kind: "week", keepDays: 21 }, { parent: "uptime_resource_samples", kind: "week", keepDays: 21 }, { parent: "server_snapshots", kind: "week", keepDays: 21 }, { parent: "internet_path_snapshots", kind: "week", keepDays: 21 }, ] const WEEK_SLACK_DAYS = 7 async function resolveKeepDays(pool: Pool): Promise> { const map = new Map(PARTITION_SPECS.map((s) => [s.parent, s.keepDays])) try { const traffic = await pool.query<{ retention_days: number }>( `SELECT retention_days FROM traffic_settings WHERE id = 1`, ) const td = Number(traffic.rows[0]?.retention_days) if (Number.isFinite(td) && td > 0) map.set("traffic_samples", td + WEEK_SLACK_DAYS) const uptime = await pool.query<{ retention_days: number }>( `SELECT retention_days FROM uptime_settings WHERE id = 1`, ) const ud = Number(uptime.rows[0]?.retention_days) if (Number.isFinite(ud) && ud > 0) { map.set("uptime_probe_samples", ud + WEEK_SLACK_DAYS) map.set("uptime_resource_samples", ud + WEEK_SLACK_DAYS) } const path = await pool.query<{ retention_days: number }>( `SELECT retention_days FROM internet_path_settings WHERE id = 1`, ) const pd = Number(path.rows[0]?.retention_days) if (Number.isFinite(pd) && pd > 0) map.set("internet_path_snapshots", pd + WEEK_SLACK_DAYS) } catch { /* settings may be absent mid-migration */ } return map } function utcDate(d: Date): Date { return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())) } function addUtcDays(d: Date, n: number): Date { const x = utcDate(d) x.setUTCDate(x.getUTCDate() + n) return x } function startOfWeekUtc(d: Date): Date { const x = utcDate(d) const day = x.getUTCDay() || 7 x.setUTCDate(x.getUTCDate() - (day - 1)) return x } function startOfMonthUtc(d: Date): Date { return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1)) } function fmtDay(d: Date): string { return d.toISOString().slice(0, 10) } function partName(parent: string, kind: PartitionKind, start: Date): string { if (kind === "day") return `${parent}_d_${fmtDay(start).replaceAll("-", "")}` if (kind === "week") return `${parent}_w_${fmtDay(start).replaceAll("-", "")}` return `${parent}_m_${start.getUTCFullYear()}${String(start.getUTCMonth() + 1).padStart(2, "0")}` } function rangeFor(kind: PartitionKind, ts: Date): { start: Date; end: Date } { if (kind === "day") { const start = utcDate(ts) return { start, end: addUtcDays(start, 1) } } if (kind === "week") { const start = startOfWeekUtc(ts) return { start, end: addUtcDays(start, 7) } } const start = startOfMonthUtc(ts) const end = new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth() + 1, 1)) return { start, end } } export async function ensurePartitionFor( pool: Pool, parent: string, kind: PartitionKind, ts: Date, ): Promise { const { start, end } = rangeFor(kind, ts) const name = partName(parent, kind, start) const from = fmtDay(start) const to = fmtDay(end) await pool.query( `CREATE TABLE IF NOT EXISTS ${name} PARTITION OF ${parent} FOR VALUES FROM ('${from}') TO ('${to}')`, ) if (FACT_PARENTS.has(parent)) { await pool.query(`ALTER TABLE ${name} SET (${FACT_LEAF_STORAGE})`) } return name } export async function ensurePartitionsBetween( pool: Pool, parent: string, kind: PartitionKind, from: Date, to: Date, ): Promise { const start = from.getTime() <= to.getTime() ? from : to const end = from.getTime() <= to.getTime() ? to : from for (let t = new Date(start.getTime()); t <= end; ) { await ensurePartitionFor(pool, parent, kind, t) if (kind === "day") t = addUtcDays(t, 1) else if (kind === "week") t = addUtcDays(t, 7) else t = new Date(Date.UTC(t.getUTCFullYear(), t.getUTCMonth() + 1, 1)) } } export async function ensurePartitionsAround(pool: Pool, around = new Date()): Promise { const keepDays = await resolveKeepDays(pool) for (const spec of PARTITION_SPECS) { const keep = keepDays.get(spec.parent) ?? spec.keepDays const daysAhead = spec.kind === "month" ? 40 : spec.kind === "week" ? 21 : 8 const start = addUtcDays(around, -keep) const end = addUtcDays(around, daysAhead) for (let t = new Date(start.getTime()); t < end; ) { await ensurePartitionFor(pool, spec.parent, spec.kind, t) if (spec.kind === "day") t = addUtcDays(t, 1) else if (spec.kind === "week") t = addUtcDays(t, 7) else t = new Date(Date.UTC(t.getUTCFullYear(), t.getUTCMonth() + 1, 1)) } } } export async function dropExpiredPartitions(pool: Pool, around = new Date()): Promise { const keepDays = await resolveKeepDays(pool) for (const spec of PARTITION_SPECS) { const keep = keepDays.get(spec.parent) ?? spec.keepDays const cutoff = addUtcDays(around, -keep) const { rows } = await pool.query<{ relname: string }>( `SELECT c.relname FROM pg_inherits i JOIN pg_class c ON c.oid = i.inhrelid JOIN pg_class p ON p.oid = i.inhparent WHERE p.relname = $1`, [spec.parent], ) for (const row of rows) { const m = row.relname.match(/_([dwm])_(\d{8}|\d{6})$/) if (!m) continue const stamp = m[2] let start: Date if (stamp.length === 6) { start = new Date(Date.UTC(Number(stamp.slice(0, 4)), Number(stamp.slice(4, 6)) - 1, 1)) } else { start = new Date(`${stamp.slice(0, 4)}-${stamp.slice(4, 6)}-${stamp.slice(6, 8)}T00:00:00Z`) } if (start < cutoff) { await pool.query(`DROP TABLE IF EXISTS ${row.relname}`) } } } } export function specForParent(parent: string): PartitionSpec | undefined { return PARTITION_SPECS.find((s) => s.parent === parent) }