instructure/canvas-lms · error
top_enrollment_by_user must be scoped
Error message
top_enrollment_by_user must be scoped
What it means
Enrollment.top_enrollment_by is a class-level ranking query built on DISTINCT ON (Postgres). It requires the receiver scope to already be narrowed (e.g. by course or section); running it over the entire enrollments table would be unbounded/incorrect, so it raises unless all.where_clause is present.
Solutions
- Always scope first: course.enrollments.top_enrollment_by_user or Enrollment.where(course_id: ...).top_enrollment_by(...)
- Add the missing where clause (e.g. by course, section, or shard-appropriate filter) before ranking
- If a global ranking is truly intended, add an explicit narrow scope anyway (e.g. where(root_account_id: account.id))
- For non-Postgres databases, avoid distinct_on-dependent methods entirely
Example fix
// before Enrollment.top_enrollment_by_user(:id) // after Enrollment.where(course_id: course.id).top_enrollment_by_user(:id)
Defensive patterns
Strategy: validation
Validate before calling
raise 'scope required' unless scope.where_clause.present? scope.top_enrollment_by_user(:id)
Try / catch
begin result = scope.top_enrollment_by_user(:id) rescue RuntimeError => e raise unless e.message =~ /must be scoped/ result = Enrollment.none end
Prevention
- Always chain top_enrollment_by onto a narrowed scope (course/section/account)
- Don't call ranking class methods bare on Enrollment
- Remember the method relies on Postgres DISTINCT ON
When it happens
Trigger: Calling Enrollment.top_enrollment_by(:user) or top_enrollment_by_user without chaining a where scope first, e.g. Enrollment.top_enrollment_by_user(:user) instead of course.enrollments.top_enrollment_by_user(:user).
Common situations: Console experiments running the query globally; refactors that dropped the course/section scope; adapters on non-Postgres setups probing the method and hitting the guard first.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- conversation_ids needs to be scoped to a user
- An institutional tag category did not pass validation
- assessor and assessee required
- association required
- can only forward one conversation at a time
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/1258a5a422e2e622.
Report an issue: GitHub.
Appendix: source
Thrown at app/models/enrollment.rb:1507
else
Rails.cache.fetch([email, "invited_enrollments2"].cache_key) do
Enrollment.invited.for_email(email).to_a
end
end
end
def self.order_by_sortable_name
clause = User.sortable_name_order_by_clause("users")
scope = order(clause)
if scope.select_values.present?
scope.select(clause)
else
scope.select(arel_table[Arel.star])
end
end
def self.top_enrollment_by(key, rank_order = :default)
raise "top_enrollment_by_user must be scoped" unless all.where_clause.present?
key = key.to_s
order(Arel.sql("#{key}, #{type_rank_sql(rank_order)}")).distinct_on(key)
end
def assign_uuid
# DON'T use ||=, because that will cause an immediate save to the db if it
# doesn't already exist
self.uuid = CanvasSlug.generate_securish_uuid unless self["uuid"]
end
protected :assign_uuid
def uuid
unless super
update_attribute(:uuid, CanvasSlug.generate_securish_uuid)
end
super
endView on GitHub (pinned to 1c9f0bb801)