ar-extensions 0.8.2 → 0.9.0

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