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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d884d53b3f3d7c0dc223d7a3a772544df81c58fd389e54d97b027b743acb3f4
4
- data.tar.gz: 63a180f14c982ed73ca57cf0ada9e3894a0b0328351d97b7a76b507e0d6276c2
3
+ metadata.gz: b2336d0d23d8af77f9f090bfd8962c25a8c97c9d9f39ffce7be9fe2034d7affc
4
+ data.tar.gz: 191d8e607720235a354e4da42e640e0fd3ca5b29aca814c1a1a0531d75f01833
5
5
  SHA512:
6
- metadata.gz: b07dc837ed05cfae41e2332822bab37cbab7a289d60da1b7fc71e8b1cc4d08ec47e5cf072dd80bdfb4b59f82d9fbce037e4c2ad0ce89838b719c88d6925efc38
7
- data.tar.gz: 28ceb0a1e948230d97571b3f1bac60b83d4d4b27ad9233cf86e7033e40337ec3dea6b704de4e85c272337c45587f3d31c5915e2234c6372f57e28625419954ca
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  module Refine
2
2
  module Rails
3
- VERSION = "2.13.3"
3
+ VERSION = "2.13.5"
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.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-01-21 00:00:00.000000000 Z
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