ai: generate postgres package draft

This commit is contained in:
nkanaev
2026-06-13 14:46:34 +01:00
parent eef482d81d
commit 5c2d9bfc4c
7 changed files with 954 additions and 0 deletions

View File

@@ -0,0 +1,332 @@
package postgres
import (
"cmp"
"database/sql/driver"
"encoding/json"
"fmt"
"log"
"slices"
"strings"
"time"
"github.com/nkanaev/yarr/src/storage/model"
)
type MediaLinks model.MediaLinks
func (m *MediaLinks) Scan(src any) error {
switch data := src.(type) {
case []byte:
return json.Unmarshal(data, m)
case string:
return json.Unmarshal([]byte(data), m)
default:
return nil
}
}
func (m MediaLinks) Value() (driver.Value, error) {
return json.Marshal(m)
}
func (s *PostgresStorage) CreateItems(items []model.Item) bool {
tx, err := s.db.Begin()
if err != nil {
log.Print(err)
return false
}
now := time.Now().UTC()
slices.SortStableFunc(items, func(a, b model.Item) int {
sa := a.Date.Format(time.RFC3339) + "::" + a.GUID
sb := b.Date.Format(time.RFC3339) + "::" + b.GUID
return cmp.Compare(sa, sb)
})
for _, item := range items {
_, err = tx.Exec(`
insert into items (
guid, feed_id, title, link, date,
content, media_links,
date_arrived, last_arrived, status
)
values (
$1, $2, $3, $4, $5,
$6, $7,
$8, $9, $10
)
on conflict (feed_id, guid) do update set
last_arrived = excluded.last_arrived`,
item.GUID,
item.FeedId,
item.Title,
item.Link,
item.Date,
item.Content,
MediaLinks(item.MediaLinks),
now,
now,
model.UNREAD,
)
if err != nil {
log.Print(err)
if err = tx.Rollback(); err != nil {
log.Print(err)
return false
}
return false
}
}
if err = tx.Commit(); err != nil {
log.Print(err)
return false
}
return true
}
func listQueryPredicate(filter model.ItemFilter, newestFirst bool) (string, []any) {
cond := make([]string, 0)
args := make([]any, 0)
n := 0
next := func() int {
n++
return n
}
if filter.FolderID != nil {
cond = append(cond, fmt.Sprintf("i.feed_id in (select id from feeds where folder_id = $%d)", next()))
args = append(args, *filter.FolderID)
}
if filter.FeedID != nil {
cond = append(cond, fmt.Sprintf("i.feed_id = $%d", next()))
args = append(args, *filter.FeedID)
}
if filter.Status != nil {
cond = append(cond, fmt.Sprintf("i.status = $%d", next()))
args = append(args, *filter.Status)
}
if filter.Search != nil {
words := strings.Fields(*filter.Search)
terms := make([]string, len(words))
for idx, word := range words {
terms[idx] = word + ":*"
}
cond = append(cond, fmt.Sprintf(
"i.search @@ to_tsquery('english', $%d)", next(),
))
args = append(args, strings.Join(terms, " & "))
}
if filter.After != nil {
compare := ">"
if newestFirst {
compare = "<"
}
cond = append(cond, fmt.Sprintf(
"(i.date, i.id) %s (select date, id from items where id = $%d)",
compare, next(),
))
args = append(args, *filter.After)
}
if filter.IDs != nil && len(*filter.IDs) > 0 {
placeholders := make([]string, len(*filter.IDs))
for i, id := range *filter.IDs {
placeholders[i] = fmt.Sprintf("$%d", next())
args = append(args, id)
}
cond = append(cond, "i.id in ("+strings.Join(placeholders, ",")+")")
}
if filter.SinceID != nil {
cond = append(cond, fmt.Sprintf("i.id > $%d", next()))
args = append(args, filter.SinceID)
}
if filter.MaxID != nil {
cond = append(cond, fmt.Sprintf("i.id < $%d", next()))
args = append(args, filter.MaxID)
}
if filter.Before != nil {
cond = append(cond, fmt.Sprintf("i.date < $%d", next()))
args = append(args, filter.Before)
}
predicate := "1"
if len(cond) > 0 {
predicate = strings.Join(cond, " and ")
}
return predicate, args
}
func (s *PostgresStorage) CountItems() int {
var count int
err := s.db.QueryRow(`select count(*) from items`).Scan(&count)
if err != nil {
log.Print(err)
return 0
}
return count
}
func (s *PostgresStorage) ListItems(
filter model.ItemFilter,
limit int,
newestFirst bool,
withContent bool,
) []model.Item {
predicate, args := listQueryPredicate(filter, newestFirst)
result := make([]model.Item, 0)
order := "date desc, id desc"
if !newestFirst {
order = "date asc, id asc"
}
if filter.IDs != nil || filter.SinceID != nil {
order = "i.id asc"
}
if filter.MaxID != nil {
order = "i.id desc"
}
selectCols := "i.id, i.guid, i.feed_id, i.title, i.link, i.date, i.status, i.media_links"
if withContent {
selectCols += ", i.content"
} else {
selectCols += ", '' as content"
}
query := fmt.Sprintf(`
select %s
from items i
where %s
order by %s
limit %d
`, selectCols, predicate, order, limit)
rows, err := s.db.Query(query, args...)
if err != nil {
log.Print(err)
return result
}
defer rows.Close()
for rows.Next() {
var x model.Item
err = rows.Scan(
&x.Id, &x.GUID, &x.FeedId,
&x.Title, &x.Link, &x.Date,
&x.Status, (*MediaLinks)(&x.MediaLinks), &x.Content,
)
if err != nil {
log.Print(err)
return result
}
result = append(result, x)
}
return result
}
func (s *PostgresStorage) GetItem(id int64) *model.Item {
i := &model.Item{}
err := s.db.QueryRow(`
select
i.id, i.guid, i.feed_id, i.title, i.link, i.content,
i.date, i.status, i.media_links
from items i
where i.id = $1
`, id).Scan(
&i.Id, &i.GUID, &i.FeedId, &i.Title, &i.Link, &i.Content,
&i.Date, &i.Status, (*MediaLinks)(&i.MediaLinks),
)
if err != nil {
log.Print(err)
return nil
}
return i
}
func (s *PostgresStorage) UpdateItemStatus(item_id int64, status model.ItemStatus) bool {
_, err := s.db.Exec(`update items set status = $2 where id = $1`,
item_id,
status,
)
return err == nil
}
func (s *PostgresStorage) MarkItemsRead(filter model.MarkFilter) bool {
predicate, args := listQueryPredicate(model.ItemFilter{
FolderID: filter.FolderID,
FeedID: filter.FeedID,
Before: filter.Before,
}, false)
query := fmt.Sprintf(`
update items as i set status = %d
where %s and i.status != %d
`, model.READ, predicate, model.STARRED)
_, err := s.db.Exec(query, args...)
if err != nil {
log.Print(err)
}
return err == nil
}
func (s *PostgresStorage) FeedStats() []model.FeedStat {
result := make([]model.FeedStat, 0)
rows, err := s.db.Query(fmt.Sprintf(`
select
feed_id,
sum(case status when %d then 1 else 0 end),
sum(case status when %d then 1 else 0 end)
from items
group by feed_id
`, model.UNREAD, model.STARRED))
if err != nil {
log.Print(err)
return result
}
defer rows.Close()
for rows.Next() {
stat := model.FeedStat{}
rows.Scan(&stat.FeedId, &stat.UnreadCount, &stat.StarredCount)
result = append(result, stat)
}
return result
}
var (
itemsKeepSize = 50
itemsKeepDays = 90
)
func (s *PostgresStorage) DeleteOldItems() {
keepDaysLimit := fmt.Sprintf("-%d days", itemsKeepDays)
result, err := s.db.Exec(`
delete from items
where id in (
select id
from (
select
id,
row_number() over (partition by feed_id order by date desc) as rn,
last_arrived,
max(last_arrived) over (partition by feed_id) as max_la
from items
where status != $1
) sub
where rn > $2
and last_arrived < max_la + $3::interval
)`,
model.STARRED,
itemsKeepSize,
keepDaysLimit,
)
if err != nil {
log.Print(err)
return
}
numDeleted, err := result.RowsAffected()
if err == nil && numDeleted > 0 {
log.Printf("Deleted %d old items", numDeleted)
}
}