refine-rails 2.13.3 → 2.13.5
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/app/models/refine/conditions/condition.rb +1 -0
- data/app/models/refine/conditions/supports_flat_queries.rb +145 -0
- data/app/models/refine/filter.rb +3 -1
- data/app/models/refine/flat_query_tools.rb +107 -0
- data/app/models/refine/inspector.rb +41 -0
- data/lib/refine/rails/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b2336d0d23d8af77f9f090bfd8962c25a8c97c9d9f39ffce7be9fe2034d7affc
|
4
|
+
data.tar.gz: 191d8e607720235a354e4da42e640e0fd3ca5b29aca814c1a1a0531d75f01833
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f0fd3bc2e744d3de0fcd7092bd87d5969d472c886eacc7d007ff711bae937449588b48cb7ed5e56f590756f6d0f214fee2b690bdc9ead7676453b7e64e8862e5
|
7
|
+
data.tar.gz: 692abd2df6a29e0687f4aa4ad985ebf88371b5a72b7b45432d42e4837cc423272a98c15dc2a139bdebe7ac0a0d24a53b293b8f7f25e43081da1fc13013c555fd
|
@@ -13,6 +13,7 @@ module Refine::Conditions
|
|
13
13
|
include HasThroughIdRelationship
|
14
14
|
include WithForcedIndex
|
15
15
|
include HasIcon
|
16
|
+
include SupportsFlatQueries
|
16
17
|
|
17
18
|
attr_reader :ensurances, :before_validations, :clause, :filter
|
18
19
|
attr_accessor :display, :id, :is_refinement, :attribute
|
@@ -0,0 +1,145 @@
|
|
1
|
+
module Refine::Conditions
|
2
|
+
module SupportsFlatQueries
|
3
|
+
|
4
|
+
LEFT_JOIN_CLAUSES = [
|
5
|
+
Refine::Conditions::Clauses::NOT_IN,
|
6
|
+
Refine::Conditions::Clauses::NOT_SET,
|
7
|
+
Refine::Conditions::Clauses::DOESNT_EQUAL,
|
8
|
+
Refine::Conditions::Clauses::DOESNT_CONTAIN
|
9
|
+
]
|
10
|
+
|
11
|
+
# Applies the criterion which can be a relationship condition
|
12
|
+
#
|
13
|
+
# @param [Hash] input The user's input
|
14
|
+
# @param [Arel::Table] table The arel_table the query is built on
|
15
|
+
# @param [ActiveRecord::Relation] initial_query The base query the query is built on
|
16
|
+
# @param [Bool] inverse_clause Whether to invert the clause
|
17
|
+
# @return [Arel::Node]
|
18
|
+
def apply_flat(input, table, initial_query, inverse_clause=false)
|
19
|
+
table ||= filter.table
|
20
|
+
# Ensurance validations are checking the developer configured correctly
|
21
|
+
run_ensurance_validations
|
22
|
+
# Allow developer to modify user input
|
23
|
+
# TODO run_before_validate(input) -> what is this for?
|
24
|
+
|
25
|
+
run_before_validate_validations(input)
|
26
|
+
|
27
|
+
# TODO Determine right place to set the clause
|
28
|
+
validate_user_input(input)
|
29
|
+
if input.dig(:filter_refinement).present?
|
30
|
+
|
31
|
+
filter_condition = call_proc_if_callable(@filter_refinement_proc)
|
32
|
+
# Set the filter on the filter_condition to be the current_condition's filter
|
33
|
+
filter_condition.set_filter(filter)
|
34
|
+
filter_condition.is_refinement = true
|
35
|
+
|
36
|
+
# Applying the filter condition will modify pending relationship subqueries in place
|
37
|
+
filter_condition.apply(input.dig(:filter_refinement), table, initial_query)
|
38
|
+
input.delete(:filter_refinement)
|
39
|
+
end
|
40
|
+
|
41
|
+
if is_relationship_attribute?
|
42
|
+
return handle_flat_relational_condition(input: input, query: initial_query, inverse_clause: inverse_clause)
|
43
|
+
end
|
44
|
+
# Not a relationship attribute, apply condition normally
|
45
|
+
nodes = apply_condition(input, table, inverse_clause)
|
46
|
+
if !is_refinement && has_any_refinements?
|
47
|
+
refined_node = apply_refinements(input)
|
48
|
+
# Count refinement will return nil because it directly modified pending relationship subquery
|
49
|
+
nodes = nodes.and(refined_node) if refined_node
|
50
|
+
end
|
51
|
+
nodes
|
52
|
+
end
|
53
|
+
|
54
|
+
def handle_flat_relational_condition(input:, query:, inverse_clause:)
|
55
|
+
# Split on first .
|
56
|
+
decompose_attribute = @attribute.split(".", 2)
|
57
|
+
# Attribute now is the back half of the initial attribute
|
58
|
+
@attribute = decompose_attribute[1]
|
59
|
+
# Relation to be handled
|
60
|
+
relation = decompose_attribute[0]
|
61
|
+
|
62
|
+
|
63
|
+
# Get the Reflection object which defines the relationship between query and relation
|
64
|
+
# First iteration pull relationship using base query which responds to model.
|
65
|
+
instance = if query.respond_to? :model
|
66
|
+
query.model.reflect_on_association(relation.to_sym)
|
67
|
+
else
|
68
|
+
# When query is sent in as subquery (recursive) the query object is the model class pulled from the
|
69
|
+
# previous instance value
|
70
|
+
query.reflect_on_association(relation.to_sym)
|
71
|
+
end
|
72
|
+
|
73
|
+
through_reflection = instance
|
74
|
+
|
75
|
+
# TODO - make sure we're accounting for refinements
|
76
|
+
if @attribute == "id"
|
77
|
+
# We're referencing a primary key ID, so we dont need the final join table and
|
78
|
+
# can just reference the foreign key of the previous step in the relation chain
|
79
|
+
through_reflection = get_through_reflection(instance: instance, relation: decompose_attribute[0])
|
80
|
+
add_pending_joins_if_needed(instance: instance, reflection: through_reflection, input: input)
|
81
|
+
# TODO - this is not the right long-term place for this.
|
82
|
+
filter.needs_distinct = true
|
83
|
+
@attribute = get_foreign_key_from_relation(instance: instance, reflection: through_reflection)
|
84
|
+
else
|
85
|
+
puts "TODO - not referencing an ID in attribute"
|
86
|
+
end
|
87
|
+
|
88
|
+
unless instance
|
89
|
+
raise "Relationship does not exist for #{relation}."
|
90
|
+
end
|
91
|
+
|
92
|
+
relation_table_being_queried = through_reflection.klass.arel_table
|
93
|
+
relation_class = through_reflection.klass
|
94
|
+
|
95
|
+
nodes = apply_condition(input, relation_table_being_queried, inverse_clause)
|
96
|
+
if !is_refinement && has_any_refinements?
|
97
|
+
refined_node = apply_refinements(input)
|
98
|
+
# Count refinement will return nil because it directly modified pending relationship subquery
|
99
|
+
nodes = nodes.and(refined_node) if refined_node
|
100
|
+
end
|
101
|
+
nodes
|
102
|
+
|
103
|
+
# if can_use_where_in_relationship_subquery?(instance)
|
104
|
+
# create_pending_wherein_subquery(input: input, relation: relation, instance: instance, query: query)
|
105
|
+
# else
|
106
|
+
# create_pending_has_many_through_subquery(input: input, relation: relation, instance: instance, query: query)
|
107
|
+
# end
|
108
|
+
end
|
109
|
+
|
110
|
+
def get_through_reflection(instance:, relation:)
|
111
|
+
if instance.is_a? ActiveRecord::Reflection::ThroughReflection
|
112
|
+
through_reflection = instance.through_reflection
|
113
|
+
instance.active_record_primary_key.to_sym
|
114
|
+
if(through_reflection.is_a?(ActiveRecord::Reflection::BelongsToReflection))
|
115
|
+
through_reflection = instance.source_reflection.through_reflection
|
116
|
+
end
|
117
|
+
through_reflection
|
118
|
+
else
|
119
|
+
puts "Not a through Reflection: #{instance.inspect}"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def get_foreign_key_from_relation(instance:, reflection:)
|
124
|
+
child_foreign_key = instance.source_reflection.foreign_key
|
125
|
+
child_foreign_key
|
126
|
+
end
|
127
|
+
|
128
|
+
def add_pending_join(relation, join_type=:inner)
|
129
|
+
# If we already are tracking the relation with a left joins, don't overwrite it
|
130
|
+
# puts "adding a pending join for relation: #{relation} with join type: #{join_type}"
|
131
|
+
unless join_type == :inner && filter.pending_joins[relation] == :left
|
132
|
+
filter.pending_joins[relation] = join_type
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def add_pending_joins_if_needed(instance:, reflection:, input:)
|
137
|
+
# Determine if we need to do left-joins due to the clause needing to include null values
|
138
|
+
if(input && LEFT_JOIN_CLAUSES.include?(input[:clause]))
|
139
|
+
add_pending_join(reflection.name, :left)
|
140
|
+
else
|
141
|
+
add_pending_join(reflection.name, :inner)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
data/app/models/refine/filter.rb
CHANGED
@@ -5,6 +5,8 @@ module Refine
|
|
5
5
|
include TracksPendingRelationshipSubqueries
|
6
6
|
include Stabilize
|
7
7
|
include Internationalized
|
8
|
+
include Inspector
|
9
|
+
include FlatQueryTools
|
8
10
|
# This validation structure sents `initial_query` as the method to validate against
|
9
11
|
define_model_callbacks :initialize, only: [:after]
|
10
12
|
after_initialize :valid?
|
@@ -211,7 +213,7 @@ module Refine
|
|
211
213
|
|
212
214
|
def apply_condition(criterion)
|
213
215
|
begin
|
214
|
-
get_condition_for_criterion(criterion)&.apply(criterion[:input], table, initial_query)
|
216
|
+
get_condition_for_criterion(criterion)&.apply(criterion[:input], table, initial_query, false, nil)
|
215
217
|
rescue Refine::Conditions::Errors::ConditionClauseError => e
|
216
218
|
e.errors.each do |error|
|
217
219
|
errors.add(:base, error.full_message, criterion_uid: criterion[:uid])
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# This module is meant to provide an alternative to #get_query which will attempt to make the query flat with inner and left joins
|
2
|
+
# instead of nested queries. This is useful for performance reasons when the query is complex and the database is large.
|
3
|
+
# NOTE: This is more specialized query construction and it is up to the implementer to use the inspector tools to ensure this is only being used for supported queries
|
4
|
+
module Refine
|
5
|
+
module FlatQueryTools
|
6
|
+
attr_accessor :pending_joins, :applied_conditions, :needs_distinct
|
7
|
+
|
8
|
+
def pending_joins
|
9
|
+
@pending_joins ||= {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def applied_conditions
|
13
|
+
@applied_conditions ||= {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def needs_distinct?
|
17
|
+
@needs_distinct ||= false
|
18
|
+
end
|
19
|
+
|
20
|
+
def get_flat_query
|
21
|
+
raise "Initial query must exist" if initial_query.nil?
|
22
|
+
raise "Cannot make flat query for a filter using OR conditions" if uses_or?
|
23
|
+
if blueprint.present?
|
24
|
+
construct_flat_query
|
25
|
+
else
|
26
|
+
@relation
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def get_flat_query!
|
31
|
+
result = get_flat_query
|
32
|
+
raise Refine::InvalidFilterError.new(filter: self) unless errors.none?
|
33
|
+
result
|
34
|
+
end
|
35
|
+
|
36
|
+
# This iterates through each blueprint item and applies the conditions.
|
37
|
+
# It is meant to be idempotent hence it checks for already applied conditions
|
38
|
+
def construct_flat_query
|
39
|
+
groups = []
|
40
|
+
blueprint.each do |criteria_or_conjunction|
|
41
|
+
if criteria_or_conjunction[:type] == "conjunction"
|
42
|
+
if criteria_or_conjunction[:word] == "or"
|
43
|
+
puts "This is an OR"
|
44
|
+
# Reset applied conditions since we're in a new group
|
45
|
+
@applied_conditions = {}
|
46
|
+
end
|
47
|
+
else
|
48
|
+
unless condition_already_applied?(criteria_or_conjunction)
|
49
|
+
node = apply_flat_condition(criteria_or_conjunction)
|
50
|
+
@relation = @relation.where(Arel.sql(node.to_sql))
|
51
|
+
track_condition_applied(criteria_or_conjunction)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
if pending_joins.present?
|
56
|
+
apply_pending_joins
|
57
|
+
end
|
58
|
+
if needs_distinct?
|
59
|
+
@relation = @relation.distinct
|
60
|
+
end
|
61
|
+
@relation
|
62
|
+
end
|
63
|
+
|
64
|
+
# Same as Filter.apply_condition but uses `supports_flat_queries` helpers instead of default path
|
65
|
+
def apply_flat_condition(criterion)
|
66
|
+
begin
|
67
|
+
get_condition_for_criterion(criterion)&.apply_flat(criterion[:input], table, initial_query, false)
|
68
|
+
rescue Refine::Conditions::Errors::ConditionClauseError => e
|
69
|
+
e.errors.each do |error|
|
70
|
+
errors.add(:base, error.full_message, criterion_uid: criterion[:uid])
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Called at the end of the filter's construct_flat_query. Applies joins from pending_joins hash constructed by individual conditions
|
76
|
+
def apply_pending_joins
|
77
|
+
if pending_joins.present?
|
78
|
+
join_count = 0
|
79
|
+
pending_joins.each do |relation, join_type|
|
80
|
+
if join_type == :left
|
81
|
+
@relation = @relation.left_joins(relation.to_sym).distinct
|
82
|
+
else
|
83
|
+
@relation = @relation.joins(relation.to_sym)
|
84
|
+
end
|
85
|
+
join_count += 1
|
86
|
+
end
|
87
|
+
|
88
|
+
if join_count > 1
|
89
|
+
@relation = @relation.distinct
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def track_condition_applied(criterion)
|
95
|
+
if applied_conditions[criterion[:condition_id]].nil?
|
96
|
+
applied_conditions[criterion[:condition_id]] = [criterion[:input]]
|
97
|
+
else
|
98
|
+
applied_conditions[criterion[:condition_id]] << criterion[:input]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def condition_already_applied?(criterion)
|
103
|
+
applied_conditions[criterion[:condition_id]] &&
|
104
|
+
applied_conditions[criterion[:condition_id]].include?(criterion[:input])
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Refine
|
2
|
+
module Inspector
|
3
|
+
def uses_or?
|
4
|
+
return false if @blueprint.nil? || @blueprint.empty?
|
5
|
+
@blueprint.select{|c| c[:type] == "conjunction" && c[:word] == "or"}.any?
|
6
|
+
end
|
7
|
+
|
8
|
+
def uses_and?
|
9
|
+
return false if @blueprint.nil? || @blueprint.empty?
|
10
|
+
@blueprint.select{|c| c[:type] == "conjunction" && c[:word] == "and"}.any?
|
11
|
+
end
|
12
|
+
|
13
|
+
def uses_condition(condition_id, using_clauses: [])
|
14
|
+
return false if @blueprint.nil? || @blueprint.empty?
|
15
|
+
condition = @blueprint.select{|c| c[:type] == "criterion" && c[:condition_id] == condition_id}.any?
|
16
|
+
using_clauses = [using_clauses] unless using_clauses&.is_a?(Array)
|
17
|
+
if(using_clauses.any?)
|
18
|
+
condition = condition && @blueprint.select{|c| c[:type] == "criterion" && using_clauses.include?(c[:input][:clause]) }.any?
|
19
|
+
end
|
20
|
+
return condition
|
21
|
+
end
|
22
|
+
|
23
|
+
def uses_condition_at_least(condition_id, occurrences: 1)
|
24
|
+
return false if @blueprint.nil? || @blueprint.empty?
|
25
|
+
conditions = @blueprint.select{|c| c[:type] == "criterion" && c[:condition_id] == condition_id}
|
26
|
+
return conditions.length >= occurrences
|
27
|
+
end
|
28
|
+
|
29
|
+
def uses_negative_clause?
|
30
|
+
return false if @blueprint.nil? || @blueprint.empty?
|
31
|
+
negative_clauses = [
|
32
|
+
Refine::Conditions::Clauses::NOT_IN,
|
33
|
+
Refine::Conditions::Clauses::NOT_SET,
|
34
|
+
Refine::Conditions::Clauses::DOESNT_EQUAL,
|
35
|
+
Refine::Conditions::Clauses::DOESNT_CONTAIN
|
36
|
+
]
|
37
|
+
@blueprint.select{|c| c[:type] == "criterion" && negative_clauses.include?(c[:input][:clause])}.any?
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
data/lib/refine/rails/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: refine-rails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.13.
|
4
|
+
version: 2.13.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Colleen Schnettler
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2025-
|
12
|
+
date: 2025-02-17 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
@@ -100,6 +100,7 @@ files:
|
|
100
100
|
- app/models/refine/conditions/has_through_id_relationship.rb
|
101
101
|
- app/models/refine/conditions/numeric_condition.rb
|
102
102
|
- app/models/refine/conditions/option_condition.rb
|
103
|
+
- app/models/refine/conditions/supports_flat_queries.rb
|
103
104
|
- app/models/refine/conditions/text_condition.rb
|
104
105
|
- app/models/refine/conditions/uses_attributes.rb
|
105
106
|
- app/models/refine/conditions/with_forced_index.rb
|
@@ -109,11 +110,13 @@ files:
|
|
109
110
|
- app/models/refine/filters/builder.rb
|
110
111
|
- app/models/refine/filters/criterion.rb
|
111
112
|
- app/models/refine/filters/query.rb
|
113
|
+
- app/models/refine/flat_query_tools.rb
|
112
114
|
- app/models/refine/inline/criteria/date_refinement.rb
|
113
115
|
- app/models/refine/inline/criteria/input.rb
|
114
116
|
- app/models/refine/inline/criteria/numeric_refinement.rb
|
115
117
|
- app/models/refine/inline/criteria/option.rb
|
116
118
|
- app/models/refine/inline/criterion.rb
|
119
|
+
- app/models/refine/inspector.rb
|
117
120
|
- app/models/refine/invalid_filter_error.rb
|
118
121
|
- app/models/refine/stabilize.rb
|
119
122
|
- app/models/refine/stabilizers/database_stabilizer.rb
|