ar-extensions 0.8.2 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
data/ChangeLog CHANGED
@@ -1,3 +1,13 @@
1
+ 2009-04-17 blythedunham <blythedunham@gmail.com>
2
+
3
+ * Added MySQL support for save, create, replace options - :ignore, :on_duplicate_key_update, :keywords, :reload, :keywords, :pre_sql, :post_sql
4
+ * Added MySQL support for find options: :keywords, :pre_sql, :post_sql, :index_hint
5
+ * Added MySQL support for find_union and count_union
6
+ * Added MySQL support for insert_select
7
+ * Added MySQL support for delete_duplicates and delete_all :batch_size => X
8
+ * Updated :on_duplicate_update_key to accept a string in addition to array
9
+ * Fixed Find Extension Range bug to exclude end when ... used instead of ..
10
+
1
11
  2009-03-16 zdennis <zach.dennis@gmail.com>
2
12
 
3
13
  * fixed Rails 2.3.1 and 2.3.2 compatibility issue (Stephen Heuer)
data/Rakefile CHANGED
@@ -66,7 +66,12 @@ namespace :test do
66
66
  Dir.chdir( old_dir )
67
67
  ENV['RUBYOPT'] = old_env
68
68
  end
69
-
69
+
70
+ desc "runs ActiveRecord unit tests for #{adapter} with ActiveRecord::Extensions with ALL available #{adapter} functionality"
71
+ task "#{adapter}_all" do |t|
72
+ ENV['LOAD_ADAPTER_EXTENSIONS'] = adapter.to_s
73
+ Rake::Task["test:activerecord:#{adapter}"].invoke
74
+ end
70
75
  end
71
76
 
72
77
  end
@@ -53,7 +53,7 @@ ActiveRecord::Schema.define do
53
53
 
54
54
  create_table :books, :force=>true do |t|
55
55
  t.column :title, :string, :null=>false
56
- t.column :publisher, :string, :null=>false
56
+ t.column :publisher, :string, :null=>false, :default => 'Default Publisher'
57
57
  t.column :author_name, :string, :null=>false
58
58
  t.column :created_at, :datetime
59
59
  t.column :created_on, :datetime
@@ -68,4 +68,29 @@ ActiveRecord::Schema.define do
68
68
  t.column :developer_id, :integer
69
69
  end
70
70
 
71
+ create_table :shopping_carts, :force=>true do |t|
72
+ t.column :name, :string, :null => true
73
+ t.column :created_at, :datetime
74
+ t.column :updated_at, :datetime
75
+ end
76
+
77
+ create_table :cart_items, :force => true do |t|
78
+ t.column :shopping_cart_id, :string, :null => false
79
+ t.column :book_id, :string, :null => false
80
+ t.column :copies, :integer, :default => 1
81
+ t.column :created_at, :datetime
82
+ t.column :updated_at, :datetime
83
+ end
84
+
85
+ add_index :cart_items, [:shopping_cart_id, :book_id], :unique => true, :name => 'uk_shopping_cart_books'
86
+
87
+ create_table :animals, :force => true do |t|
88
+ t.column :name, :string, :null => false
89
+ t.column :size, :string, :default => nil
90
+ t.column :created_at, :datetime
91
+ t.column :updated_at, :datetime
92
+ end
93
+
94
+ add_index :animals, [:name], :unique => true, :name => 'uk_animals'
95
+
71
96
  end
@@ -17,7 +17,7 @@ ActiveRecord::Schema.define do
17
17
 
18
18
  create_table :books, :options=>'ENGINE=MyISAM', :force=>true do |t|
19
19
  t.column :title, :string, :null=>false
20
- t.column :publisher, :string, :null=>false
20
+ t.column :publisher, :string, :null=>false, :default => 'Default Publisher'
21
21
  t.column :author_name, :string, :null=>false
22
22
  t.column :created_at, :datetime
23
23
  t.column :created_on, :datetime
@@ -1,4 +1,4 @@
1
1
  class SchemaInfo < ActiveRecord::Base
