ransack 2.3.2 → 4.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (145) 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 +99 -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 +131 -0
  10. data/.nojekyll +0 -0
  11. data/.rubocop.yml +50 -0
  12. data/CHANGELOG.md +251 -1
  13. data/CONTRIBUTING.md +51 -29
  14. data/Gemfile +12 -10
  15. data/README.md +45 -907
  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 +71 -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 +46 -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 +8879 -0
  66. data/lib/polyamorous/activerecord/join_association.rb +70 -0
  67. data/{polyamorous/lib/polyamorous/activerecord_6.0_ruby_2 → lib/polyamorous/activerecord}/join_dependency.rb +33 -12
  68. data/lib/polyamorous/activerecord/reflection.rb +11 -0
  69. data/{polyamorous/lib → lib/polyamorous}/polyamorous.rb +3 -4
  70. data/lib/ransack/adapters/active_record/base.rb +83 -10
  71. data/lib/ransack/adapters/active_record/context.rb +56 -44
  72. data/lib/ransack/configuration.rb +53 -10
  73. data/lib/ransack/constants.rb +126 -4
  74. data/lib/ransack/context.rb +34 -5
  75. data/lib/ransack/helpers/form_builder.rb +6 -6
  76. data/lib/ransack/helpers/form_helper.rb +14 -5
  77. data/lib/ransack/helpers.rb +1 -1
  78. data/lib/ransack/locale/sv.yml +70 -0
  79. data/lib/ransack/nodes/attribute.rb +3 -3
  80. data/lib/ransack/nodes/condition.rb +80 -9
  81. data/lib/ransack/nodes/grouping.rb +4 -4
  82. data/lib/ransack/nodes/node.rb +1 -1
  83. data/lib/ransack/nodes/sort.rb +3 -3
  84. data/lib/ransack/nodes/value.rb +3 -3
  85. data/lib/ransack/predicate.rb +1 -1
  86. data/lib/ransack/ransacker.rb +1 -1
  87. data/lib/ransack/search.rb +15 -7
  88. data/lib/ransack/translate.rb +6 -6
  89. data/lib/ransack/version.rb +1 -1
  90. data/lib/ransack/visitor.rb +38 -2
  91. data/lib/ransack.rb +5 -8
  92. data/ransack.gemspec +9 -15
  93. data/spec/blueprints/articles.rb +1 -1
  94. data/spec/blueprints/comments.rb +1 -1
  95. data/spec/blueprints/notes.rb +1 -1
  96. data/spec/blueprints/tags.rb +1 -1
  97. data/spec/console.rb +5 -5
  98. data/spec/helpers/polyamorous_helper.rb +2 -8
  99. data/spec/helpers/ransack_helper.rb +1 -1
  100. data/spec/polyamorous/activerecord_compatibility_spec.rb +15 -0
  101. data/spec/{ransack → polyamorous}/join_association_spec.rb +3 -1
  102. data/spec/{ransack → polyamorous}/join_dependency_spec.rb +0 -16
  103. data/spec/ransack/adapters/active_record/base_spec.rb +125 -16
  104. data/spec/ransack/adapters/active_record/context_spec.rb +19 -18
  105. data/spec/ransack/configuration_spec.rb +33 -9
  106. data/spec/ransack/helpers/form_builder_spec.rb +8 -8
  107. data/spec/ransack/helpers/form_helper_spec.rb +109 -20
  108. data/spec/ransack/nodes/condition_spec.rb +37 -0
  109. data/spec/ransack/nodes/grouping_spec.rb +2 -2
  110. data/spec/ransack/nodes/value_spec.rb +115 -0
  111. data/spec/ransack/predicate_spec.rb +37 -2
  112. data/spec/ransack/search_spec.rb +238 -30
  113. data/spec/ransack/translate_spec.rb +1 -1
  114. data/spec/spec_helper.rb +7 -5
  115. data/spec/support/schema.rb +108 -11
  116. metadata +98 -62
  117. data/.travis.yml +0 -47
  118. data/lib/ransack/adapters/active_record/ransack/constants.rb +0 -128
  119. data/lib/ransack/adapters/active_record/ransack/context.rb +0 -55
  120. data/lib/ransack/adapters/active_record/ransack/nodes/condition.rb +0 -61
  121. data/lib/ransack/adapters/active_record/ransack/translate.rb +0 -8
  122. data/lib/ransack/adapters/active_record/ransack/visitor.rb +0 -47
  123. data/lib/ransack/adapters.rb +0 -64
  124. data/lib/ransack/nodes.rb +0 -8
  125. data/polyamorous/lib/polyamorous/activerecord_5.2_ruby_2/join_association.rb +0 -20
  126. data/polyamorous/lib/polyamorous/activerecord_5.2_ruby_2/join_dependency.rb +0 -79
  127. data/polyamorous/lib/polyamorous/activerecord_5.2_ruby_2/reflection.rb +0 -12
  128. data/polyamorous/lib/polyamorous/activerecord_6.0_ruby_2/join_association.rb +0 -2
  129. data/polyamorous/lib/polyamorous/activerecord_6.0_ruby_2/reflection.rb +0 -2
  130. data/polyamorous/lib/polyamorous/activerecord_6.1_ruby_2/join_association.rb +0 -2
  131. data/polyamorous/lib/polyamorous/activerecord_6.1_ruby_2/join_dependency.rb +0 -2
  132. data/polyamorous/lib/polyamorous/activerecord_6.1_ruby_2/reflection.rb +0 -2
  133. data/polyamorous/lib/polyamorous/version.rb +0 -3
  134. data/polyamorous/polyamorous.gemspec +0 -27
  135. /data/{logo → docs/static/logo}/ransack-h.png +0 -0
  136. /data/{logo → docs/static/logo}/ransack-h.svg +0 -0
  137. /data/{logo → docs/static/logo}/ransack-v.png +0 -0
  138. /data/{logo → docs/static/logo}/ransack-v.svg +0 -0
  139. /data/{logo → docs/static/logo}/ransack.png +0 -0
  140. /data/{logo → docs/static/logo}/ransack.svg +0 -0
  141. /data/{polyamorous/lib → lib}/polyamorous/join.rb +0 -0
  142. /data/{polyamorous/lib → lib}/polyamorous/swapping_reflection_class.rb +0 -0
  143. /data/{polyamorous/lib → lib}/polyamorous/tree_node.rb +0 -0
  144. /data/lib/ransack/{adapters/active_record.rb → active_record.rb} +0 -0
  145. /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,33 @@ 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
