First pass implementation of an SQLite backing store for fetched opportunities
This commit is contained in:
1 parent
9b0e6dccfe
commit
045492a148
29 files changed
+3132
-9
No files matched your search
@@ -0,0 +1,200 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
|
||||
"github.com/pressly/goose/v3"
|
||||
)
|
||||
|
||||
// Each file holds both directions, marked with -- +goose Up / -- +goose Down.
|
||||
// Create new ones with: goose -dir internal/store/migrations create NAME sql
|
||||
//
|
||||
//go:embed migrations/*.sql
|
||||
var migrationsFS embed.FS
|
||||
|
||||
// ErrNeedsMigration is returned by Open when the database has pending
|
||||
// migrations. Apply them with the migrate command, or pass
|
||||
// WithAutoMigrate.
|
||||
var ErrNeedsMigration = errors.New("store: database needs migrating")
|
||||
|
||||
// ErrSchemaTooNew is returned by Open when the database has migrations
|
||||
// this build doesn't know about, i.e. it was migrated by a newer build.
|
||||
var ErrSchemaTooNew = errors.New("store: database schema is newer than this build")
|
||||
|
||||
// ErrWouldEmptyDatabase is returned by StepDown when the migration would drop
|
||||
// all of the tables and thus empty the database.
|
||||
var ErrWouldEmptyDatabase = errors.New(
|
||||
"migration cancelled; database would be emptied by undoing all migrations",
|
||||
)
|
||||
|
||||
// NewMigrator returns a goose Provider over the store's embedded migrations,
|
||||
// for applying, rolling back or inspecting them. Each migration runs in its
|
||||
// own transaction, so a failed migration leaves the database at the previous
|
||||
// version (goose has no "dirty" state).
|
||||
func NewMigrator(db *sql.DB) (*goose.Provider, error) {
|
||||
fsys, err := fs.Sub(migrationsFS, "migrations")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return goose.NewProvider(goose.DialectSQLite3, db, fsys)
|
||||
}
|
||||
|
||||
// LatestVersion is the highest migration version in this build.
|
||||
func LatestVersion(p *goose.Provider) int64 {
|
||||
sources := p.ListSources() // sorted ascending
|
||||
if len(sources) == 0 {
|
||||
return 0
|
||||
}
|
||||
return sources[len(sources)-1].Version
|
||||
}
|
||||
|
||||
// checkVersion returns ErrNeedsMigration or ErrSchemaTooNew unless the
|
||||
// database is exactly at this build's schema.
|
||||
func checkVersion(ctx context.Context, p *goose.Provider) error {
|
||||
current, err := p.GetDBVersion(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
latest := LatestVersion(p)
|
||||
if current > latest {
|
||||
return fmt.Errorf("%w (database is at version %d, this build knows up to %d)",
|
||||
ErrSchemaTooNew, current, latest)
|
||||
}
|
||||
|
||||
pending, err := p.HasPending(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if pending {
|
||||
return fmt.Errorf("%w (database is at version %d, latest is %d)",
|
||||
ErrNeedsMigration, current, latest)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkNotNewer refuses to work on a database migrated by a newer build:
|
||||
// this build has neither the newer migrations nor their down steps.
|
||||
func checkNotNewer(ctx context.Context, m *goose.Provider) error {
|
||||
current, err := m.GetDBVersion(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if latest := LatestVersion(m); current > latest {
|
||||
return fmt.Errorf(
|
||||
"database is at version %d but this build only knows up to %d; use a newer build",
|
||||
current, latest)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// prepareSchema migrates (with WithAutoMigrate) or verifies the schema
|
||||
// version.
|
||||
func prepareSchema(ctx context.Context, db *sql.DB, o options) error {
|
||||
p, err := NewMigrator(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if o.autoMigrate {
|
||||
if _, err := p.Up(ctx); err != nil {
|
||||
return fmt.Errorf("store: migrating database: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return checkVersion(ctx, p)
|
||||
}
|
||||
|
||||
// StepUp applies steps pending migrations, or all of them when steps is 0.
|
||||
func StepUp(
|
||||
ctx context.Context, m *goose.Provider, steps int,
|
||||
) ([]*goose.MigrationResult, error) {
|
||||
var results []*goose.MigrationResult
|
||||
|
||||
if err := checkNotNewer(ctx, m); err != nil {
|
||||
return results, err
|
||||
}
|
||||
|
||||
var err error
|
||||
if steps == 0 {
|
||||
results, err = m.Up(ctx)
|
||||
} else {
|
||||
for range steps {
|
||||
var r *goose.MigrationResult
|
||||
r, err = m.UpByOne(ctx)
|
||||
if errors.Is(err, goose.ErrNoNextVersion) {
|
||||
err = nil
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
results = append(results, r)
|
||||
}
|
||||
}
|
||||
|
||||
return results, err
|
||||
}
|
||||
|
||||
// StepDown rolls back steps applied migrations, or all of them when steps
|
||||
// is 0. Rolling everything back deletes all data, so confirmDrop is checked
|
||||
// first in that case.
|
||||
func StepDown(
|
||||
ctx context.Context, m *goose.Provider, steps int, confirmDrop bool,
|
||||
) ([]*goose.MigrationResult, error) {
|
||||
var results []*goose.MigrationResult
|
||||
|
||||
if err := checkNotNewer(ctx, m); err != nil {
|
||||
return results, err
|
||||
}
|
||||
|
||||
current, err := m.GetDBVersion(ctx)
|
||||
if err != nil {
|
||||
return results, err
|
||||
}
|
||||
if current == 0 {
|
||||
return results, nil
|
||||
}
|
||||
|
||||
statuses, err := m.Status(ctx)
|
||||
if err != nil {
|
||||
return results, err
|
||||
}
|
||||
applied := 0
|
||||
for _, s := range statuses {
|
||||
if s.State == goose.StateApplied {
|
||||
applied++
|
||||
}
|
||||
}
|
||||
all := steps == 0 || steps >= applied
|
||||
if all && !confirmDrop {
|
||||
return results, ErrWouldEmptyDatabase
|
||||
}
|
||||
|
||||
if all {
|
||||
results, err = m.DownTo(ctx, 0)
|
||||
} else {
|
||||
for range steps {
|
||||
var r *goose.MigrationResult
|
||||
r, err = m.Down(ctx)
|
||||
if errors.Is(err, goose.ErrNoNextVersion) {
|
||||
err = nil
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
results = append(results, r)
|
||||
}
|
||||
}
|
||||
|
||||
return results, err
|
||||
}
|
||||
Reference in new issue
Block a user