entry_query_builder.go 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  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/model"
  11. "github.com/miniflux/miniflux/timer"
  12. "github.com/miniflux/miniflux/timezone"
  13. )
  14. // EntryQueryBuilder builds a SQL query to fetch entries.
  15. type EntryQueryBuilder struct {
  16. store *Storage
  17. feedID int64
  18. userID int64
  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 timer.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 timer.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 u.timezone, e.title,
  134. e.url, e.comments_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. u.timezone
  139. FROM entries e
  140. LEFT JOIN feeds f ON f.id=e.feed_id
  141. LEFT JOIN categories c ON c.id=f.category_id
  142. LEFT JOIN feed_icons fi ON fi.feed_id=f.id
  143. LEFT JOIN users u ON u.id=e.user_id
  144. WHERE %s %s
  145. `
  146. args, conditions := e.buildCondition()
  147. query = fmt.Sprintf(query, conditions, e.buildSorting())
  148. // log.Println(query)
  149. rows, err := e.store.db.Query(query, args...)
  150. if err != nil {
  151. return nil, fmt.Errorf("unable to get entries: %v", err)
  152. }
  153. defer rows.Close()
  154. entries := make(model.Entries, 0)
  155. for rows.Next() {
  156. var entry model.Entry
  157. var iconID interface{}
  158. var tz string
  159. entry.Feed = &model.Feed{UserID: e.userID}
  160. entry.Feed.Category = &model.Category{UserID: e.userID}
  161. entry.Feed.Icon = &model.FeedIcon{}
  162. err := rows.Scan(
  163. &entry.ID,
  164. &entry.UserID,
  165. &entry.FeedID,
  166. &entry.Hash,
  167. &entry.Date,
  168. &entry.Title,
  169. &entry.URL,
  170. &entry.CommentsURL,
  171. &entry.Author,
  172. &entry.Content,
  173. &entry.Status,
  174. &entry.Starred,
  175. &entry.Feed.Title,
  176. &entry.Feed.FeedURL,
  177. &entry.Feed.SiteURL,
  178. &entry.Feed.CheckedAt,
  179. &entry.Feed.Category.ID,
  180. &entry.Feed.Category.Title,
  181. &entry.Feed.ScraperRules,
  182. &entry.Feed.RewriteRules,
  183. &entry.Feed.Crawler,
  184. &iconID,
  185. &tz,
  186. )
  187. if err != nil {
  188. return nil, fmt.Errorf("unable to fetch entry row: %v", err)
  189. }
  190. if iconID == nil {
  191. entry.Feed.Icon.IconID = 0
  192. } else {
  193. entry.Feed.Icon.IconID = iconID.(int64)
  194. }
  195. // Make sure that timestamp fields contains timezone information (API)
  196. entry.Date = timezone.Convert(tz, entry.Date)
  197. entry.Feed.CheckedAt = timezone.Convert(tz, entry.Feed.CheckedAt)
  198. entry.Feed.ID = entry.FeedID
  199. entry.Feed.Icon.FeedID = entry.FeedID
  200. entries = append(entries, &entry)
  201. }
  202. return entries, nil
  203. }
  204. // GetEntryIDs returns a list of entry IDs that match the condition.
  205. func (e *EntryQueryBuilder) GetEntryIDs() ([]int64, error) {
  206. debugStr := "[EntryQueryBuilder:GetEntryIDs] userID=%d, feedID=%d, categoryID=%d, status=%s, order=%s, direction=%s, offset=%d, limit=%d"
  207. defer timer.ExecutionTime(time.Now(), fmt.Sprintf(debugStr, e.userID, e.feedID, e.categoryID, e.status, e.order, e.direction, e.offset, e.limit))
  208. query := `
  209. SELECT
  210. e.id
  211. FROM entries e
  212. LEFT JOIN feeds f ON f.id=e.feed_id
  213. WHERE %s %s
  214. `
  215. args, conditions := e.buildCondition()
  216. query = fmt.Sprintf(query, conditions, e.buildSorting())
  217. // log.Println(query)
  218. rows, err := e.store.db.Query(query, args...)
  219. if err != nil {
  220. return nil, fmt.Errorf("unable to get entries: %v", err)
  221. }
  222. defer rows.Close()
  223. var entryIDs []int64
  224. for rows.Next() {
  225. var entryID int64
  226. err := rows.Scan(&entryID)
  227. if err != nil {
  228. return nil, fmt.Errorf("unable to fetch entry row: %v", err)
  229. }
  230. entryIDs = append(entryIDs, entryID)
  231. }
  232. return entryIDs, nil
  233. }
  234. func (e *EntryQueryBuilder) buildCondition() ([]interface{}, string) {
  235. args := []interface{}{e.userID}
  236. conditions := []string{"e.user_id = $1"}
  237. if e.categoryID != 0 {
  238. conditions = append(conditions, fmt.Sprintf("f.category_id=$%d", len(args)+1))
  239. args = append(args, e.categoryID)
  240. }
  241. if e.feedID != 0 {
  242. conditions = append(conditions, fmt.Sprintf("e.feed_id=$%d", len(args)+1))
  243. args = append(args, e.feedID)
  244. }
  245. if e.entryID != 0 {
  246. conditions = append(conditions, fmt.Sprintf("e.id=$%d", len(args)+1))
  247. args = append(args, e.entryID)
  248. }
  249. if e.greaterThanEntryID != 0 {
  250. conditions = append(conditions, fmt.Sprintf("e.id > $%d", len(args)+1))
  251. args = append(args, e.greaterThanEntryID)
  252. }
  253. if e.entryIDs != nil {
  254. conditions = append(conditions, fmt.Sprintf("e.id=ANY($%d)", len(args)+1))
  255. args = append(args, pq.Array(e.entryIDs))
  256. }
  257. if e.status != "" {
  258. conditions = append(conditions, fmt.Sprintf("e.status=$%d", len(args)+1))
  259. args = append(args, e.status)
  260. }
  261. if e.notStatus != "" {
  262. conditions = append(conditions, fmt.Sprintf("e.status != $%d", len(args)+1))
  263. args = append(args, e.notStatus)
  264. }
  265. if e.before != nil {
  266. conditions = append(conditions, fmt.Sprintf("e.published_at < $%d", len(args)+1))
  267. args = append(args, e.before)
  268. }
  269. if e.starred {
  270. conditions = append(conditions, "e.starred is true")
  271. }
  272. return args, strings.Join(conditions, " AND ")
  273. }
  274. func (e *EntryQueryBuilder) buildSorting() string {
  275. var queries []string
  276. if e.order != "" {
  277. queries = append(queries, fmt.Sprintf(`ORDER BY "%s"`, e.order))
  278. }
  279. if e.direction != "" {
  280. queries = append(queries, fmt.Sprintf(`%s`, e.direction))
  281. }
  282. if e.limit != 0 {
  283. queries = append(queries, fmt.Sprintf(`LIMIT %d`, e.limit))
  284. }
  285. if e.offset != 0 {
  286. queries = append(queries, fmt.Sprintf(`OFFSET %d`, e.offset))
  287. }
  288. return strings.Join(queries, " ")
  289. }
  290. // NewEntryQueryBuilder returns a new EntryQueryBuilder.
  291. func NewEntryQueryBuilder(store *Storage, userID int64) *EntryQueryBuilder {
  292. return &EntryQueryBuilder{
  293. store: store,
  294. userID: userID,
  295. starred: false,
  296. }
  297. }