ransack 5.0.1 → 6.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.
- checksums.yaml +4 -4
- data/README.md +13 -27
- data/lib/ransack/active_record/base.rb +154 -0
- data/lib/ransack/active_record/context.rb +504 -0
- data/lib/ransack/active_record/dialect.rb +83 -0
- data/lib/ransack/active_record/join.rb +75 -0
- data/lib/ransack/active_record/join_association.rb +104 -0
- data/lib/ransack/active_record/join_dependency.rb +123 -0
- data/lib/ransack/active_record/reflection.rb +18 -0
- data/lib/ransack/active_record/tree_node.rb +13 -0
- data/lib/ransack/active_record.rb +44 -9
- data/lib/ransack/configuration.rb +49 -1
- data/lib/ransack/constants.rb +61 -0
- data/lib/ransack/context.rb +170 -35
- data/lib/ransack/helpers/form_builder.rb +30 -25
- data/lib/ransack/nodes/bindable.rb +0 -1
- data/lib/ransack/nodes/condition.rb +215 -73
- data/lib/ransack/nodes/grouping.rb +35 -11
- data/lib/ransack/nodes/sort.rb +1 -1
- data/lib/ransack/nodes/value.rb +8 -1
- data/lib/ransack/predicate.rb +8 -1
- data/lib/ransack/search.rb +52 -9
- data/lib/ransack/version.rb +1 -1
- data/lib/ransack.rb +4 -1
- metadata +11 -13
- data/lib/polyamorous/activerecord/join_association.rb +0 -70
- data/lib/polyamorous/activerecord/join_association_7_2.rb +0 -55
- data/lib/polyamorous/activerecord/join_dependency.rb +0 -102
- data/lib/polyamorous/activerecord/reflection.rb +0 -11
- data/lib/polyamorous/join.rb +0 -70
- data/lib/polyamorous/polyamorous.rb +0 -27
- data/lib/polyamorous/swapping_reflection_class.rb +0 -11
- data/lib/polyamorous/tree_node.rb +0 -7
- data/lib/ransack/adapters/active_record/base.rb +0 -152
- data/lib/ransack/adapters/active_record/context.rb +0 -375
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
module Ransack
|
|
2
|
+
module ActiveRecord
|
|
3
|
+
# Prepended to ActiveRecord::Associations::JoinDependency::JoinAssociation.
|
|
4
|
+
#
|
|
5
|
+
# Adds the join type and the polymorphic class to a join association, and
|
|
6
|
+
# a variant of #join_constraints that builds against tables Ransack has
|
|
7
|
+
# already aliased, so a correlated subquery for a negative condition on a
|
|
8
|
+
# collection uses the same aliases as the outer query.
|
|
9
|
+
module JoinAssociationExtensions
|
|
10
|
+
def self.prepended(base)
|
|
11
|
+
base.class_eval { attr_reader :join_type }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(reflection, children, polymorphic_class = nil, join_type = Arel::Nodes::InnerJoin)
|
|
15
|
+
@join_type = join_type
|
|
16
|
+
if polymorphic_class && ::ActiveRecord::Base > polymorphic_class
|
|
17
|
+
swapping_reflection_klass(reflection, polymorphic_class) do |reflection|
|
|
18
|
+
super(reflection, children)
|
|
19
|
+
self.reflection.options[:polymorphic] = true
|
|
20
|
+
end
|
|
21
|
+
else
|
|
22
|
+
super(reflection, children)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# A polymorphic reflection has no class of its own. To join it to one
|
|
27
|
+
# concrete class, work on a copy of the reflection that names that class
|
|
28
|
+
# and drops the polymorphic option; the shared reflection on the model
|
|
29
|
+
# is left untouched, so this is safe across threads.
|
|
30
|
+
def swapping_reflection_klass(reflection, klass)
|
|
31
|
+
new_reflection = reflection.clone
|
|
32
|
+
new_reflection.instance_variable_set(:@options, reflection.options.clone)
|
|
33
|
+
new_reflection.options.delete(:polymorphic)
|
|
34
|
+
new_reflection.instance_variable_set(:@klass, klass)
|
|
35
|
+
yield new_reflection
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Same as #join_constraints, but instead of constructing tables from the
|
|
39
|
+
# given block, uses the ones passed
|
|
40
|
+
def join_constraints_with_tables(foreign_table, foreign_klass, join_type, alias_tracker, tables)
|
|
41
|
+
joins = []
|
|
42
|
+
chain = []
|
|
43
|
+
|
|
44
|
+
reflection.chain.each.with_index do |reflection, i|
|
|
45
|
+
table = tables[i]
|
|
46
|
+
|
|
47
|
+
@table ||= table
|
|
48
|
+
chain << [reflection, table]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The chain starts with the target table, but we want to end with it here (makes
|
|
52
|
+
# more sense in this context), so we reverse
|
|
53
|
+
chain.reverse_each do |reflection, table|
|
|
54
|
+
klass = reflection.klass
|
|
55
|
+
|
|
56
|
+
join_scope = reflection.join_scope(table, foreign_table, foreign_klass)
|
|
57
|
+
|
|
58
|
+
unless join_scope.references_values.empty?
|
|
59
|
+
join_dependency = join_scope.construct_join_dependency(
|
|
60
|
+
join_scope.eager_load_values | join_scope.includes_values, Arel::Nodes::OuterJoin
|
|
61
|
+
)
|
|
62
|
+
join_scope.joins!(join_dependency)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
arel = join_scope.arel(alias_tracker.aliases)
|
|
66
|
+
nodes = arel.constraints.first
|
|
67
|
+
|
|
68
|
+
if nodes.is_a?(Arel::Nodes::And)
|
|
69
|
+
others = nodes.children.extract! do |node|
|
|
70
|
+
!Arel.fetch_attribute(node) { |attr| attr.relation.name == table.name }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
joins << table.create_join(table, table.create_on(nodes), join_type)
|
|
75
|
+
|
|
76
|
+
if others && !others.empty?
|
|
77
|
+
joins.concat arel.join_sources
|
|
78
|
+
append_constraints_to(joins.last, others)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The current table in this iteration becomes the foreign table in the next
|
|
82
|
+
foreign_table, foreign_klass = table, klass
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
joins
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
# Active Record's private append_constraints takes a leading connection
|
|
91
|
+
# argument in 7.2.0–7.2.2.2 and 8.0.0–8.0.2, and none in 7.2.3+, 8.0.3+
|
|
92
|
+
# and 8.1; the arity says which is loaded.
|
|
93
|
+
def append_constraints_to(join, constraints)
|
|
94
|
+
if method(:append_constraints).arity == 3
|
|
95
|
+
base_klass.with_connection do |connection|
|
|
96
|
+
append_constraints(connection, join, constraints)
|
|
97
|
+
end
|
|
98
|
+
else
|
|
99
|
+
append_constraints(join, constraints)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
module Ransack
|
|
2
|
+
module ActiveRecord
|
|
3
|
+
# Prepended to ActiveRecord::Associations::JoinDependency.
|
|
4
|
+
#
|
|
5
|
+
# Teaches the join tree about Join nodes (join type, polymorphic class),
|
|
6
|
+
# and exposes the table aliasing Ransack needs to add one association at a
|
|
7
|
+
# time to a relation that may already carry joins of its own.
|
|
8
|
+
module JoinDependencyExtensions
|
|
9
|
+
# Replaces ActiveRecord::Associations::JoinDependency#build
|
|
10
|
+
def build(associations, base_klass)
|
|
11
|
+
associations.map do |name, right|
|
|
12
|
+
if name.is_a? Join
|
|
13
|
+
reflection = find_reflection base_klass, name.name
|
|
14
|
+
reflection.check_validity!
|
|
15
|
+
reflection.check_eager_loadable!
|
|
16
|
+
|
|
17
|
+
klass = if reflection.polymorphic?
|
|
18
|
+
name.klass || base_klass
|
|
19
|
+
else
|
|
20
|
+
reflection.klass
|
|
21
|
+
end
|
|
22
|
+
JoinAssociation.new(reflection, build(right, klass), name.klass, name.type)
|
|
23
|
+
else
|
|
24
|
+
reflection = find_reflection base_klass, name
|
|
25
|
+
reflection.check_validity!
|
|
26
|
+
reflection.check_eager_loadable!
|
|
27
|
+
|
|
28
|
+
if reflection.polymorphic?
|
|
29
|
+
raise ::ActiveRecord::EagerLoadPolymorphicError.new(reflection)
|
|
30
|
+
end
|
|
31
|
+
JoinAssociation.new(reflection, build(right, reflection.klass))
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def join_constraints(joins_to_add, alias_tracker, references)
|
|
37
|
+
@alias_tracker = alias_tracker
|
|
38
|
+
@joined_tables = {}
|
|
39
|
+
@references = {}
|
|
40
|
+
|
|
41
|
+
references.each do |table_name|
|
|
42
|
+
@references[table_name.to_sym] = table_name if table_name.is_a?(String)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
joins = make_join_constraints(join_root, join_type)
|
|
46
|
+
|
|
47
|
+
joins.concat joins_to_add.flat_map { |oj|
|
|
48
|
+
if join_root.match?(oj.join_root) && join_root.table.name == oj.join_root.table.name
|
|
49
|
+
walk join_root, oj.join_root, oj.join_type
|
|
50
|
+
else
|
|
51
|
+
make_join_constraints(oj.join_root, oj.join_type)
|
|
52
|
+
end
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def construct_tables_for_association!(join_root, association, join_type = self.join_type)
|
|
57
|
+
tables = table_aliases_for(join_root, association, join_type)
|
|
58
|
+
association.table = tables.first
|
|
59
|
+
tables
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# The tables for a node's reflection chain, assigned the way Active
|
|
65
|
+
# Record's JoinDependency#make_constraints assigns them, so that the
|
|
66
|
+
# aliases Ransack binds conditions to are the ones the query will have.
|
|
67
|
+
# Once a chain tail has been joined before, Active Record stops
|
|
68
|
+
# aliasing and reuses that join; the remaining positions here are
|
|
69
|
+
# filled from the same record for the correlated-subquery builder.
|
|
70
|
+
def table_aliases_for(parent, node, join_type)
|
|
71
|
+
@joined_tables ||= {}
|
|
72
|
+
chain = node.reflection.chain
|
|
73
|
+
terminated_at = nil
|
|
74
|
+
|
|
75
|
+
chain.each_with_index.map { |reflection, index|
|
|
76
|
+
remaining = chain[index..]
|
|
77
|
+
table, terminated = @joined_tables[remaining]
|
|
78
|
+
root = reflection == node.reflection
|
|
79
|
+
|
|
80
|
+
if terminated_at
|
|
81
|
+
table || reflection.klass.arel_table
|
|
82
|
+
elsif table && (!root || !terminated)
|
|
83
|
+
@joined_tables[remaining] = [table, root] if root
|
|
84
|
+
terminated_at = index
|
|
85
|
+
table
|
|
86
|
+
else
|
|
87
|
+
table_name = @references && @references[reflection.name.to_sym]&.to_s
|
|
88
|
+
table = alias_tracker.aliased_table_for(reflection.klass.arel_table, table_name) do
|
|
89
|
+
name = reflection.alias_candidate(parent.table_name)
|
|
90
|
+
root ? name : "#{name}_join"
|
|
91
|
+
end
|
|
92
|
+
@joined_tables[remaining] ||= [table, root] if join_type == Arel::Nodes::OuterJoin
|
|
93
|
+
table
|
|
94
|
+
end
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
module ClassMethods
|
|
99
|
+
# Prepended before ActiveRecord::Associations::JoinDependency#walk_tree
|
|
100
|
+
#
|
|
101
|
+
def walk_tree(associations, hash)
|
|
102
|
+
case associations
|
|
103
|
+
when TreeNode
|
|
104
|
+
associations.add_to_tree(hash)
|
|
105
|
+
when Hash
|
|
106
|
+
associations.each do |k, v|
|
|
107
|
+
cache =
|
|
108
|
+
if TreeNode === k
|
|
109
|
+
k.add_to_tree(hash)
|
|
110
|
+
else
|
|
111
|
+
hash[k] ||= {}
|
|
112
|
+
end
|
|
113
|
+
walk_tree(v, cache)
|
|
114
|
+
end
|
|
115
|
+
else
|
|
116
|
+
super(associations, hash)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Ransack
|
|
2
|
+
module ActiveRecord
|
|
3
|
+
# Prepended to ActiveRecord::Reflection::AbstractReflection.
|
|
4
|
+
#
|
|
5
|
+
# When a polymorphic belongs_to is joined to one concrete class, the join
|
|
6
|
+
# must also constrain the type column, or rows pointing at a different
|
|
7
|
+
# class with a colliding id would match.
|
|
8
|
+
module ReflectionExtensions
|
|
9
|
+
def join_scope(table, foreign_table, foreign_klass)
|
|
10
|
+
if respond_to?(:polymorphic?) && polymorphic?
|
|
11
|
+
super.where!(foreign_table[foreign_type].eq(klass.name))
|
|
12
|
+
else
|
|
13
|
+
super
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Ransack
|
|
2
|
+
module ActiveRecord
|
|
3
|
+
# Anything that can be placed in the association tree handed to
|
|
4
|
+
# JoinDependency. Active Record's own tree is built from symbols and
|
|
5
|
+
# hashes; a TreeNode adds itself to that tree however it likes, which is
|
|
6
|
+
# how a Join carries its join type and polymorphic class through.
|
|
7
|
+
module TreeNode
|
|
8
|
+
def add_to_tree(hash)
|
|
9
|
+
raise NotImplementedError
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -1,14 +1,49 @@
|
|
|
1
|
-
require 'ransack/
|
|
1
|
+
require 'ransack/active_record/base'
|
|
2
|
+
require 'ransack/active_record/dialect'
|
|
3
|
+
require 'ransack/active_record/context'
|
|
2
4
|
|
|
3
5
|
ActiveSupport.on_load(:active_record) do
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
module Ransack
|
|
7
|
+
# Everything Ransack needs from Active Record lives under this module: the
|
|
8
|
+
# class methods mixed into every model (Base), the Context that turns a
|
|
9
|
+
# search into a relation, and the extensions to Active Record's join
|
|
10
|
+
# machinery that let Ransack build outer joins and polymorphic joins the
|
|
11
|
+
# public query interface cannot express.
|
|
12
|
+
module ActiveRecord
|
|
13
|
+
JoinDependency = ::ActiveRecord::Associations::JoinDependency
|
|
14
|
+
JoinAssociation = ::ActiveRecord::Associations::JoinDependency::JoinAssociation
|
|
15
|
+
end
|
|
11
16
|
end
|
|
17
|
+
|
|
18
|
+
require 'ransack/active_record/tree_node'
|
|
19
|
+
require 'ransack/active_record/join'
|
|
20
|
+
require 'ransack/active_record/join_association'
|
|
21
|
+
require 'ransack/active_record/join_dependency'
|
|
22
|
+
require 'ransack/active_record/reflection'
|
|
23
|
+
|
|
24
|
+
::ActiveRecord::Reflection::AbstractReflection.prepend Ransack::ActiveRecord::ReflectionExtensions
|
|
25
|
+
Ransack::ActiveRecord::JoinDependency.prepend Ransack::ActiveRecord::JoinDependencyExtensions
|
|
26
|
+
Ransack::ActiveRecord::JoinDependency.singleton_class.prepend Ransack::ActiveRecord::JoinDependencyExtensions::ClassMethods
|
|
27
|
+
Ransack::ActiveRecord::JoinAssociation.prepend Ransack::ActiveRecord::JoinAssociationExtensions
|
|
28
|
+
|
|
29
|
+
extend Ransack::ActiveRecord::Base
|
|
12
30
|
end
|
|
13
31
|
|
|
14
|
-
|
|
32
|
+
module Ransack
|
|
33
|
+
# Ransack 5 and earlier kept the Active Record integration under
|
|
34
|
+
# Ransack::Adapters::ActiveRecord, a leftover from when Mongoid was also
|
|
35
|
+
# supported. Initializers that reopen it — usually to change the default
|
|
36
|
+
# `ransackable_attributes` — keep working for one more major release, with a
|
|
37
|
+
# deprecation warning pointing at the new name.
|
|
38
|
+
module Adapters
|
|
39
|
+
def self.const_missing(name)
|
|
40
|
+
return super unless name == :ActiveRecord
|
|
41
|
+
|
|
42
|
+
Ransack.deprecator.warn(
|
|
43
|
+
'Ransack::Adapters::ActiveRecord is deprecated and will be removed ' \
|
|
44
|
+
'in Ransack 7.0. Use Ransack::ActiveRecord instead.'
|
|
45
|
+
)
|
|
46
|
+
const_set(:ActiveRecord, Ransack::ActiveRecord)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -36,7 +36,9 @@ module Ransack
|
|
|
36
36
|
sanitize_scope_args: true,
|
|
37
37
|
fields_sort_option: nil,
|
|
38
38
|
strip_whitespace: true,
|
|
39
|
-
ignore_blank_values: true
|
|
39
|
+
ignore_blank_values: true,
|
|
40
|
+
dialect: nil,
|
|
41
|
+
null_sentinel: nil
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
def configure
|
|
@@ -230,6 +232,52 @@ module Ransack
|
|
|
230
232
|
self.options[:ignore_blank_values] = boolean
|
|
231
233
|
end
|
|
232
234
|
|
|
235
|
+
# Ransack works out which SQL dialect a model's database speaks from the
|
|
236
|
+
# connection adapter's class: PostgreSQL, MySQL (mysql2 and trilogy) and
|
|
237
|
+
# SQLite are recognised, as is any adapter that subclasses one of them,
|
|
238
|
+
# and anything else uses standard SQL only. For an adapter that speaks a
|
|
239
|
+
# known dialect without inheriting from Rails' adapter for it, name the
|
|
240
|
+
# dialect explicitly:
|
|
241
|
+
#
|
|
242
|
+
# Ransack.configure do |config|
|
|
243
|
+
# config.dialect = :postgresql # or :mysql, :sqlite, :generic
|
|
244
|
+
# end
|
|
245
|
+
#
|
|
246
|
+
def dialect=(name)
|
|
247
|
+
self.options[:dialect] = name&.to_sym
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Unset by default, so no application changes behavior on upgrade. Set
|
|
251
|
+
# this to a unique String value that cannot occur in your data, and an
|
|
252
|
+
# `eq` or `in` predicate (including their `_any` compounds) treats it as
|
|
253
|
+
# a request to also match `NULL`, instead of as a literal value to
|
|
254
|
+
# search for. A blank String, `false`, or a non-String value is not
|
|
255
|
+
# supported and produces undefined behavior:
|
|
256
|
+
#
|
|
257
|
+
# Ransack.configure do |config|
|
|
258
|
+
# config.null_sentinel = '__ransack_null__'
|
|
259
|
+
# end
|
|
260
|
+
#
|
|
261
|
+
# name_in: ['Ernie'] # name IN ('Ernie')
|
|
262
|
+
# name_in: ['Ernie', '__ransack_null__']
|
|
263
|
+
# # (name IN ('Ernie') OR name IS NULL)
|
|
264
|
+
# name_in: ['__ransack_null__'] # name IS NULL
|
|
265
|
+
#
|
|
266
|
+
# A single multi-value form field can express all three states this
|
|
267
|
+
# way, with no change to the field's name and no controller-side
|
|
268
|
+
# rewriting. `eq` takes one value, so through it the sentinel means
|
|
269
|
+
# `IS NULL` alone; "a value or NULL" from one field needs `in` or an
|
|
270
|
+
# `_any` compound.
|
|
271
|
+
#
|
|
272
|
+
# This does not bypass `ransackable_attributes`: `NULL` on an exposed
|
|
273
|
+
# attribute is already reachable through the built-in `null` predicate.
|
|
274
|
+
#
|
|
275
|
+
# See https://github.com/activerecord-hackery/ransack/issues/940
|
|
276
|
+
#
|
|
277
|
+
def null_sentinel=(value)
|
|
278
|
+
self.options[:null_sentinel] = value
|
|
279
|
+
end
|
|
280
|
+
|
|
233
281
|
def arel_predicate_with_suffix(arel_predicate, suffix)
|
|
234
282
|
if arel_predicate === Proc
|
|
235
283
|
proc { |v| "#{arel_predicate.call(v)}#{suffix}" }
|
data/lib/ransack/constants.rb
CHANGED
|
@@ -39,6 +39,21 @@ module Ransack
|
|
|
39
39
|
].freeze
|
|
40
40
|
A_S_I = ['a'.freeze, 's'.freeze, 'i'.freeze].freeze
|
|
41
41
|
|
|
42
|
+
# The predicates `Ransack.options[:null_sentinel]` applies to. Limited to
|
|
43
|
+
# the positive equality family (`eq`, `in`, and their `_any` compounds)
|
|
44
|
+
# because these are the only predicates where "or is null" is
|
|
45
|
+
# unambiguous. The `_all` compounds are excluded because they are
|
|
46
|
+
# conjunctive: `eq_all`/`in_all` require a single column to equal every
|
|
47
|
+
# supplied value at once, so ORing in `IS NULL` would widen a query that
|
|
48
|
+
# already matches nothing (or almost nothing) rather than narrow it. A
|
|
49
|
+
# negative predicate treats the sentinel as a literal value for the same
|
|
50
|
+
# reason of unambiguity: `not_eq` matching the sentinel would otherwise
|
|
51
|
+
# mean "is not unassigned", which reads as the opposite of what a caller
|
|
52
|
+
# submitting it alone almost certainly wants.
|
|
53
|
+
NULL_SENTINEL_PREDICATES = [
|
|
54
|
+
'eq'.freeze, 'in'.freeze, 'eq_any'.freeze, 'in_any'.freeze
|
|
55
|
+
].freeze
|
|
56
|
+
|
|
42
57
|
EQ = 'eq'.freeze
|
|
43
58
|
NOT_EQ = 'not_eq'.freeze
|
|
44
59
|
EQ_ANY = 'eq_any'.freeze
|
|
@@ -50,6 +65,15 @@ module Ransack
|
|
|
50
65
|
|
|
51
66
|
DISTINCT = 'DISTINCT '.freeze
|
|
52
67
|
|
|
68
|
+
# The most `_`-separated segments a single condition or sort key may have
|
|
69
|
+
# before Ransack rejects it as invalid instead of parsing it. Working out
|
|
70
|
+
# whether a key is an attribute, an `_and_`/`_or_` compound or a path
|
|
71
|
+
# through associations is superlinear in this count, and the key comes
|
|
72
|
+
# straight from the query string, so an unbounded one is a CPU-exhaustion
|
|
73
|
+
# denial of service (GHSA-j3f8-w227-4hh8). No real search key is this
|
|
74
|
+
# long: OR-ing every column of a very wide table stays well under it.
|
|
75
|
+
MAX_KEY_DEPTH = 200
|
|
76
|
+
|
|
53
77
|
DERIVED_PREDICATES = [
|
|
54
78
|
[CONT, {
|
|
55
79
|
arel_predicate: 'matches'.freeze,
|
|
@@ -83,6 +107,18 @@ module Ransack
|
|
|
83
107
|
formatter: proc { |v| "#{escape_wildcards(v)}%" }
|
|
84
108
|
}
|
|
85
109
|
],
|
|
110
|
+
['i_start'.freeze, {
|
|
111
|
+
arel_predicate: 'matches'.freeze,
|
|
112
|
+
formatter: proc { |v| "#{escape_wildcards(v.downcase)}%" },
|
|
113
|
+
case_insensitive: true
|
|
114
|
+
}
|
|
115
|
+
],
|
|
116
|
+
['not_i_start'.freeze, {
|
|
117
|
+
arel_predicate: 'does_not_match'.freeze,
|
|
118
|
+
formatter: proc { |v| "#{escape_wildcards(v.downcase)}%" },
|
|
119
|
+
case_insensitive: true
|
|
120
|
+
}
|
|
121
|
+
],
|
|
86
122
|
['end'.freeze, {
|
|
87
123
|
arel_predicate: 'matches'.freeze,
|
|
88
124
|
formatter: proc { |v| "%#{escape_wildcards(v)}" }
|
|
@@ -93,6 +129,18 @@ module Ransack
|
|
|
93
129
|
formatter: proc { |v| "%#{escape_wildcards(v)}" }
|
|
94
130
|
}
|
|
95
131
|
],
|
|
132
|
+
['i_end'.freeze, {
|
|
133
|
+
arel_predicate: 'matches'.freeze,
|
|
134
|
+
formatter: proc { |v| "%#{escape_wildcards(v.downcase)}" },
|
|
135
|
+
case_insensitive: true
|
|
136
|
+
}
|
|
137
|
+
],
|
|
138
|
+
['not_i_end'.freeze, {
|
|
139
|
+
arel_predicate: 'does_not_match'.freeze,
|
|
140
|
+
formatter: proc { |v| "%#{escape_wildcards(v.downcase)}" },
|
|
141
|
+
case_insensitive: true
|
|
142
|
+
}
|
|
143
|
+
],
|
|
96
144
|
['true'.freeze, {
|
|
97
145
|
arel_predicate: proc { |v| v ? EQ : NOT_EQ },
|
|
98
146
|
compounds: false,
|
|
@@ -197,5 +245,18 @@ module Ransack
|
|
|
197
245
|
def escape_wildcards(unescaped)
|
|
198
246
|
unescaped.to_s.gsub(/([\\%_])/) { "#{LIKE_ESCAPE_CHARACTER}#{$1}" }
|
|
199
247
|
end
|
|
248
|
+
|
|
249
|
+
def null_sentinel_predicate?(predicate_name, sentinel)
|
|
250
|
+
sentinel && NULL_SENTINEL_PREDICATES.include?(predicate_name)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def null_sentinel_value?(value, sentinel)
|
|
254
|
+
sentinel && value == sentinel
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def null_sentinel_requested?(predicate_name, values, sentinel)
|
|
258
|
+
null_sentinel_predicate?(predicate_name, sentinel) &&
|
|
259
|
+
values.any? { |v| null_sentinel_value?(v.value, sentinel) }
|
|
260
|
+
end
|
|
200
261
|
end
|
|
201
262
|
end
|