ransack 2.3.2 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +3 -0
  3. data/.github/SECURITY.md +12 -0
  4. data/.github/workflows/cronjob.yml +102 -0
  5. data/.github/workflows/rubocop.yml +20 -0
  6. data/.github/workflows/test.yml +163 -0
  7. data/.rubocop.yml +44 -0
  8. data/CHANGELOG.md +28 -1
  9. data/CONTRIBUTING.md +16 -11
  10. data/Gemfile +5 -3
  11. data/README.md +167 -30
  12. data/bug_report_templates/test-ransack-scope-and-column-same-name.rb +78 -0
  13. data/bug_report_templates/test-ransacker-arel-present-predicate.rb +71 -0
  14. data/docs/img/create_release.png +0 -0
  15. data/docs/release_process.md +17 -0
  16. data/{polyamorous/lib → lib}/polyamorous/activerecord_5.2_ruby_2/join_association.rb +4 -0
  17. data/{polyamorous/lib → lib}/polyamorous/activerecord_5.2_ruby_2/join_dependency.rb +0 -0
  18. data/lib/polyamorous/activerecord_5.2_ruby_2/reflection.rb +11 -0
  19. data/{polyamorous/lib → lib}/polyamorous/activerecord_6.0_ruby_2/join_association.rb +0 -1
  20. data/{polyamorous/lib → lib}/polyamorous/activerecord_6.0_ruby_2/join_dependency.rb +0 -1
  21. data/{polyamorous/lib → lib}/polyamorous/activerecord_6.0_ruby_2/reflection.rb +0 -1
  22. data/lib/polyamorous/activerecord_6.1_ruby_2/join_association.rb +74 -0
  23. data/lib/polyamorous/activerecord_6.1_ruby_2/join_dependency.rb +93 -0
  24. data/{polyamorous/lib → lib}/polyamorous/activerecord_6.1_ruby_2/reflection.rb +0 -1
  25. data/lib/polyamorous/activerecord_7.0_ruby_2/join_association.rb +1 -0
  26. data/lib/polyamorous/activerecord_7.0_ruby_2/join_dependency.rb +1 -0
  27. data/lib/polyamorous/activerecord_7.0_ruby_2/reflection.rb +1 -0
  28. data/{polyamorous/lib → lib}/polyamorous/join.rb +0 -0
  29. data/{polyamorous/lib → lib/polyamorous}/polyamorous.rb +1 -1
  30. data/{polyamorous/lib → lib}/polyamorous/swapping_reflection_class.rb +0 -0
  31. data/{polyamorous/lib → lib}/polyamorous/tree_node.rb +0 -0
  32. data/lib/polyamorous.rb +1 -0
  33. data/lib/ransack/adapters/active_record/base.rb +5 -1
  34. data/lib/ransack/adapters/active_record/context.rb +55 -13
  35. data/lib/ransack/adapters/active_record/ransack/constants.rb +1 -1
  36. data/lib/ransack/adapters/active_record/ransack/context.rb +1 -0
  37. data/lib/ransack/adapters/active_record/ransack/nodes/condition.rb +11 -3
  38. data/lib/ransack/configuration.rb +31 -1
  39. data/lib/ransack/constants.rb +2 -2
  40. data/lib/ransack/helpers/form_builder.rb +3 -3
  41. data/lib/ransack/helpers.rb +1 -1
  42. data/lib/ransack/locale/sv.yml +70 -0
  43. data/lib/ransack/nodes/attribute.rb +1 -1
  44. data/lib/ransack/nodes/condition.rb +0 -2
  45. data/lib/ransack/nodes/grouping.rb +1 -1
  46. data/lib/ransack/nodes/sort.rb +3 -3
  47. data/lib/ransack/nodes/value.rb +1 -1
  48. data/lib/ransack/search.rb +4 -1
  49. data/lib/ransack/translate.rb +4 -4
  50. data/lib/ransack/version.rb +1 -1
  51. data/lib/ransack.rb +2 -2
  52. data/ransack.gemspec +8 -14
  53. data/spec/blueprints/articles.rb +1 -1
  54. data/spec/blueprints/comments.rb +1 -1
  55. data/spec/blueprints/notes.rb +1 -1
  56. data/spec/blueprints/tags.rb +1 -1
  57. data/spec/console.rb +5 -5
  58. data/spec/helpers/ransack_helper.rb +1 -1
  59. data/spec/{ransack → polyamorous}/join_association_spec.rb +8 -1
  60. data/spec/{ransack → polyamorous}/join_dependency_spec.rb +0 -0
  61. data/spec/{ransack → polyamorous}/join_spec.rb +0 -0
  62. data/spec/ransack/adapters/active_record/base_spec.rb +26 -15
  63. data/spec/ransack/adapters/active_record/context_spec.rb +19 -18
  64. data/spec/ransack/configuration_spec.rb +24 -0
  65. data/spec/ransack/helpers/form_helper_spec.rb +16 -16
  66. data/spec/ransack/nodes/condition_spec.rb +13 -0
  67. data/spec/ransack/nodes/grouping_spec.rb +2 -2
  68. data/spec/ransack/predicate_spec.rb +1 -1
  69. data/spec/ransack/search_spec.rb +215 -30
  70. data/spec/spec_helper.rb +7 -5
  71. data/spec/support/schema.rb +28 -2
  72. metadata +45 -47
  73. data/.travis.yml +0 -47
  74. data/polyamorous/lib/polyamorous/activerecord_5.2_ruby_2/reflection.rb +0 -12
  75. data/polyamorous/lib/polyamorous/activerecord_6.1_ruby_2/join_association.rb +0 -2
  76. data/polyamorous/lib/polyamorous/activerecord_6.1_ruby_2/join_dependency.rb +0 -2
  77. data/polyamorous/lib/polyamorous/version.rb +0 -3
  78. data/polyamorous/polyamorous.gemspec +0 -27
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # ![Ransack](./logo/ransack-h.png "Ransack")
2
2
 
