rails-simple-search 1.3.0 → 2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 359d3f0db9c1b924fbe313abf4a30bf796442a9785fd6914457f3b387a692e8f
4
- data.tar.gz: d4f5f198779e95ee3bd60d5143f0ff89be4978429d718a1660eca7aafe221abe
3
+ metadata.gz: 2fcf45308630c8fda4c1b7ecce567017b0a77377060aff73ca41600405d2b8a5
4
+ data.tar.gz: 585040de107d0abf032daf209ad2cd83cfb45ec04ae3276dbb47730060270d2d
5
5
  SHA512:
6
- metadata.gz: c66d51b2845ad96dbf656e523a4867e6ea320836cd642b322387846cda18067a567063a3ce59f7f2d09bb4919e37604025ff414f850e8529e660366cd632e749
7
- data.tar.gz: 2334fa8f8b7ed85fe3558355e292843eb6c6a05c49b880af47589143e749eff08658683545120b09503979e90cec2a6078e48128e3263fb41d95e1d4a7ffa810
6
+ metadata.gz: cfb6223b4b906b2d4ef3e748c6c794c62bad9813c1cbfef9b220523826f0e3691874a9426ff2c9bd18fad09cf30b55782d24775efa13f282a187f7a357437247
7
+ data.tar.gz: 7b6c4c9057c3c0425c362253417932db84ef434771aa43118af9de14530e8ae3a5594e9484e2de3c080c9dc7cc46088ec830fb56775d1800aff6d2e5c971ed0e
@@ -0,0 +1,64 @@
1
+ require_relative 'selection_item'
2
+
3
+ # frozen_string_literal: true
4
+ module RailsSimpleSearch
5
+ module SqlHandler
6
+ # this class is to represent a sql select statements, union of select
7
+ # statements, or intersect of select statements
8
+ class SelectionGroup
9
+ def self.union_alias_count
10
+ @union_alias_count ||= 0
11
+ @union_alias_count += 1
12
+ @union_alias_count
13
+ end
14
+
15
+ def initialize(item = nil)
16
+ @selection_item = item if item
17
+ @children = []
18
+ end
19
+
20
+ def add_child(condition_group)
21
+ raise "It's not allowed to add child into leaf node" if leaf?
22
+
23
+ @children << condition_group if condition_group
24
+ end
25
+
26
+ def add_item(selection_item)
27
+ raise "It's not allowed to add item into non-leaf node" unless empty?
28
+
29
+ @selection_item = selection_item
30
+ end
31
+
32
+ def relation(and_or)
33
+ raise "It's no need to set relation for leaf node" if leaf?
34
+
35
+ @relation = and_or
36
+ end
37
+
38
+ def leaf?
39
+ @selection_item ? true : false
40
+ end
41
+
42
+ def empty?
43
+ @children.empty? ? true : false
44
+ end
45
+
46
+ def to_sql
47
+ if leaf?
48
+ @selection_item.to_sql
49
+ elsif @relation == :or
50
+ unioned_sql = @children.map(&:to_sql).join(' union ')
51
+ "select * from ( #{unioned_sql} ) as #{union_alias}"
52
+ elsif @relation == :and
53
+ @children.map(&:to_sql).join(' intersect ')
54
+ else
55
+ raise "This should not happen"
56
+ end
57
+ end
58
+
59
+ def union_alias
60
+ "union_alias_#{self.class.union_alias_count}"
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+ module RailsSimpleSearch
3
+ module SqlHandler
4
+ # this class represents a single select statement, without unions or intersects.
5
+ class SelectionItem
6
+ attr_reader :field, :value
7
+
8
+ def initialize(config, model_class, field, value)
9
+ @config = config
10
+ @model_class = model_class
11
+ @field = field
12
+ @value = value
13
+ @joins = {}
14
+ @verb = ''
15
+ end
16
+
17
+ def to_sql
18
+ build_select_with_condition(@field, @value)
19
+ join_str = make_joins
20
+ query = @model_class.joins(join_str)
21
+ query = query.where(to_ar_condition)
22
+ query = query.select("distinct #{@model_class.table_name}.*")
23
+ query.to_sql
24
+ end
25
+
26
+ private
27
+
28
+ # This method parse a search parameter and its value
29
+ # then produce a ConditionGroup
30
+ def build_select_with_condition(attribute, value)
31
+ # handle direct fields
32
+ unless attribute =~ /\./
33
+ condition = build_single_condition(@model_class, @model_class.table_name, attribute, value)
34
+ return condition
35
+ end
36
+
37
+ # handle association fields
38
+ association_fields = attribute.split(/\./)
39
+ field = association_fields.pop
40
+
41
+ base_class = @model_class
42
+ new_asso_chain = true
43
+ while (current_association_string = association_fields.shift)
44
+ # polymorphic association with solid target table_name
45
+ # such as 'commentable:post'
46
+ if current_association_string.include?(':')
47
+ poly_asso_name, poly_asso_type = current_association_string.split(':')
48
+ current_association = base_class.reflect_on_association(poly_asso_name.to_sym)
49
+ poly_asso_type_class = poly_asso_type.downcase.camelize.constantize
50
+ insert_join(base_class, current_association, new_asso_chain, poly_asso_type_class)
51
+ base_class = poly_asso_type_class
52
+ else
53
+ current_association = base_class.reflect_on_association(current_association_string.to_sym)
54
+ insert_join(base_class, current_association, new_asso_chain)
55
+ base_class = current_association.klass
56
+ end
57
+
58
+ new_asso_chain = false
59
+ end
60
+
61
+ association_alias = table_name_to_alias(base_class.table_name)
62
+ build_single_condition(base_class, association_alias, field, value)
63
+ end
64
+
65
+ def build_single_condition(base_class, association_alias, field, value)
66
+ field, operator = parse_field_name(field)
67
+ table = base_class.table_name
68
+ key = "#{table}.#{field}"
69
+ final_key = "#{association_alias}.#{field}"
70
+
71
+ column = base_class.columns_hash[field.to_s]
72
+ return nil unless column
73
+
74
+ if value.nil?
75
+ verb = 'is'
76
+ elsif value.is_a?(Array)
77
+ verb = 'in'
78
+ elsif operator
79
+ verb = operator
80
+ elsif text_column?(column) && ! @config[:exact_match].include?((@model_table_name == table) ? field : key)
81
+ verb = 'like'
82
+ value = "%#{value}%"
83
+ else
84
+ verb = '='
85
+ end
86
+
87
+ @final_key = final_key
88
+ @verb = verb
89
+ @value = value
90
+ end
91
+
92
+ def insert_join(base_class, asso_ref, new_asso_chain, poly_asso_type_class=nil)
93
+ return if asso_ref.polymorphic? && poly_asso_type_class.blank?
94
+ base_table = base_class.table_name
95
+ asso_table = poly_asso_type_class&.table_name || asso_ref.klass.table_name
96
+
97
+ @join_count ||= 0
98
+ return if base_table == asso_table
99
+ return unless @joins[asso_table].nil?
100
+
101
+ @join_count += 1
102
+ base_table_alias = new_asso_chain ? base_table : table_name_to_alias(base_table)
103
+ asso_table_alias = format('A%02d', @join_count)
104
+
105
+ if asso_ref.belongs_to?
106
+ if asso_ref.polymorphic?
107
+ join_cond = "#{base_table_alias}.#{asso_ref.foreign_key} = #{asso_table_alias}.#{poly_asso_type_class.primary_key}"
108
+ join_cond = "#{base_table_alias}.#{asso_ref.foreign_type} = '#{poly_asso_type_class.name}' and #{join_cond}"
109
+ else
110
+ join_cond = "#{base_table_alias}.#{asso_ref.foreign_key} = #{asso_table_alias}.#{asso_ref.klass.primary_key}"
111
+ end
112
+ else
113
+ join_cond = "#{base_table_alias}.#{base_class.primary_key} = #{asso_table_alias}.#{asso_ref.foreign_key}"
114
+ join_cond = "#{asso_table_alias}.#{asso_ref.type} = '#{base_class.name}' and #{join_cond}" if asso_ref.type
115
+ end
116
+ @joins[asso_table] = [@join_count, asso_table, join_cond]
117
+ end
118
+
119
+ def table_name_to_alias(table_name)
120
+ format('A%02d', @joins[table_name][0])
121
+ end
122
+
123
+ def parse_field_name(name)
124
+ if name =~ /^(.*)?((_(greater|less)_than)(_equal_to)?)$/
125
+ field_name = ::Regexp.last_match(1)
126
+ operator = (::Regexp.last_match(4) == 'greater' ? '>' : '<')
127
+ operator << '=' if ::Regexp.last_match(5)
128
+ else
129
+ field_name = name
130
+ end
131
+
132
+ [field_name, operator]
133
+ end
134
+
135
+ def text_column?(column)
136
+ if column.respond_to?(:text?)
137
+ column.text?
138
+ elsif column.respond_to?(:type)
139
+ column.type == :string || column.type == :text
140
+ else
141
+ raise 'encountered new version of Rails'
142
+ end
143
+ end
144
+
145
+ def make_joins
146
+ joins_str = ''
147
+ joins = @joins.values
148
+ joins.sort! { |a, b| a[0] <=> b[0] }
149
+ joins.each do |j|
150
+ table = j[1]
151
+ constrain = j[2]
152
+ joins_str += format(" inner join #{table} AS A%02d on #{constrain}", j[0])
153
+ end
154
+ joins_str
155
+ end
156
+
157
+ def to_ar_condition
158
+ condition = []
159
+ condition << "#{@final_key} #{@verb}"
160
+ condition[0] << (@verb == 'in' ? ' (?)' : ' ?')
161
+ condition << @value
162
+ condition
163
+ end
164
+ end
165
+ end
166
+ end
data/lib/sql_handler.rb CHANGED
@@ -1,3 +1,38 @@
1
+ require_relative 'selection_group.rb'
2
+ require_relative 'selection_item.rb'
3
+
4
+ # This gem returns some rows of a certain table (model), according to the search parameters.
5
+ # Each search parameter can ba a direct field (like first_name, last_name), indirect
6
+ # field (like address.city, posts.comments.author.first_nane), or composite field
7
+ # (like, address.city_or_posts.comments.author.city).
8
+ #
9
+ # For example, if we have 3 search parameters, {"serach":{"aaa":"aaa"}, {"bbb.ccc":"ccc"}, {"ddd.eee_or_ggg.hhh":"hhh"}}
10
+ # The psudo result sql statement would look like:
11
+ #
12
+ # select * from base_model
13
+ # where aaa = 'aaa'
14
+ #
15
+ # intersect
16
+ #
17
+ # select * from base_model
18
+ # join xxxxxx
19
+ # where ccc = 'ccc'
20
+ #
21
+ # intersect
22
+ #
23
+ # select * from
24
+ # (
25
+ # select * from base_model
26
+ # join xxxxxx
27
+ # where eee = 'ccc'
28
+ #
29
+ # union
30
+ #
31
+ # select * from base_model
32
+ # join xxxxxx
33
+ # where hhh= 'ccc'
34
+ # ) as union_result_1
35
+ #
1
36
  module RailsSimpleSearch
