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 +10 -0
- data/Rakefile +6 -1
- data/db/migrate/generic_schema.rb +26 -1
- data/db/migrate/mysql_schema.rb +1 -1
- data/db/migrate/version.rb +1 -1
- data/init.rb +18 -0
- data/lib/ar-extensions/adapters/abstract_adapter.rb +25 -2
- data/lib/ar-extensions/adapters/mysql.rb +2 -0
- data/lib/ar-extensions/create_and_update.rb +509 -0
- data/lib/ar-extensions/create_and_update/mysql.rb +7 -0
- data/lib/ar-extensions/csv.rb +32 -32
- data/lib/ar-extensions/delete.rb +143 -0
- data/lib/ar-extensions/delete/mysql.rb +3 -0
- data/lib/ar-extensions/extensions.rb +6 -1
- data/lib/ar-extensions/finder_options.rb +275 -0
- data/lib/ar-extensions/finder_options/mysql.rb +6 -0
- data/lib/ar-extensions/finders.rb +7 -1
- data/lib/ar-extensions/import/mysql.rb +8 -1
- data/lib/ar-extensions/insert_select.rb +178 -0
- data/lib/ar-extensions/insert_select/mysql.rb +7 -0
- data/lib/ar-extensions/union.rb +204 -0
- data/lib/ar-extensions/union/mysql.rb +6 -0
- data/lib/ar-extensions/util/sql_generation.rb +27 -0
- data/lib/ar-extensions/util/support_methods.rb +32 -0
- data/lib/ar-extensions/version.rb +1 -1
- metadata +15 -2
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
|
data/db/migrate/mysql_schema.rb
CHANGED
@@ -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
|
data/db/migrate/version.rb
CHANGED
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:
|
@@ -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
|