Skip to content

Commit b291408

Browse files
authored
Merge pull request rails#52392 from Shopify/perf-query-logs
Optimize ActiveRecord::QueryLogs
2 parents 2fdf772 + 6fb612b commit b291408

File tree

3 files changed

+124
-74
lines changed

3 files changed

+124
-74
lines changed

activerecord/lib/active_record/query_logs.rb

Lines changed: 98 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,70 @@ module ActiveRecord
7272
#
7373
# config.active_record.cache_query_log_tags = true
7474
module QueryLogs
75-
mattr_accessor :taggings, instance_accessor: false, default: {}
76-
mattr_accessor :tags, instance_accessor: false, default: [ :application ]
77-
mattr_accessor :prepend_comment, instance_accessor: false, default: false
78-
mattr_accessor :cache_query_log_tags, instance_accessor: false, default: false
79-
mattr_accessor :tags_formatter, instance_accessor: false
75+
class GetKeyHandler # :nodoc:
76+
def initialize(name)
77+
@name = name
78+
end
79+
80+
def call(context)
81+
context[@name]
82+
end
83+
end
84+
85+
class IdentityHandler # :nodoc:
86+
def initialize(value)
87+
@value = value
88+
end
89+
90+
def call(_context)
91+
@value
92+
end
93+
end
94+
95+
class ZeroArityHandler # :nodoc:
96+
def initialize(proc)
97+
@proc = proc
98+
end
99+
100+
def call(_context)
101+
@proc.call
102+
end
103+
end
104+
105+
@taggings = {}
106+
@tags = [ :application ]
107+
@prepend_comment = false
108+
@cache_query_log_tags = false
109+
@tags_formatter = false
110+
80111
thread_mattr_accessor :cached_comment, instance_accessor: false
81112

82113
class << self
114+
attr_reader :tags, :taggings, :tags_formatter # :nodoc:
115+
attr_accessor :prepend_comment, :cache_query_log_tags # :nodoc:
116+
117+
def taggings=(taggings) # :nodoc:
118+
@taggings = taggings
119+
@handlers = rebuild_handlers
120+
end
121+
122+
def tags=(tags) # :nodoc:
123+
@tags = tags
124+
@handlers = rebuild_handlers
125+
end
126+
127+
def tags_formatter=(format) # :nodoc:
128+
@tags_formatter = format
129+
@formatter = case format
130+
when :legacy
131+
LegacyFormatter
132+
when :sqlcommenter
133+
SQLCommenter
134+
else
135+
raise ArgumentError, "Formatter is unsupported: #{format}"
136+
end
137+
end
138+
83139
def call(sql, connection) # :nodoc:
84140
comment = self.comment(connection)
85141

@@ -96,19 +152,6 @@ def clear_cache # :nodoc:
96152
self.cached_comment = nil
97153
end
98154

99-
# Updates the formatter to be what the passed in format is.
100-
def update_formatter(format)
101-
self.tags_formatter =
102-
case format
103-
when :legacy
104-
LegacyFormatter.new
105-
when :sqlcommenter
106-
SQLCommenter.new
107-
else
108-
raise ArgumentError, "Formatter is unsupported: #{formatter}"
109-
end
110-
end
111-
112155
if Thread.respond_to?(:each_caller_location)
113156
def query_source_location # :nodoc:
114157
Thread.each_caller_location do |location|
@@ -126,6 +169,36 @@ def query_source_location # :nodoc:
126169
ActiveSupport::ExecutionContext.after_change { ActiveRecord::QueryLogs.clear_cache }
127170

128171
private
172+
def rebuild_handlers
173+
handlers = []
174+
@tags.each do |i|
175+
if i.is_a?(Hash)
176+
i.each do |k, v|
177+
handlers << [k, build_handler(k, v)]
178+
end
179+
else
180+
handlers << [i, build_handler(i)]
181+
end
182+
end
183+
handlers.sort_by! { |(key, _)| key.to_s }
184+
handlers
185+
end
186+
187+
def build_handler(name, handler = nil)
188+
handler ||= @taggings[name]
189+
if handler.nil?
190+
GetKeyHandler.new(name)
191+
elsif handler.respond_to?(:call)
192+
if handler.arity == 0
193+
ZeroArityHandler.new(handler)
194+
else
195+
handler
196+
end
197+
else
198+
IdentityHandler.new(handler)
199+
end
200+
end
201+
129202
# Returns an SQL comment +String+ containing the query log tags.
130203
# Sets and returns a cached comment if <tt>cache_query_log_tags</tt> is +true+.
131204
def comment(connection)
@@ -136,10 +209,6 @@ def comment(connection)
136209
end
137210
end
138211

139-
def formatter
140-
self.tags_formatter || self.update_formatter(:legacy)
141-
end
142-
143212
def uncached_comment(connection)
144213
content = tag_content(connection)
145214

@@ -165,25 +234,15 @@ def tag_content(connection)
165234
context = ActiveSupport::ExecutionContext.to_h
166235
context[:connection] ||= connection
167236

