DrMark-thinking-sphinx 0.9.8 → 0.9.9

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.
data/README CHANGED
@@ -54,4 +54,12 @@ Since I first released this library, there's been quite a few people who have su
54
54
  - Andrew Bennett
55
55
  - Jordan Fowler
56
56
  - Seth Walker
57
- - Joe Noon
57
+ - Joe Noon
58
+ - Wolfgang Postler
59
+ - Rick Olson
60
+ - Killian Murphy
61
+ - Morten Primdahl
62
+ - Ryan Bates
63
+ - David Eisinger
64
+ - Shay Arnett
65
+ - Minh Tran
@@ -73,7 +73,17 @@ module ThinkingSphinx
73
73
  # if running in the test environment.
74
74
  #
75
75
  def index_delta
76
- return true unless ThinkingSphinx.deltas_enabled?
76
+ return true unless ThinkingSphinx.updates_enabled? &&
77
+ ThinkingSphinx.deltas_enabled?
78
+
79
+ config = ThinkingSphinx::Configuration.new
80
+ client = Riddle::Client.new config.address, config.port
81
+
82
+ client.update(
83
+ "#{self.class.indexes.first.name}_core",
84
+ ['sphinx_deleted'],
85
+ {self.id => 1}
86
+ ) if self.in_core_index?
77
87
 
78
88
  configuration = ThinkingSphinx::Configuration.new
79
89
  system "indexer --config #{configuration.config_file} --rotate #{self.class.indexes.first.name}_delta"
@@ -28,7 +28,14 @@ module ThinkingSphinx
28
28
  args << options
29
29
  ThinkingSphinx::Search.search(*args)
30
30
  end
31
-
31
+
32
+ def search_count(*args)
33
+ options = args.extract_options!
34
+ options[:class] = self
35
+ args << options
36
+ ThinkingSphinx::Search.count(*args)
37
+ end
38
+
32
39
  def search_for_id(*args)
33
40
  options = args.extract_options!
34
41
  options[:class] = self
@@ -99,6 +99,10 @@ module ThinkingSphinx
99
99
  end
100
100
  result ^ 0xFFFFFFFF
101
101
  end
102
+
103
+ def to_crc32s
104
+ (subclasses << self).collect { |klass| klass.to_crc32 }
105
+ end
102
106
  end
103
107
  end
104
108
 
@@ -118,6 +122,8 @@ module ThinkingSphinx
118
122
  end
119
123
 
120
124
  def toggle_deleted
125
+ return unless ThinkingSphinx.updates_enabled?
126
+
121
127
  config = ThinkingSphinx::Configuration.new
122
128
  client = Riddle::Client.new config.address, config.port
123
129
 
@@ -95,6 +95,10 @@ module ThinkingSphinx
95
95
  (parent ? parent.ancestors : []) << self
96
96
  end
97
97
 
98
+ def has_column?(column)
99
+ @reflection.klass.column_names.include?(column.to_s)
100
+ end
101
+
98
102
  private
99
103
 
100
104
  # Returns all the objects that could be currently instantiated from a
@@ -230,9 +230,11 @@ module ThinkingSphinx
230
230
  "#{@model.quoted_table_name}.#{quote_column(column.__name)}"
231
231
  else
232
232
  associations[column].collect { |assoc|
233
+ assoc.has_column?(column.__name) ?
233
234
  "#{@model.connection.quote_table_name(assoc.join.aliased_table_name)}" +
234
- ".#{quote_column(column.__name)}"
235
- }.join(', ')
235
+ ".#{quote_column(column.__name)}" :
236
+ nil
237
+ }.compact.join(', ')
236
238
  end
237
239
  end
238
240
 
@@ -0,0 +1,23 @@
1
+ module ThinkingSphinx
2
+ class Collection < ::Array
3
+ attr_reader :total_entries, :total_pages, :current_page
4
+
5
+ def initialize(page, per_page, entries, total_entries)
6
+ @current_page, @per_page, @total_entries = page, per_page, total_entries
7
+
8
+ @total_pages = (entries / @per_page.to_f).ceil
9
+ end
10
+
11
+ def previous_page
12
+ current_page > 1 ? (current_page - 1) : nil
13
+ end
14
+
15
+ def next_page
16
+ current_page < total_pages ? (current_page + 1): nil
17
+ end
18
+
19
+ def offset
20
+ (current_page - 1) * @per_page
21
+ end
22
+ end
23
+ end
@@ -29,10 +29,9 @@ module ThinkingSphinx
29
29
  # config/sphinx.yml with settings for each environment, in a similar
