Skip to content

Commit d4f8806

Browse files
committed
Add affected_rows to ActiveRecord::Result
For database adapters that do not support RETURNING, insert_all and upsert_all does not provide what rows has been changed. Adding affected_rows to the result allows this information to be accessed like other bulk operations (like delete_all and update_all)
1 parent 4077fe0 commit d4f8806

File tree

8 files changed

+64
-45
lines changed

8 files changed

+64
-45
lines changed

activerecord/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
* Add `affected_rows` to `ActiveRecord::Result`.
2+
3+
*Jenny Shen*
4+
15
* Enable passing retryable SqlLiterals to `#where`.
26

37
*Hartley McGuire*

activerecord/lib/active_record/connection_adapters/mysql2/database_statements.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,12 @@ def perform_query(raw_connection, sql, binds, type_casted_binds, prepare:, notif
114114
end
115115

116116
def cast_result(raw_result)
117-
return ActiveRecord::Result.empty if raw_result.nil?
117+
return ActiveRecord::Result.empty(affected_rows: @affected_rows_before_warnings) if raw_result.nil?
118118

119119
fields = raw_result.fields
120120

121121
result = if fields.empty?
122-
ActiveRecord::Result.empty
122+
ActiveRecord::Result.empty(affected_rows: @affected_rows_before_warnings)
123123
else
124124
ActiveRecord::Result.new(fields, raw_result.to_a)
125125
end

activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -170,20 +170,20 @@ def perform_query(raw_connection, sql, binds, type_casted_binds, prepare:, notif
170170
end
171171

172172
def cast_result(result)
173-
if result.fields.empty?
174-
result.clear
175-
return ActiveRecord::Result.empty
176-
end
173+
ar_result = if result.fields.empty?
174+
ActiveRecord::Result.empty(affected_rows: result.cmd_tuples)
175+
else
176+
fields = result.fields
177+
types = Array.new(fields.size)
178+
fields.size.times do |index|
179+
ftype = result.ftype(index)
180+
fmod = result.fmod(index)
181+
types[index] = get_oid_type(ftype, fmod, fields[index])
182+
end
177183

178-
fields = result.fields
179-
types = Array.new(fields.size)
180-
fields.size.times do |index|
181-
ftype = result.ftype(index)
182-
fmod = result.fmod(index)
183-
types[index] = get_oid_type(ftype, fmod, fields[index])
184+
ActiveRecord::Result.new(fields, result.values, types.freeze, affected_rows: result.cmd_tuples)
184185
end
185186

186-
ar_result = ActiveRecord::Result.new(fields, result.values, types.freeze)
187187
result.clear
188188
ar_result
189189
end

activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -88,35 +88,31 @@ def perform_query(raw_connection, sql, binds, type_casted_binds, prepare:, notif
8888

8989
if batch
9090
raw_connection.execute_batch2(sql)
91-
elsif prepare
92-
stmt = @statements[sql] ||= raw_connection.prepare(sql)
93-
stmt.reset!
94-
stmt.bind_params(type_casted_binds)
95-
96-
result = if stmt.column_count.zero? # No return
97-
stmt.step
98-
ActiveRecord::Result.empty
91+
else
92+
stmt = if prepare
93+
@statements[sql] ||= raw_connection.prepare(sql)
94+
@statements[sql].reset!
9995
else
100-
ActiveRecord::Result.new(stmt.columns, stmt.to_a, stmt.types.map { |t| type_map.lookup(t) })
96+
# Don't cache statements if they are not prepared.
97+
raw_connection.prepare(sql)
10198
end
102-
else
103-
# Don't cache statements if they are not prepared.
104-
stmt = raw_connection.prepare(sql)
10599
begin
106100
unless binds.nil? || binds.empty?
107101
stmt.bind_params(type_casted_binds)
108102
end
109103
result = if stmt.column_count.zero? # No return
110104
stmt.step
111-
ActiveRecord::Result.empty
105+
@affected_rows = raw_connection.total_changes - total_changes_before_query
106+
ActiveRecord::Result.empty(affected_rows: @affected_rows)
112107
else
113-
ActiveRecord::Result.new(stmt.columns, stmt.to_a, stmt.types.map { |t| type_map.lookup(t) })
108+
rows = stmt.to_a
109+
@affected_rows = raw_connection.total_changes - total_changes_before_query
110+
ActiveRecord::Result.new(stmt.columns, rows, stmt.types.map { |t| type_map.lookup(t) }, affected_rows: @affected_rows)
114111
end
115112
ensure
116-
stmt.close
113+
stmt.close unless prepare
117114
end
118115
end
119-
@affected_rows = raw_connection.total_changes - total_changes_before_query
120116
verified!
121117

122118
notification_payload[:affected_rows] = @affected_rows

activerecord/lib/active_record/connection_adapters/trilogy/database_statements.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ def perform_query(raw_connection, sql, binds, type_casted_binds, prepare:, notif
4141

4242
def cast_result(result)
4343
if result.fields.empty?
44-
ActiveRecord::Result.empty
44+
ActiveRecord::Result.empty(affected_rows: result.affected_rows)
4545
else
46-
ActiveRecord::Result.new(result.fields, result.rows)
46+
ActiveRecord::Result.new(result.fields, result.rows, affected_rows: result.affected_rows)
4747
end
4848
end
4949

activerecord/lib/active_record/result.rb

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ module ActiveRecord
2929
# ...
3030
# ]
3131
#
32+
# # Get the number of rows affected by the query:
33+
# result = ActiveRecord::Base.lease_connection.exec_query('INSERT INTO posts (title, body) VALUES ("title_3", "body_3"), ("title_4", "body_4")')
34+
# result.affected_rows
35+
# # => 2
36+
#
3237
# # ActiveRecord::Result also includes Enumerable.
3338
# result.each do |row|
3439
# puts row['title'] + " " + row['body']
@@ -89,17 +94,17 @@ def to_h
8994
alias_method :to_hash, :to_h
9095
end
9196

92-
attr_reader :columns, :rows
97+
attr_reader :columns, :rows, :affected_rows
9398

94-
def self.empty(async: false) # :nodoc:
99+
def self.empty(async: false, affected_rows: nil) # :nodoc:
95100
if async
96-
EMPTY_ASYNC
101+
FutureResult.wrap(new(EMPTY_ARRAY, EMPTY_ARRAY, EMPTY_HASH, affected_rows: affected_rows)).freeze
97102
else
98-
EMPTY
103+
new(EMPTY_ARRAY, EMPTY_ARRAY, EMPTY_HASH, affected_rows: affected_rows).freeze
99104
end
100105
end
101106

102-
def initialize(columns, rows, column_types = nil)
107+
def initialize(columns, rows, column_types = nil, affected_rows: nil)
103108
# We freeze the strings to prevent them getting duped when
104109
# used as keys in ActiveRecord::Base's @attributes hash
105110
@columns = columns.each(&:-@).freeze
@@ -108,6 +113,7 @@ def initialize(columns, rows, column_types = nil)
108113
@column_types = column_types.freeze
109114
@types_hash = nil
110115
@column_indexes = nil
116+
@affected_rows = affected_rows
111117
end
112118

113119
# Returns true if this result set includes the column named +name+
@@ -260,14 +266,8 @@ def hash_rows
260266
end
261267
end
262268

263-
empty_array = [].freeze
269+
EMPTY_ARRAY = [].freeze
264270
EMPTY_HASH = {}.freeze
265-
private_constant :EMPTY_HASH
266-
267-
EMPTY = new(empty_array, empty_array, EMPTY_HASH).freeze
268-
private_constant :EMPTY
269-
270-
EMPTY_ASYNC = FutureResult.wrap(EMPTY).freeze
271-
private_constant :EMPTY_ASYNC
271+
private_constant :EMPTY_ARRAY, :EMPTY_HASH
272272
end
273273
end

activerecord/test/cases/adapter_test.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,23 @@ def test_current_database
153153
assert_not_empty result.columns
154154
end
155155

156+
test "#exec_query queries return an ActiveRecord::Result with affected rows" do
157+
result = @connection.exec_query "INSERT INTO subscribers(nick, name) VALUES('me', 'me'), ('you', 'you')"
158+
assert_equal 2, result.affected_rows
159+
160+
update_result = @connection.exec_query "UPDATE subscribers SET name = 'you' WHERE name = 'me'"
161+
assert_equal 1, update_result.affected_rows
162+
163+
select_result = @connection.exec_query "SELECT * FROM subscribers"
164+
assert_not_equal update_result.affected_rows, select_result.affected_rows
165+
166+
result = @connection.exec_query "DELETE FROM subscribers WHERE name = 'you'"
167+
assert_equal 2, result.affected_rows
168+
169+
result = @connection.exec_query "DELETE FROM subscribers WHERE name = 'you'"
170+
assert_equal 0, result.affected_rows
171+
end
172+
156173
if current_adapter?(:Mysql2Adapter, :TrilogyAdapter)
157174
def test_charset
158175
assert_not_nil @connection.charset

activerecord/test/cases/result_test.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ def result
99
["row 1 col 1", "row 1 col 2"],
1010
["row 2 col 1", "row 2 col 2"],
1111
["row 3 col 1", "row 3 col 2"],
12-
])
12+
], affected_rows: 3)
1313
end
1414

1515
test "includes_column?" do
@@ -138,6 +138,7 @@ def result
138138
assert_equal a.columns, b.columns
139139
assert_equal a.rows, b.rows
140140
assert_equal a.column_indexes, b.column_indexes
141+
assert_equal a.affected_rows, b.affected_rows
141142

142143
# Second round in case of mutation
143144
b = b.dup
@@ -146,6 +147,7 @@ def result
146147
assert_equal a.columns, b.columns
147148
assert_equal a.rows, b.rows
148149
assert_equal a.column_indexes, b.column_indexes
150+
assert_equal a.affected_rows, b.affected_rows
149151
end
150152

151153
test "column_types handles nil types in the column_types array" do

0 commit comments

Comments
 (0)