ebeigarts-thinking-sphinx 1.1.22 → 1.2.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/README.textile +14 -0
  2. data/VERSION.yml +4 -0
  3. data/lib/thinking_sphinx.rb +60 -64
  4. data/lib/thinking_sphinx/active_record.rb +35 -7
  5. data/lib/thinking_sphinx/active_record/scopes.rb +39 -0
  6. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +3 -2
  7. data/lib/thinking_sphinx/attribute.rb +62 -22
  8. data/lib/thinking_sphinx/configuration.rb +21 -1
  9. data/lib/thinking_sphinx/core/array.rb +7 -0
  10. data/lib/thinking_sphinx/deltas/delayed_delta.rb +3 -0
  11. data/lib/thinking_sphinx/deploy/capistrano.rb +26 -8
  12. data/lib/thinking_sphinx/excerpter.rb +22 -0
  13. data/lib/thinking_sphinx/facet.rb +8 -2
  14. data/lib/thinking_sphinx/facet_search.rb +134 -0
  15. data/lib/thinking_sphinx/index.rb +2 -2
  16. data/lib/thinking_sphinx/index/builder.rb +0 -1
  17. data/lib/thinking_sphinx/property.rb +2 -0
  18. data/lib/thinking_sphinx/rails_additions.rb +14 -0
  19. data/lib/thinking_sphinx/search.rb +633 -671
  20. data/lib/thinking_sphinx/search_methods.rb +421 -0
  21. data/lib/thinking_sphinx/source.rb +5 -5
  22. data/lib/thinking_sphinx/source/internal_properties.rb +1 -1
  23. data/lib/thinking_sphinx/source/sql.rb +10 -8
  24. data/lib/thinking_sphinx/tasks.rb +14 -9
  25. data/spec/{unit → lib}/thinking_sphinx/active_record/delta_spec.rb +1 -1
  26. data/spec/{unit → lib}/thinking_sphinx/active_record/has_many_association_spec.rb +0 -0
  27. data/spec/lib/thinking_sphinx/active_record/scopes_spec.rb +96 -0
  28. data/spec/{unit → lib}/thinking_sphinx/active_record_spec.rb +44 -5
  29. data/spec/{unit → lib}/thinking_sphinx/association_spec.rb +0 -0
  30. data/spec/{unit → lib}/thinking_sphinx/attribute_spec.rb +110 -3
  31. data/spec/{unit → lib}/thinking_sphinx/configuration_spec.rb +87 -41
  32. data/spec/lib/thinking_sphinx/core/array_spec.rb +9 -0
  33. data/spec/{unit → lib}/thinking_sphinx/core/string_spec.rb +0 -0
  34. data/spec/lib/thinking_sphinx/excerpter_spec.rb +49 -0
  35. data/spec/lib/thinking_sphinx/facet_search_spec.rb +176 -0
  36. data/spec/{unit → lib}/thinking_sphinx/facet_spec.rb +34 -15
  37. data/spec/{unit → lib}/thinking_sphinx/field_spec.rb +0 -0
  38. data/spec/{unit → lib}/thinking_sphinx/index/builder_spec.rb +100 -0
  39. data/spec/{unit → lib}/thinking_sphinx/index/faux_column_spec.rb +0 -0
  40. data/spec/{unit → lib}/thinking_sphinx/index_spec.rb +0 -0
  41. data/spec/{unit → lib}/thinking_sphinx/rails_additions_spec.rb +12 -0
  42. data/spec/lib/thinking_sphinx/search_methods_spec.rb +152 -0
  43. data/spec/lib/thinking_sphinx/search_spec.rb +1066 -0
  44. data/spec/{unit → lib}/thinking_sphinx/source_spec.rb +10 -0
  45. data/spec/{unit → lib}/thinking_sphinx_spec.rb +10 -0
  46. data/tasks/distribution.rb +20 -38
  47. data/tasks/testing.rb +3 -1
  48. data/vendor/riddle/lib/riddle.rb +1 -1
  49. data/vendor/riddle/lib/riddle/client.rb +3 -0
  50. data/vendor/riddle/lib/riddle/client/message.rb +4 -3
  51. data/vendor/riddle/lib/riddle/configuration/section.rb +1 -1
  52. data/vendor/riddle/lib/riddle/controller.rb +17 -7
  53. metadata +63 -83
  54. data/lib/thinking_sphinx/active_record/search.rb +0 -57
  55. data/lib/thinking_sphinx/collection.rb +0 -148
  56. data/lib/thinking_sphinx/facet_collection.rb +0 -59
  57. data/lib/thinking_sphinx/search/facets.rb +0 -104
  58. data/spec/unit/thinking_sphinx/active_record/search_spec.rb +0 -107
  59. data/spec/unit/thinking_sphinx/collection_spec.rb +0 -15
  60. data/spec/unit/thinking_sphinx/facet_collection_spec.rb +0 -64
  61. data/spec/unit/thinking_sphinx/search_spec.rb +0 -228
