persistence

package
v0.22.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 11, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Overview

Package store owns the database schema, GORM models, repositories, and the goose-driven migration runner. Callers depend on small interfaces declared where they consume the store (handler packages); only this package needs to know about GORM.

Index

Constants

This section is empty.

Variables

View Source
var ErrActiveReviewExists = errors.New("store: active code review exists")

ErrActiveReviewExists is returned by Start when the same user already has an active review on the PR — the partial unique index rejected the insert. Callers surface the conflict UX ("already reviewing") instead of a 500.

ErrNotFound aliases the routing domain sentinel so all existing callers keep the same value.

Functions

func MigrateDown

func MigrateDown(ctx context.Context, db *gorm.DB) error

MigrateDown rolls back the most recent migration.

func MigrateStatus

func MigrateStatus(ctx context.Context, db *gorm.DB) (string, error)

MigrateStatus returns a human-readable list of each migration and whether it has been applied.

func MigrateUp

func MigrateUp(ctx context.Context, db *gorm.DB) error

MigrateUp applies all pending migrations.

func NewTestDB

func NewTestDB(t *testing.T) *gorm.DB

NewTestDB returns a *gorm.DB backed by a fresh on-disk SQLite database inside t.TempDir, with all migrations applied. The database is closed and removed automatically when the test completes.

We use an on-disk file rather than `:memory:` because the goose-sqlite driver expects a stable database name to record migration state, and multiple in-memory connections in the same process are not always shared.

func Open

func Open(url string) (*gorm.DB, error)

Open opens a SQLite database for production use. The url accepts either a raw file path or the "file:..." DSN form used in our config defaults.

GORM's logger is silenced for "record not found" because we treat ErrNotFound as a normal return value, not a warning condition.

func RawCreateForTest

func RawCreateForTest(db *gorm.DB, pr PullRequest) error

RawCreateForTest inserts a pull_requests row preserving the caller's CreatedAt/UpdatedAt/ClosedAt, bypassing GORM's autoCreate/UpdateTime. Used by tests that need to seed PRs with a controlled age and open/closed state. A zero CreatedAt defaults to UpdatedAt.

func SQLDB

func SQLDB(db *gorm.DB) (*sql.DB, error)

SQLDB returns the underlying *sql.DB. Useful for the migrate binary which hands the connection to goose.

Types

type CodeReview

type CodeReview struct {
	ID            uint       `gorm:"primaryKey"`
	PullRequestID uint       `gorm:"column:pull_request_id;not null"`
	SlackUserID   string     `gorm:"column:slack_user_id;not null"`
	SlackUserName string     `gorm:"column:slack_user_name"`
	StartedAt     time.Time  `gorm:"column:started_at;not null"`
	FinishedAt    *time.Time `gorm:"column:finished_at"`
}

CodeReview is one review "session" for a PR: who started reviewing it and when, with FinishedAt nil while the review is in progress and set once the reviewer's GitHub review lands. It hangs off a PullRequest (cascade-deleted with it) and a partial unique index enforces at most one active (FinishedAt IS NULL) review per PR — finished rows accumulate as history.

func (CodeReview) TableName

func (CodeReview) TableName() string

TableName pins the table name.

type CodeReviews

type CodeReviews struct {
	// contains filtered or unexported fields
}

CodeReviews persists per-PR review sessions and enforces at most one active review per (PR, user); multiple distinct users may review a PR concurrently. A review is identified to callers by its PR's natural key (repository, prNumber); the surrogate pull_request_id is resolved internally.

func NewCodeReviews

func NewCodeReviews(db *gorm.DB) *CodeReviews

NewCodeReviews constructs a CodeReviews repository bound to db.

func (*CodeReviews) ActiveForUser

func (r *CodeReviews) ActiveForUser(ctx context.Context, repository string, prNumber int, slackUserID string) (CodeReview, error)

ActiveForUser returns the user's active (unfinished) review on the PR, or ErrNotFound when that user has no review in progress here. It is the app-level guard the click handler checks before Start; the DB's partial unique index on (pull_request_id, slack_user_id) is the race-safe backstop.

func (*CodeReviews) Finish

func (r *CodeReviews) Finish(ctx context.Context, repository string, prNumber int) error

Finish marks the PR's active review finished. It is idempotent: no active review (or an untracked PR) is a no-op, mirroring MarkClosed.

func (*CodeReviews) GetActive

func (r *CodeReviews) GetActive(ctx context.Context, repository string, prNumber int) (CodeReview, error)

GetActive returns the PR's active (unfinished) review, or ErrNotFound when the PR is untracked or has no review in progress.

func (*CodeReviews) Reviewers

