{"id":13521080,"url":"https://github.com/palkan/n_plus_one_control","last_synced_at":"2025-05-15T04:05:18.860Z","repository":{"id":21646734,"uuid":"93502827","full_name":"palkan/n_plus_one_control","owner":"palkan","description":"RSpec and Minitest matchers to prevent N+1 queries problem","archived":false,"fork":false,"pushed_at":"2024-08-22T18:07:30.000Z","size":111,"stargazers_count":567,"open_issues_count":2,"forks_count":20,"subscribers_count":8,"default_branch":"master","last_synced_at":"2025-05-12T15:16:53.170Z","etag":null,"topics":["database-testing","hacktoberfest","minitest","performance-testing","rspec","ruby","testing"],"latest_commit_sha":null,"homepage":"","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/palkan.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}},"created_at":"2017-06-06T09:50:59.000Z","updated_at":"2025-04-28T15:10:41.000Z","dependencies_parsed_at":"2022-07-08T16:01:29.033Z","dependency_job_id":"e1aadf6c-7025-40f8-bf7f-da9b7153c6fb","html_url":"https://github.com/palkan/n_plus_one_control","commit_stats":{"total_commits":83,"total_committers":16,"mean_commits":5.1875,"dds":0.4337349397590361,"last_synced_commit":"62bde8a9ca8ef742e093543b52eb4efaff8f496d"},"previous_names":[],"tags_count":15,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/palkan%2Fn_plus_one_control","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/palkan%2Fn_plus_one_control/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/palkan%2Fn_plus_one_control/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/palkan%2Fn_plus_one_control/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/palkan","download_url":"https://codeload.github.com/palkan/n_plus_one_control/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254270643,"owners_count":22042859,"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":["database-testing","hacktoberfest","minitest","performance-testing","rspec","ruby","testing"],"created_at":"2024-08-01T06:00:28.181Z","updated_at":"2025-05-15T04:05:13.844Z","avatar_url":"https://github.com/palkan.png","language":"Ruby","funding_links":[],"categories":["Ruby","Matchers","Gems"],"sub_categories":["Performance Optimization"],"readme":"[![Gem Version](https://badge.fury.io/rb/n_plus_one_control.svg)](https://rubygems.org/gems/n_plus_one_control)\n![Build](https://github.com/palkan/n_plus_one_control/workflows/Build/badge.svg)\n\n# N + 1 Control\n\nRSpec and Minitest matchers to prevent the N+1 queries problem.\n\n\u003cimg src=\"https://s3.amazonaws.com/anycable/n_plus_one_control.png\" alt=\"Example output\" width=\"553px\"\u003e\n\n### Why yet another gem to assert DB queries?\n\nUnlike other libraries (such as [db-query-matchers](https://github.com/brigade/db-query-matchers), [rspec-sqlimit](https://github.com/nepalez/rspec-sqlimit), etc), with `n_plus_one_control` you don't have to specify exact expectations to control your code behaviour (e.g. `expect { subject }.to query(2).times`).\n\nSuch expectations are rather hard to maintain, 'cause there is a big chance of adding more queries, not related to the system under test.\n\nNPlusOneControl works differently. It evaluates the code under consideration several times with different scale factors to make sure that the number of DB queries behaves as expected (i.e. O(1) instead of O(N)).\n\nSo, it's for _performance_ testing and not _feature_ testing.\n\n\u003e Read also [\"Squash N+1 queries early with n_plus_one_control test matchers for Ruby and Rails\"](https://evilmartians.com/chronicles/squash-n-plus-one-queries-early-with-n-plus-one-control-test-matchers-for-ruby-and-rails).\n\n### Why not just use [`bullet`](https://github.com/flyerhzm/bullet)?\n\nOf course, it's possible to use Bullet in tests (see more [here](https://evilmartians.com/chronicles/fighting-the-hydra-of-n-plus-one-queries)), but it's not a _silver bullet_: there can be both false positives and true negatives.\n\nThis gem was born after I've found myself not able to verify with a test yet another N+1 problem.\n\n\u003ca href=\"https://evilmartians.com/\"\u003e\n\u003cimg src=\"https://evilmartians.com/badges/sponsored-by-evil-martians.svg\" alt=\"Sponsored by Evil Martians\" width=\"236\" height=\"54\"\u003e\u003c/a\u003e\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngroup :test do\n  gem \"n_plus_one_control\"\nend\n```\n\nAnd then execute:\n\n    $ bundle\n\n## Usage\n\n### RSpec\n\nFirst, add NPlusOneControl to your `spec_helper.rb`:\n\n```ruby\n# spec_helper.rb\nrequire \"n_plus_one_control/rspec\"\n```\n\nThen:\n\n```ruby\n# Wrap example into a context with :n_plus_one tag\ncontext \"N+1\", :n_plus_one do\n  # Define `populate` callbacks which is responsible for data\n  # generation (and whatever else).\n  #\n  # It accepts one argument – the scale factor (read below)\n  populate { |n| create_list(:post, n) }\n\n  specify do\n    expect { get :index }.to perform_constant_number_of_queries\n  end\nend\n```\n\n**NOTE:** do not use memoized values within the expectation block!\n\n```ruby\n# BAD – won't work!\nsubject { get :index }\n\nspecify do\n  expect { subject }.to perform_constant_number_of_queries\nend\n\n# GOOD\nspecify do\n  expect { get :index }.to perform_constant_number_of_queries\nend\n\n# BAD — the `page` record would be removed from the database\n# but still present in RSpec (due to `let`'s memoization)\nlet(:page) { create(:page) }\n\npopulate { |n| create_list(:comment, n, page: page) }\n\nspecify do\n  expect { get :show, params: {id: page.id} }.to perform_constant_number_of_queries\nend\n\n# GOOD\n# Ensure the record is created before `populate`\nlet!(:page) { create(:page) }\n\npopulate { |n| create_list(:comment, n, page: page) }\n# ...\n```\n\nAvailables modifiers:\n\n```ruby\n# You can specify the RegExp to filter queries.\n# By default, it only considers SELECT queries.\nexpect { get :index }.to perform_constant_number_of_queries.matching(/INSERT/)\n\n# You can also provide custom scale factors\nexpect { get :index }.to perform_constant_number_of_queries.with_scale_factors(10, 100)\n\n# You can specify the exact number of expected queries\nexpect { get :index }.to perform_constant_number_of_queries.exactly(1)\n```\n\n#### Using scale factor in spec\n\nLet's suppose your action accepts parameter, which can make impact on the number of returned records:\n\n```ruby\nget :index, params: {per_page: 10}\n```\n\nThen it is enough to just change `per_page` parameter between executions and do not recreate records in DB. For this purpose, you can use `current_scale` method in your example:\n\n```ruby\ncontext \"N+1\", :n_plus_one do\n  before { create_list :post, 3 }\n\n  specify do\n    expect { get :index, params: {per_page: current_scale} }.to perform_constant_number_of_queries\n  end\nend\n```\n\n### Expectations in execution block\n\nBoth rspec matchers allows you to put additional expectations inside execution block to ensure that tested piece of code actually does what expected.\n\n```ruby\ncontext \"N+1\", :n_plus_one do\n  specify do\n    expect do\n      expect(my_query).to eq(actuall_results)\n    end.to perform_constant_number_of_queries\n  end\nend\n```\n\n#### Other available matchers\n\n`perform_linear_number_of_queries(slope: 1)` allows you to test that a query generates linear number of queries with the given slope.  \n\n```ruby\ncontext \"when has linear query\", :n_plus_one do\n  populate { |n| create_list(:post, n) }\n\n  specify do\n    expect { Post.find_each { |p| p.user.name } }\n      .to perform_linear_number_of_queries(slope: 1)\n  end\nend\n```\n\n### Minitest\n\nFirst, add NPlusOneControl to your `test_helper.rb`:\n\n```ruby\n# test_helper.rb\nrequire \"n_plus_one_control/minitest\"\n```\n\nThen use `assert_perform_constant_number_of_queries` assertion method:\n\n```ruby\ndef test_no_n_plus_one_error\n  populate = -\u003e(n) { create_list(:post, n) }\n\n  assert_perform_constant_number_of_queries(populate: populate) do\n    get :index\n  end\nend\n```\n\nYou can also use `assert_perform_linear_number_of_queries` to test for linear queries:\n\n```ruby\ndef test_no_n_plus_one_error\n  populate = -\u003e(n) { create_list(:post, n) }\n\n  assert_perform_linear_number_of_queries(slope: 1, populate: populate) do\n    Post.find_each { |p| p.user.name }\n  end\nend\n```\n\nYou can also specify custom scale factors or filter patterns:\n\n```ruby\nassert_perform_constant_number_of_queries(\n  populate: populate,\n  scale_factors: [2, 5, 10]\n) do\n  get :index\nend\n\nassert_perform_constant_number_of_queries(\n  populate: populate,\n  matching: /INSERT/\n) do\n  do_some_havey_stuff\nend\n```\n\nFor the constant matcher, you can also specify the expected number of queries as the first argument:\n\n```ruby\nassert_perform_constant_number_of_queries(2, populate: populate) do\n  get :index\nend\n```\n\nIt's possible to specify a filter via `NPLUSONE_FILTER` env var, e.g.:\n\n```ruby\nNPLUSONE_FILTER = users bundle exec rake test\n```\n\nYou can also specify `populate` as a test class instance method:\n\n```ruby\ndef populate(n)\n  create_list(:post, n)\nend\n\ndef test_no_n_plus_one_error\n  assert_perform_constant_number_of_queries do\n    get :index\n  end\nend\n\n```\n\nAs in RSpec, you can use `current_scale` factor instead of `populate` block:\n\n```ruby\ndef test_no_n_plus_one_error\n  assert_perform_constant_number_of_queries do\n    get :index, params: {per_page: current_scale}\n  end\nend\n```\n\n### With caching\n\nIf you use caching you can face the problem when first request performs more DB queries than others. The solution is:\n\n```ruby\n# RSpec\n\ncontext \"N + 1\", :n_plus_one do\n  populate { |n| create_list :post, n }\n\n  warmup { get :index } # cache something must be cached\n\n  specify do\n    expect { get :index }.to perform_constant_number_of_queries\n  end\nend\n\n# Minitest\n\ndef populate(n)\n  create_list(:post, n)\nend\n\ndef warmup\n  get :index\nend\n\ndef test_no_n_plus_one_error\n  assert_perform_constant_number_of_queries do\n    get :index\n  end\nend\n\n# or with params\n\ndef test_no_n_plus_one\n  populate = -\u003e(n) { create_list(:post, n) }\n  warmup = -\u003e { get :index }\n\n  assert_perform_constant_number_of_queries population: populate, warmup: warmup do\n    get :index\n  end\nend\n```\n\nIf your `warmup` and testing procs are identical, you can use:\n\n```ruby\nexpect { get :index }.to perform_constant_number_of_queries.with_warming_up # RSpec only\n```\n\n### Configuration\n\nThere are some global configuration parameters (and their corresponding defaults):\n\n```ruby\n# Default scale factors to use.\n# We use the smallest possible but representative scale factors by default.\nNPlusOneControl.default_scale_factors = [2, 3]\n\n# Print performed queries if true in the case of failure\n# You can activate verbosity through env variable NPLUSONE_VERBOSE=1\nNPlusOneControl.verbose = false\n\n# Print table hits difference, for example:\n#\n#   Unmatched query numbers by tables:\n#     users (SELECT): 2 != 3\n#     events (INSERT): 1 != 2\n#\nself.show_table_stats = true\n\n# Ignore matching queries\nNPlusOneControl.ignore = /^(BEGIN|COMMIT|SAVEPOINT|RELEASE)/\n\n# Ignore queries in cache \n# https://guides.rubyonrails.org/configuring.html#configuring-query-cache\nNPlusOneControl.ignore_cached_queries = false\n\n# ActiveSupport notifications event to track queries.\n# We track ActiveRecord event by default,\n# but can also track rom-rb events ('sql.rom') as well.\nNPlusOneControl.event = \"sql.active_record\"\n\n# configure transactional behaviour for populate method\n# in case of use multiple database connections\nNPlusOneControl::Executor.tap do |executor|\n  connections = ActiveRecord::Base.connection_handler.connection_pool_list.map(\u0026:connection)\n\n  executor.transaction_begin = -\u003e do\n    connections.each { |connection| connection.begin_transaction(joinable: false) }\n  end\n  executor.transaction_rollback = -\u003e do\n    connections.each(\u0026:rollback_transaction)\n  end\nend\n\n# Provide a backtrace cleaner callable object used to filter SQL caller location to display in the verbose mode\n# Set it to nil to disable tracing.\n#\n# In Rails apps, we use Rails.backtrace_cleaner by default.\nNPlusOneControl.backtrace_cleaner = -\u003e(locations_array) { do_some_filtering(locations_array) }\n\n# You can also specify the number of backtrace lines to show.\n# MOTE: It could be specified via NPLUSONE_BACKTRACE env var\nNPlusOneControl.backtrace_length = 1\n\n# Sometime queries could be too large to provide any meaningful insight.\n# You can configure an output length limit for queries in verbose mode by setting the following option\n# NOTE: It could be specified via NPLUSONE_TRUNCATE env var\nNPlusOneControl.truncate_query_size = 100\n```\n\n## How does it work?\n\nTake a look at our [Executor](https://github.com/palkan/n_plus_one_control/blob/master/lib/n_plus_one_control/executor.rb) to figure out what's under the hood.\n\n## What's next?\n\n- More matchers.\n\nIt may be useful to provide more matchers/assertions, for example:\n\n```ruby\n\n# Actually, that means that it is N+1))\nassert_linear_number_of_queries { some_code }\n\n# But we can tune it with `coef` and handle such cases as selecting in batches\nassert_linear_number_of_queries(coef: 0.1) do\n  Post.find_in_batches { some_code }\nend\n\n# probably, also make sense to add another curve types\nassert_logarithmic_number_of_queries { some_code }\n```\n\n- Support custom non-SQL events.\n\nN+1 problem is not a database specific: we can have N+1 Redis calls, N+1 HTTP external requests, etc.\nWe can make `n_plus_one_control` customizable to support these scenarios (technically, we need to make it possible to handle different payload in the event subscriber).\n\nIf you want to discuss or implement any of these, feel free to open an [issue](https://github.com/palkan/n_plus_one_control/issues) or propose a [pull request](https://github.com/palkan/n_plus_one_control/pulls).\n\n## Development\n\n```sh\n# install deps\nbundle install\n\n# run tests\nbundle exec rake\n```\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/palkan/n_plus_one_control.\n\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%2Fpalkan%2Fn_plus_one_control","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpalkan%2Fn_plus_one_control","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpalkan%2Fn_plus_one_control/lists"}