2
2
  set_table_name 'schema_info'
3
- VERSION = 10
3
+ VERSION = 11
4
4
  end
data/init.rb CHANGED
@@ -2,12 +2,30 @@ require 'ostruct'
2
2
  begin ; require 'active_record' ; rescue LoadError; require 'rubygems'; require 'active_record'; end
3
3
 
4
4
  $LOAD_PATH.unshift File.expand_path(File.join(File.dirname(__FILE__), 'lib'))
5
+
6
+ require 'ar-extensions/util/support_methods'
7
+ require 'ar-extensions/util/sql_generation'
5
8
  require 'ar-extensions/version'
9
+ require 'ar-extensions/delete'
6
10
  require 'ar-extensions/extensions'
11
+ require 'ar-extensions/create_and_update'
12
+ require 'ar-extensions/finder_options'
7
13
  require 'ar-extensions/foreign_keys'
8
14
  require 'ar-extensions/fulltext'
9
15
  require 'ar-extensions/import'
16
+ require 'ar-extensions/insert_select'
10
17
  require 'ar-extensions/finders'
11
18
  require 'ar-extensions/synchronize'
12
19
  require 'ar-extensions/temporary_table'
20
+ require 'ar-extensions/union'
13
21
  require 'ar-extensions/adapters/abstract_adapter'
22
+
23
+ #load all available functionality for specified adapter
24
+ # Ex. ENV['LOAD_ADAPTER_EXTENSIONS'] = 'mysql'
25
+ if ENV['LOAD_ADAPTER_EXTENSIONS']
26
+ require "active_record/connection_adapters/#{ENV['LOAD_ADAPTER_EXTENSIONS']}_adapter.rb"
27
+ file_regexp = File.join(File.dirname(__FILE__), 'lib', 'ar-extensions','**',
28
+ "#{ENV['LOAD_ADAPTER_EXTENSIONS']}.rb")
29
+
30
+ Dir.glob(file_regexp){|file| require(file) }
31
+ end
@@ -49,7 +49,23 @@ module ActiveRecord # :nodoc:
49
49
 
50
50
  number_of_inserts
51
51
  end
52
-
52
+
53
+ def pre_sql_statements(options)
54
+ sql = []
55
+ sql << options[:pre_sql] if options[:pre_sql]
56
+ sql << options[:command] if options[:command]
57
+ sql << "IGNORE" if options[:ignore]
58
+
59
+ #add keywords like IGNORE or DELAYED
60
+ if options[:keywords].is_a?(Array)
61
+ sql.concat(options[:keywords])
62
+ elsif options[:keywords]
63
+ sql << options[:keywords].to_s
64
+ end
65
+
66
+ sql
67
+ end
68
+
53
69
  # Synchronizes the passed in ActiveRecord instances with the records in
54
70
  # the database by calling +reload+ on each instance.
55
71
  def after_import_synchronize( instances )
@@ -62,6 +78,13 @@ module ActiveRecord # :nodoc:
62
78
  if options[:on_duplicate_key_update]
63
79
  post_sql_statements << sql_for_on_duplicate_key_update( table_name, options[:on_duplicate_key_update] )
64
80
  end
81
+
82
+ #custom user post_sql
83
+ post_sql_statements << options[:post_sql] if options[:post_sql]
84
+
85
+ #with rollup
86
+ post_sql_statements << rollup_sql if options[:rollup]
87
+
65
88
  post_sql_statements
66
89
  end
67
90
 
@@ -70,7 +93,7 @@ module ActiveRecord # :nodoc:
70
93
  def multiple_value_sets_insert_sql(table_name, column_names, options) # :nodoc:
71
94
  "INSERT #{options[:ignore] ? 'IGNORE ':''}INTO #{table_name} (#{column_names.join(',')}) VALUES "
72
95
  end
73
-
96
+
74
97
  # Returns SQL the VALUES for an INSERT statement given the passed in +columns+
75
98
  # and +array_of_attributes+.
