Compare commits

...

2 Commits

Author SHA1 Message Date
bel
61349beb85 no more cur versions table 2025-04-25 22:44:01 -06:00
bel
a2d1d17e23 feeds tests pass 2025-04-25 21:41:52 -06:00
4 changed files with 293 additions and 66 deletions

View File

@@ -2,9 +2,8 @@ package cron
import ( import (
"context" "context"
"fmt" "io"
"show-rss/src/db" "show-rss/src/feeds"
"strings"
"time" "time"
) )
@@ -25,71 +24,10 @@ func Main(ctx context.Context) error {
} }
func One(ctx context.Context) error { func One(ctx context.Context) error {
if err := initDB(ctx); err != nil { feeds, err := feeds.New(ctx)
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`)
if err != nil { if err != nil {
return err return err
} }
var v DatabaseVersion
if len(vs) > 0 {
v = vs[0]
}
mods := []string{ return io.EOF
`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
} }

View File

@@ -19,6 +19,7 @@ const ctxKey = "__db"
func Test(t *testing.T, ctx context.Context) context.Context { func Test(t *testing.T, ctx context.Context) context.Context {
p := path.Join(t.TempDir(), strings.ReplaceAll(t.Name()+".db", "/", "_")) p := path.Join(t.TempDir(), strings.ReplaceAll(t.Name()+".db", "/", "_"))
t.Logf("test db @ %s", p)
ctx, err := Inject(ctx, p) ctx, err := Inject(ctx, p)
if err != nil { if err != nil {
t.Fatalf("failed to inject db %s: %v", p, err) t.Fatalf("failed to inject db %s: %v", p, err)

232
src/feeds/db.go Normal file
View File

@@ -0,0 +1,232 @@
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.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) {
return db.QueryOne[Feed](ctx, `
WITH entry AS (
SELECT
id AS ID,
created_at AS Created,
updated_at AS Updated,
deleted_at AS Deleted
FROM "feed.entries"
WHERE id = ?
), WITH version AS (
SELECT
TODO
FROM "feed.versions"
), WITH execution AS (
SELECT
TODO
FROM "feed.executions"
WHERE entries_id = ?
ORDER BY executed DESC
LIMIT 1
)
SELECT
entry.ID AS "Entry.ID",
entry.Created AS "Entry.Created",
entry.Updated AS "Entry.Updated",
entry.Deleted AS "Entry.Deleted",
version.Created AS "Version.Created",
version.URL AS "Version.URL",
version.Cron AS "Version.Cron",
execution.Executed AS "",
FROM entry
FROM version
FROM execution
`)
}
func (f *Feeds) oldGet(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)
}

56
src/feeds/db_test.go Normal file
View File

@@ -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)
})
}