ebeigarts-thinking-sphinx 1.1.22 → 1.2.10

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.
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