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

  1. Always scope first: course.enrollments.top_enrollment_by_user or Enrollment.where(course_id: ...).top_enrollment_by(...)
  2. Add the missing where clause (e.g. by course, section, or shard-appropriate filter) before ranking
  3. If a global ranking is truly intended, add an explicit narrow scope anyway (e.g. where(root_account_id: account.id))
  4. 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

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


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
  end

View on GitHub (pinned to 1c9f0bb801)