Skip to content
This repository was archived by the owner on Jul 27, 2025. It is now read-only.

Commit 6d9bb7f

Browse files
authored
Temporary transactions page performance fix (#2372)
* Temporary transactions page performance fix * Fix Cursor bugs * More bugbot bug fixes
1 parent a5f1677 commit 6d9bb7f

File tree

1 file changed

+84
-26
lines changed

1 file changed

+84
-26
lines changed

app/controllers/transactions_controller.rb

Lines changed: 84 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ class TransactionsController < ApplicationController
33

44
before_action :store_params!, only: :index
55

6+
require "digest/md5"
7+
68
def new
79
super
810
@income_categories = Current.family.categories.incomes.alphabetically
@@ -15,35 +17,91 @@ def index
1517

1618
set_focused_record(transactions_query, params[:focused_record_id], default_per_page: 50)
1719

18-
@pagy, @transactions = pagy(
19-
transactions_query.includes(
20-
{ entry: :account },
21-
:category, :merchant, :tags,
22-
transfer_as_outflow: { inflow_transaction: { entry: :account } },
23-
transfer_as_inflow: { outflow_transaction: { entry: :account } }
24-
).reverse_chronological,
25-
limit: params[:per_page].presence || default_params[:per_page],
26-
params: ->(params) { params.except(:focused_record_id) }
20+
# ------------------------------------------------------------------
21+
# Cache the expensive includes & pagination block so the DB work only
22+
# runs when either the query params change *or* any entry has been
23+
# updated for the current family.
24+
# ------------------------------------------------------------------
25+
26+
latest_update_ts = Current.family.entries.maximum(:updated_at)&.utc&.to_i || 0
27+
28+
items_per_page = (params[:per_page].presence || default_params[:per_page]).to_i
29+
items_per_page = 1 if items_per_page <= 0
30+
31+
current_page = (params[:page].presence || default_params[:page]).to_i
32+
current_page = 1 if current_page <= 0
33+
34+
# Build a compact cache digest: sanitized filters + page info + a
35+
# token that changes on updates *or* deletions.
36+
entries_changed_token = [ latest_update_ts, Current.family.entries.count ].join(":")
37+
38+
digest_source = {
39+
q: @q, # processed & sanitised search params
40+
page: current_page, # requested page number
41+
per: items_per_page, # page size
42+
tok: entries_changed_token
43+
}.to_json
44+
45+
cache_key = Current.family.build_cache_key(
46+
"transactions_idx_#{Digest::MD5.hexdigest(digest_source)}"
2747
)
2848

29-
# -------------------------------------------------------------------
30-
# Cache totals
31-
# -------------------------------------------------------------------
32-
# Totals calculation is expensive (heavy SQL with grouping). We cache the
33-
# result keyed by:
34-
# • Family id
35-
# • The family-level cache key that already embeds entries.maximum(:updated_at)
36-
# • A digest of the current search params so each distinct filter set gets
37-
# its own cache entry.
38-
# When any entry is created/updated/deleted, the family cache key changes,
39-
# automatically invalidating all related totals.
40-
41-
params_digest = Digest::MD5.hexdigest(@q.to_json)
42-
cache_key = Current.family.build_cache_key("transactions_totals_#{params_digest}")
43-
44-
@totals = Rails.cache.fetch(cache_key) do
45-
Current.family.income_statement.totals(transactions_scope: transactions_query)
49+
cache_data = Rails.cache.fetch(cache_key, expires_in: 30.minutes) do
50+
current_page_i = current_page
51+
52+
# Initial query
53+
offset = (current_page_i - 1) * items_per_page
54+
ids = transactions_query
55+
.reverse_chronological
56+
.limit(items_per_page)
57+
.offset(offset)
58+
.pluck(:id)
59+
60+
total_count = transactions_query.count
61+
62+
if ids.empty? && total_count.positive? && current_page_i > 1
63+
current_page_i = (total_count.to_f / items_per_page).ceil
64+
offset = (current_page_i - 1) * items_per_page
65+
66+
ids = transactions_query
67+
.reverse_chronological
68+
.limit(items_per_page)
69+
.offset(offset)
70+
.pluck(:id)
71+
end
72+
73+
{ ids: ids, total_count: total_count, current_page: current_page_i }
4674
end
75+
76+
ids = cache_data[:ids]
77+
total_count = cache_data[:total_count]
78+
current_page = cache_data[:current_page]
79+
80+
# Build Pagy object (this part is cheap – done *after* potential
81+
# page fallback so the pagination UI reflects the adjusted page
82+
# number).
83+
@pagy = Pagy.new(
84+
count: total_count,
85+
page: current_page,
86+
items: items_per_page,
87+
params: ->(p) { p.except(:focused_record_id) }
88+
)
89+
90+
# Fetch the transactions in the cached order
91+
@transactions = Current.family.transactions
92+
.active
93+
.where(id: ids)
94+
.includes(
95+
{ entry: :account },
96+
:category, :merchant, :tags,
97+
transfer_as_outflow: { inflow_transaction: { entry: :account } },
98+
transfer_as_inflow: { outflow_transaction: { entry: :account } }
99+
)
100+
101+
# Preserve the order defined by `ids`
102+
@transactions = ids.map { |id| @transactions.detect { |t| t.id == id } }.compact
103+
104+
@totals = Current.family.income_statement.totals(transactions_scope: transactions_query)
47105
end
48106

49107
def clear_filter

0 commit comments

Comments
 (0)