{"record":{"id":"66310edf55da8996","repo":"discourse/discourse","slug":"contract-violation-query-ends-with-a-semicolon-r","errorCode":null,"errorMessage":"Contract violation:\nQuery ends with a semicolon. Remove the semicolon; your sql will be used in a subquery.","messagePattern":"Contract violation:\nQuery ends with a semicolon\\. Remove the semicolon; your sql will be used in a subquery\\.","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"app/services/badge_granter.rb","lineNumber":316,"sourceCode":"        raise \"Contract violation:\\nQuery is triggered, but does not reference the ':backfill' parameter.\\n(Hint: if :backfill is TRUE, you should ignore the :post_ids/:user_ids)\"\n      end\n    end\n\n    # TODO these three conditions have a lot of false negatives\n    if opts[:target_posts]\n      unless sql.match(/post_id/)\n        raise \"Contract violation:\\nQuery targets posts, but does not return a 'post_id' column\"\n      end\n    end\n\n    unless sql.match(/user_id/)\n      raise \"Contract violation:\\nQuery does not return a 'user_id' column\"\n    end\n    unless sql.match(/granted_at/)\n      raise \"Contract violation:\\nQuery does not return a 'granted_at' column\"\n    end\n    if sql.match(/;\\s*\\z/)\n      raise \"Contract violation:\\nQuery ends with a semicolon. Remove the semicolon; your sql will be used in a subquery.\"\n    end\n  end\n\n  # Options:\n  #   :target_posts - whether the badge targets posts\n  #   :trigger - the Badge::Trigger id\n  #   :explain - return the EXPLAIN query\n  def self.preview(sql, opts = {})\n    params = { user_ids: [], post_ids: [], backfill: true }\n\n    BadgeGranter.contract_checks!(sql, opts)\n\n    # hack to allow for params, otherwise sanitizer will trigger sprintf\n    count_sql = <<~SQL\n      SELECT COUNT(*) count\n                 FROM (\n                        #{sql}\n                      ) q","sourceCodeStart":298,"sourceCodeEnd":334,"githubUrl":"https://github.com/discourse/discourse/blob/1b2d7253e854205d2c95c80b2c6a1cdd42e45059/app/services/badge_granter.rb#L298-L334","documentation":"Error \"Contract violation:\nQuery ends with a semicolon. Remove the semicolon; your sql will be used in a subquery.\" thrown in discourse/discourse.","triggerScenarios":"Thrown at app/services/badge_granter.rb:316 when the library encounters an invalid state.","commonSituations":"See trigger scenarios.","solutions":[],"exampleFix":null,"handlingStrategy":null,"validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":[],"tags":[],"backgroundTag":null,"analyzedSha":"1b2d7253e854205d2c95c80b2c6a1cdd42e45059","analyzedAt":"2026-08-26T03:11:08.302Z","schemaVersion":2},"datasetVersion":"2026-08-26T07:17:17.940Z"}