activerecord-oracle_enhanced-adapter 1.2.4 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. data/.gitignore +0 -1
  2. data/History.txt +20 -0
  3. data/README.rdoc +7 -3
  4. data/Rakefile +1 -2
  5. data/VERSION +1 -1
  6. data/activerecord-oracle_enhanced-adapter.gemspec +96 -0
  7. data/lib/active_record/connection_adapters/oracle_enhanced.rake +11 -8
  8. data/lib/active_record/connection_adapters/oracle_enhanced_activerecord_patches.rb +37 -0
  9. data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +317 -180
  10. data/lib/active_record/connection_adapters/oracle_enhanced_context_index.rb +282 -0
  11. data/lib/active_record/connection_adapters/oracle_enhanced_core_ext.rb +3 -2
  12. data/lib/active_record/connection_adapters/oracle_enhanced_dirty.rb +1 -1
  13. data/lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb +3 -3
  14. data/lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb +6 -1
  15. data/lib/active_record/connection_adapters/oracle_enhanced_procedures.rb +143 -52
  16. data/lib/active_record/connection_adapters/oracle_enhanced_schema_definitions.rb +2 -1
  17. data/lib/active_record/connection_adapters/oracle_enhanced_schema_dumper.rb +39 -20
  18. data/lib/active_record/connection_adapters/oracle_enhanced_tasks.rb +2 -1
  19. data/lib/active_record/connection_adapters/oracle_enhanced_version.rb +1 -1
  20. data/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +70 -11
  21. data/spec/active_record/connection_adapters/oracle_enhanced_adapter_structure_dumper_spec.rb +27 -20
  22. data/spec/active_record/connection_adapters/oracle_enhanced_context_index_spec.rb +334 -0
  23. data/spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb +28 -22
  24. data/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb +24 -28
  25. data/spec/active_record/connection_adapters/oracle_enhanced_dbms_output_spec.rb +13 -11
  26. data/spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb +1 -1
  27. data/spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb +72 -69
  28. data/spec/active_record/connection_adapters/oracle_enhanced_schema_dump_spec.rb +112 -6
  29. data/spec/active_record/connection_adapters/oracle_enhanced_schema_spec.rb +49 -1
  30. data/spec/spec_helper.rb +97 -19
  31. metadata +33 -22
  32. data/Manifest.txt +0 -32
  33. data/lib/active_record/connection_adapters/oracle_enhanced_reserved_words.rb +0 -126
