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,352 @@
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
+ # add support for composite primary keys: check that the set of
189
+ # primary key(s) (converted to symbols) is a subset of the
190
+ # columns being imported, as symbols -jorahood
191
+ if ![primary_key].flatten.*.to_sym.to_set.subset?(column_names.*.to_sym.to_set) &&
192
+ sequence_name && connection.prefetch_primary_key?
193
+ column_names << primary_key
194
+ array_of_attributes.each { |a| a << nil }
195
+ end
196
+
197
+ is_validating = options.delete( :validate )
198
+
199
+ # dup the passed in array so we don't modify it unintentionally
200
+ array_of_attributes = array_of_attributes.dup
201
+
202
+ # record timestamps unless disabled in ActiveRecord::Base
203
+ if record_timestamps && options.delete( :timestamps )
204
+ add_special_rails_stamps column_names, array_of_attributes, options
205
+ end
206
+
207
+ return_obj = if is_validating
208
+ import_with_validations( column_names, array_of_attributes, options )
209
+ else
210
+ num_inserts = import_without_validations_or_callbacks( column_names, array_of_attributes, options )
211
+ OpenStruct.new :failed_instances=>[], :num_inserts=>num_inserts
212
+ end
213
+ if options[:synchronize]
214
+ synchronize( options[:synchronize] )
215
+ end
216
+
217
+ return_obj.num_inserts = 0 if return_obj.num_inserts.nil?
218
+ return_obj
219
+ end
220
+
221
+ # TODO import_from_table needs to be implemented.
222
+ def import_from_table( options ) # :nodoc:
223
+ end
224
+
225
+ # Imports the passed in +column_names+ and +array_of_attributes+
226
+ # given the passed in +options+ Hash with validations. Returns an
227
+ # object with the methods +failed_instances+ and +num_inserts+.
228
+ # +failed_instances+ is an array of instances that failed validations.
229
+ # +num_inserts+ is the number of inserts it took to import the data. See
230
+ # ActiveRecord::Base.import for more information on
231
+ # +column_names+, +array_of_attributes+ and +options+.
232
+ def import_with_validations( column_names, array_of_attributes, options={} )
233
+ failed_instances = []
234
+
235
+ # create instances for each of our column/value sets
236
+ arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
237
+
238
+ # keep track of the instance and the position it is currently at. if this fails
239
+ # validation we'll use the index to remove it from the array_of_attributes
240
+ arr.each_with_index do |hsh,i|
241
+ instance = new( hsh )
242
+ if not instance.valid?
243
+ array_of_attributes[ i ] = nil
244
+ failed_instances << instance
245
+ end
246
+ end
247
+ array_of_attributes.compact!
248
+
249
+ num_inserts = array_of_attributes.empty? ? 0 : import_without_validations_or_callbacks( column_names, array_of_attributes, options )
250
+ OpenStruct.new :failed_instances=>failed_instances, :num_inserts => num_inserts
251
+ end
252
+
253
+ # Imports the passed in +column_names+ and +array_of_attributes+
254
+ # given the passed in +options+ Hash. This will return the number
255
+ # of insert operations it took to create these records without
256
+ # validations or callbacks. See ActiveRecord::Base.import for more
257
+ # information on +column_names+, +array_of_attributes_ and
258
+ # +options+.
259
+ def import_without_validations_or_callbacks( column_names, array_of_attributes, options={} )
260
+ escaped_column_names = quote_column_names( column_names )
261
+ columns = []
262
+ array_of_attributes.first.each_with_index { |arr,i| columns << columns_hash[ column_names[i] ] }
263
+
264
+ if not supports_import?
265
+ columns_sql = "(" + escaped_column_names.join( ',' ) + ")"
266
+ insert_statements, values = [], []
267
+ number_inserted = 0
268
+ array_of_attributes.each do |arr|
269
+ my_values = []
270
+ arr.each_with_index do |val,j|
271
+ if !sequence_name.blank? && column_names[j] == primary_key && val.nil?
272
+ my_values << connection.next_value_for_sequence(sequence_name)
273
+ else
274
+ my_values << connection.quote( val, columns[j] )
275
+ end
276
+ end
277
+ insert_statements << "INSERT INTO #{quoted_table_name} #{columns_sql} VALUES(" + my_values.join( ',' ) + ")"
278
+ connection.execute( insert_statements.last )
279
+ number_inserted += 1
280
+ end
281
+ else
282
+ # generate the sql
283
+ insert_sql = connection.multiple_value_sets_insert_sql( quoted_table_name, escaped_column_names, options )
284
+ values_sql = connection.values_sql_for_column_names_and_attributes( columns, array_of_attributes )
285
+ post_sql_statements = connection.post_sql_statements( quoted_table_name, options )
286
+
287
+ # perform the inserts
288
+ number_inserted = connection.insert_many( [ insert_sql, post_sql_statements ].flatten,
289
+ values_sql,
290
+ "#{self.class.name} Create Many Without Validations Or Callbacks" )
291
+ end
292
+
293
+ number_inserted
294
+ end
295
+
296
+ # Returns an array of quoted column names
297
+ def quote_column_names( names )
298
+ names.map{ |name| connection.quote_column_name( name ) }
299
+ end
300
+
301
+
302
+ private
303
+
304
+
305
+ def add_special_rails_stamps( column_names, array_of_attributes, options )
306
+ AREXT_RAILS_COLUMNS[:create].each_pair do |key, blk|
307
+ if self.column_names.include?(key)
308
+ value = blk.call
309
+ if index=column_names.index(key)
310
+ # replace every instance of the array of attributes with our value
311
+ array_of_attributes.each{ |arr| arr[index] = value }
312
+ else
313
+ column_names << key
314
+ array_of_attributes.each { |arr| arr << value }
315
+ end
316
+ end
317
+ end
318
+
319
+ AREXT_RAILS_COLUMNS[:update].each_pair do |key, blk|
320
+ if self.column_names.include?(key)
321
+ value = blk.call
322
+ if index=column_names.index(key)
323
+ # replace every instance of the array of attributes with our value
324
+ array_of_attributes.each{ |arr| arr[index] = value }
325
+ else
326
+ column_names << key
327
+ array_of_attributes.each { |arr| arr << value }
328
+ end
329
+
330
+ if options[:on_duplicate_key_update]
331
+ options[:on_duplicate_key_update] << key.to_sym if options[:on_duplicate_key_update].is_a?(Array)
332
+ options[:on_duplicate_key_update][key.to_sym] = key.to_sym if options[:on_duplicate_key_update].is_a?(Hash)
333
+ else
334
+ options[:on_duplicate_key_update] = [ key.to_sym ]
335
+ end
336
+ end
337
+ end
338
+ end
339
+
340
+ # Returns an Array of Hashes for the passed in +column_names+ and +array_of_attributes+.
341
+ def validations_array_for_column_names_and_attributes( column_names, array_of_attributes ) # :nodoc:
342
+ arr = []
343
+ array_of_attributes.each do |attributes|
344
+ c = 0
345
+ hsh = attributes.inject( {} ){|hsh,attr| hsh[ column_names[c] ] = attr ; c+=1 ; hsh }
346
+ arr << hsh
347
+ end
348
+ arr
349
+ end
350
+
351
+ end
352
+ 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