{"id":13484370,"url":"https://github.com/brendon/ranked-model","last_synced_at":"2025-04-13T01:59:25.433Z","repository":{"id":1355433,"uuid":"1303125","full_name":"brendon/ranked-model","owner":"brendon","description":"An acts_as_sortable/acts_as_list replacement built for Rails 4+","archived":false,"fork":false,"pushed_at":"2024-11-19T18:45:49.000Z","size":189,"stargazers_count":1094,"open_issues_count":2,"forks_count":133,"subscribers_count":9,"default_branch":"master","last_synced_at":"2025-04-13T01:59:19.610Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://github.com/mixonic/ranked-model","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/brendon.png","metadata":{"files":{"readme":"Readme.mkd","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","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},"funding":{"github":["brendon"]}},"created_at":"2011-01-28T15:19:09.000Z","updated_at":"2025-04-04T03:48:56.000Z","dependencies_parsed_at":"2023-07-05T16:00:47.498Z","dependency_job_id":"e8af95c0-a65a-4128-b166-86b183043ab2","html_url":"https://github.com/brendon/ranked-model","commit_stats":{"total_commits":174,"total_committers":41,"mean_commits":"4.2439024390243905","dds":0.6724137931034483,"last_synced_commit":"26df2b5c4d38b8042571c1551a823c95a43dd542"},"previous_names":["mixonic/ranked-model"],"tags_count":21,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brendon%2Franked-model","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brendon%2Franked-model/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brendon%2Franked-model/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brendon%2Franked-model/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/brendon","download_url":"https://codeload.github.com/brendon/ranked-model/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248654047,"owners_count":21140235,"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":[],"created_at":"2024-07-31T17:01:23.173Z","updated_at":"2025-04-13T01:59:25.414Z","avatar_url":"https://github.com/brendon.png","language":"Ruby","readme":"**ranked-model** is a modern row sorting library built for Rails 4.2+.  It uses ARel aggressively and is better optimized than most other libraries.\n\n[![Build Status](https://github.com/brendon/ranked-model/actions/workflows/ci.yml/badge.svg)](https://github.com/brendon/ranked-model/actions/workflows/ci.yml)\n\nANNOUNCING: Positioning, the gem\n--------------------------------\n\nAs maintainer of both Acts As List and the Ranked Model gems, I've become intimately aquainted with the strengths and weaknesses of each. I ended up writing a small scale Rails Concern for positioning database rows for a recent project and it worked really well so I've decided to release it as a gem: [Positioning](https://github.com/brendon/positioning)\n\nPositioning works similarly to Acts As List in that it maintains a sequential list of integer values as positions. It differs in that it encourages a unique constraints on the position column and supports multiple lists per database table. It borrows Ranked Model's concept of relative positioning. I encourage you to check it out and give it a whirl on your project!\n\nInstallation\n------------\n\nranked-model passes specs with Rails 4.2, 5.0, 5.1, 5.2, 6.0 and 6.1 for MySQL, Postgres, and SQLite on Ruby 2.4 through 3.0 (with exceptions, please check the CI setup for supported combinations), and jruby-9.1.17.0 where Rails supports the platform.\n\nTo install ranked-model, just add it to your `Gemfile`:\n\n``` ruby\ngem 'ranked-model'\n\n# Or pin ranked-model to git\n# gem 'ranked-model',\n#   git: 'git@github.com:mixonic/ranked-model.git'\n```\n\nThen use `bundle install` to update your `Gemfile.lock`.\n\nSimple Use\n----------\n\nUse of ranked-model is straight ahead.  Get some ducks:\n\n``` ruby\nclass Duck \u003c ActiveRecord::Base\nend\n```\n\nGive them an order (integer column):\n\n```bash\nrails g migration AddRowOrderToDucks row_order:integer\nrails db:migrate\n```\n\n**IMPORTANT: The `_order` table column MUST allow null values. For the reason behind this requirement see [issue#167](https://github.com/mixonic/ranked-model/issues/167)**\n\n\n\n\nPut your ducks in a row:\n\n``` ruby\nclass Duck \u003c ActiveRecord::Base\n\n  include RankedModel\n  ranks :row_order\n\nend\n```\n\nOrder the Ducks by this order:\n\n``` ruby\nDuck.rank(:row_order).all\n```\n\nThe ranking integers stored in the `row_order` column will be big and spaced apart.  When you\nimplement a sorting UI, just update the resource by appending the column name with `_position` and indicating the desired position:\n\n``` ruby\n@duck.update row_order_position: 0  # or 1, 2, 37. :first, :last, :up and :down are also valid\n```\n\n**IMPORTANT: Note that you MUST append _position to the column name when setting a new position on an instance. This is a fake column that can take relative as well as absolute index-based values for position.**\n\nPosition numbers begin at zero.  A position number greater than the number of records acts the\nsame as :last. :up and :down move the record up/down the ladder by one step.\n\nSo using a normal json controller where `@duck.attributes = params[:duck]; @duck.save`, JS can\nlook pretty elegant:\n\n``` javascript\n$.ajax({\n  type: 'PUT',\n  url: '/ducks',\n  dataType: 'json',\n  data: { duck: { row_order_position: 0 } },  // or whatever your new position is\n});\n```\n\nIf you need to find the rank of an item with respect to other ranked items, you can use the `{column_name}_rank` method on the model instance. `{column_name}` is your resource ranking column.\n\nFollowing on from our examples above, the `row_order_rank` method will return the position of the duck object in the list with respect to the order defined by the row_order column.\n\n``` ruby\nDuck.rank(:row_order).first.row_order_rank # =\u003e 0\nDuck.rank(:row_order).third.row_order_rank # =\u003e 2\n```\n\nComplex Use\n-----------\n\nThe `ranks` method takes several arguments:\n\n``` ruby\nclass Duck \u003c ActiveRecord::Base\n\n  include RankedModel\n\n  ranks :row_order,                  # Name this ranker, used with rank()\n    column: :sort_order              # Override the default column, which defaults to the name\n\n  belongs_to :pond\n  ranks :swimming_order,\n    with_same: :pond_id              # Ducks belong_to Ponds, make the ranker scoped to one pond\n\n  ranks :row_order,\n    with_same: [:pond_id, :breed]    # Lets rank them by breed\n\n  scope :walking, where(walking: true )\n  ranks :walking_order,\n    scope: :walking                  # Narrow this ranker to a scope\n\n  belongs_to :parent, class_name: 'Duck', optional: true\n  ranks :child_order,\n    unless: :has_no_parent?,         # Rank only ducks that have a parent. Alternatively a Proc or lambda can be passed, e.g. proc { parent.nil? }\n    with_same: :parent_id\n\n  def has_no_parent?\n    parent.nil?\n  end\nend\n```\n\nWhen you make a query, add the rank:\n\n``` ruby\nDuck.rank(:row_order)\n\nPond.first.ducks.rank(:swimming_order)\n\nDuck.walking.rank(:walking)\n```\n\nDrawbacks\n---------\n\nWhile ranked-model is performant when storing data, it might cause N+1s depending on how you write your code. Consider this snippet:\n\n```ruby\nducks = Duck.all\nducks.map do |duck|\n  {\n    id: duck.id,\n    position: duck.row_order_rank # This causes N+1!\n  }\nend\n```\n\nEvery call to `duck.row_order_rank` will make a call to the DB to check the rank of that\nparticular element. If you have a long list of elements this might cause issues to your DB.\n\nIn order to avoid that, you can use the `rank(:your_rank)` scope and some Ruby code to get\nthe element's position:\n\n```ruby\nducks = Duck.rank(:row_order).all\nducks.map.with_index do |duck, index|\n  {\n    id: duck.id,\n    position: index\n  }\nend\n```\n\nSingle Table Inheritance (STI)\n------------------------------\n\nranked-model scopes your records' positions based on the class name of the object. If you have\na STI `type` column set in your model, ranked-model will reference that class for positioning.\n\nConsider this example:\n\n``` ruby\nclass Vehicle \u003c ActiveRecord::Base\n  ranks :row_order\nend\n\nclass Car \u003c Vehicle\nend\n\nclass Truck \u003c Vehicle\nend\n\ncar = Car.create!\ntruck = Truck.create!\n\ncar.row_order\n=\u003e 0\ntruck.row_order\n=\u003e 0\n```\n\nIn this example, the `row_order` for both `car` and `truck` will be set to `0` because they have\ndifferent class names (`Car` and `Truck`, respectively).\n\nIf you would like for both `car` and `truck` to be ranked together based on the base `Vehicle`\nclass instead, use the `class_name` option:\n\n``` ruby\nclass Vehicle \u003c ActiveRecord::Base\n  ranks :row_order, class_name: 'Vehicle'\nend\n\nclass Car \u003c Vehicle\nend\n\nclass Truck \u003c Vehicle\nend\n\ncar = Car.create!\ntruck = Truck.create!\n\ncar.row_order\n=\u003e 0\ntruck.row_order\n=\u003e 4194304\n```\n\nMigrations for existing data\n----------------------------\n\nIf you use `ranked_model` with existing data, the following migration (for Rails\n6) can be a starting point. Make sure to declare `include RankedModel` and\n`ranks :row_order` in your `Duck` before running the migration.\n\n```bash\nrails g migration AddRowOrderToDucks row_order:integer\n```\n\nThen, adjust the migration:\n```ruby\n# e.g. file db/migrate/20200325095038_add_row_order_to_ducks.rb\nclass AddRowOrderToDucks \u003c ActiveRecord::Migration[6.0]\n  def change\n    add_column :ducks, :row_order, :integer\n\n    # Newest Duck shall rank \"highest\"\" (be last).\n    Duck.update_all('row_order = EXTRACT(EPOCH FROM created_at)')\n\n    # Alternatively, implement any other sorting default\n    # Duck.order(created_at: :desc).each do |duck|\n    #   duck.update!(row_order: duck.created_at.to_i + duck.age / 2)\n    # end\n  end\nend\n```\n\nInternals\n---------\n\nThis library is written using ARel from the ground-up.  This leaves the code much cleaner\nthan many implementations.  ranked-model is also optimized to write to the database as little\nas possible: ranks are stored as a number between -2147483648 and 2147483647 (the INT range in MySQL).\nWhen an item is given a new position, it assigns itself a rank number between two neighbors.\nThis allows several movements of items before no digits are available between two neighbors. When\nthis occurs, ranked-model will try to shift other records out of the way. If items can't be easily\nshifted anymore, it will rebalance the distribution of rank numbers across all members\nof the ranked group.\n\nRecord updates to rebalance ranks do not trigger ActiveRecord callbacks. If you need to react to these updates\n(to index them in a secondary data store, for example), you can subscribe to the `ranked_model.ranks_updated`\n[ActiveSupport notification](https://api.rubyonrails.org/v7.1/classes/ActiveSupport/Notifications.html).\nSubscribed consumers receive an event for each rearrangement or rebalancing, the payload of which includes the\ntriggering instance and the `scope` and `with_same` options for the ranking, which can be used to retrieve the\naffected records.\n\n```ruby\nActiveSupport::Notifications.subscribe(\"ranked_model.ranks_updated\") do |_name, _start, _finish, _id, payload|\n  # payload[:instance] - the instance whose update triggered the rebalance\n  # payload[:scope] - the scope applied to the ranking\n  # payload[:with_same] - the with_same option applied to the ranking\nend\n```\n\nContributing\n------------\n\nFork, clone, write a test, write some code, commit, push, send a pull request.  Github FTW!\n\nThe code is published under the [MIT License](LICENSE).\n\nThe specs can be run with sqlite, postgres, and mysql:\n\n```\nbundle\nappraisal install\nDB=postgresql bundle exec appraisal rake\n```\n\nIf no DB is specified (`sqlite`, `mysql`, or `postgresql`), the tests run against sqlite.\n\nRankedModel is mostly the handiwork of Matthew Beale:\n\n* [madhatted.com](http://madhatted.com) is where I blog. Also [@mixonic](http://twitter.com/mixonic).\n\nA hearty thanks to these contributors:\n\n* [Harvest](http://getharvest.com) where this Gem started. They are great, great folks.\n* [yabawock](https://github.com/yabawock)\n* [AndrewRadev](https://github.com/AndrewRadev)\n* [adheerajkumar](https://github.com/adheerajkumar)\n* [mikeycgto](https://github.com/mikeycgto)\n* [robotex82](https://github.com/robotex82)\n* [rociiu](https://github.com/rociiu)\n* [codepodu](https://github.com/codepodu)\n* [kakra](https://github.com/kakra)\n* [metalon](https://github.com/metalon)\n* [jamesalmond](https://github.com/jamesalmond)\n* [jguyon](https://github.com/jguyon)\n* [pehrlich](https://github.com/pehrlich)\n* [petergoldstein](https://github.com/petergoldstein)\n* [brendon](https://github.com/brendon)\n","funding_links":["https://github.com/sponsors/brendon"],"categories":["ORM/ODM Extensions","Ruby","Gems","ActiveRecord"],"sub_categories":["Articles","Attributes Management"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbrendon%2Franked-model","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbrendon%2Franked-model","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbrendon%2Franked-model/lists"}