From 12d742143254f12d0dbe3d0369254e375c154766 Mon Sep 17 00:00:00 2001 From: Eugene Volkovich Date: Tue, 6 Sep 2022 16:03:11 +0300 Subject: [PATCH] Bump to 2.0: Pull original fork master (rzane's fork) (#3) * Find the table * Use polyamorous from master * With https://github.com/activerecord-hackery/ransack/pull/1004, all but one polymorphism test is fixed. There are still several failures. Basically, any test that does an outer join is still doing an inner join. * Explain how this is completely broken * Add byebug for development/testing * Have Active Record build tables for their aliases This may help with rzane/baby_squeel#97. * Fix test for equivalent logic The new version of Active Record or Polyamorous seems to switch the order of these two SQL conditions, but the new statement is logically equivalent to the old one. * Use `variants` for SQL snapshot change * Make it possible to get good polymorphic joins Actually getting polymorphic joins to work correctly will require either explicitly specifying the master branch of the `ransack` project (commit c9cc20de9e0f or something with equivalent functionality) in the project `Gemfile` or the `ransack` project releasing a new 2.x (> 2.3.2) version with that functionality. Addresses most of the Active Record 5.2.x (>= 5.2.1) problems in rzane/baby_squeel#97. The only remaining issues are around inner joins after left joins. * Track all associations used to find correct alias By tracking the associations accumulated within an expression, we can best even Active Record's own `#where` method's ability to correctly set conditions on column values via associations. * Reimplement #find_join_association iteratively Replacing recursive method calls with plain iteration is almost certainly more performant. * Revert unnecessary changes from commit 90e02e2 * Lock down the supported versions of ActiveRecord * Update rspec, rake, and sqlite3 * Run against Ruby 2.6 * Drop support for Active Record versions that have reached EOL * Remove old variants * Remove broken image from the readme * Install latest bundler * Explicit require test dependencies * Remove filewatcher * Run on GitHub actions * Require coveralls * Run as a matrix * Share the environment variables * Use the coveralls action * Not sure what is going on here... * Run against 5.2.0 and 5.2.5 * Give each job a name * Remove coverage * Mark failing tests as pending * Get 5.2.0 passing again * Remove coverage * Bump * Merge `join_dependency` back into this project, because my dreams did not really come true * Fix checking for version as string * Add changes from 1.4.0.beta1 to CHANGELOG.md * Build pull requests * remove old code from activerecord < 5.2 * removed BabySqueel::Pluck * reduce complexety after merging `join_dependency` back into this project * Fix table alias when joining a polymorphic table twice (#108) * Update changelog for v1.4.0 * Bump version to v1.4.0 * Tidy up version comparison code * Bump version to v1.4.1 * add support for activerecord 6.0 * add support for activerecord 6.1 * speed up BabySqueel::ActiveRecord::VersionHelper * add support for activerecord 7.0 * update .github/workflows/build.yml to test rails 7.0 and ruby 3.0 / 3.1 * Apply version-specific monkey-patches outside of the method definition * Update changelog for v1.4.2 * v1.4.2 * ISSUE-117: left_joins performs INNER JOIN with ActiveRecord 6.1.4.4 * Update changelog for 1.4.3 release * Bump to v1.4.3 * ISSUE-119: Nested merge-joins query causes NoMethodError with ActiveRecord 6.1.4.4 #119 * Prepare v1.4.4 * ISSUE-121: Drop support for ActiveRecord older than 6.0 * AR 6.1: fix - FrozenError: can't modify frozen object: [] * Bump v2.0.0 Co-authored-by: Ray Zane Co-authored-by: Richard Weeks Co-authored-by: Jonas S --- .github/workflows/build.yml | 41 +++++ .gitignore | 2 + .travis.yml | 22 --- CHANGELOG.md | 118 +++++++++++++-- Gemfile | 20 ++- ISSUE_TEMPLATE.md | 2 +- README.md | 13 +- Rakefile | 67 -------- baby_squeel.gemspec | 11 +- lib/baby_squeel.rb | 7 +- lib/baby_squeel/active_record/calculations.rb | 13 -- .../active_record/query_methods.rb | 51 +++++-- .../active_record/version_helper.rb | 12 ++ lib/baby_squeel/association.rb | 20 +-- lib/baby_squeel/calculation.rb | 24 ++- lib/baby_squeel/errors.rb | 10 -- lib/baby_squeel/join_dependency.rb | 143 +++++++++++++----- lib/baby_squeel/pluck.rb | 25 --- lib/baby_squeel/version.rb | 2 +- .../__snapshots__/association_spec.yaml | 8 +- .../query_methods/injector6_1_spec.rb | 37 +++++ spec/baby_squeel/association_spec.rb | 48 ++---- spec/baby_squeel/calculation_spec.rb | 7 - .../join_dependency/injector6_0_spec.rb | 43 ++++++ .../join_dependency/injector_spec.rb | 32 ---- spec/baby_squeel/pluck_spec.rb | 21 --- .../__snapshots__/joining_spec.yaml | 56 +++++-- .../__snapshots__/rails_integration_spec.yaml | 14 ++ .../__snapshots__/sifting_spec.yaml | 10 +- .../__snapshots__/when_having_spec.yaml | 36 +---- .../__snapshots__/where_chain_spec.yaml | 69 +++------ spec/integration/averaging_spec.rb | 2 +- spec/integration/counting_spec.rb | 2 +- spec/integration/joining_spec.rb | 64 +++++--- spec/integration/maximizing_spec.rb | 2 +- spec/integration/minimizing_spec.rb | 2 +- spec/integration/rails_integration_spec.rb | 21 +++ spec/integration/sifting_spec.rb | 4 +- spec/integration/summing_spec.rb | 2 +- spec/integration/when_having_spec.rb | 8 +- spec/integration/where_chain_spec.rb | 36 ++--- spec/spec_helper.rb | 19 +-- spec/support/matchers.rb | 5 +- 43 files changed, 638 insertions(+), 513 deletions(-) create mode 100644 .github/workflows/build.yml delete mode 100644 .travis.yml create mode 100644 lib/baby_squeel/active_record/version_helper.rb delete mode 100644 lib/baby_squeel/pluck.rb create mode 100644 spec/baby_squeel/active_record/query_methods/injector6_1_spec.rb create mode 100644 spec/baby_squeel/join_dependency/injector6_0_spec.rb delete mode 100644 spec/baby_squeel/join_dependency/injector_spec.rb delete mode 100644 spec/baby_squeel/pluck_spec.rb create mode 100644 spec/integration/__snapshots__/rails_integration_spec.yaml create mode 100644 spec/integration/rails_integration_spec.rb diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..dd7ea21 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,41 @@ +name: Build +on: + push: + branches: [master] + pull_request: + branches: [master] +jobs: + build: + runs-on: ubuntu-latest + name: Active Record ${{ matrix.activerecord }} on ruby ${{ matrix.ruby }} (compat=${{ matrix.compat }}) + strategy: + matrix: + activerecord: ["~> 6.0", "~> 6.1", "~> 7.0"] + compat: ["0", "1"] + ruby: [ "2.6", "2.7", "3.0", "3.1"] + exclude: + - activerecord: "~> 7.0" + ruby: "2.6" + env: + AR: ${{ matrix.activerecord }} + COMPAT: ${{ matrix.compat }} + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + + - name: Install packages + run: sudo apt-get install libsqlite3-dev + + - name: Install bundler + run: gem install bundler + + - name: Install dependencies + run: bundle install + + - name: Test + run: bundle exec rspec diff --git a/.gitignore b/.gitignore index 0cb6eeb..cc09deb 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ /pkg/ /spec/reports/ /tmp/ +.byebug_history +.idea/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 191dbff..0000000 --- a/.travis.yml +++ /dev/null @@ -1,22 +0,0 @@ -language: ruby - -rvm: - - 2.4.0 - -before_install: gem install bundler -v 1.11.2 -after_script: bundle exec rake coveralls:push - -env: - global: - - COVERALLS_REPO_TOKEN=X5ZWSPW8VW2JXNJO0qNjlZCQNW4ju89gb - matrix: - - AR=4.2.10 - - AR=5.0.6 - - AR=5.1.5 - - AR=5.2.0 - - AR=latest - - AR=4.2.10 COMPAT=1 - - AR=5.0.6 COMPAT=1 - - AR=5.1.5 COMPAT=1 - - AR=5.2.0 COMPAT=1 - - AR=latest COMPAT=1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3839784..442985c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,134 +1,227 @@ ## [Unreleased] -Nothing to see here. +- AR 6.1: fix - FrozenError: can't modify frozen object: [] +- Drop support for ActiveRecord older than 6.0. + +## [1.4.4] - 2022-02-07 + +### Fixed + +- Nested merge-joins query causes NoMethodError with ActiveRecord 6.1.4.4 (#119) + +## [1.4.3] - 2022-02-04 + +### Fixed + +- ActiveRecord::Relation#left_joins performs INNER JOIN on Active Record 6.1 (#118) + +## [1.4.2] - 2022-01-24 + +### Fixed + +- Added support for activerecord 7.0 (#116) +- Added support for activerecord 6.1 (#116) +- Added support for activerecord 6.0 (#116) + +## [1.4.1] - 2021-06-17 + +### Fixed + +- Fixed a bug related to checking the Active Record version. + +## [1.4.0] - 2021-06-17 + +### Fixed + +- Fix table alias when joining a polymorphic table twice (#108) +- Removed internal class `BabySqueel::Pluck`. You can still use `plucking`. For example, `Post.joining { author }.plucking { author.name }` +- Removed old code from Active Record < 5.2 +- Removed dependency `join_dependency` + +## [1.4.0.beta1] - 2021-04-21 + +### Fixed + +- Add Support for activerecord '>= 5.2.3' +- Drop Support for Active Record versions that have reached EOL (activerecord < 5.2) +- Use polyamorous from ransack '~> 2.3' ## [1.3.1] - 2018-05-15 -## Fixed + +### Fixed + - Upgraded `join_dependency` requirement, which fixes [issue #1](https://github.com/rzane/join_dependency/issues/1). ## [1.3.0] - 2018-05-04 -## Added + +### Added + - The ability to use `plucking` with an array of nodes. For example, `User.plucking { [id, name] }`. ## [1.2.1] - 2018-04-25 -## Fixed + +### Fixed + - Added support for Active Record 5.2 ## [1.2.0] - 2017-10-20 + ### Added + - `reordering`, which is just a BabySqueel version of Active Record's `reorder`. - `on` expressions can now be given a block that will yield the current node (#77). ## [1.1.5] - 2017-05-26 + ### Fixed + - Returning an empty hash from a `where.has {}` block would generate invalid SQL (#69). ## [1.1.4] - 2017-04-13 + ### Fixed + - Nodes::Attribute#in and #not_in generate valid SQL when given ActiveRecord::NullRelations. ## [1.1.3] - 2017-03-31 + ### Fixed + - Nodes::Attribute#in was not returning BabySqueel node. As a result, you couldn't chain on it. This fixes #61. ## [1.1.2] - 2017-03-21 + ### Fixed + - Check if a reflection has a parent reflection before comparing them. This fixes #56. ### Refactored + - The logic encapsulated in `#method_missing` and `#respond_to_missing?` was difficult to follow, because it was falling back to `super`, sometimes going up the inheritance tree multiple levels. The addition of `BabySqueel::Resolver` now handles this a little more gracefully. ## [1.1.1] - 2017-02-14 + ### Fixed + - There is a bug in Active Record where the `AliasTracker` initializes `Arel::Table`s use the wrong `type_caster`. To address this, BabySqueel must re-initialize the `Arel::Table` with the correct `type_caster` (#54). ## [1.1.0] - 2017-02-10 + > This version drops support for Active Record 4.1. If you're stil on 4.1, you should seriously consider upgrading to at least 4.2. ### Added + - DSLs for ActiveRecord::Relation::Calculations. You can now use `plucking`, `counting`, `summing`, `averaging`, `minimizing`, and `maximizing`. ## [1.0.3] - 2017-02-09 + ### Added + - Support for `pluck`. - Support for `not_in`. ## [1.0.2] - 2017-02-07 + ### Added + - `BabySqueel::Association` now has `#==` and `#!=`. This is only supported for Rails 5+. Example: `Post.where { author: Author.last }`. ### Fixed + - Incorrect alias detection caused by not tracking the full path to a join (#37). ## [1.0.1] - 2016-11-07 + ### Added -- Add DSL#_ for wrapping expressions in Arel::Node::Grouping. Thanks to [@odedniv]. + +- Add DSL#\_ for wrapping expressions in Arel::Node::Grouping. Thanks to [@odedniv]. ### Fixed + - Use strings for attribute names like Rails does. Symbols were preventing things like `unscope` from working. Thanks to [@chewi]. - `where.has {}` will now accept `nil`. - Arel::Nodes::Function did not previously include Arel::Math, so now you can do math operations on the result of SQL functions. - Arel::Nodes::Binary did not previously include Arel::AliasPredication. Binary nodes can now be aliased using `as`. ## [1.0.0] - 2016-09-09 + ### Added -- Polyamorous. Unfortunately, this *does* monkey-patch Active Record internals, but there just isn't any other reliable way to generate outer joins. Baby Squeel, itself, will still keep monkey patching to an absolute minimum. + +- Polyamorous. Unfortunately, this _does_ monkey-patch Active Record internals, but there just isn't any other reliable way to generate outer joins. Baby Squeel, itself, will still keep monkey patching to an absolute minimum. - Within DSL blocks, you can use `exists` and `not_exists` with Active Record relations. For example: `Post.where.has { exists Post.where(title: 'Fun') }`.` - Support for polymorphic associations. ### Deprecations + - Removed support for Active Record 4.0.x ### Changed + - BabySqueel::JoinDependency is no longer a class responsible for creating Arel joins. It is now a namespace for utilities used when working with the ActiveRecord::Association::JoinDependency class. - BabySqueel::Nodes::Generic is now BabySqueel::Nodes::Node. - Arel nodes are only extended with the behaviors they need. Previously, all Arel nodes were being extended with `Arel::AliasPredication`, `Arel::OrderPredications`, and `Arel::Math`. ### Fixed + - Fixed deprecation warnings on Active Record 5 when initializing an Arel::Table without a type caster. - No more duplicate joins. Previously, Baby Squeel did a very poor job of ensuring that you didn't join an association twice. -- Alias detection should now *actually* work. The previous implementation was naive. +- Alias detection should now _actually_ work. The previous implementation was naive. ## [0.3.1] - 2016-08-02 + ### Added + - Ported backticks and #my from Squeel ### Changed + - DSL#sql now returns a node wrapped in a BabySqueel proxy. ## [0.3.0] - 2016-06-26 + ### Added + - Added Squeel compatibility mode that allows `select`, `order`, `joins`, `group`, `where`, and `having` to accept DSL blocks. - Added the ability to query tables that aren't backed by Active Record models. - Added `BabySqueel::[]`, which provides a `BabySqueel::Relation` for models, or a `BabySqueel::Table` for symbols/strings. ### Changed + - Renamed `BabySqueel::Association::AliasingError` to `BabySqueel::AssociationAliasingError`. ## [0.2.2] - 2016-03-30 + ### Added + - Support for `group` (`grouping`) and `having` (`when_having`). - Support for sifters. - Added `quoted` and `sql` helpers for quoting strings and SQL literals. - More descriptive error messages when a column or association is not found. ### Fixed + - `Arel::Nodes::Grouping` does not include `Arel::Math`, so operations like `(id + 5) + 3` would fail unexpectedly. - Fix missing bind values When joining through associations with default scope. - Removed `ActiveRecord::VERSION` specific handling of the `WhereChain`. ## [0.2.1] - 2016-03-27 + ### Added + - Support for subqueries. ### Fixed + - Some Arel nodes did not have access to `as` expressions. ## [0.2.0] - 2016-03-25 + ### Added + - References to aliased joins in a `select`, `where`, or `order` expression now use the aliased table name. ### Changed + - Rely on `ActiveRecord::Relation#join_sources` for the implicit construction of join nodes, rather than using the `ActiveRecord::Associations::JoinDependency` directly. ### Fixed @@ -136,10 +229,18 @@ Nothing to see here. - Associations referencing the same table weren't being aliased. ## [0.1.0] - 2016-03-16 + ### Added + - Initial support for selects, orders, wheres, and joins. -[Unreleased]: https://github.com/rzane/baby_squeel/compare/v1.3.1...HEAD +[unreleased]: https://github.com/rzane/baby_squeel/compare/v1.4.4...HEAD +[1.4.4]: https://github.com/rzane/baby_squeel/compare/v1.4.3...v1.4.4 +[1.4.3]: https://github.com/rzane/baby_squeel/compare/v1.4.2...v1.4.3 +[1.4.2]: https://github.com/rzane/baby_squeel/compare/v1.4.1...v1.4.2 +[1.4.1]: https://github.com/rzane/baby_squeel/compare/v1.4.0...v1.4.1 +[1.4.0]: https://github.com/rzane/baby_squeel/compare/v1.4.0.beta1...v1.4.0 +[1.4.0.beta1]: https://github.com/rzane/baby_squeel/compare/v1.3.1...v1.4.0.beta1 [1.3.1]: https://github.com/rzane/baby_squeel/compare/v1.3.0...v1.3.1 [1.3.0]: https://github.com/rzane/baby_squeel/compare/v1.2.1...v1.3.0 [1.2.1]: https://github.com/rzane/baby_squeel/compare/v1.2.0...v1.2.1 @@ -159,6 +260,5 @@ Nothing to see here. [0.2.2]: https://github.com/rzane/baby_squeel/compare/v0.2.1...v0.2.2 [0.2.1]: https://github.com/rzane/baby_squeel/compare/v0.2.0...v0.2.1 [0.2.0]: https://github.com/rzane/baby_squeel/compare/v0.1.0...v0.2.0 - [@chewi]: https://github.com/chewi [@odedniv]: https://github.com/odedniv diff --git a/Gemfile b/Gemfile index ac68266..fc3c017 100644 --- a/Gemfile +++ b/Gemfile @@ -6,17 +6,33 @@ gemspec case ENV.fetch('AR', 'latest') when 'latest' gem 'activerecord' + gem 'sqlite3', '~> 1.4' when 'master' gem 'activerecord', github: 'rails/rails' + gem 'sqlite3', '~> 1.4' else gem 'activerecord', ENV['AR'] + + gem 'sqlite3', '~> 1.4' +end + +case ENV.fetch('RANSACK', 'latest') +when 'latest' + gem 'ransack', require: false +when 'master' + gem 'ransack', github: 'activerecord-hackery/ransack', require: false +else + ENV['RANSACK'].split('#').tap do |repo, branch| + opts = {git: repo, require: false} + opts[:branch] = branch if branch + gem 'ransack', opts + end end gem 'bump' group :test do gem 'pry' - gem 'coveralls' gem 'simplecov' - gem 'filewatcher' + gem 'byebug' end diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index 2db864b..1597705 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -11,7 +11,7 @@ require 'minitest/autorun' gemfile true do source 'https://rubygems.org' - gem 'activerecord', '~> 5.0.0' # which Active Record version? + gem 'activerecord', '~> 6.0.0' # which Active Record version? gem 'sqlite3' gem 'baby_squeel', github: 'rzane/baby_squeel' end diff --git a/README.md b/README.md index 4e968c5..fe09502 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,7 @@ # BabySqueel 🐷 -[![Build Status](https://travis-ci.org/rzane/baby_squeel.svg?branch=master)](https://travis-ci.org/rzane/baby_squeel) -[![Code Climate](https://codeclimate.com/github/rzane/baby_squeel/badges/gpa.svg)](https://codeclimate.com/github/rzane/baby_squeel) -[![Coverage Status](https://coveralls.io/repos/github/rzane/baby_squeel/badge.svg?branch=master)](https://coveralls.io/github/rzane/baby_squeel?branch=master) - -biddy piggy +![Build](https://github.com/rzane/baby_squeel/workflows/Build/badge.svg) +![Version](https://img.shields.io/gem/v/baby_squeel) Have you ever used the [Squeel](https://github.com/activerecord-hackery/squeel) gem? It's a really nice way to build complex queries. However, Squeel monkeypatches Active Record internals, because it was aimed at enhancing the existing API with the aim of inclusion into Rails. However, that inclusion never happened, and it left Squeel susceptible to breakage from arbitrary changes in Active Record, eventually burning out the maintainer. @@ -258,6 +255,7 @@ Post.joining { ##### Polymorphism Given this polymorphism: + ```ruby # app/models/picture.rb belongs_to :imageable, polymorphic: true @@ -267,6 +265,7 @@ has_many :pictures, as: :imageable ``` The query might look like this: + ```ruby Picture. joining { imageable.of(Post) }. @@ -316,7 +315,7 @@ Post.joins(:author).where.has { The following methods give you access to BabySqueel's DSL: | BabySqueel | Active Record Equivalent | -|---------------|--------------------------| +| ------------- | ------------------------ | | `selecting` | `select` | | `ordering` | `order` | | `joining` | `joins` | @@ -330,7 +329,7 @@ Check out the [migration guide](https://github.com/rzane/baby_squeel/wiki/Migrat ## Development -1. Pick an Active Record version to develop against, then export it: `export AR=4.2.6`. +1. Pick an Active Record version to develop against, then export it: `export AR=6.1.4`. 2. Run `bin/setup` to install dependencies. 3. Run `rake` to run the specs. diff --git a/Rakefile b/Rakefile index 7e297ff..31992df 100644 --- a/Rakefile +++ b/Rakefile @@ -1,76 +1,9 @@ -require 'yaml' -require 'open3' -require 'filewatcher' require 'bundler/gem_tasks' require 'rspec/core/rake_task' -require 'coveralls/rake/task' require 'bump/tasks' -Coveralls::RakeTask.new - RSpec::Core::RakeTask.new(:spec) -def invoke(env, cmd) - Bundler.with_clean_env do - system({ 'SKIPCOV' => '1' }.merge(env), cmd) - end -end - -def format_version(env, failed: false) - vars = env.map { |k, v| "#{k}=#{v}" } - msg = "=== #{vars.join(' ')} " - msg << 'failed ' if failed - msg << '=' * (80 - msg.length) -end - -def bundle_install(env) - FileUtils.rm_rf 'Gemfile.lock' - invoke env, 'bundle check --quiet > /dev/null || bundle install --quiet' -end - -def version_passes?(env) - puts format_version(env) - bundle_install(env) - invoke env, 'bundle exec rspec -f progress' if $?.success? - $?.success? -end - -desc 'Switch ActiveRecord version' -task :switch, [:version] do |_, args| - abort 'No version specified.' unless args[:version] - bundle_install 'AR' => args[:version] - puts "export AR=#{args[:version]}" -end - -desc 'Run against all ActiveRecord versions' -task 'spec:matrix' do - travis = YAML.load_file '.travis.yml' - - envs = travis['env']['matrix'].map do |build| - Hash[build.split(/=|\s+/).each_slice(2).to_a] - end - - envs.each do |env| - unless version_passes? env - abort format_version(env, failed: true) - end - end -end - -desc 'Watch for changes and rerun specs' -task 'spec:watch' do - puts 'Watching...' - Filewatcher.new(['lib', 'spec']).watch do |filename| - Bundler.with_clean_env do - if filename =~ /_spec\.rb$/ - system "bundle exec rspec #{filename}" - else - system 'bundle exec rspec' - end - end - end -end - Bump::Bump::BUMPS.each do |bump| desc "Increment #{bump} and release" task "release:#{bump}" => "bump:#{bump}" do diff --git a/baby_squeel.gemspec b/baby_squeel.gemspec index 7515033..ab42740 100644 --- a/baby_squeel.gemspec +++ b/baby_squeel.gemspec @@ -19,12 +19,11 @@ Gem::Specification.new do |spec| spec.files = Dir.glob('{lib/**/*,*.{md,txt,gemspec}}') - spec.add_dependency 'activerecord', '>= 4.2.0' - spec.add_dependency 'polyamorous', '~> 1.3' - spec.add_dependency 'join_dependency', '~> 0.1.2' + spec.add_dependency 'activerecord', '>= 6.0', '< 7.1' + spec.add_dependency 'ransack', '~> 2.3' - spec.add_development_dependency 'bundler', '~> 1.11' - spec.add_development_dependency 'rake', '~> 10.0' - spec.add_development_dependency 'rspec', '~> 3.4.0' + spec.add_development_dependency 'bundler', '~> 2' + spec.add_development_dependency 'rake', '~> 13.0' + spec.add_development_dependency 'rspec', '~> 3.10' spec.add_development_dependency 'sqlite3' end diff --git a/lib/baby_squeel.rb b/lib/baby_squeel.rb index 4272f40..045d84b 100644 --- a/lib/baby_squeel.rb +++ b/lib/baby_squeel.rb @@ -1,6 +1,11 @@ require 'active_record' require 'active_record/relation' -require 'polyamorous' +begin + require 'polyamorous' +rescue LoadError + # Trying loading from 'ransack' as of commit c9cc20de9 (post v2.3.2) + require 'polyamorous/polyamorous' +end require 'baby_squeel/version' require 'baby_squeel/errors' require 'baby_squeel/active_record/base' diff --git a/lib/baby_squeel/active_record/calculations.rb b/lib/baby_squeel/active_record/calculations.rb index bb9a974..83e2550 100644 --- a/lib/baby_squeel/active_record/calculations.rb +++ b/lib/baby_squeel/active_record/calculations.rb @@ -1,12 +1,10 @@ require 'baby_squeel/calculation' -require 'baby_squeel/pluck' module BabySqueel module ActiveRecord module Calculations def plucking(&block) nodes = Array.wrap(DSL.evaluate(self, &block)) - nodes = nodes.map { |node| Pluck.decorate(node) } pluck(*nodes) end @@ -40,17 +38,6 @@ def aggregate_column(column_name) super end end - - if ::ActiveRecord::VERSION::MAJOR < 5 - # @override - def type_for(field) - if field.kind_of? Calculation - field - else - super - end - end - end end end end diff --git a/lib/baby_squeel/active_record/query_methods.rb b/lib/baby_squeel/active_record/query_methods.rb index a9c585a..7e2dcc2 100644 --- a/lib/baby_squeel/active_record/query_methods.rb +++ b/lib/baby_squeel/active_record/query_methods.rb @@ -1,9 +1,26 @@ require 'baby_squeel/dsl' require 'baby_squeel/join_dependency' +require 'baby_squeel/active_record/version_helper' module BabySqueel module ActiveRecord module QueryMethods + # This class allows BabySqueel to slip custom + # joins_values into Active Record's JoinDependency + module Injector6_1 + def each(&block) + super do |join| + if join.is_a?(BabySqueel::Join) + result = block.binding.local_variables.include?(:result) && block.binding.local_variable_get(:result) + result << join if result + join + else + block.call(join) + end + end + end + end + # Constructs Arel for ActiveRecord::QueryMethods#joins using the DSL. def joining(&block) joins DSL.evaluate(self, &block) @@ -34,19 +51,33 @@ def when_having(&block) having DSL.evaluate(self, &block) end - private + if BabySqueel::ActiveRecord::VersionHelper.at_least_6_1? + def construct_join_dependency(associations, join_type) + result = super(associations, join_type) + if associations.any? { |assoc| assoc.is_a?(BabySqueel::Join) } + result.extend(BabySqueel::JoinDependency::Injector6_1) + end + result + end + + private - # This is a monkey patch, and I'm not happy about it. - # Active Record will call `group_by` on the `joins`. The - # Injector has a custom `group_by` method that handles - # BabySqueel::Join nodes. - if ::ActiveRecord::VERSION::MAJOR >= 5 && ::ActiveRecord::VERSION::MINOR >= 2 - def build_joins(manager, joins, aliases) - super manager, BabySqueel::JoinDependency::Injector.new(joins), aliases + # https://github.com/rails/rails/commit/c0c53ee9d28134757cf1418521cb97c4a135f140 + def select_association_list(*args) + if args[0].any? { |join| join.is_a?(BabySqueel::Join) } + args[0].extend(BabySqueel::ActiveRecord::QueryMethods::Injector6_1) + end + super *args end else - def build_joins(manager, joins) - super manager, BabySqueel::JoinDependency::Injector.new(joins) + private + + # Active Record will call `each` on the `joins`. The + # Injector has a custom `each` method that handles + # BabySqueel::Join nodes. + def build_joins(*args) + args[1] = BabySqueel::JoinDependency::Injector6_0.new(args.second) + super(*args) end end end diff --git a/lib/baby_squeel/active_record/version_helper.rb b/lib/baby_squeel/active_record/version_helper.rb new file mode 100644 index 0000000..2be8b64 --- /dev/null +++ b/lib/baby_squeel/active_record/version_helper.rb @@ -0,0 +1,12 @@ +require 'baby_squeel/dsl' + +module BabySqueel + module ActiveRecord + class VersionHelper + def self.at_least_6_1? + ::ActiveRecord::VERSION::MAJOR > 6 || + ::ActiveRecord::VERSION::MAJOR == 6 && ::ActiveRecord::VERSION::MINOR >= 1 + end + end + end +end diff --git a/lib/baby_squeel/association.rb b/lib/baby_squeel/association.rb index 153767f..52c8b36 100644 --- a/lib/baby_squeel/association.rb +++ b/lib/baby_squeel/association.rb @@ -1,4 +1,5 @@ require 'baby_squeel/relation' +require 'baby_squeel/active_record/version_helper' module BabySqueel class Association < Relation @@ -96,19 +97,18 @@ def _arel(associations = []) private - if ActiveRecord::VERSION::MAJOR >= 5 - def build_where_clause(other) - if valid_where_clause?(other) - relation = @parent._scope.all + def build_where_clause(other) + if valid_where_clause?(other) + relation = @parent._scope.all + + if BabySqueel::ActiveRecord::VersionHelper.at_least_6_1? + relation.send(:build_where_clause, { _reflection.name => other }, []) + else factory = relation.send(:where_clause_factory) factory.build({ _reflection.name => other }, []) - else - raise AssociationComparisonError.new(_reflection.name, other) end - end - else - def build_where_clause(_) - raise AssociationComparisonNotSupportedError.new(_reflection.name) + else + raise AssociationComparisonError.new(_reflection.name, other) end end diff --git a/lib/baby_squeel/calculation.rb b/lib/baby_squeel/calculation.rb index da2dbd3..a38dea1 100644 --- a/lib/baby_squeel/calculation.rb +++ b/lib/baby_squeel/calculation.rb @@ -6,12 +6,6 @@ def initialize(node) @node = node end - # This is only used in 4.2. We're just pretending to be - # a database column to fake the casting here. - def type_cast_from_database(value) - value - end - # In Active Record 5, we don't *need* this class to make # calculations work. They happily accept arel. However, # when grouping with a calculation, there's a really, @@ -21,15 +15,19 @@ def type_cast_from_database(value) # caching because the alias would have a unique name every # time. def to_s - names = node.map do |child| - if child.kind_of?(String) || child.kind_of?(Symbol) - child.to_s - elsif child.respond_to?(:name) - child.name.to_s + if node.respond_to?(:map) + names = node.map do |child| + if child.kind_of?(String) || child.kind_of?(Symbol) + child.to_s + elsif child.respond_to?(:name) + child.name.to_s + end end + names.compact.uniq.join('_') + else + # fix for https://github.com/rails/rails/commit/fc38ff6e4417295c870f419f7c164ab5a7dbc4a5 + node.to_sql.split('"').map { |v| v.tr('^A-Za-z0-9_', '').presence }.compact.uniq.join('_') end - - names.compact.uniq.join('_') end end end diff --git a/lib/baby_squeel/errors.rb b/lib/baby_squeel/errors.rb index efcda41..c8aa68f 100644 --- a/lib/baby_squeel/errors.rb +++ b/lib/baby_squeel/errors.rb @@ -62,14 +62,4 @@ def initialize(name, other) super "You can't compare association '#{name}' to #{other}." end end - - class AssociationComparisonNotSupportedError < StandardError # :nodoc: - MESSAGE = - "Querying association '%{name}' with '==' and '!=' " \ - "is only supported for ActiveRecord >=5." - - def initialize(name) - super format(MESSAGE, name: name) - end - end end diff --git a/lib/baby_squeel/join_dependency.rb b/lib/baby_squeel/join_dependency.rb index 249f85c..bdde798 100644 --- a/lib/baby_squeel/join_dependency.rb +++ b/lib/baby_squeel/join_dependency.rb @@ -1,77 +1,140 @@ -require 'join_dependency' +require 'baby_squeel/active_record/version_helper' module BabySqueel module JoinDependency # This class allows BabySqueel to slip custom # joins_values into Active Record's JoinDependency - class Injector < Array # :nodoc: - # Active Record will call group_by on this object - # in ActiveRecord::QueryMethods#build_joins. This - # allows BabySqueel::Joins to be treated - # like typical join hashes until Polyamorous can - # deal with them. - def group_by + class Injector6_0 < Array # :nodoc: + # https://github.com/rails/rails/pull/36805/files + # This commit changed group_by to each + def each(&block) super do |join| - case join - when BabySqueel::Join - :association_join + if block.binding.local_variables.include?(:buckets) + buckets = block.binding.local_variable_get(:buckets) + + case join + when BabySqueel::Join + buckets[:association_join] << join + else + block.call(join) + end else - yield join + block.call(join) end end end end + # This is a 'fix' for the left outer joins + # rails way would be to call left_outer_joins so the join_type gets set to Arel::Nodes::OuterJoin + # Maybe this could be fixed in joining but I do not know how. + module Injector6_1 # :nodoc: + def make_constraints(parent, child, join_type) # :nodoc: + join_type = child.join_type if child.join_type == Arel::Nodes::OuterJoin + super(parent, child, join_type) + end + end + class Builder # :nodoc: attr_reader :join_dependency def initialize(relation) - @join_dependency = ::JoinDependency.from_relation(relation) do |join| - :association_join if join.kind_of? BabySqueel::Join - end + @join_dependency = build(relation, collect_joins(relation)) end # Find the alias of a BabySqueel::Association, by passing # a list (in order of chaining) of associations and finding # the respective JoinAssociation at each level. def find_alias(associations) - table = find_join_association(associations).table - reconstruct_with_type_caster(table, associations) + if BabySqueel::ActiveRecord::VersionHelper.at_least_6_1? + # construct_tables! got removed by rails + # https://github.com/rails/rails/commit/590b045ee2c0906ff162e6658a184afb201865d7 + # + # construct_tables_for_association! is a method from the polyamorous (ransack) gem + join_root = join_dependency.send(:join_root) + join_root.each_children do |parent, child| + join_dependency.construct_tables_for_association!(parent, child) + end + else + # If we tell join_dependency to construct its tables, Active Record + # handles building the correct aliases and attaching them to its + # JoinDepenencies. + join_dependency.send(:construct_tables!, join_dependency.send(:join_root)) + end + + join_association = find_join_association(associations) + join_association.table end private + Associations = ::ActiveRecord::Associations + def find_join_association(associations) - associations.inject(join_dependency.send(:join_root)) do |parent, assoc| - parent.children.find do |join_association| - reflections_equal?( - assoc._reflection, - join_association.reflection - ) - end + current = join_dependency.send(:join_root) + + associations.each do |association| + name = association._reflection.name + current = current.children.find { |c| c.reflection.name == name && klass_equal?(association, c) } + break if current.nil? end + + current end - # Compare two reflections and see if they're the same. - def reflections_equal?(a, b) - comparable_reflection(a) == comparable_reflection(b) + # If association is not polymorphic return true. + # If association is polymorphic compare the association polymorphic class with the join association base_klass + def klass_equal?(assoc, join_association) + return true unless assoc._reflection.polymorphic? + + assoc._polymorphic_klass == join_association.base_klass end - # Get the parent of the reflection if it has one. - # In AR4, #parent_reflection returns [name, reflection] - # In AR5, #parent_reflection returns just a reflection - def comparable_reflection(reflection) - [*reflection.parent_reflection].last || reflection + def collect_joins(relation) + joins = [] + joins += relation.joins_values + joins += relation.left_outer_joins_values + + buckets = joins.group_by do |join| + case join + when String + :string_join + when Hash, Symbol, Array + :association_join + when Associations::JoinDependency + :stashed_join + when Arel::Nodes::Join + :join_node + when BabySqueel::Join + :association_join + else + raise("unknown class: %s" % join.class.name) + end + end end - # Active Record 5's AliasTracker initializes Arel tables - # with the type_caster belonging to the wrong model. - # - # See: https://github.com/rails/rails/pull/27994 - def reconstruct_with_type_caster(table, associations) - return table if ::ActiveRecord::VERSION::MAJOR < 5 - type_caster = associations.last._scope.type_caster - ::Arel::Table.new(table.name, type_caster: type_caster) + def build(relation, buckets) + buckets.default = [] + association_joins = buckets[:association_join] + stashed_association_joins = buckets[:stashed_join] + join_nodes = buckets[:join_node].uniq + string_joins = buckets[:string_join].map(&:strip).uniq + + joins = string_joins.map do |join| + relation.table.create_string_join(Arel.sql(join)) unless join.blank? + end.compact + + join_list = join_nodes + joins + + alias_tracker = Associations::AliasTracker.create(relation.klass.connection, relation.table.name, join_list) + join_dependency = Associations::JoinDependency.new(relation.klass, relation.table, association_joins, Arel::Nodes::InnerJoin) + join_dependency.instance_variable_set(:@alias_tracker, alias_tracker) + + join_nodes.each do |join| + join_dependency.send(:alias_tracker).aliases[join.left.name.downcase] = 1 + end + + join_dependency end end end diff --git a/lib/baby_squeel/pluck.rb b/lib/baby_squeel/pluck.rb deleted file mode 100644 index 5edc069..0000000 --- a/lib/baby_squeel/pluck.rb +++ /dev/null @@ -1,25 +0,0 @@ -module BabySqueel - class Pluck # :nodoc: - # In Active Record 4.2, #pluck chokes when you give it - # Arel. It calls #to_s on whatever you pass in. So the - # hacky solution is to wrap the node in a class that - # returns the node when you call #to_s. Then, it works! - # - # In Active Record 5, #pluck accepts Arel, so we won't - # bother to use this there. - - if ::ActiveRecord::VERSION::MAJOR >= 5 - def self.decorate(node); node; end - else - def self.decorate(node); new(node); end - end - - def initialize(node) - @node = node - end - - def to_s - @node - end - end -end diff --git a/lib/baby_squeel/version.rb b/lib/baby_squeel/version.rb index 67fee90..640016a 100644 --- a/lib/baby_squeel/version.rb +++ b/lib/baby_squeel/version.rb @@ -1,3 +1,3 @@ module BabySqueel - VERSION = '1.3.1'.freeze + VERSION = '2.0.0'.freeze end diff --git a/spec/baby_squeel/__snapshots__/association_spec.yaml b/spec/baby_squeel/__snapshots__/association_spec.yaml index 1be18ae..518cf17 100644 --- a/spec/baby_squeel/__snapshots__/association_spec.yaml +++ b/spec/baby_squeel/__snapshots__/association_spec.yaml @@ -1,7 +1,3 @@ --- -BabySqueel::Association#== generates SQL 1: '"posts"."author_id" = 42' -BabySqueel::Association#!= generates SQL 1: ("posts"."author_id" != 42) -'BabySqueel::Association#== generates SQL 1 (Active Record: v5.2)': '"posts"."author_id" - = ?' -'BabySqueel::Association#!= generates SQL 1 (Active Record: v5.2)': '"posts"."author_id" - != ?' +BabySqueel::Association#== generates SQL 1: '"posts"."author_id" = ?' +BabySqueel::Association#!= generates SQL 1: '"posts"."author_id" != ?' diff --git a/spec/baby_squeel/active_record/query_methods/injector6_1_spec.rb b/spec/baby_squeel/active_record/query_methods/injector6_1_spec.rb new file mode 100644 index 0000000..4097840 --- /dev/null +++ b/spec/baby_squeel/active_record/query_methods/injector6_1_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe BabySqueel::ActiveRecord::QueryMethods::Injector6_1 do + let(:join_path) { + BabySqueel::Join.new([]) + } + + let(:joins_values) { + [:something, join_path, {a: :b}] + } + + subject(:injector) { Array.new(joins_values).extend(described_class) } + + describe '#each' do + it 'do not blow up without a result' do + test_each = [] + injector.each do |join| + test_each << join + end + expect(test_each).to eq([:something, {a: :b}]) + end + + it 'adds join_path into result' do + result = [] + other = [] + injector.each { |join| other << join } + expect(other).to eq([:something, { a: :b }]) + expect(result).to eq([join_path]) + end + + it 'can accompany other result' do + result = [] + injector.each { |join| result << join } + expect(result).to eq(joins_values) + end + end +end diff --git a/spec/baby_squeel/association_spec.rb b/spec/baby_squeel/association_spec.rb index 1e0bc24..0395860 100644 --- a/spec/baby_squeel/association_spec.rb +++ b/spec/baby_squeel/association_spec.rb @@ -35,46 +35,30 @@ describe '#==' do subject(:association) { create_association Post, :author } - if ActiveRecord::VERSION::MAJOR >= 5 - it 'generates SQL' do - node = association == Author.new(id: 42) - expect(node._arel.to_sql).to match_sql_snapshot(variants: ['5.2']) - end + it 'generates SQL' do + node = association == Author.new(id: 42) + expect(node._arel.to_sql).to match_sql_snapshot + end - it 'throws for an invalid comparison' do - expect { - association == 'foo' - }.to raise_error(BabySqueel::AssociationComparisonError) - end - else - it 'throws not supported' do - expect { - association == 'foo' - }.to raise_error(BabySqueel::AssociationComparisonNotSupportedError) - end + it 'throws for an invalid comparison' do + expect { + association == 'foo' + }.to raise_error(BabySqueel::AssociationComparisonError) end end describe '#!=' do subject(:association) { create_association Post, :author } - if ActiveRecord::VERSION::MAJOR >= 5 - it 'generates SQL' do - node = association != Author.new(id: 42) - expect(node._arel.to_sql).to match_sql_snapshot(variants: ['5.2']) - end + it 'generates SQL' do + node = association != Author.new(id: 42) + expect(node._arel.to_sql).to match_sql_snapshot + end - it 'throws for an invalid comparison' do - expect { - association != 'foo' - }.to raise_error(BabySqueel::AssociationComparisonError) - end - else - it 'throws not supported' do - expect { - association != 'foo' - }.to raise_error(BabySqueel::AssociationComparisonNotSupportedError) - end + it 'throws for an invalid comparison' do + expect { + association != 'foo' + }.to raise_error(BabySqueel::AssociationComparisonError) end end diff --git a/spec/baby_squeel/calculation_spec.rb b/spec/baby_squeel/calculation_spec.rb index afddbbd..5b45a70 100644 --- a/spec/baby_squeel/calculation_spec.rb +++ b/spec/baby_squeel/calculation_spec.rb @@ -10,13 +10,6 @@ end end - describe '#type_cast_from_database' do - it 'just returns what you pass in' do - calculation = described_class.new(table[:id]) - expect(calculation.type_cast_from_database(1)).to eq(1) - end - end - describe '#to_s' do it 'generates a name for attribute' do calculation = described_class.new(table[:id]) diff --git a/spec/baby_squeel/join_dependency/injector6_0_spec.rb b/spec/baby_squeel/join_dependency/injector6_0_spec.rb new file mode 100644 index 0000000..41b0550 --- /dev/null +++ b/spec/baby_squeel/join_dependency/injector6_0_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe BabySqueel::JoinDependency::Injector6_0 do + let(:join_path) { + BabySqueel::Join.new([]) + } + + let(:joins_values) { + [:something, join_path, {a: :b}] + } + + subject(:injector) { described_class.new(joins_values) } + + describe '#each' do + it 'do not blow up without a buckets hash' do + test_each = [] + injector.each do |join| + test_each << join + end + expect(test_each).to eq(joins_values) + end + + it 'never yields JoinPath instances to the block if buckets hash is given' do + buckets = Hash.new { |h, k| h[k] = [] } + injector.each do |join| + expect(join).not_to eq(join_path) + end + end + + it 'adds JoinPath into buckets[:association_join]' do + buckets = Hash.new { |h, k| h[k] = [] } + injector.each { |join| buckets[:other] << join } + expect(buckets[:other]).to eq([:something, { a: :b }]) + expect(buckets[:association_join]).to eq([join_path]) + end + + it 'can accompany other :association_joins' do + buckets = Hash.new { |h, k| h[k] = [] } + injector.each { |join| buckets[:association_join] << join } + expect(buckets[:association_join]).to eq(joins_values) + end + end +end diff --git a/spec/baby_squeel/join_dependency/injector_spec.rb b/spec/baby_squeel/join_dependency/injector_spec.rb deleted file mode 100644 index 6eb51ba..0000000 --- a/spec/baby_squeel/join_dependency/injector_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -require 'spec_helper' - -describe BabySqueel::JoinDependency::Injector do - let(:join_path) { - BabySqueel::Join.new([]) - } - - let(:joins_values) { - [:something, join_path, {a: :b}] - } - - subject(:injector) { described_class.new(joins_values) } - - describe '#group_by' do - it 'never yields JoinPath instances to the block' do - injector.group_by do |join| - expect(join).not_to eq(join_path) - end - end - - it 'groups JoinPath into :association_join' do - buckets = injector.group_by { :other } - expect(buckets[:other]).to eq([:something, { a: :b }]) - expect(buckets[:association_join]).to eq([join_path]) - end - - it 'can accompany other :association_joins' do - buckets = injector.group_by { :association_join } - expect(buckets[:association_join]).to eq(joins_values) - end - end -end diff --git a/spec/baby_squeel/pluck_spec.rb b/spec/baby_squeel/pluck_spec.rb deleted file mode 100644 index 406e391..0000000 --- a/spec/baby_squeel/pluck_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'spec_helper' - -RSpec.describe BabySqueel::Pluck do - describe '.decorate' do - if ActiveRecord::VERSION::MAJOR >= 5 - it 'always returns identity' do - expect(described_class.decorate(1)).to eq(1) - end - else - it 'returns a Pluck' do - expect(described_class.decorate(1)).to be_a(described_class) - end - end - end - - describe '#to_s' do - it 'returns the node' do - expect(described_class.new(1).to_s).to eq(1) - end - end -end diff --git a/spec/integration/__snapshots__/joining_spec.yaml b/spec/integration/__snapshots__/joining_spec.yaml index eb845ec..628feb9 100644 --- a/spec/integration/__snapshots__/joining_spec.yaml +++ b/spec/integration/__snapshots__/joining_spec.yaml @@ -1,4 +1,16 @@ --- +"#joining when joining implicitly nested joins outer joins 1": SELECT "posts".* FROM + "posts" LEFT OUTER JOIN "authors" ON "authors"."id" = "posts"."author_id" INNER + JOIN "comments" ON "comments"."author_id" = "authors"."id" +"#joining when joining implicitly nested joins joins a through association and then back again 1": SELECT + "posts".* FROM "posts" INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" + INNER JOIN "posts" "posts_authors" ON "posts_authors"."author_id" = "authors"."id" + LEFT OUTER JOIN "authors" "authors_posts_join" ON "authors_posts_join"."id" = "posts_authors"."author_id" + LEFT OUTER JOIN "comments" ON "comments"."author_id" = "authors_posts_join"."id" + INNER JOIN "posts" "posts_comments" ON "posts_comments"."id" = "comments"."post_id" + INNER JOIN "authors" "authors_posts_join_2" ON "authors_posts_join_2"."id" = "posts_comments"."author_id" + INNER JOIN "comments" "author_comments_posts" ON "author_comments_posts"."author_id" + = "authors_posts_join_2"."id" "#joining when joining explicitly inner joins 1": SELECT "posts".* FROM "posts" INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" "#joining when joining explicitly inner joins explicitly 1": SELECT "posts".* FROM @@ -18,21 +30,24 @@ "#joining when joining explicitly aliases after the on clause 1": SELECT "posts".* FROM "posts" INNER JOIN "authors" "a" ON "authors"."id" = "posts"."author_id" "#joining when joining explicitly merges bind values 1": SELECT "posts".* FROM "posts" - INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" AND "authors"."ugly" - = 't' INNER JOIN "comments" ON "comments"."author_id" = "authors"."id" + INNER JOIN "authors" ON "authors"."ugly" = 't' AND "authors"."id" = "posts"."author_id" + INNER JOIN "comments" ON "comments"."author_id" = "authors"."id" "#joining when joining explicitly with complex conditions inner joins 1": SELECT "posts".* FROM "posts" INNER JOIN "authors" ON ("posts"."author_id" = "authors"."id" AND "authors"."id" != 5 OR "authors"."name" IS NULL) "#joining when joining explicitly with complex conditions outer joins 1": SELECT "posts".* FROM "posts" LEFT OUTER JOIN "authors" ON ("posts"."author_id" = "authors"."id" AND "authors"."id" != 5 OR "authors"."name" IS NULL) -"#joining when joining implicitly outer joins 1": SELECT "posts".* FROM "posts" LEFT - OUTER JOIN "authors" ON "authors"."id" = "posts"."author_id" "#joining when joining implicitly correctly aliases when joining the same table twice 1": SELECT "posts".* FROM "posts" LEFT OUTER JOIN "authors" ON "authors"."id" = "posts"."author_id" LEFT OUTER JOIN "posts" "parents_posts" ON "parents_posts"."id" = "posts"."parent_id" LEFT OUTER JOIN "authors" "authors_posts" ON "authors_posts"."id" = "parents_posts"."author_id" WHERE ("authors"."name" = 'Rick' OR "authors_posts"."name" = 'Flair') +"#joining when joining implicitly correctly identifies a table independenty joined via separate associations 1": SELECT + "posts".* FROM "posts" INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" + INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" INNER JOIN "authors" + "authors_comments" ON "authors_comments"."id" = "comments"."author_id" WHERE "authors_comments"."name" + = 'Bob' "#joining when joining implicitly polymorphism inner joins 1": SELECT "pictures".* FROM "pictures" INNER JOIN "posts" ON "posts"."id" = "pictures"."imageable_id" AND "pictures"."imageable_type" = 'Post' @@ -46,9 +61,6 @@ "#joining when joining implicitly nested joins inner joins 1": SELECT "posts".* FROM "posts" INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" INNER JOIN "comments" ON "comments"."author_id" = "authors"."id" -"#joining when joining implicitly nested joins outer joins 1": SELECT "posts".* FROM - "posts" LEFT OUTER JOIN "authors" ON "authors"."id" = "posts"."author_id" INNER - JOIN "comments" ON "comments"."author_id" = "authors"."id" "#joining when joining implicitly nested joins handles polymorphism 1": SELECT "pictures".* FROM "pictures" INNER JOIN "posts" ON "posts"."id" = "pictures"."imageable_id" AND "pictures"."imageable_type" = 'Post' INNER JOIN "comments" ON "comments"."post_id" @@ -59,15 +71,6 @@ "#joining when joining implicitly nested joins outer joins only the specified associations 1": SELECT "posts".* FROM "posts" INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" LEFT OUTER JOIN "comments" ON "comments"."author_id" = "authors"."id" -"#joining when joining implicitly nested joins joins a through association and then back again 1": SELECT - "posts".* FROM "posts" INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" - INNER JOIN "posts" "posts_authors" ON "posts_authors"."author_id" = "authors"."id" - LEFT OUTER JOIN "authors" "authors_posts_join" ON "authors_posts_join"."id" = "posts_authors"."author_id" - LEFT OUTER JOIN "comments" ON "comments"."author_id" = "authors_posts_join"."id" - INNER JOIN "posts" "posts_comments" ON "posts_comments"."id" = "comments"."post_id" - INNER JOIN "authors" "authors_posts_join_2" ON "authors_posts_join_2"."id" = "posts_comments"."author_id" - INNER JOIN "comments" "author_comments_posts" ON "author_comments_posts"."author_id" - = "authors_posts_join_2"."id" "#joining when joining implicitly duplicate prevention when given two DSL joins dedupes incremental joins 1": SELECT "posts".* FROM "posts" INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" INNER JOIN "posts" "posts_authors" ON "posts_authors"."author_id" = "authors"."id" @@ -91,3 +94,24 @@ : SELECT "posts".* FROM "posts" INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" LEFT OUTER JOIN "comments" ON "comments"."author_id" = "authors"."id" INNER JOIN "authors" "authors_posts" ON "authors_posts"."id" = "posts"."author_id" +"#joining when joining explicitly merges bind values 1 (Active Record: v6.0)": SELECT + "posts".* FROM "posts" INNER JOIN "authors" ON "authors"."ugly" = 1 AND "authors"."id" + = "posts"."author_id" INNER JOIN "comments" ON "comments"."author_id" = "authors"."id" +"#joining when joining implicitly polymorphism double polymorphic joining 1": SELECT + "pictures".* FROM "pictures" INNER JOIN "authors" ON "authors"."id" = "pictures"."imageable_id" + AND "pictures"."imageable_type" = 'Author' INNER JOIN "posts" ON "posts"."id" = + "pictures"."imageable_id" AND "pictures"."imageable_type" = 'Post' WHERE ("authors"."name" + = 'NameOfTheAuthor' OR "posts"."title" = 'NameOfThePost') +"#joining when joining explicitly merges bind values 1 (Active Record: v6.1)": SELECT + "posts".* FROM "posts" INNER JOIN "authors" ON "authors"."ugly" = 1 AND "authors"."id" + = "posts"."author_id" INNER JOIN "comments" ON "comments"."author_id" = "authors"."id" +"#joining when joining explicitly merges bind values 1 (Active Record: v7.0)": SELECT + "posts".* FROM "posts" INNER JOIN "authors" ON "authors"."ugly" = 1 AND "authors"."id" + = "posts"."author_id" INNER JOIN "comments" ON "comments"."author_id" = "authors"."id" +"#joining when joining implicitly inner joins 1": SELECT "posts".* FROM "posts" INNER + JOIN "authors" ON "authors"."id" = "posts"."author_id" +"#joining when joining implicitly outer joins single 1": SELECT "posts".* FROM "posts" + LEFT OUTER JOIN "authors" ON "authors"."id" = "posts"."author_id" +"#joining when joining implicitly outer joins multi 1": SELECT "posts".* FROM "posts" + LEFT OUTER JOIN "posts" "parents_posts" ON "parents_posts"."id" = "posts"."parent_id" + LEFT OUTER JOIN "authors" ON "authors"."id" = "posts"."author_id" diff --git a/spec/integration/__snapshots__/rails_integration_spec.yaml b/spec/integration/__snapshots__/rails_integration_spec.yaml new file mode 100644 index 0000000..fe8dfd4 --- /dev/null +++ b/spec/integration/__snapshots__/rails_integration_spec.yaml @@ -0,0 +1,14 @@ +--- +test that plain rails still works joins and merge 1: SELECT "authors".* FROM "authors" + INNER JOIN "posts" ON "posts"."author_id" = "authors"."id" INNER JOIN "comments" + ON "comments"."post_id" = "posts"."id" WHERE "comments"."body" = 'body' +test that plain rails still works left_joins 1: SELECT "posts".* FROM "posts" LEFT + OUTER JOIN "posts" "parents_posts" ON "parents_posts"."id" = "posts"."parent_id" + LEFT OUTER JOIN "authors" ON "authors"."id" = "posts"."author_id" +test that plain rails still works joins includes 1: SELECT "posts"."id" AS t0_r0, + "posts"."title" AS t0_r1, "posts"."author_id" AS t0_r2, "posts"."published_at" AS + t0_r3, "posts"."view_count" AS t0_r4, "posts"."parent_id" AS t0_r5, "posts"."created_at" + AS t0_r6, "posts"."updated_at" AS t0_r7, "authors"."id" AS t1_r0, "authors"."name" + AS t1_r1, "authors"."age" AS t1_r2, "authors"."ugly" AS t1_r3, "authors"."created_at" + AS t1_r4, "authors"."updated_at" AS t1_r5 FROM "posts" INNER JOIN "authors" ON "authors"."id" + = "posts"."author_id" diff --git a/spec/integration/__snapshots__/sifting_spec.yaml b/spec/integration/__snapshots__/sifting_spec.yaml index 82274c7..c3f0d9b 100644 --- a/spec/integration/__snapshots__/sifting_spec.yaml +++ b/spec/integration/__snapshots__/sifting_spec.yaml @@ -2,17 +2,9 @@ BabySqueel::ActiveRecord::Base#sifter allows the use of the sifter 1: SELECT "posts".* FROM "posts" WHERE "posts"."id" = 5 BabySqueel::ActiveRecord::Base#sifter allows the use of a sifter on an association 1: SELECT - "posts".* FROM "posts" INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" - WHERE ("authors"."name" LIKE 'boogies%') -BabySqueel::ActiveRecord::Base#sifter yield the root table to the block when arity is given 1: SELECT - "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" - INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" INNER JOIN "comments" - "author_comments_posts" ON "author_comments_posts"."author_id" = "authors"."id" - WHERE ("author_comments_posts"."id" > 1) -'BabySqueel::ActiveRecord::Base#sifter allows the use of a sifter on an association 1 (Active Record: v5.2)': SELECT "posts".* FROM "posts" INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" WHERE "authors"."name" LIKE 'boogies%' -'BabySqueel::ActiveRecord::Base#sifter yield the root table to the block when arity is given 1 (Active Record: v5.2)': SELECT +BabySqueel::ActiveRecord::Base#sifter yield the root table to the block when arity is given 1: SELECT "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" INNER JOIN "comments" "author_comments_posts" ON "author_comments_posts"."author_id" = "authors"."id" diff --git a/spec/integration/__snapshots__/when_having_spec.yaml b/spec/integration/__snapshots__/when_having_spec.yaml index ede5e55..6f3ae4f 100644 --- a/spec/integration/__snapshots__/when_having_spec.yaml +++ b/spec/integration/__snapshots__/when_having_spec.yaml @@ -1,39 +1,13 @@ --- -"#when_having adds a having clause 1 (Active Record: v4.2)": SELECT COUNT("posts"."id") - FROM "posts" GROUP BY "posts"."author_id" HAVING COUNT("posts"."id") > 5 -"#when_having adds a having clause with a calculation 1 (Active Record: v4.2)": SELECT - COUNT("posts"."id") FROM "posts" GROUP BY ("posts"."author_id" + 5) * 3 HAVING COUNT("posts"."id") - > 5 -"#when_having adds a having clause with an association 1 (Active Record: v4.2)": SELECT - COUNT("posts"."id") FROM "posts" INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" - GROUP BY "authors"."id" HAVING COUNT("authors"."id") > 5 -"#when_having adds a having clause with an aliased table 1 (Active Record: v4.2)": SELECT - COUNT("posts_authors"."id") FROM "posts" INNER JOIN "authors" ON "authors"."id" - = "posts"."author_id" INNER JOIN "posts" "posts_authors" ON "posts_authors"."author_id" - = "authors"."id" GROUP BY "posts_authors"."id" HAVING COUNT("posts_authors"."id") - > 5 "#when_having adds a having clause 1": SELECT COUNT("posts"."id") FROM "posts" GROUP - BY "posts"."author_id" HAVING (COUNT("posts"."id") > 5) + BY "posts"."author_id" HAVING COUNT("posts"."id") > 5 "#when_having adds a having clause with a calculation 1": SELECT COUNT("posts"."id") - FROM "posts" GROUP BY ("posts"."author_id" + 5) * 3 HAVING (COUNT("posts"."id") - > 5) + FROM "posts" GROUP BY ("posts"."author_id" + 5) * 3 HAVING COUNT("posts"."id") > + 5 "#when_having adds a having clause with an association 1": SELECT COUNT("posts"."id") FROM "posts" INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" GROUP - BY "authors"."id" HAVING (COUNT("authors"."id") > 5) + BY "authors"."id" HAVING COUNT("authors"."id") > 5 "#when_having adds a having clause with an aliased table 1": SELECT COUNT("posts_authors"."id") FROM "posts" INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" INNER JOIN "posts" "posts_authors" ON "posts_authors"."author_id" = "authors"."id" GROUP - BY "posts_authors"."id" HAVING (COUNT("posts_authors"."id") > 5) -"#when_having adds a having clause 1 (Active Record: v5.2)": SELECT COUNT("posts"."id") - FROM "posts" GROUP BY "posts"."author_id" HAVING COUNT("posts"."id") > 5 -"#when_having adds a having clause with a calculation 1 (Active Record: v5.2)": SELECT - COUNT("posts"."id") FROM "posts" GROUP BY ("posts"."author_id" + 5) * 3 HAVING COUNT("posts"."id") - > 5 -"#when_having adds a having clause with an association 1 (Active Record: v5.2)": SELECT - COUNT("posts"."id") FROM "posts" INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" - GROUP BY "authors"."id" HAVING COUNT("authors"."id") > 5 -"#when_having adds a having clause with an aliased table 1 (Active Record: v5.2)": SELECT - COUNT("posts_authors"."id") FROM "posts" INNER JOIN "authors" ON "authors"."id" - = "posts"."author_id" INNER JOIN "posts" "posts_authors" ON "posts_authors"."author_id" - = "authors"."id" GROUP BY "posts_authors"."id" HAVING COUNT("posts_authors"."id") - > 5 + BY "posts_authors"."id" HAVING COUNT("posts_authors"."id") > 5 diff --git a/spec/integration/__snapshots__/where_chain_spec.yaml b/spec/integration/__snapshots__/where_chain_spec.yaml index fe6ff39..edce3c7 100644 --- a/spec/integration/__snapshots__/where_chain_spec.yaml +++ b/spec/integration/__snapshots__/where_chain_spec.yaml @@ -14,21 +14,21 @@ 'Simp%' OR "authors"."name" = 'meatloaf') "#where.has wheres on deep associations 1": SELECT "posts".* FROM "posts" INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" INNER JOIN "comments" ON "comments"."author_id" - = "authors"."id" WHERE ("comments"."id" > 0) + = "authors"."id" WHERE "comments"."id" > 0 "#where.has wheres on an aliased association 1": SELECT "posts".* FROM "posts" INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" INNER JOIN "posts" "posts_authors" - ON "posts_authors"."author_id" = "authors"."id" WHERE ("posts_authors"."id" > 0) + ON "posts_authors"."author_id" = "authors"."id" WHERE "posts_authors"."id" > 0 "#where.has wheres on an aliased association with through 1": SELECT "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" INNER JOIN "comments" "author_comments_posts" - ON "author_comments_posts"."author_id" = "authors"."id" WHERE ("author_comments_posts"."id" - > 0) + ON "author_comments_posts"."author_id" = "authors"."id" WHERE "author_comments_posts"."id" + > 0 "#where.has wheres on polymorphic associations 1": SELECT "pictures".* FROM "pictures" INNER JOIN "posts" ON "posts"."id" = "pictures"."imageable_id" AND "pictures"."imageable_type" - = 'Post' WHERE ("posts"."title" LIKE 'meatloaf') + = 'Post' WHERE "posts"."title" LIKE 'meatloaf' "#where.has wheres on polymorphic associations outer join 1": SELECT "pictures".* FROM "pictures" LEFT OUTER JOIN "posts" ON "posts"."id" = "pictures"."imageable_id" - AND "pictures"."imageable_type" = 'Post' WHERE ("posts"."title" LIKE 'meatloaf') + AND "pictures"."imageable_type" = 'Post' WHERE "posts"."title" LIKE 'meatloaf' "#where.has wheres and correctly aliases 1": SELECT "posts".* FROM "posts" INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" INNER JOIN "comments" ON "comments"."author_id" = "authors"."id" WHERE "comments"."id" IN (1, 2) AND "authors"."name" = 'Joe' @@ -38,59 +38,26 @@ = 'Joe' "#where.has wheres on an alias with a function 1": SELECT "posts".* FROM "posts" INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" INNER JOIN "posts" "posts_authors" - ON "posts_authors"."author_id" = "authors"."id" WHERE (coalesce("posts_authors"."id", - 1) > 0) + ON "posts_authors"."author_id" = "authors"."id" WHERE coalesce("posts_authors"."id", + 1) > 0 "#where.has wheres with a subquery 1": SELECT "posts".* FROM "posts" INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" WHERE "authors"."id" IN (SELECT "authors"."id" FROM "authors" LIMIT 3) "#where.has wheres with an empty subquery 1": SELECT "posts".* FROM "posts" WHERE "posts"."author_id" IN (SELECT "authors"."id" FROM "authors" WHERE (1=0)) -"#where.has wheres with an empty subquery and keeps values 1 (Active Record: v4.2)": SELECT - "posts".* FROM "posts" WHERE "posts"."author_id" IN (SELECT "authors"."id" FROM - "authors" INNER JOIN "posts" ON "posts"."author_id" = "authors"."id" WHERE (1=0) - GROUP BY "authors"."id" ORDER BY "authors"."id" ASC) -"#where.has wheres with a not in subquery 1": SELECT "posts".* FROM "posts" WHERE - ("posts"."author_id" NOT IN (SELECT "authors"."id" FROM "authors" WHERE (1=0))) -"#where.has wheres using a simple table 1": SELECT "posts".* FROM "posts" INNER JOIN - "authors" ON "authors"."id" = "posts"."author_id" WHERE "authors"."name" = 'Yo Gotti' -"#where.has builds an exists query 1": SELECT "posts".* FROM "posts" WHERE (EXISTS(SELECT - "posts".* FROM "posts" WHERE "posts"."author_id" = 1)) -"#where.has builds a not exists query 1": SELECT "posts".* FROM "posts" WHERE (NOT - EXISTS(SELECT "posts".* FROM "posts" WHERE "posts"."author_id" = 1)) "#where.has wheres with an empty subquery and keeps values 1": SELECT "posts".* FROM "posts" WHERE "posts"."author_id" IN (SELECT "authors"."id" FROM "authors" INNER JOIN "posts" ON "posts"."author_id" = "authors"."id" WHERE (1=0) GROUP BY "authors"."id" ORDER BY "authors"."id" ASC) +"#where.has wheres with a not in subquery 1": SELECT "posts".* FROM "posts" WHERE + "posts"."author_id" NOT IN (SELECT "authors"."id" FROM "authors" WHERE (1=0)) +"#where.has wheres using a simple table 1": SELECT "posts".* FROM "posts" INNER JOIN + "authors" ON "authors"."id" = "posts"."author_id" WHERE "authors"."name" = 'Yo Gotti' +"#where.has builds an exists query 1": SELECT "posts".* FROM "posts" WHERE EXISTS(SELECT + "posts".* FROM "posts" WHERE "posts"."author_id" = 1) +"#where.has builds a not exists query 1": SELECT "posts".* FROM "posts" WHERE NOT + EXISTS(SELECT "posts".* FROM "posts" WHERE "posts"."author_id" = 1) "#where.has wheres an association using #== 1": SELECT "posts".* FROM "posts" WHERE - ("posts"."author_id" = 42) + "posts"."author_id" = 42 "#where.has wheres an association using #!= 1": SELECT "posts".* FROM "posts" WHERE - (("posts"."author_id" != 42)) -"#where.has wheres on deep associations 1 (Active Record: v5.2)": SELECT "posts".* - FROM "posts" INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" INNER - JOIN "comments" ON "comments"."author_id" = "authors"."id" WHERE "comments"."id" - > 0 -"#where.has wheres on an aliased association 1 (Active Record: v5.2)": SELECT "posts".* - FROM "posts" INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" INNER - JOIN "posts" "posts_authors" ON "posts_authors"."author_id" = "authors"."id" WHERE - "posts_authors"."id" > 0 -"#where.has wheres on an aliased association with through 1 (Active Record: v5.2)": SELECT - "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" - INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" INNER JOIN "comments" - "author_comments_posts" ON "author_comments_posts"."author_id" = "authors"."id" - WHERE "author_comments_posts"."id" > 0 -"#where.has wheres on an alias with a function 1 (Active Record: v5.2)": SELECT "posts".* - FROM "posts" INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" INNER - JOIN "posts" "posts_authors" ON "posts_authors"."author_id" = "authors"."id" WHERE - coalesce("posts_authors"."id", 1) > 0 -"#where.has wheres with a not in subquery 1 (Active Record: v5.2)": SELECT "posts".* - FROM "posts" WHERE "posts"."author_id" NOT IN (SELECT "authors"."id" FROM "authors" - WHERE (1=0)) -"#where.has builds an exists query 1 (Active Record: v5.2)": SELECT "posts".* FROM - "posts" WHERE EXISTS(SELECT "posts".* FROM "posts" WHERE "posts"."author_id" = 1) -"#where.has builds a not exists query 1 (Active Record: v5.2)": SELECT "posts".* FROM - "posts" WHERE NOT EXISTS(SELECT "posts".* FROM "posts" WHERE "posts"."author_id" - = 1) -"#where.has wheres an association using #== 1 (Active Record: v5.2)": SELECT "posts".* - FROM "posts" WHERE "posts"."author_id" = 42 -"#where.has wheres an association using #!= 1 (Active Record: v5.2)": SELECT "posts".* - FROM "posts" WHERE "posts"."author_id" != 42 + "posts"."author_id" != 42 diff --git a/spec/integration/averaging_spec.rb b/spec/integration/averaging_spec.rb index af815ac..cb1410c 100644 --- a/spec/integration/averaging_spec.rb +++ b/spec/integration/averaging_spec.rb @@ -36,7 +36,7 @@ end expect(queries.last).to produce_sql( - /AVG\("posts"."view_count"\) AS average_posts_view_count/ + /AVG\("posts"."view_count"\) AS "?average_posts_view_count/ ) end end diff --git a/spec/integration/counting_spec.rb b/spec/integration/counting_spec.rb index cf5b738..e3a1215 100644 --- a/spec/integration/counting_spec.rb +++ b/spec/integration/counting_spec.rb @@ -41,7 +41,7 @@ end expect(queries.last).to produce_sql( - /COUNT\("posts"."id"\) AS count_posts_id/ + /COUNT\("posts"."id"\) AS "?count_posts_id/ ) end end diff --git a/spec/integration/joining_spec.rb b/spec/integration/joining_spec.rb index 3a2fd24..02f7ecf 100644 --- a/spec/integration/joining_spec.rb +++ b/spec/integration/joining_spec.rb @@ -73,7 +73,7 @@ it 'merges bind values' do relation = Post.joining { ugly_author_comments } - expect(relation).to match_sql_snapshot + expect(relation).to match_sql_snapshot(variants: ['6.0', '6.1', '7.0']) end context 'with complex conditions' do @@ -102,13 +102,31 @@ context 'when joining implicitly' do it 'inner joins' do relation = Post.joining { author } + + expect(relation).to match_sql_snapshot expect(relation).to produce_sql(Post.joins(:author)) end - it 'outer joins' do - relation = Post.joining { author.outer } + context 'outer joins' do + it 'single' do + relation = Post.joining { author.outer } - expect(relation).to match_sql_snapshot + expect(relation).to match_sql_snapshot + expect(relation).to produce_sql(Post.left_joins(:author)) + end + + it 'multi' do + relation = Post.joining { parent.outer }.joining { author.outer } + + expect(relation).to match_sql_snapshot + expect(relation).to produce_sql(Post.joining { [parent.outer, author.outer] }) + expect(relation).to produce_sql(Post.left_joins(:parent).left_joins(:author)) + expect(relation).to produce_sql(Post.joining { parent.outer }.left_joins(:author)) + + # The order is different left_joins are at the end + relation = Post.joining { [author.outer, parent.outer] } + expect(relation).to produce_sql(Post.left_joins(:parent).joining { author.outer }) + end end it 'correctly aliases when joining the same table twice' do @@ -122,24 +140,23 @@ describe 'polymorphism' do it 'inner joins' do - if ActiveRecord::VERSION::STRING >= '5.2.0' - pending "polyamorous's support for polymorphism is broken" - end - relation = Picture.joining { imageable.of(Post) } expect(relation).to match_sql_snapshot end it 'outer joins' do - if ActiveRecord::VERSION::STRING >= '5.2.0' - pending "polyamorous's support for polymorphism is broken" - end - relation = Picture.joining { imageable.of(Post).outer } expect(relation).to match_sql_snapshot end + + it 'double polymorphic joining' do + join_scope = Picture.joining { [imageable.of(Author), imageable.of(Post)] } + relation = join_scope.where.has { imageable.of(Author).name.eq('NameOfTheAuthor').or(imageable.of(Post).title.eq('NameOfThePost')) } + + expect(relation).to match_sql_snapshot + end end describe 'habtm' do @@ -155,19 +172,18 @@ relation = Post.joining { author.comments } expect(relation).to match_sql_snapshot + expect(relation).to produce_sql(Post.joins(author: :comments)) end it 'outer joins' do + pending "This feature is known to be broken" + relation = Post.joining { author.outer.comments } expect(relation).to match_sql_snapshot end it 'handles polymorphism' do - if ActiveRecord::VERSION::STRING >= '5.2.0' - pending "polyamorous's support for polymorphism is broken" - end - relation = Picture.joining { imageable.of(Post).comments } expect(relation).to match_sql_snapshot @@ -177,6 +193,7 @@ relation = Post.joining { author.outer.comments.outer } expect(relation).to match_sql_snapshot + expect(relation).to produce_sql(Post.left_joins(author: :comments)) end it 'outer joins only the specified associations' do @@ -188,6 +205,7 @@ it 'joins back with a new alias' do baby_squeel = Post.joining { author.posts } active_record = Post.joins(author: :posts) + expect(baby_squeel).to produce_sql(active_record) end @@ -202,11 +220,13 @@ it 'joins a through association' do baby_squeel = Post.joining { author.posts.author_comments } - active_record = Post.joins(author: { posts: :author_comments}) + active_record = Post.joins(author: { posts: :author_comments }) expect(baby_squeel).to produce_sql(active_record) end it 'joins a through association and then back again' do + pending "This feature is known to be broken" + relation = Post.joining { author.posts.author_comments.outer.post.author_comments } expect(relation).to match_sql_snapshot @@ -296,5 +316,15 @@ Post.joining { author.outer.alias('a') }.to_sql }.to raise_error(BabySqueel::AssociationAliasingError, /'author' as 'a'/) end + + it "correctly identifies a table independenty joined via separate associations" do + relation = Post + relation = relation.joining { [author, comments.author] } + relation = relation.where.has { + comments.author.name == 'Bob' + } + + expect(relation.to_sql).to match_sql_snapshot + end end end diff --git a/spec/integration/maximizing_spec.rb b/spec/integration/maximizing_spec.rb index a31b438..9fd0e1a 100644 --- a/spec/integration/maximizing_spec.rb +++ b/spec/integration/maximizing_spec.rb @@ -35,7 +35,7 @@ end expect(queries.last).to produce_sql( - /MAX\("posts"."view_count"\) AS maximum_posts_view_count/ + /MAX\("posts"."view_count"\) AS "?maximum_posts_view_count/ ) end end diff --git a/spec/integration/minimizing_spec.rb b/spec/integration/minimizing_spec.rb index 0d95be3..0b0523b 100644 --- a/spec/integration/minimizing_spec.rb +++ b/spec/integration/minimizing_spec.rb @@ -35,7 +35,7 @@ end expect(queries.last).to produce_sql( - /MIN\("posts"."view_count"\) AS minimum_posts_view_count/ + /MIN\("posts"."view_count"\) AS "?minimum_posts_view_count/ ) end end diff --git a/spec/integration/rails_integration_spec.rb b/spec/integration/rails_integration_spec.rb new file mode 100644 index 0000000..418c3d3 --- /dev/null +++ b/spec/integration/rails_integration_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe 'test that plain rails still works' do + it 'joins and merge' do + relation = Author.joins(:posts).merge(Post.joins(:comments).merge(Comment.where(body: 'body'))) + + expect(relation).to match_sql_snapshot + end + + it 'left_joins' do + relation = Post.left_joins(:parent, :author) + + expect(relation).to match_sql_snapshot + end + + it 'joins includes' do + relation = Post.joins(:author).includes(:author).to_sql + + expect(relation).to match_sql_snapshot + end +end diff --git a/spec/integration/sifting_spec.rb b/spec/integration/sifting_spec.rb index 8148133..f3c6ac0 100644 --- a/spec/integration/sifting_spec.rb +++ b/spec/integration/sifting_spec.rb @@ -34,7 +34,7 @@ author.sift :boogers } - expect(relation).to match_sql_snapshot(variants: ['5.2']) + expect(relation).to match_sql_snapshot end it 'yield the root table to the block when arity is given' do @@ -42,6 +42,6 @@ sift :author_comments_id, 1 } - expect(relation).to match_sql_snapshot(variants: ['5.2']) + expect(relation).to match_sql_snapshot end end diff --git a/spec/integration/summing_spec.rb b/spec/integration/summing_spec.rb index d01f270..b87ca29 100644 --- a/spec/integration/summing_spec.rb +++ b/spec/integration/summing_spec.rb @@ -35,7 +35,7 @@ end expect(queries.last).to produce_sql( - /SUM\("posts"."view_count"\) AS sum_posts_view_count/ + /SUM\("posts"."view_count"\) AS "?sum_posts_view_count/ ) end end diff --git a/spec/integration/when_having_spec.rb b/spec/integration/when_having_spec.rb index eb21959..fe54a3c 100644 --- a/spec/integration/when_having_spec.rb +++ b/spec/integration/when_having_spec.rb @@ -6,7 +6,7 @@ .grouping { author_id } .when_having { id.count > 5 } - expect(relation).to match_sql_snapshot(variants: ['4.2', '5.2']) + expect(relation).to match_sql_snapshot end it 'adds a having clause with a calculation' do @@ -14,7 +14,7 @@ .grouping { (author_id + 5 ) * 3 } .when_having { id.count > 5 } - expect(relation).to match_sql_snapshot(variants: ['4.2', '5.2']) + expect(relation).to match_sql_snapshot end it 'adds a having clause with an association' do @@ -23,7 +23,7 @@ .grouping { author.id } .when_having { author.id.count > 5 } - expect(relation).to match_sql_snapshot(variants: ['4.2', '5.2']) + expect(relation).to match_sql_snapshot end it 'adds a having clause with an aliased table' do @@ -32,6 +32,6 @@ .grouping { author.posts.id } .when_having { author.posts.id.count > 5 } - expect(relation).to match_sql_snapshot(variants: ['4.2', '5.2']) + expect(relation).to match_sql_snapshot end end diff --git a/spec/integration/where_chain_spec.rb b/spec/integration/where_chain_spec.rb index 3ba8344..489fba5 100644 --- a/spec/integration/where_chain_spec.rb +++ b/spec/integration/where_chain_spec.rb @@ -50,7 +50,7 @@ author.comments.id > 0 } - expect(relation).to match_sql_snapshot(variants: ['5.2']) + expect(relation).to match_sql_snapshot end it 'wheres on an aliased association' do @@ -58,7 +58,7 @@ author.posts.id > 0 } - expect(relation).to match_sql_snapshot(variants: ['5.2']) + expect(relation).to match_sql_snapshot end it 'wheres on an aliased association with through' do @@ -66,14 +66,10 @@ author_comments.id > 0 } - expect(relation).to match_sql_snapshot(variants: ['5.2']) + expect(relation).to match_sql_snapshot end it 'wheres on polymorphic associations' do - if ActiveRecord::VERSION::STRING >= '5.2.0' - pending "polyamorous's support for polymorphism is broken" - end - relation = Picture.joining { imageable.of(Post) }.where.has { imageable.of(Post).title =~ 'meatloaf' } @@ -82,10 +78,6 @@ end it 'wheres on polymorphic associations outer join' do - if ActiveRecord::VERSION::STRING >= '5.2.0' - pending "polyamorous's support for polymorphism is broken" - end - relation = Picture.joining { imageable.of(Post).outer }.where.has { imageable.of(Post).title =~ 'meatloaf' } @@ -114,7 +106,7 @@ coalesce(author.posts.id, 1) > 0 } - expect(relation).to match_sql_snapshot(variants: ['5.2']) + expect(relation).to match_sql_snapshot end it 'wheres with a subquery' do @@ -141,7 +133,7 @@ .none relation = Post.where.has { author_id.in other } - expect(relation).to match_sql_snapshot(variants: ['4.2']) + expect(relation).to match_sql_snapshot end it 'wheres with a not in subquery' do @@ -149,7 +141,7 @@ author_id.not_in Author.none.select(:id) } - expect(relation).to match_sql_snapshot(variants: ['5.2']) + expect(relation).to match_sql_snapshot end it 'wheres using a simple table' do @@ -171,7 +163,7 @@ exists Post.where.has { author_id == 1 } } - expect(relation).to match_sql_snapshot(variants: ['5.2']) + expect(relation).to match_sql_snapshot end it 'builds a not exists query' do @@ -179,33 +171,25 @@ not_exists Post.where.has { author_id == 1 } } - expect(relation).to match_sql_snapshot(variants: ['5.2']) + expect(relation).to match_sql_snapshot end it 'wheres an association using #==' do - if ActiveRecord::VERSION::MAJOR < 5 - skip "This isn't supported in ActiveRecord 4" - end - author = Author.new(id: 42) relation = Post.where.has do |post| post.author == author end - expect(relation).to match_sql_snapshot(variants: ['5.2']) + expect(relation).to match_sql_snapshot end it 'wheres an association using #!=' do - if ActiveRecord::VERSION::MAJOR < 5 - skip "This isn't supported in ActiveRecord 4" - end - author = Author.new(id: 42) relation = Post.where.has do |post| post.author != author end - expect(relation).to match_sql_snapshot(variants: ['5.2']) + expect(relation).to match_sql_snapshot end it 'handles a hash' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0413de5..84dde32 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,16 +1,9 @@ -$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) - require 'bundler/setup' -Bundler.require :test - -SimpleCov.formatter = - if ENV['CI'] - SimpleCov.formatter = Coveralls::SimpleCov::Formatter - else - SimpleCov::Formatter::HTMLFormatter - end +require 'simplecov' +require 'pry' +require 'byebug' -SimpleCov.start { add_filter 'spec/' } unless ENV['SKIPCOV'] +SimpleCov.start { add_filter 'spec/' } require 'baby_squeel' require 'support/schema' @@ -28,12 +21,12 @@ config.default_formatter = config.files_to_run.one? ? :doc : :progress config.run_all_when_everything_filtered = true config.example_status_persistence_file_path = 'tmp/spec-results.log' - config.filter_run_excluding compat: !ENV['COMPAT'] + config.filter_run_excluding compat: ENV['COMPAT'] != "1" config.before :suite do puts "\nRunning with ActiveRecord #{ActiveRecord::VERSION::STRING}" - if ENV['COMPAT'] + if ENV['COMPAT'] == "1" puts "Running in Squeel Compatibility mode" BabySqueel.enable_compatibility! end diff --git a/spec/support/matchers.rb b/spec/support/matchers.rb index c5e57dd..33544ae 100644 --- a/spec/support/matchers.rb +++ b/spec/support/matchers.rb @@ -8,11 +8,8 @@ def self.version(value) end def self.suffix(variants: []) - version = ActiveRecord::VERSION::STRING - version = version.split('.').first(2).join('.') - variant = variants.find do |variant| - variant === version + ActiveRecord::VERSION::STRING.start_with?(variant) end "(Active Record: v#{variant})" if variant