rails-simple-search 1.2.2 → 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: c6310535f7e545c8b29ca69421df5a8f23efca79dab7ccec6311ed00c632c51d
4
- data.tar.gz: 5379c314d28a7e06acf4e12ec23d78799e81b89843d1abf2717122e2f90cbbfc
3
+ metadata.gz: 2fcf45308630c8fda4c1b7ecce567017b0a77377060aff73ca41600405d2b8a5
4
+ data.tar.gz: 585040de107d0abf032daf209ad2cd83cfb45ec04ae3276dbb47730060270d2d
5
5
  SHA512:
6
- metadata.gz: 25829545f58e17b7234400719ef2707369cc2a5dbd608d3527c1aafd9a41cf7a5189e4f04857ea263887307faaaa66cce9850c8af024e450fd98095c95ea2989
7
- data.tar.gz: 47bc9c46c5c56017bc1b6c468e811987f1738e1f38c4379217d77f474c62c640bbf1d9e154d7eaa099905b3337f7819d937718964b441af2c6c87958eb169259
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,196 +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
50
+ raw_sql = selection_group.to_sql
51
+ if raw_sql.blank?
52
+ @model_class.all
27
53
  else
28
- raise 'encountered new version of Rails'
54
+ @model_class.from("(#{raw_sql}) AS #{@model_table_name}")
29
55
  end
30
56
  end
31
57
 
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
58
+ private
42
59
 
43
- def run_criteria
44
- @condition_group = ConditionGroup.new
45
- @condition_group.set_relation(:and)
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
76
+ else
77
+ sg.add_item(SelectionItem.new(@config, @model_class, fields.first, value))
78
+ end
46
79
 
47
- @criteria.each do |key, value|
48
- @condition_group.add(parse_search_parameters(key, value))
80
+ selection_group.add_child(sg)
49
81
  end
50
82
 
51
- make_joins
83
+ selection_group
52
84
  end
53
85
 
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}%"
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
72
91
  else
73
- verb = '='
92
+ raise 'encountered new version of Rails'
74
93
  end
75
-
76
- ConditionGroup.new(ConditionItem.new(final_key, verb, value))
77
94
  end
78
95
 
79
96
  def table_name_to_alias(table_name)
80
97
  format('A%02d', @joins[table_name][0])
81
98
  end
82
-
83
- def insert_join(base_class, asso_ref, new_asso_chain)
84
- base_table = base_class.table_name
85
- asso_table = asso_ref.klass.table_name
86
-
87
- @join_count ||= 0
88
- return if base_table == asso_table
89
- return unless @joins[asso_table].nil?
90
-
91
- @join_count += 1
92
- base_table_alias = new_asso_chain ? base_table : table_name_to_alias(base_table)
93
- asso_table_alias = format('A%02d', @join_count)
94
-
95
- if asso_ref.belongs_to?
96
- @joins[asso_table] =[@join_count, asso_table, "#{base_table_alias}.#{asso_ref.foreign_key} = #{asso_table_alias}.#{asso_ref.klass.primary_key}"]
97
- else
98
- join_cond = "#{base_table_alias}.#{base_class.primary_key} = #{asso_table_alias}.#{asso_ref.foreign_key}"
99
- join_cond = "#{asso_table_alias}.#{asso_ref.type} = '#{base_class.name}' and #{join_cond}" if asso_ref.type
100
- @joins[asso_table] = [@join_count, asso_table, join_cond]
101
- end
102
- end
103
-
104
- # This method parse a search parameter and its value
105
- # then produce a ConditionGroup
106
- def parse_search_parameters(attribute, value)
107
- # handle _or_ parameters
108
- attributes = attribute.split(@config[:or_separator])
109
- if attributes.size > 1
110
- cg = ConditionGroup.new
111
- cg.set_relation(:or)
112
- attributes.each do |a|
113
- cg.add(parse_search_parameters(a, value))
114
- end
115
- return cg
116
- end
117
-
118
- # handle direct fields
119
- unless attribute =~ /\./
120
- condition = build_single_condition(@model_class, @model_class.table_name, attribute, value)
121
- return condition
122
- end
123
-
124
- # handle association fields
125
- association_fields = attribute.split(/\./)
126
- field = association_fields.pop
127
-
128
- base_class = @model_class
129
- new_asso_chain = true
130
- until association_fields.empty?
131
- association_fields[0] = base_class.reflect_on_association(association_fields[0].to_sym)
132
- insert_join(base_class, association_fields[0], new_asso_chain)
133
- new_asso_chain = false
134
- base_class = association_fields.shift.klass
135
- end
136
-
137
- association_alias = table_name_to_alias(base_class.table_name)
138
- build_single_condition(base_class, association_alias, field, value)
139
- end
140
- end
141
-
142
- # This class holds a single condition
143
- class ConditionItem
144
- attr_reader :field, :verb, :value
145
-
146
- def initialize(field, verb, value)
147
- @field = field
148
- @verb = verb
149
- @value = value
150
- end
151
- end
152
-
153
- # This class holds a ConditionGroup
154
- # One ConditionGroup can hold one or more
155
- # conditions
156
- class ConditionGroup
157
- def initialize(item = nil)
158
- if item
159
- @condition_item = item
160
- else
161
- @children = []
162
- end
163
- end
164
-
165
- def add(condition_group)
166
- raise "It's not allowed to add child into leaf node" if leaf?
167
-
168
- @children << condition_group if condition_group
169
- end
170
-
171
- def set_relation(and_or)
172
- raise "It's not needed to set relation for leaf node" if leaf?
173
-
174
- @relation = and_or
175
- end
176
-
177
- def leaf?
178
- @condition_item ? true : false
179
- end
180
-
181
- def empty?
182
- @children && @children.empty? ? true : false
183
- end
184
-
185
- def to_ar_condition
186
- condition = []
187
- if leaf?
188
- i = @condition_item
189
- condition << "#{i.field} #{i.verb}"
190
- condition[0] << (i.verb == 'in' ? ' (?)' : ' ?')
191
- condition << i.value
192
- else
193
- tmp_conditions = @children.map(&:to_ar_condition)
194
- tmp_condition_str = tmp_conditions.map(&:first).join(" #{@relation} ")
195
- condition << "(#{tmp_condition_str})"
196
- tmp_conditions.each do |t|
197
- (1..(t.length - 1)).each do |index|
198
- condition << t[index]
199
- end
200
- end
201
- end
202
- condition
203
- end
204
99
  end
205
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.2.2
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