diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index 776173df8..7ad834901 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -72,46 +72,104 @@ def set_event end def fetch_upcoming_events - events = [ - Workshop.includes(:chapter, :sponsors, :host, :permissions, :organisers) - .upcoming - .joins(:chapter) - .merge(Chapter.active) - ] - events << Meeting.upcoming.all - events << Event.upcoming.includes(:venue, :sponsors, :sponsorships, :permissions, :organisers).all - - sorted = events.compact.flatten.sort_by(&:date_and_time) - return [{}, nil] if sorted.empty? - - pagy, paginated = pagy(sorted, items: 20) - paginated ||= [] - - grouped = paginated.group_by(&:date) + result = paginated_events(upcoming: true) + return [{}, nil] if result.nil? + + rows, pagy = result + events = load_events(rows) + grouped = events.group_by(&:date) decorated = grouped.transform_values { |items| EventPresenter.decorate_collection(items) } [decorated, pagy] end def fetch_past_events - events = [ - Workshop.includes(:chapter, :sponsors, :host, :permissions, :organisers) - .past - .joins(:chapter) - .merge(Chapter.active) - ] - events << Meeting.past.all - events << Event.past.includes(:venue, :sponsors, :sponsorships, :permissions, :organisers).all - - sorted = events.compact.flatten.sort_by(&:date_and_time).reverse - return [{}, nil] if sorted.empty? - - pagy, paginated = pagy(sorted, items: 20) - paginated ||= [] - - grouped = paginated.group_by(&:date) + result = paginated_events(upcoming: false) + return [{}, nil] if result.nil? + + rows, pagy = result + events = load_events(rows) + grouped = events.group_by(&:date) decorated = grouped.transform_values { |items| EventPresenter.decorate_collection(items) } [decorated, pagy] end + + # Builds a UNION ALL query across Workshops, Meetings, and Events, + # paginates at the database level, and returns (id, event_type) pairs + # for the current page. Only the 20 visible rows come back from the DB. + def paginated_events(upcoming:) + now = Time.zone.now + page = (params[:page] || 1).to_i + direction = upcoming ? "ASC" : "DESC" + comparator = upcoming ? :gteq : :lt + + # Pure Arel subqueries — no ActiveRecord relation bind params to leak + w = Arel::Table.new(:workshops) + ch = Arel::Table.new(:chapters) + ws = Arel::SelectManager.new(w) + ws.project(w[:id], w[:date_and_time], Arel.sql("'Workshop' AS event_type")) + ws.join(ch).on(w[:chapter_id].eq(ch[:id])) + ws.where(ch[:active].eq(true).and(w[:date_and_time].public_send(comparator, now))) + + m = Arel::Table.new(:meetings) + ms = Arel::SelectManager.new(m) + ms.project(m[:id], m[:date_and_time], Arel.sql("'Meeting' AS event_type")) + ms.where(m[:date_and_time].public_send(comparator, now)) + + e = Arel::Table.new(:events) + es = Arel::SelectManager.new(e) + es.project(e[:id], e[:date_and_time], Arel.sql("'Event' AS event_type")) + es.where(e[:date_and_time].public_send(comparator, now)) + + union = Arel::Nodes::UnionAll.new( + Arel::Nodes::UnionAll.new(ws, ms), es + ) + + # COUNT at DB level — single query + count_query = Arel::SelectManager.new + count_query.from(union.as("events")) + count_query.project(Arel.star.count) + total = ActiveRecord::Base.connection.select_value(count_query.to_sql).to_i + return nil if total.zero? + + pagy_opts = { count: total, page: page, limit: 20, request: request } + pagy_opts[:request] = Pagy::Request.new(pagy_opts) + pagy = Pagy::Offset.new(**pagy_opts) + + # Only 20 rows leave the database + pagination_query = Arel::SelectManager.new + pagination_query.from(union.as("events")) + pagination_query.project(:id, :event_type) + pagination_query.order(Arel.sql("date_and_time #{direction}")) + pagination_query.skip(pagy.offset).take(pagy.limit) + + rows = ActiveRecord::Base.connection.select_all(pagination_query.to_sql) + [rows, pagy] + end + + # Loads full ActiveRecord objects for the paginated (id, event_type) pairs + # with eager loading, preserving the UNION order. + def load_events(rows) + grouped = rows.each_with_object({}) do |row, hash| + (hash[row["event_type"]] ||= []) << row["id"].to_i + end + + workshops = Workshop.includes(:chapter, :sponsors, :permissions, :organisers, :workshop_host) + .where(id: grouped["Workshop"]) + .to_a.index_by(&:id) + meetings = Meeting.where(id: grouped["Meeting"]) + .to_a.index_by(&:id) + events = Event.includes(:venue, :sponsors, :sponsorships, :permissions, :organisers) + .where(id: grouped["Event"]) + .to_a.index_by(&:id) + + rows.filter_map do |row| + case row["event_type"] + when "Workshop" then workshops[row["id"].to_i] + when "Meeting" then meetings[row["id"].to_i] + when "Event" then events[row["id"].to_i] + end + end + end end