Documentation
¶
Overview ¶
Package store owns the database layer: migrations, partition maintenance and the sqlc-generated queries.
Index ¶
- Constants
- Variables
- func DefaultPartitionCounts(ctx context.Context, pool *pgxpool.Pool) (map[string]int64, error)
- func Down(ctx context.Context, dsn string) error
- func DropExpiredPartitions(ctx context.Context, pool *pgxpool.Pool, retentionDays int, now time.Time) ([]string, error)
- func EnsurePartitionRange(ctx context.Context, pool *pgxpool.Pool, from, to time.Time) (int, error)
- func EnsurePartitions(ctx context.Context, pool *pgxpool.Pool, ahead int) (int, error)
- func Migrate(ctx context.Context, dsn string, log *slog.Logger) error
- func PartitionName(table string, at time.Time) string
- func Status(ctx context.Context, dsn string) ([]string, error)
Constants ¶
const PartitionLookahead = 2
PartitionLookahead is how many months of partitions to create beyond the current one.
Variables ¶
var Migrations = func() fs.FS { sub, err := fs.Sub(migrationsFS, "migrations") if err != nil { panic("store: cannot open embedded migrations: " + err.Error()) } return sub }()
Migrations is the embedded set rooted at the migration files themselves. goose scans the root of the FS it is given, so the "migrations/" prefix has to be stripped or it finds nothing.
var PartitionedTables = []string{"click_events", "visitors", "audit_logs"}
PartitionedTables are the RANGE-partitioned tables. All are keyed on a timestamptz and partitioned by month.
Two of the three are dormant in Phase 1: nothing writes visitors at all, and audit_logs has a table but no behavior. They are maintained anyway, and that is deliberate rather than an oversight. The cost is one to_regclass check per table per month — the partition already exists on all but one run an hour. The benefit is that the day something does write to them, partitions exist and retention already applies. The alternative fails in the direction that matters: rows landing in the default partition, which retention never drops, so dormant tables would quietly become the one place raw visitor data is kept forever.
var RetainedTables = []string{"click_events", "visitors"}
RetainedTables are the partitioned tables that the analytics retention window applies to.
audit_logs is partitioned identically and is deliberately not here. Audit retention is a different policy from analytics retention — the reason to keep an audit trail is that someone may need to ask what happened a long time afterwards — and quietly deleting it on the analytics setting would be a surprise of exactly the wrong kind. It grows until Phase 2 gives it a policy of its own.
Functions ¶
func DefaultPartitionCounts ¶
DefaultPartitionCounts reports how many rows sit in each default partition.
A non-zero count is an operational alert, not a curiosity: it means rows arrived outside every explicit range, and attaching the partition that should have held them will now fail until they are moved out.
func DropExpiredPartitions ¶
func DropExpiredPartitions(ctx context.Context, pool *pgxpool.Pool, retentionDays int, now time.Time) ([]string, error)
DropExpiredPartitions drops monthly partitions whose entire range is older than the retention window, and reports what it dropped.
A retentionDays of zero or less keeps everything, matching the configuration contract that 0 means "forever".
Retention is enforced at month granularity, and only when the newest row a partition could hold is already outside the window. The alternative — deleting rows older than exactly N days — would mean a DELETE across the largest table in the system, then a VACUUM to reclaim the space, on a schedule. Dropping a partition is instant, reclaims the space immediately, and cannot half-finish. The cost is that data survives up to a month past the nominal window, which is the right way to be wrong: keeping data slightly too long is recoverable, and deleting it slightly too early is not.
Daily rollups live in their own unpartitioned tables and are untouched, so historical charts keep working after the raw events are gone.
func EnsurePartitionRange ¶
EnsurePartitionRange creates monthly partitions covering every month from `from` to `to` inclusive, plus a default partition per table.
Separate from EnsurePartitions because the months that need to exist are not always the ones around today: restoring a backup and seeding a load-test dataset both write into the past, and an insert with no matching partition lands in the default one, where it silently blocks attaching the partition that should have held it.
func EnsurePartitions ¶
EnsurePartitions creates monthly partitions for the current month and the next `ahead` months, plus a default partition per table. It reports how many it created and is safe to call repeatedly.
Two things here are load-bearing.
The session timezone is pinned to UTC for the DDL. Bounds on a timestamptz column resolve against the session timezone at DDL time, so the identical bound literal produces a different absolute range under a different timezone, leaving either a gap that silently routes rows to the default partition or an overlap that makes attaching fail. Demonstrated in docs/adr/0001-partitioning-and-sqlc.md.
It looks more than one month ahead. Creating next month's partition on the last day of this one is a single point of failure with a hard deadline; two months of headroom turns a missed run into a warning rather than an outage.
func Migrate ¶
Migrate applies all pending migrations, then ensures partitions exist.
Runs in-process at boot, before the listener opens. An init container would need either a shell (distroless has none) or a second image, plus depends_on wiring that confuses a first-time operator; in-process means `docker compose up` on an empty volume produces a working app with no extra concepts.
A Postgres session lock serializes replicas racing at startup, so a rolling deploy cannot run the same migration twice.
func PartitionName ¶
PartitionName returns the partition a timestamp belongs to.
Types ¶
This section is empty.