Howdy folks.
Real person here who had to get some help from AI for this one, hope that's ok since there was no way I would've figured this one out on my own.
The short version is we're trying to upgrade from Rails 7 to Rails 8 and hitting an unusual issue with triggers. Most of the text below was crafted by GPT 5.5 under my guidance:
Title
Rails 8 + activerecord-sqlserver-adapter 8.0.10: triggered INSERT can return array-wrapped identity ([1]) and fail integer deserialization
Summary
During a Rails 7 to Rails 8 upgrade, we observed INSERT failures only when an AFTER INSERT trigger fires on SQL Server. In failing cases, the adapter returns an array-wrapped value for the inserted identity, and Rails 8 raises:
NoMethodError: undefined method to_i for an instance of Array
Non-trigger inserts continue to work.
Environment
- Rails: 8.0.5
- activerecord-sqlserver-adapter: 8.0.10
- Ruby: 3.3.x
- SQL Server: 2022
Steps To Reproduce
- Use Rails 8 and activerecord-sqlserver-adapter 8.0.10.
- Set ActiveRecord::ConnectionAdapters::SQLServerAdapter.use_output_inserted = false.
- Create a table with identity PK and an AFTER INSERT trigger that performs additional DML.
- Insert one row where trigger criteria does not match.
- Insert one row where trigger criteria matches.
- Instrument adapter methods last_inserted_id and returning_column_values to inspect returned shapes.
Minimal Trigger Shape (Anonymized)
CREATE TRIGGER [dbo].[WorkflowTrigger_NEW] ON [dbo].[EventTable]
AFTER INSERT
AS
BEGIN
INSERT [dbo].[WorkflowAudit]
(event_id, workflow_state_id, action_id)
SELECT i.id, wf.state_id, wf.action_id
FROM inserted i
LEFT JOIN [dbo].[WorkflowState] r ON r.id = i.state_id
JOIN [dbo].[WorkflowDefinition] wf ON wf.state_id = r.id
WHERE wf.criteria IN ('Shared', 'Accessed', 'Archived')
END
Expected Behavior
Both inserts return scalar identity values and succeed, regardless of trigger execution.
Actual Behavior
When trigger does not fire:
last_inserted_id => 583 (Integer)
returning_column_values => [583] (Array)
OK id=583 (Integer)
When trigger fires:
last_inserted_id => [1] (Array)
returning_column_values => [[1]] (Array)
FAIL NoMethodError: undefined method `to_i` for an instance of Array
Additional Confirmed Findings
In our testing environment:
- Session NOCOUNT: OFF
- Server default NOCOUNT: OFF
- Trigger Workflow: NOCOUNT not specified
- Trigger SetRequestOutcome: NOCOUNT not specified
So this is not caused by a session/server NOCOUNT ON configuration.
Why This Appears As Rails 7 vs Rails 8
The adapter return shape issue existed but was effectively tolerated in earlier behavior. Rails 8 stricter integer coercion now surfaces it immediately when an Array reaches integer deserialization.
Current Workaround
ActiveRecord::ConnectionAdapters::SQLServerAdapter.use_output_inserted = false
module SQLServerAdapterPatch
def last_inserted_id(result)
value = super
value = value.first if value.is_a?(Array) && value.size == 1 && !value.first.is_a?(Array)
value
end
def returning_column_values(result)
values = super
values = values.map { |v| v.is_a?(Array) && v.size == 1 ? v.first : v } if values.is_a?(Array)
values
end
end
ActiveRecord::ConnectionAdapters::SQLServerAdapter.prepend(SQLServerAdapterPatch)
Proposed Fix
Any of the following should address this class of issue:
- Ignore trigger-generated intermediate result rows when computing identity return values.
- Normalize last_inserted_id and returning_column_values so single identity paths never return array-wrapped scalars.
- Add regression tests for AFTER INSERT triggers that emit extra result shapes when use_output_inserted = false.
Notes
- exclude_output_inserted_table_names did not resolve this by itself.
- use_output_inserted = false was necessary but not sufficient in this failing path.
Ok, it's me again, the human. If necessary I can checkin a minimal repo with docker, rails, etc to demonstrate the issue.
Or if this is something obvious, I'm all ears!
Howdy folks.
Real person here who had to get some help from AI for this one, hope that's ok since there was no way I would've figured this one out on my own.
The short version is we're trying to upgrade from Rails 7 to Rails 8 and hitting an unusual issue with triggers. Most of the text below was crafted by GPT 5.5 under my guidance:
Title
Rails 8 + activerecord-sqlserver-adapter 8.0.10: triggered INSERT can return array-wrapped identity ([1]) and fail integer deserialization
Summary
During a Rails 7 to Rails 8 upgrade, we observed INSERT failures only when an AFTER INSERT trigger fires on SQL Server. In failing cases, the adapter returns an array-wrapped value for the inserted identity, and Rails 8 raises:
NoMethodError: undefined method to_i for an instance of Array
Non-trigger inserts continue to work.
Environment
Steps To Reproduce
Minimal Trigger Shape (Anonymized)
Expected Behavior
Both inserts return scalar identity values and succeed, regardless of trigger execution.
Actual Behavior
When trigger does not fire:
When trigger fires:
Additional Confirmed Findings
In our testing environment:
So this is not caused by a session/server NOCOUNT ON configuration.
Why This Appears As Rails 7 vs Rails 8
The adapter return shape issue existed but was effectively tolerated in earlier behavior. Rails 8 stricter integer coercion now surfaces it immediately when an Array reaches integer deserialization.
Current Workaround
Proposed Fix
Any of the following should address this class of issue:
Notes
Ok, it's me again, the human. If necessary I can checkin a minimal repo with docker, rails, etc to demonstrate the issue.
Or if this is something obvious, I'm all ears!