0.0
The project is in a healthy, maintained state
Automatically detect risky migration patterns (unsafe column removal, locking operations, data loss) before they reach production.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 13.2

Runtime

>= 5.2, < 8.0
>= 5.2, < 8.0
 Project Readme

QueryGuard: Migration Safety for Rails

Catch risky database changes before they reach production.

QueryGuard automatically analyzes your Rails migrations in CI to detect safety issues, preventing schema problems from entering your codebase. Built with deep query intelligence and security features for comprehensive database safety.

๐ŸŽฏ What Makes QueryGuard Different?

v1.0 Focus: Migration Safety + Query Security

QueryGuard runs in your CI pipeline to:

  • Analyze Migrations: Detect risky patterns like non-nullable columns without defaults, missing rollbacks, performance issues
  • Detect Security Issues: Monitor for SQL injection, unusual query patterns, data exfiltration, mass assignment attacks
  • Monitor Query Performance: Track slow queries and unusual database activity

Installation

Add QueryGuard to your Gemfile:

group :development, :test do
  gem 'query_guard'
end

Then install and generate the initializer:

bundle install
bundle exec rails generate query_guard:install

This creates config/initializers/query_guard.rb with essential configuration.

Configuration

The default configuration works for most Rails applications. Edit config/initializers/query_guard.rb to customize:

QueryGuard.configure do |config|
  # Directory where migrations live (default: db/migrate)
  config.migrations_directory = 'db/migrate'

  # Environments to analyze (default: [:development, :test])
  config.enabled_environments = [:development, :test]

  # Max queries to check per request (prevent timeouts)
  config.max_queries_per_request = 100

  # Max query duration in ms before flagging as slow
  config.max_duration_ms_per_query = 5000
end

Using QueryGuard

In CI (Recommended)

Add to your CI workflow to check migrations automatically:

# .github/workflows/db-safety.yml
name: Database Safety
on: [pull_request]

jobs:
  analyze:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:14
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        env:
          POSTGRES_PASSWORD: postgres

    steps:
      - uses: actions/checkout@v3
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: 3.3.0
          bundler-cache: true

      - name: Analyze migrations
        run: bundle exec queryguard analyze
        env:
          DATABASE_URL: postgres://postgres:postgres@localhost/test_db

Locally

# Analyze migrations in your current environment
bundle exec queryguard analyze

# Get JSON output for integration with CI dashboards
bundle exec queryguard analyze --format json

JSON Output

QueryGuard can output analysis results as JSON for integration with CI dashboards, security tools, or custom workflows:

bundle exec queryguard analyze --format json

JSON Schema

{
  "status": "success",
  "findings": [
    {
      "type": "risky_migration",
      "severity": "high",
      "message": "Adding non-nullable column without default",
      "file": "db/migrate/20240101120000_add_user_status.rb",
      "line": 4,
      "context": {
        "migration_name": "AddUserStatus",
        "environment": "test"
      }
    }
  ],
  "metadata": {
    "environment": "test",
    "database": "postgresql",
    "schema_version": "20240101120000"
  }
}

What's Coming Next

v2.0 (Future)

  • Query analysis for N+1 problems and missing indexes
  • Live application query monitoring
  • Performance recommendations based on actual query patterns
  • SaaS dashboard integration

v1.0 focuses exclusively on migration safety because:

  1. Migrations are deterministic and analyzable in CI
  2. Migration problems are expensive to fix in production
  3. This gives you immediate, measurable value

For query monitoring in your application, see QueryGuard's query analysis (coming v2.0) or integrate with tools like:

Development

To set up the development environment:

git clone https://github.com/yourusername/query_guard.git
cd query_guard
bundle install
bundle exec rake spec

All 186+ tests should pass. QueryGuard maintains 100% test coverage for core analysis logic.

Contributing

See CONTRIBUTING.md for contribution guidelines.

License

MIT License - See LICENSE.txt for details.

Support


Why QueryGuard?

Database migrations are a critical part of your deployment pipeline. A single unsafe migration can:

  • Lock production tables for hours
  • Cause deployment failures requiring emergency rollbacks
  • Add months of technical debt to your schema

