entry_query_builder.go 8.6 KB

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