ghazel-ar-extensions 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/ChangeLog +145 -0
  2. data/README +169 -0
  3. data/Rakefile +61 -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 +97 -0
  9. data/db/migrate/mysql_schema.rb +32 -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/adapters/abstract_adapter.rb +146 -0
  14. data/lib/ar-extensions/adapters/mysql.rb +10 -0
  15. data/lib/ar-extensions/adapters/oracle.rb +14 -0
  16. data/lib/ar-extensions/adapters/postgresql.rb +9 -0
  17. data/lib/ar-extensions/adapters/sqlite.rb +7 -0
  18. data/lib/ar-extensions/create_and_update/mysql.rb +7 -0
  19. data/lib/ar-extensions/create_and_update.rb +509 -0
  20. data/lib/ar-extensions/csv.rb +309 -0
  21. data/lib/ar-extensions/delete/mysql.rb +3 -0
  22. data/lib/ar-extensions/delete.rb +143 -0
  23. data/lib/ar-extensions/extensions.rb +513 -0
  24. data/lib/ar-extensions/finder_options/mysql.rb +6 -0
  25. data/lib/ar-extensions/finder_options.rb +275 -0
  26. data/lib/ar-extensions/finders.rb +94 -0
  27. data/lib/ar-extensions/foreign_keys.rb +70 -0
  28. data/lib/ar-extensions/fulltext/mysql.rb +44 -0
  29. data/lib/ar-extensions/fulltext.rb +62 -0
  30. data/lib/ar-extensions/import/mysql.rb +50 -0
  31. data/lib/ar-extensions/import/postgresql.rb +0 -0
  32. data/lib/ar-extensions/import/sqlite.rb +22 -0
  33. data/lib/ar-extensions/import.rb +348 -0
  34. data/lib/ar-extensions/insert_select/mysql.rb +7 -0
  35. data/lib/ar-extensions/insert_select.rb +178 -0
  36. data/lib/ar-extensions/synchronize.rb +30 -0
  37. data/lib/ar-extensions/temporary_table/mysql.rb +3 -0
  38. data/lib/ar-extensions/temporary_table.rb +131 -0
  39. data/lib/ar-extensions/union/mysql.rb +6 -0
  40. data/lib/ar-extensions/union.rb +204 -0
  41. data/lib/ar-extensions/util/sql_generation.rb +27 -0
  42. data/lib/ar-extensions/util/support_methods.rb +32 -0
  43. data/lib/ar-extensions/version.rb +9 -0
  44. data/lib/ar-extensions.rb +5 -0
  45. metadata +110 -0
