activerecord-oracle_enhanced-adapter 7.2.1-java → 8.1.0-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 (27) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +1 -7
  3. data/README.md +85 -1
  4. data/VERSION +1 -1
  5. data/lib/active_record/connection_adapters/oracle_enhanced/column.rb +2 -2
  6. data/lib/active_record/connection_adapters/oracle_enhanced/context_index.rb +17 -2
  7. data/lib/active_record/connection_adapters/oracle_enhanced/database_statements.rb +66 -36
  8. data/lib/active_record/connection_adapters/oracle_enhanced/dbms_output.rb +1 -1
  9. data/lib/active_record/connection_adapters/oracle_enhanced/jdbc_connection.rb +82 -14
  10. data/lib/active_record/connection_adapters/oracle_enhanced/lob.rb +12 -0
  11. data/lib/active_record/connection_adapters/oracle_enhanced/oci_connection.rb +8 -0
  12. data/lib/active_record/connection_adapters/oracle_enhanced/schema_creation.rb +1 -0
  13. data/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb +5 -4
  14. data/lib/active_record/connection_adapters/oracle_enhanced/structure_dump.rb +28 -0
  15. data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +25 -4
  16. data/spec/active_record/connection_adapters/oracle_enhanced/compatibility_spec.rb +6 -2
  17. data/spec/active_record/connection_adapters/oracle_enhanced/composite_spec.rb +84 -0
  18. data/spec/active_record/connection_adapters/oracle_enhanced/connection_spec.rb +19 -4
  19. data/spec/active_record/connection_adapters/oracle_enhanced/dbconsole_spec.rb +63 -0
  20. data/spec/active_record/connection_adapters/oracle_enhanced/schema_statements_spec.rb +29 -0
  21. data/spec/active_record/connection_adapters/oracle_enhanced/structure_dump_spec.rb +12 -1
  22. data/spec/active_record/oracle_enhanced/type/integer_spec.rb +11 -9
  23. data/spec/active_record/oracle_enhanced/type/national_character_string_spec.rb +1 -2
  24. data/spec/active_record/oracle_enhanced/type/text_spec.rb +51 -0
  25. data/spec/active_record/oracle_enhanced/type/timestamp_spec.rb +34 -4
  26. data/spec/spec_helper.rb +13 -2
  27. metadata +10 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8cfadd132a282a5e63722276b5a13c8f32e7d10ce9766db397e1b256113ac070
4
- data.tar.gz: 7183ed3e0bd92ba99f9eade30e9b07f3673b43970fe9de718693ec0db21adfd2
3
+ metadata.gz: '0228707379728bbeb32011a85bd19154279fb442d234e08ef5a5dc2445b391d3'
4
+ data.tar.gz: 05e7d21d330955e2227120e7dc2b1c59fd0de7e8feb162fb3c6a2acc514c692f
5
5
  SHA512:
6
- metadata.gz: 58a1bbc68da126583e22c3a2b3436ed3c144637540de12d63aeeaa7565c5d0e2c02529dfb1aac1facff5cd13cb1b3c68b9a30bb392404b541c50196b20476bf0
7
- data.tar.gz: ba5141eb3d77088066282646a8b9179dc728c46f9693a958a66cb3c56267f3db01b6417b59ee0284e8f1f01d480d1eaa0baed72f9dfcfd66ffdbeb56af6cd488
6
+ metadata.gz: 1e0531e2d250815dc629d7c78871bb30bad58687a47429e34b6ab024cd713a9a1b3f60c855a571bd2090811afea82cfd0618778c4ecba901960c842cbb676391
7
+ data.tar.gz: a357e405eaa4a7b226c02638b968c0cf92fbbc5404c573de144c92361eb79a38f864e7f45b1e3837c5fd9801949d3a402982e092e13b85b3f886557cc9d5f3cb
data/History.md CHANGED
@@ -1,10 +1,4 @@
1
- ## 7.2.1 / 2026-07-26
2
-
3
- * Changes and bug fixes
4
- * Accept `allow_retry` keyword argument in JDBC connection `exec`
5
- * Add `reset` method to JDBC connection
6
- * Skip `ObjectSpace::WeakMap` workaround on JRuby that implements `#values`
7
- * Run tests on JRuby 9.4.15.0
1
+ This file is no longer being updated. For the latest updates and release information, please see: https://github.com/rsim/oracle-enhanced/releases
8
2
 
