🏀 AndOne
Find repeated ActiveRecord queries in Rails, investigate likely N+1s, and catch query regressions in tests. No external service required.
AndOne reports query counts, observed SQL time, call sites, and qualified fix suggestions. Repeated SQL is evidence, not proof of an association N+1: findings distinguish suspected association loading, identical reads, aggregates, and generic repetition.
Installation
# Gemfile
group :development, :test do
gem "and_one"
endRequires Ruby 3.2+ and Rails components 7.0+, using mutually compatible versions. Rails installs request and job hooks automatically:
| Environment | Default behavior |
|---|---|
| Development | Log findings, show in-page notices, and enable /__and_one
|
| Test | Raise AndOne::NPlus1Error on every scan with findings |
| Production | Disabled if loaded; the Gemfile group above excludes it |
ActiveJob is supported across backends; direct Sidekiq jobs are hooked when Sidekiq is loaded at Railtie setup. Nested hooks reuse the existing scan. Development Rails consoles are scanned automatically.
Standalone scripts can use AndOne.scan with ActiveRecord; arbitrary database clients and other ORMs are not supported. See standalone setup and tested versions.
What you get
-
Actionable investigation: candidate
.includes()/.preload()advice for simple association record loading; separate guidance for counts, existence checks, scalar queries, and duplicate reads. - Location hints: the query origin and a heuristic caller to inspect—not a proven relation-construction site.
- Measured impact: exact eligible query counts and observed SQL notification time, not estimated savings.
-
Development dashboard: browse
/__and_one; sort retained findings by cumulative SQL time, occurrences, or executed query count. - Test helpers: repeated-query matchers, physical query budgets, and input-growth assertions for Minitest and RSpec.
- Reporting: text or JSON logs, callbacks, GitHub Actions warning annotations, and first-occurrence deduplication.
- Private, bounded samples: SQL literals redacted by default; bind values never retained.
Toasts appear at top-right on eligible buffered HTML responses. Streaming, compressed, download, and non-HTML responses are left untouched. Under CSP, a script-free <details> notice replaces the styled toast. UI details.
Finding classification
| Kind | Confidence | Meaning |
|---|---|---|
suspected_association_n_plus_one |
candidate |
Varying values in simple record reads with qualified equality/IN predicates |
duplicate_identical_read |
observed |
Identical lexical SQL/value signatures in non-aggregate reads |
repeated_aggregate |
observed |
Repeated COUNT, SUM/AVG/MIN/MAX, or EXISTS-style projection |
generic_repetition |
unknown |
Unsupported, missing, scalar, or complex evidence |
Classification uses bounded, per-scan HMAC signatures when value evidence is available; it never evaluates lazy bind callbacks. Intentional batching can still produce findings, and identical reads are not necessarily safe to cache. Thresholds, ignores, test matchers, and NPlus1Error apply to all finding kinds.
Association suggestions are candidates to verify, not automatic fixes. Ambiguous associations or complex SQL may receive only general guidance. includes alone does not fix association .count queries. See classification evidence and recommendation limits.
Test query behavior
Minitest
class PostsTest < ActiveSupport::TestCase
include AndOne::MinitestHelper
test "preloads comments" do
assert_no_n_plus_one do
Post.preload(:comments).each { |post| post.comments.to_a }
end
end
endRSpec
require "and_one/rspec"
RSpec.describe Post do
it "preloads comments" do
expect {
Post.preload(:comments).each { |post| post.comments.to_a }
}.not_to cause_n_plus_one
end
endThese matchers respect ignores and enabled, but do not log, invoke callbacks, change deduplication, or raise NPlus1Error. Put them around requests/jobs, not inside an already active scan.
Query budgets and growth
# Prepare sufficient records and warm schema/connection caches first.
small = -> { Post.limit(2).preload(:comments).each { |p| p.comments.to_a } }
large = -> { Post.limit(20).preload(:comments).each { |p| p.comments.to_a } }
# Minitest
assert_query_budget(max: 2, &large)
assert_query_growth(small: small, large: large, max_growth: 0)
# RSpec
expect(&large).to stay_within_query_budget(2)
expect(small: small, large: large).to stay_within_query_growth(0)Budgets count non-cached synchronous SQL notifications, including writes and transactions, but not schema events. Growth is large.count - small.count, not a ratio. Each workload runs once; AndOne does not create data, warm up, or clear caches. These measurements remain active when detection is disabled/paused and do not suppress normal scan enforcement. Full test-helper semantics.
Configuration
Configure in config/initializers/and_one.rb, before scanning:
AndOne.configure do |config|
config.min_n_queries = 2 # default repetition threshold
config.env_thresholds = { development: 3, test: 2 }
config.raise_on_detect = Rails.env.test? # Rails default
config.dev_toast_position = :top_right # default; or :top_left/:bottom_right/:bottom_left
config.capture_mode = :redacted # default; :raw is for trusted local debugging only
# Optional: share aggregate findings across cooperating processes.
# config.aggregate_store = :file # default: bounded process-local memory
# Rails development/test defaults to a session-scoped logfile.
# config.logfile = nil # disable file output
# config.logfile_format = :json # default: :text
# config.json_logging = true # JSON console/logger output
endExplicit initializer settings are preserved. Configure only between scans, not while requests/jobs are running. See the configuration reference for callbacks, custom paths, access guards, service lifecycle, and output failure behavior.
Ignore accepted findings
Create .and_one_ignore in your application root:
gem:devise
path:app/views/admin/*
query:schema_migrations
fingerprint:a1b2c3d4e5f6
Fingerprints ignore a query shape at all locations/connections; issue_id distinguishes location-specific findings. Query rules inspect original SQL, including occurrences beyond retained samples. Ignore rules · Fingerprint compatibility and migration.
Storage and deduplication
The aggregate retains at most 100 issues, evicting the least recently observed. Logs, annotations, and callbacks report new retained issues and recurrences of resolved issues; other occurrences update counts and costs. Evicted issues can be reported again. Every violating scan still raises when enforcement is enabled.
AndOne.aggregate.summary # formatted retained findings
AndOne.aggregate.detections # entries keyed by issue_id
AndOne.aggregate.reset! # clear aggregate only
AndOne.reset_session! # also flush and truncate the configured logfileMemory storage is process-local and disappears on exit. File storage shares findings under tmp/and_one/sessions; the default development session persists across restarts, while independent test boots use isolated IDs. Set AND_ONE_SESSION before boot to select a shared session. Quiesce writers before resets. Storage, cleanup, and upgrade notes.
Capture privacy and dashboard access
SQL samples are redacted before retention by default. Each finding retains at most 5 samples Ă— 2,048 bytes and 20 stack frames Ă— 256 bytes, plus exact counts and cost summaries. Detection#queries is a sample, not the full query history.
Redaction is lexical, not complete anonymization: identifiers, filenames, and method names can remain visible. Raw mode can expose SQL literals and absolute paths; changing modes cannot revoke previously returned output or logs. Do not expose findings publicly or assume they are safe for production capture.
The dashboard defaults to direct loopback peers (127.0.0.1/::1) and sends Cache-Control: no-store. A local reverse proxy can make remote clients appear local. Secure the proxy or configure dashboard_access_guard using server-verified authentication. Privacy and access configuration.
Capture coverage and limits
- Scans capture synchronous ActiveRecord SQL in the current fiber. Child threads/fibers need their own scans; async-tagged SQL is excluded.
- Request scanning ends when the application returns its response. Lazy/streaming response-body SQL is outside that scan.
- Detection excludes cached queries and schema events. Costs describe eligible executed reads, not total request time or database-server CPU time.
- Samples and aggregate history are bounded, but distinct groups per scan and incoming SQL size are not. Parsing work and total scan memory are not strictly bounded.
- Block scans clean up on exceptions and nonlocal exits; nested scans reuse the outer scope. Use
AndOne.pause { ... }to exclude known work.
Agent CLI and skill
Enable aggregate_store = :file in development, restart the app, and reproduce a finding. Then inspect it without booting Rails:
bundle binstubs and_one
bin/and-one issues --json
bin/and-one show ISSUE_ID --json
bin/and-one resolve ISSUE_ID --note "Fixed and verified by regression test" --json
bin/and-one issues --status resolved --json
bin/and-one reopen ISSUE_ID --json
bin/and-one skill install --target pi # or agents / claudeInspection commands read persisted snapshots without changing them; explicit resolve/reopen commands update workflow metadata. issues defaults to open findings. A resolved issue automatically reopens and reports on recurrence; detection and test enforcement are never suppressed. Restart all cooperating processes after upgrading before resolving issues. The bundled skill guides agents through reproduction, fixes, query-growth regression tests, and verified resolution. Skill installation is explicit, project-local, and protects local edits. See CLI commands, sessions, JSON contract, and skill updates.
Documentation
- Usage reference: output example, jobs, matchers, manual scans, configuration, privacy, and logging
- Capture and measured costs: execution boundaries, private signatures, metrics, and benchmarks
- Storage and sessions: sharing, retention, failures, resets, and migration
- SQL fingerprints: eligibility, identity, supported syntax, and limitations
- Compatibility: tested Ruby/Rails/adapters, accuracy corpus, and local test commands
- Marketing site: local preview and GitHub Pages deployment
License
MIT.