@@ -0,0 +1,282 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module OracleEnhancedContextIndex
4
+
5
+ # Define full text index with Oracle specific CONTEXT index type
6
+ #
7
+ # Oracle CONTEXT index by default supports full text indexing of one column.
8
+ # This method allows full text index creation also on several columns
9
+ # as well as indexing related table columns by generating stored procedure
10
+ # that concatenates all columns for indexing as well as generating trigger
11
+ # that will update main index column to trigger reindexing of record.
12
+ #
13
+ # Use +contains+ ActiveRecord model instance method to add CONTAINS where condition
14
+ # and order by score of matched results.
15
+ #
16
+ # ===== Examples
17
+ #
18
+ # ====== Creating single column index
19
+ # add_context_index :posts, :title
20
+ # search with
21
+ # Post.contains(:title, 'word')
22
+ #
23
+ # ====== Creating index on several columns
24
+ # add_context_index :posts, [:title, :body]
25
+ # search with (use first column as argument for contains method but it will search in all index columns)
26
+ # Post.contains(:title, 'word')
27
+ #
28
+ # ====== Creating index on several columns with dummy index column and commit option
29
+ # add_context_index :posts, [:title, :body], :index_column => :all_text, :sync => 'ON COMMIT'
30
+ # search with
31
+ # Post.contains(:all_text, 'word')
32
+ #
33
+ # ====== Creating index with trigger option (will reindex when specified columns are updated)
34
+ # add_context_index :posts, [:title, :body], :index_column => :all_text, :sync => 'ON COMMIT',
35
+ # :index_column_trigger_on => [:created_at, :updated_at]
36
+ # search with
37
+ # Post.contains(:all_text, 'word')
38
+ #
39
+ # ====== Creating index on multiple tables
40
+ # add_context_index :posts, [:title, :body], :index_column => :all_text, :sync => 'ON COMMIT',
41
+ # :index_column_trigger_on => [:created_at, :updated_at]
42
+ # add_context_index :posts,
43
+ # [:title, :body,
44
+ # # specify aliases always with AS keyword
45
+ # "SELECT comments.author AS comment_author, comments.body AS comment_body FROM comments WHERE comments.post_id = :id"
46
+ # ],
47
+ # :name => 'post_and_comments_index',
48
+ # :index_column => :all_text, :index_column_trigger_on => [:updated_at, :comments_count],
49
+ # :sync => 'ON COMMIT'
50
+ # search in any table columns
51
+ # Post.contains(:all_text, 'word')
52
+ # search in specified column
53
+ # Post.contains(:all_text, "aaa within title")
54
+ # Post.contains(:all_text, "bbb within comment_author")
55
+ #
56
+ def add_context_index(table_name, column_name, options = {})
57
+ self.all_schema_indexes = nil
58
+ column_names = Array(column_name)
59
+ index_name = options[:name] || index_name(table_name, :column => options[:index_column] || column_names,
60
+ # CONEXT index name max length is 25
61
+ :identifier_max_length => 25)
62
+
63
+ quoted_column_name = quote_column_name(options[:index_column] || column_names.first)
64
+ if options[:index_column_trigger_on]
65
+ raise ArgumentError, "Option :index_column should be specified together with :index_column_cource option" \
66
+ unless options[:index_column]
67
+ create_index_column_trigger(table_name, index_name, options[:index_column], options[:index_column_trigger_on])
68
+ end
69
+
70
+ sql = "CREATE INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}"
71
+ sql << " (#{quoted_column_name})"
72
+ sql << " INDEXTYPE IS CTXSYS.CONTEXT"
73
+ parameters = []
74
+ if column_names.size > 1
75
+ procedure_name = default_datastore_procedure(index_name)
76
+ datastore_name = default_datastore_name(index_name)
77
+ create_datastore_procedure(table_name, procedure_name, column_names, options)
78
+ create_datastore_preference(datastore_name, procedure_name)
79
+ parameters << "DATASTORE #{datastore_name} SECTION GROUP CTXSYS.AUTO_SECTION_GROUP"
80
+ end
81
+ if options[:tablespace]
82
+ storage_name = default_storage_name(index_name)
83
+ create_storage_preference(storage_name, options[:tablespace])
84
+ parameters << "STORAGE #{storage_name}"
85
+ end
86
+ if options[:sync]
87
+ parameters << "SYNC(#{options[:sync]})"
88
+ end
89
+ unless parameters.empty?
90
+ sql << " PARAMETERS ('#{parameters.join(' ')}')"
91
+ end
92
+ execute sql
93
+ end
94
+
95
+ # Drop full text index with Oracle specific CONTEXT index type
96
+ def remove_context_index(table_name, options = {})
97
+ self.all_schema_indexes = nil
98
+ unless Hash === options # if column names passed as argument
99
+ options = {:column => Array(options)}
100
+ end
101
+ index_name = options[:name] || index_name(table_name,
102
+ :column => options[:index_column] || options[:column], :identifier_max_length => 25)
103
+ execute "DROP INDEX #{index_name}"
104
+ drop_ctx_preference(default_datastore_name(index_name))
105
+ drop_ctx_preference(default_storage_name(index_name))
106
+ procedure_name = default_datastore_procedure(index_name)
107
+ execute "DROP PROCEDURE #{quote_table_name(procedure_name)}" rescue nil
108
+ drop_index_column_trigger(index_name)
109
+ end
110
+
111
+ private
112
+
113
+ def create_datastore_procedure(table_name, procedure_name, column_names, options)
114
+ quoted_table_name = quote_table_name(table_name)
115
+ select_queries = column_names.select{|c| c.to_s =~ /^SELECT /i}
116
+ column_names = column_names - select_queries
117
+ keys, selected_columns = parse_select_queries(select_queries)
118
+ quoted_column_names = (column_names+keys).map{|col| quote_column_name(col)}
119
+ execute compress_lines(<<-SQL)
120
+ CREATE OR REPLACE PROCEDURE #{quote_table_name(procedure_name)}
121
+ (p_rowid IN ROWID,
122
+ p_clob IN OUT NOCOPY CLOB) IS
123
+ -- add_context_index_parameters #{(column_names+select_queries).inspect}#{!options.empty? ? ', ' << options.inspect[1..-2] : ''}
124
+ #{
125
+ selected_columns.map do |cols|
126
+ cols.map do |col|
127
+ raise ArgumentError, "Alias #{col} too large, should be 28 or less characters long" unless col.length <= 28
128
+ "l_#{col} VARCHAR2(32767);\n"
129
+ end.join
130
+ end.join
131
+ } BEGIN
132
+ FOR r1 IN (
133
+ SELECT #{quoted_column_names.join(', ')}
134
+ FROM #{quoted_table_name}
135
+ WHERE #{quoted_table_name}.ROWID = p_rowid
136
+ ) LOOP
137
+ #{
138
+ (column_names.map do |col|
139
+ col = col.to_s
140
+ "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+2}, '<#{col}>');\n" <<
141
+ "DBMS_LOB.WRITEAPPEND(p_clob, LENGTH(r1.#{col}), r1.#{col});\n" <<
142
+ "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+3}, '</#{col}>');\n"
143
+ end.join) <<
144
+ (selected_columns.zip(select_queries).map do |cols, query|
145
+ (cols.map do |col|
146
+ "l_#{col} := '';\n"
147
+ end.join) <<
148
+ "FOR r2 IN (\n" <<
149
+ query.gsub(/:(\w+)/,"r1.\\1") << "\n) LOOP\n" <<
150
+ (cols.map do |col|
151
+ "l_#{col} := l_#{col} || r2.#{col} || CHR(10);\n"
152
+ end.join) <<
153
+ "END LOOP;\n" <<
154
+ (cols.map do |col|
155
+ col = col.to_s
156
+ "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+2}, '<#{col}>');\n" <<
157
+ "IF LENGTH(l_#{col}) > 0 THEN\n" <<
158
+ "DBMS_LOB.WRITEAPPEND(p_clob, LENGTH(l_#{col}), l_#{col});\n" <<
159
+ "END IF;\n" <<
160
+ "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+3}, '</#{col}>');\n"
161
+ end.join)
162
+ end.join)
163
+ }
164
+ END LOOP;
165
+ END;
166
+ SQL
167
+ end
168
+
169
+ def parse_select_queries(select_queries)
170
+ keys = []
171
+ selected_columns = []
172
+ select_queries.each do |query|
173
+ # get primary or foreign keys like :id or :something_id
174
+ keys << (query.scan(/:\w+/).map{|k| k[1..-1].downcase.to_sym})
175
+ select_part = query.scan(/^select\s.*\sfrom/i).first
176
+ selected_columns << select_part.scan(/\sas\s+(\w+)/i).map{|c| c.first}
177
+ end
178
+ [keys.flatten.uniq, selected_columns]
179
+ end
180
+
181
+ def create_datastore_preference(datastore_name, procedure_name)
182
+ drop_ctx_preference(datastore_name)
183
+ execute <<-SQL
184
+ BEGIN
185
+ CTX_DDL.CREATE_PREFERENCE('#{datastore_name}', 'USER_DATASTORE');
186
+ CTX_DDL.SET_ATTRIBUTE('#{datastore_name}', 'PROCEDURE', '#{procedure_name}');
187
+ END;
188
+ SQL
189
+ end
190
+
191
+ def create_storage_preference(storage_name, tablespace)
192
+ drop_ctx_preference(storage_name)
193
+ sql = "BEGIN\nCTX_DDL.CREATE_PREFERENCE('#{storage_name}', 'BASIC_STORAGE');\n"
194
+ ['I_TABLE_CLAUSE', 'K_TABLE_CLAUSE', 'R_TABLE_CLAUSE',
195
+ 'N_TABLE_CLAUSE', 'I_INDEX_CLAUSE', 'P_TABLE_CLAUSE'].each do |clause|
196
+ default_clause = case clause
197
+ when 'R_TABLE_CLAUSE'; 'LOB(DATA) STORE AS (CACHE) '
198
+ when 'I_INDEX_CLAUSE'; 'COMPRESS 2 '
199
+ else ''
200
+ end
201
+ sql << "CTX_DDL.SET_ATTRIBUTE('#{storage_name}', '#{clause}', '#{default_clause}TABLESPACE #{tablespace}');\n"
202
+ end
203
+ sql << "END;\n"
204
+ execute sql
205
+ end
206
+
207
+ def drop_ctx_preference(preference_name)
208
+ execute "BEGIN CTX_DDL.DROP_PREFERENCE('#{preference_name}'); END;" rescue nil
209
+ end
210
+
211
+ def create_index_column_trigger(table_name, index_name, index_column, index_column_source)
212
+ trigger_name = default_index_column_trigger_name(index_name)
213
+ columns = Array(index_column_source)
214
+ quoted_column_names = columns.map{|col| quote_column_name(col)}.join(', ')
215
+ execute compress_lines(<<-SQL)
216
+ CREATE OR REPLACE TRIGGER #{quote_table_name(trigger_name)}
217
+ BEFORE UPDATE OF #{quoted_column_names} ON #{quote_table_name(table_name)} FOR EACH ROW
218
+ BEGIN
219
+ :new.#{quote_column_name(index_column)} := '1';
220
+ END;
221
+ SQL
222
+ end
223
+
224
+ def drop_index_column_trigger(index_name)
225
+ trigger_name = default_index_column_trigger_name(index_name)
226
+ execute "DROP TRIGGER #{quote_table_name(trigger_name)}" rescue nil
227
+ end
228
+
229
+ def default_datastore_procedure(index_name)
230
+ "#{index_name}_prc"
231
+ end
232
+
233
+ def default_datastore_name(index_name)
234
+ "#{index_name}_dst"
235
+ end
236
+
237
+ def default_storage_name(index_name)
238
+ "#{index_name}_sto"
239
+ end
240
+
241
+ def default_index_column_trigger_name(index_name)
242
+ "#{index_name}_trg"
243
+ end
244
+
245
+ module BaseClassMethods
246
+ # Declare that model table has context index defined.
247
+ # As a result <tt>contains</tt> class scope method is defined.
248
+ def has_context_index
249
+ extend ContextIndexClassMethods
250
+ end
251
+ end
252
+
253
+ module ContextIndexClassMethods
254
+ # Add context index condition.
255
+ case ::ActiveRecord::VERSION::MAJOR
256
+ when 3
257
+ def contains(column, query, options ={})
258
+ score_label = options[:label].to_i || 1
259
+ where("CONTAINS(#{connection.quote_column_name(column)}, ?, #{score_label}) > 0", query).
260
+ order("SCORE(#{score_label}) DESC")
261
+ end
262
+ when 2
263
+ def contains(column, query, options ={})
264
+ score_label = options[:label].to_i || 1
265
+ scoped(:conditions => ["CONTAINS(#{connection.quote_column_name(column)}, ?, #{score_label}) > 0", query],
266
+ :order => "SCORE(#{score_label}) DESC")
267
+ end
268
+ end
269
+ end
270
+
271
+ end
272
+
273
+ end
274
+ end
275
+
276
+ ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class_eval do
277
+ include ActiveRecord::ConnectionAdapters::OracleEnhancedContextIndex
278
+ end
279
+
280
+ ActiveRecord::Base.class_eval do
281
+ extend ActiveRecord::ConnectionAdapters::OracleEnhancedContextIndex::BaseClassMethods
282
+ end
@@ -26,7 +26,6 @@ end
26
26
  # Add Unicode aware String#upcase and String#downcase methods when mb_chars method is called
