Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions lib/tasks/db_benchmark.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# frozen_string_literal: true

# rubocop:disable Style/FormatStringToken
namespace :db do
desc 'Capture performance baseline for key database operations'
task benchmark: :environment do
require 'benchmark'
require 'json'

results = {}
component = Component.joins(:rules).where('rules_count > 0').first
project = Project.joins(:components).first

abort 'No component with rules found. Seed the database first.' unless component
abort 'No project with components found. Seed the database first.' unless project

puts 'Benchmarking against:'
puts " Component: #{component.name} (#{component.rules_count} rules)"
puts " Project: #{project.name} (#{project.components.count} components)"
puts

iterations = 10

# 1. Component show (full rule load with associations)
puts '1. Component show (full rule load)...'
results['component_show_ms'] = (Benchmark.measure do
iterations.times do
component.reload
component.rules.includes(
:reviews, :disa_rule_descriptions, :checks,
:satisfies, :satisfied_by,
srg_rule: %i[disa_rule_descriptions checks]
).to_a
end
end.real / iterations * 1000).round(1)

# 2. Paginated comments (triage table)
puts '2. Paginated comments...'
results['paginated_comments_ms'] = (Benchmark.measure do
iterations.times do
component.paginated_comments(triage_status: 'all', per_page: 25)
end
end.real / iterations * 1000).round(1)

# 3. Pending comment counts (project index)
puts '3. Pending comment counts...'
component_ids = project.component_ids
results['pending_counts_ms'] = (Benchmark.measure do
iterations.times { Component.pending_comment_counts(component_ids) }
end.real / iterations * 1000).round(1)

# 4. SQL query count for component show
puts '4. Counting queries for component show...'
query_count = 0
counter = lambda do |_name, _start, _finish, _id, payload|
sql = payload[:sql].to_s
next if payload[:name] == 'SCHEMA'
next if sql.match?(/\A\s*(BEGIN|COMMIT|ROLLBACK|SAVEPOINT|RELEASE)/i)

query_count += 1
end
ActiveSupport::Notifications.subscribed(counter, 'sql.active_record') do
component.reload
component.rules.includes(
:reviews, :disa_rule_descriptions, :checks
).to_a
end
results['component_show_queries'] = query_count

# 5. SQL query count for paginated_comments
puts '5. Counting queries for paginated_comments...'
query_count = 0
ActiveSupport::Notifications.subscribed(counter, 'sql.active_record') do
component.paginated_comments(triage_status: 'all', per_page: 25)
end
results['paginated_comments_queries'] = query_count

# 6. Component duplicate (write performance)
puts '6. Component duplicate...'
if component.rules.exists?(locked: false)
component.rules.update_all(locked: true) # rubocop:disable Rails/SkipsModelValidations
component.update_column(:released, true) # rubocop:disable Rails/SkipsModelValidations
end
results['component_duplicate_ms'] = (Benchmark.measure do
dup = component.duplicate(
new_name: 'Benchmark Dup', new_version: 99,
new_release: 99, new_title: 'Benchmark', new_description: 'perf test'
)
dup&.destroy
end.real * 1000).round(1)
Comment on lines +80 to +90

# Print results
puts
puts '=' * 60
puts 'PERFORMANCE BASELINE'
puts '=' * 60
results.each do |key, value|
unit = key.end_with?('_queries') ? 'queries' : 'ms'
puts format(' %-35s %8s %s', key, value, unit)
end
puts '=' * 60

# Save to file
output_dir = Rails.root.join('tmp')
FileUtils.mkdir_p(output_dir)
output_path = output_dir.join("db_benchmark_#{Time.zone.now.strftime('%Y%m%d_%H%M%S')}.json")
File.write(output_path, JSON.pretty_generate(
captured_at: Time.now.iso8601,
component: { id: component.id, name: component.name, rules_count: component.rules_count },
project: { id: project.id, name: project.name },
iterations: iterations,
results: results
))
puts "\nSaved to #{output_path}"

# Compare with previous baseline if exists
previous = Dir.glob(output_dir.join('db_benchmark_*.json')).reverse[1]
if previous
prev_data = JSON.parse(File.read(previous))
puts "\nComparison with #{File.basename(previous)}:"
prev_data['results'].each do |key, prev_val|
next unless results[key]

curr = results[key].to_f
prev = prev_val.to_f
next if prev.zero?

pct = ((curr - prev) / prev * 100).round(1)
indicator = if pct > 10
"⚠️ +#{pct}% REGRESSION"
elsif pct < -10
"✅ #{pct}% improvement"
else
"→ #{pct}% (within threshold)"
end
puts format(' %-35s %s', key, indicator)
end
end
end
end
# rubocop:enable Style/FormatStringToken
232 changes: 232 additions & 0 deletions lib/tasks/db_validate.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
# frozen_string_literal: true

