entry_query_builder.go 6.5 KB

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