168-
pairs = tags.flat_map { |i| [*i] }.filter_map do |tag|
169-
key, handler = tag
170-
handler ||= taggings[key]
171-
172-
val = if handler.nil?
173-
context[key]
174-
elsif handler.respond_to?(:call)
175-
if handler.arity == 0
176-
handler.call
177-
else
178-
handler.call(context)
179-
end
180-
else
181-
handler
182-
end
183-
[key, val] unless val.nil?
237+
pairs = @handlers.filter_map do |(key, handler)|
238+
val = handler.call(context)
239+
@formatter.format(key, val) unless val.nil?
184240
end
185-
self.formatter.format(pairs)
241+
@formatter.join(pairs)
186242
end
187243
end
244+
245+
@handlers = rebuild_handlers
246+
self.tags_formatter = :legacy
188247
end
189248
end

activerecord/lib/active_record/query_logs_formatter.rb

Lines changed: 17 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,29 @@
22

33
module ActiveRecord
44
module QueryLogs
5-
class LegacyFormatter # :nodoc:
6-
def initialize
7-
@key_value_separator = ":"
8-
end
9-
10-
# Formats the key value pairs into a string.
11-
def format(pairs)
12-
pairs.map! do |key, value|
13-
"#{key}#{key_value_separator}#{format_value(value)}"
14-
end.join(",")
15-
end
16-
17-
private
18-
attr_reader :key_value_separator
19-
20-
def format_value(value)
21-
value
5+
module LegacyFormatter # :nodoc:
6+
class << self
7+
# Formats the key value pairs into a string.
8+
def format(key, value)
9+
"#{key}:#{value}"
2210
end
23-
end
2411

25-
class SQLCommenter < LegacyFormatter # :nodoc:
26-
def initialize
27-
@key_value_separator = "="
12+
def join(pairs)
13+
pairs.join(",")
14+
end
2815
end
16+
end
2917

30-
def format(pairs)
31-
pairs.sort_by! { |pair| pair.first.to_s }
32-
super
33-
end
18+
class SQLCommenter # :nodoc:
19+
class << self
20+
def format(key, value)
21+
"#{key}='#{ERB::Util.url_encode(value)}'"
22+
end
3423

35-
private
36-
def format_value(value)
37-
"'#{ERB::Util.url_encode(value)}'"
24+
def join(pairs)
25+
pairs.join(",")
3826
end
27+
end
3928
end
4029
end
4130
end

activerecord/test/cases/query_logs_test.rb

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def teardown
3333
ActiveRecord::QueryLogs.prepend_comment = false
3434
ActiveRecord::QueryLogs.cache_query_log_tags = false
3535
ActiveRecord::QueryLogs.clear_cache
36-
ActiveRecord::QueryLogs.update_formatter(:legacy)
36+
ActiveRecord::QueryLogs.tags_formatter = :legacy
3737

3838
# ActiveSupport::ExecutionContext context is automatically reset in Rails app via an executor hooks set in railtie
3939
# But not in Active Record's own test suite.
@@ -45,7 +45,8 @@ def test_escaping_good_comment
4545
end
4646

4747
def test_escaping_good_comment_with_custom_separator
48-
ActiveRecord::QueryLogs.update_formatter(:sqlcommenter)
48+
ActiveRecord::QueryLogs.tags_formatter = :sqlcommenter
49+
4950
assert_equal "app='foo'", ActiveRecord::QueryLogs.send(:escape_sql_comment, "app='foo'")
5051
end
5152

@@ -183,7 +184,8 @@ def test_empty_comments_are_not_added
183184
end
184185

185186
def test_sql_commenter_format
186-
ActiveRecord::QueryLogs.update_formatter(:sqlcommenter)
187+
ActiveRecord::QueryLogs.tags_formatter = :sqlcommenter
188+
187189
assert_queries_match(%r{/\*application='active_record'\*/}) do
188190
Dashboard.first
189191
end
@@ -211,13 +213,13 @@ def test_multiple_custom_tags
211213
{ custom_proc: -> { "test content" }, another_proc: -> { "more test content" } },
212214
]
213215

214-
assert_queries_match(%r{/\*application:active_record,custom_proc:test content,another_proc:more test content\*/}) do
216+
assert_queries_match(%r{/\*another_proc:more test content,application:active_record,custom_proc:test content\*/}) do
215217
Dashboard.first
216218
end
217219
end
218220

219221
def test_sqlcommenter_format_value
220-
ActiveRecord::QueryLogs.update_formatter(:sqlcommenter)
222+
ActiveRecord::QueryLogs.tags_formatter = :sqlcommenter
221223

222224
ActiveRecord::QueryLogs.tags = [
223225
:application,
@@ -230,7 +232,7 @@ def test_sqlcommenter_format_value
230232
end
231233

232234
def test_sqlcommenter_format_allows_string_keys
233-
ActiveRecord::QueryLogs.update_formatter(:sqlcommenter)
235+
ActiveRecord::QueryLogs.tags_formatter = :sqlcommenter
234236

235237
ActiveRecord::QueryLogs.tags = [
236238
:application,
@@ -247,7 +249,7 @@ def test_sqlcommenter_format_allows_string_keys
247249
end
248250

249251
def test_sqlcommenter_format_value_string_coercible
250-
ActiveRecord::QueryLogs.update_formatter(:sqlcommenter)
252+
ActiveRecord::QueryLogs.tags_formatter = :sqlcommenter
251253

252254
ActiveRecord::QueryLogs.tags = [
253255
:application,

0 commit comments

Comments
 (0)