jorahood-ar-extensions 0.9.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. data/ChangeLog +145 -0
  2. data/README +167 -0
  3. data/Rakefile +79 -0
  4. data/config/database.yml +7 -0
  5. data/config/database.yml.template +7 -0
  6. data/config/mysql.schema +72 -0
  7. data/config/postgresql.schema +39 -0
  8. data/db/migrate/generic_schema.rb +96 -0
  9. data/db/migrate/mysql_schema.rb +31 -0
  10. data/db/migrate/oracle_schema.rb +5 -0
  11. data/db/migrate/version.rb +4 -0
  12. data/init.rb +31 -0
  13. data/lib/ar-extensions/create_and_update.rb +509 -0
  14. data/lib/ar-extensions/csv.rb +309 -0
  15. data/lib/ar-extensions/delete.rb +143 -0
  16. data/lib/ar-extensions/extensions.rb +506 -0
  17. data/lib/ar-extensions/finder_options.rb +275 -0
  18. data/lib/ar-extensions/finders.rb +94 -0
  19. data/lib/ar-extensions/foreign_keys.rb +70 -0
  20. data/lib/ar-extensions/fulltext.rb +62 -0
  21. data/lib/ar-extensions/import.rb +352 -0
  22. data/lib/ar-extensions/insert_select.rb +178 -0
  23. data/lib/ar-extensions/synchronize.rb +30 -0
  24. data/lib/ar-extensions/temporary_table.rb +124 -0
  25. data/lib/ar-extensions/union.rb +204 -0
  26. data/lib/ar-extensions/version.rb +9 -0
  27. data/tests/connections/native_mysql/connection.rb +16 -0
  28. data/tests/connections/native_oracle/connection.rb +16 -0
  29. data/tests/connections/native_postgresql/connection.rb +19 -0
  30. data/tests/connections/native_sqlite/connection.rb +14 -0
  31. data/tests/connections/native_sqlite3/connection.rb +14 -0
  32. data/tests/fixtures/addresses.yml +25 -0
  33. data/tests/fixtures/books.yml +46 -0
  34. data/tests/fixtures/developers.yml +20 -0
  35. data/tests/fixtures/unit/active_record_base_finders/addresses.yml +25 -0
  36. data/tests/fixtures/unit/active_record_base_finders/books.yml +64 -0
  37. data/tests/fixtures/unit/active_record_base_finders/developers.yml +20 -0
  38. data/tests/fixtures/unit/synchronize/books.yml +16 -0
  39. data/tests/fixtures/unit/to_csv_headers/addresses.yml +8 -0
  40. data/tests/fixtures/unit/to_csv_headers/developers.yml +6 -0
  41. data/tests/fixtures/unit/to_csv_with_common_options/addresses.yml +40 -0
  42. data/tests/fixtures/unit/to_csv_with_common_options/developers.yml +13 -0
  43. data/tests/fixtures/unit/to_csv_with_common_options/languages.yml +29 -0
  44. data/tests/fixtures/unit/to_csv_with_common_options/teams.yml +3 -0
  45. data/tests/fixtures/unit/to_csv_with_default_options/developers.yml +7 -0
  46. data/tests/models/address.rb +4 -0
  47. data/tests/models/animal.rb +2 -0
  48. data/tests/models/book.rb +3 -0
  49. data/tests/models/cart_item.rb +4 -0
  50. data/tests/models/developer.rb +8 -0
  51. data/tests/models/group.rb +3 -0
  52. data/tests/models/language.rb +5 -0
  53. data/tests/models/mysql/book.rb +3 -0
  54. data/tests/models/mysql/test_innodb.rb +3 -0
  55. data/tests/models/mysql/test_memory.rb +3 -0
  56. data/tests/models/mysql/test_myisam.rb +3 -0
  57. data/tests/models/project.rb +2 -0
  58. data/tests/models/shopping_cart.rb +4 -0
  59. data/tests/models/team.rb +4 -0
  60. data/tests/models/topic.rb +13 -0
  61. data/tests/mysql/test_create_and_update.rb +290 -0
  62. data/tests/mysql/test_delete.rb +142 -0
  63. data/tests/mysql/test_finder_options.rb +121 -0
  64. data/tests/mysql/test_finders.rb +29 -0
  65. data/tests/mysql/test_import.rb +354 -0
  66. data/tests/mysql/test_insert_select.rb +173 -0
  67. data/tests/mysql/test_mysql_adapter.rb +45 -0
  68. data/tests/mysql/test_union.rb +81 -0
  69. data/tests/oracle/test_adapter.rb +14 -0
  70. data/tests/postgresql/test_adapter.rb +14 -0
  71. metadata +147 -0