+ @joined_tables ||= {}
60
+ node.reflection.chain.map { |reflection|
61
+ table, terminated = @joined_tables[reflection]
62
+ root = reflection == node.reflection
63
+
64
+ if table && (!root || !terminated)
65
+ @joined_tables[reflection] = [table, true] if root
66
+ table
67
+ else
68
+ table = alias_tracker.aliased_table_for(reflection.klass.arel_table) do
69
+ name = reflection.alias_candidate(parent.table_name)
70
+ root ? name : "#{name}_join"
71
+ end
72
+ @joined_tables[reflection] ||= [table, root] if join_type == Arel::Nodes::OuterJoin
73
+ table
74
+ end
75
+ }
76
+ end
56
77
 
57
78
  module ClassMethods
58
79
  # 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
@@ -11,10 +11,9 @@ 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
- %w(join_association join_dependency reflection).each do |file|
16
- require "polyamorous/activerecord_#{ar_version}_ruby_2/#{file}"
17
- end
14
+ require 'polyamorous/activerecord/join_association'
15
+ require 'polyamorous/activerecord/join_dependency'
16
+ require 'polyamorous/activerecord/reflection'
18
17
 
19
18
  ActiveRecord::Reflection::AbstractReflection.send(:prepend, Polyamorous::ReflectionExtensions)
20
19
 
@@ -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,5 +1,5 @@
1
1
  require 'ransack/context'
2
- require 'polyamorous'
2
+ require 'polyamorous/polyamorous'
3
3
 