30
30
  # fashion to database.yml - using the following keys: config_file,
31
31
  # searchd_log_file, query_log_file, pid_file, searchd_file_path, port,
32
- # allow_star, min_prefix_len, min_infix_len, mem_limit, max_matches,
33
- # morphology, charset_type, charset_table, ignore_chars, html_strip,
34
- # html_remove_elements. I think you've got
35
- # the idea.
32
+ # allow_star, enable_star, min_prefix_len, min_infix_len, mem_limit,
33
+ # max_matches, # morphology, charset_type, charset_table, ignore_chars,
34
+ # html_strip, # html_remove_elements. I think you've got the idea.
36
35
  #
37
36
  # Each setting in the YAML file is optional - so only put in the ones you
38
37
  # want to change.
@@ -43,10 +42,10 @@ module ThinkingSphinx
43
42
  #
44
43
  class Configuration
45
44
  attr_accessor :config_file, :searchd_log_file, :query_log_file,
46
- :pid_file, :searchd_file_path, :address, :port, :allow_star,
47
- :min_prefix_len, :min_infix_len, :mem_limit, :max_matches, :morphology,
48
- :charset_type, :charset_table, :ignore_chars, :html_strip,
49
- :html_remove_elements, :app_root
45
+ :pid_file, :searchd_file_path, :address, :port, :enable_star,
46
+ :allow_star, :min_prefix_len, :min_infix_len, :mem_limit, :max_matches,
47
+ :morphology, :charset_type, :charset_table, :ignore_chars, :html_strip,
48
+ :html_remove_elements, :database_yml_file, :app_root
50
49
 
51
50
  attr_reader :environment
52
51
 
@@ -58,6 +57,7 @@ module ThinkingSphinx
58
57
  self.app_root = Merb.root if defined?(Merb)
59
58
  self.app_root ||= app_root
60
59
 
60
+ self.database_yml_file = "#{self.app_root}/config/database.yml"
61
61
  self.config_file = "#{self.app_root}/config/#{environment}.sphinx.conf"
62
62
  self.searchd_log_file = "#{self.app_root}/log/searchd.log"
63
63
  self.query_log_file = "#{self.app_root}/log/searchd.query.log"
@@ -66,8 +66,9 @@ module ThinkingSphinx
66
66
  self.address = "127.0.0.1"
67
67
  self.port = 3312
68
68
  self.allow_star = false
69
- self.min_prefix_len = 1
70
- self.min_infix_len = 1
69
+ self.enable_star = false
70
+ self.min_prefix_len = nil
71
+ self.min_infix_len = nil
71
72
  self.mem_limit = "64M"
72
73
  self.max_matches = 1000
73
74
  self.morphology = "stem_en"
@@ -95,9 +96,8 @@ module ThinkingSphinx
95
96
  # indexer and searchd configuration, and sources and indexes details.
96
97
  #
97
98
  def build(file_path=nil)
98
- load_models
99
99
  file_path ||= "#{self.config_file}"
100
- database_confs = YAML::load(ERB.new(IO.read("#{app_root}/config/database.yml")).result)
100
+ database_confs = YAML::load(ERB.new(IO.read("#{self.database_yml_file}")).result)
101
101
  database_confs.symbolize_keys!
102
102
  database_conf = database_confs[environment.to_sym]
103
103
  database_conf.symbolize_keys!
@@ -122,22 +122,24 @@ searchd
122
122
  }
123
123
  CONFIG
124
124
 
125
- ThinkingSphinx.indexed_models.each do |model|
125
+ ThinkingSphinx.indexed_models.each_with_index do |model, model_index|
126
126
  model = model.constantize
127
127
  sources = []
128
128
  delta_sources = []
129
129
  prefixed_fields = []
130
130
  infixed_fields = []
131
131
 
132
- model.indexes.each_with_index do |index, i|
133
- file.write index.to_config(i, database_conf, charset_type)
132
+ model.indexes.select { |index| index.model == model }.each_with_index do |index, i|
133
+ file.write index.to_config(model, i, database_conf, charset_type, model_index)
134
134
 
135
135
  create_array_accum if index.adapter == :postgres