27
27
  if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'ruby' && RUBY_VERSION >= '1.9'
28
28
  begin
29
- gem "unicode_utils", ">=1.0.0"
30
29
  require "unicode_utils/upcase"
31
30
  require "unicode_utils/downcase"
32
31
 
@@ -53,7 +52,9 @@ if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'ruby' && RUBY_VERSION >= '1.9'
53
52
 
54
53
  rescue LoadError
55
54
  warning_message = "WARNING: Please install unicode_utils gem to support Unicode aware upcase and downcase for String#mb_chars"
56
- if defined?(RAILS_DEFAULT_LOGGER)
55
+ if defined?(Rails.logger) && Rails.logger
56
+ Rails.logger.warn warning_message
57
+ elsif defined?(RAILS_DEFAULT_LOGGER) && RAILS_DEFAULT_LOGGER
57
58
  RAILS_DEFAULT_LOGGER.warn warning_message
58
59
  else
59
60
  STDERR.puts warning_message
@@ -32,7 +32,7 @@ module ActiveRecord #:nodoc:
32
32
  end
33
33
  end
34
34
 
35
- if ActiveRecord::Base.instance_methods.include?('changed?')
35
+ if ActiveRecord::Base.method_defined?(:changed?)
36
36
  ActiveRecord::Base.class_eval do
