You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

200 lines
4.5 KiB
Go

package dbtimesprovider
import (
"bufio"
"context"
"database/sql"
_ "embed"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/doug-martin/goqu/v9"
"github.com/samber/lo"
"prayertimes/pkg/prayer"
)
//go:embed schema.sql
var schema string
//go:embed locations.jsonl
var locationsJSON string
type Provider struct {
db *goqu.Database
provider prayer.TimesProvider
clockFunc func() time.Time
}
func New(db *goqu.Database, provider prayer.TimesProvider) Provider {
return Provider{
provider: provider,
clockFunc: time.Now,
db: db,
}
}
func (p Provider) Name() string {
return "db:" + p.provider.Name()
}
func (p Provider) Get(ctx context.Context, location string) ([]prayer.Times, error) {
times, err := p.loadTimes(ctx, location)
if err == nil && len(times) != 0 {
return times, nil
} else if err != nil {
return nil, fmt.Errorf("failed to load prayer times from db: %w", err)
}
times, err = p.provider.Get(ctx, location)
if err != nil {
return nil, fmt.Errorf("failed to get prayer times: %w", err)
}
if len(times) > 1 {
if err := p.saveTimes(ctx, location, times); err != nil {
return nil, fmt.Errorf("failed to save times to db: %w", err)
}
}
return times, nil
}
func Migrate(con *sql.DB) error {
db := goqu.New("sqlite3", con)
if _, err := db.Exec(schema); err != nil {
return fmt.Errorf("failed to migrate: %w", err)
}
count, _ := db.From("locations").Count()
if count > 0 {
return nil
}
type entry struct {
ID int `json:"id" db:"id"`
Country string `json:"country" db:"country"`
Region string `json:"region" db:"region"`
City string `json:"city" db:"city"`
}
s := bufio.NewScanner(strings.NewReader(locationsJSON))
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("failed to begin tx: %w", err)
}
if err := tx.Wrap(func() error {
for s.Scan() {
var e entry
if err := json.Unmarshal(s.Bytes(), &e); err != nil {
return fmt.Errorf("failed to parse as json: %w", err)
}
q := tx.Insert("locations").
OnConflict(goqu.DoNothing()).
Rows(e)
if _, err := q.Executor().Exec(); err != nil {
return fmt.Errorf("failed to insert location: %w", err)
}
}
return nil
}); err != nil {
return err
}
return nil
}
type prayerTimesRow struct {
ProviderID int64 `db:"provider_id"`
LocationID string `db:"location_id"`
Date time.Time `db:"date"`
Fajr string `db:"fajr"`
Sunrise string `db:"sunrise"`
Dhuhr string `db:"dhuhr"`
Asr string `db:"asr"`
Maghrib string `db:"maghrib"`
Isha string `db:"isha"`
}
func (r prayerTimesRow) toDomain() prayer.Times {
return prayer.Times{
Date: r.Date,
Fajr: r.Fajr,
Sunrise: r.Sunrise,
Dhuhr: r.Dhuhr,
Asr: r.Asr,
Maghrib: r.Maghrib,
Isha: r.Isha,
}
}
func (p Provider) saveTimes(ctx context.Context, locationID string, times []prayer.Times) error {
providerID, err := p.saveProvider(ctx, p.provider.Name())
if err != nil {
return err
}
rows := lo.Map(times, func(item prayer.Times, _ int) prayerTimesRow {
return prayerTimesRow{
ProviderID: providerID,
LocationID: locationID,
Date: item.Date,
Fajr: item.Fajr,
Sunrise: item.Sunrise,
Dhuhr: item.Dhuhr,
Asr: item.Asr,
Maghrib: item.Maghrib,
Isha: item.Isha,
}
})
q := p.db.
Insert("prayer_times").
OnConflict(goqu.DoNothing()).
Rows(rows)
if _, err := q.Executor().ExecContext(ctx); err != nil {
return fmt.Errorf("failed to save times: %w", err)
}
return nil
}
func (p Provider) loadTimes(ctx context.Context, locationID string) ([]prayer.Times, error) {
q := p.db.
From(goqu.T("prayer_times").As("pt")).
Join(goqu.T("providers").As("p"), goqu.On(goqu.I("p.id").Eq(goqu.I("pt.provider_id")))).
Where(
goqu.I("p.name").Eq(p.provider.Name()),
goqu.I("pt.location_id").Eq(locationID),
goqu.I("pt.date").Gte(p.clockFunc()),
).
Limit(100)
var rows []prayerTimesRow
if err := q.ScanStructsContext(ctx, &rows); err != nil {
return nil, fmt.Errorf("failed to scan times: %w", err)
}
return lo.Map(rows, func(row prayerTimesRow, _ int) prayer.Times {
return row.toDomain()
}), nil
}
func (p Provider) saveProvider(ctx context.Context, name string) (int64, error) {
q := p.db.Insert("providers").
OnConflict(goqu.DoUpdate("name", goqu.Record{"name": name})).
Rows(goqu.Record{"name": name}).
Returning("id")
var id int64
_, err := q.Executor().ScanValContext(ctx, &id)
if err != nil {
return 0, fmt.Errorf("failed to insert provider: %w", err)
}
return id, nil
}