activerecord-oracle_enhanced-adapter 7.2.1-java

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 (76) hide show
  1. checksums.yaml +7 -0
  2. data/History.md +1977 -0
  3. data/License.txt +20 -0
  4. data/README.md +863 -0
  5. data/VERSION +1 -0
  6. data/lib/active_record/connection_adapters/emulation/oracle_adapter.rb +7 -0
  7. data/lib/active_record/connection_adapters/oracle_enhanced/column.rb +24 -0
  8. data/lib/active_record/connection_adapters/oracle_enhanced/connection.rb +137 -0
  9. data/lib/active_record/connection_adapters/oracle_enhanced/context_index.rb +344 -0
  10. data/lib/active_record/connection_adapters/oracle_enhanced/database_limits.rb +47 -0
  11. data/lib/active_record/connection_adapters/oracle_enhanced/database_statements.rb +295 -0
  12. data/lib/active_record/connection_adapters/oracle_enhanced/database_tasks.rb +63 -0
  13. data/lib/active_record/connection_adapters/oracle_enhanced/dbms_output.rb +71 -0
  14. data/lib/active_record/connection_adapters/oracle_enhanced/jdbc_connection.rb +561 -0
  15. data/lib/active_record/connection_adapters/oracle_enhanced/jdbc_quoting.rb +38 -0
  16. data/lib/active_record/connection_adapters/oracle_enhanced/lob.rb +45 -0
  17. data/lib/active_record/connection_adapters/oracle_enhanced/oci_connection.rb +457 -0
  18. data/lib/active_record/connection_adapters/oracle_enhanced/oci_quoting.rb +44 -0
  19. data/lib/active_record/connection_adapters/oracle_enhanced/procedures.rb +195 -0
  20. data/lib/active_record/connection_adapters/oracle_enhanced/quoting.rb +186 -0
  21. data/lib/active_record/connection_adapters/oracle_enhanced/schema_creation.rb +94 -0
  22. data/lib/active_record/connection_adapters/oracle_enhanced/schema_definitions.rb +99 -0
  23. data/lib/active_record/connection_adapters/oracle_enhanced/schema_dumper.rb +197 -0
  24. data/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb +738 -0
  25. data/lib/active_record/connection_adapters/oracle_enhanced/structure_dump.rb +366 -0
  26. data/lib/active_record/connection_adapters/oracle_enhanced/type_metadata.rb +34 -0
  27. data/lib/active_record/connection_adapters/oracle_enhanced/version.rb +3 -0
  28. data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +865 -0
  29. data/lib/active_record/type/oracle_enhanced/boolean.rb +19 -0
  30. data/lib/active_record/type/oracle_enhanced/character_string.rb +36 -0
  31. data/lib/active_record/type/oracle_enhanced/integer.rb +14 -0
  32. data/lib/active_record/type/oracle_enhanced/json.rb +10 -0
  33. data/lib/active_record/type/oracle_enhanced/national_character_string.rb +26 -0
  34. data/lib/active_record/type/oracle_enhanced/national_character_text.rb +36 -0
  35. data/lib/active_record/type/oracle_enhanced/raw.rb +25 -0
  36. data/lib/active_record/type/oracle_enhanced/string.rb +29 -0
  37. data/lib/active_record/type/oracle_enhanced/text.rb +32 -0
  38. data/lib/active_record/type/oracle_enhanced/timestampltz.rb +25 -0
  39. data/lib/active_record/type/oracle_enhanced/timestamptz.rb +25 -0
  40. data/lib/activerecord-oracle_enhanced-adapter.rb +25 -0
  41. data/lib/arel/visitors/oracle.rb +216 -0
  42. data/lib/arel/visitors/oracle12.rb +121 -0
  43. data/lib/arel/visitors/oracle_common.rb +51 -0
  44. data/spec/active_record/connection_adapters/emulation/oracle_adapter_spec.rb +24 -0
  45. data/spec/active_record/connection_adapters/oracle_enhanced/compatibility_spec.rb +36 -0
  46. data/spec/active_record/connection_adapters/oracle_enhanced/connection_spec.rb +574 -0
  47. data/spec/active_record/connection_adapters/oracle_enhanced/context_index_spec.rb +431 -0
  48. data/spec/active_record/connection_adapters/oracle_enhanced/database_tasks_spec.rb +122 -0
  49. data/spec/active_record/connection_adapters/oracle_enhanced/dbms_output_spec.rb +69 -0
  50. data/spec/active_record/connection_adapters/oracle_enhanced/procedures_spec.rb +362 -0
  51. data/spec/active_record/connection_adapters/oracle_enhanced/quoting_spec.rb +181 -0
  52. data/spec/active_record/connection_adapters/oracle_enhanced/schema_dumper_spec.rb +492 -0
  53. data/spec/active_record/connection_adapters/oracle_enhanced/schema_statements_spec.rb +1289 -0
  54. data/spec/active_record/connection_adapters/oracle_enhanced/structure_dump_spec.rb +474 -0
  55. data/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +815 -0
  56. data/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb +230 -0
  57. data/spec/active_record/oracle_enhanced/type/binary_spec.rb +119 -0
  58. data/spec/active_record/oracle_enhanced/type/boolean_spec.rb +206 -0
  59. data/spec/active_record/oracle_enhanced/type/character_string_spec.rb +67 -0
  60. data/spec/active_record/oracle_enhanced/type/custom_spec.rb +90 -0
  61. data/spec/active_record/oracle_enhanced/type/decimal_spec.rb +56 -0
  62. data/spec/active_record/oracle_enhanced/type/dirty_spec.rb +141 -0
  63. data/spec/active_record/oracle_enhanced/type/float_spec.rb +48 -0
  64. data/spec/active_record/oracle_enhanced/type/integer_spec.rb +99 -0
  65. data/spec/active_record/oracle_enhanced/type/json_spec.rb +56 -0
  66. data/spec/active_record/oracle_enhanced/type/national_character_string_spec.rb +56 -0
  67. data/spec/active_record/oracle_enhanced/type/national_character_text_spec.rb +230 -0
  68. data/spec/active_record/oracle_enhanced/type/raw_spec.rb +137 -0
  69. data/spec/active_record/oracle_enhanced/type/text_spec.rb +244 -0
  70. data/spec/active_record/oracle_enhanced/type/timestamp_spec.rb +77 -0
  71. data/spec/spec_config.yaml.template +11 -0
  72. data/spec/spec_helper.rb +214 -0
  73. data/spec/support/alter_system_set_open_cursors.sql +1 -0
  74. data/spec/support/alter_system_user_password.sql +2 -0
  75. data/spec/support/create_oracle_enhanced_users.sql +31 -0
  76. metadata +177 -0
