ghazel-ar-extensions 0.9.3

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