feat(db): перевести хранилище с SQLite на PostgreSQL
Docker images / prepare-release (push) Successful in 12s
Docker images / backend-test (push) Successful in 4m9s
Docker images / frontend-image (push) Successful in 4m28s
Docker images / updater-image (push) Successful in 58s
Docker images / backend-image (push) Successful in 2m49s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 11s
Docker images / prepare-release (push) Successful in 12s
Docker images / backend-test (push) Successful in 4m9s
Docker images / frontend-image (push) Successful in 4m28s
Docker images / updater-image (push) Successful in 58s
Docker images / backend-image (push) Successful in 2m49s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 11s
При старте backend накатывает схему PostgreSQL 18 и, если база пустая, один раз импортирует mikrotik.db с тома. Повторный старт не копирует данные. Бэкап в UI идёт через pg_dump. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import type { Pool } from "pg"
|
||||
|
||||
export type PartitionKind = "day" | "week" | "month"
|
||||
|
||||
export interface PartitionSpec {
|
||||
parent: string
|
||||
kind: PartitionKind
|
||||
keepDays: number
|
||||
}
|
||||
|
||||
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: "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 },
|
||||
]
|
||||
|
||||
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<string> {
|
||||
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}')`,
|
||||
)
|
||||
return name
|
||||
}
|
||||
|
||||
export async function ensurePartitionsAround(pool: Pool, around = new Date()): Promise<void> {
|
||||
for (const spec of PARTITION_SPECS) {
|
||||
const daysAhead = spec.kind === "month" ? 40 : spec.kind === "week" ? 21 : 8
|
||||
const start = addUtcDays(around, -spec.keepDays)
|
||||
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<void> {
|
||||
for (const spec of PARTITION_SPECS) {
|
||||
const cutoff = addUtcDays(around, -spec.keepDays)
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user