mirror of
https://github.com/nkanaev/yarr.git
synced 2026-06-27 10:35:15 +00:00
ai: generate postgres package draft
This commit is contained in:
332
src/storage/postgres/item.go
Normal file
332
src/storage/postgres/item.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user