9
3
  ## 7.2.0 / 2025-06-23
10
4
 
data/README.md CHANGED
@@ -6,10 +6,30 @@ Oracle enhanced adapter for ActiveRecord
6
6
  DESCRIPTION
7
7
  -----------
8
8
 
9
- Oracle enhanced ActiveRecord adapter provides Oracle database access from Ruby on Rails applications. Oracle enhanced adapter can be used from Ruby on Rails versions between 2.3.x and 7.2 and it is working with Oracle database versions 10g and higher
9
+ Oracle enhanced ActiveRecord adapter provides Oracle database access from Ruby on Rails applications. Oracle enhanced adapter can be used from Ruby on Rails versions between 2.3.x and 8.1 and it is working with Oracle database versions 10g and higher
10
10
 
11
11
  INSTALLATION
12
12
  ------------
13
+ ### Rails 8.1
14
+
15
+ Oracle enhanced adapter version 8.1 supports Rails 8.1
16
+ When using Ruby on Rails version 8.1 then in Gemfile include
17
+
18
+ ```ruby
19
+ # Use oracle as the database for Active Record
20
+ gem 'activerecord-oracle_enhanced-adapter', '~> 8.1.0'
21
+ ```
22
+
23
+ ### Rails 8.0
24
+
25
+ Oracle enhanced adapter version 8.0 supports Rails 8.0
26
+ When using Ruby on Rails version 8.0 then in Gemfile include
27
+
28
+ ```ruby
29
+ # Use oracle as the database for Active Record
30
+ gem 'activerecord-oracle_enhanced-adapter', '~> 8.0.0'
31
+ ```
32
+
13
33
  ### Rails 7.2
14
34
 
15
35
  Oracle enhanced adapter version 7.2 supports Rails 7.2
@@ -823,6 +843,70 @@ complete.
823
843
 
824
844
  See the **Timeouts** section above.
825
845
 