76
99
  def values_sql_for_column_names_and_attributes( columns, array_of_attributes ) # :nodoc:
@@ -5,4 +5,6 @@ ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
5
5
  result = execute( "SHOW VARIABLES like 'max_allowed_packet';" )
6
6
  result.fetch_row[1].to_i
7
7
  end
8
+
9
+ def rollup_sql; " WITH ROLLUP "; end
8
10
  end
@@ -0,0 +1,509 @@
1
+ # ActiveRecord::Extensions::CreateAndUpdate extends ActiveRecord adding additionaly functionality for
2
+ # insert and updates. Methods +create+, +update+, and +save+ accept
3
+ # additional hash map of parameters to allow customization of database access.
4
+ #
5
+ # Include the appropriate adapter file in <tt>environment.rb</tt> to access this functionality
6
+ # require 'ar-extenstion/create_and_update/mysql'
7
+ #
8
+ # === Options
9
+ # * <tt>:pre_sql</tt> inserts SQL before the +INSERT+ or +UPDATE+ command
10
+ # * <tt>:post_sql</tt> appends additional SQL to the end of the statement
11
+ # * <tt>:keywords</tt> additional keywords to follow the command. Examples
12
+ # include +LOW_PRIORITY+, +HIGH_PRIORITY+, +DELAYED+
13
+ # * <tt>:on_duplicate_key_update</tt> - an array of fields (or a custom string) specifying which parameters to
14
+ # update if there is a duplicate row (unique key violoation)
15
+ # * <tt>:ignore => true </tt> - skips insert or update for duplicate existing rows on a unique key value
16
+ # * <tt>:command</tt> an additional command to replace +INSERT+ or +UPDATE+
17
+ # * <tt>:reload</tt> - If a duplicate is ignored (+ignore+) or updated with
18
+ # +on_duplicate_key_update+, the instance is reloaded to reflect the data
19
+ # in the database. If the record is not reloaded, it may contain stale data and
20
+ # <tt>stale_record?</tt> will evaluate to true. If the object is discared after
21
+ # create or update, it is preferrable to avoid reloading the record to avoid
22
+ # superflous queries
23
+ # * <tt>:duplicate_columns</tt> - an Array required with +reload+ to specify the columns used
24
+ # to locate the duplicate record. These are the unique key columns.
25
+ # Refer to the documentation under the +duplicate_columns+ method.
26
+ #
27
+ #
28
+ # === Create Examples
29
+ # Assume that there is a unique key on the +name+ field
30
+ #
31
+ # Create a new giraffe, and ignore the error if a giraffe already exists
32
+ # If a giraffe exists, then the instance of animal is stale, as it may not
33
+ # reflect the data in the database.
34
+ # animal = Animal.create!({:name => 'giraffe', :size => 'big'}, :ignore => true)
35
+ #
36
+ #
37
+ # Create a new giraffe; update the existing +size+ and +updated_at+ fields if the
38
+ # giraffe already exists. The instance of animal is not stale and reloaded
39
+ # to reflect the content in the database.
40
+ # animal = Animal.create({:name => 'giraffe', :size => 'big'},
41
+ # :on_duplicate_key_update => [:size, :updated_at],
42
+ # :duplicate_columns => [:name], :reload => true)
43
+ #
44
+ # Save a new giraffe, ignoring existing duplicates and inserting a comment
45
+ # in the SQL before the insert.
46
+ # giraffe = Animal.new(:name => 'giraffe', :size => 'small')
47
+ # giraffe.save!(:ignore => true, :pre_sql => '/* My Comment */')
48
+ #
49
+ #
50
+ # === Update Examples
51
+ # Update the giraffe with the low priority keyword
52
+ # big_giraffe.update(:keywords => 'LOW_PRIORITY')
53
+ #
54
+ # Update an existing record. If a duplicate exists, it is updated with the
55
+ # fields specified by +:on_duplicate_key_update+. The original instance(big_giraffe) is
56
+ # deleted, and the instance is reloaded to reflect the database (giraffe).
57
+ # big_giraffe = Animal.create!(:name => 'big_giraffe', :size => 'biggest')
58
+ # big_giraffe.name = 'giraffe'
59
+ # big_giraffe.save(:on_duplicate_key_update => [:size, :updated_at],
60
+ # :duplicate_columns => [:name], :reload => true)
61
+ #
62
+ # === Misc
63
+ #
64
+ # <tt>stale_record?</tt> - returns true if the record is stale
65
+ # Example: <tt>animal.stale_record?</tt>
66
+ #
67
+ # == Developers
68
+ # * Blythe Dunham http://blythedunham.com
69
+ #
70
+ # == Homepage
71
+ # * Project Site: http://www.continuousthinking.com/tags/arext
72
+ # * Rubyforge Project: http://rubyforge.org/projects/arext
73
+ # * Anonymous SVN: svn checkout svn://rubyforge.org/var/svn/arext
74
+ #
75
+
76
+ module ActiveRecord::Extensions::ConnectionAdapters; end
77
+
78
+ module ActiveRecord
79
+ module Extensions
80
+
81
+
82
+ # ActiveRecord::Extensions::CreateAndUpdate extends ActiveRecord adding additionaly functionality for
83
+ # insert and updates. Methods +create+, +update+, and +save+ accept
84
+ # additional hash map of parameters to allow customization of database access.
85
+ #
86
+ # Include the appropriate adapter file in <tt>environment.rb</tt> to access this functionality
87
+ # require 'ar-extenstion/create_and_update/mysql'
88
+ #
89
+ # === Options
90
+ # * <tt>:pre_sql</tt> inserts +SQL+ before the +INSERT+ or +UPDATE+ command
91
+ # * <tt>:post_sql</tt> appends additional +SQL+ to the end of the statement
92
+ # * <tt>:keywords</tt> additional keywords to follow the command. Examples
93
+ # include +LOW_PRIORITY+, +HIGH_PRIORITY+, +DELAYED+
94
+ # * <tt>:on_duplicate_key_update</tt> - an array of fields (or a custom string) specifying which parameters to
95
+ # update if there is a duplicate row (unique key violoation)
96
+ # * <tt>:ignore => true </tt> - skips insert or update for duplicate existing rows on a unique key value
97
+ # * <tt>:command</tt> an additional command to replace +INSERT+ or +UPDATE+
98
+ # * <tt>:reload</tt> - If a duplicate is ignored (+ignore+) or updated with
99
+ # +on_duplicate_key_update+, the instance is reloaded to reflect the data
100
+ # in the database. If the record is not reloaded, it may contain stale data and
101
+ # <tt>stale_record?</tt> will evaluate to true. If the object is discared after
102
+ # create or update, it is preferrable to avoid reloading the record to avoid
103
+ # superflous queries
104
+ # * <tt>:duplicate_columns</tt> - an Array required with +reload+ to specify the columns used
105
+ # to locate the duplicate record. These are the unique key columns.
106
+ # Refer to the documentation under the +duplicate_columns+ method.
107
+ #
108
+ #
109
+ # === Create Examples
110
+ # Assume that there is a unique key on the +name+ field
111
+ #
112
+ # Create a new giraffe, and ignore the error if a giraffe already exists
113
+ # If a giraffe exists, then the instance of animal is stale, as it may not
114
+ # reflect the data in the database.
115
+ # animal = Animal.create!({:name => 'giraffe', :size => 'big'}, :ignore => true)
116
+ #
117
+ #
118
+ # Create a new giraffe; update the existing +size+ and +updated_at+ fields if the
119
+ # giraffe already exists. The instance of animal is not stale and reloaded
120
+ # to reflect the content in the database.
121
+ # animal = Animal.create({:name => 'giraffe', :size => 'big'},
122
+ # :on_duplicate_key_update => [:size, :updated_at],
123
+ # :duplicate_columns => [:name], :reload => true)
124
+ #
125
+ # Save a new giraffe, ignoring existing duplicates and inserting a comment
126
+ # in the SQL before the insert.
127
+ # giraffe = Animal.new(:name => 'giraffe', :size => 'small')
128
+ # giraffe.save!(:ignore => true, :pre_sql => '/* My Comment */')
129
+ #
130
+ #
131
+ # === Update Examples
132
+ # Update the giraffe with the low priority keyword
133
+ # big_giraffe.update(:keywords => 'LOW_PRIORITY')
134
+ #
135
+ # Update an existing record. If a duplicate exists, it is updated with the
136
+ # fields specified by +:on_duplicate_key_update+. The original instance(big_giraffe) is
137
+ # deleted, and the instance is reloaded to reflect the database (giraffe).
138
+ # big_giraffe = Animal.create!(:name => 'big_giraffe', :size => 'biggest')
139
+ # big_giraffe.name = 'giraffe'
140
+ # big_giraffe.save(:on_duplicate_key_update => [:size, :updated_at],
141
+ # :duplicate_columns => [:name], :reload => true)
142
+ #
143
+ module CreateAndUpdate
144
+
145
+ class NoDuplicateFound < Exception; end
146
+
147
+ def self.included(base) #:nodoc:
148
+ base.extend(ClassMethods)
149
+ base.extend(ActiveRecord::Extensions::SqlGeneration)
150
+
151
+ #alias chain active record methods if they have not already
152
+ #been chained
153
+ unless base.method_defined?(:save_without_extension)
154
+ base.class_eval do
155
+ [:save, :update, :save!, :create_or_update, :create].each { |method| alias_method_chain method, :extension }
156
+
157
+ class << self
158
+ [:create, :create!].each {|method| alias_method_chain method, :extension }
159
+ end
160
+
161
+ end
162
+ end
163
+ end
164
+
165
+ def supports_create_and_update? #:nodoc:
166
+ true
167
+ end
168
+
169
+ module ClassMethods#:nodoc:
170
+
171
+ # Creates an object, instantly saves it as a record (if the validation permits it), and returns it. If the save
172
+ # fails under validations, the unsaved object is still returned.
173
+ def create_with_extension(attributes = nil, options={}, &block)#:nodoc:
174
+ return create_without_extension(attributes, &block) unless options.any?
175
+ if attributes.is_a?(Array)
176
+ attributes.collect { |attr| create(attr, &block) }
177
+ else
178
+ object = new(attributes)
179
+ yield(object) if block_given?
180
+ object.save(options)
181
+ object
182
+ end
183
+ end
184
+
185
+ # Creates an object just like Base.create but calls save! instead of save
186
+ # so an exception is raised if the record is invalid.
187
+ def create_with_extension!(attributes = nil, options={}, &block)#:nodoc:
188
+ return create_without_extension!(attributes, &block) unless options.any?
189
+ create_with_extension(attributes, options.merge(:raise_exception => true), &block)
190
+ end
191
+
192
+ end#ClassMethods
193
+
194
+
195
+ def save_with_extension(options={})#:nodoc:
196
+
197
+ #invoke save_with_validation if the argument is not a hash
198
+ return save_without_extension(options) if !options.is_a?(Hash)
199
+ return save_without_extension unless options.any?
200
+
201
+ perform_validation = options.delete(:perform_validation)
202
+ raise_exception = options.delete(:raise_exception)
203
+
204
+ if (perform_validation.is_a?(FalseClass)) || valid?
205
+ raise ActiveRecord::ReadOnlyRecord if readonly?
206
+ create_or_update(options)
207
+ else
208
+ raise ActiveRecord::RecordInvalid.new(self) if raise_exception
209
+ false
210
+ end
211
+ end
212
+
213
+ def save_with_extension!(options={})#:nodoc:
214
+
215
+ return save_without_extension!(options) if !options.is_a?(Hash)
216
+ return save_without_extension! unless options.any?
217
+
218
+ save_with_extension(options.merge(:raise_exception => true)) || raise(ActiveRecord::RecordNotSaved)
219
+ end
220
+
221
+ #overwrite the create_or_update to call into
222
+ #the appropriate method create or update with the new options
223
+ #call the callbacks here
224
+ def create_or_update_with_extension(options={})#:nodoc:
225
+ return create_or_update_without_extension unless options.any?
226
+
227
+ return false if callback(:before_save) == false
228
+ raise ReadOnlyRecord if readonly?
229
+ result = new_record? ? create(options) : update(@attributes.keys, options)
230
+ callback(:after_save)
231
+
232
+ result != false
233
+ end
234
+
235
+
236
+ # Updates the associated record with values matching those of the instance attributes.
237
+ def update_with_extension(attribute_names = @attributes.keys, options={})#:nodoc:
238
+
239
+ return update_without_extension unless options.any?
240
+
241
+ check_insert_and_update_arguments(options)
242
+
243
+ return false if callback(:before_update) == false
244
+ insert_with_timestamps(false)
245
+
246
+ #set the command to update unless specified
247
+ #remove the duplicate_update_key if any
248
+ sql_options = options.dup
249
+ sql_options[:command]||='UPDATE'
250
+ sql_options.delete(:on_duplicate_key_update)
251
+
252
+ quoted_attributes = attributes_with_quotes(false, false, attribute_names)
253
+ return 0 if quoted_attributes.empty?
254
+
255
+ locking_sql = update_locking_sql
256
+
257
+ sql = self.class.construct_ar_extension_sql(sql_options) do |sql, o|
258
+ sql << "#{self.class.quoted_table_name} "
259
+ sql << "SET #{quoted_comma_pair_list(connection, quoted_attributes)} " +
260
+ "WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quote_value(id)}"
261
+ sql << locking_sql if locking_sql
262
+ end
263
+
264
+
265
+ reloaded = false
266
+
267
+ begin
268
+ affected_rows = connection.update(sql,
269
+ "#{self.class.name} Update X #{'With optimistic locking' if locking_sql} ")
270
+ #raise exception if optimistic locking is enabled and no rows were updated
271
+ raise ActiveRecord::StaleObjectError, "#{affected_rows} Attempted to update a stale object" if locking_sql && affected_rows != 1
272
+ @stale_record = (affected_rows == 0)
273
+ callback(:after_update)
274
+
275
+ #catch the duplicate error and update the existing record
276
+ rescue Exception => e
277
+ if (duplicate_columns(options) && options[:on_duplicate_key_update] &&
278
+ connection.respond_to?('duplicate_key_update_error?') &&
279
+ connection.duplicate_key_update_error?(e))
280
+ update_existing_record(options)
281
+ reloaded = true
282
+ else
283
+ raise e
284
+ end
285
+
286
+ end
287
+
288
+ load_duplicate_record(options) if options[:reload] && !reloaded
289
+
290
+ return true
291
+ end
292
+
293
+ # Creates a new record with values matching those of the instance attributes.
294
+ def create_with_extension(options={})#:nodoc:
295
+ return create_without_extension unless options.any?
296
+
297
+ check_insert_and_update_arguments(options)
298
+
299
+ return 0 if callback(:before_create) == false
300
+ insert_with_timestamps(true)
301
+
302
+ if self.id.nil? && connection.prefetch_primary_key?(self.class.table_name)
303
+ self.id = connection.next_sequence_value(self.class.sequence_name)
304
+
305
+ end
306
+
307
+ quoted_attributes = attributes_with_quotes
308
+
309
+ statement = if quoted_attributes.empty?
310
+ connection.empty_insert_statement(self.class.table_name)
311
+ else
312
+ options[:command]||='INSERT'
313
+ sql = self.class.construct_ar_extension_sql(options) do |sql, options|
314
+ sql << "INTO #{self.class.table_name} (#{quoted_column_names.join(', ')}) "
315
+ sql << "VALUES(#{attributes_with_quotes.values.join(', ')})"
316
+ end
317
+ end
318
+
319
+ self.id = connection.insert(statement, "#{self.class.name} Create X",
320
+ self.class.primary_key, self.id, self.class.sequence_name)
321
+
322
+
323
+ @new_record = false
324
+
325
+ #most adapters update the insert id number even if nothing was
326
+ #inserted. Reset to 0 for all :on_duplicate_key_update
327
+ self.id = 0 if options[:on_duplicate_key_update]
328
+
329
+
330
+ #the record was not created. Set the value to stale
331
+ if self.id == 0
332
+ @stale_record = true
333
+ load_duplicate_record(options) if options[:reload]
334
+ end
335
+
336
+ callback(:after_create)
337
+
338
+ self.id
339
+ end
340
+
341
+ # Replace deletes the existing duplicate if one exists and then
342
+ # inserts the new record. Foreign keys are updated only if
343
+ # performed by the database.
344
+ #
345
+ # The +options+ hash accepts the following attributes:
346
+ # * <tt>:pre_sql</tt> - sql that appears before the query
347
+ # * <tt>:post_sql</tt> - sql that appears after the query
348
+ # * <tt>:keywords</tt> - text that appears after the 'REPLACE' command
349
+ #
350
+ # ==== Examples
351
+ # Replace a single object
352
+ # user.replace
353
+
354
+ def replace(options={})
355
+ options.assert_valid_keys(:pre_sql, :post_sql, :keywords)
356
+ create_with_extension(options.merge(:command => 'REPLACE'))
357
+ end
358
+
359
+ # Returns true if the record data is stale
360
+ # This can occur when creating or updating a record with
361
+ # options <tt>:on_duplicate_key_update</tt> or <tt>:ignore</tt>
362
+ # without reloading(<tt> :reload => true</tt>)
363
+ #
364
+ # In other words, the attributes of a stale record may not reflect those
365
+ # in the database
366
+ def stale_record?; @stale_record.is_a?(TrueClass); end
367
+
368
+ # Reload Duplicate records like +reload_duplicate+ but
369
+ # throw an exception if no duplicate record is found
370
+ def reload_duplicate!(options={})
371
+ options.assert_valid_keys(:duplicate_columns, :force, :delete)
372
+ raise NoDuplicateFound.new("Record is not stale") if !stale_record? and !options[:force].is_a?(TrueClass)
373
+ load_duplicate_record(options.merge(:reload => true))
374
+ end
375
+
376
+ # Reload the record's duplicate based on the
377
+ # the duplicate_columns. Returns true if the reload was successful.
378
+ # <tt>:duplicate_columns</tt> - the columns to search on
379
+ # <tt>:force</tt> - force a reload even if the record is not stale
380
+ # <tt>:delete</tt> - delete the existing record if there is one. Defaults to true
381
+ def reload_duplicate(options={})
382
+ reload_duplicate!(options)
383
+ rescue NoDuplicateFound => e
384
+ return false
385
+ end
386
+ protected
387
+
388
+ # Returns the list of fields for which there is a unique key.
389
+ # When reloading duplicates during updates, with the <tt> :reload => true </tt>
390
+ # the reloaded existing duplicate record is the one matching the attributes specified
391
+ # by +duplicate_columns+.
392
+ #
393
+ # This data can either be passed into the save command, or the
394
+ # +duplicate_columns+ method can be overridden in the
395
+ # ActiveRecord subclass to return the columns with a unique key
396
+ #
397
+ # ===Example
398
+ # User has a unique key on name. If a user exists already
399
+ # the user object will be replaced by the existing user
400
+ # user.name = 'blythe'
401
+ # user.save(:ignore => true, :duplicate_columns => 'name', :reload => true)
402
+ #
403
+ # Alternatively, the User class can be overridden
404
+ # class User
405
+ # protected
406
+ # def duplicate_columns(options={}); [:name]; end
407
+ # end
408
+ #
409
+ # Then, the <tt>:duplicate_columns</tt> field is not needed during save
410
+ # user.update(:on_duplicate_key_update => [:password, :updated_at], :reload => true)
411
+ #
412
+
413
+ def duplicate_columns(options={})
414
+ options[:duplicate_columns]
415
+ end
416
+
417
+ #update timestamps
418
+ def insert_with_timestamps(bCreate=true)#:nodoc:
419
+ if record_timestamps
420
+ t = ( self.class.default_timezone == :utc ? Time.now.utc : Time.now )
421
+ write_attribute('created_at', t) if bCreate && respond_to?(:created_at) && created_at.nil?
422
+ write_attribute('created_on', t) if bCreate && respond_to?(:created_on) && created_on.nil?
423
+
424
+ write_attribute('updated_at', t) if respond_to?(:updated_at)
425
+ write_attribute('updated_on', t) if respond_to?(:updated_on)
426
+ end
427
+ end
428
+
429
+ # Update the optimistic locking column and
430
+ # return the sql necessary. update_with_lock is not called
431
+ # since update_x is aliased to update
432
+ def update_locking_sql()#:nodoc:
433
+ if locking_enabled?
434
+ lock_col = self.class.locking_column
435
+ previous_value = send(lock_col)
436
+ send(lock_col + '=', previous_value + 1)
437
+ " AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)}"
438
+ else
439
+ nil
440
+ end
441
+ end
442
+
443
+
444
+ def duplicate_option_check?(options)#:nodoc:
445
+ options.has_key?(:on_duplicate_key_update) ||
446
+ options[:keywords].to_s.downcase == 'ignore' ||
447
+ options[:ignore]
448
+ end
449
+
450
+ #Update the existing record with the new data from the duplicate column fields
451
+ #automatically delete and reload the object
452
+ def update_existing_record(options)#:nodoc:
453
+ load_duplicate_record(options.merge(:reload => true)) do |record|
454
+ updated_attributes = options[:on_duplicate_key_update].inject({}) {|map, attribute| map[attribute] = self.send(attribute); map}
455
+ record.update_attributes(updated_attributes)
456
+ end
457
+ end
458
+
459
+ #reload the record's duplicate based on the
460
+ #the duplicate_columns parameter or overwritten function
461
+ def load_duplicate_record(options, &block)#:nodoc:
462
+
463
+ search_columns = duplicate_columns(options)
464
+
465
+ #search for the existing columns
466
+ conditions = search_columns.inject([[],{}]){|sql, field|
467
+ sql[0] << "#{field} = :#{field}"
468
+ sql[1][field] = send(field)
469
+ sql
470
+ }
471
+
472
+ conditions[0] = conditions[0].join(' and ')
473
+
474
+ record = self.class.find :first, :conditions => conditions
475
+
476
+ raise NoDuplicateFound.new("Cannot find duplicate record.") if record.nil?
477
+
478
+ yield record if block
479
+
480
+ @stale_record = true
481
+
482
+ if options[:reload]
483
+ #do not delete new records, the same record or
484
+ #if user specified not to delete
485
+ if self.id.to_i > 0 && self.id != record.id && !options[:delete].is_a?(FalseClass)
486
+ self.class.delete_all(['id = ?', self.id])
487
+ end
488
+ reset_to_record(record)
489
+ end
490
+ true
491
+ end
492
+ #reload this object to the specified record
493
+ def reset_to_record(record)#:nodoc:
494
+ self.id = record.id
495
+ self.reload
496
+ @stale_record = false
497
+ end
498
+
499
+ #assert valid options
500
+ #ensure that duplicate_columns are specified with reload
501
+ def check_insert_and_update_arguments(options)#:nodoc:
502
+ options.assert_valid_keys([:on_duplicate_key_update, :reload, :command, :ignore, :pre_sql, :post_sql, :keywords, :duplicate_columns])
503
+ if duplicate_columns(options).blank? && duplicate_option_check?(options) && options[:reload]
504
+ raise(ArgumentError, "Unknown key: on_duplicate_key_update is not supported for updates without :duplicate_columns")
505
+ end
506
+ end
507
+ end
508
+ end
509
+ end