Eager-load message associations in to_llm to prevent N+1 queries - #717
Merged
crmne merged 1 commit intoJul 3, 2026
Merged
Conversation
crmne
force-pushed
the
fix/eager-load-message-associations
branch
from
July 3, 2026 11:21
cd02004 to
7746e37
Compare
ChatMethods#to_llm loads all messages then iterates calling msg.to_llm on each, which accesses tool_calls, parent_tool_call, and model associations per message — causing N+1 queries. This is invisible without strict_loading, but with Rails' strict_loading_by_default = true and :n_plus_one_only mode, every call to conversation.ask raises StrictLoadingViolationError. Fix by adding an eager_load_messages helper that preloads all associations accessed by Message#to_llm in a single query. Applied to both to_llm and cleanup_orphaned_tool_results.
crmne
force-pushed
the
fix/eager-load-message-associations
branch
from
July 3, 2026 11:23
7746e37 to
99ed1a4
Compare
Owner
|
Thanks for your work! |
21 tasks
MatheusRich
added a commit
to MatheusRich/ruby_llm
that referenced
this pull request
Sep 4, 2026
Building a message payload walked each ActiveStorage::Attachment and reached through it to the blob for the filename and the download, so a message with four attachments spent four queries on active_storage_blobs instead of one. The existence check in front of the walk ran once per message too, which cost a query per message even on a transcript with no attachments at all. This commit preloads the blob for the attachment rows it is about to read, and adds the attachment association to the transcript-wide preload that crmne#717 introduced, so a whole conversation resolves its attachments in two queries. A twenty-message chat with two attachments each drops from 107 queries to 9. Preloader is used rather than includes because it leaves an application's own eager loading in place, which includes would discard by building a fresh relation. The with_attached_ scope is not used for the transcript preload because it also loads variant records and preview images, which the payload never reads.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
ChatMethods#to_llmloads all messages viamessages_association.to_a, then iterates callingmsg.to_llmon each.Message#to_llmaccessestool_calls,parent_tool_call, andmodelassociations per message — an N+1 query pattern.This is invisible without
strict_loading, but with Rails'strict_loading_by_default = trueand:n_plus_one_onlymode, every call toconversation.askraisesActiveRecord::StrictLoadingViolationError.The same issue exists in
cleanup_orphaned_tool_results, which reloads messages and accessestool_call?/tool_result?on the last message without eager-loading.Fix: add an
eager_load_messageshelper that preloadstool_calls,parent_tool_call, andmodelin a single query. Falls back to plain.to_awhenmessages_associationis not an ActiveRecord relation (e.g. in test stubs).Type of change
Scope check
Quality check
overcommit --installand all hooks passbundle exec rake vcr:record[provider_name]bundle exec rspecmodels.json,aliases.json)AI-generated code
API changes