1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
|
# frozen_string_literal: true
module Gitlab
module Database
# The purpose of this class is to implement a various query analyzers based on `pg_query`
# And process them all via `Gitlab::Database::QueryAnalyzers::*`
#
# Sometimes this might cause errors in specs.
# This is best to be disable with `describe '...', query_analyzers: false do`
class QueryAnalyzer
include ::Singleton
Parsed = Struct.new(
:sql, :connection, :pg
)
attr_reader :all_analyzers
def initialize
@all_analyzers = []
end
def hook!
@subscriber = ActiveSupport::Notifications.subscribe('sql.active_record') do |event|
# In some cases analyzer code might trigger another SQL call
# to avoid stack too deep this detects recursive call of subscriber
with_ignored_recursive_calls do
process_sql(event.payload[:sql], event.payload[:connection])
end
end
end
def within(user_analyzers = nil)
# Due to singleton nature of analyzers
# only an outer invocation of the `.within`
# is allowed to initialize them
if already_within?
raise 'Query analyzers are already defined, cannot re-define them.' if user_analyzers
return yield
end
begin!(user_analyzers || all_analyzers)
begin
yield
ensure
end!
end
end
def already_within?
# If analyzers are set they are already configured
!enabled_analyzers.nil?
end
def process_sql(sql, connection)
analyzers = enabled_analyzers
return unless analyzers&.any?
parsed = parse(sql, connection)
return unless parsed
analyzers.each do |analyzer|
next if analyzer.suppressed? && !analyzer.requires_tracking?(parsed)
analyzer.analyze(parsed)
rescue StandardError, ::Gitlab::Database::QueryAnalyzers::Base::QueryAnalyzerError => e
# We catch all standard errors to prevent validation errors to introduce fatal errors in production
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
end
end
# Enable query analyzers
def begin!(analyzers = all_analyzers)
analyzers = analyzers.select do |analyzer|
if analyzer.enabled?
analyzer.begin!
true
end
rescue StandardError, ::Gitlab::Database::QueryAnalyzers::Base::QueryAnalyzerError => e
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
false
end
Thread.current[:query_analyzer_enabled_analyzers] = analyzers
end
# Disable enabled query analyzers
def end!
enabled_analyzers.select do |analyzer|
analyzer.end!
rescue StandardError, ::Gitlab::Database::QueryAnalyzers::Base::QueryAnalyzerError => e
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
end
Thread.current[:query_analyzer_enabled_analyzers] = nil
end
private
def enabled_analyzers
Thread.current[:query_analyzer_enabled_analyzers]
end
def parse(sql, connection)
parsed = PgQuery.parse(sql)
return unless parsed
normalized = PgQuery.normalize(sql)
Parsed.new(normalized, connection, parsed)
rescue PgQuery::ParseError => e
# Ignore PgQuery parse errors (due to depth limit or other reasons)
Gitlab::ErrorTracking.track_exception(e)
nil
end
def with_ignored_recursive_calls
return if Thread.current[:query_analyzer_recursive]
begin
Thread.current[:query_analyzer_recursive] = true
yield
ensure
Thread.current[:query_analyzer_recursive] = nil
end
end
end
end
end
|