2
37
  module SqlHandler
3
38
  def init
@@ -10,213 +45,56 @@ module RailsSimpleSearch
10
45
  instance_eval(&pre_processor)
11
46
  end
12
47
 
13
- run_criteria
48
+ selection_group = generate_selection_group(@criteria)
14
49
 
15
- query = @model_class.joins(@joins_str)
16
- query = query.where(@condition_group.to_ar_condition) unless @condition_group.empty?
17
- query.select("distinct #{@model_class.table_name}.*")
18
- end
19
-
20
- private
21
-
22
- def text_column?(column)
23
- if column.respond_to?(:text?)
24
- column.text?
25
- elsif column.respond_to?(:type)
26
- column.type == :string || column.type == :text
27
- else
28
- raise 'encountered new version of Rails'
29
- end
30
- end
31
-
32
- def make_joins
33
- @joins_str = ''
34
- joins = @joins.values
35
- joins.sort! { |a, b| a[0] <=> b[0] }
36
- joins.each do |j|
37
- table = j[1]
38
- constrain = j[2]
39
- @joins_str << format(" inner join #{table} AS A%02d on #{constrain}", j[0])
40
- end
41
- end
42
-
43
- def run_criteria
44
- @condition_group = ConditionGroup.new
45
- @condition_group.set_relation(:and)
46
-
47
- @criteria.each do |key, value|
48
- @condition_group.add(parse_search_parameters(key, value))
49
- end
50
-
51
- make_joins
52
- end
53
-
54
- def build_single_condition(base_class, association_alias, field, value)
55
- field, operator = parse_field_name(field)
56
- table = base_class.table_name
57
- key = "#{table}.#{field}"
58
- final_key = "#{association_alias}.#{field}"
59
-
60
- column = base_class.columns_hash[field.to_s]
61
- return nil unless column
62
-
63
- if value.nil?
64
- verb = 'is'
65
- elsif value.is_a?(Array)
66
- verb = 'in'
67
- elsif operator
68
- verb = operator
69
- elsif text_column?(column) && ! @config[:exact_match].include?((@model_table_name == table) ? field : key)
70
- verb = 'like'
71
- value = "%#{value}%"
72
- else
73
- verb = '='
74
- end
75
-
76
- ConditionGroup.new(ConditionItem.new(final_key, verb, value))
77
- end
78
-
79
- def table_name_to_alias(table_name)
80
- format('A%02d', @joins[table_name][0])
81
- end
82
-
83
- def insert_join(base_class, asso_ref, new_asso_chain, poly_asso_type_class=nil)
84
- return if asso_ref.polymorphic? && poly_asso_type_class.blank?
85
- base_table = base_class.table_name
86
- asso_table = poly_asso_type_class&.table_name || asso_ref.klass.table_name
87
-
88
- @join_count ||= 0
89
- return if base_table == asso_table
90
- return unless @joins[asso_table].nil?
91
-
92
- @join_count += 1
93
- base_table_alias = new_asso_chain ? base_table : table_name_to_alias(base_table)
94
- asso_table_alias = format('A%02d', @join_count)
95
-
96
- if asso_ref.belongs_to?
97
- if asso_ref.polymorphic?
98
- join_cond = "#{base_table_alias}.#{asso_ref.foreign_key} = #{asso_table_alias}.#{poly_asso_type_class.primary_key}"
99
- join_cond = "#{base_table_alias}.#{asso_ref.foreign_type} = '#{poly_asso_type_class.name}' and #{join_cond}"
100
- else
101
- join_cond = "#{base_table_alias}.#{asso_ref.foreign_key} = #{asso_table_alias}.#{asso_ref.klass.primary_key}"
102
- end
50
+ raw_sql = selection_group.to_sql
51
+ if raw_sql.blank?
52
+ @model_class.all
103
53
  else
