Skip to content

Commit 4eeca2a

Browse files
authored
Merge pull request rails#43097 from Shopify/ar-query-transformers
Refactor ActiveRecord::QueryLogs hook point
2 parents dc047f7 + 12cc24a commit 4eeca2a

File tree

10 files changed

+49
-44
lines changed

10 files changed

+49
-44
lines changed

activerecord/lib/active_record.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,9 @@ def self.global_executor_concurrency # :nodoc:
329329
singleton_class.attr_accessor :verify_foreign_keys_for_fixtures
330330
self.verify_foreign_keys_for_fixtures = false
331331

332+
singleton_class.attr_accessor :query_transformers
333+
self.query_transformers = []
334+
332335
def self.eager_load!
333336
super
334337
ActiveRecord::Locking.eager_load!

activerecord/lib/active_record/connection_adapters/abstract_adapter.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,13 @@ def log(sql, name = "SQL", binds = [], type_casted_binds = [], statement_name =
741741
end
742742
end
743743

744+
def transform_query(sql)
745+
ActiveRecord.query_transformers.each do |transformer|
746+
sql = transformer.call(sql)
747+
end
748+
sql
749+
end
750+
744751
def translate_exception(exception, message:, sql:, binds:)
745752
# override in derived class
746753
case exception

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,26 @@ def explain(arel, binds = [])
3737
MySQL::ExplainPrettyPrinter.new.pp(result, elapsed)
3838
end
3939

40+
def execute(sql, name = nil, async: false)
41+
# make sure we carry over any changes to ActiveRecord.default_timezone that have been
42+
# made since we established the connection
43+
@connection.query_options[:database_timezone] = ActiveRecord.default_timezone
44+
45+
super
46+
end
47+
alias_method :raw_execute, :execute
48+
private :raw_execute
49+
4050
# Executes the SQL statement in the context of this connection.
4151
def execute(sql, name = nil, async: false)
52+
sql = transform_query(sql)
4253
check_if_write_query(sql)
4354

4455
# make sure we carry over any changes to ActiveRecord.default_timezone that have been
4556
# made since we established the connection
4657
@connection.query_options[:database_timezone] = ActiveRecord.default_timezone
4758

48-
super
59+
raw_execute(sql, name, async: async)
4960
end
5061

5162
def exec_query(sql, name = "SQL", binds = [], prepare: false, async: false) # :nodoc:
@@ -81,8 +92,9 @@ def exec_delete(sql, name = nil, binds = []) # :nodoc:
8192

8293
private
8394
def execute_batch(statements, name = nil)
95+
statements = statements.map { |sql| transform_query(sql) }
8496
combine_multi_statements(statements).each do |statement|
85-
execute(statement, name)
97+
raw_execute(statement, name)
8698
end
8799
@connection.abandon_results!
88100
end
@@ -147,6 +159,7 @@ def max_allowed_packet
147159
end
148160

149161
def exec_stmt_and_free(sql, name, binds, cache_stmt: false, async: false)
162+
sql = transform_query(sql)
150163
check_if_write_query(sql)
151164

152165
materialize_transactions

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def write_query?(sql) # :nodoc:
3535
# Note: the PG::Result object is manually memory managed; if you don't
3636
# need it specifically, you may want consider the <tt>exec_query</tt> wrapper.
3737
def execute(sql, name = nil)
38+
sql = transform_query(sql)
3839
check_if_write_query(sql)
3940

4041
materialize_transactions

activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,7 @@ def load_types_queries(initializer, oids)
696696
FEATURE_NOT_SUPPORTED = "0A000" # :nodoc:
697697

698698
def execute_and_clear(sql, name, binds, prepare: false, async: false)
699+
sql = transform_query(sql)
699700
check_if_write_query(sql)
700701

701702
if !prepare || without_prepared_statement?(binds)

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ def explain(arel, binds = [])
1919
end
2020

2121
def execute(sql, name = nil) # :nodoc:
22+
sql = transform_query(sql)
2223
check_if_write_query(sql)
2324

2425
materialize_transactions
@@ -32,6 +33,7 @@ def execute(sql, name = nil) # :nodoc:
3233
end
3334

3435
def exec_query(sql, name = nil, binds = [], prepare: false, async: false) # :nodoc:
36+
sql = transform_query(sql)
3537
check_if_write_query(sql)
3638

3739
materialize_transactions
@@ -104,6 +106,7 @@ def reset_read_uncommitted
104106
end
105107

106108
def execute_batch(statements, name = nil)
109+
statements = statements.map { |sql| transform_query(sql) }
107110
sql = combine_multi_statements(statements)
108111

109112
check_if_write_query(sql)

activerecord/lib/active_record/query_logs.rb

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,14 @@ def with_tag(tag, &block)
118118
inline_tags.pop
119119
end
120120

121-
def add_query_log_tags_to_sql(sql) # :nodoc:
122-
comments.each do |comment|
123-
unless sql.include?(comment)
124-
sql = prepend_comment ? "#{comment} #{sql}" : "#{sql} #{comment}"
125-
end
121+
def call(sql) # :nodoc:
122+
parts = self.comments
123+
if prepend_comment
124+
parts << sql
125+
else
126+
parts.unshift(sql)
126127
end
127-
sql
128+
parts.join(" ")
128129
end
129130

130131
private
@@ -198,15 +199,5 @@ def inline_tag_content
198199
inline_tags.join
199200
end
200201
end
201-
202-
module ExecutionMethods
203-
def execute(sql, *args, **kwargs)
204-
super(ActiveRecord::QueryLogs.add_query_log_tags_to_sql(sql), *args, **kwargs)
205-
end
206-
207-
def exec_query(sql, *args, **kwargs)
208-
super(ActiveRecord::QueryLogs.add_query_log_tags_to_sql(sql), *args, **kwargs)
209-
end
210-
end
211202
end
212203
end

activerecord/lib/active_record/railtie.rb

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ class Railtie < Rails::Railtie # :nodoc:
360360
initializer "active_record.query_log_tags_config" do |app|
361361
config.after_initialize do
362362
if app.config.active_record.query_log_tags_enabled
363+
ActiveRecord.query_transformers << ActiveRecord::QueryLogs
363364
ActiveRecord::QueryLogs.taggings.merge!(
364365
application: Rails.application.class.name.split("::").first,
365366
pid: -> { Process.pid },
@@ -375,12 +376,6 @@ class Railtie < Rails::Railtie # :nodoc:
375376
if app.config.active_record.cache_query_log_tags
376377
ActiveRecord::QueryLogs.cache_query_log_tags = true
377378
end
378-
379-
ActiveSupport.on_load(:active_record) do
380-
ConnectionAdapters::AbstractAdapter.descendants.each do |klass|
381-
klass.prepend(QueryLogs::ExecutionMethods) if klass.descendants.empty?
382-
end
383-
end
384379
end
385380
end
386381
end

activerecord/test/cases/query_logs_test.rb

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,16 @@ class QueryLogsTest < ActiveRecord::TestCase
1212

1313
def setup
1414
# Enable the query tags logging
15-
ActiveRecord::Base.connection.class_eval do
16-
prepend(ActiveRecord::QueryLogs::ExecutionMethods)
17-
end
15+
@original_transformers = ActiveRecord.query_transformers
1816
@original_prepend = ActiveRecord::QueryLogs.prepend_comment
17+
ActiveRecord.query_transformers += [ActiveRecord::QueryLogs]
1918
ActiveRecord::QueryLogs.prepend_comment = false
2019
ActiveRecord::QueryLogs.cache_query_log_tags = false
2120
ActiveRecord::QueryLogs.cached_comment = nil
2221
end
2322

2423
def teardown
24+
ActiveRecord.query_transformers = @original_transformers
2525
ActiveRecord::QueryLogs.prepend_comment = @original_prepend
2626
ActiveRecord::QueryLogs.tags = []
2727
end
@@ -100,10 +100,10 @@ def test_retrieves_comment_from_cache_when_enabled_and_set
100100
ActiveRecord::QueryLogs.cache_query_log_tags = true
101101
ActiveRecord::QueryLogs.tags = [ :application ]
102102

103-
assert_equal " /*application:active_record*/", ActiveRecord::QueryLogs.add_query_log_tags_to_sql("")
103+
assert_equal " /*application:active_record*/", ActiveRecord::QueryLogs.call("")
104104

105105
ActiveRecord::QueryLogs.stub(:cached_comment, "/*cached_comment*/") do
106-
assert_equal " /*cached_comment*/", ActiveRecord::QueryLogs.add_query_log_tags_to_sql("")
106+
assert_equal " /*cached_comment*/", ActiveRecord::QueryLogs.call("")
107107
end
108108
ensure
109109
ActiveRecord::QueryLogs.cached_comment = nil
@@ -115,12 +115,12 @@ def test_resets_cache_on_context_update
115115
ActiveRecord::QueryLogs.update_context(temporary: "value")
116116
ActiveRecord::QueryLogs.tags = [ temporary_tag: ->(context) { context[:temporary] } ]
117117

118-
assert_equal " /*temporary_tag:value*/", ActiveRecord::QueryLogs.add_query_log_tags_to_sql("")
118+
assert_equal " /*temporary_tag:value*/", ActiveRecord::QueryLogs.call("")
119119

120120
ActiveRecord::QueryLogs.update_context(temporary: "new_value")
121121

122122
assert_nil ActiveRecord::QueryLogs.cached_comment
123-
assert_equal " /*temporary_tag:new_value*/", ActiveRecord::QueryLogs.add_query_log_tags_to_sql("")
123+
assert_equal " /*temporary_tag:new_value*/", ActiveRecord::QueryLogs.call("")
124124
ensure
125125
ActiveRecord::QueryLogs.cached_comment = nil
126126
ActiveRecord::QueryLogs.cache_query_log_tags = false
@@ -201,15 +201,6 @@ def test_bad_inline_tags
201201
end
202202
end
203203

204-
def test_inline_tags_are_deduped
205-
ActiveRecord::QueryLogs.tags = [ :application ]
206-
assert_sql(%r{select id from posts /\*foo\*/ /\*application:active_record\*/$}) do
207-
ActiveRecord::QueryLogs.with_tag("foo") do
208-
ActiveRecord::Base.connection.execute "select id from posts /*foo*/"
209-
end
210-
end
211-
end
212-
213204
def test_empty_comments_are_not_added
214205
original_tags = ActiveRecord::QueryLogs.tags
215206
ActiveRecord::QueryLogs.tags = [ empty: -> { nil } ]

railties/test/application/query_logs_test.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class User < ActiveRecord::Base
1818
app_file "app/controllers/users_controller.rb", <<-RUBY
1919
class UsersController < ApplicationController
2020
def index
21-
render inline: ActiveRecord::QueryLogs.add_query_log_tags_to_sql("")
21+
render inline: ActiveRecord::QueryLogs.call("")
2222
end
2323
2424
def dynamic_content
@@ -30,7 +30,7 @@ def dynamic_content
3030
app_file "app/jobs/user_job.rb", <<-RUBY
3131
class UserJob < ActiveJob::Base
3232
def perform
33-
ActiveRecord::QueryLogs.add_query_log_tags_to_sql("")
33+
ActiveRecord::QueryLogs.call("")
3434
end
3535
3636
def dynamic_content
@@ -57,15 +57,15 @@ def app
5757
test "does not modify the query execution path by default" do
5858
boot_app
5959

60-
assert_equal ActiveRecord::Base.connection.method(:execute).owner, ActiveRecord::ConnectionAdapters::SQLite3::DatabaseStatements
60+
assert_not_includes ActiveRecord.query_transformers, ActiveRecord::QueryLogs
6161
end
6262

6363
test "prepends the query execution path when enabled" do
6464
add_to_config "config.active_record.query_log_tags_enabled = true"
6565

6666
boot_app
6767

68-
assert_equal ActiveRecord::Base.connection.method(:execute).owner, ActiveRecord::QueryLogs::ExecutionMethods
68+
assert_includes ActiveRecord.query_transformers, ActiveRecord::QueryLogs
6969
end
7070

7171
test "controller and job tags are defined by default" do

0 commit comments

Comments
 (0)