The activerecord-oracle_enhanced-adapter has a two-phase LOB write mechanism. When prepared_statements is disabled (which happens automatically in Rails 8 development with query logging), Oracle can't bind LOB data inline. Instead:
- INSERT with
empty_clob()/empty_blob()placeholders - SELECT the row back FOR UPDATE using the primary key, then write the LOB data via the locator
The stock implementation of step 2 assumes a single-column primary key:
id = quote(attributes[klass.primary_key])
# ...
WHERE #{quote_column_name(klass.primary_key)} = #{id} FOR UPDATEThis breaks for:
- Composite primary keys —
primary_keyreturns an Array,attributes[array]returns nil,quote_column_name(array)produces garbage. Result:StatementInvalid. - Tables with no primary key —
primary_keyreturns nil, WHERE clause is nonsense. Result:RecordNotFound, LOB data silently lost.
After an INSERT, Oracle's cursor holds the ROWID of the inserted row. ROWID uniquely identifies any row in any table regardless of key structure. By capturing it right after exec_update, we can use it in the subsequent SELECT FOR UPDATE:
SELECT "DESCRIPTION" FROM "MY_TABLE" WHERE ROWID = 'AAAFo1AABAAAMk1AAA' FOR UPDATEThis works because:
- The
after_createcallback fires immediately after INSERT on the same connection - ROWID is valid for the lifetime of the row (stable within a transaction)
- No dependency on primary key structure
The monkey patch captures ROWID via cursor.rowid (ruby-oci8 API) in exec_insert, then uses it as a fallback in write_lobs when no primary key is available.
The patched write_lobs tries these strategies in order:
| Priority | Condition | WHERE clause |
|---|---|---|
| 1 | Composite PK (pk.is_a?(Array)) |
col_a = x AND col_b = y |
| 2 | Single PK | pk = id (original behavior) |
| 3 | No PK + ROWID captured | ROWID = 'xxx' |
| 4 | No PK + no ROWID | Log warning, bail |
This is experimental and has known limitations:
- ruby-oci8 only —
cursor.rowidis a ruby-oci8 API. JDBC cursors don't expose ROWID the same way. The patch usesrespond_to?(:rowid)to degrade gracefully, but JDBC users with no-PK tables won't benefit. - ROWID stability — ROWIDs can change during table reorganization (ALTER TABLE MOVE, partition operations, etc.). This is safe within a transaction but not across them. Since the callback fires immediately after INSERT on the same connection, this is fine in practice.
- Instance variable on connection —
@last_insert_rowidis stored on the connection adapter instance. In theory, if something else runs an INSERT betweenexec_insertand theafter_createcallback, it could overwrite the ROWID. In practice, ActiveRecord's callback chain runs synchronously.
For the composite primary key fix alone (no ROWID, works on both platforms), see: https://gist.github.com/andynu/508ed5b16f348d474db377ab1e870778
activerecord-oracle_enhanced-adapter7.x and 8.x- Only manifests when
prepared_statements: false - ROWID capture requires ruby-oci8 (CRuby); composite PK fix works everywhere
Drop into an initializer:
# config/initializers/oracle_enhanced_rowid_lob_fix.rb
require_relative "../../lib/patches/oracle_enhanced_rowid_lob_fix"