104
- join_cond = "#{base_table_alias}.#{base_class.primary_key} = #{asso_table_alias}.#{asso_ref.foreign_key}"
105
- join_cond = "#{asso_table_alias}.#{asso_ref.type} = '#{base_class.name}' and #{join_cond}" if asso_ref.type
54
+ @model_class.from("(#{raw_sql}) AS #{@model_table_name}")
106
55
  end
107
- @joins[asso_table] = [@join_count, asso_table, join_cond]
108
56
  end
109
57
 
110
- # This method parse a search parameter and its value
111
- # then produce a ConditionGroup
112
- def parse_search_parameters(attribute, value)
113
- # handle _or_ parameters
114
- attributes = attribute.split(@config[:or_separator])
115
- if attributes.size > 1
116
- cg = ConditionGroup.new
117
- cg.set_relation(:or)
118
- attributes.each do |a|
119
- cg.add(parse_search_parameters(a, value))
120
- end
121
- return cg
122
- end
123
-
124
- # handle direct fields
125
- unless attribute =~ /\./
126
- condition = build_single_condition(@model_class, @model_class.table_name, attribute, value)
127
- return condition
128
- end
129
-
130
- # handle association fields
131
- association_fields = attribute.split(/\./)
132
- field = association_fields.pop
58
+ private
133
59
 