37
37
  include ActiveRecord::ConnectionAdapters::OracleEnhancedDirty::InstanceMethods
38
38
  end
@@ -154,7 +154,7 @@ module ActiveRecord
154
154
  @active = true
155
155
  rescue NativeException => e
156
156
  @active = false
157
- if e.message =~ /^java\.sql\.SQLException/
157
+ if e.message =~ /^java\.sql\.SQL(Recoverable)?Exception/
158
158
  raise OracleEnhancedConnectionException, e.message
159
159
  else
160
160
  raise
@@ -169,7 +169,7 @@ module ActiveRecord
169
169
  @active = true
170
170
  rescue NativeException => e
171
171
  @active = false
172
- if e.message =~ /^java\.sql\.SQLException/
172
+ if e.message =~ /^java\.sql\.SQL(Recoverable)?Exception/
173
173
  raise OracleEnhancedConnectionException, e.message
174
174
  else
175
175
  raise
@@ -183,7 +183,7 @@ module ActiveRecord
183
183
  begin
184
184
  yield if block_given?
185
185
  rescue NativeException => e
186
- raise unless e.message =~ /^java\.sql\.SQLException: (Closed Connection|Io exception:|No more data to read from socket)/
186
+ raise unless e.message =~ /^java\.sql\.SQL(Recoverable)?Exception: (Closed Connection|Io exception:|No more data to read from socket)/
187
187
  @active = false
