{"record":{"id":"1258a5a422e2e622","repo":"instructure/canvas-lms","slug":"top-enrollment-by-user-must-be-scoped","errorCode":null,"errorMessage":"top_enrollment_by_user must be scoped","messagePattern":"top_enrollment_by_user must be scoped","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/models/enrollment.rb","lineNumber":1507,"sourceCode":"    else\n      Rails.cache.fetch([email, \"invited_enrollments2\"].cache_key) do\n        Enrollment.invited.for_email(email).to_a\n      end\n    end\n  end\n\n  def self.order_by_sortable_name\n    clause = User.sortable_name_order_by_clause(\"users\")\n    scope = order(clause)\n    if scope.select_values.present?\n      scope.select(clause)\n    else\n      scope.select(arel_table[Arel.star])\n    end\n  end\n\n  def self.top_enrollment_by(key, rank_order = :default)\n    raise \"top_enrollment_by_user must be scoped\" unless all.where_clause.present?\n\n    key = key.to_s\n    order(Arel.sql(\"#{key}, #{type_rank_sql(rank_order)}\")).distinct_on(key)\n  end\n\n  def assign_uuid\n    # DON'T use ||=, because that will cause an immediate save to the db if it\n    # doesn't already exist\n    self.uuid = CanvasSlug.generate_securish_uuid unless self[\"uuid\"]\n  end\n  protected :assign_uuid\n\n  def uuid\n    unless super\n      update_attribute(:uuid, CanvasSlug.generate_securish_uuid)\n    end\n    super\n  end","sourceCodeStart":1489,"sourceCodeEnd":1525,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/models/enrollment.rb#L1489-L1525","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before\nEnrollment.top_enrollment_by_user(:id)\n// after\nEnrollment.where(course_id: course.id).top_enrollment_by_user(:id)","handlingStrategy":"validation","validationCode":"raise 'scope required' unless scope.where_clause.present?\nscope.top_enrollment_by_user(:id)","typeGuard":null,"tryCatchPattern":"begin\n  result = scope.top_enrollment_by_user(:id)\nrescue RuntimeError => e\n  raise unless e.message =~ /must be scoped/\n  result = Enrollment.none\nend","preventionTips":["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"],"tags":["ruby","rails","activerecord","query-scope","postgres"],"backgroundTag":"invalid-argument-value","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}