Compare commits

...

3 Commits

Author SHA1 Message Date
Bel LaPointe
88ab880a8c not used err 2025-04-27 11:13:09 -06:00
Bel LaPointe
e6d9e356ca hm sqlite transactions dont play with begin commit 2025-04-27 11:12:46 -06:00
Bel LaPointe
b85df7bd31 db can query 1 level nested dotnotation 2025-04-27 11:04:17 -06:00
5 changed files with 110 additions and 33 deletions

View File

@@ -28,6 +28,7 @@ func One(ctx context.Context) error {
if err != nil { if err != nil {
return err return err
} }
_ = feeds
return io.EOF return io.EOF
} }

View File

@@ -5,6 +5,7 @@ import (
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
"strings"
) )
func QueryOne[T any](ctx context.Context, q string, args ...any) (T, error) { func QueryOne[T any](ctx context.Context, q string, args ...any) (T, error) {
@@ -33,12 +34,33 @@ func Query[T any](ctx context.Context, q string, args ...any) ([]T, error) {
} }
scanners := func(columns []string) ([]any, error) { scanners := func(columns []string) ([]any, error) {
s := make([]any, len(columns)) s := make([]any, len(columns))
for i, k := range columns { i := 0
v, ok := m[k] for i < len(columns) {
if !ok { k := columns[i]
return nil, fmt.Errorf("cannot scan column %s to %T (%+v)", k, a, m) if strings.Contains(k, ".") {
columns := strings.SplitN(k, ".", 2)
m2, ok := m[columns[0]]
if !ok {
return nil, fmt.Errorf("no column %s in %T (%+v)", columns[0], a, m)
}
m3, ok := m2.(map[string]any)
if !ok {
return nil, fmt.Errorf("cannot scan subfield %s of %s of %T (%+v)", columns[1], columns[0], a, m)
}
v, ok := m3[columns[1]]
if !ok {
return nil, fmt.Errorf("no subfield %s of %s of %T (%+v)", columns[1], columns[0], a, m)
}
s[i] = &v
i += 1
} else {
v, ok := m[k]
if !ok {
return nil, fmt.Errorf("no column %s in %T (%+v)", k, a, m)
}
s[i] = &v
i += 1
} }
s[i] = &v
} }
return s, nil return s, nil
} }
@@ -67,7 +89,21 @@ func Query[T any](ctx context.Context, q string, args ...any) ([]T, error) {
m := map[string]any{} m := map[string]any{}
for i, column := range columns { for i, column := range columns {
m[column] = scanners[i] if !strings.Contains(column, ".") {
m[column] = scanners[i]
} else {
columns := strings.SplitN(column, ".", 2)
m2, ok := m[columns[0]]
if !ok {
m2 = map[string]any{}
}
m3, ok := m2.(map[string]any)
if !ok {
return fmt.Errorf("%s is not a submap", columns[0])
}
m3[columns[1]] = scanners[i]
m[columns[0]] = m3
}
} }
var a T var a T

View File

@@ -46,4 +46,15 @@ func TestDB(t *testing.T) {
} else if gots[1].K != "b" { } else if gots[1].K != "b" {
t.Errorf("expected [1]='b' but got %q", gots[1].K) t.Errorf("expected [1]='b' but got %q", gots[1].K)
} }
type NestedResult struct {
Nest struct {
K string `json:"k"`
}
}
if got, err := db.QueryOne[NestedResult](ctx, `SELECT k AS "Nest.k" FROM test WHERE k='a'`); err != nil {
t.Errorf("failed nested query one: %v", err)
} else if got.Nest.K != "a" {
t.Errorf("bad nested query one: %+v", got)
}
} }

View File

@@ -128,20 +128,20 @@ func (f *Feeds) Get(ctx context.Context, id string) (Feed, error) {
versions.created_at AS "Version.Created", versions.created_at AS "Version.Created",
versions.url AS "Version.URL", versions.url AS "Version.URL",
versions.cron AS "Version.Cron", versions.cron AS "Version.Cron",
( (
SELECT executed_at SELECT executed_at
FROM "feed.executions" FROM "feed.executions"
WHERE entries_id = entry.ID WHERE entries_id = entry.ID
ORDER BY executed_at DESC ORDER BY executed_at DESC
LIMIT 1 LIMIT 1
) AS "Execution.Executed", ) AS "Execution.Executed",
( (
SELECT versions_created_at SELECT versions_created_at
FROM "feed.executions" FROM "feed.executions"
WHERE entries_id = entry.ID WHERE entries_id = entry.ID
ORDER BY executed_at DESC ORDER BY executed_at DESC
LIMIT 1 LIMIT 1
) AS "Execution.Version" ) AS "Execution.Version"
FROM entry FROM entry
JOIN "feed.versions" version_entries_id ON JOIN "feed.versions" version_entries_id ON
version_entries_id.entries_id=entry.ID version_entries_id.entries_id=entry.ID
@@ -196,22 +196,21 @@ func (f *Feeds) oldGet(ctx context.Context, id string) (Feed, error) {
func (f *Feeds) Insert(ctx context.Context, url, cron string) (string, error) { func (f *Feeds) Insert(ctx context.Context, url, cron string) (string, error) {
now := time.Now() now := time.Now()
id := uuid.New().String() id := uuid.New().String()
q := ` return id, db.Exec(ctx, `
BEGIN; BEGIN;
INSERT INTO "feed.entries" ( INSERT INTO "feed.entries" (
id, id,
created_at, created_at,
updated_at updated_at
) VALUES (?, ?, ?); ) VALUES ($1, $2, $3);
INSERT INTO "feed.versions" ( INSERT INTO "feed.versions" (
entries_id, entries_id,
created_at, created_at,
url, url,
cron cron
) VALUES (?, ?, ?, ?); ) VALUES ($4, $5, $6, $7);
COMMIT; COMMIT;
` `,
return id, db.Exec(ctx, q,
id, now, now, id, now, now,
id, now, url, cron, id, now, url, cron,
) )

View File

@@ -51,6 +51,36 @@ func TestFeeds(t *testing.T) {
if err != nil { if err != nil {
t.Fatal("cannot get:", err) t.Fatal("cannot get:", err)
} }
t.Errorf("%+v", got) t.Logf("%+v", got)
if got.Entry.ID == "" {
t.Error("no entry.id")
}
if got.Entry.Created.IsZero() {
t.Error("no entry.created")
}
if got.Entry.Updated.IsZero() {
t.Error("no entry.updated")
}
if !got.Entry.Deleted.IsZero() {
t.Error("entry.deleted")
}
if got.Version.Created.IsZero() {
t.Error("no version.created")
}
if got.Version.URL != "url" {
t.Error("no version.url")
}
if got.Version.Cron != "cron" {
t.Error("no version.cron")
}
if !got.Execution.Executed.IsZero() {
t.Error("execution.executed")
}
if !got.Execution.Version.IsZero() {
t.Error("execution.version")
}
}) })
} }