activerecord-oracle_enhanced-adapter 1.2.4 → 1.3.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.
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