{"id":13879111,"url":"https://github.com/toptal/database_validations","last_synced_at":"2025-04-14T23:30:22.599Z","repository":{"id":32923907,"uuid":"146292684","full_name":"toptal/database_validations","owner":"toptal","description":"Database validations for ActiveRecord","archived":false,"fork":false,"pushed_at":"2023-06-18T16:06:06.000Z","size":909,"stargazers_count":545,"open_issues_count":3,"forks_count":13,"subscribers_count":121,"default_branch":"master","last_synced_at":"2025-04-07T16:13:57.883Z","etag":null,"topics":["activerecord","database","performance","ruby","ruby-on-rails"],"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/toptal.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2018-08-27T12:11:58.000Z","updated_at":"2025-03-24T04:58:09.000Z","dependencies_parsed_at":"2024-01-13T20:57:05.452Z","dependency_job_id":"67302f26-612b-426a-8de4-70058934c08f","html_url":"https://github.com/toptal/database_validations","commit_stats":{"total_commits":121,"total_committers":12,"mean_commits":"10.083333333333334","dds":"0.25619834710743805","last_synced_commit":"33e196d2a86ba141a4e5b97b0278a048afff3a33"},"previous_names":["djezzzl/database_validations"],"tags_count":20,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/toptal%2Fdatabase_validations","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/toptal%2Fdatabase_validations/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/toptal%2Fdatabase_validations/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/toptal%2Fdatabase_validations/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/toptal","download_url":"https://codeload.github.com/toptal/database_validations/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248978206,"owners_count":21192738,"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","database","performance","ruby","ruby-on-rails"],"created_at":"2024-08-06T08:02:10.211Z","updated_at":"2025-04-14T23:30:22.577Z","avatar_url":"https://github.com/toptal.png","language":"Ruby","readme":"# DatabaseValidations\n\n[![CircleCI](https://circleci.com/gh/toptal/database_validations/tree/master.svg?style=svg)](https://circleci.com/gh/toptal/database_validations/tree/master)\n[![Gem Version](https://badge.fury.io/rb/database_validations.svg)](https://badge.fury.io/rb/database_validations)\n[![Maintainability](https://api.codeclimate.com/v1/badges/a7df40a29c63f7ba518b/maintainability)](https://codeclimate.com/github/toptal/database_validations/maintainability)\n\nDatabaseValidations helps you to keep the database consistency with better performance.\nRight now, it supports only ActiveRecord.\n\n*The more you use the gem, the more performance increase you have. Try it now!*\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'database_validations'\n```\n\nAnd then execute:\n\n```bash\nbundle\n```\n\nOr install it yourself as:\n\n```bash\ngem install database_validations\n```\n\nHave a look at [example](example) application for details.\n\n## Benchmark ([code](benchmarks/composed_benchmarks.rb))\n\nImagine, you have `User` model defines as\n\n```ruby\nclass User \u003c ActiveRecord::Base\n  validates :email, :full_name, uniqueness: true\n\n  belongs_to :company\n  belongs_to :country\nend\n```\n\nand then replace with\n\n```ruby\nclass User \u003c ActiveRecord::Base\n  validates :email, :full_name, db_uniqueness: true\n  # OR\n  # validates_db_uniqueness_of :email, :full_name\n\n  db_belongs_to :company\n  db_belongs_to :country\n  # OR\n  # belongs_to :company\n  # belongs_to :country\n  # validates :company, :country, db_presence: true\nend\n```\n\nyou will get the following performance improvement:\n\n![](benchmarks/composed.png)\n\n## Caveats\n\n- `db_belongs_to` doesn't work with SQLite due to a poor error message.\n- In Rails 4, the gem validations work differently than the ActiveRecord ones when `validate: false` option is passed to `save`/`save!`. They incorrectly return a validation message instead of raising a proper constraint violation exception. In Rails \u003e= 5 they correctly raise the exceptions they supposed to.\n\n## db_belongs_to\n\nSupported databases are `PostgreSQL` and `MySQL`.\n**Note**: Unfortunately, `SQLite` raises a poor error message\nby which we can not determine exact foreign key which raised an error.\n\n### Usage\n\n```ruby\nclass User \u003c ActiveRecord::Base\n  db_belongs_to :company\nend\n\nuser = User.create(company_id: nil)\n# =\u003e false\nuser.errors.messages\n# =\u003e {:company=\u003e[\"must exist\"]}\n```\n\n### Problem\n\nActiveRecord's `belongs_to` has `optional: false` by default. Unfortunately, this\napproach does not ensure existence of the related object. For example, we can skip\nvalidations or remove the related object after we save the object. After that, our\ndatabase becomes inconsistent because we assume the object has his relation but it\ndoes not.\n\n`db_belongs_to` solves the problem using foreign key constraints in the database\nalso providing backward compatibility with nice validations errors.\n\n### Pros and Cons\n\n**Advantages**:\n- Ensures relation existence because it uses foreign keys constraints.\n- Checks the existence of proper foreign key constraint at the boot time.\nUse `ENV['SKIP_DB_UNIQUENESS_VALIDATOR_INDEX_CHECK'] = 'true'` if you want to\nskip it in some cases. (For example, when you run migrations.) _Note:_ we skip it for the abstract classes.\n- It's almost two times faster because it skips unnecessary SQL query. See benchmarks\nbelow for details.\n\n**Disadvantages**:\n- Cannot handle multiple database validations at once because database\nraises only one error per query.\n\n### Configuration options\n\n| Option name   | PostgreSQL | MySQL |\n| ------------- | :--------: | :---: |\n| class_name    | +          | +     |\n| foreign_key   | +          | +     |\n| foreign_type  | -          | -     |\n| primary_key   | +          | +     |\n| dependent     | +          | +     |\n| counter_cache | +          | +     |\n| polymorphic   | -          | -     |\n| validate      | +          | +     |\n| autosave      | +          | +     |\n| touch         | +          | +     |\n| inverse_of    | +          | +     |\n| optional      | -          | -     |\n| required      | -          | -     |\n| default       | +          | +     |\n\n### Benchmarks ([code](benchmarks/db_belongs_to_benchmark.rb))\n\n![](benchmarks/db_belongs_to.png)\n\n## validates_db_uniqueness_of\n\nSupported databases are `PostgreSQL`, `MySQL` and `SQLite`.\n\n### Usage\n\n```ruby\nclass User \u003c ActiveRecord::Base\n  validates :email, db_uniqueness: true\n  # The same as following:\n  # validates :email, uniqueness: {case_sensitive: true, allow_nil: true, allow_blank: false}\nend\n\noriginal = User.create(email: 'email@mail.com')\ndupe = User.create(email: 'email@mail.com')\n# =\u003e false\ndupe.errors.messages\n# =\u003e {:email=\u003e[\"has already been taken\"]}\nUser.create!(email: 'email@mail.com')\n# =\u003e ActiveRecord::RecordInvalid Validation failed: email has already been taken\n```\n\nComplete `case_sensitive` replacement example (for `PostgreSQL` only):\n\n```ruby\nvalidates :slug, uniqueness: { case_sensitive: false, scope: :field }\n```\n\nShould be replaced by:\n\n```ruby\nvalidates :slug, db_uniqueness: {index_name: :unique_index, case_sensitive: false, scope: :field}\n```\n\n**Keep in mind**: because `valid?` method uses default validator you should:\n\n- if your index has many fields, provide proper `scope` option\n- if your index has lower function, provide `case_sensitive` option\n- if your index has where condition, provide proper `where` option\n\n### Problem\n\nUnfortunately, ActiveRecord's `validates_uniqueness_of` approach does not ensure\nuniqueness. For example, we can skip validations or create two records in parallel\nqueries. After that, our database becomes inconsistent because we assume some uniqueness\nover the table but it has duplicates.\n\n`validates_db_uniqueness_of` solves the problem using unique index constraints\nin the database also providing backward compatibility with nice validations errors.\n\n### Advantages\n\n- Ensures uniqueness because it uses unique constraints.\n- Checks the existence of proper unique index at the boot time.\nUse `ENV['SKIP_DB_UNIQUENESS_VALIDATOR_INDEX_CHECK'] = 'true'`\nif you want to skip it in some cases. (For example, when you run migrations.) _Note:_ we skip it for the abstract classes.\n- It's two times faster in average because it skips unnecessary SQL query. See benchmarks below for details.\n- It has different [modes](#modes) so you can pick up the best for your needs.\n\n### Configuration options\n\n| Option name    | PostgreSQL | MySQL | SQLite |\n| -------------- | :--------: | :---: | :----: |\n| mode           | +          | +     | +      |\n| scope          | +          | +     | +      |\n| message        | +          | +     | +      |\n| if             | +          | +     | +      |\n| unless         | +          | +     | +      |\n| index_name     | +          | +     | -      |\n| where          | +          | -     | -      |\n| case_sensitive | +          | -     | -      |\n| allow_nil      | -          | -     | -      |\n| allow_blank    | -          | -     | -      |\n\n### Rescue option\n\nThe validation has an option `:rescue` with two values:\n- `:default` (default option) that follows default ActiveRecord behavior. It respects `validate: false` option for `save/save!` (for example, this is being used for nested associations)\n- `:always` that catches database constraint errors and turns them to ActiveRecord validations filling `.errors` properly. \n\nYou may want to use `rescue: :always` in case you save nested associations with `accepts_nested_attributes_for` helper and you want the validation to happen automatically when a user\nprovides duplicated data in the same request.\n\n### Modes\n\nThere are 3 `mode` options:\n\n- `:optimized` - the default one. In this mode it turns DB constraint exceptions into proper validation messages.\n- `:enhanced` - a combination of the standard uniqueness validation and the db uniqueness validation. Runs a query first but also rescues from exception. The preferable mode for user-facing validations.\n- `:standard` - in this mode works pretty much the same way as `validates_uniqueness_of` (except the index existence check).\n\n### Benchmark ([code](benchmarks/uniqueness_validator_benchmark.rb))\n\n![](benchmarks/validates_db_uniqueness_of.png)\n\n## Testing (RSpec)\n\nAdd `require database_validations/rspec/matchers'` to your `spec` file.\n\n### validate_db_uniqueness_of\n\nExample:\n\n```ruby\nclass User \u003c ActiveRecord::Base\n  validates_db_uniqueness_of :field, message: 'duplicate', where: '(some_field IS NULL)', scope: :another_field, index_name: :unique_index\nend\n\ndescribe 'validations' do\n  subject { User }\n\n  it { is_expected.to validate_db_uniqueness_of(:field).with_message('duplicate').with_where('(some_field IS NULL)').scoped_to(:another_field).with_index(:unique_index) }\nend\n```\n\n## Using with RuboCop\n\nDatabaseValidations provides custom cops for RuboCop to help you consistently apply the improvements.\nTo use all of them, use `rubocop --require database_validations/rubocop/cops` or add to your `.rubocop.yml` file:\n\n```yaml\nrequire:\n  - database_validations/rubocop/cops\n```\n\nOr you case use some specific cop directly:\n```yaml\nrequire:\n  - database_validations/rubocop/cop/belongs_to\n  - database_validations/rubocop/cop/uniqueness_of\n```\n\n## Development\n\nYou need to have installed and running `postgresql` and `mysql`.\nAnd for each adapter manually create a database called `database_validations_test` accessible by your local user.\n\nThen, run `rake spec` to run the tests.\n\nTo check the conformance with the style guides, run:\n\n```bash\nrubocop\n```\n\nTo run benchmarks, run:\n\n```bash\nruby -I lib benchmarks/composed_benchmarks.rb\n```\n\nTo install this gem onto your local machine, run `bundle exec rake install`.\nTo release a new version, update the version number in `version.rb`, and then\nrun `bundle exec rake release`, which will create a git tag for the version,\npush git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).\n\n## Contributing\n\n[Bug reports](https://github.com/toptal/database_validations/issues)\nand [pull requests](https://github.com/toptal/database_validations/pulls) are\nwelcome on GitHub. This project is intended to be a safe, welcoming space for\ncollaboration, and contributors are expected to adhere\nto the [Contributor Covenant](http://contributor-covenant.org) code of conduct.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n\n## Code of Conduct\n\nEveryone interacting in the DatabaseValidations project’s codebases, issue trackers, chat rooms and mailing\nlists is expected to follow the [code of conduct](https://github.com/toptal/database_validations/blob/master/CODE_OF_CONDUCT.md).\n\n## Contributors\n\n- [Evgeniy Demin](https://github.com/djezzzl) (author)\n- [Filipp Pirozhkov](https://github.com/pirj)\n- [Maxim Krizhanovski](https://github.com/Darhazer)\n- [Alfonso Uceda](https://github.com/AlfonsoUceda)\n","funding_links":[],"categories":["Ruby"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftoptal%2Fdatabase_validations","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftoptal%2Fdatabase_validations","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftoptal%2Fdatabase_validations/lists"}