4
4
  module Ransack
5
5
  module Adapters
@@ -12,8 +12,9 @@ module Ransack
12
12
 
13
13
  def type_for(attr)
14
14
  return nil unless attr && attr.valid?
15
+ relation = attr.arel_attribute.relation
15
16
  name = attr.arel_attribute.name.to_s
16
- table = attr.arel_attribute.relation.table_name
17
+ table = relation.respond_to?(:table_name) ? relation.table_name : relation.name
17
18
  schema_cache = self.klass.connection.schema_cache
18
19
  unless schema_cache.send(:data_source_exists?, table)
19
20
  raise "No table named #{table} exists."
@@ -42,6 +43,17 @@ module Ransack
42
43
  if scope_or_sort.is_a?(Symbol)
43
44
  relation = relation.send(scope_or_sort)
44
45
  else
46
+ case Ransack.options[:postgres_fields_sort_option]
47
+ when :nulls_first
48
+ 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")
49
+ when :nulls_last
50
+ 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")
51
+ when :nulls_always_first
52
+ scope_or_sort = Arel.sql("#{scope_or_sort.to_sql} NULLS FIRST")
53
+ when :nulls_always_last
54
+ scope_or_sort = Arel.sql("#{scope_or_sort.to_sql} NULLS LAST")
55
+ end
56
+
45
57
  relation = relation.order(scope_or_sort)
46
58
  end
47
59
  end
@@ -99,11 +111,7 @@ module Ransack
99
111
  def join_sources
100
112
  base, joins = begin
101
113
  alias_tracker = ::ActiveRecord::Associations::AliasTracker.create(self.klass.connection, @object.table.name, [])
102
- constraints = if ::Gem::Version.new(::ActiveRecord::VERSION::STRING) >= ::Gem::Version.new(Constants::RAILS_6_0)
103
- @join_dependency.join_constraints(@object.joins_values, alias_tracker)
104
- else
105
- @join_dependency.join_constraints(@object.joins_values, @join_type, alias_tracker)
106
- end
114
+ constraints = @join_dependency.join_constraints(@object.joins_values, alias_tracker, @object.references_values)
107
115
 
108
116
  [
109
117
  Arel::SelectManager.new(@object.table),
@@ -130,6 +138,7 @@ module Ransack
130
138
  stashed.eql?(association)
131
139
  }
132
140
  @object.joins_values.delete_if { |jd|
141
+ jd.instance_variables.include?(:@join_root) &&
133
142
  jd.instance_variable_get(:@join_root).children.map(&:object_id) == [association.object_id]
134
143
  }
135
144
  end
@@ -172,16 +181,31 @@ module Ransack
172
181
  private
173
182
 
174
183
  def extract_correlated_key(join_root)
175
- correlated_key = join_root.right.expr.left
176
-
177
- if correlated_key.is_a? Arel::Nodes::And
178
- correlated_key = correlated_key.left.left
179
- elsif correlated_key.is_a? Arel::Nodes::Equality
180
- correlated_key = correlated_key.left
181
- elsif correlated_key.is_a? Arel::Nodes::Grouping
182
- correlated_key = join_root.right.expr.right.left
184
+ case join_root
185
+ when Arel::Nodes::OuterJoin
186
+ # one of join_root.right/join_root.left is expected to be Arel::Nodes::On
187
+ if join_root.right.is_a?(Arel::Nodes::On)
188
+ extract_correlated_key(join_root.right.expr)
189
+ elsif join_root.left.is_a?(Arel::Nodes::On)
190
+ extract_correlated_key(join_root.left.expr)
191
+ else
192
+ raise 'Ransack encountered an unexpected arel structure'
193
+ end
194
+ when Arel::Nodes::Equality
195
+ pk = primary_key
196
+ if join_root.left == pk
197
+ join_root.right
198
+ elsif join_root.right == pk
199
+ join_root.left
200
+ else
201
+ nil
202
+ end
203
+ when Arel::Nodes::And
204
+ extract_correlated_key(join_root.left) || extract_correlated_key(join_root.right)
183
205
  else
