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,39 @@
1
+ CREATE TABLE topics (
2
+ id serial NOT NULL,
3
+ title character varying(255) default NULL,
4
+ author_name character varying(255) default NULL,
5
+ author_email_address character varying(255) default NULL,
6
+ written_on timestamp default NULL,
7
+ bonus_time time default NULL,
8
+ last_read date default NULL,
9
+ content text,
10
+ approved bool default TRUE,
11
+ replies_count integer default 0,
12
+ parent_id serial default NULL,
13
+ type character varying(50) default NULL,
14
+ PRIMARY KEY (id)
15
+ );
16
+
17
+ CREATE TABLE projects (
18
+ id serial NOT NULL,
19
+ name character varying(100) default NULL,
20
+ type character varying(255) NOT NULL,
21
+ PRIMARY KEY (id)
22
+ );
23
+
24
+ CREATE TABLE developers (
25
+ id serial NOT NULL,
26
+ name character varying(100) default NULL,
27
+ salary integer default 70000,
28
+ created_at timestamp default NULL,
29
+ updated_at timestamp default NULL,
30
+ PRIMARY KEY (id)
31
+ );
32
+
33
+ CREATE TABLE books (
34
+ id serial NOT NULL,
35
+ title character varying(255) NOT NULL,
36
+ publisher character varying(255) NOT NULL,
37
+ author_name character varying(255) NOT NULL,
38
+ PRIMARY KEY (id)
39
+ );
@@ -0,0 +1,96 @@
1
+ ActiveRecord::Schema.define do
2
+
3
+ create_table :schema_info, :force=>true do |t|
4
+ t.column :version, :integer, :unique=>true
5
+ end
6
+ SchemaInfo.create :version=>SchemaInfo::VERSION
7
+
8
+ create_table :group, :force => true do |t|
9
+ t.column :order, :string
10
+ t.timestamps
11
+ end
12
+
13
+ create_table :topics, :force=>true do |t|
14
+ t.column :title, :string, :null=>false
15
+ t.column :author_name, :string
16
+ t.column :author_email_address, :string
17
+ t.column :written_on, :datetime
18
+ t.column :bonus_time, :time
19
+ t.column :last_read, :datetime
20
+ t.column :content, :text
21
+ t.column :approved, :boolean, :default=>'1'
22
+ t.column :replies_count, :integer
23
+ t.column :parent_id, :integer
24
+ t.column :type, :string
25
+ t.column :created_at, :datetime
26
+ t.column :updated_at, :datetime
27
+ end
28
+
29
+ create_table :projects, :force=>true do |t|
30
+ t.column :name, :string
31
+ t.column :type, :string
32
+ end
33
+
34
+ create_table :developers, :force=>true do |t|
35
+ t.column :name, :string
36
+ t.column :salary, :integer, :default=>'70000'
37
+ t.column :created_at, :datetime
38
+ t.column :team_id, :integer
39
+ t.column :updated_at, :datetime
40
+ end
41
+
42
+ create_table :addresses, :force=>true do |t|
43
+ t.column :address, :string
44
+ t.column :city, :string
45
+ t.column :state, :string
46
+ t.column :zip, :string
47
+ t.column :developer_id, :integer
48
+ end
49
+
50
+ create_table :teams, :force=>true do |t|
51
+ t.column :name, :string
52
+ end
53
+
54
+ create_table :books, :force=>true do |t|
55
+ t.column :title, :string, :null=>false
56
+ t.column :publisher, :string, :null=>false, :default => 'Default Publisher'
57
+ t.column :author_name, :string, :null=>false
58
+ t.column :created_at, :datetime
59
+ t.column :created_on, :datetime
60
+ t.column :updated_at, :datetime
61
+ t.column :updated_on, :datetime
62
+ t.column :topic_id, :integer
63
+ t.column :for_sale, :boolean, :default => true
64
+ end
65
+
66
+ create_table :languages, :force=>true do |t|
67
+ t.column :name, :string
68
+ t.column :developer_id, :integer
69
+ end
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
+
96
+ end
@@ -0,0 +1,31 @@
1
+ ActiveRecord::Schema.define do
2
+
3
+ create_table :test_myisam, :options=>'ENGINE=MyISAM', :force=>true do |t|
4
+ t.column :my_name, :string, :null=>false
5
+ t.column :description, :string
6
+ end
7
+
8
+ create_table :test_innodb, :options=>'ENGINE=InnoDb', :force=>true do |t|
9
+ t.column :my_name, :string, :null=>false
10
+ t.column :description, :string
11
+ end
12
+
13
+ create_table :test_memory, :options=>'ENGINE=Memory', :force=>true do |t|
14
+ t.column :my_name, :string, :null=>false
15
+ t.column :description, :string
16
+ end
17
+
18
+ create_table :books, :options=>'ENGINE=MyISAM', :force=>true do |t|
19
+ t.column :title, :string, :null=>false
20
+ t.column :publisher, :string, :null=>false, :default => 'Default Publisher'
21
+ t.column :author_name, :string, :null=>false
22
+ t.column :created_at, :datetime
23
+ t.column :created_on, :datetime
24
+ t.column :updated_at, :datetime
25
+ t.column :updated_on, :datetime
26
+ t.column :topic_id, :integer
27
+ t.column :for_sale, :boolean, :default => true
28
+ end
29
+ execute "ALTER TABLE books ADD FULLTEXT( `title`, `publisher`, `author_name` )"
30
+
31
+ end
@@ -0,0 +1,5 @@
1
+ ActiveRecord::Schema.define do
2
+
3
+ execute "CREATE SEQUENCE books_seq START_WITH 1"
4
+
5
+ end
@@ -0,0 +1,4 @@
1
+ class SchemaInfo < ActiveRecord::Base
2
+ set_table_name 'schema_info'
3
+ VERSION = 11
4
+ end
data/init.rb ADDED
@@ -0,0 +1,31 @@
1
+ require 'ostruct'
2
+ begin ; require 'active_record' ; rescue LoadError; require 'rubygems'; require 'active_record'; end
3
+
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'
8
+ require 'ar-extensions/version'
9
+ require 'ar-extensions/delete'
10
+ require 'ar-extensions/extensions'
11
+ require 'ar-extensions/create_and_update'
12
+ require 'ar-extensions/finder_options'
13
+ require 'ar-extensions/foreign_keys'
14
+ require 'ar-extensions/fulltext'
15
+ require 'ar-extensions/import'
16
+ require 'ar-extensions/insert_select'
17
+ require 'ar-extensions/finders'
18
+ require 'ar-extensions/synchronize'
19
+ require 'ar-extensions/temporary_table'
20
+ require 'ar-extensions/union'
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
@@ -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