@@ -30,6 +30,10 @@ Alternatively, install the ginger gem directly from the freelancing-god github r
30
30
 
31
31
  sudo gem sources -a http://gems.github.com
32
32
  sudo gem install freelancing-god-ginger
33
+
34
+ Then install the cucumber, yard, jeweler and rspec gems. Make sure you have a git install version 1.6.0.0 or higher, otherwise the jeweler gem won't install.
35
+
36
+ sudo gem install cucumber yard jeweler rspec
33
37
 
34
38
  Then set up your database:
35
39
 
@@ -141,3 +145,13 @@ Since I first released this library, there's been quite a few people who have su
141
145
  * Paul Campbell
142
146
  * Matthew Beale
143
147
  * Tom Simnett
148
+ * Erik Ostrom
149
+ * Ole Riesenberg
150
+ * Josh Kalderimis
151
+ * J.D. Hollis
152
+ * Jeffrey Chupp
153
+ * Rob Anderton
154
+ * Zach Inglis
155
+ * Ward Bekker
156
+ * Brian Terlson
157
+ * Christian Aust
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 1
3
+ :minor: 2
4
+ :patch: 10
@@ -5,22 +5,25 @@ end
5
5
  require 'active_record'
6
6
  require 'riddle'
7
7
  require 'after_commit'
8
+ require 'yaml'
8
9
 
10
+ require 'thinking_sphinx/core/array'
9
11
  require 'thinking_sphinx/core/string'
10
12
  require 'thinking_sphinx/property'
11
13
  require 'thinking_sphinx/active_record'
12
14
  require 'thinking_sphinx/association'
13
15
  require 'thinking_sphinx/attribute'
14
- require 'thinking_sphinx/collection'
15
16
  require 'thinking_sphinx/configuration'
17
+ require 'thinking_sphinx/excerpter'
16
18
  require 'thinking_sphinx/facet'
17
19
  require 'thinking_sphinx/class_facet'
18
- require 'thinking_sphinx/facet_collection'
20
+ require 'thinking_sphinx/facet_search'
19
21
  require 'thinking_sphinx/field'
20
22
  require 'thinking_sphinx/index'
21
23
  require 'thinking_sphinx/source'
22
24
  require 'thinking_sphinx/rails_additions'
23
25
  require 'thinking_sphinx/search'
26
+ require 'thinking_sphinx/search_methods'
24
27
  require 'thinking_sphinx/deltas'
25
28
 
26
29
  require 'thinking_sphinx/adapters/abstract_adapter'
@@ -36,19 +39,11 @@ Merb::Plugins.add_rakefiles(
36
39
  ) if defined?(Merb)
37
40
 
38
41
  module ThinkingSphinx
39
- module Version #:nodoc:
40
- Major = 1
41
- Minor = 1
42
- Tiny = 22
43
-
44
- String = [Major, Minor, Tiny].join('.')
45
- end
46
-
47
42
  # A ConnectionError will get thrown when a connection to Sphinx can't be