# rubocop:disable Style/FormatStringToken
namespace :db do
desc 'Validate data integrity after migration phase'
task validate: :environment do
errors = []
conn = ActiveRecord::Base.connection

puts '=' * 60
puts 'DATABASE INTEGRITY VALIDATION'
puts '=' * 60

# 1. Row counts
puts "\n--- Row Counts ---"
%w[
users projects components security_requirements_guides stigs
base_rules reviews reactions rule_satisfactions memberships
audits additional_questions additional_answers
project_access_requests
].each do |table|
next unless conn.table_exists?(table)

count = conn.execute("SELECT COUNT(*) AS c FROM #{conn.quote_table_name(table)}").first['c']
puts format(' %-40s %8d', table, count)
end

# 2. Orphaned FK records
puts "\n--- Orphaned Record Checks ---"
orphan_checks = {
'base_rules → components' => <<~SQL.squish,
SELECT COUNT(*) AS c FROM base_rules r
LEFT JOIN components c ON r.component_id = c.id
WHERE c.id IS NULL AND r.type = 'Rule' AND r.deleted_at IS NULL
SQL
'base_rules → security_requirements_guides' => <<~SQL.squish,
SELECT COUNT(*) AS c FROM base_rules r
LEFT JOIN security_requirements_guides s ON r.security_requirements_guide_id = s.id
WHERE s.id IS NULL AND r.type = 'SrgRule'
SQL
'reviews → users' => <<~SQL.squish,
SELECT COUNT(*) AS c FROM reviews r
LEFT JOIN users u ON r.user_id = u.id
WHERE r.user_id IS NOT NULL AND u.id IS NULL
SQL
'reviews → base_rules (rule_id)' => <<~SQL.squish,
SELECT COUNT(*) AS c FROM reviews r
LEFT JOIN base_rules br ON r.rule_id = br.id
WHERE r.rule_id IS NOT NULL AND br.id IS NULL
SQL
'reviews → responding_to (self-ref)' => <<~SQL.squish,
SELECT COUNT(*) AS c FROM reviews r
LEFT JOIN reviews parent ON r.responding_to_review_id = parent.id
WHERE r.responding_to_review_id IS NOT NULL AND parent.id IS NULL
SQL
'reviews → duplicate_of (self-ref)' => <<~SQL.squish,
SELECT COUNT(*) AS c FROM reviews r
LEFT JOIN reviews target ON r.duplicate_of_review_id = target.id
WHERE r.duplicate_of_review_id IS NOT NULL AND target.id IS NULL
SQL
'reactions → reviews' => <<~SQL.squish,
SELECT COUNT(*) AS c FROM reactions r
LEFT JOIN reviews rv ON r.review_id = rv.id
WHERE rv.id IS NULL
SQL
'reactions → users' => <<~SQL.squish,
SELECT COUNT(*) AS c FROM reactions r
LEFT JOIN users u ON r.user_id = u.id
WHERE u.id IS NULL
SQL
'memberships → users' => <<~SQL.squish,
SELECT COUNT(*) AS c FROM memberships m
LEFT JOIN users u ON m.user_id = u.id
WHERE u.id IS NULL
SQL
'rule_satisfactions → base_rules (rule_id)' => <<~SQL.squish,
SELECT COUNT(*) AS c FROM rule_satisfactions rs
LEFT JOIN base_rules r ON rs.rule_id = r.id
WHERE r.id IS NULL
SQL
'rule_satisfactions → base_rules (satisfied_by)' => <<~SQL.squish,
SELECT COUNT(*) AS c FROM rule_satisfactions rs
LEFT JOIN base_rules r ON rs.satisfied_by_rule_id = r.id
WHERE r.id IS NULL
SQL
'components → projects' => <<~SQL.squish,
SELECT COUNT(*) AS c FROM components c
LEFT JOIN projects p ON c.project_id = p.id
WHERE p.id IS NULL
SQL
'components → security_requirements_guides' => <<~SQL.squish
SELECT COUNT(*) AS c FROM components c
LEFT JOIN security_requirements_guides s ON c.security_requirements_guide_id = s.id
WHERE s.id IS NULL
SQL
}

orphan_checks.each do |label, sql|
count = conn.execute(sql).first['c'].to_i
status = count.zero? ? '✓' : "✗ #{count} orphaned"
errors << "Orphaned: #{label} (#{count})" unless count.zero?
puts format(' %-50s %s', label, status)
end