@@ -0,0 +1,348 @@
1
+ module ActiveRecord::Extensions::ConnectionAdapters ; end
2
+
3
+ module ActiveRecord::Extensions::Import #:nodoc:
4
+
5
+ module ImportSupport #:nodoc:
6
+ def supports_import? #:nodoc:
7
+ true
8
+ end
9
+ end
10
+
11
+ module OnDuplicateKeyUpdateSupport #:nodoc:
12
+ def supports_on_duplicate_key_update? #:nodoc:
13
+ true
14
+ end
15
+ end
16
+
17
+ end
18
+
19
+ class ActiveRecord::Base
20
+ class << self
21
+
22
+ # use tz as set in ActiveRecord::Base
23
+ tproc = @@default_timezone == :utc ? lambda { Time.now.utc } : lambda { Time.now }
24
+ AREXT_RAILS_COLUMNS = {
25
+ :create => { "created_on" => tproc ,
26
+ "created_at" => tproc },
27
+ :update => { "updated_on" => tproc ,
28
+ "updated_at" => tproc }
29
+ }
30
+ AREXT_RAILS_COLUMN_NAMES = AREXT_RAILS_COLUMNS[:create].keys + AREXT_RAILS_COLUMNS[:update].keys
31
+
32
+ # Returns true if the current database connection adapter
33
+ # supports import functionality, otherwise returns false.
34
+ def supports_import?
35
+ connection.supports_import?
36
+ rescue NoMethodError
37
+ false
38
+ end
39
+
40
+ # Returns true if the current database connection adapter
41
+ # supports on duplicate key update functionality, otherwise
42
+ # returns false.
43
+ def supports_on_duplicate_key_update?
44
+ connection.supports_on_duplicate_key_update?
45
+ rescue NoMethodError
46
+ false
47
+ end
48
+
49
+ # Imports a collection of values to the database.
50
+ #
51
+ # This is more efficient than using ActiveRecord::Base#create or
52
+ # ActiveRecord::Base#save multiple times. This method works well if
53
+ # you want to create more than one record at a time and do not care
54
+ # about having ActiveRecord objects returned for each record
55
+ # inserted.
56
+ #
57
+ # This can be used with or without validations. It does not utilize
58
+ # the ActiveRecord::Callbacks during creation/modification while
59
+ # performing the import.
60
+ #
61
+ # == Usage
62
+ # Model.import array_of_models
63
+ # Model.import column_names, array_of_values
64
+ # Model.import column_names, array_of_values, options
65
+ #
66
+ # ==== Model.import array_of_models
67
+ #
68
+ # With this form you can call _import_ passing in an array of model
69
+ # objects that you want updated.
70
+ #
71
+ # ==== Model.import column_names, array_of_values
72
+ #
73
+ # The first parameter +column_names+ is an array of symbols or
74
+ # strings which specify the columns that you want to update.
75
+ #
76
+ # The second parameter, +array_of_values+, is an array of
77
+ # arrays. Each subarray is a single set of values for a new
78
+ # record. The order of values in each subarray should match up to
79
+ # the order of the +column_names+.
80
+ #
81
+ # ==== Model.import column_names, array_of_values, options
82
+ #
83
+ # The first two parameters are the same as the above form. The third
84
+ # parameter, +options+, is a hash. This is optional. Please see
85
+ # below for what +options+ are available.
86
+ #
87
+ # == Options
88
+ # * +validate+ - true|false, tells import whether or not to use \
89
+ # ActiveRecord validations. Validations are enforced by default.
90
+ # * +on_duplicate_key_update+ - an Array or Hash, tells import to \
91
+ # use MySQL's ON DUPLICATE KEY UPDATE ability. See On Duplicate\
92
+ # Key Update below.
93
+ # * +synchronize+ - an array of ActiveRecord instances for the model
94
+ # that you are currently importing data into. This synchronizes
95
+ # existing model instances in memory with updates from the import.
96
+ # * +timestamps+ - true|false, tells import to not add timestamps \
97
+ # (if false) even if record timestamps is disabled in ActiveRecord::Base
98
+ #
99
+ # == Examples
100
+ # class BlogPost < ActiveRecord::Base ; end
101
+ #
102
+ # # Example using array of model objects
103
+ # posts = [ BlogPost.new :author_name=>'Zach Dennis', :title=>'AREXT',
104
+ # BlogPost.new :author_name=>'Zach Dennis', :title=>'AREXT2',
105
+ # BlogPost.new :author_name=>'Zach Dennis', :title=>'AREXT3' ]
106
+ # BlogPost.import posts
107
+ #
108
+ # # Example using column_names and array_of_values
109
+ # columns = [ :author_name, :title ]
110
+ # values = [ [ 'zdennis', 'test post' ], [ 'jdoe', 'another test post' ] ]
111
+ # BlogPost.import columns, values
112
+ #
113
+ # # Example using column_names, array_of_value and options
114
+ # columns = [ :author_name, :title ]
115
+ # values = [ [ 'zdennis', 'test post' ], [ 'jdoe', 'another test post' ] ]
116
+ # BlogPost.import( columns, values, :validate => false )
117
+ #
118
+ # # Example synchronizing existing instances in memory
119
+ # post = BlogPost.find_by_author_name( 'zdennis' )
120
+ # puts post.author_name # => 'zdennis'
121
+ # columns = [ :author_name, :title ]
122
+ # values = [ [ 'yoda', 'test post' ] ]
123
+ # BlogPost.import posts, :synchronize=>[ post ]
124
+ # puts post.author_name # => 'yoda'
125
+ #
126
+ # == On Duplicate Key Update (MySQL only)
127
+ #
128
+ # The :on_duplicate_key_update option can be either an Array or a Hash.
129
+ #
130
+ # ==== Using an Array
131
+ #
132
+ # The :on_duplicate_key_update option can be an array of column
133
+ # names. The column names are the only fields that are updated if
134
+ # a duplicate record is found. Below is an example:
135
+ #
136
+ # BlogPost.import columns, values, :on_duplicate_key_update=>[ :date_modified, :content, :author ]
137
+ #
138
+ # ==== Using A Hash
139
+ #
140
+ # The :on_duplicate_key_update option can be a hash of column name
141
+ # to model attribute name mappings. This gives you finer grained
142
+ # control over what fields are updated with what attributes on your
143
+ # model. Below is an example:
144
+ #
145
+ # BlogPost.import columns, attributes, :on_duplicate_key_update=>{ :title => :title }
146
+ #
147
+ # = Returns
148
+ # This returns an object which responds to +failed_instances+ and +num_inserts+.
149
+ # * failed_instances - an array of objects that fails validation and were not committed to the database. An empty array if no validation is performed.
150
+ # * num_inserts - the number of insert statements it took to import the data
151
+ def import( *args )
152
+ @logger = Logger.new(STDOUT)
153
+ @logger.level = Logger::DEBUG
154
+ options = { :validate=>true, :timestamps=>true }
155
+ options.merge!( args.pop ) if args.last.is_a? Hash
156
+
157
+ # assume array of model objects
158
+ if args.last.is_a?( Array ) and args.last.first.is_a? ActiveRecord::Base
159
+ if args.length == 2
160
+ models = args.last
161
+ column_names = args.first
162
+ else
163
+ models = args.first
164
+ column_names = self.column_names.dup
165
+ end
166
+
167
+ array_of_attributes = []
168
+ models.each do |model|
169
+ # this next line breaks sqlite.so with a segmentation fault
170
+ # if model.new_record? || options[:on_duplicate_key_update]
171
+ attributes = []
172
+ column_names.each do |name|
173
+ attributes << model.send( "#{name}_before_type_cast" )
174
+ end
175
+ array_of_attributes << attributes
176
+ # end
177
+ end
178
+ # supports 2-element array and array
179
+ elsif args.size == 2 and args.first.is_a?( Array ) and args.last.is_a?( Array )
180
+ column_names, array_of_attributes = args
181
+ else
182
+ raise ArgumentError.new( "Invalid arguments!" )
183
+ end
184
+
185
+ # Force the primary key col into the insert if it's not
186
+ # on the list and we are using a sequence and stuff a nil
187
+ # value for it into each row so the sequencer will fire later
188
+ if !column_names.include?(primary_key) && sequence_name && connection.prefetch_primary_key?
189
+ column_names << primary_key
190
+ array_of_attributes.each { |a| a << nil }
191
+ end
192
+
193
+ is_validating = options.delete( :validate )
194
+
195
+ # dup the passed in array so we don't modify it unintentionally
196
+ array_of_attributes = array_of_attributes.dup
197
+
198
+ # record timestamps unless disabled in ActiveRecord::Base
199
+ if record_timestamps && options.delete( :timestamps )
200
+ add_special_rails_stamps column_names, array_of_attributes, options
201
+ end
202
+
203
+ return_obj = if is_validating
204
+ import_with_validations( column_names, array_of_attributes, options )
205
+ else
206
+ num_inserts = import_without_validations_or_callbacks( column_names, array_of_attributes, options )
207
+ OpenStruct.new :failed_instances=>[], :num_inserts=>num_inserts
208
+ end
209
+ if options[:synchronize]
210
+ synchronize( options[:synchronize] )
211
+ end
212
+
213
+ return_obj.num_inserts = 0 if return_obj.num_inserts.nil?
214
+ return_obj
215
+ end
216
+
217
+ # TODO import_from_table needs to be implemented.
218
+ def import_from_table( options ) # :nodoc:
219
+ end
220
+
221
+ # Imports the passed in +column_names+ and +array_of_attributes+
222
+ # given the passed in +options+ Hash with validations. Returns an
223
+ # object with the methods +failed_instances+ and +num_inserts+.
224
+ # +failed_instances+ is an array of instances that failed validations.
225
+ # +num_inserts+ is the number of inserts it took to import the data. See
226
+ # ActiveRecord::Base.import for more information on
227
+ # +column_names+, +array_of_attributes+ and +options+.
228
+ def import_with_validations( column_names, array_of_attributes, options={} )
229
+ failed_instances = []
230
+
231
+ # create instances for each of our column/value sets
232
+ arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
233
+
234
+ # keep track of the instance and the position it is currently at. if this fails
235
+ # validation we'll use the index to remove it from the array_of_attributes
236
+ arr.each_with_index do |hsh,i|
237
+ instance = new( hsh )
238
+ if not instance.valid?
239
+ array_of_attributes[ i ] = nil
240
+ failed_instances << instance
241
+ end
242
+ end
243
+ array_of_attributes.compact!
244
+
245
+ num_inserts = array_of_attributes.empty? ? 0 : import_without_validations_or_callbacks( column_names, array_of_attributes, options )
246
+ OpenStruct.new :failed_instances=>failed_instances, :num_inserts => num_inserts
247
+ end
248
+
249
+ # Imports the passed in +column_names+ and +array_of_attributes+
250
+ # given the passed in +options+ Hash. This will return the number
251
+ # of insert operations it took to create these records without
252
+ # validations or callbacks. See ActiveRecord::Base.import for more
253
+ # information on +column_names+, +array_of_attributes_ and
254
+ # +options+.
255
+ def import_without_validations_or_callbacks( column_names, array_of_attributes, options={} )
256
+ escaped_column_names = quote_column_names( column_names )
257
+ columns = []
258
+ array_of_attributes.first.each_with_index { |arr,i| columns << columns_hash[ column_names[i] ] }
259
+
260
+ if not supports_import?
261
+ columns_sql = "(" + escaped_column_names.join( ',' ) + ")"
262
+ insert_statements, values = [], []
263
+ number_inserted = 0
264
+ array_of_attributes.each do |arr|
265
+ my_values = []
266
+ arr.each_with_index do |val,j|
267
+ if !sequence_name.blank? && column_names[j] == primary_key && val.nil?
268
+ my_values << connection.next_value_for_sequence(sequence_name)
269
+ else
270
+ my_values << connection.quote( val, columns[j] )
271
+ end
272
+ end
273
+ insert_statements << "INSERT INTO #{quoted_table_name} #{columns_sql} VALUES(" + my_values.join( ',' ) + ")"
274
+ connection.execute( insert_statements.last )
275
+ number_inserted += 1
276
+ end
277
+ else
278
+ # generate the sql
279
+ insert_sql = connection.multiple_value_sets_insert_sql( quoted_table_name, escaped_column_names, options )
280
+ values_sql = connection.values_sql_for_column_names_and_attributes( columns, array_of_attributes )
281
+ post_sql_statements = connection.post_sql_statements( quoted_table_name, options )
282
+
283
+ # perform the inserts
284
+ number_inserted = connection.insert_many( [ insert_sql, post_sql_statements ].flatten,
285
+ values_sql,
286
+ "#{self.class.name} Create Many Without Validations Or Callbacks" )
287
+ end
288
+
289
+ number_inserted
290
+ end
291
+
292
+ # Returns an array of quoted column names
293
+ def quote_column_names( names )
294
+ names.map{ |name| connection.quote_column_name( name ) }
295
+ end
296
+
297
+
298
+ private
299
+
300
+
301
+ def add_special_rails_stamps( column_names, array_of_attributes, options )
302
+ AREXT_RAILS_COLUMNS[:create].each_pair do |key, blk|
303
+ if self.column_names.include?(key)
304
+ value = blk.call
305
+ if index=column_names.index(key)
306
+ # replace every instance of the array of attributes with our value
307
+ array_of_attributes.each{ |arr| arr[index] = value }
308
+ else
309
+ column_names << key
310
+ array_of_attributes.each { |arr| arr << value }
311
+ end
312
+ end
313
+ end
314
+
315
+ AREXT_RAILS_COLUMNS[:update].each_pair do |key, blk|
316
+ if self.column_names.include?(key)
317
+ value = blk.call
318
+ if index=column_names.index(key)
319
+ # replace every instance of the array of attributes with our value
320
+ array_of_attributes.each{ |arr| arr[index] = value }
321
+ else
322
+ column_names << key
323
+ array_of_attributes.each { |arr| arr << value }
324
+ end
325
+
326
+ if options[:on_duplicate_key_update]
327
+ options[:on_duplicate_key_update] << key.to_sym if options[:on_duplicate_key_update].is_a?(Array)
328
+ options[:on_duplicate_key_update][key.to_sym] = key.to_sym if options[:on_duplicate_key_update].is_a?(Hash)
329
+ else
330
+ options[:on_duplicate_key_update] = [ key.to_sym ]
331
+ end
332
+ end
333
+ end
334
+ end
335
+
336
+ # Returns an Array of Hashes for the passed in +column_names+ and +array_of_attributes+.
337
+ def validations_array_for_column_names_and_attributes( column_names, array_of_attributes ) # :nodoc:
338
+ arr = []
339
+ array_of_attributes.each do |attributes|
340
+ c = 0
341
+ hsh = attributes.inject( {} ){|hsh,attr| hsh[ column_names[c] ] = attr ; c+=1 ; hsh }
342
+ arr << hsh
343
+ end
344
+ arr
345
+ end
346
+
347
+ end
348
+ end
@@ -0,0 +1,7 @@
1
+ #insert select functionality is dependent on finder options and import
2
+ require 'ar-extensions/finder_options/mysql'
3
+ require 'ar-extensions/import/mysql'
4
+
5
+ ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
6
+ include ActiveRecord::Extensions::InsertSelectSupport
7
+ end
@@ -0,0 +1,178 @@
1
+ # Insert records in bulk with a select statement
2
+ #
3
+ # == Parameters
4
+ # * +options+ - the options used for the finder sql (select)
5
+ #
6
+ # === Options
7
+ # Any valid finder options (options for <tt>ActiveRecord::Base.find(:all)</tt> )such as <tt>:joins</tt>, <tt>:conditions</tt>, <tt>:include</tt>, etc including:
8
+ # * <tt>:from</tt> - the symbol, class name or class used for the finder SQL (select)
9
+ # * <tt>:on_duplicate_key_update</tt> - an array of fields to update, or a custom string
10
+ # * <tt>:select</tt> - An array of fields to select or custom string. The SQL will be sanitized and ? replaced with values as with <tt>:conditions</tt>.
11
+ # * <tt>:ignore => true </tt> - will ignore any duplicates
12
+ # * <tt>:into</tt> - Specifies the columns for which data will be inserted. An array of fields to select or custom string.
13
+ #
14
+ # == Examples
15
+ # Create cart items for all books for shopping cart <tt>@cart+
16
+ # setting the +copies+ field to 1, the +updated_at+ field to Time.now and the +created_at+ field to the database function now()
17
+ # CartItem.insert_select(:from => :book,
18
+ # :select => ['books.id, ?, ?, ?, now()', @cart.to_param, 1, Time.now],
19
+ # :into => [:book_id, :shopping_cart_id, :copies, :updated_at, :created_at]})
20
+ #
21
+ # GENERATED SQL example (MySQL):
22
+ # INSERT INTO `cart_items` ( `book_id`, `shopping_cart_id`, `copies`, `updated_at`, `created_at` )
23
+ # SELECT books.id, '134', 1, '2009-03-02 18:28:25', now() FROM `books`
24
+ #
25
+ # A similar example that
26
+ # * uses the class +Book+ instead of symbol <tt>:book</tt>
27
+ # * a custom string (instead of an Array) for the <tt>:select</tt> of the +insert_options+
28
+ # * Updates the +updated_at+ field of all existing cart item. This assumes there is a unique composite index on the +book_id+ and +shopping_cart_id+ fields
29
+ #
30
+ # CartItem.insert_select(:from => Book,
31
+ # :select => ['books.id, ?, ?, ?, now()', @cart.to_param, 1, Time.now],
32
+ # :into => 'cart_items.book_id, shopping_cart_id, copies, updated_at, created_at',
33
+ # :on_duplicate_key_update => [:updated_at])
34
+ # GENERATED SQL example (MySQL):
35
+ # INSERT INTO `cart_items` ( cart_items.book_id, shopping_cart_id, copies, updated_at, created_at )
36
+ # SELECT books.id, '138', 1, '2009-03-02 18:32:34', now() FROM `books`
37
+ # ON DUPLICATE KEY UPDATE `cart_items`.`updated_at`=VALUES(`updated_at`)
38
+ #
39
+ #
40
+ # Similar example ignoring duplicates
41
+ # CartItem.insert_select(:from => :book,
42
+ # :select => ['books.id, ?, ?, ?, now()', @cart.to_param, 1, Time.now],
43
+ # :into => [:book_id, :shopping_cart_id, :copies, :updated_at, :created_at],
44
+ # :ignore => true)
45
+ #
46
+ # == Developers
47
+ # * Blythe Dunham http://blythedunham.com
48
+ #
49
+ # == Homepage
50
+ # * Project Site: http://www.continuousthinking.com/tags/arext
51
+ # * Rubyforge Project: http://rubyforge.org/projects/arext
52
+ # * Anonymous SVN: svn checkout svn://rubyforge.org/var/svn/arext
53
+ #
54
+
55
+ module ActiveRecord::Extensions::ConnectionAdapters; end
56
+
57
+ module ActiveRecord::Extensions::InsertSelectSupport #:nodoc:
58
+ def supports_insert_select? #:nodoc:
59
+ true
60
+ end
61
+ end
62
+
63
+ class ActiveRecord::Base
64
+
65
+ include ActiveRecord::Extensions::SqlGeneration
66
+
67
+ class << self
68
+ # Insert records in bulk with a select statement
69
+ #
70
+ # == Parameters
71
+ # * +options+ - the options used for the finder sql (select)
72
+ #
73
+ # === Options
74
+ # Any valid finder options (options for <tt>ActiveRecord::Base.find(:all)</tt> )such as <tt>:joins</tt>, <tt>:conditions</tt>, <tt>:include</tt>, etc including:
75
+ # * <tt>:from</tt> - the symbol, class name or class used for the finder SQL (select)
76
+ # * <tt>:on_duplicate_key_update</tt> - an array of fields to update, or a custom string
77
+ # * <tt>:select</tt> - An array of fields to select or custom string. The SQL will be sanitized and ? replaced with values as with <tt>:conditions</tt>.
78
+ # * <tt>:ignore => true </tt> - will ignore any duplicates
79
+ # * <tt>:into</tt> - Specifies the columns for which data will be inserted. An array of fields to select or custom string.
80
+ #
81
+ # == Examples
82
+ # Create cart items for all books for shopping cart <tt>@cart+
83
+ # setting the +copies+ field to 1, the +updated_at+ field to Time.now and the +created_at+ field to the database function now()
84
+ # CartItem.insert_select(:from => :book,
85
+ # :select => ['books.id, ?, ?, ?, now()', @cart.to_param, 1, Time.now],
86
+ # :into => [:book_id, :shopping_cart_id, :copies, :updated_at, :created_at]})
87
+ #
88
+ # GENERATED SQL example (MySQL):
89
+ # INSERT INTO `cart_items` ( `book_id`, `shopping_cart_id`, `copies`, `updated_at`, `created_at` )
90
+ # SELECT books.id, '134', 1, '2009-03-02 18:28:25', now() FROM `books`
91
+ #
92
+ # A similar example that
93
+ # * uses the class +Book+ instead of symbol <tt>:book</tt>
94
+ # * a custom string (instead of an Array) for the <tt>:select</tt> of the +insert_options+
95
+ # * Updates the +updated_at+ field of all existing cart item. This assumes there is a unique composite index on the +book_id+ and +shopping_cart_id+ fields
96
+ #
97
+ # CartItem.insert_select(:from => Book,
98
+ # :select => ['books.id, ?, ?, ?, now()', @cart.to_param, 1, Time.now],
99
+ # :into => 'cart_items.book_id, shopping_cart_id, copies, updated_at, created_at',
100
+ # :on_duplicate_key_update => [:updated_at])
101
+ # GENERATED SQL example (MySQL):
102
+ # INSERT INTO `cart_items` ( cart_items.book_id, shopping_cart_id, copies, updated_at, created_at )
103
+ # SELECT books.id, '138', 1, '2009-03-02 18:32:34', now() FROM `books`
104
+ # ON DUPLICATE KEY UPDATE `cart_items`.`updated_at`=VALUES(`updated_at`)
105
+ #
106
+ #
107
+ # Similar example ignoring duplicates
108
+ # CartItem.insert_select(:from => :book,
109
+ # :select => ['books.id, ?, ?, ?, now()', @cart.to_param, 1, Time.now],
110
+ # :into => [:book_id, :shopping_cart_id, :copies, :updated_at, :created_at],
111
+ # :ignore => true)
112
+ def insert_select(options={})
113
+ select_obj = options.delete(:from).to_s.classify.constantize
114
+ #TODO: add batch support for high volume inserts
115
+ #return insert_select_batch(select_obj, select_options, insert_options) if insert_options[:batch]
116
+ sql = construct_insert_select_sql(select_obj, options)
117
+ connection.insert(sql, "#{name} Insert Select #{select_obj}")
118
+ end
119
+
120
+ protected
121
+
122
+ def construct_insert_select_sql(select_obj, options)#:nodoc:
123
+ construct_ar_extension_sql(gather_insert_options(options), valid_insert_select_options) do |sql, into_op|
124
+ sql << " INTO #{quoted_table_name} "
125
+ sql << "( #{into_column_sql(options.delete(:into))} ) "
126
+
127
+ #sanitize the select sql based on the select object
128
+ sql << select_obj.send(:finder_sql_to_string, sanitize_select_options(options))
129
+ sql
130
+ end
131
+ end
132
+
133
+ # return a list of the column names quoted accordingly
134
+ # nil => All columns except primary key (auto update)
135
+ # String => Exact String
136
+ # Array
137
+ # needs sanitation ["?, ?", 5, 'test'] => "5, 'test'" or [":date", {:date => Date.today}] => "12-30-2006"]
138
+ # list of strings or symbols returns quoted values [:start, :name] => `start`, `name` or ['abc'] => `start`
139
+ def select_column_sql(field_list=nil)#:nodoc:
140
+ if field_list.kind_of?(String)
141
+ field_list.dup
142
+ elsif ((field_list.kind_of?(Array) && field_list.first.is_a?(String)) &&
143
+ (field_list.last.is_a?(Hash) || field_list.first.include?('?')))
144
+ sanitize_sql(field_list)
145
+ else
146
+ field_list = field_list.blank? ? self.column_names - [self.primary_key] : [field_list].flatten
147
+ field_list.collect{|field| self.connection.quote_column_name(field.to_s) }.join(", ")
148
+ end
149
+ end
150
+
151
+ alias_method :into_column_sql, :select_column_sql
152
+
153
+ #sanitize the select options for insert select
154
+ def sanitize_select_options(options)#:nodoc:
155
+ o = options.dup
156
+ select = o.delete :select
157
+ o[:override_select] = select ? select_column_sql(select) : ' * '
158
+ o
159
+ end
160
+
161
+
162
+ def valid_insert_select_options#:nodoc:
163
+ @@valid_insert_select_options ||= [:command, :into_pre, :into_post,
164
+ :into_keywords, :ignore,
165
+ :on_duplicate_key_update]
166
+ end
167
+
168
+ #move all the insert options to a seperate map
169
+ def gather_insert_options(options)#:nodoc:
170
+ into_options = valid_insert_select_options.inject(:command => 'INSERT') do |map, o|
171
+ v = options.delete(o)
172
+ map[o] = v if v
173
+ map
174
+ end
175
+ end
176
+
177
+ end
178
+ end
@@ -0,0 +1,30 @@
1
+ module ActiveRecord # :nodoc:
2
+ class Base # :nodoc:
3
+
4
+ # Synchronizes the passed in ActiveRecord instances with data
5
+ # from the database. This is like calling reload
6
+ # on an individual ActiveRecord instance but it is intended for use on
7
+ # multiple instances.
8
+ #
9
+ # This uses one query for all instance updates and then updates existing
10
+ # instances rather sending one query for each instance
11
+ def self.synchronize(instances, key=self.primary_key)
12
+ return if instances.empty?
13
+
14
+ keys = instances.map(&"#{key}".to_sym)
15
+ klass = instances.first.class
16
+ fresh_instances = klass.find( :all, :conditions=>{ key=>keys }, :order=>"#{key} ASC" )
17
+
18
+ instances.each_with_index do |instance, index|
19
+ instance.clear_aggregation_cache
20
+ instance.clear_association_cache
21
+ instance.instance_variable_set '@attributes', fresh_instances[index].attributes
22
+ end
23
+ end
24
+
25
+ # See ActiveRecord::ConnectionAdapters::AbstractAdapter.synchronize
26
+ def synchronize(instances, key=ActiveRecord::Base.primary_key)
27
+ self.class.synchronize(instances, key)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,3 @@
1
+ ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
2
+ include ActiveRecord::Extensions::TemporaryTableSupport
3
+ end