134
- base_class = @model_class
135
- new_asso_chain = true
136
- while (current_association_string = association_fields.shift)
137
- # polymorphic association with solid target table_name
138
- # such as 'commentable:post'
139
- if current_association_string.include?(':')
140
- poly_asso_name, poly_asso_type = current_association_string.split(':')
141
- current_association = base_class.reflect_on_association(poly_asso_name.to_sym)
142
- poly_asso_type_class = poly_asso_type.downcase.camelize.constantize
143
- insert_join(base_class, current_association, new_asso_chain, poly_asso_type_class)
144
- base_class = poly_asso_type_class
60
+ def generate_selection_group(criteria)
61
+ selection_group = SelectionGroup.new
62
+ selection_group.relation(:and)
63
+
64
+ criteria.each do |key, value|
65
+ sg = SelectionGroup.new
66
+ fields = key.split(@config[:or_separator])
67
+ if fields.size > 1
68
+ # if the key is "or"ed by muiltiple fields
69
+ # we generate a SelectionGroup to include all the fields
70
+ sg.relation(:or)
71
+ fields.each do |f|
72
+ si = SelectionItem.new(@config, @model_class, f, value)
73
+ sg_si = SelectionGroup.new(si)
74
+ sg.add_child(sg_si)
75
+ end
145
76
  else
