├── .github └── workflows │ └── test.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── Appraisals ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── activerecord-pretty-comparator.gemspec ├── bin ├── console └── setup ├── gemfiles ├── activerecord_6.1.gemfile ├── activerecord_7.0.gemfile ├── activerecord_7.1.gemfile ├── activerecord_7.2.gemfile └── activerecord_8.0.gemfile ├── lib ├── active_record │ └── pretty │ │ ├── comparator.rb │ │ └── comparator │ │ └── version.rb └── activerecord-pretty-comparator.rb ├── sig └── active_record │ └── pretty │ └── comparator.rbs └── spec ├── active_record └── pretty │ └── comparator_spec.rb └── spec_helper.rb /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | name: Test on ${{ matrix.activerecord_version }} and Ruby ${{ matrix.ruby_version }} 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | ruby_version: 17 | - "3.4" 18 | - "3.3" 19 | - "3.2" 20 | - "3.1" 21 | activerecord_version: 22 | - "activerecord_8.0" 23 | - "activerecord_7.2" 24 | - "activerecord_7.1" 25 | - "activerecord_7.0" 26 | - "activerecord_6.1" 27 | exclude: 28 | - ruby_version: "3.1" 29 | activerecord_version: "activerecord_8.0" 30 | 31 | steps: 32 | - uses: actions/checkout@v4 33 | - uses: ruby/setup-ruby@v1 34 | with: 35 | ruby-version: ${{ matrix.ruby_version }} 36 | - name: Bundle install 37 | run: bundle install 38 | - name: Setup activerecord 39 | run: bundle exec appraisal install 40 | - name: Test with ${{ matrix.activerecord_version }} 41 | run: bundle exec appraisal ${{ matrix.activerecord_version }} rspec 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | /vendor/ 10 | 11 | # rspec failure tracking 12 | .rspec_status 13 | 14 | gemfiles/*.lock 15 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: rubocop-rspec 2 | AllCops: 3 | TargetRubyVersion: 3.0 4 | Exclude: 5 | - 'activerecord-pretty-comparator.gemspec' 6 | - 'lib/activerecord-pretty-comparator.rb' 7 | - 'vendor/**/*' 8 | RespectGitIgnore: true 9 | SuggestExtensions: false 10 | 11 | Style/StringLiterals: 12 | EnforcedStyle: double_quotes 13 | 14 | Style/StringLiteralsInInterpolation: 15 | EnforcedStyle: double_quotes 16 | 17 | RSpec/IndexedLet: 18 | Enabled: false 19 | 20 | RSpec/MultipleExpectations: 21 | Max: 4 22 | 23 | RSpec/ExampleLength: 24 | Max: 6 25 | 26 | Metrics/BlockLength: 27 | Exclude: 28 | - spec/*.rb 29 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "activerecord_6.1" do 2 | gem "activerecord", "~> 6.1.0" 3 | gem "sqlite3", "~> 1.4" 4 | 5 | gem "database_cleaner" 6 | gem "rake" 7 | gem "rspec" 8 | gem "rubocop" 9 | gem "rubocop-rspec" 10 | end 11 | 12 | appraise "activerecord_7.0" do 13 | gem "activerecord", "~> 7.0.0" 14 | gem "sqlite3", "~> 1.4" 15 | 16 | gem "database_cleaner" 17 | gem "rake" 18 | gem "rspec" 19 | gem "rubocop" 20 | gem "rubocop-rspec" 21 | end 22 | 23 | appraise "activerecord_7.1" do 24 | gem "activerecord", "~> 7.1.0" 25 | gem "sqlite3", "~> 1.4" 26 | 27 | gem "database_cleaner" 28 | gem "rake" 29 | gem "rspec" 30 | gem "rubocop" 31 | gem "rubocop-rspec" 32 | end 33 | 34 | appraise "activerecord_7.2" do 35 | gem "activerecord", "~> 7.2.0" 36 | gem "sqlite3" 37 | 38 | gem "database_cleaner" 39 | gem "rake" 40 | gem "rspec" 41 | gem "rubocop" 42 | gem "rubocop-rspec" 43 | end 44 | 45 | if RUBY_VERSION >= "3.2" 46 | appraise "activerecord_8.0" do 47 | gem "activerecord", "~> 8.0.0" 48 | gem "sqlite3" 49 | 50 | gem "database_cleaner" 51 | gem "rake" 52 | gem "rspec" 53 | gem "rubocop" 54 | gem "rubocop-rspec" 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [Unreleased] 2 | 3 | ## [0.1.1] - 2024-10-19 4 | 5 | - Updated gem metadata on rubygems.org (no functional changes). 6 | 7 | ## [0.1.0] - 2024-07-17 8 | 9 | - Initial release 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in activerecord-pretty-comparator.gemspec 6 | gemspec 7 | 8 | gem "appraisal" 9 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | activerecord-pretty-comparator (0.1.1) 5 | activerecord (>= 6.1) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | activemodel (7.1.3.4) 11 | activesupport (= 7.1.3.4) 12 | activerecord (7.1.3.4) 13 | activemodel (= 7.1.3.4) 14 | activesupport (= 7.1.3.4) 15 | timeout (>= 0.4.0) 16 | activesupport (7.1.3.4) 17 | base64 18 | bigdecimal 19 | concurrent-ruby (~> 1.0, >= 1.0.2) 20 | connection_pool (>= 2.2.5) 21 | drb 22 | i18n (>= 1.6, < 2) 23 | minitest (>= 5.1) 24 | mutex_m 25 | tzinfo (~> 2.0) 26 | appraisal (2.5.0) 27 | bundler 28 | rake 29 | thor (>= 0.14.0) 30 | ast (2.4.2) 31 | base64 (0.2.0) 32 | bigdecimal (3.1.8) 33 | concurrent-ruby (1.3.3) 34 | connection_pool (2.4.1) 35 | database_cleaner (2.0.2) 36 | database_cleaner-active_record (>= 2, < 3) 37 | database_cleaner-active_record (2.2.0) 38 | activerecord (>= 5.a) 39 | database_cleaner-core (~> 2.0.0) 40 | database_cleaner-core (2.0.1) 41 | diff-lcs (1.5.1) 42 | drb (2.2.1) 43 | i18n (1.14.5) 44 | concurrent-ruby (~> 1.0) 45 | json (2.7.2) 46 | language_server-protocol (3.17.0.3) 47 | mini_portile2 (2.8.7) 48 | minitest (5.24.1) 49 | mutex_m (0.2.0) 50 | parallel (1.25.1) 51 | parser (3.3.3.0) 52 | ast (~> 2.4.1) 53 | racc 54 | racc (1.8.0) 55 | rainbow (3.1.1) 56 | rake (13.2.1) 57 | regexp_parser (2.9.2) 58 | rexml (3.3.1) 59 | strscan 60 | rspec (3.13.0) 61 | rspec-core (~> 3.13.0) 62 | rspec-expectations (~> 3.13.0) 63 | rspec-mocks (~> 3.13.0) 64 | rspec-core (3.13.0) 65 | rspec-support (~> 3.13.0) 66 | rspec-expectations (3.13.1) 67 | diff-lcs (>= 1.2.0, < 2.0) 68 | rspec-support (~> 3.13.0) 69 | rspec-mocks (3.13.1) 70 | diff-lcs (>= 1.2.0, < 2.0) 71 | rspec-support (~> 3.13.0) 72 | rspec-support (3.13.1) 73 | rubocop (1.64.1) 74 | json (~> 2.3) 75 | language_server-protocol (>= 3.17.0) 76 | parallel (~> 1.10) 77 | parser (>= 3.3.0.2) 78 | rainbow (>= 2.2.2, < 4.0) 79 | regexp_parser (>= 1.8, < 3.0) 80 | rexml (>= 3.2.5, < 4.0) 81 | rubocop-ast (>= 1.31.1, < 2.0) 82 | ruby-progressbar (~> 1.7) 83 | unicode-display_width (>= 2.4.0, < 3.0) 84 | rubocop-ast (1.31.3) 85 | parser (>= 3.3.1.0) 86 | rubocop-rspec (3.0.3) 87 | rubocop (~> 1.61) 88 | ruby-progressbar (1.13.0) 89 | sqlite3 (1.7.3) 90 | mini_portile2 (~> 2.8.0) 91 | sqlite3 (1.7.3-arm64-darwin) 92 | strscan (3.1.0) 93 | thor (1.3.2) 94 | timeout (0.4.1) 95 | tzinfo (2.0.6) 96 | concurrent-ruby (~> 1.0) 97 | unicode-display_width (2.5.0) 98 | 99 | PLATFORMS 100 | arm64-darwin-22 101 | ruby 102 | 103 | DEPENDENCIES 104 | activerecord-pretty-comparator! 105 | appraisal 106 | database_cleaner 107 | rake 108 | rspec 109 | rubocop 110 | rubocop-rspec 111 | sqlite3 112 | 113 | BUNDLED WITH 114 | 2.5.9 115 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 technuma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ActiveRecord::Pretty::Comparator 2 | 3 | A simple ActiveRecord extension to support `where` with comparison operators (`>`, `>=`, `<`, and `<=`). 4 | 5 | ## Installation 6 | 7 | Add this line to your application's Gemfile: 8 | 9 | ```ruby 10 | gem 'activerecord-pretty-comparator' 11 | ``` 12 | 13 | And then execute: 14 | 15 | $ bundle 16 | 17 | Or install it yourself as: 18 | 19 | $ gem install activerecord-pretty-comparator 20 | 21 | 22 | ## Usage 23 | ```ruby 24 | posts = Post.order(:id) 25 | 26 | posts.where("id >": 9).pluck(:id) # => [10, 11] 27 | posts.where("id >=": 9).pluck(:id) # => [9, 10, 11] 28 | posts.where("id <": 3).pluck(:id) # => [1, 2] 29 | posts.where("id <=": 3).pluck(:id) # => [1, 2, 3] 30 | ``` 31 | 32 | ## Why not `Post.where("id > ?", 1)` ? 33 | 34 | As a main contributor of the predicate builder area, [@kamipo](https://github.com/kamipo) recommends 35 | using the hash syntax, the hash syntax also have other useful 36 | effects (making boundable queries, unscopeable queries, hash-like 37 | relation merging friendly, automatic other table references detection). 38 | ref: https://github.com/rails/rails/pull/39863#issue-659298581 39 | 40 | ### Merge examples 41 | ```ruby 42 | # it doesn't work 43 | # SELECT `posts`.`id` FROM `posts` WHERE (id > 1) AND `posts`.`id` = ? [["id", 1]] 44 | Post.where("id > ?", 1).merge(Post.where(id: 1)).pluck(:id) # [] 45 | 46 | # it works 47 | # SELECT `posts`.`id` FROM `posts` WHERE `posts`.`id` = ? [["id", 1]] 48 | Post.where("id >": 1).merge(Post.where(id: 1)).pluck(:id) # [1] 49 | ``` 50 | 51 | ### Unscope examples 52 | ```ruby 53 | # it doesn't work 54 | # SELECT `posts`.* FROM `posts` WHERE (id > 1) 55 | Post.where("id > ?", 1).unscope(where: :id) 56 | 57 | # it works 58 | # SELECT `posts`.* FROM `posts` 59 | Post.where("id >": 1).unscope(where: :id) 60 | ``` 61 | 62 | ### Precision examples 63 | From type casting and table/column name resolution's point of view, 64 | `where("created_at >=": time)` is better alternative than `where("created_at >= ?", time)`. 65 | 66 | ```ruby 67 | class Post < ActiveRecord::Base 68 | attribute :created_at, :datetime, precision: 3 69 | end 70 | 71 | time = Time.now.utc # => 2020-06-24 10:11:12.123456 UTC 72 | 73 | Post.create!(created_at: time) # => # 74 | 75 | # SELECT `posts`.* FROM `posts` WHERE (created_at >= '2020-06-24 10:11:12.123456') 76 | Post.where("created_at >= ?", time) # => [] 77 | 78 | # SELECT `posts`.* FROM `posts` WHERE `posts`.`created_at` >= '2020-06-24 10:11:12.123000' 79 | Post.where("created_at >=": time) # => [#] 80 | ``` 81 | 82 | ## Development 83 | 84 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 85 | 86 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). 87 | 88 | ## Contributing 89 | 90 | Bug reports and pull requests are welcome on GitHub at https://github.com/technuma/activerecord-pretty-comparator. 91 | 92 | ## License 93 | 94 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 95 | 96 | ## Special thanks 97 | 98 | [@kamipo](https://github.com/kamipo) :bow: 99 | 100 | Original idea: https://blog.kamipo.net/entry/2021/01/14/191753 (ja) 101 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rspec/core/rake_task" 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | require "rubocop/rake_task" 9 | 10 | RuboCop::RakeTask.new 11 | 12 | task default: %i[spec rubocop] 13 | -------------------------------------------------------------------------------- /activerecord-pretty-comparator.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/active_record/pretty/comparator/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "activerecord-pretty-comparator" 7 | spec.version = ActiveRecord::Pretty::Comparator::VERSION 8 | spec.authors = ["Kazuya Onuma", "Ryuta Kamizono"] 9 | spec.email = ["technuma@gmail.com", "kamipo@gmail.com"] 10 | 11 | spec.summary = "A simple ActiveRecord extension to support where with comparison operators (`>`, `>=`, `<`, and `<=`)." 12 | spec.description = "A simple ActiveRecord extension to support where with comparison operators (`>`, `>=`, `<`, and `<=`)." 13 | spec.homepage = "https://github.com/technuma/activerecord-pretty-comparator" 14 | spec.license = "MIT" 15 | spec.required_ruby_version = ">= 3.0.0" 16 | 17 | spec.metadata["homepage_uri"] = spec.homepage 18 | spec.metadata["source_code_uri"] = spec.homepage 19 | spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md" 20 | 21 | gemspec = File.basename(__FILE__) 22 | spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls| 23 | ls.readlines("\x0", chomp: true).reject do |f| 24 | (f == gemspec) || 25 | f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile]) 26 | end 27 | end 28 | spec.bindir = "exe" 29 | spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } 30 | spec.require_paths = ["lib"] 31 | 32 | spec.add_dependency "activerecord", ">= 6.1" 33 | 34 | spec.add_development_dependency "bigdecimal" 35 | spec.add_development_dependency "base64" 36 | spec.add_development_dependency "database_cleaner" 37 | spec.add_development_dependency "mutex_m" 38 | spec.add_development_dependency "rake" 39 | spec.add_development_dependency "rspec" 40 | spec.add_development_dependency "rubocop" 41 | spec.add_development_dependency "rubocop-rspec" 42 | spec.add_development_dependency "sqlite3" 43 | end 44 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "activerecord/pretty/comparator" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | require "irb" 11 | IRB.start(__FILE__) 12 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /gemfiles/activerecord_6.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "appraisal" 6 | gem "activerecord", "~> 6.1.0" 7 | gem "sqlite3", "~> 1.4" 8 | gem "database_cleaner" 9 | gem "rake" 10 | gem "rspec" 11 | gem "rubocop" 12 | gem "rubocop-rspec" 13 | 14 | gemspec path: "../" 15 | -------------------------------------------------------------------------------- /gemfiles/activerecord_7.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "appraisal" 6 | gem "activerecord", "~> 7.0.0" 7 | gem "sqlite3", "~> 1.4" 8 | gem "database_cleaner" 9 | gem "rake" 10 | gem "rspec" 11 | gem "rubocop" 12 | gem "rubocop-rspec" 13 | 14 | gemspec path: "../" 15 | -------------------------------------------------------------------------------- /gemfiles/activerecord_7.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "appraisal" 6 | gem "activerecord", "~> 7.1.0" 7 | gem "sqlite3", "~> 1.4" 8 | gem "database_cleaner" 9 | gem "rake" 10 | gem "rspec" 11 | gem "rubocop" 12 | gem "rubocop-rspec" 13 | 14 | gemspec path: "../" 15 | -------------------------------------------------------------------------------- /gemfiles/activerecord_7.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "appraisal" 6 | gem "activerecord", "~> 7.2.0" 7 | gem "sqlite3" 8 | gem "database_cleaner" 9 | gem "rake" 10 | gem "rspec" 11 | gem "rubocop" 12 | gem "rubocop-rspec" 13 | 14 | gemspec path: "../" 15 | -------------------------------------------------------------------------------- /gemfiles/activerecord_8.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "appraisal" 6 | gem "activerecord", "~> 8.0.0" 7 | gem "sqlite3" 8 | gem "database_cleaner" 9 | gem "rake" 10 | gem "rspec" 11 | gem "rubocop" 12 | gem "rubocop-rspec" 13 | 14 | gemspec path: "../" 15 | -------------------------------------------------------------------------------- /lib/active_record/pretty/comparator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "comparator/version" 4 | require "active_record" 5 | 6 | module ActiveRecord 7 | module Pretty 8 | # :nodoc: 9 | module Comparator 10 | class Error < StandardError; end 11 | ActiveSupport.on_load(:active_record) do 12 | ActiveRecord::PredicateBuilder.prepend(Module.new do 13 | def [](attr_name, value, operator = nil) 14 | if !operator && attr_name.end_with?(">", ">=", "<", "<=") 15 | /\A(?.+?)\s*(?>|>=|<|<=)\z/ =~ attr_name 16 | operator = OPERATORS[operator] 17 | end 18 | 19 | super 20 | end 21 | 22 | OPERATORS = { ">" => :gt, ">=" => :gteq, "<" => :lt, "<=" => :lteq }.freeze # rubocop:disable Lint/ConstantDefinitionInBlock 23 | end) 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/active_record/pretty/comparator/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module Pretty 5 | module Comparator 6 | VERSION = "0.1.1" 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/activerecord-pretty-comparator.rb: -------------------------------------------------------------------------------- 1 | require "active_record/pretty/comparator" 2 | -------------------------------------------------------------------------------- /sig/active_record/pretty/comparator.rbs: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | module Pretty 3 | module Comparator 4 | VERSION: String 5 | # See the writing guide of rbs: https://github.com/ruby/rbs#guides 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/active_record/pretty/comparator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ActiveRecord::Pretty::Comparator do 4 | it "has a version number" do 5 | expect(ActiveRecord::Pretty::Comparator::VERSION).not_to be_nil 6 | end 7 | 8 | describe "simple Post Model" do 9 | describe "where with comparison operator key" do 10 | let!(:post1) { Post.create! } 11 | let!(:post2) { Post.create! } 12 | let!(:post3) { Post.create! } 13 | let(:posts) { Post.order(:id) } 14 | 15 | it "correctly applies comparison operators in where clauses" do 16 | expect(posts.where("id >": post1.id).pluck(:id)).to eq([post2.id, post3.id]) 17 | expect(posts.where("id >=": post1.id).pluck(:id)).to eq([post1.id, post2.id, post3.id]) 18 | expect(posts.where("id <": post2.id).pluck(:id)).to eq([post1.id]) 19 | expect(posts.where("id <=": post2.id).pluck(:id)).to eq([post1.id, post2.id]) 20 | end 21 | end 22 | 23 | describe "alias_attribute :comments_count, :legacy_comments_count" do 24 | before do 25 | Post.create!(comments_count: 4) 26 | Post.create!(comments_count: 5) 27 | Post.create!(comments_count: 6) 28 | end 29 | 30 | it "allows querying using the aliased attribute name 'comments_count'" do 31 | expect(Post.where("comments_count >=": 5).count).to eq(2) 32 | expect(Post.where("comments_count >": 5).count).to eq(1) 33 | expect(Post.where("comments_count <=": 5).count).to eq(2) 34 | expect(Post.where("comments_count <": 5).count).to eq(1) 35 | end 36 | end 37 | 38 | describe "#unscope" do 39 | let(:post1) { Post.create!(comments_count: 1) } 40 | let!(:comment1) { post1.comments.create! } 41 | let(:post2) { Post.create!(comments_count: 1) } 42 | let!(:comment2) { post2.comments.create! } 43 | 44 | it "correctly unscopes table name qualified column" do 45 | comments = Comment.joins(:post).where("posts.id <=": post1.id) 46 | expect(comments).to eq([comment1]) 47 | 48 | comments = comments.where("id >=": post2.id) 49 | expect(comments).to be_empty 50 | 51 | comments = comments.unscope(where: :"posts.id") 52 | expect(comments).to eq([comment2]) 53 | end 54 | end 55 | 56 | describe "#merge" do 57 | let!(:post1) { Post.create! } 58 | let!(:post2) { Post.create! } 59 | 60 | it "merges with a post condition" do 61 | expect(Post.where("id <=": post2.id).merge(Post.where(id: post2.id))).to eq([post2]) 62 | 63 | # See https://github.com/rails/rails/commit/515aa1e 64 | if ActiveRecord.version >= Gem::Version.new('7.0') 65 | expect(Post.where(id: post2.id).merge(Post.where("id <=": post2.id))).to eq([post1, post2]) 66 | else 67 | expect(Post.where(id: post2.id).merge(Post.where("id <=": post2.id))).to eq([post2]) 68 | end 69 | end 70 | end 71 | 72 | describe "references detection" do 73 | let(:post) { Post.create!(comments_count: 1) } 74 | let!(:comment) { post.comments.create! } 75 | 76 | it "correctly adds references when using string or hash conditions" do 77 | expect(Post.eager_load(:comments).where("comments.id >= ?", comment.id).references_values).to eq([]) 78 | expect(Post.eager_load(:comments).where("comments.id >=": comment.id).references_values).to eq(["comments"]) 79 | end 80 | end 81 | 82 | describe "datetime precision" do 83 | let(:time) { Time.utc(2014, 8, 17, 12, 30, 0, 999_999) } 84 | 85 | before do 86 | Post.create!(created_at: time, updated_at: time) 87 | end 88 | 89 | it "formats datetime according to precision" do 90 | expect(Post.find_by("created_at >= ?", time)).to be_nil 91 | expect(Post.where("updated_at >= ?", time).count).to eq(0) 92 | expect(Post.find_by("created_at >=": time)).to be_truthy 93 | expect(Post.where("updated_at >=": time).count).to eq(1) 94 | end 95 | end 96 | 97 | describe "time precision" do 98 | let(:time) { Time.utc(2000, 1, 1, 12, 30, 0, 999_999) } 99 | 100 | before do 101 | Post.create!(start: time, finish: time) 102 | end 103 | 104 | it "handles time precision correctly" do 105 | expect(Post.find_by("start >= ?", time)).to be_nil 106 | expect(Post.where("finish >= ?", time).count).to eq(0) 107 | 108 | expect(Post.find_by("start >=": time)).to be_truthy 109 | expect(Post.where("finish >=": time).count).to eq(1) 110 | end 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "logger" 4 | require "active_record/pretty/comparator" 5 | 6 | require "database_cleaner" 7 | 8 | RSpec.configure do |config| 9 | # Enable flags like --only-failures and --next-failure 10 | config.example_status_persistence_file_path = ".rspec_status" 11 | 12 | # Disable RSpec exposing methods globally on `Module` and `main` 13 | config.disable_monkey_patching! 14 | 15 | config.expect_with :rspec do |c| 16 | c.syntax = :expect 17 | end 18 | config.before(:suite) do 19 | ActiveRecord::Base.logger = Logger.new($stdout) 20 | ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") 21 | ActiveRecord::Schema.define do 22 | create_table :posts, force: true do |t| 23 | t.integer :legacy_comments_count, default: 0 24 | t.time :start, precision: 0 25 | t.time :finish, precision: 4 26 | t.datetime :created_at, precision: 0 27 | t.datetime :updated_at, precision: 4 28 | end 29 | 30 | create_table :comments, force: true do |t| 31 | t.integer :post_id 32 | end 33 | end 34 | 35 | class Post < ActiveRecord::Base # rubocop: disable Lint/ConstantDefinitionInBlock 36 | has_many :comments 37 | alias_attribute :comments_count, :legacy_comments_count 38 | end 39 | 40 | class Comment < ActiveRecord::Base # rubocop: disable Lint/ConstantDefinitionInBlock 41 | belongs_to :post 42 | end 43 | end 44 | config.before do 45 | DatabaseCleaner.start 46 | end 47 | config.after do 48 | DatabaseCleaner.clean 49 | end 50 | end 51 | --------------------------------------------------------------------------------