@@ -0,0 +1,506 @@
1
+ require 'forwardable'
2
+
3
+ # ActiveRecord::Extensions provides additional functionality to the ActiveRecord
4
+ # ORM library created by DHH for Rails.
5
+ #
6
+ # It's main features include:
7
+ # * better finder support using a :conditions Hash for ActiveRecord::Base#find
8
+ # * better finder support using any object that responds to the to_sql method
9
+ # * mass data import functionality
10
+ # * a more modular design to extending ActiveRecord
11
+ #
12
+ #
13
+ # == Using Better Finder Hash Support
14
+ # Here are a few examples, please refer to the class documentation for each
15
+ # extensions:
16
+ #
17
+ # class Post < ActiveRecord::Base ; end
18
+ #
19
+ # Post.find( :all, :conditions=>{
20
+ # :title => "Title", # title='Title'
21
+ # :author_contains => "Zach", # author like '%Zach%'
22
+ # :author_starts_with => "Zach", # author like 'Zach%'
23
+ # :author_ends_with => "Dennis", # author like '%Zach'
24
+ # :published_at => (Date.now-30 .. Date.now), # published_at BETWEEN xxx AND xxx
25
+ # :rating => [ 4, 5, 6 ], # rating IN ( 4, 5, 6 )
26
+ # :rating_not_in => [ 7, 8, 9 ] # rating NOT IN( 4, 5, 6 )
27
+ # :rating_ne => 4, # rating != 4
28
+ # :rating_gt => 4, # rating > 4
29
+ # :rating_lt => 4, # rating < 4
30
+ # :content => /(a|b|c)/ # REGEXP '(a|b|c)'
31
+ # )
32
+ #
33
+ #
34
+ # == Create Your Own Finder Extension Example
35
+ # The following example shows you how-to create a robust and reliable
36
+ # finder extension which allows you to use Ranges in your :conditions Hash. This
37
+ # is the actual implementation in ActiveRecord::Extensions.
38
+ #
39
+ # class RangeExt
40
+ # NOT_IN_RGX = /^(.+)_(ne|not|not_in|not_between)$/
41
+ #
42
+ # def self.process( key, val, caller )
43
+ # return nil unless val.is_a?( Range )
44
+ # match_data = key.to_s.match( NOT_IN_RGX )
45
+ # key = match_data.captures[0] if match_data
46
+ # fieldname = caller.connection.quote_column_name( key )
47
+ # min = caller.connection.quote( val.first, caller.columns_hash[ key ] )
48
+ # max = caller.connection.quote( val.last, caller.columns_hash[ key ] )
49
+ # str = "#{caller.quoted_table_name}.#{fieldname} #{match_data ? 'NOT ' : '' } BETWEEN #{min} AND #{max}"
50
+ # Result.new( str, nil )
51
+ # end
52
+ #
53
+ #
54
+ # == Using to_sql Ducks In Your Find Methods!
55
+ # The below example shows you how-to utilize objects that respond_to the method +to_sql+ in
56
+ # your finds:
57
+ #
58
+ # class InsuranceClaim < ActiveRecord::Base ; end
59
+ #
60
+ # class InsuranceClaimAgeAndTypeQuery
61
+ # def to_sql
62
+ # "age_in_days BETWEEN 1 AND 60 AND claim_type IN( 'typea', 'typeb' )"
63
+ # end
64
+ # end
65
+ #
66
+ # claims = InsuranceClaim.find( :all, InsuranceClaimAgeAndTypeQuery.new )
67
+ #
68
+ # claims = InsuranceClaim.find( :all, :conditions=>{
69
+ # :claim_amount_gt => 30000,
70
+ # :age_and_type => InsuranceClaimAgeAndTypeQuery.new }
71
+ # )
72
+ #
73
+ # == Importing Lots of Data
74
+ #
75
+ # ActiveRecord executes a single INSERT statement for every call to 'create'
76
+ # and for every call to 'save' on a new model object. When you have only
77
+ # a handful of records to create or save this is not a big deal, but when
78
+ # you have hundreds, thousands or hundreds of thousands of records
79
+ # you need to have better performance.
80
+ #
81
+ # Below is an example of how to import the least amount of INSERT statements
82
+ # using mechanisms provided by your database vendor:
83
+ #
84
+ # class Student < ActiveRecord::Base ; end
85
+ #
86
+ # column_names = Student.columns.map{ |column| column.name }
87
+ # value_sets = some_method_to_load_data_from_csv_file( 'students.csv' )
88
+ # options = { :valudate => true }
89
+ #
90
+ # Student.import( column_names, value_sets, options )
91
+ #
92
+ # The +import+ functionality can be used even if there is not specific
93
+ # support for you vendor. This happens when a particular database vendor
94
+ # specific enhancement hasn't been added to ActiveRecord::Extensions.
95
+ # You can still use +import+ though because the +import+ functionality has
96
+ # been created with backwards compatibility. You may still get better
97
+ # performance using +import+, but you will definitely get no worse then
98
+ # ActiveRecord's create or save methods.
99
+ #
100
+ # See ActiveRecord::Base.import for more information and other ways to use
101
+ # this functionality.
102
+ #
103
+ # == Developers
104
+ # * Zach Dennis
105
+ # * Mark Van Holsytn
106
+ #
107
+ # == Homepage
108
+ # * Project Site: http://www.continuousthinking.com/tags/arext
109
+ # * Rubyforge Project: http://rubyforge.org/projects/arext
110
+ # * Anonymous SVN: svn checkout svn://rubyforge.org/var/svn/arext
111
+ #
112
+ module ActiveRecord::Extensions
113
+
114
+ Result = Struct.new( :sql, :value )
115
+
116
+ # ActiveRecored::Extensions::Registry is used to register finder extensions.
117
+ # Extensions are processed in last in first out order, like a stack.
118
+ class Registry # :nodoc:
119
+
120
+ def options( extension )
121
+ extension_arr = @registry.detect{ |arr| arr.first == extension }
122
+ return unless extension_arr
123
+ extension_arr.last
124
+ end
125
+
126
+ def registers?( extension ) # :nodoc:
127
+ @registry.detect{ |arr| arr.first == extension }
128
+ end
129
+
130
+ def register( extension, options ) # :nodoc:
131
+ @registry << [ extension, options ]
132
+ end
133
+
134
+ def initialize # :nodoc:
135
+ @registry = []
136
+ end
137
+
138
+ def process( field, value, caller ) # :nodoc:
139
+ current_adapter = caller.connection.adapter_name.downcase
140
+ @registry.reverse.each do |(extension,options)|
141
+ adapters = options[:adapters]
142
+ adapters.map!{ |e| e.to_s } unless adapters == :all
143
+ next if options[:adapters] != :all and adapters.grep( /#{current_adapter}/ ).empty?
144
+ if result=extension.process( field, value, caller )
145
+ return result
146
+ end
147
+ end
148
+ nil
149
+ end
150
+
151
+ end
152
+
153
+ class << self
154
+ extend Forwardable
155
+
156
+ def register( extension, options ) # :nodoc:
157
+ @registry ||= Registry.new
158
+ @registry.register( extension, options )
159
+ end
160
+
161
+ def_delegator :@registry, :process, :process
162
+ end
163
+
164
+
165
+ # ActiveRecord::Extension to translate an Array of values
166
+ # into the approriate IN( ... ) or NOT IN( ... ) SQL.
167
+ #
168
+ # == Examples
169
+ # Model.find :all, :conditions=>{ :id => [ 1,2,3 ] }
170
+ #
171
+ # # the following three calls are equivalent
172
+ # Model.find :all, :conditions=>{ :id_ne => [ 4,5,6 ] }
173
+ # Model.find :all, :conditions=>{ :id_not => [ 4,5,6 ] }
174
+ # Model.find :all, :conditions=>{ :id_not_in => [ 4,5,6 ] }
175
+ class ArrayExt
176
+
177
+ NOT_EQUAL_RGX = /(.+)_(ne|not|not_in)/
178
+
179
+ def self.process( key, val, caller )
180
+ if val.is_a?( Array )
181
+ match_data = key.to_s.match( NOT_EQUAL_RGX )
182
+ key = match_data.captures[0] if match_data
183
+ str = "#{caller.quoted_table_name}.#{caller.connection.quote_column_name( key )} " +
184
+ (match_data ? 'NOT ' : '') + "IN( ? )"
185
+ return Result.new( str, val )
186
+ end
187
+ nil
188
+ end
189
+
190
+ end
191
+
192
+
193
+ # ActiveRecord::Extension to translate Hash keys which end in
194
+ # +_lt+, +_lte+, +_gt+, or +_gte+ with the approriate <, <=, >,
195
+ # or >= symbols.
196
+ # * +_lt+ - denotes less than
197
+ # * +_gt+ - denotes greater than
198
+ # * +_lte+ - denotes less than or equal to
199
+ # * +_gte+ - denotes greater than or equal to
200
+ #
201
+ # == Examples
202
+ # Model.find :all, :conditions=>{ 'number_gt'=>100 }
203
+ # Model.find :all, :conditions=>{ 'number_lt'=>100 }
204
+ # Model.find :all, :conditions=>{ 'number_gte'=>100 }
205
+ # Model.find :all, :conditions=>{ 'number_lte'=>100 }
206
+ class Comparison
207
+
208
+ SUFFIX_MAP = { 'eq'=>'=', 'lt'=>'<', 'lte'=>'<=', 'gt'=>'>', 'gte'=>'>=', 'ne'=>'!=', 'not'=>'!=' }
209
+ ACCEPTABLE_COMPARISONS = [ String, Numeric, Time, DateTime ]
210
+
211
+ def self.process( key, val, caller )
212
+ process_without_suffix( key, val, caller ) || process_with_suffix( key, val, caller )
213
+ end
214
+
215
+ def self.process_without_suffix( key, val, caller )
216
+ return nil unless caller.columns_hash.has_key?( key )
217
+ if val.nil?
218
+ str = "#{caller.quoted_table_name}.#{caller.connection.quote_column_name( key )} is ?"
219
+ else
220
+ str = "#{caller.quoted_table_name}.#{caller.connection.quote_column_name( key )}=?"
221
+ end
222
+ Result.new( str, val )
223
+ end
224
+
225
+ def self.process_with_suffix( key, val, caller )
226
+ return nil unless ACCEPTABLE_COMPARISONS.find{ |klass| val.is_a?(klass) }
227
+ SUFFIX_MAP.each_pair do |k,v|
228
+ match_data = key.to_s.match( /(.+)_#{k}$/ )
229
+ if match_data
230
+ fieldname = match_data.captures[0]
231
+ return nil unless caller.columns_hash.has_key?( fieldname )
232
+ str = "#{caller.quoted_table_name}.#{caller.connection.quote_column_name( fieldname )} " +
233
+ "#{v} #{caller.connection.quote( val, caller.columns_hash[ fieldname ] )} "
234
+ return Result.new( str, nil )
235
+ end
236
+ end
237
+ nil
238
+ end
239
+
240
+ end
241
+
242
+
243
+ # ActiveRecord::Extension to translate Hash keys which end in
244
+ # +_like+ or +_contains+ with the approriate LIKE keyword
245
+ # used in SQL.
246
+ #
247
+ # == Examples
248
+ # # the below two examples are equivalent
249
+ # Model.find :all, :conditions=>{ :name_like => 'John' }
250
+ # Model.find :all, :conditions=>{ :name_contains => 'John' }
251
+ #
252
+ # Model.find :all, :conditions=>{ :name_starts_with => 'J' }
253
+ # Model.find :all, :conditions=>{ :name_ends_with => 'n' }
254
+ class Like
255
+ LIKE_RGX = /(.+)_(like|contains)$/
256
+ STARTS_WITH_RGX = /(.+)_starts_with$/
257
+ ENDS_WITH_RGX = /(.+)_ends_with$/
258
+ def self.process( key, val, caller )
259
+ values = [*val]
260
+ case key.to_s
261
+ when LIKE_RGX
262
+ str = values.collect do |v|
263
+ "#{caller.quoted_table_name}.#{caller.connection.quote_column_name( $1 )} LIKE " +
264
+ "#{caller.connection.quote( '%%' + v + '%%', caller.columns_hash[ $1 ] )} "
265
+ end
266
+ when STARTS_WITH_RGX
267
+ str = values.collect do |v|
268
+ "#{caller.quoted_table_name}.#{caller.connection.quote_column_name( $1 )} LIKE " +
269
+ "#{caller.connection.quote( v + '%%', caller.columns_hash[ $1 ] )} "
270
+ end
271
+ when ENDS_WITH_RGX
272
+ str = values.collect do |v|
273
+ "#{caller.quoted_table_name}.#{caller.connection.quote_column_name( $1 )} LIKE " +
274
+ "#{caller.connection.quote( '%%' + v, caller.columns_hash[ $1 ] )} "
275
+ end
276
+ else
277
+ return nil
278
+ end
279
+
280
+ str = str.join(' OR ')
281
+ result_values = []
282
+ str.gsub!(/'((%%)?([^\?]*\?[^%]*|[^%]*%[^%]*)(%%)?)'/) do |match|
283
+ result_values << $2
284
+ '?'
285
+ end
286
+ result_values = nil if result_values.empty?
287
+ return Result.new(str , result_values)
288
+ end
289
+ end
290
+
291
+
292
+ # ActiveRecord::Extension to translate a ruby Range object into SQL's BETWEEN ... AND ...
293
+ # or NOT BETWEEN ... AND ... . This works on Ranges of Numbers, Dates, Times, etc.
294
+ #
295
+ # == Examples
296
+ # # the following two statements are identical because of how Ranges treat .. and ...
297
+ # Model.find :all, :conditions=>{ :id => ( 1 .. 2 ) }
298
+ # Model.find :all, :conditions=>{ :id => ( 1 ... 2 ) }
299
+ #
300
+ # # the following four statements are identical, this finds NOT BETWEEN matches
301
+ # Model.find :all, :conditions=>{ :id_ne => ( 4 .. 6 ) }
302
+ # Model.find :all, :conditions=>{ :id_not => ( 4 .. 6 ) }
303
+ # Model.find :all, :conditions=>{ :id_not_in => ( 4 ..6 ) }
304
+ # Model.find :all, :conditions=>{ :id_not_between => ( 4 .. 6 ) }
305
+ #
306
+ # # a little more creative, working with date ranges
307
+ # Model.find :all, :conditions=>{ :created_on => (Date.now-30 .. Date.now) }
308
+ class RangeExt
309
+ NOT_IN_RGX = /^(.+)_(ne|not|not_in|not_between)$/
310
+
311
+ def self.process( key, val, caller )
312
+ if val.is_a?( Range )
313
+ match_data = key.to_s.match( NOT_IN_RGX )
314
+ key = match_data.captures[0] if match_data
315
+ fieldname = caller.connection.quote_column_name( key )
316
+ min = caller.connection.quote( val.first, caller.columns_hash[ key ] )
317
+ max = caller.connection.quote( val.last, caller.columns_hash[ key ] )
318
+ str = if val.exclude_end?
319
+ "#{match_data ? 'NOT ' : '' }(#{caller.quoted_table_name}.#{fieldname} >= #{min} AND #{caller.quoted_table_name}.#{fieldname} < #{max})"
320
+ else
321
+ "#{caller.quoted_table_name}.#{fieldname} #{match_data ? 'NOT ' : '' } BETWEEN #{min} AND #{max}"
322
+ end
323
+
324
+ return Result.new( str, nil )
325
+ end
326
+ nil
327
+ end
328
+
329
+ end
330
+
331
+ # A base class for database vendor specific Regexp implementations. This is meant to be
332
+ # subclassed only because of the helper method(s) it provides.
333
+ class RegexpBase
334
+
335
+ NOT_EQUAL_RGX = /^(.+)_(ne|not|does_not_match)$/
336
+
337
+ # A result class which provides an easy interface.
338
+ class RegexpResult
339
+ attr_reader :fieldname, :negate
340
+
341
+ def initialize( fieldname, negate=false )
342
+ @fieldname, @negate = fieldname, negate
343
+ end
344
+
345
+ def negate?
346
+ negate ? true : false
347
+ end
348
+ end
349
+
350
+ # Given the passed in +str+ and +caller+ this will return a RegexpResult object
351
+ # which gives the database quoted fieldname/column and can tell you whether or not
352
+ # the original +str+ is indicating a negated regular expression.
353
+ #
354
+ # == Examples
355
+ # r = RegexpBase.field_result( 'id' )
356
+ # r.fieldname => # 'id'
357
+ # r.negate? => # false
358
+ #
359
+ # r = RegexpBase.field_result( 'id_ne' )
360
+ # r.fieldname => # 'id'
361
+ # r.negate? => # true
362
+ #
363
+ # r = RegexpBase.field_result( 'id_not' )
364
+ # r.fieldname => # 'id'
365
+ # r.negate? => # true
366
+ #
367
+ # r = RegexpBase.field_result( 'id_does_not_match' )
368
+ # r.fieldname => # 'id'
369
+ # r.negate? => # true
370
+ def self.field_result( str, caller )
371
+ negate = false
372
+ if match_data=str.to_s.match( NOT_EQUAL_RGX )
373
+ negate = true
374
+ str = match_data.captures[0]
375
+ end
376
+ fieldname = caller.connection.quote_column_name( str )
377
+ RegexpResult.new( fieldname, negate )
378
+ end
379
+
380
+ end
381
+
382
+
383
+ # ActiveRecord::Extension for implementing Regexp implementation for MySQL.
384
+ # See documention for RegexpBase.
385
+ class MySQLRegexp < RegexpBase
386
+
387
+ def self.process( key, val, caller )
388
+ return nil unless val.is_a?( Regexp )
389
+ r = field_result( key, caller )
390
+ Result.new( "#{caller.quoted_table_name}.#{r.fieldname} #{r.negate? ? 'NOT ':''} REGEXP ?", val )
391
+ end
392
+
393
+ end
394
+
395
+
396
+ # ActiveRecord::Extension for implementing Regexp implementation for PostgreSQL.
397
+ # See documention for RegexpBase.
398
+ #
399
+ # Note: this doesn't support case insensitive matches.
400
+ class PostgreSQLRegexp < RegexpBase
401
+
402
+ def self.process( key, val, caller )
403
+ return nil unless val.is_a?( Regexp )
404
+ r = field_result( key, caller )
405
+ return Result.new( "#{caller.quoted_table_name}.#{r.fieldname} #{r.negate? ? '!~ ':'~'} ?", val )
406
+ end
407
+
408
+ end
409
+
410
+ # ActiveRecord::Extension for implementing Regexp implementation for Oracle.
411
+ # See documention for RegexpBase.
412
+ #
413
+ class OracleRegexp < RegexpBase
414
+
415
+ def self.process( key, val, caller )
416
+ return nil unless val.is_a?( Regexp )
417
+ r = field_result( key, caller )
418
+ return Result.new( "#{r.negate? ? ' NOT ':''} REGEXP_LIKE(#{caller.quoted_table_name}.#{r.fieldname} , ?)", val )
419
+ end
420
+
421
+ end
422
+
423
+
424
+ # ActiveRecord::Extension for implementing Regexp implementation for MySQL.
425
+ # See documention for RegexpBase.
426
+ class SqliteRegexp < RegexpBase
427
+ class_inheritable_accessor :connections
428
+ self.connections = []
429
+
430
+ def self.add_rlike_function( connection )
431
+ self.connections << connection
432
+ unless connection.respond_to?( 'sqlite_regexp_support?' )
433
+ class << connection
434
+ def sqlite_regexp_support? ; true ; end
435
+ end
436
+ connection.instance_eval( '@connection' ).create_function( 'rlike', 3 ) do |func, a, b, negate|
437
+ if negate =~ /true/
438
+ func.set_result 1 if a.to_s !~ /#{b}/
439
+ else
440
+ func.set_result 1 if a.to_s =~ /#{b}/
441
+ end
442
+ end
443
+ end
444
+ end
445
+
446
+ def self.process( key, val, caller )
447
+ return nil unless val.is_a?( Regexp )
448
+ r = field_result( key, caller )
449
+ unless self.connections.include?( caller.connection )
450
+ add_rlike_function( caller.connection )
451
+ end
452
+ Result.new( "rlike( #{r.fieldname}, ?, '#{r.negate?}' )", val )
453
+ end
454
+
455
+ end
456
+
457
+ class DatetimeSupport
458
+ SUFFIX_MAP = { 'eq'=>'=', 'lt'=>'<', 'lte'=>'<=', 'gt'=>'>', 'gte'=>'>=', 'ne'=>'!=', 'not'=>'!=' }
459
+
460
+ def self.process( key, val, caller )
461
+ return unless val.is_a?( Time )
462
+ process_without_suffix( key, val, caller ) || process_with_suffix( key, val, caller )
463
+ end
464
+
465
+ def self.process_without_suffix( key, val, caller )
466
+ return nil unless caller.columns_hash.has_key?( key )
467
+ if val.nil?
468
+ str = "#{caller.quoted_table_name}.#{caller.connection.quote_column_name( key )} IS NULL"
469
+ else
470
+ str = "#{caller.quoted_table_name}.#{caller.connection.quote_column_name( key )}=" +
471
+ "#{caller.connection.quote( val.to_s(:db), caller.columns_hash[ key ] )} "
472
+ end
473
+ Result.new( str, nil )
474
+ end
475
+
476
+ def self.process_with_suffix( key, val, caller )
477
+ SUFFIX_MAP.each_pair do |k,v|
478
+ match_data = key.to_s.match( /(.+)_#{k}$/ )
479
+ if match_data
480
+ fieldname = match_data.captures[0]
481
+ return nil unless caller.columns_hash.has_key?( fieldname )
482
+ str = "#{caller.quoted_table_name}.#{caller.connection.quote_column_name( fieldname )} " +
483
+ "#{v} #{caller.connection.quote( val.to_s(:db), caller.columns_hash[ fieldname ] )} "
484
+ return Result.new( str, nil )
485
+ end
486
+ end
487
+ nil
488
+ end
489
+
490
+
491
+ end
492
+
493
+
494
+ register Comparison, :adapters=>:all
495
+ register ArrayExt, :adapters=>:all
496
+ register Like, :adapters=>:all
497
+ register RangeExt, :adapters=>:all
498
+ register MySQLRegexp, :adapters=>[ :mysql ]
499
+ register PostgreSQLRegexp, :adapters=>[ :postgresql ]
500
+ register SqliteRegexp, :adapters =>[ :sqlite ]
501
+ register OracleRegexp, :adapters =>[ :oracle ]
502
+ register DatetimeSupport, :adapters =>[ :mysql, :sqlite, :oracle ]
503
+ end
504
+
505
+
506
+