
Rails Query Object
- 2 installs
- Updated February 9, 2026
- dchuk/rails_ai_agents
Creates Rails query objects that encapsulate complex database queries, aggregations, and reports, tests first.
About
Generates query objects that encapsulate complex database queries in Rails with TDD. A developer uses it when aggregating statistics, building reports, or handling dashboard queries.
- Encapsulates complex queries with tests first
- Aggregations, stats, and report building
Rails Query Object by the numbers
- 2 all-time installs (skills.sh)
- Ranked #743 of 911 Databases skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/dchuk/rails_ai_agents --skill rails-query-objectAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| Last updated | February 9, 2026 |
| Repository | dchuk/rails_ai_agents ↗ |
What it does
Creates Rails query objects that encapsulate complex database queries, aggregations, and reports, tests first.
Files
Rails Query Object Generator (TDD)
Creates query objects that encapsulate complex database queries with tests first.
Quick Start
1. Write failing test in test/queries/ 2. Run test to confirm RED 3. Implement query object in app/queries/ 4. Run test to confirm GREEN
When to Use Query Objects vs Scopes
| Scenario | Use |
|---|---|
| Simple WHERE clause | Scope on the model |
| Single-condition filter | Scope on the model |
| Multi-table joins with conditions | Query object |
| Dashboard aggregations | Query object |
| Report generation | Query object |
| Queries needing constructor params | Query object |
| Reusable across controllers | Query object |
Rule of thumb: If the query fits in one line and needs no context, use a scope. If it needs parameters, joins multiple tables, or returns computed data, use a query object.
Project Conventions
Query objects in this project:
- Accept context via constructor (
user:oraccount:) - Return
ActiveRecord::Relationfor chainability ORHashfor aggregations - Have a
callmethod for primary operation - Support multi-tenancy (scoped to account)
TDD Workflow
Step 1: Create Query Test (RED)
# test/queries/stale_leads_query_test.rb
require "test_helper"
class StaleLeadsQueryTest < ActiveSupport::TestCase
setup do
@account = accounts(:one)
@other_account = accounts(:two)
end
test "requires an account parameter" do
assert_raises(ArgumentError) { StaleLeadsQuery.new }
end
test "#call returns ActiveRecord::Relation" do
query = StaleLeadsQuery.new(account: @account)
assert_kind_of ActiveRecord::Relation, query.call
end
test "#call returns only leads for the account (multi-tenant)" do
own_lead = leads(:stale_one)
other_lead = leads(:other_account_stale)
results = StaleLeadsQuery.new(account: @account).call
assert_includes results, own_lead
assert_not_includes results, other_lead
end
test "#call returns only stale leads" do
stale = leads(:stale_one)
fresh = leads(:fresh_one)
results = StaleLeadsQuery.new(account: @account).call
assert_includes results, stale
assert_not_includes results, fresh
end
test "multi-tenant isolation" do
other_query = StaleLeadsQuery.new(account: @other_account)
own_query = StaleLeadsQuery.new(account: @account)
assert_empty(other_query.call.where(id: leads(:stale_one).id))
assert_not_empty(own_query.call.where(id: leads(:stale_one).id))
end
endStep 2: Run Test (Confirm RED)
bin/rails test test/queries/stale_leads_query_test.rbStep 3: Implement Query Object (GREEN)
# app/queries/stale_leads_query.rb
class StaleLeadsQuery
attr_reader :account
def initialize(account:)
@account = account
end
def call
account.leads.stale
end
endStep 4: Run Test (Confirm GREEN)
bin/rails test test/queries/stale_leads_query_test.rbQuery Object Patterns
Pattern 1: Simple Filtered Query
# app/queries/stale_leads_query.rb
class StaleLeadsQuery
attr_reader :account
def initialize(account:)
@account = account
end
def call
account.leads.stale
end
endPattern 2: Aggregation Query (Multiple Methods)
# app/queries/dashboard_stats_query.rb
class DashboardStatsQuery
attr_reader :user, :account
def initialize(user:)
@user = user
@account = user.account
end
def upcoming_events(limit: 3)
account.events
.where("event_date >= ?", Date.today)
.order(event_date: :asc)
.limit(limit)
end
def pending_commissions_total
EventVendor
.joins(:event)
.where(events: { account_id: account.id })
.where(commission_status: :to_invoice)
.sum(:commission_value)
end
def top_vendors(limit: 5)
account.vendors
.left_joins(:event_vendors)
.select("vendors.*, COUNT(event_vendors.id) as events_count")
.group("vendors.id")
.order("events_count DESC")
.limit(limit)
end
def leads_by_status
account.leads.group(:status).count
end
endPattern 3: Grouping Query
# app/queries/leads_by_status_query.rb
class LeadsByStatusQuery
attr_reader :account
def initialize(account:)
@account = account
end
def call
leads = account.leads.order(created_at: :desc)
result = Lead.statuses.keys.map(&:to_sym).index_with { [] }
leads.group_by(&:status).each do |status, status_leads|
result[status.to_sym] = status_leads
end
result
end
endTesting Aggregation Queries
# test/queries/dashboard_stats_query_test.rb
require "test_helper"
class DashboardStatsQueryTest < ActiveSupport::TestCase
setup do
@user = users(:one)
@query = DashboardStatsQuery.new(user: @user)
end
test "#upcoming_events returns future events only" do
results = @query.upcoming_events
results.each do |event|
assert event.event_date >= Date.today
end
end
test "#upcoming_events respects limit" do
results = @query.upcoming_events(limit: 2)
assert results.size <= 2
end
test "#leads_by_status returns hash of status to count" do
result = @query.leads_by_status
assert_kind_of Hash, result
end
test "scoped to user account only" do
other_user = users(:other_account)
other_query = DashboardStatsQuery.new(user: other_user)
own_events = @query.upcoming_events
other_events = other_query.upcoming_events
own_events.each do |event|
assert_equal @user.account_id, event.account_id
end
end
endUsage in Controllers
# Simple query
def index
@leads_by_status = LeadsByStatusQuery.new(account: current_account).call
end
# Aggregation query with presenter
def index
stats_query = DashboardStatsQuery.new(user: current_user)
@stats = DashboardStatsPresenter.new(stats_query)
endDirectory Structure
app/queries/
stale_leads_query.rb
leads_by_status_query.rb
dashboard_stats_query.rb
events/
upcoming_query.rb
by_vendor_query.rb
test/queries/
stale_leads_query_test.rb
dashboard_stats_query_test.rbChecklist
- [ ] Test written first (RED)
- [ ] Constructor accepts context (
user:oraccount:) - [ ] Multi-tenant isolation tested
- [ ] Return type documented
- [ ] Methods have clear, descriptive names
- [ ] Complex queries use
.includes()to prevent N+1 - [ ] Database-agnostic (no PostgreSQL-specific SQL)
- [ ] All tests GREEN