entry_query_builder.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  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/miniflux/miniflux2/helper"
  10. "github.com/miniflux/miniflux2/model"
  11. )
  12. // EntryQueryBuilder builds a SQL query to fetch entries.
  13. type EntryQueryBuilder struct {
  14. store *Storage
  15. feedID int64
  16. userID int64
  17. timezone string
  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. conditions []string
  27. args []interface{}
  28. }
  29. // WithCondition defines a new condition.
  30. func (e *EntryQueryBuilder) WithCondition(column, operator string, value interface{}) *EntryQueryBuilder {
  31. e.args = append(e.args, value)
  32. e.conditions = append(e.conditions, fmt.Sprintf("%s %s $%d", column, operator, len(e.args)+1))
  33. return e
  34. }
  35. // WithEntryID set the entryID.
  36. func (e *EntryQueryBuilder) WithEntryID(entryID int64) *EntryQueryBuilder {
  37. e.entryID = entryID
  38. return e
  39. }
  40. // WithFeedID set the feedID.
  41. func (e *EntryQueryBuilder) WithFeedID(feedID int64) *EntryQueryBuilder {
  42. e.feedID = feedID
  43. return e
  44. }
  45. // WithCategoryID set the categoryID.
  46. func (e *EntryQueryBuilder) WithCategoryID(categoryID int64) *EntryQueryBuilder {
  47. e.categoryID = categoryID
  48. return e
  49. }
  50. // WithStatus set the entry status.
  51. func (e *EntryQueryBuilder) WithStatus(status string) *EntryQueryBuilder {
  52. e.status = status
  53. return e
  54. }
  55. // WithoutStatus set the entry status that should not be returned.
  56. func (e *EntryQueryBuilder) WithoutStatus(status string) *EntryQueryBuilder {
  57. e.notStatus = status
  58. return e
  59. }
  60. // WithOrder set the sorting order.
  61. func (e *EntryQueryBuilder) WithOrder(order string) *EntryQueryBuilder {
  62. e.order = order
  63. return e
  64. }
  65. // WithDirection set the sorting direction.
  66. func (e *EntryQueryBuilder) WithDirection(direction string) *EntryQueryBuilder {
  67. e.direction = direction
  68. return e
  69. }
  70. // WithLimit set the limit.
  71. func (e *EntryQueryBuilder) WithLimit(limit int) *EntryQueryBuilder {
  72. e.limit = limit
  73. return e
  74. }
  75. // WithOffset set the offset.
  76. func (e *EntryQueryBuilder) WithOffset(offset int) *EntryQueryBuilder {
  77. e.offset = offset
  78. return e
  79. }
  80. // CountEntries count the number of entries that match the condition.
  81. func (e *EntryQueryBuilder) CountEntries() (count int, err error) {
  82. defer helper.ExecutionTime(
  83. time.Now(),
  84. fmt.Sprintf("[EntryQueryBuilder:CountEntries] userID=%d, feedID=%d, status=%s", e.userID, e.feedID, e.status),
  85. )
  86. query := `SELECT count(*) FROM entries e LEFT JOIN feeds f ON f.id=e.feed_id WHERE %s`
  87. args, condition := e.buildCondition()
  88. err = e.store.db.QueryRow(fmt.Sprintf(query, condition), args...).Scan(&count)
  89. if err != nil {
  90. return 0, fmt.Errorf("unable to count entries: %v", err)
  91. }
  92. return count, nil
  93. }
  94. // GetEntry returns a single entry that match the condition.
  95. func (e *EntryQueryBuilder) GetEntry() (*model.Entry, error) {
  96. e.limit = 1
  97. entries, err := e.GetEntries()
  98. if err != nil {
  99. return nil, err
  100. }
  101. if len(entries) != 1 {
  102. return nil, nil
  103. }
  104. entries[0].Enclosures, err = e.store.GetEnclosures(entries[0].ID)
  105. if err != nil {
  106. return nil, err
  107. }
  108. return entries[0], nil
  109. }
  110. // GetEntries returns a list of entries that match the condition.
  111. func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
  112. debugStr := "[EntryQueryBuilder:GetEntries] userID=%d, feedID=%d, categoryID=%d, status=%s, order=%s, direction=%s, offset=%d, limit=%d"
  113. defer helper.ExecutionTime(time.Now(), fmt.Sprintf(debugStr, e.userID, e.feedID, e.categoryID, e.status, e.order, e.direction, e.offset, e.limit))
  114. query := `
  115. SELECT
  116. 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,
  117. f.title as feed_title, f.feed_url, f.site_url, f.checked_at,
  118. f.category_id, c.title as category_title,
  119. fi.icon_id
  120. FROM entries e
  121. LEFT JOIN feeds f ON f.id=e.feed_id
  122. LEFT JOIN categories c ON c.id=f.category_id
  123. LEFT JOIN feed_icons fi ON fi.feed_id=f.id
  124. WHERE %s %s
  125. `
  126. args, conditions := e.buildCondition()
  127. query = fmt.Sprintf(query, e.timezone, conditions, e.buildSorting())
  128. // log.Println(query)
  129. rows, err := e.store.db.Query(query, args...)
  130. if err != nil {
  131. return nil, fmt.Errorf("unable to get entries: %v", err)
  132. }
  133. defer rows.Close()
  134. entries := make(model.Entries, 0)
  135. for rows.Next() {
  136. var entry model.Entry
  137. var iconID interface{}
  138. entry.Feed = &model.Feed{UserID: e.userID}
  139. entry.Feed.Category = &model.Category{UserID: e.userID}
  140. entry.Feed.Icon = &model.FeedIcon{}
  141. err := rows.Scan(
  142. &entry.ID,
  143. &entry.UserID,
  144. &entry.FeedID,
  145. &entry.Hash,
  146. &entry.Date,
  147. &entry.Title,
  148. &entry.URL,
  149. &entry.Author,
  150. &entry.Content,
  151. &entry.Status,
  152. &entry.Feed.Title,
  153. &entry.Feed.FeedURL,
  154. &entry.Feed.SiteURL,
  155. &entry.Feed.CheckedAt,
  156. &entry.Feed.Category.ID,
  157. &entry.Feed.Category.Title,
  158. &iconID,
  159. )
  160. if err != nil {
  161. return nil, fmt.Errorf("Unable to fetch entry row: %v", err)
  162. }
  163. if iconID == nil {
  164. entry.Feed.Icon.IconID = 0
  165. } else {
  166. entry.Feed.Icon.IconID = iconID.(int64)
  167. }
  168. entry.Feed.ID = entry.FeedID
  169. entry.Feed.Icon.FeedID = entry.FeedID
  170. entries = append(entries, &entry)
  171. }
  172. return entries, nil
  173. }
  174. func (e *EntryQueryBuilder) buildCondition() ([]interface{}, string) {
  175. args := []interface{}{e.userID}
  176. conditions := []string{"e.user_id = $1"}
  177. if len(e.conditions) > 0 {
  178. conditions = append(conditions, e.conditions...)
  179. args = append(args, e.args...)
  180. }
  181. if e.categoryID != 0 {
  182. conditions = append(conditions, fmt.Sprintf("f.category_id=$%d", len(args)+1))
  183. args = append(args, e.categoryID)
  184. }
  185. if e.feedID != 0 {
  186. conditions = append(conditions, fmt.Sprintf("e.feed_id=$%d", len(args)+1))
  187. args = append(args, e.feedID)
  188. }
  189. if e.entryID != 0 {
  190. conditions = append(conditions, fmt.Sprintf("e.id=$%d", len(args)+1))
  191. args = append(args, e.entryID)
  192. }
  193. if e.status != "" {
  194. conditions = append(conditions, fmt.Sprintf("e.status=$%d", len(args)+1))
  195. args = append(args, e.status)
  196. }
  197. if e.notStatus != "" {
  198. conditions = append(conditions, fmt.Sprintf("e.status != $%d", len(args)+1))
  199. args = append(args, e.notStatus)
  200. }
  201. return args, strings.Join(conditions, " AND ")
  202. }
  203. func (e *EntryQueryBuilder) buildSorting() string {
  204. var queries []string
  205. if e.order != "" {
  206. queries = append(queries, fmt.Sprintf(`ORDER BY "%s"`, e.order))
  207. }
  208. if e.direction != "" {
  209. queries = append(queries, fmt.Sprintf(`%s`, e.direction))
  210. }
  211. if e.limit != 0 {
  212. queries = append(queries, fmt.Sprintf(`LIMIT %d`, e.limit))
  213. }
  214. if e.offset != 0 {
  215. queries = append(queries, fmt.Sprintf(`OFFSET %d`, e.offset))
  216. }
  217. return strings.Join(queries, " ")
  218. }
  219. // NewEntryQueryBuilder returns a new EntryQueryBuilder.
  220. func NewEntryQueryBuilder(store *Storage, userID int64, timezone string) *EntryQueryBuilder {
  221. return &EntryQueryBuilder{
  222. store: store,
  223. userID: userID,
  224. timezone: timezone,
  225. }
  226. }