146
- current_association = base_class.reflect_on_association(current_association_string.to_sym)
147
- insert_join(base_class, current_association, new_asso_chain)
148
- base_class = current_association.klass
77
+ sg.add_item(SelectionItem.new(@config, @model_class, fields.first, value))
149
78
  end
150
79
 
151
- new_asso_chain = false
80
+ selection_group.add_child(sg)
152
81
  end
153
82
 
154
- association_alias = table_name_to_alias(base_class.table_name)
155
- build_single_condition(base_class, association_alias, field, value)
83
+ selection_group
156
84
  end
157
- end
158
-
159
- # This class holds a single condition
160
- class ConditionItem
161
- attr_reader :field, :verb, :value
162
-
163
- def initialize(field, verb, value)
164
- @field = field
165
- @verb = verb
166
- @value = value
167
- end
168
- end
169
85
 
170
- # This class holds a ConditionGroup
171
- # One ConditionGroup can hold one or more
172
- # conditions
173
- class ConditionGroup
174
- def initialize(item = nil)
175
- if item
176
- @condition_item = item
86
+ def text_column?(column)
87
+ if column.respond_to?(:text?)
88
+ column.text?
89
+ elsif column.respond_to?(:type)
90
+ column.type == :string || column.type == :text
177
91
  else
178
- @children = []
92
+ raise 'encountered new version of Rails'
179
93
  end
180
94
  end
181
95
 
182
- def add(condition_group)
183
- raise "It's not allowed to add child into leaf node" if leaf?
184
-
185
- @children << condition_group if condition_group
186
- end
187
-
188
- def set_relation(and_or)
189
- raise "It's not needed to set relation for leaf node" if leaf?
190
-
191
- @relation = and_or
192
- end
193
-
194
- def leaf?
195
- @condition_item ? true : false
196
- end
197
-
198
- def empty?
199
- @children && @children.empty? ? true : false
200
- end
201
-
202
- def to_ar_condition
203
- condition = []
204
- if leaf?
205
- i = @condition_item
206
- condition << "#{i.field} #{i.verb}"
207
- condition[0] << (i.verb == 'in' ? ' (?)' : ' ?')
208
- condition << i.value
209
- else
210
- tmp_conditions = @children.map(&:to_ar_condition)
211
- tmp_condition_str = tmp_conditions.map(&:first).join(" #{@relation} ")
212
- condition << "(#{tmp_condition_str})"
213
- tmp_conditions.each do |t|
214
- (1..(t.length - 1)).each do |index|
215
- condition << t[index]
216
- end
217
- end
218
- end
219
- condition
96
+ def table_name_to_alias(table_name)
97
+ format('A%02d', @joins[table_name][0])
220
98
  end
221
99
  end
222
100
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-simple-search
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yi Zhang
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-12-27 00:00:00.000000000 Z
11
+ date: 2024-09-01 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: rails-simple-search is a light and easy to use gem. It could help developers
14
14
  quickly build a search page.
@@ -19,6 +19,8 @@ extra_rdoc_files: []
19
19
  files:
20
20
  - README.md
21
21
  - lib/rails-simple-search.rb
22
+ - lib/selection_group.rb
23
+ - lib/selection_item.rb
22
24
  - lib/sql_handler.rb
23
25
  homepage: http://github.com/yzhanginwa/rails-simple-search
24
26
  licenses:
@@ -39,7 +41,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
39
41
  - !ruby/object:Gem::Version
40
42
  version: '0'
41
43
  requirements: []
42
- rubygems_version: 3.3.26
44
+ rubygems_version: 3.5.11
43
45
  signing_key:
44
46
  specification_version: 4
45
47
  summary: A very simple and light gem to quickly build search/filter function in rails