entry_query_builder.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. // Copyright 2017 Frédéric Guillot. All rights reserved.
  2. // Use of this source code is governed by the Apache 2.0
  3. // license that can be found in the LICENSE file.
  4. package storage
  5. import (
  6. "fmt"
  7. "strings"
  8. "time"
  9. "github.com/lib/pq"
  10. "github.com/miniflux/miniflux/helper"
  11. "github.com/miniflux/miniflux/model"
  12. )
  13. // EntryQueryBuilder builds a SQL query to fetch entries.
  14. type EntryQueryBuilder struct {
  15. store *Storage
  16. feedID int64
  17. userID int64
  18. timezone string
  19. categoryID int64
  20. status string
  21. notStatus string
  22. order string
  23. direction string
  24. limit int
  25. offset int
  26. entryID int64
  27. greaterThanEntryID int64
  28. entryIDs []int64
  29. before *time.Time
  30. starred bool
  31. }
  32. // WithStarred adds starred filter.
  33. func (e *EntryQueryBuilder) WithStarred() *EntryQueryBuilder {
  34. e.starred = true
  35. return e
  36. }
  37. // Before add condition base on the entry date.
  38. func (e *EntryQueryBuilder) Before(date *time.Time) *EntryQueryBuilder {
  39. e.before = date
  40. return e
  41. }
  42. // WithGreaterThanEntryID adds a condition > entryID.
  43. func (e *EntryQueryBuilder) WithGreaterThanEntryID(entryID int64) *EntryQueryBuilder {
  44. e.greaterThanEntryID = entryID
  45. return e
  46. }
  47. // WithEntryIDs adds a condition to fetch only the given entry IDs.
  48. func (e *EntryQueryBuilder) WithEntryIDs(entryIDs []int64) *EntryQueryBuilder {
  49. e.entryIDs = entryIDs
  50. return e
  51. }
  52. // WithEntryID set the entryID.
  53. func (e *EntryQueryBuilder) WithEntryID(entryID int64) *EntryQueryBuilder {
  54. e.entryID = entryID
  55. return e
  56. }
  57. // WithFeedID set the feedID.
  58. func (e *EntryQueryBuilder) WithFeedID(feedID int64) *EntryQueryBuilder {
  59. e.feedID = feedID
  60. return e
  61. }
  62. // WithCategoryID set the categoryID.
  63. func (e *EntryQueryBuilder) WithCategoryID(categoryID int64) *EntryQueryBuilder {
  64. e.categoryID = categoryID
  65. return e
  66. }
  67. // WithStatus set the entry status.
  68. func (e *EntryQueryBuilder) WithStatus(status string) *EntryQueryBuilder {
  69. e.status = status
  70. return e
  71. }
  72. // WithoutStatus set the entry status that should not be returned.
  73. func (e *EntryQueryBuilder) WithoutStatus(status string) *EntryQueryBuilder {
  74. e.notStatus = status
  75. return e
  76. }
  77. // WithOrder set the sorting order.
  78. func (e *EntryQueryBuilder) WithOrder(order string) *EntryQueryBuilder {
  79. e.order = order
  80. return e
  81. }
  82. // WithDirection set the sorting direction.
  83. func (e *EntryQueryBuilder) WithDirection(direction string) *EntryQueryBuilder {
  84. e.direction = direction
  85. return e
  86. }
  87. // WithLimit set the limit.
  88. func (e *EntryQueryBuilder) WithLimit(limit int) *EntryQueryBuilder {
  89. e.limit = limit
  90. return e
  91. }
  92. // WithOffset set the offset.
  93. func (e *EntryQueryBuilder) WithOffset(offset int) *EntryQueryBuilder {
  94. e.offset = offset
  95. return e
  96. }
  97. // CountEntries count the number of entries that match the condition.
  98. func (e *EntryQueryBuilder) CountEntries() (count int, err error) {
  99. defer helper.ExecutionTime(
  100. time.Now(),
  101. fmt.Sprintf("[EntryQueryBuilder:CountEntries] userID=%d, feedID=%d, status=%s", e.userID, e.feedID, e.status),
  102. )
  103. query := `SELECT count(*) FROM entries e LEFT JOIN feeds f ON f.id=e.feed_id WHERE %s`
  104. args, condition := e.buildCondition()
  105. err = e.store.db.QueryRow(fmt.Sprintf(query, condition), args...).Scan(&count)
  106. if err != nil {
  107. return 0, fmt.Errorf("unable to count entries: %v", err)
  108. }
  109. return count, nil
  110. }
  111. // GetEntry returns a single entry that match the condition.
  112. func (e *EntryQueryBuilder) GetEntry() (*model.Entry, error) {
  113. e.limit = 1
  114. entries, err := e.GetEntries()
  115. if err != nil {
  116. return nil, err
  117. }
  118. if len(entries) != 1 {
  119. return nil, nil
  120. }
  121. entries[0].Enclosures, err = e.store.GetEnclosures(entries[0].ID)
  122. if err != nil {
  123. return nil, err
  124. }
  125. return entries[0], nil
  126. }
  127. // GetEntries returns a list of entries that match the condition.
  128. func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
  129. debugStr := "[EntryQueryBuilder:GetEntries] userID=%d, feedID=%d, categoryID=%d, status=%s, order=%s, direction=%s, offset=%d, limit=%d"
  130. defer helper.ExecutionTime(time.Now(), fmt.Sprintf(debugStr, e.userID, e.feedID, e.categoryID, e.status, e.order, e.direction, e.offset, e.limit))
  131. query := `
  132. SELECT
  133. e.id, e.user_id, e.feed_id, e.hash, e.published_at at time zone '%s', e.title,
  134. e.url, e.author, e.content, e.status, e.starred,
  135. f.title as feed_title, f.feed_url, f.site_url, f.checked_at,
  136. f.category_id, c.title as category_title, f.scraper_rules, f.rewrite_rules, f.crawler,
  137. fi.icon_id
  138. FROM entries e
  139. LEFT JOIN feeds f ON f.id=e.feed_id
  140. LEFT JOIN categories c ON c.id=f.category_id
  141. LEFT JOIN feed_icons fi ON fi.feed_id=f.id
  142. WHERE %s %s
  143. `
  144. args, conditions := e.buildCondition()
  145. query = fmt.Sprintf(query, e.timezone, conditions, e.buildSorting())
  146. // log.Println(query)
  147. rows, err := e.store.db.Query(query, args...)
  148. if err != nil {
  149. return nil, fmt.Errorf("unable to get entries: %v", err)
  150. }
  151. defer rows.Close()
  152. entries := make(model.Entries, 0)
  153. for rows.Next() {
  154. var entry model.Entry
  155. var iconID interface{}
  156. entry.Feed = &model.Feed{UserID: e.userID}
  157. entry.Feed.Category = &model.Category{UserID: e.userID}
  158. entry.Feed.Icon = &model.FeedIcon{}
  159. err := rows.Scan(
  160. &entry.ID,
  161. &entry.UserID,
  162. &entry.FeedID,
  163. &entry.Hash,
  164. &entry.Date,
  165. &entry.Title,
  166. &entry.URL,
  167. &entry.Author,
  168. &entry.Content,
  169. &entry.Status,
  170. &entry.Starred,
  171. &entry.Feed.Title,
  172. &entry.Feed.FeedURL,
  173. &entry.Feed.SiteURL,
  174. &entry.Feed.CheckedAt,
  175. &entry.Feed.Category.ID,
  176. &entry.Feed.Category.Title,
  177. &entry.Feed.ScraperRules,
  178. &entry.Feed.RewriteRules,
  179. &entry.Feed.Crawler,
  180. &iconID,
  181. )
  182. if err != nil {
  183. return nil, fmt.Errorf("unable to fetch entry row: %v", err)
  184. }
  185. if iconID == nil {
  186. entry.Feed.Icon.IconID = 0
  187. } else {
  188. entry.Feed.Icon.IconID = iconID.(int64)
  189. }
  190. entry.Feed.ID = entry.FeedID
  191. entry.Feed.Icon.FeedID = entry.FeedID
  192. entries = append(entries, &entry)
  193. }
  194. return entries, nil
  195. }
  196. // GetEntryIDs returns a list of entry IDs that match the condition.
  197. func (e *EntryQueryBuilder) GetEntryIDs() ([]int64, error) {
  198. debugStr := "[EntryQueryBuilder:GetEntryIDs] userID=%d, feedID=%d, categoryID=%d, status=%s, order=%s, direction=%s, offset=%d, limit=%d"
  199. defer helper.ExecutionTime(time.Now(), fmt.Sprintf(debugStr, e.userID, e.feedID, e.categoryID, e.status, e.order, e.direction, e.offset, e.limit))
  200. query := `
  201. SELECT
  202. e.id
  203. FROM entries e
  204. LEFT JOIN feeds f ON f.id=e.feed_id
  205. WHERE %s %s
  206. `
  207. args, conditions := e.buildCondition()
  208. query = fmt.Sprintf(query, conditions, e.buildSorting())
  209. // log.Println(query)
  210. rows, err := e.store.db.Query(query, args...)
  211. if err != nil {
  212. return nil, fmt.Errorf("unable to get entries: %v", err)
  213. }
  214. defer rows.Close()
  215. var entryIDs []int64
  216. for rows.Next() {
  217. var entryID int64
  218. err := rows.Scan(&entryID)
  219. if err != nil {
  220. return nil, fmt.Errorf("unable to fetch entry row: %v", err)
  221. }
  222. entryIDs = append(entryIDs, entryID)
  223. }
  224. return entryIDs, nil
  225. }
  226. func (e *EntryQueryBuilder) buildCondition() ([]interface{}, string) {
  227. args := []interface{}{e.userID}
  228. conditions := []string{"e.user_id = $1"}
  229. if e.categoryID != 0 {
  230. conditions = append(conditions, fmt.Sprintf("f.category_id=$%d", len(args)+1))
  231. args = append(args, e.categoryID)
  232. }
  233. if e.feedID != 0 {
  234. conditions = append(conditions, fmt.Sprintf("e.feed_id=$%d", len(args)+1))
  235. args = append(args, e.feedID)
  236. }
  237. if e.entryID != 0 {
  238. conditions = append(conditions, fmt.Sprintf("e.id=$%d", len(args)+1))
  239. args = append(args, e.entryID)
  240. }
  241. if e.greaterThanEntryID != 0 {
  242. conditions = append(conditions, fmt.Sprintf("e.id > $%d", len(args)+1))
  243. args = append(args, e.greaterThanEntryID)
  244. }
  245. if e.entryIDs != nil {
  246. conditions = append(conditions, fmt.Sprintf("e.id=ANY($%d)", len(args)+1))
  247. args = append(args, pq.Array(e.entryIDs))
  248. }
  249. if e.status != "" {
  250. conditions = append(conditions, fmt.Sprintf("e.status=$%d", len(args)+1))
  251. args = append(args, e.status)
  252. }
  253. if e.notStatus != "" {
  254. conditions = append(conditions, fmt.Sprintf("e.status != $%d", len(args)+1))
  255. args = append(args, e.notStatus)
  256. }
  257. if e.before != nil {
  258. conditions = append(conditions, fmt.Sprintf("e.published_at < $%d", len(args)+1))
  259. args = append(args, e.before)
  260. }
  261. if e.starred {
  262. conditions = append(conditions, "e.starred is true")
  263. }
  264. return args, strings.Join(conditions, " AND ")
  265. }
  266. func (e *EntryQueryBuilder) buildSorting() string {
  267. var queries []string
  268. if e.order != "" {
  269. queries = append(queries, fmt.Sprintf(`ORDER BY "%s"`, e.order))
  270. }
  271. if e.direction != "" {
  272. queries = append(queries, fmt.Sprintf(`%s`, e.direction))
  273. }
  274. if e.limit != 0 {
  275. queries = append(queries, fmt.Sprintf(`LIMIT %d`, e.limit))
  276. }
  277. if e.offset != 0 {
  278. queries = append(queries, fmt.Sprintf(`OFFSET %d`, e.offset))
  279. }
  280. return strings.Join(queries, " ")
  281. }
  282. // NewEntryQueryBuilder returns a new EntryQueryBuilder.
  283. func NewEntryQueryBuilder(store *Storage, userID int64, timezone string) *EntryQueryBuilder {
  284. return &EntryQueryBuilder{
  285. store: store,
  286. userID: userID,
  287. timezone: timezone,
  288. starred: false,
  289. }
  290. }