diff --git a/src/cmd/cron/main.go b/src/cmd/cron/main.go index 308c5e4..8764097 100644 --- a/src/cmd/cron/main.go +++ b/src/cmd/cron/main.go @@ -2,9 +2,8 @@ package cron import ( "context" - "fmt" - "show-rss/src/db" - "strings" + "io" + "show-rss/src/feeds" "time" ) @@ -25,71 +24,10 @@ func Main(ctx context.Context) error { } func One(ctx context.Context) error { - if err := initDB(ctx); err != nil { - return fmt.Errorf("failed init db: %w", err) - } - return nil -} - -func initDB(ctx context.Context) error { - if err := db.Exec(ctx, `CREATE TABLE IF NOT EXISTS database_version (v NUMBER, t TIMESTAMP)`); err != nil { - return fmt.Errorf("failed to create database_version table: %w", err) - } - - type DatabaseVersion struct { - V int `json:"v"` - T time.Time `json:"t"` - } - vs, err := db.Query[DatabaseVersion](ctx, `SELECT v, t FROM database_version ORDER BY v DESC LIMIT 1`) + feeds, err := feeds.New(ctx) if err != nil { return err } - var v DatabaseVersion - if len(vs) > 0 { - v = vs[0] - } - mods := []string{ - `CREATE TABLE "feed.entries" ( - id SERIAL PRIMARY KEY NOT NULL, - created_at TIMESTAMP NOT NULL, - updated_at TIMESTAMP NOT NULL, - deleted_at TIMESTAMP - )`, - - `CREATE TABLE "feed.versions" ( - entries_id NUMBER NOT NULL, - created_at TIMESTAMP NOT NULL, - PRIMARY KEY (entries_id, created_at), - FOREIGN KEY (entries_id) REFERENCES "feed.entries" (id) - )`, - `ALTER TABLE "feed.versions" ADD COLUMN url TEXT NOT NULL`, - `ALTER TABLE "feed.versions" ADD COLUMN cron TEXT NOT NULL DEFAULT '0 0 * * *'`, - - `CREATE TABLE "feed.current_versions" ( - entries_id NUMBER NOT NULL, - versions_created_at TIMESTAMP NOT NULL, - updated_at TIMESTAMP NOT NULL, - FOREIGN KEY (entries_id, versions_created_at) REFERENCES "feed.versions" (entries_id, created_at) - )`, - - `CREATE TABLE "feed.executions" ( - entries_id NUMBER, - versions_created_at TIMESTAMP NOT NULL, - executed_at TIMESTAMP, - FOREIGN KEY (entries_id, versions_created_at) REFERENCES "feed.versions" (entries_id, created_at) - )`, - } - mods = append([]string{""}, mods...) - for i := v.V + 1; i < len(mods); i++ { - q := mods[i] - q = strings.TrimSpace(q) - q = strings.TrimSuffix(q, ";") - q = fmt.Sprintf("BEGIN; %s; INSERT INTO database_version (v, t) VALUES (?, ?); COMMIT;", q) - if err := db.Exec(ctx, q, i, time.Now()); err != nil { - return fmt.Errorf("[%d] failed mod %s: %w", i, mods[i], err) - } - } - - return nil + return io.EOF } diff --git a/src/db/ctx.go b/src/db/ctx.go index 0909bc1..1ca8c30 100644 --- a/src/db/ctx.go +++ b/src/db/ctx.go @@ -19,6 +19,7 @@ const ctxKey = "__db" func Test(t *testing.T, ctx context.Context) context.Context { p := path.Join(t.TempDir(), strings.ReplaceAll(t.Name()+".db", "/", "_")) + t.Logf("test db @ %s", p) ctx, err := Inject(ctx, p) if err != nil { t.Fatalf("failed to inject db %s: %v", p, err) diff --git a/src/feeds/db.go b/src/feeds/db.go new file mode 100644 index 0000000..abd6fe0 --- /dev/null +++ b/src/feeds/db.go @@ -0,0 +1,202 @@ +package feeds + +import ( + "context" + "fmt" + "io" + "show-rss/src/db" + "strings" + "time" + + "github.com/google/uuid" +) + +type Feeds struct{} + +func New(ctx context.Context) (Feeds, error) { + return Feeds{}, initDB(ctx) +} + +func initDB(ctx context.Context) error { + if err := db.Exec(ctx, `CREATE TABLE IF NOT EXISTS database_version (v NUMBER, t TIMESTAMP)`); err != nil { + return fmt.Errorf("failed to create database_version table: %w", err) + } + + type DatabaseVersion struct { + V int `json:"v"` + T time.Time `json:"t"` + } + vs, err := db.Query[DatabaseVersion](ctx, `SELECT v, t FROM database_version ORDER BY v DESC LIMIT 1`) + if err != nil { + return err + } + var v DatabaseVersion + if len(vs) > 0 { + v = vs[0] + } + + mods := []string{ + `CREATE TABLE "feed.entries" ( + id TEXT PRIMARY KEY NOT NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + deleted_at TIMESTAMP + )`, + + `CREATE TABLE "feed.versions" ( + entries_id TEXT NOT NULL, + created_at TIMESTAMP NOT NULL, + PRIMARY KEY (entries_id, created_at), + FOREIGN KEY (entries_id) REFERENCES "feed.entries" (id) + )`, + `ALTER TABLE "feed.versions" ADD COLUMN url TEXT NOT NULL`, + `ALTER TABLE "feed.versions" ADD COLUMN cron TEXT NOT NULL DEFAULT '0 0 * * *'`, + + `CREATE TABLE "feed.current_versions" ( + entries_id TEXT NOT NULL, + versions_created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + FOREIGN KEY (entries_id, versions_created_at) REFERENCES "feed.versions" (entries_id, created_at) + )`, + + `CREATE TABLE "feed.executions" ( + entries_id TEXT, + versions_created_at TIMESTAMP NOT NULL, + executed_at TIMESTAMP, + FOREIGN KEY (entries_id, versions_created_at) REFERENCES "feed.versions" (entries_id, created_at) + )`, + } + mods = append([]string{""}, mods...) + for i := v.V + 1; i < len(mods); i++ { + q := mods[i] + q = strings.TrimSpace(q) + q = strings.TrimSuffix(q, ";") + q = fmt.Sprintf("BEGIN; %s; INSERT INTO database_version (v, t) VALUES (?, ?); COMMIT;", q) + if err := db.Exec(ctx, q, i, time.Now()); err != nil { + return fmt.Errorf("[%d] failed mod %s: %w", i, mods[i], err) + } + } + + return nil +} + +type ( + Feed struct { + Entry Entry + Version Version + Execution Execution + } + + Entry struct { + ID string + Created time.Time + Updated time.Time + Deleted time.Time + } + + Version struct { + Created time.Time + URL string + Cron string + } + + Execution struct { + Executed time.Time + VersionCreated time.Time + } +) + +func (f *Feeds) Get(ctx context.Context, id string) (Feed, error) { + entry, err := f.getEntry(ctx, id) + if err != nil { + return Feed{}, err + } + + version, err := db.QueryOne[Version](ctx, ` + SELECT + "feed.current_versions".versions_created_at AS Created, + "feed.current_versions" AS URL, + "feed.current_versions" AS Cron + FROM + "feed.current_versions" + JOIN + "feed.versions" versions_a ON + "feed.current_versions".entries_id=versions_a.entries_id + JOIN + "feed.versions" versions_b ON + "feed.current_versions".versions_created_at=versions_b.created_at + WHERE + "feed.current_versions".entries_id = ? + `, id) + if err != nil { + return Feed{}, err + } + + execution, err := db.QueryOne[Execution](ctx, ` + SELECT + "feed.executed_at" AS Executed, + "feed.versions_created_at" AS VersionsCreated + FROM + "feed.executions" + WHERE + "feed.executions".entries_id = ? + ORDER BY "feed.executions".executed_at DESC + `, id) + if err != nil { + return Feed{}, err + } + + return Feed{}, fmt.Errorf("%+v, %+v, %+v", entry, version, execution) +} + +func (f *Feeds) Insert(ctx context.Context, url, cron string) (string, error) { + now := time.Now() + id := uuid.New().String() + q := ` + BEGIN; + INSERT INTO "feed.entries" ( + id, + created_at, + updated_at + ) VALUES (?, ?, ?); + INSERT INTO "feed.versions" ( + entries_id, + created_at, + url, + cron + ) VALUES (?, ?, ?, ?); + INSERT INTO "feed.current_versions" ( + entries_id, + versions_created_at, + updated_at + ) VALUES (?, ?, ?); + COMMIT; + ` + return id, db.Exec(ctx, q, + id, now, now, + id, now, url, cron, + id, now, now, + ) +} + +func (f *Feeds) Update(ctx context.Context, id string, url, cron *string) error { + return io.EOF +} + +func (f *Feeds) Delete(ctx context.Context, id string) error { + return io.EOF +} + +func (f *Feeds) getEntry(ctx context.Context, id string) (Entry, error) { + return db.QueryOne[Entry](ctx, ` + SELECT + id AS ID, + created_at AS Created, + updated_at AS Updated, + deleted_at AS Deleted + FROM + "feed.entries" + WHERE + id = ? + `, id) +} diff --git a/src/feeds/db_test.go b/src/feeds/db_test.go new file mode 100644 index 0000000..6e67314 --- /dev/null +++ b/src/feeds/db_test.go @@ -0,0 +1,56 @@ +package feeds_test + +import ( + "context" + "show-rss/src/db" + "show-rss/src/feeds" + "strconv" + "testing" + "time" +) + +func TestFeeds(t *testing.T) { + ctx, can := context.WithTimeout(context.Background(), 5*time.Second) + defer can() + + t.Run("same ctx", func(t *testing.T) { + ctx := db.Test(t, ctx) + for i := 0; i < 2; i++ { + t.Run(strconv.Itoa(i), func(t *testing.T) { + if _, err := feeds.New(ctx); err != nil && ctx.Err() == nil { + t.Fatalf("failed %d: %v", i, err) + } + }) + } + }) + + t.Run("new ctx", func(t *testing.T) { + for i := 0; i < 2; i++ { + t.Run(strconv.Itoa(i), func(t *testing.T) { + if _, err := feeds.New(db.Test(t, ctx)); err != nil && ctx.Err() == nil { + t.Fatalf("failed %d: %v", i, err) + } + }) + } + }) + + t.Run("crud", func(t *testing.T) { + ctx := db.Test(t, ctx) + + f, err := feeds.New(ctx) + if err != nil && ctx.Err() == nil { + t.Fatalf("failed: %v", err) + } + + id, err := f.Insert(ctx, "url", "cron") + if err != nil { + t.Fatal("cannot insert:", err) + } + + got, err := f.Get(ctx, id) + if err != nil { + t.Fatal("cannot get:", err) + } + t.Errorf("%+v", got) + }) +}