136
- sources << "#{model.indexes.first.name}_#{i}_core"
137
- delta_sources << "#{model.indexes.first.name}_#{i}_delta" if index.delta?
136
+ sources << "#{ThinkingSphinx::Index.name(model)}_#{i}_core"
137
+ delta_sources << "#{ThinkingSphinx::Index.name(model)}_#{i}_delta" if index.delta?
138
138
  end
139
139
 
140
- source_list = sources.collect { |s| "source = #{s}" }.join("\n")
140
+ next if sources.empty?
141
+
142
+ source_list = sources.collect { |s| "source = #{s}" }.join("\n")
141
143
  delta_list = delta_sources.collect { |s| "source = #{s}" }.join("\n")
142
144
 
143
145
  file.write core_index_for_model(model, source_list)
@@ -167,8 +169,7 @@ searchd
167
169
  begin
168
170
  model_name.camelize.constantize
169
171
  rescue LoadError
170
- model_name.gsub!(/.*[\/\\]/, '')
171
- retry
172
+ model_name.gsub!(/.*[\/\\]/, '').nil? ? next : retry
172
173
  rescue NameError
173
174
  next
174
175
  end
@@ -194,10 +195,10 @@ searchd
194
195
  def core_index_for_model(model, sources)
195
196
  output = <<-INDEX
196
197
 
