thinking-sphinx 2.0.5 → 2.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. data/README.textile +7 -1
  2. data/features/searching_by_model.feature +24 -30
  3. data/features/step_definitions/common_steps.rb +5 -5
  4. data/features/thinking_sphinx/db/.gitignore +1 -0
  5. data/features/thinking_sphinx/db/fixtures/post_keywords.txt +1 -0
  6. data/spec/fixtures/data.sql +32 -0
  7. data/spec/fixtures/database.yml.default +3 -0
  8. data/spec/fixtures/models.rb +161 -0
  9. data/spec/fixtures/structure.sql +146 -0
  10. data/spec/spec_helper.rb +62 -0
  11. data/spec/sphinx_helper.rb +61 -0
  12. data/spec/support/rails.rb +18 -0
  13. data/spec/thinking_sphinx/active_record/delta_spec.rb +24 -24
  14. data/spec/thinking_sphinx/active_record/has_many_association_spec.rb +27 -0
  15. data/spec/thinking_sphinx/active_record/scopes_spec.rb +25 -25
  16. data/spec/thinking_sphinx/active_record_spec.rb +108 -107
  17. data/spec/thinking_sphinx/adapters/abstract_adapter_spec.rb +38 -38
  18. data/spec/thinking_sphinx/association_spec.rb +69 -35
  19. data/spec/thinking_sphinx/context_spec.rb +61 -64
  20. data/spec/thinking_sphinx/search_spec.rb +7 -0
  21. data/spec/thinking_sphinx_spec.rb +47 -46
  22. metadata +49 -141
  23. data/VERSION +0 -1
  24. data/lib/cucumber/thinking_sphinx/external_world.rb +0 -12
  25. data/lib/cucumber/thinking_sphinx/internal_world.rb +0 -127
  26. data/lib/cucumber/thinking_sphinx/sql_logger.rb +0 -20
  27. data/lib/thinking-sphinx.rb +0 -1
  28. data/lib/thinking_sphinx.rb +0 -301
  29. data/lib/thinking_sphinx/action_controller.rb +0 -31
  30. data/lib/thinking_sphinx/active_record.rb +0 -384
  31. data/lib/thinking_sphinx/active_record/attribute_updates.rb +0 -52
  32. data/lib/thinking_sphinx/active_record/delta.rb +0 -65
  33. data/lib/thinking_sphinx/active_record/has_many_association.rb +0 -36
  34. data/lib/thinking_sphinx/active_record/has_many_association_with_scopes.rb +0 -21
  35. data/lib/thinking_sphinx/active_record/log_subscriber.rb +0 -61
  36. data/lib/thinking_sphinx/active_record/scopes.rb +0 -93
  37. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +0 -87
  38. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +0 -62
  39. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +0 -157
  40. data/lib/thinking_sphinx/association.rb +0 -219
  41. data/lib/thinking_sphinx/attribute.rb +0 -396
  42. data/lib/thinking_sphinx/auto_version.rb +0 -38
  43. data/lib/thinking_sphinx/bundled_search.rb +0 -44
  44. data/lib/thinking_sphinx/class_facet.rb +0 -20
  45. data/lib/thinking_sphinx/configuration.rb +0 -339
  46. data/lib/thinking_sphinx/context.rb +0 -76
  47. data/lib/thinking_sphinx/core/string.rb +0 -15
  48. data/lib/thinking_sphinx/deltas.rb +0 -28
  49. data/lib/thinking_sphinx/deltas/default_delta.rb +0 -62
  50. data/lib/thinking_sphinx/deploy/capistrano.rb +0 -101
  51. data/lib/thinking_sphinx/excerpter.rb +0 -23
  52. data/lib/thinking_sphinx/facet.rb +0 -128
  53. data/lib/thinking_sphinx/facet_search.rb +0 -170
  54. data/lib/thinking_sphinx/field.rb +0 -98
  55. data/lib/thinking_sphinx/index.rb +0 -157
  56. data/lib/thinking_sphinx/index/builder.rb +0 -312
  57. data/lib/thinking_sphinx/index/faux_column.rb +0 -118
  58. data/lib/thinking_sphinx/join.rb +0 -37
  59. data/lib/thinking_sphinx/property.rb +0 -185
  60. data/lib/thinking_sphinx/railtie.rb +0 -46
  61. data/lib/thinking_sphinx/search.rb +0 -972
  62. data/lib/thinking_sphinx/search_methods.rb +0 -439
  63. data/lib/thinking_sphinx/sinatra.rb +0 -7
  64. data/lib/thinking_sphinx/source.rb +0 -194
  65. data/lib/thinking_sphinx/source/internal_properties.rb +0 -51
  66. data/lib/thinking_sphinx/source/sql.rb +0 -157
  67. data/lib/thinking_sphinx/tasks.rb +0 -130
  68. data/lib/thinking_sphinx/test.rb +0 -55
  69. data/tasks/distribution.rb +0 -33
  70. data/tasks/testing.rb +0 -80