@@ -0,0 +1,457 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "delegate"
4
+
5
+ begin
6
+ require "oci8"
7
+ rescue LoadError => e
8
+ # OCI8 driver is unavailable or failed to load a required library.
9
+ raise LoadError, "ERROR: '#{e.message}'. "\
10
+ "ActiveRecord oracle_enhanced adapter could not load ruby-oci8 library. "\
11
+ "You may need install ruby-oci8 gem."
12
+ end
13
+
14
+ # check ruby-oci8 version
15
+ required_oci8_version = [2, 2, 4]
16
+ oci8_version_ints = OCI8::VERSION.scan(/\d+/).map { |s| s.to_i }
17
+ if (oci8_version_ints <=> required_oci8_version) < 0
18
+ $stderr.puts <<~EOS
19
+ "ERROR: ruby-oci8 version #{OCI8::VERSION} is too old. Please install ruby-oci8 version #{required_oci8_version.join('.')} or later."
20
+ EOS
21
+
22
+ exit!
23
+ end
24
+
25
+ module ActiveRecord
26
+ module ConnectionAdapters
27
+ # OCI database interface for MRI
28
+ module OracleEnhanced
29
+ class OCIConnection < OracleEnhanced::Connection # :nodoc:
30
+ def initialize(config)
31
+ @raw_connection = OCI8EnhancedAutoRecover.new(config, OracleEnhancedOCIFactory)
32
+ # default schema owner
33
+ @owner = config[:schema]
34
+ @owner ||= config[:username]
35
+ @owner = @owner.to_s.upcase
36
+ end
37
+
38
+ def raw_oci_connection
39
+ if @raw_connection.is_a? OCI8
40
+ @raw_connection
41
+ # ActiveRecord Oracle enhanced adapter puts OCI8EnhancedAutoRecover wrapper around OCI8
42
+ # in this case we need to pass original OCI8 connection
43
+ else
44
+ @raw_connection.instance_variable_get(:@raw_connection)
45
+ end
46
+ end
47
+
48
+ def auto_retry
49
+ @raw_connection.auto_retry if @raw_connection
50
+ end
51
+
52
+ def auto_retry=(value)
53
+ @raw_connection.auto_retry = value if @raw_connection
54
+ end
55
+
56
+ def logoff
57
+ @raw_connection.logoff
58
+ @raw_connection.active = false
59
+ end
60
+
61
+ def commit
62
+ @raw_connection.commit
63
+ end
64
+
65
+ def rollback
66
+ @raw_connection.rollback
67
+ end
68
+
69
+ def autocommit?
70
+ @raw_connection.autocommit?
71
+ end
72
+
73
+ def autocommit=(value)
74
+ @raw_connection.autocommit = value
75
+ end
76
+
77
+ # Checks connection, returns true if active. Note that ping actively
78
+ # checks the connection, while #active? simply returns the last
79
+ # known state.
80
+ def ping
81
+ @raw_connection.ping
82
+ rescue OCIException => e
83
+ raise OracleEnhanced::ConnectionException, e.message
84
+ end
85
+
86
+ def active?
87
+ @raw_connection.active?
88
+ end
89
+
90
+ def reset
91
+ @raw_connection.reset
92
+ end
93
+
94
+ def reset!
95
+ @raw_connection.reset!
96
+ rescue OCIException => e
97
+ raise OracleEnhanced::ConnectionException, e.message
98
+ end
99
+
100
+ def exec(sql, *bindvars, allow_retry: false, &block)
101
+ with_retry(allow_retry: allow_retry) { @raw_connection.exec(sql, *bindvars, &block) }
102
+ end
103
+
104
+ def with_retry(allow_retry: false, &block)
105
+ @raw_connection.with_retry(allow_retry: allow_retry, &block)
106
+ end
107
+
108
+ def prepare(sql)
109
+ Cursor.new(self, @raw_connection.parse(sql))
110
+ end
111
+
112
+ class Cursor
113
+ def initialize(connection, raw_cursor)
114
+ @raw_connection = connection
115
+ @raw_cursor = raw_cursor
116
+ end
117
+
118
+ def bind_params(*bind_vars)
119
+ index = 1
120
+ bind_vars.flatten.each do |var|
121
+ if Hash === var
122
+ var.each { |key, val| bind_param key, val }
123
+ else
124
+ bind_param index, var
125
+ index += 1
126
+ end
127
+ end
128
+ end
129
+
130
+ def bind_param(position, value)
131
+ case value
132
+ when Type::OracleEnhanced::Raw
133
+ @raw_cursor.bind_param(position, OracleEnhanced::Quoting.encode_raw(value))
134
+ when ActiveModel::Type::Decimal
135
+ @raw_cursor.bind_param(position, BigDecimal(value.to_s))
136
+ when Type::OracleEnhanced::CharacterString::Data
137
+ @raw_cursor.bind_param(position, value.to_character_str)
138
+ when NilClass
139
+ @raw_cursor.bind_param(position, nil, String)
140
+ else
141
+ @raw_cursor.bind_param(position, value)
142
+ end
143
+ end
144
+
145
+ def bind_returning_param(position, bind_type)
146
+ @raw_cursor.bind_param(position, nil, bind_type)
147
+ end
148
+
149
+ def exec
150
+ @raw_cursor.exec
151
+ end
152
+
153
+ def exec_update
154
+ @raw_cursor.exec
155
+ end
156
+
157
+ def get_col_names
158
+ @raw_cursor.get_col_names
159
+ end
160
+
161
+ def fetch(options = {})
162
+ if row = @raw_cursor.fetch
163
+ get_lob_value = options[:get_lob_value]
164
+ col_index = 0
165
+ row.map do |col|
166
+ col_value = @raw_connection.typecast_result_value(col, get_lob_value)
167
+ col_metadata = @raw_cursor.column_metadata.fetch(col_index)
168
+ if !col_metadata.nil?
169
+ key = col_metadata.data_type
170
+ case key.to_s.downcase
171
+ when "char"
172
+ col_value = col.to_s.rstrip
173
+ end
174
+ end
175
+ col_index = col_index + 1
176
+ col_value
177
+ end
178
+ end
179
+ end
180
+
181
+ def get_returning_param(position, type)
182
+ @raw_cursor[position]
183
+ end
184
+
185
+ def close
186
+ @raw_cursor.close
187
+ end
188
+ end
189
+
190
+ def select(sql, name = nil, return_column_names = false)
191
+ cursor = @raw_connection.exec(sql)
192
+ cols = []
193
+ # Ignore raw_rnum_ which is used to simulate LIMIT and OFFSET
194
+ cursor.get_col_names.each do |col_name|
195
+ col_name = _oracle_downcase(col_name)
196
+ cols << col_name unless col_name == "raw_rnum_"
197
+ end
198
+ # Reuse the same hash for all rows
199
+ column_hash = {}
200
+ cols.each { |c| column_hash[c] = nil }
201
+ rows = []
202
+ get_lob_value = !(name == "Writable Large Object")
203
+
204
+ while row = cursor.fetch
205
+ hash = column_hash.dup
206
+
207
+ cols.each_with_index do |col, i|
208
+ col_value = typecast_result_value(row[i], get_lob_value)
209
+ col_metadata = cursor.column_metadata.fetch(i)
210
+ if !col_metadata.nil?
211
+ key = col_metadata.data_type
212
+ case key.to_s.downcase
213
+ when "char"
214
+ col_value = col_value.to_s.rstrip
215
+ end
216
+ end
217
+ hash[col] = col_value
218
+ end
219
+
220
+ rows << hash
221
+ end
222
+
223
+ return_column_names ? [rows, cols] : rows
224
+ ensure
225
+ cursor.close if cursor
226
+ end
227
+
228
+ def write_lob(lob, value, is_binary = false)
229
+ lob.write value
230
+ end
231
+
232
+ def describe(name)
233
+ super
234
+ end
235
+
236
+ # Return OCIError error code
237
+ def error_code(exception)
238
+ case exception
239
+ when OCIError
240
+ exception.code
241
+ else
242
+ nil
243
+ end
244
+ end
245
+
246
+ def typecast_result_value(value, get_lob_value)
247
+ case value
248
+ when Integer
249
+ value
250
+ when String
251
+ value
252
+ when Float, BigDecimal
253
+ # return Integer if value is integer (to avoid issues with _before_type_cast values for id attributes)
254
+ value == (v_to_i = value.to_i) ? v_to_i : value
255
+ when OCI8::LOB
256
+ if get_lob_value
257
+ data = value.read || "" # if value.read returns nil, then we have an empty_clob() i.e. an empty string
258
+ # In Ruby 1.9.1 always change encoding to ASCII-8BIT for binaries
259
+ data.force_encoding("ASCII-8BIT") if data.respond_to?(:force_encoding) && value.is_a?(OCI8::BLOB)
260
+ data
261
+ else
262
+ value
263
+ end
264
+ when Time, DateTime
265
+ create_time_with_default_timezone(value)
266
+ else
267
+ value
268
+ end
269
+ end
270
+
271
+ def database_version
272
+ @database_version ||= (version = raw_connection.oracle_server_version) && [version.major, version.minor]
273
+ end
274
+
275
+ private
276
+ def date_without_time?(value)
277
+ case value
278
+ when OraDate
279
+ value.hour == 0 && value.minute == 0 && value.second == 0
280
+ else
281
+ value.hour == 0 && value.min == 0 && value.sec == 0
282
+ end
283
+ end
284
+
285
+ def create_time_with_default_timezone(value)
286
+ year, month, day, hour, min, sec, usec = case value
287
+ when Time
288
+ [value.year, value.month, value.day, value.hour, value.min, value.sec, value.usec]
289
+ when OraDate
290
+ [value.year, value.month, value.day, value.hour, value.minute, value.second, 0]
291
+ else
292
+ [value.year, value.month, value.day, value.hour, value.min, value.sec, 0]
293
+ end
294
+ # code from Time.time_with_datetime_fallback
295
+ begin
296
+ Time.send(ActiveRecord.default_timezone, year, month, day, hour, min, sec, usec)
297
+ rescue
298
+ offset = ActiveRecord.default_timezone.to_sym == :local ? ::DateTime.local_offset : 0
299
+ ::DateTime.civil(year, month, day, hour, min, sec, offset)
300
+ end
301
+ end
302
+ end
303
+
304
+ # The OracleEnhancedOCIFactory factors out the code necessary to connect and
305
+ # configure an Oracle/OCI connection.
306
+ class OracleEnhancedOCIFactory # :nodoc:
307
+ DEFAULT_TCP_KEEPALIVE_TIME = 600
308
+
309
+ def self.new_connection(config)
310
+ # to_s needed if username, password or database is specified as number in database.yml file
311
+ username = config[:username] && config[:username].to_s
312
+ password = config[:password] && config[:password].to_s
313
+ database = config[:database] && config[:database].to_s
314
+ schema = config[:schema] && config[:schema].to_s
315
+ host, port = config[:host], config[:port]
316
+ privilege = config[:privilege] && config[:privilege].to_sym
317
+ async = config[:allow_concurrency]
318
+ prefetch_rows = config[:prefetch_rows] || 100
319
+ cursor_sharing = config[:cursor_sharing] || "force"
320
+ # get session time_zone from configuration or from TZ environment variable
321
+ time_zone = config[:time_zone] || ENV["TZ"]
322
+
323
+ # using a connection string via DATABASE_URL
324
+ connection_string = if host == "connection-string"
325
+ database
326
+ # connection using host, port and database name
327
+ elsif host || port
328
+ host ||= "localhost"
329
+ host = "[#{host}]" if /^[^\[].*:/.match?(host) # IPv6
330
+ port ||= 1521
331
+ database = "/#{database}" unless database.start_with?("/")
332
+ "//#{host}:#{port}#{database}"
333
+ # if no host is specified then assume that
334
+ # database parameter is TNS alias or TNS connection string
335
+ else
336
+ database
337
+ end
338
+
339
+ OCI8.properties[:tcp_keepalive] = config[:tcp_keepalive] == false ? false : true
340
+ begin
341
+ OCI8.properties[:tcp_keepalive_time] = config[:tcp_keepalive_time] || DEFAULT_TCP_KEEPALIVE_TIME
342
+ rescue NotImplementedError
343
+ end
344
+
345
+ conn = OCI8.new username, password, connection_string, privilege
346
+ conn.autocommit = true
347
+ conn.non_blocking = true if async
348
+ conn.prefetch_rows = prefetch_rows
349
+ conn.exec "alter session set cursor_sharing = #{cursor_sharing}" rescue nil if cursor_sharing
350
+ if ActiveRecord.default_timezone == :local
351
+ conn.exec "alter session set time_zone = '#{time_zone}'" unless time_zone.blank?
352
+ elsif ActiveRecord.default_timezone == :utc
353
+ conn.exec "alter session set time_zone = '+00:00'"
354
+ end
355
+ conn.exec "alter session set current_schema = #{schema}" unless schema.blank?
356
+
357
+ # Initialize NLS parameters
358
+ OracleEnhancedAdapter::DEFAULT_NLS_PARAMETERS.each do |key, default_value|
359
+ value = config[key] || ENV[key.to_s.upcase] || default_value
360
+ if value
361
+ conn.exec "alter session set #{key} = '#{value}'"
362
+ end
363
+ end
364
+
365
+ OracleEnhancedAdapter::FIXED_NLS_PARAMETERS.each do |key, value|
366
+ conn.exec "alter session set #{key} = '#{value}'"
367
+ end
368
+ conn
369
+ end
370
+ end
371
+ end
372
+ end
373
+ end
374
+
375
+ # The OCI8AutoRecover class enhances the OCI8 driver with auto-recover and
376
+ # reset functionality. If a call to #exec fails, and autocommit is turned on
377
+ # (ie., we're not in the middle of a longer transaction), it will
378
+ # automatically reconnect and try again. If autocommit is turned off,
379
+ # this would be dangerous (as the earlier part of the implied transaction
380
+ # may have failed silently if the connection died) -- so instead the
381
+ # connection is marked as dead, to be reconnected on it's next use.
382
+ # :stopdoc:
383
+ class OCI8EnhancedAutoRecover < DelegateClass(OCI8) # :nodoc:
384
+ attr_accessor :active # :nodoc:
385
+ alias :active? :active # :nodoc:
386
+
387
+ cattr_accessor :auto_retry
388
+ class << self
389
+ alias :auto_retry? :auto_retry # :nodoc:
390
+ end
391
+ @@auto_retry = false
392
+
393
+ def initialize(config, factory) # :nodoc:
394
+ @active = true
395
+ @config = config
396
+ @factory = factory
397
+ @raw_connection = @factory.new_connection @config
398
+ super @raw_connection
399
+ end
400
+
401
+ # Checks connection, returns true if active. Note that ping actively
402
+ # checks the connection, while #active? simply returns the last
403
+ # known state.
404
+ def ping # :nodoc:
405
+ @raw_connection.exec("select 1 from dual") { |r| nil }
406
+ @active = true
407
+ rescue
408
+ @active = false
409
+ raise
410
+ end
411
+
412
+ def reset
413
+ # tentative
414
+ reset!
415
+ end
416
+
417
+ # Resets connection, by logging off and creating a new connection.
418
+ def reset! # :nodoc:
419
+ logoff rescue nil
420
+ begin
421
+ @raw_connection = @factory.new_connection @config
422
+ __setobj__ @raw_connection
423
+ @active = true
424
+ rescue
425
+ @active = false
426
+ raise
427
+ end
428
+ end
429
+
430
+ # ORA-00028: your session has been killed
431
+ # ORA-01012: not logged on
432
+ # ORA-03113: end-of-file on communication channel
433
+ # ORA-03114: not connected to ORACLE
434
+ # ORA-03135: connection lost contact
435
+ LOST_CONNECTION_ERROR_CODES = [ 28, 1012, 3113, 3114, 3135 ] # :nodoc:
436
+
437
+ # Adds auto-recovery functionality.
438
+ def with_retry(allow_retry: false) # :nodoc:
439
+ should_retry = (allow_retry || self.class.auto_retry?) && autocommit?
440
+
441
+ begin
442
+ yield
443
+ rescue OCIException => e
444
+ raise unless e.is_a?(OCIError) && LOST_CONNECTION_ERROR_CODES.include?(e.code)
445
+ @active = false
446
+ raise unless should_retry
447
+ should_retry = false
448
+ reset! rescue nil
449
+ retry
450
+ end
451
+ end
452
+
453
+ def exec(sql, *bindvars, &block) # :nodoc:
454
+ with_retry { @raw_connection.exec(sql, *bindvars, &block) }
455
+ end
456
+ end
457
+ # :startdoc:
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module OracleEnhanced
6
+ module OCIQuoting
7
+ def type_cast(value)
8
+ case value
9
+ when ActiveModel::Type::Binary::Data
10
+ lob_value = value == "" ? " " : value
11
+ bind_type = OCI8::BLOB
12
+ ora_value = bind_type.new(_connection.raw_oci_connection, lob_value)
13
+ ora_value.size = 0 if value == ""
14
+ ora_value
15
+ when Type::OracleEnhanced::Text::Data
16
+ lob_value = value.to_s == "" ? " " : value.to_s
17
+ bind_type = OCI8::CLOB
18
+ ora_value = bind_type.new(_connection.raw_oci_connection, lob_value)
19
+ ora_value.size = 0 if value.to_s == ""
20
+ ora_value
21
+ when Type::OracleEnhanced::NationalCharacterText::Data
22
+ lob_value = value.to_s == "" ? " " : value.to_s
23
+ bind_type = OCI8::NCLOB
24
+ ora_value = bind_type.new(_connection.raw_oci_connection, lob_value)
25
+ ora_value.size = 0 if value.to_s == ""
26
+ ora_value
27
+ else
28
+ super
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ module ActiveRecord
37
+ module ConnectionAdapters
38
+ module OracleEnhanced
39
+ module Quoting
40
+ prepend OCIQuoting
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support"
4
+
5
+ module ActiveRecord # :nodoc:
6
+ # Custom create, update, delete methods functionality.
7
+ #
8
+ # Example:
9
+ #
10
+ # class Employee < ActiveRecord::Base
11
+ # include ActiveRecord::OracleEnhancedProcedures
12
+ #
13
+ # set_create_method do
14
+ # plsql.employees_pkg.create_employee(
15
+ # :p_first_name => first_name,
16
+ # :p_last_name => last_name,
17
+ # :p_employee_id => nil
18
+ # )[:p_employee_id]
19
+ # end
20
+ #
21
+ # set_update_method do
22
+ # plsql.employees_pkg.update_employee(
23
+ # :p_employee_id => id,
24
+ # :p_first_name => first_name,
25
+ # :p_last_name => last_name
26
+ # )
27
+ # end
28
+ #
29
+ # set_delete_method do
30
+ # plsql.employees_pkg.delete_employee(
31
+ # :p_employee_id => id
32
+ # )
33
+ # end
34
+ # end
35
+ #
36
+ module OracleEnhancedProcedures # :nodoc:
37
+ module ClassMethods
38
+ # Specify custom create method which should be used instead of Rails generated INSERT statement.
39
+ # Provided block should return ID of new record.
40
+ # Example:
41
+ # set_create_method do
42
+ # plsql.employees_pkg.create_employee(
43
+ # :p_first_name => first_name,
44
+ # :p_last_name => last_name,
45
+ # :p_employee_id => nil
46
+ # )[:p_employee_id]
47
+ # end
48
+ def set_create_method(&block)
49
+ self.custom_create_method = block
50
+ end
51
+
52
+ # Specify custom update method which should be used instead of Rails generated UPDATE statement.
53
+ # Example:
54
+ # set_update_method do
55
+ # plsql.employees_pkg.update_employee(
56
+ # :p_employee_id => id,
57
+ # :p_first_name => first_name,
58
+ # :p_last_name => last_name
59
+ # )
60
+ # end
61
+ def set_update_method(&block)
62
+ self.custom_update_method = block
63
+ end
64
+
65
+ # Specify custom delete method which should be used instead of Rails generated DELETE statement.
66
+ # Example:
67
+ # set_delete_method do
68
+ # plsql.employees_pkg.delete_employee(
69
+ # :p_employee_id => id
70
+ # )
71
+ # end
72
+ def set_delete_method(&block)
73
+ self.custom_delete_method = block
74
+ end
75
+ end
76
+
77
+ def self.included(base)
78
+ base.class_eval do
79
+ extend ClassMethods
80
+ class_attribute :custom_create_method
81
+ class_attribute :custom_update_method
82
+ class_attribute :custom_delete_method
83
+ end
84
+ end
85
+
86
+ def destroy # :nodoc:
87
+ # check if class has custom delete method
88
+ if self.class.custom_delete_method
89
+ # wrap destroy in transaction
90
+ with_transaction_returning_status do
91
+ # run before/after callbacks defined in model
92
+ run_callbacks(:destroy) { destroy_using_custom_method }
93
+ end
94
+ else
95
+ super
96
+ end
97
+ end
98
+
99
+ private
100
+ # Creates a record with custom create method
101
+ # and returns its id.
102
+ def _create_record
103
+ # check if class has custom create method
104
+ if self.class.custom_create_method
105
+ # run before/after callbacks defined in model
106
+ run_callbacks(:create) do
107
+ # timestamp
108
+ if self.record_timestamps
109
+ current_time = current_time_from_proper_timezone
110
+
111
+ all_timestamp_attributes_in_model.each do |column|
112
+ if respond_to?(column) && respond_to?("#{column}=") && self.send(column).nil?
113
+ write_attribute(column.to_s, current_time)
114
+ end
115
+ end
116
+ end
117
+ # run
118
+ create_using_custom_method
119
+ end
120
+ else
121
+ super
122
+ end
123
+ end
124
+
125
+ def create_using_custom_method
126
+ log_custom_method("custom create method", "#{self.class.name} Create") do
127
+ self.id = instance_eval(&self.class.custom_create_method)
128
+ end
129
+ @new_record = false
130
+ # Starting from ActiveRecord 3.0.3 @persisted is used instead of @new_record
131
+ @persisted = true
132
+ id
133
+ end
134
+
135
+ # Updates the associated record with custom update method
136
+ # Returns the number of affected rows.
137
+ def _update_record(attribute_names = @attributes.keys)
138
+ # check if class has custom update method
139
+ if self.class.custom_update_method
140
+ # run before/after callbacks defined in model
141
+ run_callbacks(:update) do
142
+ # timestamp
143
+ if should_record_timestamps?
144
+ current_time = current_time_from_proper_timezone
145
+
146
+ timestamp_attributes_for_update_in_model.each do |column|
147
+ column = column.to_s
148
+ next if will_save_change_to_attribute?(column)
149
+ write_attribute(column, current_time)
150
+ end
151
+ end
152
+ # update just dirty attributes
153
+ if partial_updates?
154
+ # Serialized attributes should always be written in case they've been
155
+ # changed in place.
156
+ update_using_custom_method(changed | (attributes.keys & self.class.columns.select { |column| column.is_a?(Type::Serialized) }))
157
+ else
158
+ update_using_custom_method(attributes.keys)
159
+ end
160
+ end
161
+ else
162
+ super
163
+ end
164
+ end
165
+
166
+ def update_using_custom_method(attribute_names)
167
+ return 0 if attribute_names.empty?
168
+ log_custom_method("custom update method with #{self.class.primary_key}=#{self.id}", "#{self.class.name} Update") do
169
+ instance_eval(&self.class.custom_update_method)
170
+ end
171
+ 1
172
+ end
173
+
174
+ # Deletes the record in the database with custom delete method
175
+ # and freezes this instance to reflect that no changes should
176
+ # be made (since they can't be persisted).
177
+ def destroy_using_custom_method
178
+ unless new_record? || @destroyed
179
+ log_custom_method("custom delete method with #{self.class.primary_key}=#{self.id}", "#{self.class.name} Destroy") do
180
+ instance_eval(&self.class.custom_delete_method)
181
+ end
182
+ end
183
+
184
+ @destroyed = true
185
+ freeze
186
+ end
187
+
188
+ def log_custom_method(*args, &block)
189
+ self.class.connection.send(:log, *args, &block)
190
+ end
191
+
192
+ alias_method :update_record, :_update_record if private_method_defined?(:_update_record)
193
+ alias_method :create_record, :_create_record if private_method_defined?(:_create_record)
194
+ end
195
+ end