QueryGuard catches these issues in CI, before they reach production.

Stop deploying database surprises. Start using QueryGuard.

QueryGuard fills a unique niche between traditional APMs and Rails profilers:

  • vs Datadog APM: Datadog excels at distributed tracing, logs/metrics correlation, and infrastructure monitoring, but is generic across languages. QueryGuard is deeply Rails and ActiveRecord-aware, providing schema intelligence and query budgets that Datadog doesn't offer.

  • vs Skylight: Skylight is an excellent Rails profiler with request timelines, deploy tracking, and background job support. QueryGuard complements this by adding query budgets/SLOs per endpoint, schema-aware linting (missing indexes, wide scans), and first-class test/CI support via RSpec matchers.

  • vs Grafana: Grafana provides powerful multi-signal visualization and metricsโ†”traces workflows via exemplars, but isn't Rails-specific. QueryGuard offers Rails-native tooling that works in development, tests, and production with minimal configuration.

QueryGuard's Focus: SQL + Schema + Budget enforcement + Developer UX (console, tests, CI).

๐Ÿ“ฆ Installation

Add to your Gemfile:

gem "query_guard"

Then run:

bundle install

โš™๏ธ Configuration

Create an initializer at config/initializers/query_guard.rb:

QueryGuard.configure do |config|
  # Environments where QueryGuard should be active
  config.enabled_environments = %i[development test production]

  # === Budget System (New in v2) ===
  # Define query budgets/SLOs for specific endpoints or jobs
  
  # Controller actions
  config.budget.for("users#index", count: 10, duration_ms: 500)
  config.budget.for("posts#show", count: 5, duration_ms: 200)
  config.budget.for("admin/reports#dashboard", count: 50, duration_ms: 2000)
  
  # Background jobs
  config.budget.for_job("EmailJob", count: 20, duration_ms: 1000)
  config.budget.for_job("DataExportJob", count: 100, duration_ms: 5000)
  
  # Budget enforcement mode
  config.budget.mode = :log       # :log (warn only), :notify (callback), :raise (exception)
  
  # Optional: callback for :notify mode
  config.budget.on_violation = ->(key, violation) {
    # Send to error tracker, metrics service, etc.
    Honeybadger.notify("Budget violation", context: { key: key, violation: violation })
  }

  # === Legacy Limits (Still Supported) ===
  config.max_queries_per_request = 100
  config.max_duration_ms_per_query = 100.0
  config.block_select_star = true
  
  # Ignore certain SQL patterns
  config.ignored_sql = [
    /^PRAGMA /i,
    /^BEGIN/i,
    /^COMMIT/i,
    /^SHOW /i
  ]

  # === Security Features ===
  config.enable_security = true
  config.detect_sql_injection = true
  config.detect_unusual_query_pattern = true
  config.detect_data_exfiltration = true
  config.detect_mass_assignment = true

  # === Export Configuration ===
  config.base_url = ENV["QUERY_GUARD_API_URL"]
  config.api_key = ENV["QUERY_GUARD_API_KEY"]
  config.project = "my_app"
  config.env = Rails.env
  
  # Logging
  config.raise_on_violation = false  # Set to true in CI
  config.log_prefix = "[QueryGuard]"
end

๐Ÿš€ Features

1. Query Budgets & SLOs

Define query budgets for specific endpoints or background jobs:

# In config/initializers/query_guard.rb
QueryGuard.configure do |config|
  # Set budgets for controller actions
  config.budget.for("users#index", count: 10, duration_ms: 500)
  config.budget.for("posts#show", count: 5, duration_ms: 200)
  
  # Set budgets for background jobs
  config.budget.for_job("EmailJob", count: 20)
  config.budget.for_job("ReportJob", count: 100, duration_ms: 5000)
  
  # Choose enforcement mode
  config.budget.mode = :log  # Options: :log, :notify, :raise
end

Enforcement Modes:

  • :log - Logs warnings when budgets are exceeded (default, safe for production)
  • :notify - Calls a custom callback for integration with error trackers
  • :raise - Raises an exception (useful in test/CI environments)

