giga-pure-kit 0.0.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.
- checksums.yaml +7 -0
- data/giga-pure-kit.gemspec +12 -0
- data/ransack-4.4.1/LICENSE +20 -0
- data/ransack-4.4.1/README.md +101 -0
- data/ransack-4.4.1/lib/polyamorous/activerecord/join_association.rb +218 -0
- data/ransack-4.4.1/lib/polyamorous/activerecord/join_association_7_2.rb +55 -0
- data/ransack-4.4.1/lib/polyamorous/activerecord/join_dependency.rb +102 -0
- data/ransack-4.4.1/lib/polyamorous/activerecord/reflection.rb +11 -0
- data/ransack-4.4.1/lib/polyamorous/join.rb +70 -0
- data/ransack-4.4.1/lib/polyamorous/polyamorous.rb +27 -0
- data/ransack-4.4.1/lib/polyamorous/swapping_reflection_class.rb +11 -0
- data/ransack-4.4.1/lib/polyamorous/tree_node.rb +7 -0
- data/ransack-4.4.1/lib/ransack/active_record.rb +14 -0
- data/ransack-4.4.1/lib/ransack/adapters/active_record/base.rb +152 -0
- data/ransack-4.4.1/lib/ransack/adapters/active_record/context.rb +373 -0
- data/ransack-4.4.1/lib/ransack/configuration.rb +209 -0
- data/ransack-4.4.1/lib/ransack/constants.rb +176 -0
- data/ransack-4.4.1/lib/ransack/context.rb +190 -0
- data/ransack-4.4.1/lib/ransack/helpers/form_builder.rb +277 -0
- data/ransack-4.4.1/lib/ransack/helpers/form_helper.rb +311 -0
- data/ransack-4.4.1/lib/ransack/helpers.rb +2 -0
- data/ransack-4.4.1/lib/ransack/invalid_search_error.rb +3 -0
- data/ransack-4.4.1/lib/ransack/locale/ar.yml +70 -0
- data/ransack-4.4.1/lib/ransack/locale/az.yml +70 -0
- data/ransack-4.4.1/lib/ransack/locale/bg.yml +70 -0
- data/ransack-4.4.1/lib/ransack/locale/ca.yml +70 -0
- data/ransack-4.4.1/lib/ransack/locale/cs.yml +70 -0
- data/ransack-4.4.1/lib/ransack/locale/da.yml +70 -0
- data/ransack-4.4.1/lib/ransack/locale/de.yml +70 -0
- data/ransack-4.4.1/lib/ransack/locale/el.yml +70 -0
- data/ransack-4.4.1/lib/ransack/locale/en.yml +70 -0
- data/ransack-4.4.1/lib/ransack/locale/es.yml +70 -0
- data/ransack-4.4.1/lib/ransack/locale/fa.yml +70 -0
- data/ransack-4.4.1/lib/ransack/locale/fi.yml +71 -0
- data/ransack-4.4.1/lib/ransack/locale/fr.yml +70 -0
- data/ransack-4.4.1/lib/ransack/locale/hu.yml +70 -0
- data/ransack-4.4.1/lib/ransack/locale/id.yml +70 -0
- data/ransack-4.4.1/lib/ransack/locale/it.yml +70 -0
- data/ransack-4.4.1/lib/ransack/locale/ja.yml +70 -0
- data/ransack-4.4.1/lib/ransack/locale/ko.yml +70 -0
- data/ransack-4.4.1/lib/ransack/locale/nl.yml +70 -0
- data/ransack-4.4.1/lib/ransack/locale/pt-BR.yml +70 -0
- data/ransack-4.4.1/lib/ransack/locale/ro.yml +70 -0
- data/ransack-4.4.1/lib/ransack/locale/ru.yml +70 -0
- data/ransack-4.4.1/lib/ransack/locale/sk.yml +70 -0
- data/ransack-4.4.1/lib/ransack/locale/sv.yml +70 -0
- data/ransack-4.4.1/lib/ransack/locale/tr.yml +70 -0
- data/ransack-4.4.1/lib/ransack/locale/uk.yml +72 -0
- data/ransack-4.4.1/lib/ransack/locale/zh-CN.yml +70 -0
- data/ransack-4.4.1/lib/ransack/locale/zh-TW.yml +70 -0
- data/ransack-4.4.1/lib/ransack/naming.rb +58 -0
- data/ransack-4.4.1/lib/ransack/nodes/attribute.rb +59 -0
- data/ransack-4.4.1/lib/ransack/nodes/bindable.rb +52 -0
- data/ransack-4.4.1/lib/ransack/nodes/condition.rb +399 -0
- data/ransack-4.4.1/lib/ransack/nodes/grouping.rb +206 -0
- data/ransack-4.4.1/lib/ransack/nodes/node.rb +34 -0
- data/ransack-4.4.1/lib/ransack/nodes/sort.rb +54 -0
- data/ransack-4.4.1/lib/ransack/nodes/value.rb +115 -0
- data/ransack-4.4.1/lib/ransack/predicate.rb +75 -0
- data/ransack-4.4.1/lib/ransack/ransacker.rb +24 -0
- data/ransack-4.4.1/lib/ransack/search.rb +195 -0
- data/ransack-4.4.1/lib/ransack/translate.rb +156 -0
- data/ransack-4.4.1/lib/ransack/version.rb +3 -0
- data/ransack-4.4.1/lib/ransack/visitor.rb +87 -0
- data/ransack-4.4.1/lib/ransack.rb +33 -0
- data/ransack-4.4.1/spec/console.rb +13 -0
- data/ransack-4.4.1/spec/factories/articles.rb +7 -0
- data/ransack-4.4.1/spec/factories/comments.rb +7 -0
- data/ransack-4.4.1/spec/factories/notes.rb +13 -0
- data/ransack-4.4.1/spec/factories/people.rb +10 -0
- data/ransack-4.4.1/spec/factories/tags.rb +5 -0
- data/ransack-4.4.1/spec/helpers/polyamorous_helper.rb +13 -0
- data/ransack-4.4.1/spec/helpers/ransack_helper.rb +9 -0
- data/ransack-4.4.1/spec/polyamorous/activerecord_compatibility_spec.rb +15 -0
- data/ransack-4.4.1/spec/polyamorous/join_association_spec.rb +29 -0
- data/ransack-4.4.1/spec/polyamorous/join_dependency_spec.rb +80 -0
- data/ransack-4.4.1/spec/polyamorous/join_spec.rb +19 -0
- data/ransack-4.4.1/spec/ransack/adapters/active_record/base_spec.rb +943 -0
- data/ransack-4.4.1/spec/ransack/adapters/active_record/context_spec.rb +219 -0
- data/ransack-4.4.1/spec/ransack/configuration_spec.rb +201 -0
- data/ransack-4.4.1/spec/ransack/helpers/form_builder_spec.rb +169 -0
- data/ransack-4.4.1/spec/ransack/helpers/form_helper_spec.rb +1075 -0
- data/ransack-4.4.1/spec/ransack/invalid_search_error_spec.rb +27 -0
- data/ransack-4.4.1/spec/ransack/nodes/condition_spec.rb +333 -0
- data/ransack-4.4.1/spec/ransack/nodes/grouping_spec.rb +111 -0
- data/ransack-4.4.1/spec/ransack/nodes/value_spec.rb +126 -0
- data/ransack-4.4.1/spec/ransack/predicate_spec.rb +504 -0
- data/ransack-4.4.1/spec/ransack/ransacker_spec.rb +69 -0
- data/ransack-4.4.1/spec/ransack/search_spec.rb +821 -0
- data/ransack-4.4.1/spec/ransack/translate_spec.rb +16 -0
- data/ransack-4.4.1/spec/spec_helper.rb +52 -0
- data/ransack-4.4.1/spec/support/en.yml +18 -0
- data/ransack-4.4.1/spec/support/schema.rb +419 -0
- metadata +133 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module Polyamorous
|
|
2
|
+
module SwappingReflectionClass
|
|
3
|
+
def swapping_reflection_klass(reflection, klass)
|
|
4
|
+
new_reflection = reflection.clone
|
|
5
|
+
new_reflection.instance_variable_set(:@options, reflection.options.clone)
|
|
6
|
+
new_reflection.options.delete(:polymorphic)
|
|
7
|
+
new_reflection.instance_variable_set(:@klass, klass)
|
|
8
|
+
yield new_reflection
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require 'ransack/adapters/active_record/base'
|
|
2
|
+
|
|
3
|
+
ActiveSupport.on_load(:active_record) do
|
|
4
|
+
extend Ransack::Adapters::ActiveRecord::Base
|
|
5
|
+
|
|
6
|
+
Ransack::SUPPORTS_ATTRIBUTE_ALIAS =
|
|
7
|
+
begin
|
|
8
|
+
ActiveRecord::Base.respond_to?(:attribute_aliases)
|
|
9
|
+
rescue NameError
|
|
10
|
+
false
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
require 'ransack/adapters/active_record/context'
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
module Ransack
|
|
2
|
+
module Adapters
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
module Base
|
|
5
|
+
|
|
6
|
+
def self.extended(base)
|
|
7
|
+
base.class_eval do
|
|
8
|
+
class_attribute :_ransackers
|
|
9
|
+
class_attribute :_ransack_aliases
|
|
10
|
+
self._ransackers ||= {}
|
|
11
|
+
self._ransack_aliases ||= {}
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def ransack(params = {}, options = {})
|
|
16
|
+
Search.new(self, params, options)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def ransack!(params = {}, options = {})
|
|
20
|
+
ransack(params, options.merge(ignore_unknown_conditions: false))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def ransacker(name, opts = {}, &block)
|
|
24
|
+
self._ransackers = _ransackers.merge name.to_s => Ransacker
|
|
25
|
+
.new(self, name, opts, &block)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def ransack_alias(new_name, old_name)
|
|
29
|
+
self._ransack_aliases = _ransack_aliases.merge new_name.to_s =>
|
|
30
|
+
old_name.to_s
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Ransackable_attributes, by default, returns all column names
|
|
34
|
+
# and any defined ransackers as an array of strings.
|
|
35
|
+
# For overriding with a whitelist array of strings.
|
|
36
|
+
#
|
|
37
|
+
def ransackable_attributes(auth_object = nil)
|
|
38
|
+
@ransackable_attributes ||= deprecated_ransackable_list(:ransackable_attributes)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Ransackable_associations, by default, returns the names
|
|
42
|
+
# of all associations as an array of strings.
|
|
43
|
+
# For overriding with a whitelist array of strings.
|
|
44
|
+
#
|
|
45
|
+
def ransackable_associations(auth_object = nil)
|
|
46
|
+
@ransackable_associations ||= deprecated_ransackable_list(:ransackable_associations)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Ransortable_attributes, by default, returns the names
|
|
50
|
+
# of all attributes available for sorting as an array of strings.
|
|
51
|
+
# For overriding with a whitelist array of strings.
|
|
52
|
+
#
|
|
53
|
+
def ransortable_attributes(auth_object = nil)
|
|
54
|
+
ransackable_attributes(auth_object)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Ransackable_scopes, by default, returns an empty array
|
|
58
|
+
# i.e. no class methods/scopes are authorized.
|
|
59
|
+
# For overriding with a whitelist array of *symbols*.
|
|
60
|
+
#
|
|
61
|
+
def ransackable_scopes(auth_object = nil)
|
|
62
|
+
[]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# ransack_scope_skip_sanitize_args, by default, returns an empty array.
|
|
66
|
+
# i.e. use the sanitize_scope_args setting to determine if args should be converted.
|
|
67
|
+
# For overriding with a list of scopes which should be passed the args as-is.
|
|
68
|
+
#
|
|
69
|
+
def ransackable_scopes_skip_sanitize_args
|
|
70
|
+
[]
|
|
71
|
+
end
|
|
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
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
require 'ransack/context'
|
|
2
|
+
require 'polyamorous/polyamorous'
|
|
3
|
+
|
|
4
|
+
module Ransack
|
|
5
|
+
module Adapters
|
|
6
|
+
module ActiveRecord
|
|
7
|
+
class Context < ::Ransack::Context
|
|
8
|
+
|
|
9
|
+
def relation_for(object)
|
|
10
|
+
object.all
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def type_for(attr)
|
|
14
|
+
return nil unless attr && attr.valid?
|
|
15
|
+
relation = attr.arel_attribute.relation
|
|
16
|
+
name = attr.arel_attribute.name.to_s
|
|
17
|
+
table = relation.respond_to?(:table_name) ? relation.table_name : relation.name
|
|
18
|
+
schema_cache = self.klass.connection.schema_cache
|
|
19
|
+
unless schema_cache.send(:data_source_exists?, table)
|
|
20
|
+
raise "No table named #{table} exists."
|
|
21
|
+
end
|
|
22
|
+
column = attr.klass.columns.find { |column| column.name == name }
|
|
23
|
+
column&.type
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def evaluate(search, opts = {})
|
|
27
|
+
viz = Visitor.new
|
|
28
|
+
relation = @object.where(viz.accept(search.base))
|
|
29
|
+
|
|
30
|
+
if search.sorts.any?
|
|
31
|
+
relation = relation.except(:order)
|
|
32
|
+
# Rather than applying all of the search's sorts in one fell swoop,
|
|
33
|
+
# as the original implementation does, we apply one at a time.
|
|
34
|
+
#
|
|
35
|
+
# If the sort (returned by the Visitor above) is a symbol, we know
|
|
36
|
+
# that it represents a scope on the model and we can apply that
|
|
37
|
+
# scope.
|
|
38
|
+
#
|
|
39
|
+
# Otherwise, we fall back to the applying the sort with the "order"
|
|
40
|
+
# method as the original implementation did. Actually the original
|
|
41
|
+
# implementation used "reorder," which was overkill since we already
|
|
42
|
+
# have a clean slate after "relation.except(:order)" above.
|
|
43
|
+
viz.accept(search.sorts).each do |scope_or_sort|
|
|
44
|
+
if scope_or_sort.is_a?(Symbol)
|
|
45
|
+
relation = relation.send(scope_or_sort)
|
|
46
|
+
else
|
|
47
|
+
case Ransack.options[:postgres_fields_sort_option]
|
|
48
|
+
when :nulls_first
|
|
49
|
+
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")
|
|
50
|
+
when :nulls_last
|
|
51
|
+
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")
|
|
52
|
+
when :nulls_always_first
|
|
53
|
+
scope_or_sort = Arel.sql("#{scope_or_sort.to_sql} NULLS FIRST")
|
|
54
|
+
when :nulls_always_last
|
|
55
|
+
scope_or_sort = Arel.sql("#{scope_or_sort.to_sql} NULLS LAST")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
relation = relation.order(scope_or_sort)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
opts[:distinct] ? relation.distinct : relation
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def attribute_method?(str, klass = @klass)
|
|
67
|
+
exists = false
|
|
68
|
+
if ransackable_attribute?(str, klass)
|
|
69
|
+
exists = true
|
|
70
|
+
elsif (segments = str.split(Constants::UNDERSCORE)).size > 1
|
|
71
|
+
remainder = []
|
|
72
|
+
found_assoc = nil
|
|
73
|
+
while !found_assoc && remainder.unshift(segments.pop) &&
|
|
74
|
+
segments.size > 0 do
|
|
75
|
+
assoc, poly_class = unpolymorphize_association(
|
|
76
|
+
segments.join(Constants::UNDERSCORE)
|
|
77
|
+
)
|
|
78
|
+
if found_assoc = get_association(assoc, klass)
|
|
79
|
+
exists = attribute_method?(
|
|
80
|
+
remainder.join(Constants::UNDERSCORE),
|
|
81
|
+
poly_class || found_assoc.klass
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
exists
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def table_for(parent)
|
|
90
|
+
parent.table
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def klassify(obj)
|
|
94
|
+
if Class === obj && ::ActiveRecord::Base > obj
|
|
95
|
+
obj
|
|
96
|
+
elsif obj.respond_to? :klass
|
|
97
|
+
obj.klass
|
|
98
|
+
elsif obj.respond_to? :base_klass
|
|
99
|
+
obj.base_klass
|
|
100
|
+
else
|
|
101
|
+
raise ArgumentError, "Don't know how to klassify #{obj}"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# All dependent Arel::Join nodes used in the search query.
|
|
106
|
+
#
|
|
107
|
+
# This could otherwise be done as `@object.arel.join_sources`, except
|
|
108
|
+
# that ActiveRecord's build_joins sets up its own JoinDependency.
|
|
109
|
+
# This extracts what we need to access the joins using our existing
|
|
110
|
+
# JoinDependency to track table aliases.
|
|
111
|
+
#
|
|
112
|
+
def join_sources
|
|
113
|
+
base, joins = begin
|
|
114
|
+
alias_tracker = @object.alias_tracker
|
|
115
|
+
constraints = @join_dependency.join_constraints(@object.joins_values, alias_tracker, @object.references_values)
|
|
116
|
+
|
|
117
|
+
[
|
|
118
|
+
Arel::SelectManager.new(@object.table),
|
|
119
|
+
constraints
|
|
120
|
+
]
|
|
121
|
+
end
|
|
122
|
+
joins.each do |aliased_join|
|
|
123
|
+
base.from(aliased_join)
|
|
124
|
+
end
|
|
125
|
+
base.join_sources
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def alias_tracker
|
|
129
|
+
@join_dependency.send(:alias_tracker)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def lock_association(association)
|
|
133
|
+
@lock_associations << association
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def remove_association(association)
|
|
137
|
+
return if @lock_associations.include?(association)
|
|
138
|
+
@join_dependency.instance_variable_get(:@join_root).children.delete_if { |stashed|
|
|
139
|
+
stashed.eql?(association)
|
|
140
|
+
}
|
|
141
|
+
@object.joins_values.delete_if { |jd|
|
|
142
|
+
jd.instance_variables.include?(:@join_root) &&
|
|
143
|
+
jd.instance_variable_get(:@join_root).children.map(&:object_id) == [association.object_id]
|
|
144
|
+
}
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Build an Arel subquery that selects keys for the top query,
|
|
148
|
+
# drawn from the first join association's foreign_key.
|
|
149
|
+
#
|
|
150
|
+
# Example: for an Article that has_and_belongs_to_many Tags
|
|
151
|
+
#
|
|
152
|
+
# context = Article.search.context
|
|
153
|
+
# attribute = Attribute.new(context, "tags_name").tap do |a|
|
|
154
|
+
# context.bind(a, a.name)
|
|
155
|
+
# end
|
|
156
|
+
# context.build_correlated_subquery(attribute.parent).to_sql
|
|
157
|
+
#
|
|
158
|
+
# # SELECT "articles_tags"."article_id" FROM "articles_tags"
|
|
159
|
+
# # INNER JOIN "tags" ON "tags"."id" = "articles_tags"."tag_id"
|
|
160
|
+
# # WHERE "articles_tags"."article_id" = "articles"."id"
|
|
161
|
+
#
|
|
162
|
+
# The WHERE condition on this query makes it invalid by itself,
|
|
163
|
+
# because it is correlated to the primary key on the outer query.
|
|
164
|
+
#
|
|
165
|
+
def build_correlated_subquery(association)
|
|
166
|
+
join_constraints = extract_joins(association)
|
|
167
|
+
join_root = join_constraints.shift
|
|
168
|
+
correlated_key = extract_correlated_key(join_root)
|
|
169
|
+
subquery = Arel::SelectManager.new(association.base_klass)
|
|
170
|
+
subquery.from(join_root.left)
|
|
171
|
+
subquery.project(correlated_key)
|
|
172
|
+
join_constraints.each do |j|
|
|
173
|
+
subquery.join_sources << Arel::Nodes::InnerJoin.new(j.left, j.right)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Handle polymorphic associations where correlated_key is an array
|
|
177
|
+
if correlated_key.is_a?(Array)
|
|
178
|
+
# For polymorphic associations, we need to add conditions for both the foreign key and type
|
|
179
|
+
correlated_key.each_with_index do |key, index|
|
|
180
|
+
if index == 0
|
|
181
|
+
# This is the foreign key
|
|
182
|
+
subquery = subquery.where(key.eq(primary_key))
|
|
183
|
+
else
|
|
184
|
+
# This is the type key, which should be equal to the model name
|
|
185
|
+
subquery = subquery.where(key.eq(@klass.name))
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
else
|
|
189
|
+
# Original behavior for non-polymorphic associations
|
|
190
|
+
subquery = subquery.where(correlated_key.eq(primary_key))
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
subquery
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def primary_key
|
|
197
|
+
@object.table[@object.primary_key]
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
private
|
|
201
|
+
|
|
202
|
+
def extract_correlated_key(join_root)
|
|
203
|
+
case join_root
|
|
204
|
+
when Arel::Nodes::OuterJoin
|
|
205
|
+
# one of join_root.right/join_root.left is expected to be Arel::Nodes::On
|
|
206
|
+
if join_root.right.is_a?(Arel::Nodes::On)
|
|
207
|
+
extract_correlated_key(join_root.right.expr)
|
|
208
|
+
elsif join_root.left.is_a?(Arel::Nodes::On)
|
|
209
|
+
extract_correlated_key(join_root.left.expr)
|
|
210
|
+
else
|
|
211
|
+
raise 'Ransack encountered an unexpected arel structure'
|
|
212
|
+
end
|
|
213
|
+
when Arel::Nodes::Equality
|
|
214
|
+
pk = primary_key
|
|
215
|
+
if join_root.left == pk
|
|
216
|
+
join_root.right
|
|
217
|
+
elsif join_root.right == pk
|
|
218
|
+
join_root.left
|
|
219
|
+
else
|
|
220
|
+
nil
|
|
221
|
+
end
|
|
222
|
+
when Arel::Nodes::And
|
|
223
|
+
# And may have multiple children, so we need to check all, not via left/right
|
|
224
|
+
if join_root.children.any?
|
|
225
|
+
join_root.children.each do |child|
|
|
226
|
+
key = extract_correlated_key(child)
|
|
227
|
+
return key if key
|
|
228
|
+
end
|
|
229
|
+
else
|
|
230
|
+
extract_correlated_key(join_root.left) || extract_correlated_key(join_root.right)
|
|
231
|
+
end
|
|
232
|
+
else
|
|
233
|
+
# eg parent was Arel::Nodes::And and the evaluated side was one of
|
|
234
|
+
# Arel::Nodes::Grouping or MultiTenant::TenantEnforcementClause
|
|
235
|
+
nil
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def get_parent_and_attribute_name(str, parent = @base)
|
|
240
|
+
attr_name = nil
|
|
241
|
+
|
|
242
|
+
if ransackable_attribute?(str, klassify(parent))
|
|
243
|
+
attr_name = str
|
|
244
|
+
elsif (segments = str.split(Constants::UNDERSCORE)).size > 1
|
|
245
|
+
remainder = []
|
|
246
|
+
found_assoc = nil
|
|
247
|
+
while remainder.unshift(segments.pop) && segments.size > 0 &&
|
|
248
|
+
!found_assoc do
|
|
249
|
+
assoc, klass = unpolymorphize_association(
|
|
250
|
+
segments.join(Constants::UNDERSCORE)
|
|
251
|
+
)
|
|
252
|
+
if found_assoc = get_association(assoc, parent)
|
|
253
|
+
join = build_or_find_association(
|
|
254
|
+
found_assoc.name, parent, klass
|
|
255
|
+
)
|
|
256
|
+
parent, attr_name = get_parent_and_attribute_name(
|
|
257
|
+
remainder.join(Constants::UNDERSCORE), join
|
|
258
|
+
)
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
[parent, attr_name]
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def get_association(str, parent = @base)
|
|
267
|
+
klass = klassify parent
|
|
268
|
+
ransackable_association?(str, klass) &&
|
|
269
|
+
klass.reflect_on_all_associations.detect { |a| a.name.to_s == str }
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def join_dependency(relation)
|
|
273
|
+
if relation.respond_to?(:join_dependency) # Polyamorous enables this
|
|
274
|
+
relation.join_dependency
|
|
275
|
+
else
|
|
276
|
+
build_joins(relation)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Checkout active_record/relation/query_methods.rb +build_joins+ for
|
|
281
|
+
# reference. Lots of duplicated code maybe we can avoid it
|
|
282
|
+
def build_joins(relation)
|
|
283
|
+
buckets = relation.joins_values + relation.left_outer_joins_values
|
|
284
|
+
|
|
285
|
+
buckets = buckets.group_by do |join|
|
|
286
|
+
case join
|
|
287
|
+
when String
|
|
288
|
+
:string_join
|
|
289
|
+
when Hash, Symbol, Array
|
|
290
|
+
:association_join
|
|
291
|
+
when Polyamorous::JoinDependency, Polyamorous::JoinAssociation
|
|
292
|
+
:stashed_join
|
|
293
|
+
when Arel::Nodes::Join
|
|
294
|
+
:join_node
|
|
295
|
+
else
|
|
296
|
+
raise 'unknown class: %s' % join.class.name
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
buckets.default = []
|
|
300
|
+
association_joins = buckets[:association_join]
|
|
301
|
+
stashed_association_joins = buckets[:stashed_join]
|
|
302
|
+
join_nodes = buckets[:join_node].uniq
|
|
303
|
+
string_joins = buckets[:string_join].map(&:strip)
|
|
304
|
+
string_joins.uniq!
|
|
305
|
+
|
|
306
|
+
join_list = join_nodes + convert_join_strings_to_ast(relation.table, string_joins)
|
|
307
|
+
|
|
308
|
+
alias_tracker = relation.alias_tracker(join_list)
|
|
309
|
+
join_dependency = Polyamorous::JoinDependency.new(relation.klass, relation.table, association_joins, Arel::Nodes::OuterJoin)
|
|
310
|
+
join_dependency.instance_variable_set(:@alias_tracker, alias_tracker)
|
|
311
|
+
join_nodes.each do |join|
|
|
312
|
+
join_dependency.send(:alias_tracker).aliases[join.left.name.downcase] = 1
|
|
313
|
+
end
|
|
314
|
+
join_dependency
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def convert_join_strings_to_ast(table, joins)
|
|
318
|
+
joins.map! { |join| table.create_string_join(Arel.sql(join)) unless join.blank? }
|
|
319
|
+
joins.compact!
|
|
320
|
+
joins
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def build_or_find_association(name, parent = @base, klass = nil)
|
|
324
|
+
find_association(name, parent, klass) or build_association(name, parent, klass)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def find_association(name, parent = @base, klass = nil)
|
|
328
|
+
@join_dependency.instance_variable_get(:@join_root).children.detect do |assoc|
|
|
329
|
+
assoc.reflection.name == name && assoc.table &&
|
|
330
|
+
(@associations_pot.empty? || @associations_pot[assoc] == parent || !@associations_pot.key?(assoc)) &&
|
|
331
|
+
(!klass || assoc.reflection.klass == klass)
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def build_association(name, parent = @base, klass = nil)
|
|
336
|
+
jd = Polyamorous::JoinDependency.new(
|
|
337
|
+
parent.base_klass,
|
|
338
|
+
parent.table,
|
|
339
|
+
Polyamorous::Join.new(name, @join_type, klass),
|
|
340
|
+
@join_type
|
|
341
|
+
)
|
|
342
|
+
found_association = jd.instance_variable_get(:@join_root).children.last
|
|
343
|
+
|
|
344
|
+
@associations_pot[found_association] = parent
|
|
345
|
+
|
|
346
|
+
# TODO maybe we dont need to push associations here, we could loop
|
|
347
|
+
# through the @associations_pot instead
|
|
348
|
+
@join_dependency.instance_variable_get(:@join_root).children.push found_association
|
|
349
|
+
|
|
350
|
+
# Builds the arel nodes properly for this association
|
|
351
|
+
@tables_pot[found_association] = @join_dependency.construct_tables_for_association!(jd.instance_variable_get(:@join_root), found_association)
|
|
352
|
+
|
|
353
|
+
# Leverage the stashed association functionality in AR
|
|
354
|
+
@object = @object.joins(jd)
|
|
355
|
+
found_association
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def extract_joins(association)
|
|
359
|
+
parent = @join_dependency.instance_variable_get(:@join_root)
|
|
360
|
+
reflection = association.reflection
|
|
361
|
+
join_constraints = association.join_constraints_with_tables(
|
|
362
|
+
parent.table,
|
|
363
|
+
parent.base_klass,
|
|
364
|
+
Arel::Nodes::OuterJoin,
|
|
365
|
+
@join_dependency.instance_variable_get(:@alias_tracker),
|
|
366
|
+
@tables_pot[association]
|
|
367
|
+
)
|
|
368
|
+
join_constraints.to_a.flatten
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
end
|