Files
member-console/internal/db/partitions.go
T
cgalo5758 efe3f1528d Restrict member surfaces to published products
Add lifecycle_status = 'published' to the public-catalog queries
(plans and add-ons listings) and reject checkout before any Stripe
call unless the product behind the price clears the shared member
gate (published + active + public). The currently-enrolled ladder
rung stays renderable even if its product is later drafted or
retired, fetched directly so members keep seeing what they are on.

Introduce a single evaluateMemberGate definition shared by the
catalog paths and the operator readiness panel so the surfaces
cannot disagree about what is publishable for members.
2026-08-22 12:58:05 -05:00

67 lines
3.1 KiB
Go

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
}