188
188
  raise unless should_retry
189
189
  should_retry = false
@@ -138,7 +138,12 @@ module ActiveRecord
138
138
 
139
139
  # Return OCIError error code
140
140
  def error_code(exception)
141
- exception.code
141
+ case exception
142
+ when OCIError
143
+ exception.code
144
+ else
145
+ nil
146
+ end
142
147
  end
143
148
 
144
149
  private
@@ -51,68 +51,106 @@ module ActiveRecord #:nodoc:
51
51
  include_with_custom_methods
52
52
  self.custom_delete_method = block
53
53
  end
54
-
55
- def create_method_name_before_custom_methods #:nodoc:
56
- if private_method_defined?(:create_without_timestamps) && defined?(ActiveRecord::VERSION) && ActiveRecord::VERSION::STRING.to_f >= 2.3
57
- :create_without_timestamps
58
- elsif private_method_defined?(:create_without_callbacks)
59
- :create_without_callbacks
60
- else
61
- :create
54
+
55
+ if ActiveRecord::VERSION::MAJOR < 3
56
+ def create_method_name_before_custom_methods #:nodoc:
57
+ if private_method_defined?(:create_without_timestamps) && defined?(ActiveRecord::VERSION) && ActiveRecord::VERSION::STRING.to_f >= 2.3
58
+ :create_without_timestamps
59
+ elsif private_method_defined?(:create_without_callbacks)
60
+ :create_without_callbacks
61
+ else
62
+ :create
63
+ end
62
64
  end
63
- end
64
-
65
- def update_method_name_before_custom_methods #:nodoc:
66
- if private_method_defined?(:update_without_dirty)
67
- :update_without_dirty
68
- elsif private_method_defined?(:update_without_timestamps) && defined?(ActiveRecord::VERSION) && ActiveRecord::VERSION::STRING.to_f >= 2.3
69
- :update_without_timestamps
70
- elsif private_method_defined?(:update_without_callbacks)
71
- :update_without_callbacks
72
- else
73
- :update
65
+
66
+ def update_method_name_before_custom_methods #:nodoc:
67
+ if private_method_defined?(:update_without_dirty)
68
+ :update_without_dirty
69
+ elsif private_method_defined?(:update_without_timestamps) && defined?(ActiveRecord::VERSION) && ActiveRecord::VERSION::STRING.to_f >= 2.3
70
+ :update_without_timestamps
71
+ elsif private_method_defined?(:update_without_callbacks)
72
+ :update_without_callbacks
73
+ else
74
+ :update
75
+ end
74
76
  end