197
- index #{model.indexes.first.name}_core
198
+ index #{ThinkingSphinx::Index.name(model)}_core
198
199
  {
199
200
  #{sources}
200
- path = #{self.searchd_file_path}/#{model.indexes.first.name}_core
201
+ path = #{self.searchd_file_path}/#{ThinkingSphinx::Index.name(model)}_core
201
202
  charset_type = #{self.charset_type}
202
203
  INDEX
203
204
 
@@ -209,20 +210,30 @@ INDEX
209
210
  output += " ignore_chars = #{self.ignore_chars}\n" unless self.ignore_chars.nil?
210
211
 
211
212
  if self.allow_star
213
+ # Ye Olde way of turning on enable_star
212
214
  output += " enable_star = 1\n"
213
215
  output += " min_prefix_len = #{self.min_prefix_len}\n"
214
- output += " min_infix_len = #{self.min_infix_len}\n"
216
+ else
217
+ # New, better way of turning on enable_star - thanks to James Healy
218
+ output += " enable_star = 1\n" if self.enable_star
219
+ output += " min_prefix_len = #{self.min_prefix_len}\n" unless self.min_prefix_len.nil?
220
+ output += " min_infix_len = #{self.min_infix_len}\n" unless self.min_infix_len.nil?
215
221
  end
216
222
 
223
+
217
224
  output += " html_strip = 1\n" if self.html_strip
218
225
  output += " html_remove_elements = #{self.html_remove_elements}\n" unless self.html_remove_elements.blank?
219
226
 
220
227
  unless model.indexes.collect(&:prefix_fields).flatten.empty?
221
- output += " prefix_fields = #{model.indexes.collect(&:prefix_fields).flatten.join(', ')}\n"
228
+ output += " prefix_fields = #{model.indexes.collect(&:prefix_fields).flatten.map(&:unique_name).join(', ')}\n"
229
+ else
230
+ output += " prefix_fields = _\n" unless model.indexes.collect(&:infix_fields).flatten.empty?
222
231
  end
223
232
 
224
233
  unless model.indexes.collect(&:infix_fields).flatten.empty?
225
- output += " infix_fields = #{model.indexes.collect(&:infix_fields).flatten.join(', ')}\n"
234
+ output += " infix_fields = #{model.indexes.collect(&:infix_fields).flatten.map(&:unique_name).join(', ')}\n"
235
+ else
236
+ output += " infix_fields = -\n" unless model.indexes.collect(&:prefix_fields).flatten.empty?
226
237
  end
227
238
 
228
239
  output + "}\n"
@@ -230,22 +241,22 @@ INDEX
230
241
 
231
242
  def delta_index_for_model(model, sources)
232
243
  <<-INDEX
233
- index #{model.indexes.first.name}_delta : #{model.indexes.first.name}_core
244
+ index #{ThinkingSphinx::Index.name(model)}_delta : #{ThinkingSphinx::Index.name(model)}_core
234
245
  {
235
246
  #{sources}
236
- path = #{self.searchd_file_path}/#{model.indexes.first.name}_delta
247
+ path = #{self.searchd_file_path}/#{ThinkingSphinx::Index.name(model)}_delta
237
248
  }
238
249
  INDEX
239
250
  end
240
251
 
241
252
  def distributed_index_for_model(model)
242
- sources = ["local = #{model.indexes.first.name}_core"]
253
+ sources = ["local = #{ThinkingSphinx::Index.name(model)}_core"]
243
254
  if model.indexes.any? { |index| index.delta? }
244
- sources << "local = #{model.indexes.first.name}_delta"
255
+ sources << "local = #{ThinkingSphinx::Index.name(model)}_delta"
245
256
  end
246
257
 
247
258
  <<-INDEX
248
- index #{model.indexes.first.name}
259
+ index #{ThinkingSphinx::Index.name(model)}
249
260
  {
250
261
  type = distributed
251
262
  #{ sources.join("\n ") }
@@ -178,13 +178,17 @@ module ThinkingSphinx
178
178
  # figure out how to correctly reference a column in SQL.
179
179
  #
180
180
  def column_with_prefix(column)
181
- if associations[column].empty?
181
+ if column.is_string?
182
+ column.__name
183
+ elsif associations[column].empty?
182
184
  "#{@model.quoted_table_name}.#{quote_column(column.__name)}"
183
185
  else
184
186
  associations[column].collect { |assoc|
187
+ assoc.has_column?(column.__name) ?
185
188
  "#{@model.connection.quote_table_name(assoc.join.aliased_table_name)}" +
186
- ".#{quote_column(column.__name)}"
187
- }.join(', ')
189
+ ".#{quote_column(column.__name)}" :
190
+ nil
191
+ }.compact.join(', ')
188
192
  end
189
193
  end
190
194
 
@@ -194,5 +198,9 @@ module ThinkingSphinx
194
198
  def is_many?
195
199
  associations.values.flatten.any? { |assoc| assoc.is_many? }
196
200
  end
201
+
202
+ def is_string?
203
+ columns.all? { |col| col.is_string? }
204
+ end
197
205
  end
198
206
  end
@@ -81,8 +81,7 @@ module ThinkingSphinx
81
81
  def indexes(*args)
82
82
  options = args.extract_options!
83
83
  args.each do |columns|
84
- columns = FauxColumn.new(columns) if columns.is_a?(Symbol)
85
- fields << Field.new(columns, options)
84
+ fields << Field.new(FauxColumn.coerce(columns), options)
86
85
 
87
86
  if fields.last.sortable
88
87
  attributes << Attribute.new(
@@ -137,23 +136,7 @@ module ThinkingSphinx
137
136
  def has(*args)
138
137
  options = args.extract_options!
139
138
  args.each do |columns|
140
- columns = case columns
141
- when Symbol, String
142
- FauxColumn.new(columns)
143
- when Array
144
- columns.collect { |col|
145
- case col
146
- when Symbol, String
147
- FauxColumn.new(col)
148
- else
149
- col
150
- end
151
- }
152
- else
153
- columns
154
- end
155
-
156
- attributes << Attribute.new(columns, options)
139
+ attributes << Attribute.new(FauxColumn.coerce(columns), options)
157
140
  end
158
141
  end
159
142
  alias_method :attribute, :has
@@ -15,6 +15,19 @@ module ThinkingSphinx
15
15
  @stack = stack
16
16
  end
17
17
 
18
+ def self.coerce(columns)
19
+ case columns
20
+ when Symbol, String
21
+ FauxColumn.new(columns)
22
+ when Array
23
+ columns.collect { |col| FauxColumn.coerce(col) }
24
+ when FauxColumn
25
+ columns
26
+ else
27
+ nil
28
+ end
29
+ end
30
+
18
31
  # Can't use normal method name, as that could be an association or
19
32
  # column name.
20
33
  #
@@ -38,6 +38,10 @@ module ThinkingSphinx
38
38
  end
39
39
 
40
40
  def name
41
+ self.class.name(@model)
42
+ end
43
+
44
+ def self.name(model)
41
45
  model.name.underscore.tr(':/\\', '_')
42
46
  end
43
47
 
@@ -46,8 +50,9 @@ module ThinkingSphinx
46
50
  File.size?("#{config.searchd_file_path}/#{self.name}_#{part}.spa").nil?
47
51
  end
48
52
 
49
- def to_config(index, database_conf, charset_type)
53
+ def to_config(model, index, database_conf, charset_type, offset)
50
54
  # Set up associations and joins
55
+ add_internal_attributes
51
56
  link!
52
57
 
53
58
  attr_sources = attributes.collect { |attrib|
@@ -65,20 +70,21 @@ module ThinkingSphinx
65
70
 
66
71
  config = <<-SOURCE
67
72
 
68
- source #{model.indexes.first.name}_#{index}_core
73
+ source #{self.class.name(model)}_#{index}_core
69
74
  {
70
75
  type = #{db_adapter}
71
76
  sql_host = #{database_conf[:host] || "localhost"}
72
- sql_user = #{database_conf[:username]}
73
- sql_pass = #{database_conf[:password]}
77
+ sql_user = #{database_conf[:username] || database_conf[:user]}
78
+ sql_pass = #{(database_conf[:password] || "").gsub('#', '\#')}
74
79
  sql_db = #{database_conf[:database]}
80
+ #{"sql_sock = #{database_conf[:socket]}" unless database_conf[:socket].blank? }
75
81
 
76
82
  sql_query_pre = #{charset_type == "utf-8" && adapter == :mysql ? "SET NAMES utf8" : ""}
77
83
  #{"sql_query_pre = SET SESSION group_concat_max_len = #{@options[:group_concat_max_len]}" if @options[:group_concat_max_len]}
78
84
  sql_query_pre = #{to_sql_query_pre}
79
- sql_query = #{to_sql.gsub(/\n/, ' ')}
80
- sql_query_range = #{to_sql_query_range}
81
- sql_query_info = #{to_sql_query_info}
85
+ sql_query = #{to_sql(:offset => offset).gsub(/\n/, ' ')}
86
+ sql_query_range = #{to_sql_query_range :offset => offset}
87
+ sql_query_info = #{to_sql_query_info(offset)}
82
88
  #{attr_sources}
83
89
  }
84
90
  SOURCE
@@ -86,13 +92,13 @@ sql_query_info = #{to_sql_query_info}
86
92
  if delta?
87
93
  config += <<-SOURCE
88
94
 
89
- source #{model.indexes.first.name}_#{index}_delta : #{model.indexes.first.name}_#{index}_core
95
+ source #{self.class.name(model)}_#{index}_delta : #{self.class.name(model)}_#{index}_core
90
96
  {
91
97
  sql_query_pre =
92
98
  sql_query_pre = #{charset_type == "utf-8" && adapter == :mysql ? "SET NAMES utf8" : ""}
93
99
  #{"sql_query_pre = SET SESSION group_concat_max_len = #{@options[:group_concat_max_len]}" if @options[:group_concat_max_len]}
94
- sql_query = #{to_sql(:delta => true).gsub(/\n/, ' ')}
95
- sql_query_range = #{to_sql_query_range :delta => true}
100
+ sql_query = #{to_sql(:delta => true, :offset => offset).gsub(/\n/, ' ')}
101
+ sql_query_range = #{to_sql_query_range :offset => offset, :delta => true}
96
102
  }
97
103
  SOURCE
98
104
  end
@@ -149,16 +155,18 @@ sql_query_range = #{to_sql_query_range :delta => true}
149
155
  where_clause << " AND " << @conditions.join(" AND ")
150
156
  end
151
157
 
158
+ unique_id_expr = "* #{ThinkingSphinx.indexed_models.size} + #{options[:offset] || 0}"
159
+
152
160
  sql = <<-SQL
153
161
  SELECT #{ (
154
- ["#{@model.quoted_table_name}.#{quote_column(@model.primary_key)}"] +
162
+ ["#{@model.quoted_table_name}.#{quote_column(@model.primary_key)} #{unique_id_expr} AS #{quote_column(@model.primary_key)} "] +
155
163
  @fields.collect { |field| field.to_select_sql } +
156
164
  @attributes.collect { |attribute| attribute.to_select_sql }
157
165
  ).join(", ") }
158
166
  FROM #{ @model.table_name }
159
167
  #{ assocs.collect { |assoc| assoc.to_sql }.join(' ') }
160
- WHERE #{@model.quoted_table_name}.#{quote_column(@model.primary_key)} >= $start
161
- AND #{@model.quoted_table_name}.#{quote_column(@model.primary_key)} <= $end
168
+ WHERE #{@model.quoted_table_name}.#{quote_column(@model.primary_key)} #{unique_id_expr} >= $start
169
+ AND #{@model.quoted_table_name}.#{quote_column(@model.primary_key)} #{unique_id_expr} <= $end
162
170
  #{ where_clause }
163
171
  GROUP BY #{ (
164
172
  ["#{@model.quoted_table_name}.#{quote_column(@model.primary_key)}"] +
@@ -177,9 +185,9 @@ GROUP BY #{ (
177
185
  # Simple helper method for the query info SQL - which is a statement that
178
186
  # returns the single row for a corresponding id.
179
187
  #
180
- def to_sql_query_info
188
+ def to_sql_query_info(offset)
181
189
  "SELECT * FROM #{@model.quoted_table_name} WHERE " +
182
- " #{quote_column(@model.primary_key)} = $id"
190
+ " #{quote_column(@model.primary_key)} = (($id - #{offset}) / #{ThinkingSphinx.indexed_models.size})"
183
191
  end
184
192
 
185
193
  # Simple helper method for the query range SQL - which is a statement that
@@ -187,8 +195,10 @@ GROUP BY #{ (
187
195
  # so pass in :delta => true to get the delta version of the SQL.
188
196
  #
189
197
  def to_sql_query_range(options={})
190
- min_statement = "MIN(#{quote_column(@model.primary_key)})"
191
- max_statement = "MAX(#{quote_column(@model.primary_key)})"
198
+ unique_id_expr = "* #{ThinkingSphinx.indexed_models.size} + #{options[:offset] || 0}"
199
+
200
+ min_statement = "MIN(#{quote_column(@model.primary_key)} #{unique_id_expr})"
201
+ max_statement = "MAX(#{quote_column(@model.primary_key)} #{unique_id_expr})"
192
202
 
193
203
  # Fix to handle Sphinx PostgreSQL bug (it doesn't like NULLs or 0's)
194
204
  if adapter == :postgres
@@ -265,17 +275,6 @@ GROUP BY #{ (
265
275
  @conditions = builder.conditions
266
276
  @delta = builder.properties[:delta]
267
277
  @options = builder.properties.except(:delta)
268
-
269
- @attributes << Attribute.new(
270
- FauxColumn.new(@model.to_crc32.to_s),
271
- :type => :integer,
272
- :as => :class_crc
273
- )
274
- @attributes << Attribute.new(
275
- FauxColumn.new("0"),
276
- :type => :integer,
277
- :as => :sphinx_deleted
278
- )
279
278
  end
280
279
 
281
280
  # Returns all associations used amongst all the fields and attributes.
@@ -335,5 +334,43 @@ GROUP BY #{ (
335
334
  val ? '1' : '0'
336
335
  end
337
336
  end
337
+
338
+ def crc_column
339
+ if adapter == :postgres
340
+ @model.to_crc32.to_s
341
+ elsif @model.column_names.include?(@model.inheritance_column)
342
+ "IFNULL(CRC32(#{@model.quoted_table_name}.#{quote_column(@model.inheritance_column)}), #{@model.to_crc32.to_s})"
343
+ else
344
+ @model.to_crc32.to_s
345
+ end
346
+ end
347
+
348
+ def add_internal_attributes
349
+ @attributes << Attribute.new(
350
+ FauxColumn.new(:id),
351
+ :type => :integer,
352
+ :as => :sphinx_internal_id
353
+ ) unless @attributes.detect { |attr| attr.alias == :sphinx_internal_id }
354
+
355
+ @attributes << Attribute.new(
356
+ FauxColumn.new(crc_column),
357
+ :type => :integer,
358
+ :as => :class_crc
359
+ ) unless @attributes.detect { |attr| attr.alias == :class_crc }
360
+
361
+ @attributes << Attribute.new(
362
+ FauxColumn.new("'" + (@model.send(:subclasses).collect { |klass|
363
+ klass.to_crc32.to_s
364
+ } << @model.to_crc32.to_s).join(",") + "'"),
365
+ :type => :multi,
366
+ :as => :subclass_crcs
367
+ ) unless @attributes.detect { |attr| attr.alias == :subclass_crcs }
368
+
369
+ @attributes << Attribute.new(
370
+ FauxColumn.new("0"),
371
+ :type => :integer,
372
+ :as => :sphinx_deleted
373
+ ) unless @attributes.detect { |attr| attr.alias == :sphinx_deleted }
374
+ end
338
375
  end
339
376
  end