Introduce a commercial license option alongside AGPL-3.0-only, require a CLA for contributors, and document the terms in COMMERCIAL.md and NOTICE. Add a script to stamp SPDX headers on Go files and apply it across the tree.
70 lines
3.2 KiB
Go
70 lines
3.2 KiB
Go
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
|
|
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// DefaultWebhookPartitionMonthsAhead is the default lookahead window for
|
|
// EnsureWebhookEventPartitions: the current month plus this many months
|
|
// ahead. One more than the baseline migration's initial 0..2 loop
|
|
// (migrations/00001_init.sql:1054), so a fresh install immediately gains
|
|
// headroom (design.md D3).
|
|
const DefaultWebhookPartitionMonthsAhead = 3
|
|
|
|
// EnsureWebhookEventPartitions creates the monthly RANGE partitions of
|
|
// core.webhook_events covering the UTC month of `now` through `now`'s month
|
|
// plus monthsAhead (inclusive) — e.g. monthsAhead=3 ensures four partitions:
|
|
// the current month and the next three.
|
|
//
|
|
// It mirrors the baseline migration's partition-creation DO block
|
|
// (migrations/00001_init.sql:1048-1064) exactly: the same
|
|
// core.webhook_events_YYYY_MM naming (zero-padded four-digit year, two-digit
|
|
// month) and the same month-start/next-month-start boundaries. That parity
|
|
// is load-bearing — partitions created here must never overlap the ones the
|
|
// migration creates, whichever runs first — so any change to the naming or
|
|
// boundary computation here MUST be mirrored back into that DO block, and
|
|
// vice versa.
|
|
//
|
|
// Idempotent via CREATE TABLE IF NOT EXISTS: running it twice, or racing the
|
|
// migration's own initial-partition creation, is safe. A concurrent-create
|
|
// race between two runners surfaces as an error to whichever loses; the
|
|
// caller logs it and the next run (boot pass or scheduled workflow) succeeds
|
|
// (design.md D3, Risks).
|
|
func EnsureWebhookEventPartitions(ctx context.Context, database *sql.DB, now time.Time, monthsAhead int) error {
|
|
monthStart := time.Date(now.UTC().Year(), now.UTC().Month(), 1, 0, 0, 0, 0, time.UTC)
|
|
|
|
for i := 0; i <= monthsAhead; i++ {
|
|
start := monthStart.AddDate(0, i, 0)
|
|
end := start.AddDate(0, 1, 0)
|
|
|
|
// The partition name and the boundary literals are composed entirely
|
|
// from computed integers (never user input), so building the DDL
|
|
// string with fmt.Sprintf is safe — mirroring the migration's own
|
|
// EXECUTE FORMAT('... %s ... %L ... %L', part, m_start, m_end)
|
|
// approach exactly, which relies on the same property. Boundaries are
|
|
// embedded as literals rather than query parameters deliberately: pgx
|
|
// against PG18 rejects placeholders inside this DDL shape ("mismatched
|
|
// param and argument count") — CREATE TABLE ... PARTITION OF ...
|
|
// FOR VALUES FROM/TO isn't a statement pgx's extended protocol can
|
|
// describe placeholders for, the same family of pgx/PG18 DDL
|
|
// interaction ConnectPlain's doc comment (database.go) already works
|
|
// around for multi-statement migrations.
|
|
partition := fmt.Sprintf("core.webhook_events_%04d_%02d", start.Year(), int(start.Month()))
|
|
stmt := fmt.Sprintf(
|
|
`CREATE TABLE IF NOT EXISTS %s PARTITION OF core.webhook_events FOR VALUES FROM ('%s') TO ('%s')`,
|
|
partition, start.Format(time.RFC3339), end.Format(time.RFC3339),
|
|
)
|
|
if _, err := database.ExecContext(ctx, stmt); err != nil {
|
|
return fmt.Errorf("ensure webhook_events partition %s: %w", partition, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|