{"id":28317587,"url":"https://github.com/codesnik/calculate-all","last_synced_at":"2025-06-24T14:31:18.447Z","repository":{"id":62554862,"uuid":"51338328","full_name":"codesnik/calculate-all","owner":"codesnik","description":"calculate_all method for aggregate functions in Active Record","archived":false,"fork":false,"pushed_at":"2025-05-19T14:21:22.000Z","size":62,"stargazers_count":135,"open_issues_count":0,"forks_count":16,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-06-01T14:17:29.435Z","etag":null,"topics":["activerecord","aggregate","average","groupdate","median","mysql","postgres","rails","statistics"],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/codesnik.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2016-02-09T00:47:54.000Z","updated_at":"2025-05-30T02:10:06.000Z","dependencies_parsed_at":"2025-05-09T15:31:45.643Z","dependency_job_id":"c09a6d38-3a7f-467d-a4c9-e36b8281a0ba","html_url":"https://github.com/codesnik/calculate-all","commit_stats":null,"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/codesnik/calculate-all","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codesnik%2Fcalculate-all","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codesnik%2Fcalculate-all/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codesnik%2Fcalculate-all/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codesnik%2Fcalculate-all/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/codesnik","download_url":"https://codeload.github.com/codesnik/calculate-all/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codesnik%2Fcalculate-all/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":261694132,"owners_count":23195517,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["activerecord","aggregate","average","groupdate","median","mysql","postgres","rails","statistics"],"created_at":"2025-05-25T06:12:22.005Z","updated_at":"2025-06-24T14:31:18.438Z","avatar_url":"https://github.com/codesnik.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"# CalculateAll\n\nProvides the `#calculate_all` method for your Active Record models, scopes and relations.\nIt's a small addition to Active Record's `#count`, `#maximum`, `#minimum`, `#average`, `#sum`\nand `#calculate`.\nIt allows you to fetch all of the above, as well as other aggregate function results,\nin a single request, with support for grouping.\n\nShould be useful for dashboards, timeseries stats, and charts.\n\nCurrently tested with PostgreSQL, MySQL and SQLite3, ruby \u003e= 2.6, rails \u003e= 4, groupdate \u003e= 4.\n\nWith rails \u003e= 7.1, `#async_calculate_all` is also available, which does the same but in a separate\ndb connection and ruby thread, and returns `ActiveRecord::Promise`\n\n## Usage\n\n(example SQL snippets are given for PostgreSQL)\n\n```ruby\nstats = Order.group(:department_id).group(:payment_method).order(:payment_method).calculate_all(\n  :payment_method,\n  :count,\n  :price_max,\n  :price_min,\n  :price_avg,\n  total_users: :count_distinct_user_id,\n  price_median: \"percentile_cont(0.5) within group (order by price asc)\",\n  plan_ids: \"array_agg(distinct plan_id order by plan_id)\",\n  earnings: \"sum(price) filter (where status = 'paid')\"\n)\n#   Order Pluck (20.0ms)  SELECT \"orders\".\"department_id\", \"payment_method\", COUNT(*), MAX(price), MIN(price), AVG(price),\n#        COUNT(DISTINCT user_id), percentile_cont(0.5) within group (order by price asc),\n#        array_agg(distinct plan_id order by plan_id), sum(price) filter (where status = 'paid')\n#      FROM \"orders\" GROUP BY \"orders\".\"department_id\", \"payment_method\" ORDER BY \"payment_method\" ASC\n# =\u003e {\n#   [1, \"card\"] =\u003e {\n#     payment_method: \"card\",\n#     count: 10,\n#     price_max: 500,\n#     price_min: 100,\n#     price_avg: 0.3e3,\n#     total_users: 5,\n#     price_median: 0.4e3,\n#     plan_ids: [4, 7, 12],\n#     earnings: 2340\n#   },\n#   [1, \"cash\"] =\u003e {\n#     ...\n#   }\n# }\n```\n\n```ruby\nstats_by_department = Order.group(:department_id).async_calculate_all(:count, :price_sum)\n# continue with some other expensive stuff\n\n# later, in some view\nstats_by_department.value.each do |department_id, stats|\n   # ...\nend\n```\n\n## Rationale\n\nActive Record makes it really easy to use most common database aggregate functions like COUNT(), MAX(), MIN(), AVG(), SUM().\nBut there's a whole world of other aggregate functions in\n[PostgreSQL](http://www.postgresql.org/docs/current/functions-aggregate.html),\n[MySQL](https://dev.mysql.com/doc/refman/9.3/en/aggregate-functions.html)\nand [SQLite](https://www.sqlite.org/lang_aggfunc.html)\nwhich I can’t recommend enough, especially if you’re working with statistics or business intelligence.\n\nAlso, in many cases, you’ll need multiple metrics at once. Typically, the database performs a full scan of the table for each metric.\nHowever, it can calculate all of them in a single scan and a single request.\n\n`#calculate_all` to the rescue!\n\n## Arguments\n\n`#calculate_all` accepts a single SQL expression with aggregate functions,\n\n```ruby\n  Model.calculate_all('CAST(SUM(price) as decimal) / COUNT(DISTINCT user_id)')\n```\n\nor arbitrary symbols and keyword arguments with SQL snippets, aggregate function shortcuts or previously given grouping values.\n\n```ruby\n  Model.group(:currency).calculate_all(\n    :average_price, :currency, total: :sum_price, average_spendings: 'SUM(price)::decimal / COUNT(DISTINCT user_id)'\n  )\n```\n\nFor convenience, `calculate_all(:count, :avg_column)` is the same as `calculate_all(count: :count, avg_column: :avg_column)`\n\nHere's a cheatsheet of recognized shortcuts:\n\n| symbol                                                                 | would fetch\n|------------------------------------------------------------------------|------------\n| `:count`                                                               | `COUNT(*)`\n| `:count_column1`, `:column1_count`                                     | `COUNT(column1)` (doesn't count NULL's in that column)\n| `:count_distinct_column1`, `:column1_distinct_count`                   | `COUNT(DISTINCT column1)`\n| `:max_column1`, `:column1_max`, `:maximum_column1`, `:column1_maximum` | `MAX(column1)`\n| `:min_column1`, `:column1_min`, `:minimum_column1`, `:column1_minimum` | `MIN(column1)`\n| `:avg_column1`, `:column1_avg`, `:average_column1`, `:column1_average` | `AVG(column1)`\n| `:sum_column1`, `:column1_sum`                                         | `SUM(column1)`\n\nOther functions are a bit too database specific, and are better to be given with an explicit SQL snippet.\n\nPlease don't put values from unverified sources (like HTML form or javascript call) into expression list,\nit could result in malicious SQL injection.\n\n## Result\n\n`#calculate_all` tries to mimic magic of Active Record's `#group`, `#count` and `#pluck`\nso result type depends on arguments and on groupings.\n\nIf you have no `group()` on underlying scope, `#calculate_all` will return just one row.\n\n```ruby\nOrder.calculate_all(:price_sum)\n# =\u003e {price_sum: 123500}\n```\n\nIf you have a single `group()`, it will return a hash of results with simple keys.\n\n```ruby\nOrder.group(:department_id).calculate_all(:count_distinct_user_id)\n# =\u003e {\n#   1 =\u003e {count_distinct_user_id: 20},\n#   2 =\u003e {count_distinct_user_id: 10},\n#   ...\n# }\n```\n\nIf you have two or more groupings, each result will have an array as a key.\n\n```ruby\nOrder.group(:department_id).group(:payment_method).calculate_all(:count)\n# =\u003e {\n#   [1, \"cash\"] =\u003e {count: 5},\n#   [1, \"card\"] =\u003e {count: 15},\n#   [2, \"cash\"] =\u003e {count: 1},\n#   ...\n# }\n```\n\nIf you provide only one *string* argument to `#calculate_all`, its calculated value will be returned as-is.\nThis is just to make grouped companion to `Model.group(...).count` and friends, but for arbitrary expressions\nwith aggregate functions.\n\n```ruby\nOrder.group(:payment_method).calculate_all('CAST(SUM(price) AS decimal) / COUNT(DISTINCT user_id)')\n# =\u003e {\n#   \"card\" =\u003e 0.524e3,\n#   \"cash\" =\u003e 0.132e3\n# }\n```\n\nOtherwise, the results will be returned as hash(es) with symbol keys.\n\n```ruby\nOrder.group(:department_id).group(:payment_method).calculate_all(\n  :min_price, type: :payment_method, expr1: 'count(distinct user_id)'\n)\n# =\u003e {\n#   [1, 'cash'] =\u003e {min_price: 100, type: 'cash', expr1: 5},\n#   [1, 'card'] =\u003e {min_price: 150, type: 'card', expr1: 15},\n#   ...\n# }\n```\n\nYou can pass a block to `calculate_all`. Rows will be passed to it, and returned value will be used instead of\nthe row in the result hash (or returned as-is if there's no grouping).\n\n```ruby\nOrder.group(:country_id).calculate_all(:count, :avg_price) { |count:, avg_price:|\n  \"#{count} orders, #{avg_price.to_i} dollars average\"\n}\n# =\u003e {\n#   1 =\u003e \"5 orders, 120 dollars average\",\n#   2 =\u003e \"10 orders, 200 dollars average\"\n# }\n\nOrder.group(:country_id).calculate_all(\"AVG(price)\") { |avg_price| avg_price.to_i }\n# =\u003e {\n#   1 =\u003e 120,\n#   2 =\u003e 200\n# }\n\nOrder.calculate_all(:count, :max_price, \u0026OpenStruct.method(:new))\n# =\u003e #\u003cOpenStruct max_price=500, count=15\u003e\n\nStats = Data.define(:count, :max_price) do\n  # needed only for groupdate to provide defaults for empty periods\n  def initialize(count: 0, max_price: nil) = super\nend\nOrder.group_by_year(:created_at).calculate_all(*Stats.members, \u0026Stats.method(:new))\n# =\u003e {\n#   Wed, 01 Jan 2014 =\u003e #\u003cdata Stats count=2, max_price=700\u003e,\n#   Thu, 01 Jan 2015 =\u003e #\u003cdata Stats count=0, max_price=nil\u003e,\n#   Fri, 01 Jan 2016 =\u003e #\u003cdata Stats count=3, max_price=800\u003e\n# }\n```\n\n## groupdate compatibility\n\ncalculate-all should work with [groupdate](https://github.com/ankane/groupdate) too:\n\n```ruby\nOrder.group_by_year(:created_at, last: 5).calculate_all(:price_min, :price_max)\n# =\u003e {\n#   Sun, 01 Jan 2012 =\u003e {},\n#   Tue, 01 Jan 2013 =\u003e {},\n#   Wed, 01 Jan 2014 =\u003e {},\n#   Thu, 01 Jan 2015 =\u003e {},\n#   Fri, 01 Jan 2016 =\u003e {:price_min=\u003e100, :price_max=\u003e500}\n# }\n```\n\nIt works even with groupdate \u003c 4, though you'd have to explicitly provide `default_value: {}` for blank periods.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'calculate-all'\n```\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install calculate-all\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.\nRun `BUNDLE_GEMFILE=gemfiles/activerecord60.gemfile bundle` then `BUNDLE_GEMFILE=gemfiles/activerecord60.gemfile rake`\nto test against specific active record version.\n\nTo experiment you can load a test database and jump to IRB with\n\n```sh\n   rake VERBOSE=1 CONSOLE=1 TESTOPTS=\"--name=test_console\" test:postgresql\n```\n\nTo install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version\nnumber in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags,\nand push the `.gem` file to [rubygems.org](https://rubygems.org).\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/codesnik/calculate-all.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcodesnik%2Fcalculate-all","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcodesnik%2Fcalculate-all","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcodesnik%2Fcalculate-all/lists"}