{"record":{"id":"24691b6e4a8c4196","repo":"instructure/canvas-lms","slug":"subclasses-must-implement-process-record-when-mode-is","errorCode":null,"errorMessage":"Subclasses must implement #process_record when mode is :individual_record","messagePattern":"Subclasses must implement #process_record when mode is :individual_record","errorType":"exception","errorClass":"NoMethodError","httpStatus":null,"severity":"error","filePath":"lib/canvas_operations/data_fixup.rb","lineNumber":152,"sourceCode":"\n    # Processes a batch of records. Ideal for making bulk updates to\n    # entire batches of rows that don't need to be individually loaded / processed.\n    #\n    # Subclasses must implement this method to define how to process a batch of records\n    #\n    # Only used when mode is set to :batch.\n    def process_batch(_batch)\n      raise NoMethodError, \"Subclasses must implement #process_batch when mode is :batch\"\n    end\n\n    # Processes an individual record. Ideal for when each record needs to be\n    # loaded and processed separately.\n    #\n    # Subclasses must implement this method to define how to process an individual record.\n    #\n    # Only used when mode is set to :individual_record.\n    def process_record(_record)\n      raise NoMethodError, \"Subclasses must implement #process_record when mode is :individual_record\"\n    end\n\n    # Determines if the current shard is a valid target for the DataFixup operation.\n    #\n    # In its base form this check returns true unless run_on_default_shard is false,\n    # in which case it returns true only if the current shard is not the default shard.\n    #\n    # Be wary if overriding this method. The default shard contains shadow copies of all\n    # root accounts, which may lead to unexpected fixup behavior.\n    #\n    # @return [Boolean] true if the current shard is not the default, false otherwise.\n    def valid_shard?\n      return true if self.class.run_on_default_shard?\n\n      !switchman_shard.default?\n    end\n\n    private","sourceCodeStart":134,"sourceCodeEnd":170,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/lib/canvas_operations/data_fixup.rb#L134-L170","documentation":"DataFixup is an abstract base class for shard-scoped data fixups. When mode is :individual_record, the engine calls #process_record per record; the base implementation is intentionally unimplemented and raises NoMethodError to force subclasses to define it. Hitting this means a fixup class declared :individual_record mode but never overrode process_record.","triggerScenarios":"A subclass of CanvasOperations::DataFixup sets mode to :individual_record (or inherits it) and process_range is invoked without the subclass defining its own #process_record.","commonSituations":"Creating a new data fixup and forgetting to implement process_record; copying a subclass that used :range or :batch mode but switching mode to :individual_record; a subclass silently renamed/removed the override so the base method is reached.","solutions":["Implement #process_record(record) in the subclass with the per-record fixup logic","If per-record processing is not needed, set mode to :range (or the appropriate mode) and implement the matching hook (e.g. #process)","Add a spec asserting the fixup class responds to process_record before running it on production shards"],"exampleFix":"# before\nclass FixBrokenEnrollments < CanvasOperations::DataFixup\n  self.mode = :individual_record\nend\n\n# after\nclass FixBrokenEnrollments < CanvasOperations::DataFixup\n  self.mode = :individual_record\n\n  def process_record(enrollment)\n    enrollment.update!(workflow_state: 'completed')\n  end\nend","handlingStrategy":"validation","validationCode":"raise ArgumentError, 'must implement #process_record' if fixup.mode == :individual_record && !(fixup.method(:process_record).owner != CanvasOperations::DataFixup)","typeGuard":"def individual_record_capable?(fixup_class)\n  fixup_class.mode != :individual_record ||\n    fixup_class.instance_method(:process_record).owner != CanvasOperations::DataFixup\nend","tryCatchPattern":"begin\n  fixup.process_range\nrescue NoMethodError => e\n  raise e unless e.message.include?('process_record')\n  Rails.logger.error(\"#{fixup.class} missing #process_record for :individual_record mode\")\nend","preventionTips":["Always implement #process_record in the same commit that sets mode = :individual_record","Add a shared spec for every DataFixup subclass asserting the mode/hook pairing","Run new fixups against a test shard before production"],"tags":["ruby","abstract-method","data-fixup","subclassing"],"backgroundTag":"abstract-method-not-implemented","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"}