Skip to content

Render Arel returning as RETURNING ... INTO with OUT binds - #2944

Open
yahonda wants to merge 5 commits into
rsim:masterfrom
yahonda:arel-returning-into
Open

yahonda wants to merge 5 commits into
rsim:masterfrom
yahonda:arel-returning-into

Conversation

@yahonda

@yahonda yahonda commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

Depends on #2941. This branch is stacked on it, so the first two commits belong to #2941; please review the last three commits (a738cfc, fd704bd and 101ee0c) here.

The Oracle Arel visitor now renders a non-empty returning on INSERT, UPDATE and DELETE statements as Oracle's RETURNING <columns> INTO <placeholders>, adding an OUT bind for each column with its type from the schema cache. The adapter already reads OUT binds back after execution, so Active Record's own apply_returning_to! and update_with_result work unchanged.

Changes

  • OracleCommon#visit_Arel_Nodes_{Insert,Update,Delete}Statement render returning as RETURNING ... INTO. A node that is not a column of the target table, such as Arel.star or a column of another table, raises ArgumentError, as does a column whose type cannot be read back through an OUT bind, such as DATE.
  • apply_returning_to! delegates Arel INSERTs to Active Record and keeps the string append only for SQL strings.
  • Without prepared statements, SubstituteBinds would inline the OUT binds as NULL, so to_sql_and_binds keeps every bind as a placeholder for statements with returning.

Removed

Direct Arel callers can now use returning, which is option (a) in #2796:

t = Arel::Table.new(name: :items)
dm = Arel::DeleteManager.new.from(t).where(t[:id].eq(1)).returning([t[:id], t[:name]])
ActiveRecord::Base.lease_connection.to_sql(dm)
# => DELETE FROM "ITEMS" WHERE "ITEMS"."ID" = 1 RETURNING "ITEMS"."ID", "ITEMS"."NAME" INTO :a1, :a2

The specs run INSERT, UPDATE and DELETE statements like this with and without prepared statements and read the returned rows.

INSERT statements built by Active Record now use the visitor's placeholders (:aN) instead of :returning_<column>.

Tests

Full suite against rails/rails@2fe713e2e0:

prepared statements ORACLE_ENHANCED_PREPARED_STATEMENTS_FALSE=1
CRuby (OCI) 1216 examples, 0 failures 1216 examples, 0 failures
JRuby 1223 examples, 0 failures 1223 examples, 0 failures

yahonda and others added 5 commits October 25, 2026 08:40
Rails can reload virtual columns on update with `UPDATE ... RETURNING` when
the adapter opts in with `supports_update_returning?` (rails/rails#48628).
Oracle needs `RETURNING ... INTO` with OUT binds, which Arel cannot express,
so `update_with_result` appends the clause to the compiled SQL instead of
setting `returning` on the Arel statement, the same way INSERT does.

- Numeric virtual columns are read back as BigDecimal so fractions are kept.
- Virtual columns of other types, such as DATE, are not returned and still
  need a reload.
- An UPDATE that matches no rows returns no rows instead of a row of nils.
- `cast_result` now passes `affected_rows` to `ActiveRecord::Result`.
- JDBC reads every RETURNING column from the same row instead of advancing
  the return result set once per column.

Optimistic locking takes the regular update path in Active Record, so
models with a lock column still need a reload.

Fix rsim#2795

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without a length, ruby-oci8 binds a String OUT parameter with a 1333-byte
buffer, so a string virtual column longer than that came back as nil. Bind
it with the VARCHAR2 maximum instead.

Read `row_count` after fetching, as before, so SELECTs on OCI report the
number of fetched rows instead of 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Oracle visitor now renders a non-empty `returning` on INSERT, UPDATE
and DELETE statements as `RETURNING <columns> INTO <placeholders>`, adding
an OUT bind for each column with its type from the schema cache. Active
Record's `apply_returning_to!` and `update_with_result` set `returning` on
the Arel statement, so oracle-enhanced no longer rewrites the compiled SQL
for them, and direct Arel callers can use `returning` too.

Without prepared statements, SubstituteBinds would inline the OUT binds as
NULL, so statements with `returning` keep every bind as a placeholder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Raise ArgumentError for a returning column of another table, or of a
  type that cannot be read back through an OUT bind, such as DATE.
- Raise ArgumentError instead of NoMethodError when the target relation
  is not a table.
- Raise for binds passed with an Arel AST in the `to_sql_and_binds`
  override, as Active Record does.
- Use the module function `OracleEnhanced::Quoting.oracle_downcase`.
- Run INSERT, UPDATE and DELETE with returning with and without prepared
  statements; the UPDATE also binds its WHERE value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Assert the returned columns for INSERT and UPDATE too, drop an unused
helper, and name a rejected attribute as table.column in the error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@yahonda
yahonda force-pushed the arel-returning-into branch from 101ee0c to 4864934 Compare October 24, 2026 23:47

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant