entry_query_builder.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  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. )
  13. // EntryQueryBuilder builds a SQL query to fetch entries.
  14. type EntryQueryBuilder struct {
  15. store *Storage
  16. feedID int64
  17. userID int64
  18. categoryID int64
  19. status string
  20. notStatus string
  21. order string
  22. direction string
  23. limit int
  24. offset int
  25. entryID int64
  26. greaterThanEntryID int64
  27. entryIDs []int64
  28. before *time.Time
  29. starred bool
  30. }
  31. // WithStarred adds starred filter.
  32. func (e *EntryQueryBuilder) WithStarred() *EntryQueryBuilder {
  33. e.starred = true
  34. return e
  35. }
  36. // Before add condition base on the entry date.
  37. func (e *EntryQueryBuilder) Before(date *time.Time) *EntryQueryBuilder {
  38. e.before = date
  39. return e
  40. }
  41. // WithGreaterThanEntryID adds a condition > entryID.
  42. func (e *EntryQueryBuilder) WithGreaterThanEntryID(entryID int64) *EntryQueryBuilder {
  43. e.greaterThanEntryID = entryID
  44. return e
  45. }
  46. // WithEntryIDs adds a condition to fetch only the given entry IDs.
  47. func (e *EntryQueryBuilder) WithEntryIDs(entryIDs []int64) *EntryQueryBuilder {
  48. e.entryIDs = entryIDs
  49. return e
  50. }
  51. // WithEntryID set the entryID.
  52. func (e *EntryQueryBuilder) WithEntryID(entryID int64) *EntryQueryBuilder {
  53. e.entryID = entryID
  54. return e
  55. }
  56. // WithFeedID set the feedID.
  57. func (e *EntryQueryBuilder) WithFeedID(feedID int64) *EntryQueryBuilder {
  58. e.feedID = feedID
  59. return e
  60. }
  61. // WithCategoryID set the categoryID.
  62. func (e *EntryQueryBuilder) WithCategoryID(categoryID int64) *EntryQueryBuilder {
  63. e.categoryID = categoryID
  64. return e
  65. }
  66. // WithStatus set the entry status.
  67. func (e *EntryQueryBuilder) WithStatus(status string) *EntryQueryBuilder {
  68. e.status = status
  69. return e
  70. }
  71. // WithoutStatus set the entry status that should not be returned.
  72. func (e *EntryQueryBuilder) WithoutStatus(status string) *EntryQueryBuilder {
  73. e.notStatus = status
  74. return e
  75. }
  76. // WithOrder set the sorting order.
  77. func (e *EntryQueryBuilder) WithOrder(order string) *EntryQueryBuilder {
  78. e.order = order
  79. return e
  80. }
  81. // WithDirection set the sorting direction.
  82. func (e *EntryQueryBuilder) WithDirection(direction string) *EntryQueryBuilder {
  83. e.direction = direction
  84. return e
  85. }
  86. // WithLimit set the limit.
  87. func (e *EntryQueryBuilder) WithLimit(limit int) *EntryQueryBuilder {
  88. e.limit = limit
  89. return e
  90. }
  91. // WithOffset set the offset.
  92. func (e *EntryQueryBuilder) WithOffset(offset int) *EntryQueryBuilder {
  93. e.offset = offset
  94. return e
  95. }
  96. // CountEntries count the number of entries that match the condition.
  97. func (e *EntryQueryBuilder) CountEntries() (count int, err error) {
  98. defer timer.ExecutionTime(
  99. time.Now(),
  100. fmt.Sprintf("[EntryQueryBuilder:CountEntries] userID=%d, feedID=%d, status=%s", e.userID, e.feedID, e.status),
  101. )
  102. query := `SELECT count(*) FROM entries e LEFT JOIN feeds f ON f.id=e.feed_id WHERE %s`
  103. args, condition := e.buildCondition()
  104. err = e.store.db.QueryRow(fmt.Sprintf(query, condition), args...).Scan(&count)
  105. if err != nil {
  106. return 0, fmt.Errorf("unable to count entries: %v", err)
  107. }
  108. return count, nil
  109. }
  110. // GetEntry returns a single entry that match the condition.
  111. func (e *EntryQueryBuilder) GetEntry() (*model.Entry, error) {
  112. e.limit = 1
  113. entries, err := e.GetEntries()
  114. if err != nil {
  115. return nil, err
  116. }
  117. if len(entries) != 1 {
  118. return nil, nil
  119. }
  120. entries[0].Enclosures, err = e.store.GetEnclosures(entries[0].ID)
  121. if err != nil {
  122. return nil, err
  123. }
  124. return entries[0], nil
  125. }
  126. // GetEntries returns a list of entries that match the condition.
  127. func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
  128. debugStr := "[EntryQueryBuilder:GetEntries] userID=%d, feedID=%d, categoryID=%d, status=%s, order=%s, direction=%s, offset=%d, limit=%d"
  129. defer timer.ExecutionTime(time.Now(), fmt.Sprintf(debugStr, e.userID, e.feedID, e.categoryID, e.status, e.order, e.direction, e.offset, e.limit))
  130. query := `
  131. SELECT
  132. e.id, e.user_id, e.feed_id, e.hash, e.published_at at time zone u.timezone, e.title,
  133. e.url, e.author, e.content, e.status, e.starred,
  134. f.title as feed_title, f.feed_url, f.site_url, f.checked_at,
  135. f.category_id, c.title as category_title, f.scraper_rules, f.rewrite_rules, f.crawler,
  136. fi.icon_id
  137. FROM entries e
  138. LEFT JOIN feeds f ON f.id=e.feed_id
  139. LEFT JOIN categories c ON c.id=f.category_id
  140. LEFT JOIN feed_icons fi ON fi.feed_id=f.id
  141. LEFT JOIN users u ON u.id=e.user_id
  142. WHERE %s %s
  143. `
  144. args, conditions := e.buildCondition()
  145. query = fmt.Sprintf(query, 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 timer.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) *EntryQueryBuilder {
  284. return &EntryQueryBuilder{
  285. store: store,
  286. userID: userID,
  287. starred: false,
  288. }
  289. }