With Callbacks:

config.budget.mode = :notify
config.budget.on_violation = ->(key, violation) {
  # Send to your monitoring service
  Datadog::Statsd.new.increment("query_guard.budget.exceeded", tags: ["endpoint:#{key}"])
  
  # Or send to error tracker
  Sentry.capture_message("Budget exceeded", extra: { key: key, violation: violation })
}

2. Trace API (Console & Testing)

Manually trace query performance in any context:

# In Rails console or tests
result, report = QueryGuard.trace("load active users") do
  User.where(active: true).limit(100).to_a
end

puts "Queries executed: #{report.query_count}"
puts "Total duration: #{report.total_duration_ms}ms"
puts "Violations: #{report.violations.inspect}"
puts "Queries:"
report.queries.each do |q|
  puts "  #{q[:duration_ms]}ms: #{q[:sql]}"
end

With Context:

result, report = QueryGuard.trace("process batch", context: { batch_id: 123, user_id: 456 }) do
  Batch.find(123).process!
end

# Context is included in the report for correlation
puts report.context  # => { batch_id: 123, user_id: 456 }

3. RSpec Matchers

Test query performance in your specs:

require "query_guard/rspec"

RSpec.describe UsersController, type: :controller do
  describe "GET #index" do
    it "stays within query budget" do
      expect {
        get :index
      }.to_not exceed_query_budget(count: 10, duration_ms: 500)
    end
    
    # Or use named budgets defined in config
    it "respects users#index budget" do
      expect {
        get :index
      }.to_not exceed_query_budget("users#index")
    end
  end
end

Helper Method:

RSpec.describe "batch processing" do
  it "processes batch efficiently" do
    report = within_query_budget(count: 50, duration_ms: 2000) do
      Batch.process_all
    end
    
    expect(report.query_count).to be < 50
  end
end

4. SQL Fingerprinting & Statistics

Track query patterns across your application:

# In console or background job
QueryGuard::Fingerprint.record("SELECT * FROM users WHERE id = 123", 45.2)
QueryGuard::Fingerprint.record("SELECT * FROM users WHERE id = 456", 32.1)

# Get stats for a specific fingerprint
fp = QueryGuard::Fingerprint.generate("SELECT * FROM users WHERE id = ?")
stats = QueryGuard::Fingerprint.stats_for(fp)

puts stats[:count]              # => 2
puts stats[:total_duration_ms]  # => 77.3
puts stats[:min_duration_ms]    # => 32.1
puts stats[:max_duration_ms]    # => 45.2
puts stats[:first_seen_at]
puts stats[:last_seen_at]

# Get top queries by various metrics
QueryGuard::Fingerprint.top_by_count(10)         # Most frequently executed
QueryGuard::Fingerprint.top_by_duration(10)      # Highest total time
QueryGuard::Fingerprint.top_by_avg_duration(10)  # Slowest on average

Fingerprinting normalizes SQL:

  • Removes string and numeric literals
  • Collapses whitespace
  • Normalizes IN (...) lists
  • Returns consistent SHA1 hash
# These all produce the same fingerprint:
QueryGuard::Fingerprint.generate("SELECT * FROM users WHERE id = 1")
QueryGuard::Fingerprint.generate("SELECT * FROM users WHERE id = 999")
QueryGuard::Fingerprint.generate("SELECT  *  FROM users WHERE id = 42")
# All normalize to: "select * from users where id = ?"

5. Security Features (Existing)

QueryGuard includes built-in security detection:

  • SQL Injection Detection: Flags suspicious patterns (OR 1=1, UNION SELECT, etc.)
  • Unusual Query Patterns: Rate limiting per actor (IP/user)
  • Data Exfiltration: Monitors large responses and suspicious endpoints
  • Mass Assignment: Detects unpermitted parameters
config.enable_security = true
config.detect_sql_injection = true
config.detect_unusual_query_pattern = true
config.max_queries_per_minute_per_actor = 300

