Module: QueryGuard::Fingerprint
- Defined in:
- lib/query_guard/fingerprint.rb
Overview
SQL fingerprinting and statistics module. Normalizes SQL queries and tracks per-fingerprint statistics.
Class Method Summary collapse
-
.generate(sql) ⇒ Object
Generate a stable fingerprint for a SQL query by: - Normalizing whitespace - Replacing string literals with ? - Replacing numeric literals with ? - Replacing IN (...) lists with IN (?) Returns a SHA1 hash of the normalized query.
-
.normalize(sql) ⇒ Object
Normalize SQL by removing all literals and standardizing whitespace.
-
.record(sql, duration_ms) ⇒ Object
Record a query execution.
-
.reset! ⇒ Object
Reset all stats (useful for testing).
-
.stats ⇒ Object
Get or initialize the stats store for the current process.
-
.stats_for(fingerprint) ⇒ Object
Get stats for a specific fingerprint.
-
.top_by_avg_duration(limit = 10) ⇒ Object
Get all fingerprints sorted by average duration.
-
.top_by_count(limit = 10) ⇒ Object
Get all fingerprints sorted by frequency.
-
.top_by_duration(limit = 10) ⇒ Object
Get all fingerprints sorted by total duration.
Class Method Details
.generate(sql) ⇒ Object
Generate a stable fingerprint for a SQL query by:
- Normalizing whitespace
- Replacing string literals with ?
- Replacing numeric literals with ?
- Replacing IN (...) lists with IN (?) Returns a SHA1 hash of the normalized query
15 16 17 18 |
# File 'lib/query_guard/fingerprint.rb', line 15 def generate(sql) normalized = normalize(sql) Digest::SHA1.hexdigest(normalized) end |
.normalize(sql) ⇒ Object
Normalize SQL by removing all literals and standardizing whitespace
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
# File 'lib/query_guard/fingerprint.rb', line 21 def normalize(sql) s = sql.to_s.dup # Replace string literals (both single and double quotes) s.gsub!(/'(?:''|[^'])*'/, "?") s.gsub!(/"(?:""|[^"])*"/, "?") # Replace numeric literals (integers and floats) s.gsub!(/\b\d+\.?\d*\b/, "?") # Replace IN (...) with IN (?) s.gsub!(/IN\s*\([^)]+\)/i, "IN (?)") # Normalize whitespace s.gsub!(/\s+/, " ") s.strip.downcase end |
.record(sql, duration_ms) ⇒ Object
Record a query execution
55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/query_guard/fingerprint.rb', line 55 def record(sql, duration_ms) fp = generate(sql) stat = stats[fp] stat[:count] += 1 stat[:total_duration_ms] += duration_ms stat[:min_duration_ms] = [stat[:min_duration_ms], duration_ms].min stat[:max_duration_ms] = [stat[:max_duration_ms], duration_ms].max stat[:last_seen_at] = Time.now fp end |
.reset! ⇒ Object
Reset all stats (useful for testing)
91 92 93 |
# File 'lib/query_guard/fingerprint.rb', line 91 def reset! @stats = nil end |
.stats ⇒ Object
Get or initialize the stats store for the current process
41 42 43 44 45 46 47 48 49 50 51 52 |
# File 'lib/query_guard/fingerprint.rb', line 41 def stats @stats ||= Hash.new do |h, fp| h[fp] = { count: 0, total_duration_ms: 0.0, min_duration_ms: Float::INFINITY, max_duration_ms: 0.0, first_seen_at: Time.now, last_seen_at: Time.now } end end |
.stats_for(fingerprint) ⇒ Object
Get stats for a specific fingerprint
69 70 71 |
# File 'lib/query_guard/fingerprint.rb', line 69 def stats_for(fingerprint) stats[fingerprint] end |
.top_by_avg_duration(limit = 10) ⇒ Object
Get all fingerprints sorted by average duration
84 85 86 87 88 |
# File 'lib/query_guard/fingerprint.rb', line 84 def top_by_avg_duration(limit = 10) stats.sort_by { |_fp, s| s[:count] > 0 ? -(s[:total_duration_ms] / s[:count]) : 0 }.first(limit).to_h end |
.top_by_count(limit = 10) ⇒ Object
Get all fingerprints sorted by frequency
74 75 76 |
# File 'lib/query_guard/fingerprint.rb', line 74 def top_by_count(limit = 10) stats.sort_by { |_fp, s| -s[:count] }.first(limit).to_h end |
.top_by_duration(limit = 10) ⇒ Object
Get all fingerprints sorted by total duration
79 80 81 |
# File 'lib/query_guard/fingerprint.rb', line 79 def top_by_duration(limit = 10) stats.sort_by { |_fp, s| -s[:total_duration_ms] }.first(limit).to_h end |