846
+ Development with Devcontainer
847
+ ------------------------------
848
+
849
+ This project includes a devcontainer configuration that provides a complete development environment with Oracle Database and all necessary dependencies pre-configured. The devcontainer supports both x64 and ARM64 architectures.
850
+
851
+ ### Prerequisites
852
+
853
+ - [Docker](https://www.docker.com/get-started) installed and running
854
+ - [VS Code](https://code.visualstudio.com/) with the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
855
+
856
+ ### Getting Started
857
+
858
+ 1. Clone the repository:
859
+ ```bash
860
+ git clone https://github.com/rsim/oracle-enhanced.git
861
+ cd oracle-enhanced
862
+ ```
863
+
864
+ 2. Open the project in VS Code:
865
+ ```bash
866
+ code .
867
+ ```
868
+
869
+ 3. When prompted, click "Reopen in Container" or use the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`) and select "Dev Containers: Reopen in Container"
870
+
871
+ 4. VS Code will build and start the development environment automatically. This includes:
872
+ - Ruby 3.4
873
+ - Oracle Database Free
874
+ - Oracle Instant Client 23.8
875
+ - All required gems installed via `bundle install`
876
+
877
+ ### What's Included
878
+
879
+ The devcontainer provides:
880
+
881
+ - **Ruby**: 3.4
882
+ - **Oracle Database**: Oracle Database Free (23c)
883
+ - **Oracle Instant Client**: Version 23.8
884
+ - **Database Configuration**:
885
+ - Database: `FREEPDB1`
886
+ - System password: `Oracle18`
887
+ - TNS configuration in `ci/network/admin`
888
+
889
+ ### Database Access
890
+
891
+ The Oracle database is automatically started and configured with:
892
+ - Port: `1521` (forwarded from container)
893
+ - Service Name: `FREEPDB1`
894
+ - System Password: `Oracle18`
895
+ - Test users are created automatically from `spec/support/create_oracle_enhanced_users.sql`
896
+
897
+
898
+ ### Running Tests
899
+
900
+ Once the devcontainer is running, you can run tests directly:
901
+
902
+ ```bash
903
+ # Run all tests
904
+ bundle exec rspec
905
+
906
+ # Run specific test files
907
+ bundle exec rspec spec/test_file_spec.rb
908
+ ```
909
+
826
910
  RUNNING TESTS
827
911
  -------------
828
912
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 7.2.1
1
+ 8.1.0
@@ -6,8 +6,8 @@ module ActiveRecord
6
6
  class Column < ActiveRecord::ConnectionAdapters::Column
7
7
  delegate :virtual, to: :sql_type_metadata, allow_nil: true
8
8
 
9
- def initialize(name, default, sql_type_metadata = nil, null = true, comment: nil) # :nodoc:
10
- super(name, default, sql_type_metadata, null, comment: comment)
9
+ def initialize(name, cast_type, default, sql_type_metadata = nil, null = true, comment: nil) # :nodoc:
10
+ super(name, cast_type, default, sql_type_metadata, null, comment: comment)
11
11
  end
12
12
 
13
13
  def virtual?
@@ -330,8 +330,23 @@ module ActiveRecord
330
330
  # Add context index condition.
331
331
  def contains(column, query, options = {})
332
332
  score_label = options[:label].to_i || 1
333
- where("CONTAINS(#{connection.quote_table_name(column)}, ?, #{score_label}) > 0", query).
334
- order(Arel.sql("SCORE(#{score_label}) DESC"))
333
+ quoted_column = connection.quote_table_name(column)
334
+
335
+ # Create an Arel node for the CONTAINS function
336
+ contains_node = Arel::Nodes::NamedFunction.new(
337
+ "CONTAINS",
338
+ [
339
+ Arel::Nodes::SqlLiteral.new(quoted_column),
340
+ Arel::Nodes::BindParam.new(query),
341
+ Arel::Nodes::SqlLiteral.new(score_label.to_s)
342
+ ]
343
+ )
344
+
345
+ # Create comparison node: CONTAINS(...) > 0
346
+ condition = Arel::Nodes::GreaterThan.new(contains_node, Arel::Nodes::SqlLiteral.new("0"))
347
+
348
+ # Create the where clause and order by score
349
+ where(condition).order(Arel.sql("SCORE(#{score_label}) DESC"))
335
350
  end
336
351
  end
337
352
  end
@@ -8,58 +8,79 @@ module ActiveRecord
8
8
  #
9
9
  # see: abstract/database_statements.rb
10
10
 
11
- # Executes a SQL statement
12
- def execute(sql, name = nil, async: false, allow_retry: false)
13
- sql = transform_query(sql)
11
+ READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(
12
+ :close, :declare, :fetch, :move, :set, :show
13
+ ) # :nodoc:
14
+ private_constant :READ_QUERY
15
+
16
+ def write_query?(sql) # :nodoc:
17
+ !READ_QUERY.match?(sql)
18
+ rescue ArgumentError # Invalid encoding
19
+ !READ_QUERY.match?(sql.b)
20
+ end
14
21
 
15
- log(sql, name, async: async) { _connection.exec(sql, allow_retry: allow_retry) }
22
+ # Executes a SQL statement
23
+ def execute(...)
24
+ super
16
25
  end
17
26
 
18
- def exec_query(sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false)
19
- sql = transform_query(sql)
27
+ # Low level execution of a SQL statement on the connection returning adapter specific result object.
28
+ def raw_execute(sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false, materialize_transactions: false)
29
+ sql = preprocess_query(sql)
20
30
 
21
31
  type_casted_binds = type_casted_binds(binds)
32
+ with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
33
+ log(sql, name, binds, type_casted_binds, async: async) do
34
+ cursor = nil
35
+ cached = false
36
+ with_retry do
37
+ if binds.nil? || binds.empty?
38
+ cursor = conn.prepare(sql)
39
+ else
40
+ unless @statements.key? sql
41
+ @statements[sql] = conn.prepare(sql)
42
+ end
22
43
 
23
- log(sql, name, binds, type_casted_binds, async: async) do
24
- cursor = nil
25
- cached = false
26
- with_retry do
27
- if without_prepared_statement?(binds)
28
- cursor = _connection.prepare(sql)
29
- else
30
- unless @statements.key? sql
31
- @statements[sql] = _connection.prepare(sql)
32
- end
33
-
34
- cursor = @statements[sql]
35
-
36
- cursor.bind_params(type_casted_binds)
44
+ cursor = @statements[sql]
45
+ cursor.bind_params(type_casted_binds)
37
46
 
38
- cached = true
47
+ cached = true
48
+ end
49
+ cursor.exec
39
50
  end
40
51
 
41
- cursor.exec
42
- end
43
-
44
- if (name == "EXPLAIN") && sql.start_with?("EXPLAIN")
45
- res = true
46
- else
47
52
  columns = cursor.get_col_names.map do |col_name|
48
53
  oracle_downcase(col_name)
49
54
  end
55
+
50
56
  rows = []
51
- fetch_options = { get_lob_value: (name != "Writable Large Object") }
52
- while row = cursor.fetch(fetch_options)
53
- rows << row
57
+ if cursor.select_statement?
58
+ fetch_options = { get_lob_value: (name != "Writable Large Object") }
59
+ while row = cursor.fetch(fetch_options)
60
+ rows << row
61
+ end
54
62
  end
55
- res = build_result(columns: columns, rows: rows)
63
+
64
+ affected_rows_count = cursor.row_count
65
+
66
+ cursor.close unless cached
67
+
68
+ { columns: columns, rows: rows, affected_rows_count: affected_rows_count }
56
69
  end
70
+ end
71
+ end
57
72
 
58
- cursor.close unless cached
59
- res
73
+ def cast_result(result)
74
+ if result.nil?
75
+ ActiveRecord::Result.empty
76
+ else
77
+ ActiveRecord::Result.new(result[:columns], result[:rows])
60
78
  end
61
79
  end
62
- alias_method :internal_exec_query, :exec_query
80
+
81
+ def affected_rows(result)
82
+ result[:affected_rows_count]
83
+ end
63
84
 
64
85
  def supports_explain?
65
86
  true
@@ -106,7 +127,7 @@ module ActiveRecord
106
127
  cursor = nil
107
128
  returning_id_col = returning_id_index = nil
108
129
  with_retry do
109
- if without_prepared_statement?(binds)
130
+ if binds.nil? || binds.empty?
110
131
  cursor = _connection.prepare(sql)
111
132
  else
112
133
  unless @statements.key?(sql)
@@ -146,7 +167,7 @@ module ActiveRecord
146
167
  log(sql, name, binds, type_casted_binds) do
147
168
  with_retry do
148
169
  cached = false
149
- if without_prepared_statement?(binds)
170
+ if binds.nil? || binds.empty?
150
171
  cursor = _connection.prepare(sql)
151
172
  else
152
173
  if @statements.key?(sql)
@@ -289,6 +310,15 @@ module ActiveRecord
289
310
  raise
290
311
  end
291
312
  end
313
+
314
+ def handle_warnings(sql)
315
+ @notice_receiver_sql_warnings.each do |warning|
316
+ next if warning_ignored?(warning)
317
+
318
+ warning.sql = sql
319
+ ActiveRecord.db_warnings_action.call(warning)
320
+ end
321
+ end
292
322
  end
293
323
  end
294
324
  end
@@ -32,7 +32,7 @@ module ActiveRecord
32
32
 
33
33
  private
34
34
  def log(sql, name = "SQL", binds = [], type_casted_binds = [], statement_name = nil, async: false, &block)
35
- @instrumenter.instrument(
35
+ instrumenter.instrument(
36
36
  "sql.active_record",
37
37
  sql: sql,
38
38
  name: name,
@@ -17,7 +17,8 @@ begin
17
17
  # Oracle 12c Release 1 client provides ojdbc7.jar
18
18
  # Oracle 12c Release 2 client provides ojdbc8.jar
19
19
  # Oracle 21c provides ojdbc11.jar for Java 11 and above
20
- ojdbc_jars = %w(ojdbc11.jar ojdbc8.jar ojdbc7.jar ojdbc6.jar)
20
+ # Oracle 23c provides ojdbc17.jar for Java 17 and above
21
+ ojdbc_jars = %w(ojdbc17.jar ojdbc11.jar ojdbc8.jar ojdbc7.jar ojdbc6.jar)
21
22
 
22
23
  if !ENV_JAVA["java.class.path"]&.match?(Regexp.new(ojdbc_jars.join("|")))
23
24
  # On Unix environment variable should be PATH, on Windows it is sometimes Path
@@ -60,6 +61,8 @@ module ActiveRecord
60
61
  alias :auto_retry? :auto_retry
61
62
  @auto_retry = false
62
63
 
64
+ attr_reader :session_time_zone
65
+
63
66
  def initialize(config)
64
67
  @active = true
65
68
  @config = config
@@ -154,8 +157,10 @@ module ActiveRecord
154
157
  # Set session time zone to current time zone
155
158
  if ActiveRecord.default_timezone == :local
156
159
  @raw_connection.setSessionTimeZone(time_zone)
160
+ @session_time_zone = time_zone
157
161
  elsif ActiveRecord.default_timezone == :utc
158
162
  @raw_connection.setSessionTimeZone("UTC")
163
+ @session_time_zone = "UTC"
159
164
  end
160
165
 
161
166
  if config[:jdbc_statement_cache_size]
@@ -236,12 +241,11 @@ module ActiveRecord
236
241
  raise OracleEnhanced::ConnectionException, e.message
237
242
  end
238
243
 
244
+ # Resets connection, by logging off and creating a new connection.
239
245
  def reset
240
- # tentative
241
246
  reset!
242
247
  end
243
248
 
244
- # Resets connection, by logging off and creating a new connection.
245
249
  def reset!
246
250
  logoff rescue nil
247
251
  begin
@@ -259,7 +263,7 @@ module ActiveRecord
259
263
  begin
260
264
  yield if block_given?
261
265
  rescue Java::JavaSql::SQLException => e
262
- raise unless /^(Closed Connection|Io exception:|No more data to read from socket|IO Error:)/.match?(e.message)
266
+ raise unless /^(Closed Connection|Io exception:|No more data to read from socket|IO Error:|ORA-03113:|ORA-03114:|ORA-17008:)/.match?(e.message)
263
267
  @active = false
264
268
  raise unless should_retry
265
269
  should_retry = false
@@ -268,7 +272,11 @@ module ActiveRecord
268
272
  end
269
273
  end
270
274
 
271
- def exec(sql, allow_retry: false)
275
+ def exec(sql, *bindvars, allow_retry: false)
276
+ # The signature mirrors the OCI implementation for polymorphic
277
+ # callers, but the JDBC path here has no bindvar handling. Fail
278
+ # loudly rather than silently dropping values on the floor.
279
+ raise ArgumentError, "JDBC exec does not support bindvars" unless bindvars.empty?
272
280
  with_retry(allow_retry: allow_retry) do
273
281
  exec_no_retry(sql)
274
282
  end
@@ -298,7 +306,16 @@ module ActiveRecord
298
306
  end
299
307
 
300
308
  def prepare(sql)
301
- Cursor.new(self, @raw_connection.prepareStatement(sql))
309
+ # Use a plain Statement for DDL and PL/SQL blocks: PreparedStatement
310
+ # interprets colons as bind-parameter markers which breaks trigger
311
+ # definitions (:NEW/:OLD) and PL/SQL with named parameters.
312
+ if /\A\s*(CREATE|DROP|BEGIN|DECLARE)/i.match?(sql)
313
+ s = @raw_connection.createStatement()
314
+ s.setEscapeProcessing(false)
315
+ Cursor.new(self, s, sql)
316
+ else
317
+ Cursor.new(self, @raw_connection.prepareStatement(sql))
318
+ end
302
319
  end
303
320
 
304
321
  def database_version
@@ -306,9 +323,10 @@ module ActiveRecord
306
323
  end
307
324
 
308
325
  class Cursor
309
- def initialize(connection, raw_statement)
326
+ def initialize(connection, raw_statement, exec_sql = nil)
310
327
  @raw_connection = connection
311
328
  @raw_statement = raw_statement
329
+ @exec_sql = exec_sql # non-nil for DDL/PL/SQL using createStatement
312
330
  end
313
331
 
314
332
  def bind_params(*bind_vars)
@@ -348,8 +366,14 @@ module ActiveRecord
348
366
  when Java::JavaSql::Timestamp
349
367
  @raw_statement.setTimestamp(position, value)
350
368
  when Time
351
- new_value = Java::java.sql.Timestamp.new(value.year - 1900, value.month - 1, value.day, value.hour, value.min, value.sec, value.usec * 1000)
352
- @raw_statement.setTimestamp(position, new_value)
369
+ new_value = java.sql.Timestamp.new(value.to_i * 1000)
370
+ new_value.setNanos(value.nsec)
371
+ # Oracle JDBC getTimestamp uses the session timezone, but setTimestamp
372
+ # (without Calendar) uses the JVM default timezone. Pass a Calendar
373
+ # matching the session timezone so both sides use the same reference.
374
+ tz_id = @raw_connection.session_time_zone || java.util.TimeZone.default.getID
375
+ cal = java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone(tz_id))
376
+ @raw_statement.setTimestamp(position, new_value, cal)
353
377
  when NilClass
354
378
  # TODO: currently nil is always bound as NULL with VARCHAR type.
355
379
  # When nils will actually be used by ActiveRecord as bound parameters
@@ -369,7 +393,13 @@ module ActiveRecord
369
393
  end
370
394
 
371
395
  def exec
372
- @raw_result_set = @raw_statement.executeQuery
396
+ if @exec_sql
397
+ # DDL / PL/SQL block executed via createStatement — never returns rows
398
+ @raw_statement.execute(@exec_sql)
399
+ elsif @raw_statement.execute
400
+ # execute() returns true when the result is a ResultSet (SELECT)
401
+ @raw_result_set = @raw_statement.getResultSet
402
+ end
373
403
  true
374
404
  end
375
405
 
@@ -378,18 +408,28 @@ module ActiveRecord
378
408
  end
379
409
 
380
410
  def metadata
381
- @metadata ||= @raw_result_set.getMetaData
411
+ @metadata ||= @raw_result_set&.getMetaData
382
412
  end
383
413
 
384
414
  def column_types
415
+ return [] unless @raw_result_set
385
416
  @column_types ||= (1..metadata.getColumnCount).map { |i| metadata.getColumnTypeName(i).to_sym }
386
417
  end
387
418
 
388
419
  def column_names
420
+ return [] unless @raw_result_set
389
421
  @column_names ||= (1..metadata.getColumnCount).map { |i| metadata.getColumnName(i) }
390
422
  end
391
423
  alias :get_col_names :column_names
392
424
 
425
+ def select_statement?
426
+ !@raw_result_set.nil?
427
+ end
428
+
429
+ def row_count
430
+ @raw_statement.getUpdateCount
431
+ end
432
+
393
433
  def fetch(options = {})
394
434
  if @raw_result_set.next
395
435
  get_lob_value = options[:get_lob_value]
@@ -419,6 +459,17 @@ module ActiveRecord
419
459
 
420
460
  def close
421
461
  @raw_statement.close
462
+ rescue Java::JavaSql::SQLException => e
463
+ # Tolerate closing a statement whose underlying connection or
464
+ # statement has already been torn down (typical after with_retry
465
+ # has reset the connection). Re-raise anything else so genuine
466
+ # driver/resource bugs surface.
467
+ #
468
+ # ORA-17002 "Io exception" - network/socket failure
469
+ # ORA-17008 "Closed Connection" - connection already closed
470
+ # ORA-17009 "Closed Statement" - statement already closed
471
+ raise unless [17_002, 17_008, 17_009].include?(e.getErrorCode)
472
+ nil
422
473
  end
423
474
  end
424
475
 
@@ -514,10 +565,27 @@ module ActiveRecord
514
565
  else
515
566
  nil
516
567
  end
517
- when :TIMESTAMP, :TIMESTAMPTZ, :TIMESTAMPLTZ, :"TIMESTAMP WITH TIME ZONE", :"TIMESTAMP WITH LOCAL TIME ZONE"
568
+ when :TIMESTAMP
569
+ # Oracle JDBC getTimestamp for plain TIMESTAMP uses the JVM default timezone,
570
+ # but we store values using the session timezone. Pass a matching Calendar so
571
+ # the driver interprets the stored wall-clock value in the session timezone.
572
+ tz_id = @session_time_zone || java.util.TimeZone.default.getID
573
+ cal = java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone(tz_id))
574
+ ts = rset.getTimestamp(i, cal)
575
+ if ts
576
+ instant = ts.toInstant
577
+ t = Time.at(instant.getEpochSecond, instant.getNano, :nanosecond)
578
+ ActiveRecord.default_timezone == :utc ? t.utc : t.localtime
579
+ end
580
+ when :TIMESTAMPTZ, :TIMESTAMPLTZ, :"TIMESTAMP WITH TIME ZONE", :"TIMESTAMP WITH LOCAL TIME ZONE"
581
+ # TIMESTAMPTZ/TIMESTAMPLTZ include timezone info so getTimestamp returns
582
+ # the correct UTC epoch without needing a Calendar.
518
583
  ts = rset.getTimestamp(i)
519
- ts && Time.send(ActiveRecord.default_timezone, ts.year + 1900, ts.month + 1, ts.date, ts.hours, ts.minutes, ts.seconds,
520
- ts.nanos / 1000)
584
+ if ts
585
+ instant = ts.toInstant
586
+ t = Time.at(instant.getEpochSecond, instant.getNano, :nanosecond)
587
+ ActiveRecord.default_timezone == :utc ? t.utc : t.localtime
588
+ end
521
589
  when :CLOB
522
590
  get_lob_value ? lob_to_ruby_value(rset.getClob(i)) : rset.getClob(i)
523
591
  when :NCLOB
@@ -11,6 +11,8 @@ module ActiveRecord # :nodoc:
11
11
 
12
12
  # After setting large objects to empty, select the OCI8::LOB
13
13
  # and write back the data.
14
+ before_create :record_lobs_for_create
15
+ after_create :enhanced_write_lobs
14
16
  before_update :record_changed_lobs
15
17
  after_update :enhanced_write_lobs
16
18
  end
@@ -25,11 +27,21 @@ module ActiveRecord # :nodoc:
25
27
 
26
28
  private
27
29
  def enhanced_write_lobs
30
+ # Skip if using prepared statements - LOB data is already written via temp LOB binding
31
+ return if self.class.connection.prepared_statements
32
+
28
33
  if self.class.connection.is_a?(ConnectionAdapters::OracleEnhancedAdapter) &&
29
34
  !(self.class.custom_create_method || self.class.custom_update_method)
30
35
  self.class.connection.write_lobs(self.class.table_name, self.class, attributes, @changed_lob_columns)
31
36
  end
32
37
  end
38
+
39
+ def record_lobs_for_create
40
+ @changed_lob_columns = self.class.lob_columns.select do |col|
41
+ !attributes[col.name].nil? && !self.class.readonly_attributes.to_a.include?(col.name)
42
+ end
43
+ end
44
+
33
45
  def record_changed_lobs
34
46
  @changed_lob_columns = self.class.lob_columns.select do |col|
35
47
  self.will_save_change_to_attribute?(col.name) && !self.class.readonly_attributes.to_a.include?(col.name)
@@ -158,6 +158,14 @@ module ActiveRecord
158
158
  @raw_cursor.get_col_names
159
159
  end
160
160
 
161
+ def row_count
162
+ @raw_cursor.row_count
163
+ end
164
+
165
+ def select_statement?
166
+ @raw_cursor.type == :select_stmt
167
+ end
168
+
161
169
  def fetch(options = {})
162
170
  if row = @raw_cursor.fetch
163
171
  get_lob_value = options[:get_lob_value]
@@ -12,6 +12,7 @@ module ActiveRecord
12
12
  @lob_tablespaces[o.name] = tablespace
13
13
  end
14
14
  end
15
+ o.cast_type = lookup_cast_type(sql_type)
15
16
  super
16
17
  end
17
18
 
@@ -219,9 +219,9 @@ module ActiveRecord
219
219
  unless create_sequence
220
220
  class << td
221
221
  attr_accessor :create_sequence
222
- def primary_key(*args)
222
+ def primary_key(name, type = :primary_key, **options)
223
223
  self.create_sequence = true
224
- super(*args)
224
+ super(name, type, **options)
225
225
  end
226
226
  end
227
227
  end
@@ -284,7 +284,7 @@ module ActiveRecord
284
284
  } << "SELECT * FROM DUAL\n"
285
285
  else
286
286
  if versions.is_a?(Array)
287
- # called from ActiveRecord::Base.connection#dump_schema_information
287
+ # called from ActiveRecord::Base.connection#dump_schema_versions
288
288
  versions.map { |version|
289
289
  "INSERT INTO #{sm_table} (version) VALUES (#{quote(version)})"
290
290
  }.join("\n\n/\n\n")
@@ -674,6 +674,7 @@ module ActiveRecord
674
674
  default_value = extract_value_from_default(field["data_default"])
675
675
  default_value = nil if is_virtual
676
676
  OracleEnhanced::Column.new(oracle_downcase(field["name"]),
677
+ lookup_cast_type(field["sql_type"]),
677
678
  default_value,
678
679
  type_metadata,
679
680
  field["nullable"] == "Y",
@@ -698,7 +699,7 @@ module ActiveRecord
698
699
  end
699
700
 
700
701
  def default_tablespace_for(type)
701
- (default_tablespaces[type] || default_tablespaces[native_database_types[type][:name]]) rescue nil
702
+ default_tablespaces[type]
702
703
  end
703
704
 
704
705
  def column_for(table_name, column_name)
@@ -51,6 +51,7 @@ module ActiveRecord # :nodoc:
51
51
  structure << ddl
52
52
  structure << structure_dump_indexes(table_name)
53
53
  structure << structure_dump_unique_keys(table_name)
54
+ structure << structure_dump_check_constraints(table_name)
54
55
  structure << structure_dump_table_comments(table_name)
55
56
  structure << structure_dump_column_comments(table_name)
56
57
  end
@@ -161,6 +162,33 @@ module ActiveRecord # :nodoc:
161
162
  join_with_statement_token(fks)
162
163
  end
163
164
 
165
+ # Only user-named CHECK constraints are preserved. Anonymous
166
+ # constraints created via `ALTER TABLE ... ADD CHECK (...)` receive
167
+ # Oracle-generated names (e.g. SYS_C00123) and are skipped to avoid
168
+ # also emitting the implicit NOT NULL check constraints that Oracle
169
+ # stores with constraint_type = 'C'.
170
+ def structure_dump_check_constraints(table_name) # :nodoc:
171
+ # `search_condition` is a LONG column, so it cannot appear in a
172
+ # WHERE clause (ORA-00997). The driver reads it as a String on
173
+ # SELECT. Implicit NOT NULL constraints Oracle stores with
174
+ # constraint_type = 'C' are excluded by `generated = 'USER NAME'`
175
+ # since they receive system-generated names.
176
+ check_constraints = select_all(<<~SQL.squish, "SCHEMA", [bind_string("table_name", table_name.upcase)])
177
+ SELECT constraint_name, search_condition
178
+ FROM all_constraints
179
+ WHERE owner = SYS_CONTEXT('userenv', 'current_schema')
180
+ AND table_name = :table_name
181
+ AND constraint_type = 'C'
182
+ AND generated = 'USER NAME'
183
+ ORDER BY constraint_name
184
+ SQL
185
+ check_constraints.filter_map do |row|
186
+ condition = row["search_condition"]
187
+ next if condition.nil?
188
+ "ALTER TABLE #{quote_table_name(table_name)} ADD CONSTRAINT #{quote_column_name(row["constraint_name"])} CHECK (#{condition})"
189
+ end
190
+ end
191
+
164
192
  def structure_dump_table_comments(table_name)
165
193
  comments = []
166
194
  comment = table_comment(table_name)