Empact-ar-extensions 0.9.2

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 (45) hide show
  1. data/ChangeLog +145 -0
  2. data/README +167 -0
  3. data/Rakefile +61 -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 +97 -0
  9. data/db/migrate/mysql_schema.rb +32 -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.rb +5 -0
  14. data/lib/ar-extensions/adapters/abstract_adapter.rb +146 -0
  15. data/lib/ar-extensions/adapters/mysql.rb +10 -0
  16. data/lib/ar-extensions/adapters/oracle.rb +14 -0
  17. data/lib/ar-extensions/adapters/postgresql.rb +9 -0
  18. data/lib/ar-extensions/adapters/sqlite.rb +7 -0
  19. data/lib/ar-extensions/create_and_update.rb +508 -0
  20. data/lib/ar-extensions/create_and_update/mysql.rb +7 -0
  21. data/lib/ar-extensions/csv.rb +309 -0
  22. data/lib/ar-extensions/delete.rb +143 -0
  23. data/lib/ar-extensions/delete/mysql.rb +3 -0
  24. data/lib/ar-extensions/extensions.rb +509 -0
  25. data/lib/ar-extensions/finder_options.rb +275 -0
  26. data/lib/ar-extensions/finder_options/mysql.rb +6 -0
  27. data/lib/ar-extensions/finders.rb +96 -0
  28. data/lib/ar-extensions/foreign_keys.rb +70 -0
  29. data/lib/ar-extensions/fulltext.rb +62 -0
  30. data/lib/ar-extensions/fulltext/mysql.rb +44 -0
  31. data/lib/ar-extensions/import.rb +354 -0
  32. data/lib/ar-extensions/import/mysql.rb +50 -0
  33. data/lib/ar-extensions/import/postgresql.rb +0 -0
  34. data/lib/ar-extensions/import/sqlite.rb +22 -0
  35. data/lib/ar-extensions/insert_select.rb +178 -0
  36. data/lib/ar-extensions/insert_select/mysql.rb +7 -0
  37. data/lib/ar-extensions/synchronize.rb +30 -0
  38. data/lib/ar-extensions/temporary_table.rb +131 -0
  39. data/lib/ar-extensions/temporary_table/mysql.rb +3 -0
  40. data/lib/ar-extensions/union.rb +204 -0
  41. data/lib/ar-extensions/union/mysql.rb +6 -0
  42. data/lib/ar-extensions/util/sql_generation.rb +27 -0
  43. data/lib/ar-extensions/util/support_methods.rb +32 -0
  44. data/lib/ar-extensions/version.rb +9 -0
  45. metadata +128 -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,97 @@
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 :publish_date, :date
63
+ t.column :topic_id, :integer
64
+ t.column :for_sale, :boolean, :default => true
65
+ end
66
+
67
+ create_table :languages, :force=>true do |t|
68
+ t.column :name, :string
69
+ t.column :developer_id, :integer
70
+ end
71
+
72
+ create_table :shopping_carts, :force=>true do |t|
73
+ t.column :name, :string, :null => true
74
+ t.column :created_at, :datetime
75
+ t.column :updated_at, :datetime
76
+ end
77
+
78
+ create_table :cart_items, :force => true do |t|
79
+ t.column :shopping_cart_id, :string, :null => false
80
+ t.column :book_id, :string, :null => false
81
+ t.column :copies, :integer, :default => 1
82
+ t.column :created_at, :datetime
83
+ t.column :updated_at, :datetime
84
+ end
85
+
86
+ add_index :cart_items, [:shopping_cart_id, :book_id], :unique => true, :name => 'uk_shopping_cart_books'
87
+
88
+ create_table :animals, :force => true do |t|
89
+ t.column :name, :string, :null => false
90
+ t.column :size, :string, :default => nil
91
+ t.column :created_at, :datetime
92
+ t.column :updated_at, :datetime
93
+ end
94
+
95
+ add_index :animals, [:name], :unique => true, :name => 'uk_animals'
96
+
97
+ end
@@ -0,0 +1,32 @@
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 :publish_date, :date
27
+ t.column :topic_id, :integer
28
+ t.column :for_sale, :boolean, :default => true
29
+ end
30
+ execute "ALTER TABLE books ADD FULLTEXT( `title`, `publisher`, `author_name` )"
31
+
32
+ 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 = 12
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,5 @@
1
+ begin ; require 'rubygems' rescue LoadError; end
2
+ require 'active_record' # ActiveRecord loads the Benchmark library automatically
3
+ require 'active_record/version'
4
+
5
+ require File.expand_path(File.join( File.dirname( __FILE__ ), '..', 'init.rb' ))
@@ -0,0 +1,146 @@
1
+ module ActiveRecord # :nodoc:
2
+ module ConnectionAdapters # :nodoc:
3
+ class AbstractAdapter # :nodoc:
4
+ NO_MAX_PACKET = 0
5
+ QUERY_OVERHEAD = 8 #This was shown to be true for MySQL, but it's not clear where the overhead is from.
6
+
7
+ def next_value_for_sequence(sequence_name)
8
+ %{#{sequence_name}.nextval}
9
+ end
10
+
11
+ # +sql+ can be a single string or an array. If it is an array all
12
+ # elements that are in position >= 1 will be appended to the final SQL.
13
+ def insert_many( sql, values, *args ) # :nodoc:
14
+ # the number of inserts default
15
+ number_of_inserts = 0
16
+
17
+ base_sql,post_sql = if sql.is_a?( String )
18
+ [ sql, '' ]
19
+ elsif sql.is_a?( Array )
20
+ [ sql.shift, sql.join( ' ' ) ]
21
+ end
22
+
23
+ sql_size = QUERY_OVERHEAD + base_sql.size + post_sql.size
24
+
25
+ # the number of bytes the requested insert statement values will take up
26
+ values_in_bytes = self.class.sum_sizes( *values )
27
+
28
+ # the number of bytes (commas) it will take to comma separate our values
29
+ comma_separated_bytes = values.size-1
30
+
31
+ # the total number of bytes required if this statement is one statement
32
+ total_bytes = sql_size + values_in_bytes + comma_separated_bytes
33
+
34
+ max = max_allowed_packet
35
+
36
+ # if we can insert it all as one statement
37
+ if NO_MAX_PACKET == max or total_bytes < max
38
+ number_of_inserts += 1
39
+ sql2insert = base_sql + values.join( ',' ) + post_sql
40
+ insert( sql2insert, *args )
41
+ else
42
+ value_sets = self.class.get_insert_value_sets( values, sql_size, max )
43
+ value_sets.each do |values|
44
+ number_of_inserts += 1
45
+ sql2insert = base_sql + values.join( ',' ) + post_sql
46
+ insert( sql2insert, *args )
47
+ end
48
+ end
49
+
50
+ number_of_inserts
51
+ end
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
+
69
+ # Synchronizes the passed in ActiveRecord instances with the records in
70
+ # the database by calling +reload+ on each instance.
71
+ def after_import_synchronize( instances )
72
+ instances.each { |e| e.reload }
73
+ end
74
+
75
+ # Returns an array of post SQL statements given the passed in options.
76
+ def post_sql_statements( table_name, options ) # :nodoc:
77
+ post_sql_statements = []
78
+ if options[:on_duplicate_key_update]
79
+ post_sql_statements << sql_for_on_duplicate_key_update( table_name, options[:on_duplicate_key_update] )
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
+
88
+ post_sql_statements
89
+ end
90
+
91
+
92
+ # Generates the INSERT statement used in insert multiple value sets.
93
+ def multiple_value_sets_insert_sql(table_name, column_names, options) # :nodoc:
94
+ "INSERT #{options[:ignore] ? 'IGNORE ':''}INTO #{table_name} (#{column_names.join(',')}) VALUES "
95
+ end
96
+
97
+ # Returns SQL the VALUES for an INSERT statement given the passed in +columns+
98
+ # and +array_of_attributes+.
99
+ def values_sql_for_column_names_and_attributes( columns, array_of_attributes ) # :nodoc:
100
+ values = []
101
+ array_of_attributes.each do |arr|
102
+ my_values = []
103
+ arr.each_with_index do |val,j|
104
+ my_values << quote( val, columns[j] )
105
+ end
106
+ values << my_values
107
+ end
108
+ values_arr = values.map{ |arr| '(' + arr.join( ',' ) + ')' }
109
+ end
110
+
111
+ # Returns the sum of the sizes of the passed in objects. This should
112
+ # probably be moved outside this class, but to where?
113
+ def self.sum_sizes( *objects ) # :nodoc:
114
+ objects.inject( 0 ){|sum,o| sum += o.size }
115
+ end
116
+
117
+ # Returns the maximum number of bytes that the server will allow
118
+ # in a single packet
119
+ def max_allowed_packet
120
+ NO_MAX_PACKET
121
+ end
122
+
123
+ def self.get_insert_value_sets( values, sql_size, max_bytes ) # :nodoc:
124
+ value_sets = []
125
+ arr, current_arr_values_size, current_size = [], 0, 0
126
+ values.each_with_index do |val,i|
127
+ comma_bytes = arr.size
128
+ sql_size_thus_far = sql_size + current_size + val.size + comma_bytes
129
+ if NO_MAX_PACKET == max_bytes or sql_size_thus_far <= max_bytes
130
+ current_size += val.size
131
+ arr << val
132
+ else
133
+ value_sets << arr
134
+ arr = [ val ]
135
+ current_size = val.size
136
+ end
137
+
138
+ # if we're on the last iteration push whatever we have in arr to value_sets
139
+ value_sets << arr if i == (values.size-1)
140
+ end
141
+ [ *value_sets ]
142
+ end
143
+
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,10 @@
1
+ ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
2
+ # Returns the maximum number of bytes that the server will allow
3
+ # in a single packet
4
+ def max_allowed_packet # :nodoc:
5
+ result = execute( "SHOW VARIABLES like 'max_allowed_packet';" )
6
+ result.fetch_row[1].to_i
7
+ end
8
+
9
+ def rollup_sql; " WITH ROLLUP "; end
10
+ end
@@ -0,0 +1,14 @@
1
+ module ActiveRecord # :nodoc:
2
+ module ConnectionAdapters # :nodoc:
3
+ class OracleAdapter # :nodoc:
4
+
5
+ def next_value_for_sequence(sequence_name)
6
+ %{#{sequence_name}.nextval}
7
+ end
8
+
9
+ def supports_import?
10
+ true
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ module ActiveRecord # :nodoc:
2
+ module ConnectionAdapters # :nodoc:
3
+ class PostgreSQLAdapter # :nodoc:
4
+ def next_value_for_sequence(sequence_name)
5
+ %{nextval('#{sequence_name}')}
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ module ActiveRecord # :nodoc:
2
+ module ConnectionAdapters # :nodoc:
3
+ class SqliteAdapter # :nodoc:
4
+
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,508 @@
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
284
+ end
285
+ end
286
+
287
+ load_duplicate_record(options) if options[:reload] && !reloaded
288
+
289
+ return true
290
+ end
291
+
292
+ # Creates a new record with values matching those of the instance attributes.
293
+ def create_with_extension(options={})#:nodoc:
294
+ return create_without_extension unless options.any?
295
+
296
+ check_insert_and_update_arguments(options)
297
+
298
+ return 0 if callback(:before_create) == false
299
+ insert_with_timestamps(true)
300
+
301
+ if self.id.nil? && connection.prefetch_primary_key?(self.class.table_name)
302
+ self.id = connection.next_sequence_value(self.class.sequence_name)
303
+
304
+ end
305
+
306
+ quoted_attributes = attributes_with_quotes
307
+
308
+ statement = if quoted_attributes.empty?
309
+ connection.empty_insert_statement(self.class.table_name)
310
+ else
311
+ options[:command]||='INSERT'
312
+ sql = self.class.construct_ar_extension_sql(options) do |sql, options|
313
+ sql << "INTO #{self.class.table_name} (#{quoted_column_names.join(', ')}) "
314
+ sql << "VALUES(#{attributes_with_quotes.values.join(', ')})"
315
+ end
316
+ end
317
+
318
+ self.id = connection.insert(statement, "#{self.class.name} Create X",
319
+ self.class.primary_key, self.id, self.class.sequence_name)
320
+
321
+
322
+ @new_record = false
323
+
324
+ #most adapters update the insert id number even if nothing was
325
+ #inserted. Reset to 0 for all :on_duplicate_key_update
326
+ self.id = 0 if options[:on_duplicate_key_update]
327
+
328
+
329
+ #the record was not created. Set the value to stale
330
+ if self.id == 0
331
+ @stale_record = true
332
+ load_duplicate_record(options) if options[:reload]
333
+ end
334
+
335
+ callback(:after_create)
336
+
337
+ self.id
338
+ end
339
+
340
+ # Replace deletes the existing duplicate if one exists and then
341
+ # inserts the new record. Foreign keys are updated only if
342
+ # performed by the database.
343
+ #
344
+ # The +options+ hash accepts the following attributes:
345
+ # * <tt>:pre_sql</tt> - sql that appears before the query
346
+ # * <tt>:post_sql</tt> - sql that appears after the query
347
+ # * <tt>:keywords</tt> - text that appears after the 'REPLACE' command
348
+ #
349
+ # ==== Examples
350
+ # Replace a single object
351
+ # user.replace
352
+
353
+ def replace(options={})
354
+ options.assert_valid_keys(:pre_sql, :post_sql, :keywords)
355
+ create_with_extension(options.merge(:command => 'REPLACE'))
356
+ end
357
+
358
+ # Returns true if the record data is stale
359
+ # This can occur when creating or updating a record with
360
+ # options <tt>:on_duplicate_key_update</tt> or <tt>:ignore</tt>
361
+ # without reloading(<tt> :reload => true</tt>)
362
+ #
363
+ # In other words, the attributes of a stale record may not reflect those
364
+ # in the database
365
+ def stale_record?; @stale_record.is_a?(TrueClass); end
366
+
367
+ # Reload Duplicate records like +reload_duplicate+ but
368
+ # throw an exception if no duplicate record is found
369
+ def reload_duplicate!(options={})
370
+ options.assert_valid_keys(:duplicate_columns, :force, :delete)
371
+ raise NoDuplicateFound.new("Record is not stale") if !stale_record? and !options[:force].is_a?(TrueClass)
372
+ load_duplicate_record(options.merge(:reload => true))
373
+ end
374
+
375
+ # Reload the record's duplicate based on the
376
+ # the duplicate_columns. Returns true if the reload was successful.
377
+ # <tt>:duplicate_columns</tt> - the columns to search on
378
+ # <tt>:force</tt> - force a reload even if the record is not stale
379
+ # <tt>:delete</tt> - delete the existing record if there is one. Defaults to true
380
+ def reload_duplicate(options={})
381
+ reload_duplicate!(options)
382
+ rescue NoDuplicateFound => e
383
+ return false
384
+ end
385
+ protected
386
+
387
+ # Returns the list of fields for which there is a unique key.
388
+ # When reloading duplicates during updates, with the <tt> :reload => true </tt>
389
+ # the reloaded existing duplicate record is the one matching the attributes specified
390
+ # by +duplicate_columns+.
391
+ #
392
+ # This data can either be passed into the save command, or the
393
+ # +duplicate_columns+ method can be overridden in the
394
+ # ActiveRecord subclass to return the columns with a unique key
395
+ #
396
+ # ===Example
397
+ # User has a unique key on name. If a user exists already
398
+ # the user object will be replaced by the existing user
399
+ # user.name = 'blythe'
400
+ # user.save(:ignore => true, :duplicate_columns => 'name', :reload => true)
401
+ #
402
+ # Alternatively, the User class can be overridden
403
+ # class User
404
+ # protected
405
+ # def duplicate_columns(options={}); [:name]; end
406
+ # end
407
+ #
408
+ # Then, the <tt>:duplicate_columns</tt> field is not needed during save
409
+ # user.update(:on_duplicate_key_update => [:password, :updated_at], :reload => true)
410
+ #
411
+
412
+ def duplicate_columns(options={})
413
+ options[:duplicate_columns]
414
+ end
415
+
416
+ #update timestamps
417
+ def insert_with_timestamps(bCreate=true)#:nodoc:
418
+ if record_timestamps
419
+ t = ( self.class.default_timezone == :utc ? Time.now.utc : Time.now )
420
+ write_attribute('created_at', t) if bCreate && respond_to?(:created_at) && created_at.nil?
421
+ write_attribute('created_on', t) if bCreate && respond_to?(:created_on) && created_on.nil?
422
+
423
+ write_attribute('updated_at', t) if respond_to?(:updated_at)
424
+ write_attribute('updated_on', t) if respond_to?(:updated_on)
425
+ end
426
+ end
427
+
428
+ # Update the optimistic locking column and
429
+ # return the sql necessary. update_with_lock is not called
430
+ # since update_x is aliased to update
431
+ def update_locking_sql()#:nodoc:
432
+ if locking_enabled?
433
+ lock_col = self.class.locking_column
434
+ previous_value = send(lock_col)
435
+ send(lock_col + '=', previous_value + 1)
436
+ " AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)}"
437
+ else
438
+ nil
439
+ end
440
+ end
441
+
442
+
443
+ def duplicate_option_check?(options)#:nodoc:
444
+ options.has_key?(:on_duplicate_key_update) ||
445
+ options[:keywords].to_s.downcase == 'ignore' ||
446
+ options[:ignore]
447
+ end
448
+
449
+ #Update the existing record with the new data from the duplicate column fields
450
+ #automatically delete and reload the object
451
+ def update_existing_record(options)#:nodoc:
452
+ load_duplicate_record(options.merge(:reload => true)) do |record|
453
+ updated_attributes = options[:on_duplicate_key_update].inject({}) {|map, attribute| map[attribute] = self.send(attribute); map}
454
+ record.update_attributes(updated_attributes)
455
+ end
456
+ end
457
+
458
+ #reload the record's duplicate based on the
459
+ #the duplicate_columns parameter or overwritten function
460
+ def load_duplicate_record(options, &block)#:nodoc:
461
+
462
+ search_columns = duplicate_columns(options)
463
+
464
+ #search for the existing columns
465
+ conditions = search_columns.inject([[],{}]){|sql, field|
466
+ sql[0] << "#{field} = :#{field}"
467
+ sql[1][field] = send(field)
468
+ sql
469
+ }
470
+
471
+ conditions[0] = conditions[0].join(' and ')
472
+
473
+ record = self.class.find :first, :conditions => conditions
474
+
475
+ raise NoDuplicateFound.new("Cannot find duplicate record.") if record.nil?
476
+
477
+ yield record if block
478
+
479
+ @stale_record = true
480
+
481
+ if options[:reload]
482
+ #do not delete new records, the same record or
483
+ #if user specified not to delete
484
+ if self.id.to_i > 0 && self.id != record.id && !options[:delete].is_a?(FalseClass)
485
+ self.class.delete_all(['id = ?', self.id])
486
+ end
487
+ reset_to_record(record)
488
+ end
489
+ true
490
+ end
491
+ #reload this object to the specified record
492
+ def reset_to_record(record)#:nodoc:
493
+ self.id = record.id
494
+ self.reload
495
+ @stale_record = false
496
+ end
497
+
498
+ #assert valid options
499
+ #ensure that duplicate_columns are specified with reload
500
+ def check_insert_and_update_arguments(options)#:nodoc:
501
+ options.assert_valid_keys([:on_duplicate_key_update, :reload, :command, :ignore, :pre_sql, :post_sql, :keywords, :duplicate_columns])
502
+ if duplicate_columns(options).blank? && duplicate_option_check?(options) && options[:reload]
503
+ raise(ArgumentError, "Unknown key: on_duplicate_key_update is not supported for updates without :duplicate_columns")
504
+ end
505
+ end
506
+ end
507
+ end
508
+ end