jorahood-ar-extensions 0.9.2.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.
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