184
- correlated_key
206
+ # eg parent was Arel::Nodes::And and the evaluated side was one of
207
+ # Arel::Nodes::Grouping or MultiTenant::TenantEnforcementClause
208
+ nil
185
209
  end
186
210
  end
187
211
 
@@ -255,11 +279,7 @@ module Ransack
255
279
  join_list = join_nodes + convert_join_strings_to_ast(relation.table, string_joins)
256
280
 
257
281
  alias_tracker = ::ActiveRecord::Associations::AliasTracker.create(self.klass.connection, relation.table.name, join_list)
258
- join_dependency = if ::Gem::Version.new(::ActiveRecord::VERSION::STRING) >= ::Gem::Version.new(Constants::RAILS_6_0)
259
- Polyamorous::JoinDependency.new(relation.klass, relation.table, association_joins, Arel::Nodes::OuterJoin)
260
- else
261
- Polyamorous::JoinDependency.new(relation.klass, relation.table, association_joins)
262
- end
282
+ join_dependency = Polyamorous::JoinDependency.new(relation.klass, relation.table, association_joins, Arel::Nodes::OuterJoin)
263
283
  join_dependency.instance_variable_set(:@alias_tracker, alias_tracker)
264
284
  join_nodes.each do |join|
265
285
  join_dependency.send(:alias_tracker).aliases[join.left.name.downcase] = 1
@@ -286,22 +306,13 @@ module Ransack
286
306
  end
287
307
 
288
308
  def build_association(name, parent = @base, klass = nil)
289
- if ::Gem::Version.new(::ActiveRecord::VERSION::STRING) >= ::Gem::Version.new(Constants::RAILS_6_0)
290
- jd = Polyamorous::JoinDependency.new(
291
- parent.base_klass,
292
- parent.table,
293
- Polyamorous::Join.new(name, @join_type, klass),
294
- @join_type
295
- )
296
- found_association = jd.instance_variable_get(:@join_root).children.last
297
- else
298
- jd = Polyamorous::JoinDependency.new(
299
- parent.base_klass,
300
- parent.table,
301
- Polyamorous::Join.new(name, @join_type, klass)
302
- )
303
- found_association = jd.instance_variable_get(:@join_root).children.last
304
- end
309
+ jd = Polyamorous::JoinDependency.new(
310
+ parent.base_klass,
311
+ parent.table,
312
+ Polyamorous::Join.new(name, @join_type, klass),
313
+ @join_type
314
+ )
315
+ found_association = jd.instance_variable_get(:@join_root).children.last
305
316
 
306
317
  @associations_pot[found_association] = parent
307
318
 
@@ -310,7 +321,7 @@ module Ransack
310
321
  @join_dependency.instance_variable_get(:@join_root).children.push found_association
311
322
 
312
323
  # Builds the arel nodes properly for this association
313
- @join_dependency.send(:construct_tables!, jd.instance_variable_get(:@join_root))
324
+ @tables_pot[found_association] = @join_dependency.construct_tables_for_association!(jd.instance_variable_get(:@join_root), found_association)
314
325
 
315
326
  # Leverage the stashed association functionality in AR
316
327
  @object = @object.joins(jd)
@@ -320,12 +331,13 @@ module Ransack
320
331
  def extract_joins(association)
321
332
  parent = @join_dependency.instance_variable_get(:@join_root)
322
333
  reflection = association.reflection
323
- join_constraints = association.join_constraints(
324
- parent.table,
325
- parent.base_klass,
326
- Arel::Nodes::OuterJoin,
327
- @join_dependency.instance_variable_get(:@alias_tracker)
328
- )
334
+ join_constraints = association.join_constraints_with_tables(
335
+ parent.table,
336
+ parent.base_klass,
337
+ Arel::Nodes::OuterJoin,
338
+ @join_dependency.instance_variable_get(:@alias_tracker),
339
+ @tables_pot[association]
340
+ )
329
341
  join_constraints.to_a.flatten
330
342
  end
331
343
  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}" }