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.
- data/ChangeLog +145 -0
- data/README +169 -0
- data/Rakefile +61 -0
- data/config/database.yml +7 -0
- data/config/database.yml.template +7 -0
- data/config/mysql.schema +72 -0
- data/config/postgresql.schema +39 -0
- data/db/migrate/generic_schema.rb +97 -0
- data/db/migrate/mysql_schema.rb +32 -0
- data/db/migrate/oracle_schema.rb +5 -0
- data/db/migrate/version.rb +4 -0
- data/init.rb +31 -0
- data/lib/ar-extensions/adapters/abstract_adapter.rb +146 -0
- data/lib/ar-extensions/adapters/mysql.rb +10 -0
- data/lib/ar-extensions/adapters/oracle.rb +14 -0
- data/lib/ar-extensions/adapters/postgresql.rb +9 -0
- data/lib/ar-extensions/adapters/sqlite.rb +7 -0
- data/lib/ar-extensions/create_and_update/mysql.rb +7 -0
- data/lib/ar-extensions/create_and_update.rb +509 -0
- data/lib/ar-extensions/csv.rb +309 -0
- data/lib/ar-extensions/delete/mysql.rb +3 -0
- data/lib/ar-extensions/delete.rb +143 -0
- data/lib/ar-extensions/extensions.rb +513 -0
- data/lib/ar-extensions/finder_options/mysql.rb +6 -0
- data/lib/ar-extensions/finder_options.rb +275 -0
- data/lib/ar-extensions/finders.rb +94 -0
- data/lib/ar-extensions/foreign_keys.rb +70 -0
- data/lib/ar-extensions/fulltext/mysql.rb +44 -0
- data/lib/ar-extensions/fulltext.rb +62 -0
- data/lib/ar-extensions/import/mysql.rb +50 -0
- data/lib/ar-extensions/import/postgresql.rb +0 -0
- data/lib/ar-extensions/import/sqlite.rb +22 -0
- data/lib/ar-extensions/import.rb +348 -0
- data/lib/ar-extensions/insert_select/mysql.rb +7 -0
- data/lib/ar-extensions/insert_select.rb +178 -0
- data/lib/ar-extensions/synchronize.rb +30 -0
- data/lib/ar-extensions/temporary_table/mysql.rb +3 -0
- data/lib/ar-extensions/temporary_table.rb +131 -0
- data/lib/ar-extensions/union/mysql.rb +6 -0
- data/lib/ar-extensions/union.rb +204 -0
- data/lib/ar-extensions/util/sql_generation.rb +27 -0
- data/lib/ar-extensions/util/support_methods.rb +32 -0
- data/lib/ar-extensions/version.rb +9 -0
- data/lib/ar-extensions.rb +5 -0
- 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
|