75
- end
76
-
77
- def destroy_method_name_before_custom_methods #:nodoc:
78
- if public_method_defined?(:destroy_without_callbacks)
79
- :destroy_without_callbacks
80
- else
81
- :destroy
77
+
78
+ def destroy_method_name_before_custom_methods #:nodoc:
79
+ if public_method_defined?(:destroy_without_callbacks)
80
+ :destroy_without_callbacks
81
+ else
82
+ :destroy
83
+ end
82
84
  end
83
85
  end
84
-
86
+
85
87
  private
88
+
86
89
  def include_with_custom_methods
87
90
  unless included_modules.include? InstanceMethods
88
91
  include InstanceMethods
89
92
  end
90
93
  end
91
94
  end
92
-
95
+
93
96
  module InstanceMethods #:nodoc:
94
97
  def self.included(base)
95
- base.instance_eval do
96
- alias_method :create_without_custom_method, create_method_name_before_custom_methods
97
- alias_method create_method_name_before_custom_methods, :create_with_custom_method
98
- alias_method :update_without_custom_method, update_method_name_before_custom_methods
99
- alias_method update_method_name_before_custom_methods, :update_with_custom_method
100
- alias_method :destroy_without_custom_method, destroy_method_name_before_custom_methods
101
- alias_method destroy_method_name_before_custom_methods, :destroy_with_custom_method
102
- private :create, :update
103
- public :destroy
98
+ # alias methods just for ActiveRecord 2.x
99
+ # for ActiveRecord 3.0 will just redefine create, update, delete methods which call super
100
+ if ActiveRecord::VERSION::MAJOR < 3
101
+ base.instance_eval do
102
+ alias_method :create_without_custom_method, create_method_name_before_custom_methods
103
+ alias_method create_method_name_before_custom_methods, :create_with_custom_method
104
+ alias_method :update_without_custom_method, update_method_name_before_custom_methods
105
+ alias_method update_method_name_before_custom_methods, :update_with_custom_method
106
+ alias_method :destroy_without_custom_method, destroy_method_name_before_custom_methods
107
+ alias_method destroy_method_name_before_custom_methods, :destroy_with_custom_method
108
+ private :create, :update
109
+ public :destroy
110
+ end
111
+ end
112
+ end
113
+
114
+ if ActiveRecord::VERSION::MAJOR >= 3
115
+ def destroy #:nodoc:
116
+ # check if class has custom delete method
117
+ if self.class.custom_delete_method
118
+ # wrap destroy in transaction
119
+ with_transaction_returning_status do
120
+ # run before/after callbacks defined in model
121
+ _run_destroy_callbacks { destroy_using_custom_method }
122
+ end
123
+ else
124
+ super
125
+ end
104
126
  end
105
127
  end
106
-
128
+
107
129
  private
108
-
130
+
109
131
  # Creates a record with custom create method
110
132
  # and returns its id.
111
- def create_with_custom_method
112
- # check if class has custom create method
113
- return create_without_custom_method unless self.class.custom_create_method
133
+ if ActiveRecord::VERSION::MAJOR < 3
134
+ def create_with_custom_method
135
+ # check if class has custom create method
136
+ self.class.custom_create_method ? create_using_custom_method : create_without_custom_method
137
+ end
138
+ else # ActiveRecord 3.x
139
+ def create
140
+ # check if class has custom create method
141
+ if self.class.custom_create_method
142
+ set_timestamps_before_custom_create_method
143
+ # run before/after callbacks defined in model
144
+ _run_create_callbacks { create_using_custom_method }
145
+ else
146
+ super
147
+ end
148
+ end
149
+ end
150
+
151
+ def create_using_custom_method
114
152
  self.class.connection.log_custom_method("custom create method", "#{self.class.name} Create") do
115
- self.id = self.class.custom_create_method.bind(self).call
153
+ self.id = instance_eval(&self.class.custom_create_method)
116
154
  end