func (r *CodeReviews) Reviewers(ctx context.Context, repository string, prNumber int) ([]CodeReview, error)

Reviewers returns all code-review sessions for the PR ordered by started_at ascending (earliest first). An untracked PR or a PR with no reviews returns an empty slice and nil error.

func (*CodeReviews) Start

func (r *CodeReviews) Start(ctx context.Context, repository string, prNumber int, slackUserID, slackUserName string) error

Start opens a review on the PR for the given Slack user. It returns ErrNotFound when the PR is not tracked, and ErrActiveReviewExists when the PR already has an active review — the DB's partial unique index is the source of truth, so two near-simultaneous Starts can't both win.

type Message

type Message struct {
	ID            uint   `gorm:"primaryKey"`
	PullRequestID uint   `gorm:"column:pull_request_id;not null"`
	Channel       string `gorm:"column:channel;not null"`
	MessageID     string `gorm:"column:message_id;not null"`
}

Message is one posted messenger message for a PR. (PullRequestID, Channel) is unique — at most one message per channel per PR. Channel is a room in the messenger; MessageID is the messenger's id for the post (Slack's ts).

func (Message) TableName

func (Message) TableName() string

TableName pins the table name.

type PullRequest

type PullRequest struct {
	ID         uint       `gorm:"primaryKey"`
	Repository string     `gorm:"column:gh_repository;not null"`
	PRNumber   int        `gorm:"column:pr_number;not null"`
	CreatedAt  time.Time  `gorm:"column:created_at;not null"`
	UpdatedAt  time.Time  `gorm:"column:updated_at;not null"`
	ClosedAt   *time.Time `gorm:"column:closed_at"`
	Messages   []Message  `gorm:"foreignKey:PullRequestID;constraint:OnDelete:CASCADE"`
}

PullRequest is one tracked PR. (Repository, PRNumber) is the natural key; CreatedAt is kept for later statistics, UpdatedAt is the activity clock (bumped on open and every review/comment) driving digest idle-detection and cleanup, and ClosedAt (nil = open) marks merged/closed so the digest skips it.

func (PullRequest) TableName

func (PullRequest) TableName() string

TableName pins the table name; do not rely on GORM pluralization.

type PullRequests

type PullRequests struct {
	// contains filtered or unexported fields
}

PullRequests persists tracked PRs and their per-channel messenger messages.

func NewPullRequests

func NewPullRequests(db *gorm.DB) *PullRequests

NewPullRequests constructs a PullRequests repository bound to db.

func (*PullRequests) AddMessage

func (r *PullRequests) AddMessage(ctx context.Context, repository string, prNumber int, channel, messageID string) error

AddMessage records one posted message, creating the PR row on first sight. Insertion is idempotent on (pull_request_id, channel): re-adding the same channel for the same PR is a no-op, which makes the open fan-out safe to replay after a partial failure or GitHub redelivery.

func (*PullRequests) Delete

func (r *PullRequests) Delete(ctx context.Context, repository string, prNumber int) error

Delete removes the PR and (by cascade) its messages. Missing PR is a no-op.

func (*PullRequests) DeleteStaleBefore

func (r *PullRequests) DeleteStaleBefore(ctx context.Context, cutoff time.Time) (int64, error)

DeleteStaleBefore removes PRs idle since before cutoff (messages cascade).

func (*PullRequests) FindStuck

func (r *PullRequests) FindStuck(ctx context.Context, cutoff time.Time) ([]PullRequest, error)

FindStuck returns open PRs idle since before cutoff, messages preloaded, oldest first.

func (*PullRequests) ListOpen

func (r *PullRequests) ListOpen(ctx context.Context) ([]PullRequest, error)

ListOpen returns every not-yet-closed PR, ordered for stable output.

func (*PullRequests) MarkClosed

func (r *PullRequests) MarkClosed(ctx context.Context, repository string, prNumber int) error

MarkClosed sets closed_at. Missing PR is a no-op.

func (*PullRequests) Messages

func (r *PullRequests) Messages(ctx context.Context, repository string, prNumber int) ([]Message, error)

Messages returns the PR's messages, or ErrNotFound when the PR is unknown.

func (*PullRequests) Touch

func (r *PullRequests) Touch(ctx context.Context, repository string, prNumber int) error

Touch bumps updated_at, recording activity. Missing PR is a no-op.

type Reactions

type Reactions = routingdomain.Reactions

Reactions aliases the routing domain value object during the migration.

type RepoMapping

type RepoMapping = routingdomain.RepoMapping

RepoMapping aliases the routing domain value object during the migration.

type Target

type Target = routingdomain.Target

Target aliases the routing domain value object during the migration.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL