ransack 2.3.0 → 4.0.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 (159) 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/codeql.yml +72 -0
  5. data/.github/workflows/cronjob.yml +102 -0
  6. data/.github/workflows/deploy.yml +35 -0
  7. data/.github/workflows/rubocop.yml +20 -0
  8. data/.github/workflows/test-deploy.yml +29 -0
  9. data/.github/workflows/test.yml +128 -0
  10. data/.nojekyll +0 -0
  11. data/.rubocop.yml +47 -0
  12. data/CHANGELOG.md +228 -1
  13. data/CONTRIBUTING.md +47 -22
  14. data/Gemfile +24 -10
  15. data/README.md +49 -917
  16. data/bug_report_templates/test-ransack-scope-and-column-same-name.rb +78 -0
  17. data/bug_report_templates/test-ransacker-arel-present-predicate.rb +75 -0
  18. data/docs/.gitignore +19 -0
  19. data/docs/.nojekyll +0 -0
  20. data/docs/babel.config.js +3 -0
  21. data/docs/blog/2022-03-27-ransack-3.0.0.md +20 -0
  22. data/docs/docs/getting-started/_category_.json +4 -0
  23. data/docs/docs/getting-started/advanced-mode.md +46 -0
  24. data/docs/docs/getting-started/configuration.md +47 -0
  25. data/docs/docs/getting-started/search-matches.md +67 -0
  26. data/docs/docs/getting-started/simple-mode.md +288 -0
  27. data/docs/docs/getting-started/sorting.md +79 -0
  28. data/docs/docs/getting-started/using-predicates.md +282 -0
  29. data/docs/docs/going-further/_category_.json +4 -0
  30. data/docs/docs/going-further/acts-as-taggable-on.md +114 -0
  31. data/docs/docs/going-further/associations.md +70 -0
  32. data/docs/docs/going-further/custom-predicates.md +52 -0
  33. data/docs/docs/going-further/documentation.md +43 -0
  34. data/docs/docs/going-further/exporting-to-csv.md +49 -0
  35. data/docs/docs/going-further/external-guides.md +57 -0
  36. data/docs/docs/going-further/form-customisation.md +63 -0
  37. data/docs/docs/going-further/i18n.md +53 -0
  38. data/docs/docs/going-further/img/create_release.png +0 -0
  39. data/docs/docs/going-further/merging-searches.md +41 -0
  40. data/docs/docs/going-further/other-notes.md +428 -0
  41. data/docs/docs/going-further/polymorphic-search.md +40 -0
  42. data/docs/docs/going-further/ransackers.md +331 -0
  43. data/docs/docs/going-further/release_process.md +36 -0
  44. data/docs/docs/going-further/saving-queries.md +82 -0
  45. data/docs/docs/going-further/searching-postgres.md +57 -0
  46. data/docs/docs/going-further/wiki-contributors.md +82 -0
  47. data/docs/docs/intro.md +99 -0
  48. data/docs/docusaurus.config.js +120 -0
  49. data/docs/package.json +42 -0
  50. data/docs/sidebars.js +31 -0
  51. data/docs/src/components/HomepageFeatures/index.js +64 -0
  52. data/docs/src/components/HomepageFeatures/styles.module.css +11 -0
  53. data/docs/src/css/custom.css +39 -0
  54. data/docs/src/pages/index.module.css +23 -0
  55. data/docs/src/pages/markdown-page.md +7 -0
  56. data/docs/static/.nojekyll +0 -0
  57. data/docs/static/img/docusaurus.png +0 -0
  58. data/docs/static/img/favicon.ico +0 -0
  59. data/docs/static/img/logo.svg +1 -0
  60. data/docs/static/img/tutorial/docsVersionDropdown.png +0 -0
  61. data/docs/static/img/tutorial/localeDropdown.png +0 -0
  62. data/docs/static/img/undraw_docusaurus_mountain.svg +171 -0
  63. data/docs/static/img/undraw_docusaurus_react.svg +170 -0
  64. data/docs/static/img/undraw_docusaurus_tree.svg +40 -0
  65. data/docs/yarn.lock +8790 -0
  66. data/lib/polyamorous/activerecord_6.1_ruby_2/join_association.rb +70 -0
  67. data/{polyamorous/lib/polyamorous/activerecord_6.0_ruby_2 → lib/polyamorous/activerecord_6.1_ruby_2}/join_dependency.rb +23 -12
  68. data/lib/polyamorous/activerecord_6.1_ruby_2/reflection.rb +11 -0
  69. data/lib/polyamorous/activerecord_7.0_ruby_2/join_association.rb +1 -0
  70. data/lib/polyamorous/activerecord_7.0_ruby_2/join_dependency.rb +1 -0
  71. data/lib/polyamorous/activerecord_7.0_ruby_2/reflection.rb +1 -0
  72. data/lib/polyamorous/activerecord_7.1_ruby_2/join_association.rb +1 -0
  73. data/lib/polyamorous/activerecord_7.1_ruby_2/join_dependency.rb +1 -0
  74. data/lib/polyamorous/activerecord_7.1_ruby_2/reflection.rb +1 -0
  75. data/{polyamorous/lib → lib/polyamorous}/polyamorous.rb +3 -8
  76. data/lib/ransack/adapters/active_record/base.rb +83 -10
  77. data/lib/ransack/adapters/active_record/context.rb +59 -115
  78. data/lib/ransack/configuration.rb +53 -10
  79. data/lib/ransack/constants.rb +126 -7
  80. data/lib/ransack/context.rb +34 -5
  81. data/lib/ransack/helpers/form_builder.rb +11 -17
  82. data/lib/ransack/helpers/form_helper.rb +14 -5
  83. data/lib/ransack/helpers.rb +1 -1
  84. data/lib/ransack/locale/sk.yml +70 -0
  85. data/lib/ransack/locale/sv.yml +70 -0
  86. data/lib/ransack/nodes/attribute.rb +3 -3
  87. data/lib/ransack/nodes/condition.rb +87 -8
  88. data/lib/ransack/nodes/grouping.rb +4 -4
  89. data/lib/ransack/nodes/node.rb +1 -1
  90. data/lib/ransack/nodes/sort.rb +3 -3
  91. data/lib/ransack/nodes/value.rb +3 -3
  92. data/lib/ransack/predicate.rb +3 -2
  93. data/lib/ransack/ransacker.rb +1 -1
  94. data/lib/ransack/search.rb +15 -7
  95. data/lib/ransack/translate.rb +6 -6
  96. data/lib/ransack/version.rb +1 -1
  97. data/lib/ransack/visitor.rb +38 -2
  98. data/lib/ransack.rb +6 -10
  99. data/ransack.gemspec +9 -24
  100. data/spec/blueprints/articles.rb +1 -1
  101. data/spec/blueprints/comments.rb +1 -1
  102. data/spec/blueprints/notes.rb +1 -1
  103. data/spec/blueprints/tags.rb +1 -1
  104. data/spec/console.rb +5 -5
  105. data/spec/helpers/polyamorous_helper.rb +2 -17
  106. data/spec/helpers/ransack_helper.rb +1 -1
  107. data/spec/polyamorous/activerecord_compatibility_spec.rb +15 -0
  108. data/spec/{ransack → polyamorous}/join_association_spec.rb +3 -1
  109. data/spec/{ransack → polyamorous}/join_dependency_spec.rb +0 -16
  110. data/spec/ransack/adapters/active_record/base_spec.rb +109 -16
  111. data/spec/ransack/adapters/active_record/context_spec.rb +19 -18
  112. data/spec/ransack/configuration_spec.rb +33 -9
  113. data/spec/ransack/helpers/form_builder_spec.rb +8 -8
  114. data/spec/ransack/helpers/form_helper_spec.rb +109 -20
  115. data/spec/ransack/nodes/condition_spec.rb +37 -0
  116. data/spec/ransack/nodes/grouping_spec.rb +2 -2
  117. data/spec/ransack/nodes/value_spec.rb +115 -0
  118. data/spec/ransack/predicate_spec.rb +75 -2
  119. data/spec/ransack/search_spec.rb +239 -38
  120. data/spec/ransack/translate_spec.rb +1 -1
  121. data/spec/spec_helper.rb +9 -5
  122. data/spec/support/schema.rb +83 -12
  123. metadata +105 -195
  124. data/.travis.yml +0 -49
  125. data/lib/ransack/adapters/active_record/ransack/constants.rb +0 -116
  126. data/lib/ransack/adapters/active_record/ransack/context.rb +0 -60
  127. data/lib/ransack/adapters/active_record/ransack/nodes/condition.rb +0 -61
  128. data/lib/ransack/adapters/active_record/ransack/translate.rb +0 -8
  129. data/lib/ransack/adapters/active_record/ransack/visitor.rb +0 -47
  130. data/lib/ransack/adapters.rb +0 -64
  131. data/lib/ransack/nodes.rb +0 -8
  132. data/polyamorous/lib/polyamorous/activerecord_5.0_ruby_2/join_association.rb +0 -2
  133. data/polyamorous/lib/polyamorous/activerecord_5.0_ruby_2/join_dependency.rb +0 -2
  134. data/polyamorous/lib/polyamorous/activerecord_5.1_ruby_2/join_association.rb +0 -31
  135. data/polyamorous/lib/polyamorous/activerecord_5.1_ruby_2/join_dependency.rb +0 -112
  136. data/polyamorous/lib/polyamorous/activerecord_5.2.0_ruby_2/join_association.rb +0 -31
  137. data/polyamorous/lib/polyamorous/activerecord_5.2.0_ruby_2/join_dependency.rb +0 -112
  138. data/polyamorous/lib/polyamorous/activerecord_5.2.0_ruby_2/reflection.rb +0 -12
  139. data/polyamorous/lib/polyamorous/activerecord_5.2.1_ruby_2/join_association.rb +0 -22
  140. data/polyamorous/lib/polyamorous/activerecord_5.2.1_ruby_2/join_dependency.rb +0 -81
  141. data/polyamorous/lib/polyamorous/activerecord_5.2.1_ruby_2/reflection.rb +0 -2
  142. data/polyamorous/lib/polyamorous/activerecord_6.0_ruby_2/join_association.rb +0 -2
  143. data/polyamorous/lib/polyamorous/activerecord_6.0_ruby_2/reflection.rb +0 -2
  144. data/polyamorous/lib/polyamorous/activerecord_6.1_ruby_2/join_association.rb +0 -2
  145. data/polyamorous/lib/polyamorous/activerecord_6.1_ruby_2/join_dependency.rb +0 -2
  146. data/polyamorous/lib/polyamorous/activerecord_6.1_ruby_2/reflection.rb +0 -2
  147. data/polyamorous/lib/polyamorous/version.rb +0 -3
  148. data/polyamorous/polyamorous.gemspec +0 -35
  149. /data/{logo → docs/static/logo}/ransack-h.png +0 -0
  150. /data/{logo → docs/static/logo}/ransack-h.svg +0 -0
  151. /data/{logo → docs/static/logo}/ransack-v.png +0 -0
  152. /data/{logo → docs/static/logo}/ransack-v.svg +0 -0
  153. /data/{logo → docs/static/logo}/ransack.png +0 -0
  154. /data/{logo → docs/static/logo}/ransack.svg +0 -0
  155. /data/{polyamorous/lib → lib}/polyamorous/join.rb +0 -0
  156. /data/{polyamorous/lib → lib}/polyamorous/swapping_reflection_class.rb +0 -0
  157. /data/{polyamorous/lib → lib}/polyamorous/tree_node.rb +0 -0
  158. /data/lib/ransack/{adapters/active_record.rb → active_record.rb} +0 -0
  159. /data/spec/{ransack → polyamorous}/join_spec.rb +0 -0
@@ -0,0 +1,70 @@
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
+ end
70
+ end
@@ -1,5 +1,3 @@
1
- # active_record_6.0_ruby_2/join_dependency.rb
2
-
3
1
  module Polyamorous
4
2
  module JoinDependencyExtensions
5
3
  # Replaces ActiveRecord::Associations::JoinDependency#build
@@ -29,14 +27,18 @@ module Polyamorous
29
27
  end
30
28
  end
31
29
 
32
- def join_constraints(joins_to_add, alias_tracker)
30
+ def join_constraints(joins_to_add, alias_tracker, references)
33
31
  @alias_tracker = alias_tracker
32
+ @joined_tables = {}
33
+ @references = {}
34
+
35
+ references.each do |table_name|
36
+ @references[table_name.to_sym] = table_name if table_name.is_a?(String)
37
+ end
34
38
 
35
- construct_tables!(join_root)
36
39
  joins = make_join_constraints(join_root, join_type)
37
40
 
38
41
  joins.concat joins_to_add.flat_map { |oj|
39
- construct_tables!(oj.join_root)
40
42
  if join_root.match?(oj.join_root) && join_root.table.name == oj.join_root.table.name
41
43
  walk join_root, oj.join_root, oj.join_type
42
44
  else
@@ -45,14 +47,23 @@ module Polyamorous
45
47
  }
46
48
  end
47
49
 
50
+ def construct_tables_for_association!(join_root, association)
51
+ tables = table_aliases_for(join_root, association)
52
+ association.table = tables.first
53
+ tables
54
+ end
55
+
48
56
  private
49
- def make_constraints(parent, child, join_type = Arel::Nodes::OuterJoin)
50
- foreign_table = parent.table
51
- foreign_klass = parent.base_klass
52
- join_type = child.join_type || join_type if join_type == Arel::Nodes::InnerJoin
53
- joins = child.join_constraints(foreign_table, foreign_klass, join_type, alias_tracker)
54
- joins.concat child.children.flat_map { |c| make_constraints(child, c, join_type) }
55
- end
57
+
58
+ def table_aliases_for(parent, node)
59
+ node.reflection.chain.map { |reflection|
60
+ alias_tracker.aliased_table_for(reflection.klass.arel_table) do
61
+ root = reflection == node.reflection
62
+ name = reflection.alias_candidate(parent.table_name)
63
+ root ? name : "#{name}_join"
64
+ end
65
+ }
66
+ end
56
67
 
57
68
  module ClassMethods
58
69
  # Prepended before ActiveRecord::Associations::JoinDependency#walk_tree
@@ -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
@@ -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'
@@ -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'
@@ -11,17 +11,12 @@ 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]
15
- ar_version = ::ActiveRecord::VERSION::STRING[0,5] if ar_version >= "5.2" && ::ActiveRecord.version < ::Gem::Version.new("6.0")
16
- ar_version = "5.2.1" if ::ActiveRecord::VERSION::STRING >= "5.2.1" && ::ActiveRecord.version < ::Gem::Version.new("6.0")
17
- %w(join_association join_dependency).each do |file|
14
+ ar_version = ::ActiveRecord::VERSION::STRING[0, 3]
15
+ %w(join_association join_dependency reflection).each do |file|
18
16
  require "polyamorous/activerecord_#{ar_version}_ruby_2/#{file}"
19
17
  end
20
18
 
21
- if ar_version >= "5.2.0"
22
- require "polyamorous/activerecord_#{ar_version}_ruby_2/reflection.rb"
23
- ::ActiveRecord::Reflection::AbstractReflection.send(:prepend, Polyamorous::ReflectionExtensions)
24
- end
19
+ ActiveRecord::Reflection::AbstractReflection.send(:prepend, Polyamorous::ReflectionExtensions)
25
20
 
26
21
  Polyamorous::JoinDependency.send(:prepend, Polyamorous::JoinDependencyExtensions)
27
22
  Polyamorous::JoinDependency.singleton_class.send(:prepend, Polyamorous::JoinDependencyExtensions::ClassMethods)
@@ -4,7 +4,6 @@ module Ransack
4
4
  module Base
5
5
 
6
6
  def self.extended(base)
7
- alias :search :ransack unless base.respond_to? :search
8
7
  base.class_eval do
9
8
  class_attribute :_ransackers
10
9
  class_attribute :_ransack_aliases
@@ -14,10 +13,13 @@ module Ransack
14
13
  end
15
14
 
16
15
  def ransack(params = {}, options = {})
17
- ActiveSupport::Deprecation.warn("#search is deprecated and will be removed in 2.3, please use #ransack instead") if __callee__ == :search
18
16
  Search.new(self, params, options)
19
17
  end
20
18
 
19
+ def ransack!(params = {}, options = {})
20
+ ransack(params, options.merge(ignore_unknown_conditions: false))
21
+ end
22
+
21
23
  def ransacker(name, opts = {}, &block)
22
24
  self._ransackers = _ransackers.merge name.to_s => Ransacker
23
25
  .new(self, name, opts, &block)
@@ -33,12 +35,7 @@ module Ransack
33
35
  # For overriding with a whitelist array of strings.
34
36
  #
35
37
  def ransackable_attributes(auth_object = nil)
36
- @ransackable_attributes ||= if Ransack::SUPPORTS_ATTRIBUTE_ALIAS
37
- column_names + _ransackers.keys + _ransack_aliases.keys +
38
- attribute_aliases.keys
39
- else
40
- column_names + _ransackers.keys + _ransack_aliases.keys
41
- end
38
+ @ransackable_attributes ||= deprecated_ransackable_list(:ransackable_attributes)
42
39
  end
43
40
 
44
41
  # Ransackable_associations, by default, returns the names
@@ -46,7 +43,7 @@ module Ransack
46
43
  # For overriding with a whitelist array of strings.
47
44
  #
48
45
  def ransackable_associations(auth_object = nil)
49
- @ransackable_associations ||= reflect_on_all_associations.map { |a| a.name.to_s }
46
+ @ransackable_associations ||= deprecated_ransackable_list(:ransackable_associations)
50
47
  end
51
48
 
52
49
  # Ransortable_attributes, by default, returns the names
@@ -66,13 +63,89 @@ module Ransack
66
63
  end
67
64
 
68
65
  # ransack_scope_skip_sanitize_args, by default, returns an empty array.
69
- # i.e. use the sanitize_scope_args setting to determin if args should be converted.
66
+ # i.e. use the sanitize_scope_args setting to determine if args should be converted.
70
67
  # For overriding with a list of scopes which should be passed the args as-is.
71
68
  #
72
69
  def ransackable_scopes_skip_sanitize_args
73
70
  []
74
71
  end
75
72
 
73
+ # Bare list of all potentially searchable attributes. Searchable attributes
74
+ # need to be explicitly allowlisted through the `ransackable_attributes`
75
+ # method in each model, but if you're allowing almost everything to be
76
+ # searched, this list can be used as a base for exclusions.
77
+ #
78
+ def authorizable_ransackable_attributes
79
+ if Ransack::SUPPORTS_ATTRIBUTE_ALIAS
80
+ column_names + _ransackers.keys + _ransack_aliases.keys +
81
+ attribute_aliases.keys
82
+ else
83
+ column_names + _ransackers.keys + _ransack_aliases.keys
84
+ end.uniq
85
+ end
86
+
87
+ # Bare list of all potentially searchable associations. Searchable
88
+ # associations need to be explicitly allowlisted through the
89
+ # `ransackable_associations` method in each model, but if you're
90
+ # allowing almost everything to be searched, this list can be used as a
91
+ # base for exclusions.
92
+ #
93
+ def authorizable_ransackable_associations
94
+ reflect_on_all_associations.map { |a| a.name.to_s }
95
+ end
96
+
97
+ private
98
+
99
+ def deprecated_ransackable_list(method)
100
+ list_type = method.to_s.delete_prefix("ransackable_")
101
+
102
+ if explicitly_defined?(method)
103
+ warn_deprecated <<~ERROR
104
+ Ransack's builtin `#{method}` method is deprecated and will result
105
+ in an error in the future. If you want to authorize the full list
106
+ of searchable #{list_type} for this model, use
107
+ `authorizable_#{method}` instead of delegating to `super`.
108
+ ERROR
109
+
110
+ public_send("authorizable_#{method}")
111
+ else
112
+ raise <<~MESSAGE
113
+ Ransack needs #{name} #{list_type} explicitly allowlisted as
114
+ searchable. Define a `#{method}` class method in your `#{name}`
115
+ model, watching out for items you DON'T want searchable (for
116
+ example, `encrypted_password`, `password_reset_token`, `owner` or
117
+ other sensitive information). You can use the following as a base:
118
+
119
+ ```ruby
120
+ class #{name} < ApplicationRecord
121
+
122
+ # ...
123
+
124
+ def self.#{method}(auth_object = nil)
125
+ #{public_send("authorizable_#{method}").sort.inspect}
126
+ end
127
+
128
+ # ...
129
+
130
+ end
131
+ ```
132
+ MESSAGE
133
+ end
134
+ end
135
+
136
+ def explicitly_defined?(method)
137
+ definer_ancestor = singleton_class.ancestors.find do |ancestor|
138
+ ancestor.instance_methods(false).include?(method)
139
+ end
140
+
141
+ definer_ancestor != Ransack::Adapters::ActiveRecord::Base
142
+ end
143
+
144
+ def warn_deprecated(message)
145
+ caller_location = caller_locations.find { |location| !location.path.start_with?(File.expand_path("../..", __dir__)) }
146
+
147
+ warn "DEPRECATION WARNING: #{message.squish} (called at #{caller_location.path}:#{caller_location.lineno})"
148
+ end
76
149
  end
77
150
  end
78
151
  end
@@ -1,18 +1,11 @@
1
1
  require 'ransack/context'
2
- require 'polyamorous'
2
+ require 'polyamorous/polyamorous'
3
3
 
4
4
  module Ransack
5
5
  module Adapters
6
6
  module ActiveRecord
7
7
  class Context < ::Ransack::Context
8
8
 
9
- def initialize(object, options = {})
10
- super
11
- if ::ActiveRecord::VERSION::STRING < Constants::RAILS_5_2
12
- @arel_visitor = @engine.connection.visitor
13
- end
14
- end
15
-
16
9
  def relation_for(object)
17
10
  object.all
18
11
  end
@@ -49,6 +42,17 @@ module Ransack
49
42
  if scope_or_sort.is_a?(Symbol)
50
43
  relation = relation.send(scope_or_sort)
51
44
  else
45
+ case Ransack.options[:postgres_fields_sort_option]
46
+ when :nulls_first
47
+ scope_or_sort = scope_or_sort.direction == :asc ? Arel.sql("#{scope_or_sort.to_sql} NULLS FIRST") : Arel.sql("#{scope_or_sort.to_sql} NULLS LAST")
48
+ when :nulls_last
49
+ scope_or_sort = scope_or_sort.direction == :asc ? Arel.sql("#{scope_or_sort.to_sql} NULLS LAST") : Arel.sql("#{scope_or_sort.to_sql} NULLS FIRST")
50
+ when :nulls_always_first
51
+ scope_or_sort = Arel.sql("#{scope_or_sort.to_sql} NULLS FIRST")
52
+ when :nulls_always_last
53
+ scope_or_sort = Arel.sql("#{scope_or_sort.to_sql} NULLS LAST")
54
+ end
55
+
52
56
  relation = relation.order(scope_or_sort)
53
57
  end
54
58
  end
@@ -104,26 +108,15 @@ module Ransack
104
108
  # JoinDependency to track table aliases.
105
109
  #
106
110
  def join_sources
107
- base, joins =
108
- if ::ActiveRecord::VERSION::STRING > Constants::RAILS_5_2_0
111
+ base, joins = begin
109
112
  alias_tracker = ::ActiveRecord::Associations::AliasTracker.create(self.klass.connection, @object.table.name, [])
110
- constraints = if ::Gem::Version.new(::ActiveRecord::VERSION::STRING) >= ::Gem::Version.new(Constants::RAILS_6_0)
111
- @join_dependency.join_constraints(@object.joins_values, alias_tracker)
112
- else
113
- @join_dependency.join_constraints(@object.joins_values, @join_type, alias_tracker)
114
- end
113
+ constraints = @join_dependency.join_constraints(@object.joins_values, alias_tracker, @object.references_values)
115
114
 
116
115
  [
117
116
  Arel::SelectManager.new(@object.table),
118
117
  constraints
119
118
  ]
120
- else
121
- [
122
- Arel::SelectManager.new(@object.table),
123
- @join_dependency.join_constraints(@object.joins_values, @join_type)
124
- ]
125
119
  end
126
- joins = joins.collect(&:joins).flatten if ::ActiveRecord::VERSION::STRING < Constants::RAILS_5_2
127
120
  joins.each do |aliased_join|
128
121
  base.from(aliased_join)
129
122
  end
@@ -144,6 +137,7 @@ module Ransack
144
137
  stashed.eql?(association)
145
138
  }
146
139
  @object.joins_values.delete_if { |jd|
140
+ jd.instance_variables.include?(:@join_root) &&
147
141
  jd.instance_variable_get(:@join_root).children.map(&:object_id) == [association.object_id]
148
142
  }
149
143
  end
@@ -186,16 +180,31 @@ module Ransack
186
180
  private
187
181
 
188
182
  def extract_correlated_key(join_root)
189
- correlated_key = join_root.right.expr.left
190
-
191
- if correlated_key.is_a? Arel::Nodes::And
192
- correlated_key = correlated_key.left.left
193
- elsif correlated_key.is_a? Arel::Nodes::Equality
194
- correlated_key = correlated_key.left
195
- elsif correlated_key.is_a? Arel::Nodes::Grouping
196
- correlated_key = join_root.right.expr.right.left
183
+ case join_root
184
+ when Arel::Nodes::OuterJoin
185
+ # one of join_root.right/join_root.left is expected to be Arel::Nodes::On
186
+ if join_root.right.is_a?(Arel::Nodes::On)
187
+ extract_correlated_key(join_root.right.expr)
188
+ elsif join_root.left.is_a?(Arel::Nodes::On)
189
+ extract_correlated_key(join_root.left.expr)
190
+ else
191
+ raise 'Ransack encountered an unexpected arel structure'
192
+ end
193
+ when Arel::Nodes::Equality
194
+ pk = primary_key
195
+ if join_root.left == pk
196
+ join_root.right
197
+ elsif join_root.right == pk
198
+ join_root.left
199
+ else
200
+ nil
201
+ end
202
+ when Arel::Nodes::And
203
+ extract_correlated_key(join_root.left) || extract_correlated_key(join_root.right)
197
204
  else
198
- correlated_key
205
+ # eg parent was Arel::Nodes::And and the evaluated side was one of
206
+ # Arel::Nodes::Grouping or MultiTenant::TenantEnforcementClause
207
+ nil
199
208
  end
200
209
  end
201
210
 
@@ -268,28 +277,11 @@ module Ransack
268
277
 
269
278
  join_list = join_nodes + convert_join_strings_to_ast(relation.table, string_joins)
270
279
 
271
- if ::ActiveRecord::VERSION::STRING < Constants::RAILS_5_2_0
272
- join_dependency = Polyamorous::JoinDependency.new(relation.klass, association_joins, join_list)
273
- join_nodes.each do |join|
274
- join_dependency.send(:alias_tracker).aliases[join.left.name.downcase] = 1
275
- end
276
- elsif ::ActiveRecord::VERSION::STRING == Constants::RAILS_5_2_0
277
- alias_tracker = ::ActiveRecord::Associations::AliasTracker.create(self.klass.connection, relation.table.name, join_list)
278
- join_dependency = Polyamorous::JoinDependency.new(relation.klass, relation.table, association_joins, alias_tracker)
279
- join_nodes.each do |join|
280
- join_dependency.send(:alias_tracker).aliases[join.left.name.downcase] = 1
281
- end
282
- else
283
- alias_tracker = ::ActiveRecord::Associations::AliasTracker.create(self.klass.connection, relation.table.name, join_list)
284
- join_dependency = if ::Gem::Version.new(::ActiveRecord::VERSION::STRING) >= ::Gem::Version.new(Constants::RAILS_6_0)
285
- Polyamorous::JoinDependency.new(relation.klass, relation.table, association_joins, Arel::Nodes::OuterJoin)
286
- else
287
- Polyamorous::JoinDependency.new(relation.klass, relation.table, association_joins)
288
- end
289
- join_dependency.instance_variable_set(:@alias_tracker, alias_tracker)
290
- join_nodes.each do |join|
291
- join_dependency.send(:alias_tracker).aliases[join.left.name.downcase] = 1
292
- end
280
+ alias_tracker = ::ActiveRecord::Associations::AliasTracker.create(self.klass.connection, relation.table.name, join_list)
281
+ join_dependency = Polyamorous::JoinDependency.new(relation.klass, relation.table, association_joins, Arel::Nodes::OuterJoin)
282
+ join_dependency.instance_variable_set(:@alias_tracker, alias_tracker)
283
+ join_nodes.each do |join|
284
+ join_dependency.send(:alias_tracker).aliases[join.left.name.downcase] = 1
293
285
  end
294
286
  join_dependency
295
287
  end
@@ -313,38 +305,13 @@ module Ransack
313
305
  end
314
306
 
315
307
  def build_association(name, parent = @base, klass = nil)
316
- if ::Gem::Version.new(::ActiveRecord::VERSION::STRING) >= ::Gem::Version.new(Constants::RAILS_6_0)
317
- jd = Polyamorous::JoinDependency.new(
318
- parent.base_klass,
319
- parent.table,
320
- Polyamorous::Join.new(name, @join_type, klass),
321
- @join_type
322
- )
323
- found_association = jd.instance_variable_get(:@join_root).children.last
324
- elsif ::Gem::Version.new(::ActiveRecord::VERSION::STRING) < ::Gem::Version.new(Constants::RAILS_5_2_0)
325
- jd = Polyamorous::JoinDependency.new(
326
- parent.base_klass,
327
- Polyamorous::Join.new(name, @join_type, klass),
328
- []
329
- )
330
- found_association = jd.join_root.children.last
331
- elsif ::ActiveRecord::VERSION::STRING == Constants::RAILS_5_2_0
332
- alias_tracker = ::ActiveRecord::Associations::AliasTracker.create(self.klass.connection, parent.table.name, [])
333
- jd = Polyamorous::JoinDependency.new(
334
- parent.base_klass,
335
- parent.table,
336
- Polyamorous::Join.new(name, @join_type, klass),
337
- alias_tracker
338
- )
339
- found_association = jd.instance_variable_get(:@join_root).children.last
340
- else
341
- jd = Polyamorous::JoinDependency.new(
342
- parent.base_klass,
343
- parent.table,
344
- Polyamorous::Join.new(name, @join_type, klass)
345
- )
346
- found_association = jd.instance_variable_get(:@join_root).children.last
347
- end
308
+ jd = Polyamorous::JoinDependency.new(
309
+ parent.base_klass,
310
+ parent.table,
311
+ Polyamorous::Join.new(name, @join_type, klass),
312
+ @join_type
313
+ )
314
+ found_association = jd.instance_variable_get(:@join_root).children.last
348
315
 
349
316
  @associations_pot[found_association] = parent
350
317
 
@@ -353,11 +320,7 @@ module Ransack
353
320
  @join_dependency.instance_variable_get(:@join_root).children.push found_association
354
321
 
355
322
  # Builds the arel nodes properly for this association
356
- if ::ActiveRecord::VERSION::STRING > Constants::RAILS_5_2_0
357
- @join_dependency.send(:construct_tables!, jd.instance_variable_get(:@join_root))
358
- else
359
- @join_dependency.send(:construct_tables!, jd.instance_variable_get(:@join_root), found_association)
360
- end
323
+ @tables_pot[found_association] = @join_dependency.construct_tables_for_association!(jd.instance_variable_get(:@join_root), found_association)
361
324
 
362
325
  # Leverage the stashed association functionality in AR
363
326
  @object = @object.joins(jd)
@@ -367,32 +330,13 @@ module Ransack
367
330
  def extract_joins(association)
368
331
  parent = @join_dependency.instance_variable_get(:@join_root)
369
332
  reflection = association.reflection
370
- join_constraints = if ::ActiveRecord::VERSION::STRING < Constants::RAILS_5_1
371
- association.join_constraints(
372
- parent.table,
373
- parent.base_klass,
374
- association,
375
- Arel::Nodes::OuterJoin,
376
- association.tables,
377
- reflection.scope_chain,
378
- reflection.chain
379
- )
380
- elsif ::ActiveRecord::VERSION::STRING <= Constants::RAILS_5_2_0
381
- association.join_constraints(
382
- parent.table,
383
- parent.base_klass,
384
- Arel::Nodes::OuterJoin,
385
- association.tables,
386
- reflection.chain
387
- )
388
- else
389
- association.join_constraints(
390
- parent.table,
391
- parent.base_klass,
392
- Arel::Nodes::OuterJoin,
393
- @join_dependency.instance_variable_get(:@alias_tracker)
394
- )
395
- end
333
+ join_constraints = association.join_constraints_with_tables(
334
+ parent.table,
335
+ parent.base_klass,
336
+ Arel::Nodes::OuterJoin,
337
+ @join_dependency.instance_variable_get(:@alias_tracker),
338
+ @tables_pot[association]
339
+ )
396
340
  join_constraints.to_a.flatten