@@ -1,219 +0,0 @@
1
- module ThinkingSphinx
2
- # Association tracks a specific reflection and join to reference data that
3
- # isn't in the base model. Very much an internal class for Thinking Sphinx -
4
- # perhaps because I feel it's not as strong (or simple) as most of the rest.
5
- #
6
- class Association
7
- attr_accessor :parent, :reflection, :join
8
-
9
- # Create a new association by passing in the parent association, and the
10
- # corresponding reflection instance. If there is no parent, pass in nil.
11
- #
12
- # top = Association.new nil, top_reflection
13
- # child = Association.new top, child_reflection
14
- #
15
- def initialize(parent, reflection)
16
- @parent, @reflection = parent, reflection
17
- @children = {}
18
- end
19
-
20
- # Get the children associations for a given association name. The only time
21
- # that there'll actually be more than one association is when the
22
- # relationship is polymorphic. To keep things simple though, it will always
23
- # be an Array that gets returned (an empty one if no matches).
24
- #
25
- # # where pages is an association on the class tied to the reflection.
26
- # association.children(:pages)
27
- #
28
- def children(assoc)
29
- @children[assoc] ||= Association.children(@reflection.klass, assoc, self)
30
- end
31
-
32
- # Get the children associations for a given class, association name and
33
- # parent association. Much like the instance method of the same name, it
34
- # will return an empty array if no associations have the name, and only
35
- # have multiple association instances if the underlying relationship is
36
- # polymorphic.
37
- #
38
- # Association.children(User, :pages, user_association)
39
- #
40
- def self.children(klass, assoc, parent=nil)
41
- ref = klass.reflect_on_association(assoc)
42
-
43
- return [] if ref.nil?
44
- return [Association.new(parent, ref)] unless ref.options[:polymorphic]
45
-
46
- # association is polymorphic - create associations for each
47
- # non-polymorphic reflection.
48
- polymorphic_classes(ref).collect { |poly_class|
49
- reflection = depolymorphic_reflection(ref, poly_class)
50
- klass.reflections[reflection.name] = reflection
51
- Association.new parent, reflection
52
- }
53
- end
54
-
55
- # Link up the join for this model from a base join - and set parent
56
- # associations' joins recursively.
57
- #
58
- def join_to(base_join)
59
- parent.join_to(base_join) if parent && parent.join.nil?
60
-
61
- @join ||= join_association_class.new(
62
- @reflection, base_join, parent ? parent.join : join_parent(base_join)
63
- )
64
- end
65
-
66
- def arel_join
67
- @join.join_type = Arel::OuterJoin
68
- rewrite_conditions
69
-
70
- @join
71
- end
72
-
73
- # Returns true if the association - or a parent - is a has_many or
74
- # has_and_belongs_to_many.
75
- #
76
- def is_many?
77
- case @reflection.macro
78
- when :has_many, :has_and_belongs_to_many
79
- true
80
- else
81
- @parent ? @parent.is_many? : false
82
- end
83
- end
84
-
85
- # Returns an array of all the associations that lead to this one - starting
86
- # with the top level all the way to the current association object.
87
- #
88
- def ancestors
89
- (parent ? parent.ancestors : []) << self
90
- end
91
-
92
- def has_column?(column)
93
- @reflection.klass.column_names.include?(column.to_s)
94
- end
95
-
96
- def primary_key_from_reflection
97
- if @reflection.options[:through]
98
- @reflection.source_reflection.options[:foreign_key] ||
99
- @reflection.source_reflection.primary_key_name
100
- elsif @reflection.macro == :has_and_belongs_to_many
101
- @reflection.association_foreign_key
102
- else
103
- nil
104
- end
105
- end
106
-
107
- def table
108
- if @reflection.options[:through] ||
109
- @reflection.macro == :has_and_belongs_to_many
110
- @join.aliased_join_table_name
111
- else
112
- @join.aliased_table_name
113
- end
114
- end
115
-
116
- private
117
-
118
- def self.depolymorphic_reflection(reflection, klass)
119
- ::ActiveRecord::Reflection::AssociationReflection.new(
120
- reflection.macro,
121
- "#{reflection.name}_#{klass.name}".to_sym,
122
- casted_options(klass, reflection),
123
- reflection.active_record
124
- )
125
- end
126
-
127
- # Returns all the objects that could be currently instantiated from a
128
- # polymorphic association. This is pretty damn fast if there's an index on
129
- # the foreign type column - but if there isn't, it can take a while if you
130
- # have a lot of data.
131
- #
132
- def self.polymorphic_classes(ref)
133
- ref.active_record.connection.select_all(
134
- "SELECT DISTINCT #{ref.options[:foreign_type]} " +
135
- "FROM #{ref.active_record.table_name} " +
136
- "WHERE #{ref.options[:foreign_type]} IS NOT NULL"
137
- ).collect { |row|
138
- row[ref.options[:foreign_type]].constantize
139
- }
140
- end
141
-
142
- # Returns a new set of options for an association that mimics an existing
143
- # polymorphic relationship for a specific class. It adds a condition to
144
- # filter by the appropriate object.
145
- #
146
- def self.casted_options(klass, ref)
147
- options = ref.options.clone
148
- options[:polymorphic] = nil
149
- options[:class_name] = klass.name
150
- options[:foreign_key] ||= "#{ref.name}_id"
151
-
152
- quoted_foreign_type = klass.connection.quote_column_name ref.options[:foreign_type]
153
- case options[:conditions]
154
- when nil
155
- options[:conditions] = "::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
156
- when Array
157
- options[:conditions] << "::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
158
- when Hash
159
- options[:conditions].merge!(ref.options[:foreign_type] => klass.name)
160
- else
161
- options[:conditions] << " AND ::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
162
- end
163
-
164
- options
165
- end
166
-
167
- def join_association_class
168
- if rails_3_1?
169
- ::ActiveRecord::Associations::JoinDependency::JoinAssociation
170
- else
171
- ::ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation
172
- end
173
- end
174
-
175
- def join_parent(join)
176
- if rails_3_1?
177
- join.join_parts.first
178
- else
179
- join.joins.first
180
- end
181
- end
182
-
183
- def rails_3_1?
184
- ::ActiveRecord::Associations.constants.include?(:JoinDependency) ||
185
- ::ActiveRecord::Associations.constants.include?('JoinDependency')
186
- end
187
-
188
- def rewrite_conditions
189
- @join.options[:conditions] = case @join.options[:conditions]
190
- when String
191
- rewrite_condition @join.options[:conditions]
192
- when Array
193
- @join.options[:conditions].collect { |condition|
194
- rewrite_condition condition
195
- }
196
- else
197
- @join.options[:conditions]
198
- end
199
- end
200
-
201
- def rewrite_condition(condition)
202
- return condition unless condition.is_a?(String)
203
-
204
- if defined?(ActsAsTaggableOn) &&
205
- @reflection.klass == ActsAsTaggableOn::Tagging &&
206
- @reflection.name.to_s[/_taggings$/]
207
- condition = condition.gsub /taggings\./, "#{quoted_alias @join}."
208
- end
209
-
210
- condition.gsub /::ts_join_alias::/, quoted_alias(@join.parent)
211
- end
212
-
213
- def quoted_alias(join)
214
- @reflection.klass.connection.quote_table_name(
215
- join.aliased_table_name
216
- )
217
- end
218
- end
219
- end
@@ -1,396 +0,0 @@
1
- module ThinkingSphinx
2
- # Attributes - eternally useful when it comes to filtering, sorting or
3
- # grouping. This class isn't really useful to you unless you're hacking
4
- # around with the internals of Thinking Sphinx - but hey, don't let that
5
- # stop you.
6
- #
7
- # One key thing to remember - if you're using the attribute manually to
8
- # generate SQL statements, you'll need to set the base model, and all the
9
- # associations. Which can get messy. Use Index.link!, it really helps.
10
- #
11
- class Attribute < ThinkingSphinx::Property
12
- attr_accessor :query_source
13
-
14
- SphinxTypeMappings = {
15
- :multi => :sql_attr_multi,
16
- :datetime => :sql_attr_timestamp,
17
- :string => :sql_attr_str2ordinal,
18
- :float => :sql_attr_float,
19
- :boolean => :sql_attr_bool,
20
- :integer => :sql_attr_uint,
21
- :bigint => :sql_attr_bigint,
22
- :wordcount => :sql_attr_str2wordcount
23
- }
24
-
25
- if Riddle.loaded_version.to_i > 1
26
- SphinxTypeMappings[:string] = :sql_attr_string
27
- end
28
-
29
- # To create a new attribute, you'll need to pass in either a single Column
30
- # or an array of them, and some (optional) options.
31
- #
32
- # Valid options are:
33
- # - :as => :alias_name
34
- # - :type => :attribute_type
35
- # - :source => :field, :query, :ranged_query
36
- #
37
- # Alias is only required in three circumstances: when there's
38
- # another attribute or field with the same name, when the column name is
39
- # 'id', or when there's more than one column.
40
- #
41
- # Type is not required, unless you want to force a column to be a certain
42
- # type (but keep in mind the value will not be CASTed in the SQL
43
- # statements). The only time you really need to use this is when the type
44
- # can't be figured out by the column - ie: when not actually using a
45
- # database column as your source.
46
- #
47
- # Source is only used for multi-value attributes (MVA). By default this will
48
- # use a left-join and a group_concat to obtain the values. For better performance
49
- # during indexing it can be beneficial to let Sphinx use a separate query to retrieve
50
- # all document,value-pairs.
51
- # Either :query or :ranged_query will enable this feature, where :ranged_query will cause
52
- # the query to be executed incremental.
53
- #
54
- # Example usage:
55
- #
56
- # Attribute.new(
57
- # Column.new(:created_at)
58
- # )
59
- #
60
- # Attribute.new(
61
- # Column.new(:posts, :id),
62
- # :as => :post_ids
63
- # )
64
- #
65
- # Attribute.new(
66
- # Column.new(:posts, :id),
67
- # :as => :post_ids,
68
- # :source => :ranged_query
69
- # )
70
- #
71
- # Attribute.new(
72
- # [Column.new(:pages, :id), Column.new(:articles, :id)],
73
- # :as => :content_ids
74
- # )
75
- #
76
- # Attribute.new(
77
- # Column.new("NOW()"),
78
- # :as => :indexed_at,
79
- # :type => :datetime
80
- # )
81
- #
82
- # If you're creating attributes for latitude and longitude, don't forget
83
- # that Sphinx expects these values to be in radians.
84
- #
85
- def initialize(source, columns, options = {})
86
- super
87
-
88
- @type = options[:type]
89
- @query_source = options[:source]
90
- @crc = options[:crc]
91
-
92
- @type ||= :multi unless @query_source.nil?
93
- if @type == :string && @crc
94
- @type = is_many? ? :multi : :integer
95
- end
96
-
97
- source.attributes << self
98
- end
99
-
100
- # Get the part of the SELECT clause related to this attribute. Don't forget
101
- # to set your model and associations first though.
102
- #
103
- # This will concatenate strings and arrays of integers, and convert
104
- # datetimes to timestamps, as needed.
105
- #
106
- def to_select_sql
107
- return nil unless include_as_association? && available?
108
-
109
- separator = all_ints? || all_datetimes? || @crc ? ',' : ' '
110
-
111
- clause = columns_with_prefixes.collect { |column|
112
- case type
113
- when :string
114
- adapter.convert_nulls(column)
115
- when :datetime
116
- adapter.cast_to_datetime(column)
117
- when :multi
118
- column = adapter.cast_to_datetime(column) if is_many_datetimes?
119
- column = adapter.convert_nulls(column, '0') if is_many_ints?
120
- column
121
- else
122
- column
123
- end
124
- }.join(', ')
125
-
126
- clause = adapter.crc(clause) if @crc
127
- clause = adapter.concatenate(clause, separator) if concat_ws?
128
- clause = adapter.group_concatenate(clause, separator) if is_many?
129
- clause = adapter.downcase(clause) if insensitive?
130
-
131
- "#{clause} AS #{quote_column(unique_name)}"
132
- end
133
-
134
- def type_to_config
135
- SphinxTypeMappings[type]
136
- end
137
-
138
- def include_as_association?
139
- ! (type == :multi && (query_source == :query || query_source == :ranged_query))
140
- end
141
-
142
- # Returns the configuration value that should be used for
143
- # the attribute.
144
- # Special case is the multi-valued attribute that needs some
145
- # extra configuration.
146
- #
147
- def config_value(offset = nil, delta = false)
148
- if type == :multi
149
- multi_config = include_as_association? ? "field" :
150
- source_value(offset, delta).gsub(/\s+/m, " ").strip
151
- "uint #{unique_name} from #{multi_config}"
152
- else
153
- unique_name
154
- end
155
- end
156
-
157
- # Returns the type of the column. If that's not already set, it returns
158
- # :multi if there's the possibility of more than one value, :string if
159
- # there's more than one association, otherwise it figures out what the
160
- # actual column's datatype is and returns that.
161
- #
162
- def type
163
- @type ||= begin
164
- base_type = case
165
- when is_many?, is_many_ints?
166
- :multi
167
- when @associations.values.flatten.length > 1
168
- :string
169
- else
170
- translated_type_from_database
171
- end
172
-
173
- if base_type == :string && @crc
174
- base_type = :integer
175
- else
176
- @crc = false unless base_type == :multi && is_many_strings? && @crc
177
- end
178
-
179
- base_type
180
- end
181
- end
182
-
183
- def updatable?
184
- [:integer, :datetime, :boolean].include?(type) && !is_string?
185
- end
186
-
187
- def live_value(instance)
188
- object = instance
189
- column = @columns.first
190
- column.__stack.each { |method|
191
- object = object.send(method)
192
- return sphinx_value(nil) if object.nil?
193
- }
194
-
195
- sphinx_value object.send(column.__name)
196
- end
197
-
198
- def all_ints?
199
- all_of_type?(:integer)
200
- end
201
-
202
- def all_datetimes?
203
- all_of_type?(:datetime, :date, :timestamp)
204
- end
205
-
206
- def all_strings?
207
- all_of_type?(:string, :text)
208
- end
209
-
210
- private
211
-
212
- def source_value(offset, delta)
213
- if is_string?
214
- return "#{query_source.to_s.dasherize}; #{columns.first.__name}"
215
- end
216
-
217
- query = query(offset)
218
-
219
- if query_source == :ranged_query
220
- query += query_clause
221
- query += " AND #{query_delta.strip}" if delta
222
- "ranged-query; #{query}; #{range_query}"
223
- else
224
- query += " WHERE #{query_delta.strip}" if delta
225
- "query; #{query}"
226
- end
227
- end
228
-
229
- def query(offset)
230
- base_assoc = base_association_for_mva
231
- end_assoc = end_association_for_mva
232
- raise "Could not determine SQL for MVA" if base_assoc.nil?
233
-
234
- relation = Arel::Table.new(base_assoc.table)
235
-
236
- association_joins.each do |join|
237
- relation = relation.join(join.relation, Arel::OuterJoin).
238
- on(*join.association_join)
239
- end
240
-
241
- relation = relation.project "#{foreign_key_for_mva base_assoc} #{ThinkingSphinx.unique_id_expression(adapter, offset)} AS #{quote_column('id')}, #{primary_key_for_mva(end_assoc)} AS #{quote_column(unique_name)}"
242
-
243
- relation.to_sql
244
- end
245
-
246
- def query_clause
247
- foreign_key = foreign_key_for_mva base_association_for_mva
248
- " WHERE #{foreign_key} >= $start AND #{foreign_key} <= $end"
249
- end
250
-
251
- def query_delta
252
- foreign_key = foreign_key_for_mva base_association_for_mva
253
- <<-SQL
254
- #{foreign_key} IN (SELECT #{quote_column model.primary_key}
255
- FROM #{model.quoted_table_name}
256
- WHERE #{@source.index.delta_object.clause(model, true)})
257
- SQL
258
- end
259
-
260
- def range_query
261
- assoc = base_association_for_mva
262
- foreign_key = foreign_key_for_mva assoc
263
- "SELECT MIN(#{foreign_key}), MAX(#{foreign_key}) FROM #{quote_table_name assoc.table}"
264
- end
265
-
266
- def primary_key_for_mva(assoc)
267
- quote_with_table(
268
- assoc.table, assoc.primary_key_from_reflection || columns.first.__name
269
- )
270
- end
271
-
272
- def foreign_key_for_mva(assoc)
273
- quote_with_table assoc.table, assoc.reflection.primary_key_name
274
- end
275
-
276
- def end_association_for_mva
277
- @association_for_mva ||= associations[columns.first].detect { |assoc|
278
- assoc.has_column?(columns.first.__name)
279
- }
280
- end
281
-
282
- def base_association_for_mva
283
- @first_association_for_mva ||= begin
284
- assoc = end_association_for_mva
285
- while !assoc.parent.nil?
286
- assoc = assoc.parent
287
- end
288
-
289
- assoc
290
- end
291
- end
292
-
293
- def association_joins
294
- joins = []
295
- assoc = end_association_for_mva
296
- while assoc != base_association_for_mva
297
- joins << assoc.join
298
- assoc = assoc.parent
299
- end
300
-
301
- joins
302
- end
303
-
304
- def is_many_ints?
305
- concat_ws? && all_ints?
306
- end
307
-
308
- def is_many_datetimes?
309
- is_many? && all_datetimes?
310
- end
311
-
312
- def is_many_strings?
313
- is_many? && all_strings?
314
- end
315
-
316
- def translated_type_from_database
317
- case type_from_db = type_from_database
318
- when :integer
319
- integer_type_from_db
320
- when :datetime, :string, :float, :boolean
321
- type_from_db
322
- when :decimal
323
- :float
324
- when :timestamp, :date
325
- :datetime
326
- else
327
- raise <<-MESSAGE
328
-
329
- Cannot automatically map attribute #{unique_name} in #{@model.name} to an
330
- equivalent Sphinx type (integer, float, boolean, datetime, string as ordinal).
331
- You could try to explicitly convert the column's value in your define_index
332
- block:
333
- has "CAST(column AS INT)", :type => :integer, :as => :column
334
- MESSAGE
335
- end
336
- end
337
-
338
- def type_from_database
339
- column = column_from_db
340
- column.nil? ? nil : column.type
341
- end
342
-
343
- def integer_type_from_db
344
- column = column_from_db
345
- return nil if column.nil?
346
-
347
- case column.sql_type
348
- when adapter.bigint_pattern
349
- :bigint
350
- else
351
- :integer
352
- end
353
- end
354
-
355
- def column_from_db
356
- klass = @associations.values.flatten.first ?
357
- @associations.values.flatten.first.reflection.klass : @model
358
-
359
- klass.columns.detect { |col|
360
- @columns.collect { |c| c.__name.to_s }.include? col.name
361
- }
362
- end
363
-
364
- def all_of_type?(*column_types)
365
- @columns.all? { |col|
366
- klasses = @associations[col].empty? ? [@model] :
367
- @associations[col].collect { |assoc| assoc.reflection.klass }
368
- klasses.all? { |klass|
369
- column = klass.columns.detect { |column| column.name == col.__name.to_s }
370
- !column.nil? && column_types.include?(column.type)
371
- }
372
- }
373
- end
374
-
375
- def sphinx_value(value)
376
- case value
377
- when TrueClass
378
- 1
379
- when FalseClass, NilClass
380
- 0
381
- when Time
382
- value.to_i
383
- when Date
384
- value.to_time.to_i
385
- when String
386
- value.to_crc32
387
- else
388
- value
389
- end
390
- end
391
-
392
- def insensitive?
393
- @sortable == :insensitive
394
- end
395
- end
396
- end