Skip to content

Commit e9a2288

Browse files
Add row_count field to sql.active_record notification
This field returns the amount of rows returned by the query that emitted the notification. This metric is useful in cases where one wants to detect queries with big result sets.
1 parent 68eade8 commit e9a2288

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)