48
43
  # made.
49
44
  class ConnectionError < StandardError
50
45
  end
51
-
46
+
52
47
  # A StaleIdsException is thrown by Collection.instances_from_matches if there
53
48
  # are records in Sphinx but not in the database, so the search can be retried.
54
49
  class StaleIdsException < StandardError
@@ -58,41 +53,50 @@ module ThinkingSphinx
58
53
  end
59
54
  end
60
55
 
56
+ # The current version of Thinking Sphinx.
57
+ #
58
+ # @return [String] The version number as a string
59
+ #
60
+ def self.version
61
+ hash = YAML.load_file File.join(File.dirname(__FILE__), '../VERSION.yml')
62
+ [hash[:major], hash[:minor], hash[:patch]].join('.')
63
+ end
64
+
61
65
  # The collection of indexed models. Keep in mind that Rails lazily loads
62
66
  # its classes, so this may not actually be populated with _all_ the models
63
67
  # that have Sphinx indexes.
64
68
  def self.indexed_models
65
69
  @@indexed_models ||= []
66
70
  end
67
-
71
+
68
72
  def self.unique_id_expression(offset = nil)
69
73
  "* #{ThinkingSphinx.indexed_models.size} + #{offset || 0}"
70
74
  end
71
-
75
+
72
76
  # Check if index definition is disabled.
73
- #
77
+ #
74
78
  def self.define_indexes?
75
79
  @@define_indexes = true unless defined?(@@define_indexes)
76
80
  @@define_indexes == true
77
81
  end
78
-
82
+
79
83
  # Enable/disable indexes - you may want to do this while migrating data.
80
- #
84
+ #
81
85
  # ThinkingSphinx.define_indexes = false
82
- #
86
+ #
83
87
  def self.define_indexes=(value)
84
88
  @@define_indexes = value
85
89
  end
86
-
90
+
87
91
  @@deltas_enabled = nil
88
92
 
89
93
  # Check if delta indexing is enabled.
90
- #
94
+ #
91
95
  def self.deltas_enabled?
92
96
  @@deltas_enabled = (ThinkingSphinx::Configuration.environment != 'test') if @@deltas_enabled.nil?
93
97
  @@deltas_enabled
94
98
  end
95
-
99
+
96
100
  # Enable/disable all delta indexing.
97
101
  #
98
102
  # ThinkingSphinx.deltas_enabled = false
@@ -100,38 +104,38 @@ module ThinkingSphinx
100
104
  def self.deltas_enabled=(value)
101
105
  @@deltas_enabled = value
102
106
  end
103
-
107
+
104
108
  @@updates_enabled = nil
105
-
109
+
106
110
  # Check if updates are enabled. True by default, unless within the test
107
111
  # environment.
108
- #
112
+ #
109
113
  def self.updates_enabled?
110
114
  @@updates_enabled = (ThinkingSphinx::Configuration.environment != 'test') if @@updates_enabled.nil?
111
115
  @@updates_enabled
112
116
  end
113
-
117
+
114
118
  # Enable/disable updates to Sphinx
115
- #
119
+ #
116
120
  # ThinkingSphinx.updates_enabled = false
117
121
  #
118
122
  def self.updates_enabled=(value)
119
123
  @@updates_enabled = value
120
124
  end
121
-
125
+
122
126
  @@suppress_delta_output = false
123
-
127
+
124
128
  def self.suppress_delta_output?
125
129
  @@suppress_delta_output
126
130
  end
127
-
131
+
128
132
  def self.suppress_delta_output=(value)
129
133
  @@suppress_delta_output = value
130
134
  end
131
-
135
+
132
136
  # Checks to see if MySQL will allow simplistic GROUP BY statements. If not,
133
137
  # or if not using MySQL, this will return false.
134
- #
138
+ #
135
139
  def self.use_group_by_shortcut?
