ransack 2.3.2 → 4.1.1

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 (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}" }