397
341
  end
398
342
  end
@@ -27,13 +27,15 @@ module Ransack
27
27
  self.predicates = PredicateCollection.new
28
28
 
29
29
  self.options = {
30
- :search_key => :q,
31
- :ignore_unknown_conditions => true,
32
- :hide_sort_order_indicators => false,
33
- :up_arrow => '&#9660;'.freeze,
34
- :down_arrow => '&#9650;'.freeze,
35
- :default_arrow => nil,
36
- :sanitize_scope_args => true
30
+ search_key: :q,
31
+ ignore_unknown_conditions: true,
32
+ hide_sort_order_indicators: false,
33
+ up_arrow: '&#9660;'.freeze,
34
+ down_arrow: '&#9650;'.freeze,
35
+ default_arrow: nil,
36
+ sanitize_scope_args: true,
37
+ postgres_fields_sort_option: nil,
38
+ strip_whitespace: true
37
39
  }
38
40
 
39
41
  def configure
@@ -53,11 +55,11 @@ module Ransack
53
55
  compound_name = name + suffix
54
56
  self.predicates[compound_name] = Predicate.new(
55
57
  opts.merge(
56
- :name => compound_name,
57
- :arel_predicate => arel_predicate_with_suffix(
58
+ name: compound_name,
59
+ arel_predicate: arel_predicate_with_suffix(
58
60
  opts[:arel_predicate], suffix
59
61
  ),
60
- :compound => true
62
+ compound: true
61
63
  )
62
64
  )
63
65
  end if compounds
@@ -99,6 +101,19 @@ module Ransack
99
101
  self.options[:ignore_unknown_conditions] = boolean
100
102
  end
101
103
 
104
+ # By default Ransack ignores empty predicates. Ransack can also fallback to
105
+ # a default predicate by setting it in an initializer file
106
+ # like `config/initializers/ransack.rb` as follows:
107
+ #
108
+ # Ransack.configure do |config|
109
+ # # Use the 'eq' predicate if an unknown predicate is passed
110
+ # config.default_predicate = 'eq'
111
+ # end
112
+ #
113
+ def default_predicate=(name)
114
+ self.options[:default_predicate] = name
115
+ end
116
+
102
117
  # By default, Ransack displays sort order indicator arrows with HTML codes:
103
118
  #
104
119
  # up_arrow: '&#9660;'
@@ -141,6 +156,21 @@ module Ransack
141
156
  self.options[:sanitize_scope_args] = boolean
142
157
  end
143
158
 
159
+ # The `NULLS FIRST` and `NULLS LAST` options can be used to determine
160
+ # whether nulls appear before or after non-null values in the sort ordering.
161
+ #
162
+ # User may want to configure it like this:
163
+ #
164
+ # Ransack.configure do |c|
165
+ # c.postgres_fields_sort_option = :nulls_first # or e.g. :nulls_always_last
166
+ # end
167
+ #
168
+ # See this feature: https://www.postgresql.org/docs/13/queries-order.html
169
+ #
170
+ def postgres_fields_sort_option=(setting)
171
+ self.options[:postgres_fields_sort_option] = setting
172
+ end
173
+
144
174
  # By default, Ransack displays sort order indicator arrows in sort links.
145
175
  # The default may be globally overridden in an initializer file like
146
176
  # `config/initializers/ransack.rb` as follows:
@@ -154,6 +184,19 @@ module Ransack
154
184
  self.options[:hide_sort_order_indicators] = boolean
155
185
  end
156
186
 
187
+ # By default, Ransack displays strips all whitespace when searching for a string.
188
+ # The default may be globally changed in an initializer file like
189
+ # `config/initializers/ransack.rb` as follows:
190
+ #
191
+ # Ransack.configure do |config|
192
+ # # Enable whitespace stripping for string searches
193
+ # config.strip_whitespace = true
194
+ # end
195
+ #
196
+ def strip_whitespace=(boolean)
197
+ self.options[:strip_whitespace] = boolean
198
+ end
199
+
157
200
  def arel_predicate_with_suffix(arel_predicate, suffix)
158
201
  if arel_predicate === Proc
159
202
  proc { |v| "#{arel_predicate.call(v)}#{suffix}" }