Skip to content

Commit e7073e4

Browse files
authored
Merge pull request rails#50887 from marvin-bitterlich/marvin.bitterlich/result-set
Add row_count field to sql.active_record notification
2 parents 5606783 + e9a2288 commit e7073e4

File tree

9 files changed

+70
-12
lines changed

9 files changed

+70
-12
lines changed

activerecord/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
* Add row_count field to sql.active_record notification
2+
3+
This field returns the amount of rows returned by the query that emitted the notification.
4+
5+
This metric is useful in cases where one wants to detect queries with big result sets.
6+
7+
*Marvin Bitterlich*
8+
19
* Consistently raise an `ArgumentError` when passing an invalid argument to a nested attributes association writer.
210

311
Previously, this would only raise on collection associations and produce a generic error on singular associations.

activerecord/lib/active_record/connection_adapters/abstract_adapter.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,6 +1156,7 @@ def log(sql, name = "SQL", binds = [], type_casted_binds = [], statement_name =
11561156
statement_name: statement_name,
11571157
async: async,
11581158
connection: self,
1159+
row_count: 0,
11591160
&block
11601161
)
11611162
rescue ActiveRecord::StatementInvalid => ex

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,13 @@ def with_multi_statements
9898
end
9999

100100
def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
101-
log(sql, name, async: async) do
101+
log(sql, name, async: async) do |notification_payload|
102102
with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
103103
sync_timezone_changes(conn)
104104
result = conn.query(sql)
105105
verified!
106106
handle_warnings(sql)
107+
notification_payload[:row_count] = result&.size || 0
107108
result
108109
end
109110
end
@@ -117,7 +118,7 @@ def exec_stmt_and_free(sql, name, binds, cache_stmt: false, async: false)
117118

118119
type_casted_binds = type_casted_binds(binds)
119120

120-
log(sql, name, binds, type_casted_binds, async: async) do
121+
log(sql, name, binds, type_casted_binds, async: async) do |notification_payload|
121122
with_raw_connection do |conn|
122123
sync_timezone_changes(conn)
123124

@@ -143,6 +144,7 @@ def exec_stmt_and_free(sql, name, binds, cache_stmt: false, async: false)
143144
end
144145

145146
ret = yield stmt, result
147+
notification_payload[:row_count] = result&.size || 0
146148
result.free if result
147149
stmt.close unless cache_stmt
148150
ret

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ def explain(arel, binds = [], options = [])
1414
def query(sql, name = nil) # :nodoc:
1515
mark_transaction_written_if_write(sql)
1616

17-
log(sql, name) do
17+
log(sql, name) do |notification_payload|
1818
with_raw_connection do |conn|
1919
result = conn.async_exec(sql).map_types!(@type_map_for_results).values
2020
verified!
21+
notification_payload[:row_count] = result.count
2122
result
2223
end
2324
end
@@ -50,11 +51,12 @@ def execute(...) # :nodoc:
5051
end
5152

5253
def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
53-
log(sql, name, async: async) do
54+
log(sql, name, async: async) do |notification_payload|
5455
with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
5556
result = conn.async_exec(sql)
5657
verified!
5758
handle_warnings(result)
59+
notification_payload[:row_count] = result.count
5860
result
5961
end
6062
end

activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -880,10 +880,11 @@ def exec_no_cache(sql, name, binds, async:, allow_retry:, materialize_transactio
880880
update_typemap_for_default_timezone
881881

882882
type_casted_binds = type_casted_binds(binds)
883-
log(sql, name, binds, type_casted_binds, async: async) do
883+
log(sql, name, binds, type_casted_binds, async: async) do |notification_payload|
884884
with_raw_connection(allow_retry: false, materialize_transactions: materialize_transactions) do |conn|
885885
result = conn.exec_params(sql, type_casted_binds)
886886
verified!
887+
notification_payload[:row_count] = result.count
887888
result
888889
end
889890
end
@@ -898,9 +899,10 @@ def exec_cache(sql, name, binds, async:, allow_retry:, materialize_transactions:
898899
stmt_key = prepare_statement(sql, binds, conn)
899900
type_casted_binds = type_casted_binds(binds)
900901

901-
log(sql, name, binds, type_casted_binds, stmt_key, async: async) do
902+
log(sql, name, binds, type_casted_binds, stmt_key, async: async) do |notification_payload|
902903
result = conn.exec_prepared(stmt_key, type_casted_binds)
903904
verified!
905+
notification_payload[:row_count] = result.count
904906
result
905907
end
906908
end

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def internal_exec_query(sql, name = nil, binds = [], prepare: false, async: fals
2929

3030
type_casted_binds = type_casted_binds(binds)
3131

32-
log(sql, name, binds, type_casted_binds, async: async) do
32+
log(sql, name, binds, type_casted_binds, async: async) do |notification_payload|
3333
with_raw_connection do |conn|
3434
# Don't cache statements if they are not prepared
3535
unless prepare
@@ -52,7 +52,9 @@ def internal_exec_query(sql, name = nil, binds = [], prepare: false, async: fals
5252
end
5353
verified!
5454

55-
build_result(columns: cols, rows: records)
55+
result = build_result(columns: cols, rows: records)
56+
notification_payload[:row_count] = result.length
57+
result
5658
end
5759
end
5860
end
@@ -113,10 +115,11 @@ def high_precision_current_timestamp
113115

114116
private
115117
def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: false)
116-
log(sql, name, async: async) do
118+
log(sql, name, async: async) do |notification_payload|
117119
with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
118120
result = conn.execute(sql)
119121
verified!
122+
notification_payload[:row_count] = result.length
120123
result
121124
end
122125
end
@@ -136,10 +139,11 @@ def execute_batch(statements, name = nil)
136139
check_if_write_query(sql)
137140
mark_transaction_written_if_write(sql)
138141

139-
log(sql, name) do
142+
log(sql, name) do |notification_payload|
140143
with_raw_connection do |conn|
141144
result = conn.execute_batch2(sql)
142145
verified!
146+
notification_payload[:row_count] = result.length
143147
result
144148
end
145149
end

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,13 @@ def exec_delete(sql, name = nil, binds = []) # :nodoc:
4343

4444
private
4545
def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
46-
log(sql, name, async: async) do
46+
log(sql, name, async: async) do |notification_payload|
4747
with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
4848
sync_timezone_changes(conn)
4949
result = conn.query(sql)
5050
verified!
5151
handle_warnings(sql)
52+
notification_payload[:row_count] = result.count
5253
result
5354
end
5455
end

activerecord/test/cases/instrumentation_test.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,42 @@ def test_payload_name_on_grouped_count
111111
ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
112112
end
113113

114+
def test_payload_row_count_on_select_all
115+
10.times { Book.create(name: "row count book 1") }
116+
subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |_, _, _, _, payload|
117+
if payload[:sql].match?("SELECT")
118+
assert_equal 10, payload[:row_count]
119+
end
120+
end
121+
Book.where(name: "row count book 1").to_a
122+
ensure
123+
ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
124+
end
125+
126+
def test_payload_row_count_on_pluck
127+
10.times { Book.create(name: "row count book 2") }
128+
subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |_, _, _, _, payload|
129+
if payload[:sql].match?("SELECT")
130+
assert_equal 10, payload[:row_count]
131+
end
132+
end
133+
Book.where(name: "row count book 2").pluck(:name)
134+
ensure
135+
ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
136+
end
137+
138+
def test_payload_row_count_on_raw_sql
139+
10.times { Book.create(name: "row count book 3") }
140+
subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |_, _, _, _, payload|
141+
if payload[:sql].match?("SELECT")
142+
assert_equal 10, payload[:row_count]
143+
end
144+
end
145+
ActiveRecord::Base.connection.execute("SELECT * FROM books WHERE name='row count book 3';")
146+
ensure
147+
ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
148+
end
149+
114150
def test_payload_connection_with_query_cache_disabled
115151
connection = Book.connection
116152
subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |_, _, _, _, payload|

guides/source/active_support_instrumentation.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,7 @@ The `:cache_hits` key is only included if the collection is rendered with `cache
365365
| `:statement_name` | SQL Statement name |
366366
| `:async` | `true` if query is loaded asynchronously |
367367
| `:cached` | `true` is added when cached queries used |
368+
| `:row_count` | Number of rows returned by the query |
368369

369370
Adapters may add their own data as well.
370371

@@ -375,7 +376,8 @@ Adapters may add their own data as well.
375376
connection: <ActiveRecord::ConnectionAdapters::SQLite3Adapter:0x00007f9f7a838850>,
376377
binds: [<ActiveModel::Attribute::WithCastValue:0x00007fe19d15dc00>],
377378
type_casted_binds: [11],
378-
statement_name: nil
379+
statement_name: nil,
380+
row_count: 5
379381
}
380382
```
381383

0 commit comments

Comments
 (0)