117
155
  @new_record = false
118
156
  id
@@ -120,12 +158,37 @@ module ActiveRecord #:nodoc:
120
158
 
121
159
  # Updates the associated record with custom update method
122
160
  # Returns the number of affected rows.
123
- def update_with_custom_method(attribute_names = @attributes.keys)
124
- # check if class has custom create method
125
- return update_without_custom_method unless self.class.custom_update_method
161
+ if ActiveRecord::VERSION::MAJOR < 3
162
+ def update_with_custom_method(attribute_names = @attributes.keys)
163
+ # check if class has custom create method
164
+ self.class.custom_update_method ? update_using_custom_method(attribute_names) : update_without_custom_method(attribute_names)
165
+ end
166
+ else # ActiveRecord 3.x
167
+ def update(attribute_names = @attributes.keys)
168
+ # check if class has custom update method
169
+ if self.class.custom_update_method
170
+ set_timestamps_before_custom_update_method
171
+ # run before/after callbacks defined in model
172
+ _run_update_callbacks do
173
+ # update just dirty attributes
174
+ if partial_updates?
175
+ # Serialized attributes should always be written in case they've been
176
+ # changed in place.
177
+ update_using_custom_method(changed | (attributes.keys & self.class.serialized_attributes.keys))
178
+ else
179
+ update_using_custom_method(attribute_names)
180
+ end
181
+ end
182
+ else
183
+ super
184
+ end
185
+ end
186
+ end
187
+
188
+ def update_using_custom_method(attribute_names)
126
189
  return 0 if attribute_names.empty?
127
190
  self.class.connection.log_custom_method("custom update method with #{self.class.primary_key}=#{self.id}", "#{self.class.name} Update") do
128
- self.class.custom_update_method.bind(self).call
191
+ instance_eval(&self.class.custom_update_method)
129
192
  end
130
193
  1
131
194
  end
@@ -133,12 +196,17 @@ module ActiveRecord #:nodoc:
133
196
  # Deletes the record in the database with custom delete method
134
197
  # and freezes this instance to reflect that no changes should
135
198
  # be made (since they can't be persisted).
136
- def destroy_with_custom_method
137
- # check if class has custom create method
138
- return destroy_without_custom_method unless self.class.custom_delete_method
139
- unless new_record?
199
+ if ActiveRecord::VERSION::MAJOR < 3
200
+ def destroy_with_custom_method
201
+ # check if class has custom delete method
202
+ self.class.custom_delete_method ? destroy_using_custom_method : destroy_without_custom_method
203
+ end
204
+ end
205
+
206
+ def destroy_using_custom_method
207
+ unless new_record? || @destroyed
140
208
  self.class.connection.log_custom_method("custom delete method with #{self.class.primary_key}=#{self.id}", "#{self.class.name} Destroy") do
141
- self.class.custom_delete_method.bind(self).call
209
+ instance_eval(&self.class.custom_delete_method)
142
210
  end
143
211
  end
144
212
 
@@ -146,6 +214,29 @@ module ActiveRecord #:nodoc:
146
214
  freeze
147
215
  end
148
216
 
217
+ if ActiveRecord::VERSION::MAJOR >= 3
218
+ def set_timestamps_before_custom_create_method
219
+ if record_timestamps
220
+ current_time = current_time_from_proper_timezone
221
+
222
+ write_attribute('created_at', current_time) if respond_to?(:created_at) && created_at.nil?
223
+ write_attribute('created_on', current_time) if respond_to?(:created_on) && created_on.nil?
224
+
225
+ write_attribute('updated_at', current_time) if respond_to?(:updated_at) && updated_at.nil?
226
+ write_attribute('updated_on', current_time) if respond_to?(:updated_on) && updated_on.nil?
227
+ end
228
+ end
229
+
230
+ def set_timestamps_before_custom_update_method
231
+ if record_timestamps && (!partial_updates? || changed?)
232
+ current_time = current_time_from_proper_timezone
233
+
234
+ write_attribute('updated_at', current_time) if respond_to?(:updated_at)
235
+ write_attribute('updated_on', current_time) if respond_to?(:updated_on)
236
+ end
237
+ end
238
+ end
239
+
149
240
  end
150
241
 
151
242
  end