3
- [![Build Status](https://travis-ci.org/activerecord-hackery/ransack.svg)](https://travis-ci.org/activerecord-hackery/ransack)
3
+ [![Build Status](https://github.com/activerecord-hackery/ransack/workflows/test/badge.svg)](https://github.com/activerecord-hackery/ransack/actions)
4
4
  [![Gem Version](https://badge.fury.io/rb/ransack.svg)](http://badge.fury.io/rb/ransack)
5
5
  [![Code Climate](https://codeclimate.com/github/activerecord-hackery/ransack/badges/gpa.svg)](https://codeclimate.com/github/activerecord-hackery/ransack)
6
6
  [![Backers on Open Collective](https://opencollective.com/ransack/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/ransack/sponsors/badge.svg)](#sponsors)
@@ -11,13 +11,11 @@ Ransack enables the creation of both
11
11
  for your Ruby on Rails application
12
12
  ([demo source code here](https://github.com/activerecord-hackery/ransack_demo)).
13
13
  If you're looking for something that simplifies query generation at the model
14
- or controller layer, you're probably not looking for Ransack (or MetaSearch,
15
- for that matter). Try [Squeel](https://github.com/activerecord-hackery/squeel)
16
- instead.
14
+ or controller layer, you're probably not looking for Ransack.
17
15
 
18
16
  ## Getting started
19
17
 
20
- Ransack is compatible with Rails 6.0, 5.0, 5.1 and 5.2 on Ruby 2.3 and later.
18
+ Ransack is supported for Rails 6.1, 6.0, 5.2 on Ruby 2.6.6 and later.
21
19
 
22
20
  In your Gemfile, for the last officially released gem:
23
21
 
@@ -41,27 +39,14 @@ gem 'ransack', github: 'activerecord-hackery/ransack'
41
39
 
42
40
  ## Usage
43
41
 
44
- Ransack can be used in one of two modes, simple or advanced.
42
+ Ransack can be used in one of two modes, simple or advanced. For
43
+ searching/filtering not requiring complex boolean logic, Ransack's simple
44
+ mode should meet your needs.
45
45
 
46
- ### Simple Mode
47
-
48
- This mode works much like MetaSearch, for those of you who are familiar with
49
- it, and requires very little setup effort.
50
-
51
- If you're coming from MetaSearch, things to note:
52
-
53
- 1. The default param key for search params is now `:q`, instead of `:search`.
54
- This is primarily to shorten query strings, though advanced queries (below)
55
- will still run afoul of URL length limits in most browsers and require a
56
- switch to HTTP POST requests. This key is [configurable](https://github.com/activerecord-hackery/ransack/wiki/Configuration).
46
+ If you're coming from MetaSearch (Ransack's predecessor), refer to the
47
+ [Updating From MetaSearch](#updating-from-metasearch) section
57
48
 
58
- 2. `form_for` is now `search_form_for`, and validates that a Ransack::Search
59
- object is passed to it.
60
-
61
- 3. Common ActiveRecord::Relation methods are no longer delegated by the
62
- search object. Instead, you will get your search results (an
63
- ActiveRecord::Relation in the case of the ActiveRecord adapter) via a call to
64
- `Ransack#result`.
49
+ ### Simple Mode
65
50
 
66
51
  #### In your controller
67
52
 
@@ -78,9 +63,35 @@ this example, with preloading each Person's Articles and pagination):
78
63
  def index
79
64
  @q = Person.ransack(params[:q])
80
65
  @people = @q.result.includes(:articles).page(params[:page])
66
+ end
67
+ ```
81
68
 
82
- # or use `to_a.uniq` to remove duplicates (can also be done in the view):
83
- @people = @q.result.includes(:articles).page(params[:page]).to_a.uniq
69
+ ##### Default search options
70
+
71
+ **Search parameter**
72
+
73
+ Ransack uses a default `:q` param key for search params. This may be changed by
74
+ setting the `search_key` option in a Ransack initializer file (typically
75
+ `config/initializers/ransack.rb`):
76
+
77
+ ```ruby
78
+ Ransack.configure do |c|
79
+ # Change default search parameter key name.
80
+ # Default key name is :q
81
+ c.search_key = :query
82
+ end
83
+ ```
84
+
85
+ **String search**
86
+
87
+ After version 2.4.0 when searching a string query Ransack by default strips all whitespace around the query string.
88
+ This may be disabled by setting the `strip_whitespace` option in a Ransack initializer file:
89
+
90
+ ```ruby
91
+ Ransack.configure do |c|
92
+ # Change whitespace stripping behaviour.
93
+ # Default is true
94
+ c.strip_whitespace = false
84
95
  end
85
96
  ```
86
97
 
@@ -250,6 +261,57 @@ the order indicator arrow by passing `hide_indicator: true` in the sort link:
250
261
  default_order: { last_name: 'asc', first_name: 'desc' }) %>
251
262
  ```
252
263
 
264
+ #### PostgreSQL's sort option
265
+
266
+ The `NULLS FIRST` and `NULLS LAST` options can be used to determine whether nulls appear before or after non-null values in the sort ordering.
267
+
268
+ You may want to configure it like this:
269
+
270
+ ```rb
271
+ Ransack.configure do |c|
272
+ c.postgres_fields_sort_option = :nulls_first # or :nulls_last
273
+ end
274
+ ```
275
+
276
+ To treat nulls as having the lowest or highest value respectively. To force nulls to always be first or last, use
277
+
278
+ ```rb
279
+ Ransack.configure do |c|
280
+ c.postgres_fields_sort_option = :nulls_always_first # or :nulls_always_last
281
+ end
282
+ ```
283
+
284
+ See this feature: https://www.postgresql.org/docs/13/queries-order.html
285
+
286
+ #### Case Insensitive Sorting in PostgreSQL
287
+
288
+ In order to request PostgreSQL to do a case insensitive sort for all string columns of a model at once, Ransack can be extended by using this approach:
289
+
290
+ ```ruby
291
+ module RansackObject
292
+
293
+ def self.included(base)
294
+ base.columns.each do |column|
295
+ if column.type == :string
296
+ base.ransacker column.name.to_sym, type: :string do
297
+ Arel.sql("lower(#{base.table_name}.#{column.name})")
298
+ end
299
+ end
300
+ end
301
+ end
302
+ end
303
+ ```
304
+
305
+ ```ruby
306
+ class UserWithManyAttributes < ActiveRecord::Base
307
+ include RansackObject
308
+ end
309
+ ```
310
+
311
+ If this approach is taken, it is advisable to [add a functional index](https://www.postgresql.org/docs/13/citext.html).
312
+
313
+ This was originally asked in [a Ransack issue](https://github.com/activerecord-hackery/ransack/issues/1201) and a solution was found on [Stack Overflow](https://stackoverflow.com/a/34677378).
314
+
253
315
  ### Advanced Mode
254
316
 
255
317
  "Advanced" searches (ab)use Rails' nested attributes functionality in order to
@@ -408,6 +470,25 @@ query parameters in your URLs.
408
470
  <% end %>
409
471
  ```
410
472
 
473
+ You can also use `ransack_alias` for sorting.
474
+
475
+ ```ruby
476
+ class Post < ActiveRecord::Base
477
+ belongs_to :author
478
+
479
+ # Abbreviate :author_first_name to :author
480
+ ransack_alias :author, :author_first_name
481
+ end
482
+ ```
483
+
484
+ Now, you can use `:author` instead of `:author_first_name` in a `sort_link`.
485
+
486
+ ```erb
487
+ <%= sort_link(@q, :author) %>
488
+ ```
489
+
490
+ Note that using `:author_first_name_or_author_last_name_cont` would produce an invalid sql query. In those cases, Ransack ignores the sorting clause.
491
+
411
492
  ### Search Matchers
412
493
 
413
494
  List of all possible predicates
@@ -437,13 +518,10 @@ List of all possible predicates
437
518
  | `*_lteq_any` | Less than or equal to any | |
438
519
  | `*_gt_any` | Greater than any | |
439
520
  | `*_gteq_any` | Greater than or equal to any | |
440
- | `*_matches_any` | `*_does_not_match_any` | same as above but with `LIKE` |
441
521
  | `*_lt_all` | Less than all | SQL: `col < value1 AND col < value2` |
442
522
  | `*_lteq_all` | Less than or equal to all | |
443
523
  | `*_gt_all` | Greater than all | |
444
524
  | `*_gteq_all` | Greater than or equal to all | |
445
- | `*_matches_all` | Matches all | same as above but with `LIKE` |
446
- | `*_does_not_match_all` | Does not match all | |
447
525
  | `*_not_eq_all` | none of values in a set | |
448
526
  | `*_start` | Starts with | SQL: `col LIKE 'value%'` |
449
527
  | `*_not_start` | Does not start with | |
@@ -463,7 +541,7 @@ List of all possible predicates
463
541
  | `*_not_cont` | Does not contain |
464
542
  | `*_not_cont_any` | Does not contain any of | |
465
543
  | `*_not_cont_all` | Does not contain all of | |
466
- | `*_i_cont` | Contains value with case insensitive | uses `LIKE` |
544
+ | `*_i_cont` | Contains value with case insensitive | uses `ILIKE` |
467
545
  | `*_i_cont_any` | Contains any of values with case insensitive | |
468
546
  | `*_i_cont_all` | Contains all of values with case insensitive | |
469
547
  | `*_not_i_cont` | Does not contain with case insensitive |
@@ -673,6 +751,43 @@ Trying it out in `rails console`:
673
751
 
674
752
  That's it! Now you know how to whitelist/blacklist various elements in Ransack.
675
753
 
754
+ ### Handling unknown predicates or attributes
755
+
756
+ By default, Ransack will ignore any unknown predicates or attributes:
757
+
758
+ ```ruby
759
+ Article.ransack(unknown_attr_eq: 'Ernie').result.to_sql
760
+ => SELECT "articles".* FROM "articles"
761
+ ```
762
+
763
+ Ransack may be configured to raise an error if passed an unknown predicate or
764
+ attributes, by setting the `ignore_unknown_conditions` option to `false` in your
765
+ Ransack initializer file at `config/initializers/ransack.rb`:
766
+
767
+ ```ruby
768
+ Ransack.configure do |c|
769
+ # Raise errors if a query contains an unknown predicate or attribute.
770
+ # Default is true (do not raise error on unknown conditions).
771
+ c.ignore_unknown_conditions = false
772
+ end
773
+ ```
774
+
775
+ ```ruby
776
+ Article.ransack(unknown_attr_eq: 'Ernie')
777
+ # ArgumentError (Invalid search term unknown_attr_eq)
778
+ ```
779
+
780
+ As an alternative to setting a global configuration option, the `.ransack!`
781
+ class method also raises an error if passed an unknown condition:
782
+
783
+ ```ruby
784
+ Article.ransack!(unknown_attr_eq: 'Ernie')
785
+ # ArgumentError: Invalid search term unknown_attr_eq
786
+ ```
787
+
788
+ This is equivalent to the `ignore_unknown_conditions` configuration option,
789
+ except it may be applied on a case-by-case basis.
790
+
676
791
  ### Using Scopes/Class Methods
677
792
 
678
793
  Continuing on from the preceding section, searching by scopes requires defining
@@ -869,6 +984,28 @@ en:
869
984
  title: Old Ransack Namespaced Title
870
985
  ```
871
986
 
987
+ ### Updating From MetaSearch
988
+
989
+ Ransack works much like MetaSearch, for those of you who are familiar with
990
+ it, and requires very little setup effort.
991
+
992
+ If you're coming from MetaSearch, things to note:
993
+
994
+ 1. The default param key for search params is now `:q`, instead of `:search`.
995
+ This is primarily to shorten query strings, though advanced queries (below)
996
+ will still run afoul of URL length limits in most browsers and require a
997
+ switch to HTTP POST requests. This key is
998
+ [configurable](default-search-parameter) via setting the `search_key` option
999
+ in your Ransack intitializer file.
1000
+
1001
+ 2. `form_for` is now `search_form_for`, and validates that a Ransack::Search
1002
+ object is passed to it.
1003
+
1004
+ 3. Common ActiveRecord::Relation methods are no longer delegated by the
1005
+ search object. Instead, you will get your search results (an
1006
+ ActiveRecord::Relation in the case of the ActiveRecord adapter) via a call to
1007
+ `Ransack#result`.
1008
+
872
1009
  ## Mongoid
873
1010
 
874
1011
  Mongoid support has been moved to its own gem at [ransack-mongoid](https://github.com/activerecord-hackery/ransack-mongoid).
@@ -0,0 +1,78 @@
1
+ # test-ransack-scope-and-column-same-name.rb
2
+
3
+ # This is a stand-alone test case.
4
+
5
+ # Run it in your console with: `ruby test-ransack-scope-and-column-same-name.rb`
6
+
7
+ # If you change the gem dependencies, run it with:
8
+ # `rm gemfile* && ruby test-ransack-scope-and-column-same-name.rb`
9
+
10
+ unless File.exist?('Gemfile')
11
+ File.write('Gemfile', <<-GEMFILE)
12
+ source 'https://rubygems.org'
13
+
14
+ # Rails master
15
+ gem 'rails', github: 'rails/rails', branch: '6-1-stable'
16
+
17
+ # Rails last release
18
+ # gem 'rails'
19
+
20
+ gem 'sqlite3'
21
+ gem 'ransack', github: 'activerecord-hackery/ransack'
22
+ GEMFILE
23
+
24
+ system 'bundle install'
25
+ end
26
+
27
+ require 'bundler'
28
+ Bundler.setup(:default)
29
+
30
+ require 'active_record'
31
+ require 'minitest/autorun'
32
+ require 'logger'
33
+ require 'ransack'
34
+
35
+ # This connection will do for database-independent bug reports.
36
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
37
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
38
+
39
+ # Display versions.
40
+ message = "Running test case with Ruby #{RUBY_VERSION}, Active Record #{
41
+ ::ActiveRecord::VERSION::STRING}, Arel #{Arel::VERSION} and #{
42
+ ::ActiveRecord::Base.connection.adapter_name}"
43
+ line = '=' * message.length
44
+ puts line, message, line
45
+
46
+ ActiveRecord::Schema.define do
47
+ create_table :users, force: true do |t|
48
+ t.boolean :active, null: false, default: true
49
+ end
50
+ end
51
+
52
+ class User < ActiveRecord::Base
53
+ scope :activated, -> (boolean = true) { where(active: boolean) }
54
+
55
+ private
56
+
57
+ def self.ransackable_scopes(auth_object = nil)
58
+ %i(activated)
59
+ end
60
+ end
61
+
62
+ class BugTest < Minitest::Test
63
+ def test_activated_scope_equals_true
64
+ sql = User.ransack({ activated: true }).result.to_sql
65
+ puts sql
66
+ assert_equal(
67
+ "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"active\" = 1", sql
68
+ )
69
+ end
70
+
71
+ def test_activated_scope_equals_false
72
+ sql = User.ransack({ activated: false }).result.to_sql
73
+ puts sql
74
+ assert_equal(
75
+ "SELECT \"users\".* FROM \"users\"", sql
76
+ )
77
+ end
78
+ end
@@ -0,0 +1,71 @@
1
+ # test-ransacker-arel-present-predicate.rb
2
+
3
+ # Run it in your console with: `ruby test-ransacker-arel-present-predicate.rb`
4
+
5
+ # If you change the gem dependencies, run it with:
6
+ # `rm gemfile* && ruby test-ransacker-arel-present-predicate.rb`
7
+
8
+ unless File.exist?('Gemfile')
9
+ File.write('Gemfile', <<-GEMFILE)
10
+ source 'https://rubygems.org'
11
+
12
+ # Rails master
13
+ gem 'rails', github: 'rails/rails', branch: '6-1-stable'
14
+
15
+ # Rails last release
16
+ # gem 'rails'
17
+
18
+ gem 'sqlite3'
19
+ gem 'ransack', github: 'activerecord-hackery/ransack'
20
+ GEMFILE
21
+
22
+ system 'bundle install'
23
+ end
24
+
25
+ require 'bundler'
26
+ Bundler.setup(:default)
27
+
28
+ require 'active_record'
29
+ require 'minitest/autorun'
30
+ require 'logger'
31
+ require 'ransack'
32
+
33
+ # This connection will do for database-independent bug reports.
34
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
35
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
36
+
37
+ # Display versions.
38
+ message = "Running test case with Ruby #{RUBY_VERSION}, Active Record #{
39
+ ::ActiveRecord::VERSION::STRING}, Arel #{Arel::VERSION} and #{
40
+ ::ActiveRecord::Base.connection.adapter_name}"
41
+ line = '=' * message.length
42
+ puts line, message, line
43
+
44
+ ActiveRecord::Schema.define do
45
+ create_table :projects, force: true do |t|
46
+ t.string :name
47
+ t.string :number
48
+ end
49
+ end
50
+
51
+ class Project < ActiveRecord::Base
52
+ ransacker :name do
53
+ Arel.sql('projects.name')
54
+ end
55
+
56
+ ransacker :number do |parent|
57
+ parent.table[:number]
58
+ end
59
+ end
60
+
61
+ class BugTest < Minitest::Test
62
+ def test_ransackers
63
+ sql = Project.ransack({ number_present: 1 }).result.to_sql
64
+ puts sql
65
+ assert_equal "SELECT \"projects\".* FROM \"projects\" WHERE (\"projects\".\"number\" IS NOT NULL AND \"projects\".\"number\" != '')", sql
66
+
67
+ sql = Project.ransack({ name_present: 1 }).result.to_sql
68
+ puts sql
69
+ assert_equal "SELECT \"projects\".* FROM \"projects\" WHERE (projects.name IS NOT NULL AND projects.name != '')", sql
70
+ end
71
+ end
Binary file
@@ -0,0 +1,17 @@
1
+ ## Release Process
2
+
3
+ *For maintainers of Ransack.*
4
+
5
+ To release a new version of Ransack and publish it to RubyGems, take the following steps:
6
+
7
+ - Create a new release, marked `Prerelease`.
8
+ - Update the versions file to the new release, commit and push to `master`.
9
+ - Update the [`version.rb`](../lib/ransack/version.rb) file to the new release, commit and push to `master`.
10
+ - From the terminal, run the following commands:
11
+
12
+ ```bash
13
+ rake build
14
+ rake release
15
+ ```
16
+
17
+ ![Create a Release](img/create_release.png)
@@ -16,5 +16,9 @@ module Polyamorous
16
16
  super(reflection, children)
17
17
  end
18
18
  end
19
+
20
+ def ==(other)
21
+ base_klass == other.base_klass
22
+ end
19
23
  end
20
24
  end
@@ -0,0 +1,11 @@
1
+ module Polyamorous
2
+ module ReflectionExtensions
3
+ def join_scope(table, foreign_table, foreign_klass)
4
+ if respond_to?(:polymorphic?) && polymorphic?
5
+ super.where!(foreign_table[foreign_type].eq(klass.name))
6
+ else
7
+ super
8
+ end
9
+ end
10
+ end
11
+ end
@@ -1,2 +1 @@
1
- # active_record_6.0_ruby_2/join_association
2
1
  require 'polyamorous/activerecord_5.2_ruby_2/join_association'
@@ -1,5 +1,4 @@
1
1
  # active_record_6.0_ruby_2/join_dependency.rb
2
-
3
2
  module Polyamorous
4
3
  module JoinDependencyExtensions
5
4
  # Replaces ActiveRecord::Associations::JoinDependency#build
@@ -1,2 +1 @@
1
- # active_record_6.0_ruby_2/reflection.rb
2
1
  require 'polyamorous/activerecord_5.2_ruby_2/reflection'
@@ -0,0 +1,74 @@
1
+ module Polyamorous
2
+ module JoinAssociationExtensions
3
+ include SwappingReflectionClass
4
+ def self.prepended(base)
5
+ base.class_eval { attr_reader :join_type }
6
+ end
7
+
8
+ def initialize(reflection, children, polymorphic_class = nil, join_type = Arel::Nodes::InnerJoin)
9
+ @join_type = join_type
10
+ if polymorphic_class && ::ActiveRecord::Base > polymorphic_class
11
+ swapping_reflection_klass(reflection, polymorphic_class) do |reflection|
12
+ super(reflection, children)
13
+ self.reflection.options[:polymorphic] = true
14
+ end
15
+ else
16
+ super(reflection, children)
17
+ end
18
+ end
19
+
20
+ # Same as #join_constraints, but instead of constructing tables from the
21
+ # given block, uses the ones passed
22
+ def join_constraints_with_tables(foreign_table, foreign_klass, join_type, alias_tracker, tables)
23
+ joins = []
24
+ chain = []
25
+
26
+ reflection.chain.each.with_index do |reflection, i|
27
+ table = tables[i]
28
+
29
+ @table ||= table
30
+ chain << [reflection, table]
31
+ end
32
+
33
+ # The chain starts with the target table, but we want to end with it here (makes
34
+ # more sense in this context), so we reverse
35
+ chain.reverse_each do |reflection, table|
36
+ klass = reflection.klass
37
+
38
+ join_scope = reflection.join_scope(table, foreign_table, foreign_klass)
39
+
40
+ unless join_scope.references_values.empty?
41
+ join_dependency = join_scope.construct_join_dependency(
42
+ join_scope.eager_load_values | join_scope.includes_values, Arel::Nodes::OuterJoin
43
+ )
44
+ join_scope.joins!(join_dependency)
45
+ end
46
+
47
+ arel = join_scope.arel(alias_tracker.aliases)
48
+ nodes = arel.constraints.first
49
+
50
+ if nodes.is_a?(Arel::Nodes::And)
51
+ others = nodes.children.extract! do |node|
52
+ !Arel.fetch_attribute(node) { |attr| attr.relation.name == table.name }
53
+ end
54
+ end
55
+
56
+ joins << table.create_join(table, table.create_on(nodes), join_type)
57
+
58
+ if others && !others.empty?
59
+ joins.concat arel.join_sources
60
+ append_constraints(joins.last, others)
61
+ end
62
+
63
+ # The current table in this iteration becomes the foreign table in the next
64
+ foreign_table, foreign_klass = table, klass
65
+ end
66
+
67
+ joins
68
+ end
69
+
70
+ def ==(other)
71
+ base_klass == other.base_klass
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,93 @@
1
+ # active_record_6.1_ruby_2/join_dependency.rb
2
+ module Polyamorous
3
+ module JoinDependencyExtensions
4
+ # Replaces ActiveRecord::Associations::JoinDependency#build
5
+ def build(associations, base_klass)
6
+ associations.map do |name, right|
7
+ if name.is_a? Join
8
+ reflection = find_reflection base_klass, name.name
9
+ reflection.check_validity!
10
+ reflection.check_eager_loadable!
11
+
12
+ klass = if reflection.polymorphic?
13
+ name.klass || base_klass
14
+ else
15
+ reflection.klass
16
+ end
17
+ JoinAssociation.new(reflection, build(right, klass), name.klass, name.type)
18
+ else
19
+ reflection = find_reflection base_klass, name
20
+ reflection.check_validity!
21
+ reflection.check_eager_loadable!
22
+
23
+ if reflection.polymorphic?
24
+ raise ActiveRecord::EagerLoadPolymorphicError.new(reflection)
25
+ end
26
+ JoinAssociation.new(reflection, build(right, reflection.klass))
27
+ end
28
+ end
29
+ end
30
+
31
+ def join_constraints(joins_to_add, alias_tracker, references)
32
+ @alias_tracker = alias_tracker
33
+ @joined_tables = {}
34
+ @references = {}
35
+
36
+ references.each do |table_name|
37
+ @references[table_name.to_sym] = table_name if table_name.is_a?(String)
38
+ end
39
+
40
+ joins = make_join_constraints(join_root, join_type)
41
+
42
+ joins.concat joins_to_add.flat_map { |oj|
43
+ if join_root.match?(oj.join_root) && join_root.table.name == oj.join_root.table.name
44
+ walk join_root, oj.join_root, oj.join_type
45
+ else
46
+ make_join_constraints(oj.join_root, oj.join_type)
47
+ end
48
+ }
49
+ end
50
+
51
+ def construct_tables_for_association!(join_root, association)
52
+ tables = table_aliases_for(join_root, association)
53
+ association.table = tables.first
54
+ tables
55
+ end
56
+
57
+ private
58
+
59
+ def table_aliases_for(parent, node)
60
+ node.reflection.chain.map { |reflection|
61
+ alias_tracker.aliased_table_for(reflection.klass.arel_table) do
62
+ root = reflection == node.reflection
63
+ name = reflection.alias_candidate(parent.table_name)
64
+ root ? name : "#{name}_join"
65
+ end
66
+ }
67
+ end
68
+
69
+ module ClassMethods
70
+ # Prepended before ActiveRecord::Associations::JoinDependency#walk_tree
71
+ #
72
+ def walk_tree(associations, hash)
73
+ case associations
74
+ when TreeNode
75
+ associations.add_to_tree(hash)
76
+ when Hash
77
+ associations.each do |k, v|
78
+ cache =
79
+ if TreeNode === k
80
+ k.add_to_tree(hash)
81
+ else
82
+ hash[k] ||= {}
83
+ end
84
+ walk_tree(v, cache)
85
+ end
86
+ else
87
+ super(associations, hash)
88
+ end
89
+ end
90
+ end
91
+
92
+ end
93
+ end
@@ -1,2 +1 @@
1
- # active_record_6.1_ruby_2/reflection.rb
2
1
  require 'polyamorous/activerecord_6.0_ruby_2/reflection'
@@ -0,0 +1 @@
1
+ require 'polyamorous/activerecord_6.1_ruby_2/join_association'
@@ -0,0 +1 @@
1
+ require 'polyamorous/activerecord_6.1_ruby_2/join_dependency'
@@ -0,0 +1 @@
1
+ require 'polyamorous/activerecord_6.1_ruby_2/reflection'
File without changes
@@ -11,7 +11,7 @@ if defined?(::ActiveRecord)
11
11
  require 'polyamorous/join'
12
12
  require 'polyamorous/swapping_reflection_class'
13
13
 
14
- ar_version = ::ActiveRecord::VERSION::STRING[0,3]
14
+ ar_version = ::ActiveRecord::VERSION::STRING[0, 3]
15
15
  %w(join_association join_dependency reflection).each do |file|
16
16
  require "polyamorous/activerecord_#{ar_version}_ruby_2/#{file}"
17
17
  end
File without changes
@@ -0,0 +1 @@
1
+ require 'polyamorous/polyamorous'