refine-rails 2.13.3 → 2.13.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d884d53b3f3d7c0dc223d7a3a772544df81c58fd389e54d97b027b743acb3f4
4
- data.tar.gz: 63a180f14c982ed73ca57cf0ada9e3894a0b0328351d97b7a76b507e0d6276c2
3
+ metadata.gz: 40da5d98e34b1e013ee9019520147512b97f1e7130ccca08d05b0ba0429e6540
4
+ data.tar.gz: e8b762451c73d83718f1fb98811bbb89aa5d2dee1b3f5ec828c3f1da85f563c7
5
5
  SHA512:
6
- metadata.gz: b07dc837ed05cfae41e2332822bab37cbab7a289d60da1b7fc71e8b1cc4d08ec47e5cf072dd80bdfb4b59f82d9fbce037e4c2ad0ce89838b719c88d6925efc38
7
- data.tar.gz: 28ceb0a1e948230d97571b3f1bac60b83d4d4b27ad9233cf86e7033e40337ec3dea6b704de4e85c272337c45587f3d31c5915e2234c6372f57e28625419954ca
6
+ metadata.gz: 145af9023fa64fa006748da902668154244a59cf75a5669c9c47a8d038a06bdaa920fdf34207305837defe4390d1dfcc3533af7f257781f59b2a4db94cc82217
7
+ data.tar.gz: 537698a2c4e28b6630e934adf7b13ccf0460b7f2fe3760e9189f4cd01340343a03ff57b178b5fc189e24b3e0dd039ec756c8a08baab17d2115405885ee13de8e
@@ -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,143 @@
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
+ @attribute = get_foreign_key_from_relation(instance: instance, reflection: through_reflection)
82
+ else
83
+ puts "TODO - not referencing an ID in attribute"
84
+ end
85
+
86
+ unless instance
87
+ raise "Relationship does not exist for #{relation}."
88
+ end
89
+
90
+ relation_table_being_queried = through_reflection.klass.arel_table
91
+ relation_class = through_reflection.klass
92
+
93
+ nodes = apply_condition(input, relation_table_being_queried, inverse_clause)
94
+ if !is_refinement && has_any_refinements?
95
+ refined_node = apply_refinements(input)
96
+ # Count refinement will return nil because it directly modified pending relationship subquery
97
+ nodes = nodes.and(refined_node) if refined_node
98
+ end
99
+ nodes
100
+
101
+ # if can_use_where_in_relationship_subquery?(instance)
102
+ # create_pending_wherein_subquery(input: input, relation: relation, instance: instance, query: query)
103
+ # else
104
+ # create_pending_has_many_through_subquery(input: input, relation: relation, instance: instance, query: query)
105
+ # end
106
+ end
107
+
108
+ def get_through_reflection(instance:, relation:)
109
+ if instance.is_a? ActiveRecord::Reflection::ThroughReflection
110
+ through_reflection = instance.through_reflection
111
+ instance.active_record_primary_key.to_sym
112
+ if(through_reflection.is_a?(ActiveRecord::Reflection::BelongsToReflection))
113
+ through_reflection = instance.source_reflection.through_reflection
114
+ end
115
+ through_reflection
116
+ else
117
+ puts "Not a through Reflection: #{instance.inspect}"
118
+ end
119
+ end
120
+
121
+ def get_foreign_key_from_relation(instance:, reflection:)
122
+ child_foreign_key = instance.source_reflection.foreign_key
123
+ child_foreign_key
124
+ end
125
+
126
+ def add_pending_join(relation, join_type=:inner)
127
+ # If we already are tracking the relation with a left joins, don't overwrite it
128
+ # puts "adding a pending join for relation: #{relation} with join type: #{join_type}"
129
+ unless join_type == :inner && filter.pending_joins[relation] == :left
130
+ filter.pending_joins[relation] = join_type
131
+ end
132
+ end
133
+
134
+ def add_pending_joins_if_needed(instance:, reflection:, input:)
135
+ # Determine if we need to do left-joins due to the clause needing to include null values
136
+ if(input && LEFT_JOIN_CLAUSES.include?(input[:clause]))
137
+ add_pending_join(reflection.name, :left)
138
+ else
139
+ add_pending_join(reflection.name, :inner)
140
+ end
141
+ end
142
+ end
143
+ end
@@ -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,100 @@
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
7
+
8
+ def pending_joins
9
+ @pending_joins ||= {}
10
+ end
11
+
12
+ def applied_conditions
13
+ @applied_conditions ||= {}
14
+ end
15
+
16
+ def get_flat_query
17
+ raise "Initial query must exist" if initial_query.nil?
18
+ raise "Cannot make flat query for a filter using OR conditions" if uses_or?
19
+ if blueprint.present?
20
+ construct_flat_query
21
+ else
22
+ @relation
23
+ end
24
+ end
25
+
26
+ def get_flat_query!
27
+ result = get_flat_query
28
+ raise Refine::InvalidFilterError.new(filter: self) unless errors.none?
29
+ result
30
+ end
31
+
32
+ # This iterates through each blueprint item and applies the conditions.
33
+ # It is meant to be idempotent hence it checks for already applied conditions
34
+ def construct_flat_query
35
+ groups = []
36
+ blueprint.each do |criteria_or_conjunction|
37
+ if criteria_or_conjunction[:type] == "conjunction"
38
+ if criteria_or_conjunction[:word] == "or"
39
+ puts "This is an OR"
40
+ # Reset applied conditions since we're in a new group
41
+ @applied_conditions = {}
42
+ end
43
+ else
44
+ unless condition_already_applied?(criteria_or_conjunction)
45
+ node = apply_flat_condition(criteria_or_conjunction)
46
+ @relation = @relation.where(Arel.sql(node.to_sql))
47
+ track_condition_applied(criteria_or_conjunction)
48
+ end
49
+ end
50
+ end
51
+ if pending_joins.present?
52
+ apply_pending_joins
53
+ end
54
+ @relation
55
+ end
56
+
57
+ # Same as Filter.apply_condition but uses `supports_flat_queries` helpers instead of default path
58
+ def apply_flat_condition(criterion)
59
+ begin
60
+ get_condition_for_criterion(criterion)&.apply_flat(criterion[:input], table, initial_query, false)
61
+ rescue Refine::Conditions::Errors::ConditionClauseError => e
62
+ e.errors.each do |error|
63
+ errors.add(:base, error.full_message, criterion_uid: criterion[:uid])
64
+ end
65
+ end
66
+ end
67
+
68
+ # Called at the end of the filter's construct_flat_query. Applies joins from pending_joins hash constructed by individual conditions
69
+ def apply_pending_joins
70
+ if pending_joins.present?
71
+ join_count = 0
72
+ pending_joins.each do |relation, join_type|
73
+ if join_type == :left
74
+ @relation = @relation.left_joins(relation.to_sym).distinct
75
+ else
76
+ @relation = @relation.joins(relation.to_sym)
77
+ end
78
+ join_count += 1
79
+ end
80
+
81
+ if join_count > 1
82
+ @relation = @relation.distinct
83
+ end
84
+ end
85
+ end
86
+
87
+ def track_condition_applied(criterion)
88
+ if applied_conditions[criterion[:condition_id]].nil?
89
+ applied_conditions[criterion[:condition_id]] = [criterion[:input]]
90
+ else
91
+ applied_conditions[criterion[:condition_id]] << criterion[:input]
92
+ end
93
+ end
94
+
95
+ def condition_already_applied?(criterion)
96
+ applied_conditions[criterion[:condition_id]] &&
97
+ applied_conditions[criterion[:condition_id]].include?(criterion[:input])
98
+ end
99
+ end
100
+ 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
@@ -1,5 +1,5 @@
1
1
  module Refine
2
2
  module Rails
3
- VERSION = "2.13.3"
3
+ VERSION = "2.13.4"
4
4
  end
5
5
  end
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.3
4
+ version: 2.13.4
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-01-21 00:00:00.000000000 Z
12
+ date: 2025-02-12 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