136
140
  !!(
137
141
  mysql? && ::ActiveRecord::Base.connection.select_all(
@@ -139,79 +143,71 @@ module ThinkingSphinx
139
143
  ).all? { |key,value| value.nil? || value[/ONLY_FULL_GROUP_BY/].nil? }
140
144
  )
141
145
  end
142
-
146
+
143
147
  @@remote_sphinx = false
144
-
148
+
145
149
  # An indication of whether Sphinx is running on a remote machine instead of
146
150
  # the same machine.
147
- #
151
+ #
148
152
  def self.remote_sphinx?
149
153
  @@remote_sphinx
150
154
  end
151
-
155
+
152
156
  # Tells Thinking Sphinx that Sphinx is running on a different machine, and
153
- # thus it can't reliably guess whether it is running or not (ie: the
157
+ # thus it can't reliably guess whether it is running or not (ie: the
154
158
  # #sphinx_running? method), and so just assumes it is.
155
- #
159
+ #
156
160
  # Useful for multi-machine deployments. Set it in your production.rb file.
157
- #
161
+ #
158
162
  # ThinkingSphinx.remote_sphinx = true
159
- #
163
+ #
160
164
  def self.remote_sphinx=(value)
161
165
  @@remote_sphinx = value
162
166
  end
163
-
167
+
164
168
  # Check if Sphinx is running. If remote_sphinx is set to true (indicating
165
169
  # Sphinx is on a different machine), this will always return true, and you
166
170
  # will have to handle any connection errors yourself.
167
- #
171
+ #
168
172
  def self.sphinx_running?
169
173
  remote_sphinx? || sphinx_running_by_pid?
170
174
  end
171
-
175
+
172
176
  # Check if Sphinx is actually running, provided the pid is on the same
173
177
  # machine as this code.
174
- #
178
+ #
175
179
  def self.sphinx_running_by_pid?
176
180
  !!sphinx_pid && pid_active?(sphinx_pid)
177
181
  end
178
-
182
+
179
183
  def self.sphinx_pid
180
- pid_file = ThinkingSphinx::Configuration.instance.pid_file
181
- cat_command = 'cat'
182
- return nil unless File.exists?(pid_file)
183
-
184
- if microsoft?
185
- pid_file.gsub!('/', '\\')
186
- cat_command = 'type'
184
+ if File.exists?(ThinkingSphinx::Configuration.instance.pid_file)
185
+ File.read(ThinkingSphinx::Configuration.instance.pid_file)[/\d+/]
186
+ else
187
+ nil
187
188
  end
188
-
189
- `#{cat_command} #{pid_file}`[/\d+/]
190
189
  end
191
-
190
+
192
191
  def self.pid_active?(pid)
193
- return true if microsoft?
194
-
195
- begin
196
- # In JRuby this returns -1 if the process doesn't exist
197
- Process.getpgid(pid.to_i) != -1
198
- rescue Exception => e
199
- false
200
- end
192
+ !!Process.kill(0, pid.to_i)
193
+ rescue Exception => e
194
+ false
201
195
  end
202
-
196
+
203
197
  def self.microsoft?
204
198
  RUBY_PLATFORM =~ /mswin/
205
199
  end
206
-
200
+
207
201
  def self.jruby?
208
202
  defined?(JRUBY_VERSION)
209
203
  end
210
-
204
+
211
205
  def self.mysql?
212
206
  ::ActiveRecord::Base.connection.class.name.demodulize == "MysqlAdapter" ||
213
207
  ::ActiveRecord::Base.connection.class.name.demodulize == "MysqlplusAdapter" || (
214
208
  jruby? && ::ActiveRecord::Base.connection.config[:adapter] == "jdbcmysql"
215
209
  )
216
210
  end
211
+
212
+ extend ThinkingSphinx::SearchMethods::ClassMethods
217
213
  end
@@ -1,7 +1,7 @@
1
1
  require 'thinking_sphinx/active_record/attribute_updates'