# 3. Unexpected NULLs in required fields
puts "\n--- NULL Checks (required fields) ---"
null_checks = {
'base_rules.type (STI)' =>
'SELECT COUNT(*) AS c FROM base_rules WHERE type IS NULL',
'base_rules.component_id (Rules)' =>
"SELECT COUNT(*) AS c FROM base_rules WHERE type = 'Rule' AND component_id IS NULL AND deleted_at IS NULL",
'reviews.action' =>
'SELECT COUNT(*) AS c FROM reviews WHERE action IS NULL',
'components.project_id' =>
'SELECT COUNT(*) AS c FROM components WHERE project_id IS NULL',
'components.prefix' =>
"SELECT COUNT(*) AS c FROM components WHERE prefix IS NULL OR prefix = ''",
'memberships.user_id' =>
'SELECT COUNT(*) AS c FROM memberships WHERE user_id IS NULL',
'memberships.role' =>
"SELECT COUNT(*) AS c FROM memberships WHERE role IS NULL OR role = ''"
}

null_checks.each do |field, sql|
count = conn.execute(sql).first['c'].to_i
status = count.zero? ? '✓' : "✗ #{count} unexpected NULLs"
errors << "NULLs: #{field} (#{count})" unless count.zero?
puts format(' %-50s %s', field, status)
end

# 4. Duplicate detection
puts "\n--- Duplicate Checks ---"
dup_checks = {
'rule_satisfactions (rule_id, satisfied_by_rule_id)' => <<~SQL.squish,
SELECT COUNT(*) AS c FROM (
SELECT rule_id, satisfied_by_rule_id
FROM rule_satisfactions
GROUP BY rule_id, satisfied_by_rule_id
HAVING COUNT(*) > 1
) dupes
SQL
'reactions (review_id, user_id) uniqueness' => <<~SQL.squish,
SELECT COUNT(*) AS c FROM (
SELECT review_id, user_id
FROM reactions
GROUP BY review_id, user_id
HAVING COUNT(*) > 1
) dupes
SQL
'memberships (user_id, membership_type, membership_id)' => <<~SQL.squish
SELECT COUNT(*) AS c FROM (
SELECT user_id, membership_type, membership_id
FROM memberships
GROUP BY user_id, membership_type, membership_id
HAVING COUNT(*) > 1
) dupes
SQL
}

dup_checks.each do |label, sql|
count = conn.execute(sql).first['c'].to_i
status = count.zero? ? '✓' : "✗ #{count} duplicate sets"
errors << "Duplicates: #{label} (#{count})" unless count.zero?
puts format(' %-50s %s', label, status)
end

# 5. Counter cache consistency
puts "\n--- Counter Cache Checks ---"
cache_sql = <<~SQL.squish
SELECT c.id, c.name, c.rules_count AS cached,
(SELECT COUNT(*) FROM base_rules r
WHERE r.component_id = c.id AND r.type = 'Rule'
AND r.deleted_at IS NULL) AS actual
FROM components c
WHERE c.rules_count != (
SELECT COUNT(*) FROM base_rules r
WHERE r.component_id = c.id AND r.type = 'Rule'
AND r.deleted_at IS NULL
)
SQL
mismatches = conn.execute(cache_sql).to_a
if mismatches.empty?
puts ' rules_count matches actual ✓'
else
mismatches.each do |m|
puts format(' %-50s ✗ cached=%d actual=%d', "Component #{m['id']} (#{m['name']})", m['cached'], m['actual'])
end
errors << "Counter cache drift: #{mismatches.size} components"
end

# 6. Post-migration override tables (if they exist)
if conn.table_exists?('rule_check_overrides')
puts "\n--- Override Table Checks ---"
override_orphan_sql = <<~SQL.squish
SELECT COUNT(*) AS c FROM rule_check_overrides rco
LEFT JOIN base_rules r ON rco.rule_id = r.id
WHERE r.id IS NULL
SQL
count = conn.execute(override_orphan_sql).first['c'].to_i
status = count.zero? ? '✓' : "✗ #{count} orphaned"
errors << "Orphaned: rule_check_overrides (#{count})" unless count.zero?
puts format(' %-50s %s', 'rule_check_overrides → rules', status)
end

if conn.table_exists?('rule_description_overrides')
override_orphan_sql = <<~SQL.squish
SELECT COUNT(*) AS c FROM rule_description_overrides rdo
LEFT JOIN base_rules r ON rdo.rule_id = r.id
WHERE r.id IS NULL
SQL
count = conn.execute(override_orphan_sql).first['c'].to_i
status = count.zero? ? '✓' : "✗ #{count} orphaned"
errors << "Orphaned: rule_description_overrides (#{count})" unless count.zero?
puts format(' %-50s %s', 'rule_description_overrides → rules', status)
end

# Summary
puts
puts '=' * 60
if errors.empty?
puts "ALL CHECKS PASSED ✓ (#{orphan_checks.size + null_checks.size + dup_checks.size + 1} checks)"
else
puts "#{errors.size} ISSUES FOUND:"
errors.each { |e| puts " ✗ #{e}" }
puts
puts 'Run db:validate after fixing to re-check.'
exit 1
end
puts '=' * 60
end
end
# rubocop:enable Style/FormatStringToken
Loading
Loading