# Custom actor resolver
config.actor_resolver = ->(env) {
  env["warden"].user&.id || env["action_dispatch.remote_ip"]
}

6. Export & Monitoring

Export query data to external services:

config.base_url = "https://your-monitoring-service.com"
config.api_key = ENV["MONITORING_API_KEY"]
config.project = "my_rails_app"
config.env = Rails.env
config.export_mode = :async  # Don't block requests

Exported data includes:

  • Query statements with fingerprints
  • Durations and timestamps
  • Budget violations
  • Security threat events
  • Request context (controller, action, user, etc.)

๐Ÿ“Š Use Cases

Development

# In Rails console
result, report = QueryGuard.trace("diagnose N+1") do
  Post.limit(10).each { |post| post.comments.to_a }
end

puts "Queries: #{report.query_count}"  # Spot N+1 problems immediately
report.queries.each { |q| puts q[:sql] }

Testing (CI)

# spec/support/query_guard.rb
RSpec.configure do |config|
  config.around(:each, :query_budget) do |example|
    metadata = example.metadata
    budget = metadata[:query_budget]
    
    expect {
      example.run
    }.to_not exceed_query_budget(**budget)
  end
end

# spec/controllers/users_controller_spec.rb
RSpec.describe UsersController do
  describe "GET #index", :query_budget, query_budget: { count: 5, duration_ms: 200 } do
    it "loads users" do
      get :index
      expect(response).to be_successful
    end
  end
end

Production Monitoring

# config/initializers/query_guard.rb
QueryGuard.configure do |config|
  config.enabled_environments = [:production]
  config.budget.mode = :notify
  
  config.budget.for("api/v1/users#index", count: 10, duration_ms: 100)
  config.budget.for("api/v1/posts#feed", count: 15, duration_ms: 150)
  
  config.budget.on_violation = ->(key, violation) {
    # Alert when budgets exceeded in production
    Datadog::Statsd.new.increment("query.budget.exceeded", tags: [
      "endpoint:#{key}",
      "type:#{violation[:type]}"
    ])
  }
end

๐Ÿ”ง API Reference

QueryGuard.configure

Configure QueryGuard settings. See Configuration section above.

QueryGuard.trace(label, context: {}, &block)

Trace a block of code and capture query statistics.

Arguments:

  • label (String): Descriptive label for the trace
  • context (Hash): Additional context (user_id, batch_id, etc.)
  • &block: Code to trace

Returns: [result, report] tuple

QueryGuard::Budget

Methods:

  • .for(key, **limits): Define budget for controller action
  • .for_job(job, **limits): Define budget for background job
  • .mode=: Set enforcement mode (:log, :notify, :raise)
  • .on_violation=: Set callback for :notify mode

QueryGuard::Fingerprint

Methods:

  • .generate(sql): Generate fingerprint for SQL
  • .normalize(sql): Normalize SQL query
  • .record(sql, duration_ms): Record query execution
  • .stats_for(fingerprint): Get stats for fingerprint
  • .top_by_count(limit): Top queries by count
  • .top_by_duration(limit): Top queries by total duration
  • .top_by_avg_duration(limit): Top queries by average duration
  • .reset!: Clear all stats

RSpec Matchers

require "query_guard/rspec"

expect { code }.to_not exceed_query_budget(count: 10)
expect { code }.to_not exceed_query_budget(count: 10, duration_ms: 500)
expect { code }.to_not exceed_query_budget("users#index")

report = within_query_budget(count: 10) { code }

๐Ÿงช Testing

Run the test suite:

bundle exec rspec

๐Ÿ“„ License

MIT License. See LICENSE.txt for details.

๐Ÿ™ Contributing

Contributions welcome! Please:

  1. Fork the repository
  2. Create a feature branch
  3. Write tests for your changes
  4. Submit a pull request

๐Ÿ“ฎ Support

  • Issues: GitHub Issues
  • Documentation: This README
  • Example Rails App: [Coming soon]

Built with โค๏ธ to make Rails query performance monitoring delightful.

2fd3b3b87eaac58f779bc220c6bff2389c1b1c7b