2
2
  require 'thinking_sphinx/active_record/delta'
3
- require 'thinking_sphinx/active_record/search'
4
3
  require 'thinking_sphinx/active_record/has_many_association'
4
+ require 'thinking_sphinx/active_record/scopes'
5
5
 
6
6
  module ThinkingSphinx
7
7
  # Core additions to ActiveRecord models - define_index for creating indexes
@@ -13,6 +13,15 @@ module ThinkingSphinx
13
13
  base.class_eval do
14
14
  class_inheritable_array :sphinx_indexes, :sphinx_facets
15
15
  class << self
16
+
17
+ def set_sphinx_primary_key(attribute)
18
+ @sphinx_primary_key_attribute = attribute
19
+ end
20
+
21
+ def primary_key_for_sphinx
22
+ @sphinx_primary_key_attribute || primary_key
23
+ end
24
+
16
25
  # Allows creation of indexes for Sphinx. If you don't do this, there
17
26
  # isn't much point trying to search (or using this plugin at all,
18
27
  # really).
@@ -80,7 +89,9 @@ module ThinkingSphinx
80
89
 
81
90
  after_destroy :toggle_deleted
82
91
 
92
+ include ThinkingSphinx::SearchMethods
83
93
  include ThinkingSphinx::ActiveRecord::AttributeUpdates
94
+ include ThinkingSphinx::ActiveRecord::Scopes
84
95
 
85
96
  index
86
97
 
@@ -140,12 +151,20 @@ module ThinkingSphinx
140
151
  ThinkingSphinx::AbstractAdapter.detect(self)
141
152
  end
142
153
 
143
- private
144
-
145
154
  def sphinx_name
146
155
  self.name.underscore.tr(':/\\', '_')
147
156
  end
148
157
 
158
+ def sphinx_index_names
159
+ klass = source_of_sphinx_index
160
+ names = ["#{klass.sphinx_name}_core"]
161
+ names << "#{klass.sphinx_name}_delta" if sphinx_delta?
162
+
163
+ names
164
+ end
165
+
166
+ private
167
+
149
168
  def sphinx_delta?
150
169
  self.sphinx_indexes.any? { |index| index.delta? }
151
170
  end
@@ -215,7 +234,6 @@ module ThinkingSphinx
215
234
  end
216
235
 
217
236
  base.send(:include, ThinkingSphinx::ActiveRecord::Delta)
218
- base.send(:include, ThinkingSphinx::ActiveRecord::Search)
219
237
 
220
238
  ::ActiveRecord::Associations::HasManyAssociation.send(
221
239
  :include, ThinkingSphinx::ActiveRecord::HasManyAssociation
@@ -264,13 +282,23 @@ module ThinkingSphinx
264
282
  # nothing
265
283
  end
266
284
 
285
+ # Returns the unique integer id for the object. This method uses the
286
+ # attribute hash to get around ActiveRecord always mapping the #id method
287
+ # to whatever the real primary key is (which may be a unique string hash).
288
+ #
289
+ # @return [Integer] Unique record id for the purposes of Sphinx.
290
+ #
291
+ def primary_key_for_sphinx
292
+ @primary_key_for_sphinx ||= read_attribute(self.class.primary_key_for_sphinx)
293
+ end
294
+
267
295
  def sphinx_document_id
268
- (self.id * ThinkingSphinx.indexed_models.size) +
296
+ primary_key_for_sphinx * ThinkingSphinx.indexed_models.size +
269
297
  ThinkingSphinx.indexed_models.index(self.class.source_of_sphinx_index.name)
270
298
  end
271
-
299
+
272
300
  private
273
-
301
+
274
302
  def sphinx_index_name(suffix)
275
303
  "#{self.class.source_of_sphinx_index.name.underscore.tr(':/\\', '_')}_#{suffix}"
276
304
  end
@@ -0,0 +1,39 @@
1
+ module ThinkingSphinx
2
+ module ActiveRecord
3
+ module Scopes
4
+ def self.included(base)
5
+ base.class_eval do
6
+ extend ThinkingSphinx::ActiveRecord::Scopes::ClassMethods
7
+ end
8
+ end
9
+
10
+ module ClassMethods
11
+ def sphinx_scope(method, &block)
12
+ @sphinx_scopes ||= []
13
+ @sphinx_scopes << method
14
+
15
+ metaclass.instance_eval do
16
+ define_method(method) do |*args|
17
+ options = {:classes => classes_option}
18
+ options.merge! block.call(*args)
19
+
20
+ ThinkingSphinx::Search.new(options)
21
+ end
22
+ end
23
+ end
24
+
25
+ def sphinx_scopes
26
+ @sphinx_scopes || []
27
+ end
28
+
29
+ def remove_sphinx_scopes
30
+ sphinx_scopes.each do |scope|
31
+ metaclass.send(:undef_method, scope)
32
+ end
33
+
34
+ sphinx_scopes.clear
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -37,7 +37,8 @@ module ThinkingSphinx
37
37
  end
38
38
 
39
39
  def convert_nulls(clause, default = '')
40
- default = "'#{default}'" if default.is_a?(String)
40
+ default = "'#{default}'" if default.is_a?(String)
41
+ default = 'NULL' if default.nil?
41
42
 
42
43
  "COALESCE(#{clause}, #{default})"
43
44
  end
@@ -132,4 +133,4 @@ module ThinkingSphinx
132
133
  execute function, true
133
134
  end
134
135
  end
135
- end
136
+ end
@@ -75,7 +75,9 @@ module ThinkingSphinx
75
75
  @crc = options[:crc]
76
76
 
77
77
  @type ||= :multi unless @query_source.nil?
78
- @type = :integer if @type == :string && @crc
78
+ if @type == :string && @crc
79
+ @type = is_many? ? :multi : :integer
80
+ end
79
81
 
80
82
  source.attributes << self
81
83
  end
@@ -89,14 +91,21 @@ module ThinkingSphinx
89
91
  def to_select_sql
90
92
  return nil unless include_as_association?
91
93
 
92
- separator = all_ints? || @crc ? ',' : ' '
94
+ separator = all_ints? || all_datetimes? || @crc ? ',' : ' '
93
95
 
94
96
  clause = @columns.collect { |column|
95
97
  part = column_with_prefix(column)
96
- type == :string ? adapter.convert_nulls(part) : part
98
+ case type
99
+ when :string
100
+ adapter.convert_nulls(part)
101
+ when :datetime
102
+ adapter.cast_to_datetime(part)
103
+ else
104
+ part
105
+ end
97
106
  }.join(', ')
98
107
 
99
- clause = adapter.cast_to_datetime(clause) if type == :datetime
108
+ # clause = adapter.cast_to_datetime(clause) if type == :datetime
100
109
  clause = adapter.crc(clause) if @crc
101
110
  clause = adapter.concatenate(clause, separator) if concat_ws?
102
111
  clause = adapter.group_concatenate(clause, separator) if is_many?
@@ -125,10 +134,10 @@ module ThinkingSphinx
125
134
  # Special case is the multi-valued attribute that needs some
126
135
  # extra configuration.
127
136
  #
128
- def config_value(offset = nil)
137
+ def config_value(offset = nil, delta = false)
129
138
  if type == :multi && ThinkingSphinx::Configuration.instance.type != "xml"
130
139
  multi_config = include_as_association? ? "field" :
131
- source_value(offset).gsub(/\s+/m, " ").strip
140
+ source_value(offset, delta).gsub(/\s+/m, " ").strip
132
141
  "uint #{unique_name} from #{multi_config}"
133
142
  else
134
143
  unique_name
@@ -143,6 +152,8 @@ module ThinkingSphinx
143
152
  def type
144
153
  @type ||= begin
145
154
  base_type = case
155
+ when is_many_datetimes?
156
+ :datetime
146
157
  when is_many?, is_many_ints?
147
158
  :multi
148
159
  when @associations.values.flatten.length > 1
@@ -172,25 +183,29 @@ module ThinkingSphinx
172
183
  end
173
184
 
174
185
  def all_ints?
175
- @columns.all? { |col|
176
- klasses = @associations[col].empty? ? [@model] :
177
- @associations[col].collect { |assoc| assoc.reflection.klass }
178
- klasses.all? { |klass|
179
- column = klass.columns.detect { |column| column.name == col.__name.to_s }
180
- !column.nil? && column.type == :integer
181
- }
182
- }
186
+ all_of_type?(:integer)
187
+ end
188
+
189
+ def all_datetimes?
190
+ all_of_type?(:datetime, :date, :timestamp)
183
191
  end
184
192
 
185
193
  private
186
194
 
187
- def source_value(offset)
195
+ def source_value(offset, delta)
188
196
  if is_string?
189
- "#{query_source.to_s.dasherize}; #{columns.first.__name}"
190
- elsif query_source == :ranged_query
191
- "ranged-query; #{query offset} #{query_clause}; #{range_query}"
197
+ return "#{query_source.to_s.dasherize}; #{columns.first.__name}"
198
+ end
199
+
200
+ query = query(offset)
201
+
202
+ if query_source == :ranged_query
203
+ query += query_clause
204
+ query += " AND #{query_delta.strip}" if delta
205
+ "ranged-query; #{query}; #{range_query}"
192
206
  else
193
- "query; #{query offset}"
207
+ query += "WHERE #{query_delta.strip}" if delta
208
+ "query; #{query}"
194
209
  end
195
210
  end
196
211
 
@@ -212,6 +227,15 @@ FROM #{quote_table_name base_assoc.table} #{association_joins}
212
227
  "WHERE #{foreign_key} >= $start AND #{foreign_key} <= $end"
213
228
  end
214
229
 
230
+ def query_delta
231
+ foreign_key = foreign_key_for_mva base_association_for_mva
232
+ <<-SQL
233
+ #{foreign_key} IN (SELECT #{quote_column model.primary_key}
234
+ FROM #{model.quoted_table_name}
235
+ WHERE #{@source.index.delta_object.clause(model, true)})
236
+ SQL
237
+ end
238
+
215
239
  def range_query
216
240
  assoc = base_association_for_mva
217
241
  foreign_key = foreign_key_for_mva assoc
@@ -259,14 +283,19 @@ FROM #{quote_table_name base_assoc.table} #{association_joins}
259
283
  def is_many_ints?
260
284
  concat_ws? && all_ints?
261
285
  end
262
-
286
+
287
+ def is_many_datetimes?
288
+ is_many? && all_datetimes?
289
+ end
290
+
263
291
  def type_from_database
264
292
  klass = @associations.values.flatten.first ?
265
293
  @associations.values.flatten.first.reflection.klass : @model
266
294
 
267
- klass.columns.detect { |col|
295
+ column = klass.columns.detect { |col|
268
296
  @columns.collect { |c| c.__name.to_s }.include? col.name
269
- }.type
297
+ }
298
+ column.nil? ? nil : column.type
270
299
  end
271
300
 
272
301
  def translated_type_from_database
@@ -288,5 +317,16 @@ block:
288
317
  MESSAGE
289
318
  end
290
319
  end
320
+
321
+ def all_of_type?(*column_types)
322
+ @columns.all? { |col|
323
+ klasses = @associations[col].empty? ? [@model] :
324
+ @associations[col].collect { |assoc| assoc.reflection.klass }
325
+ klasses.all? { |klass|
326
+ column = klass.columns.detect { |column| column.name == col.__name.to_s }
327
+ !column.nil? && column_types.include?(column.type)
328
+ }
329
+ }
330
+ end
291
331
  end
292
332
  end