Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Query Guard Laravel Package

alex-frolov/query-guard

Query Guard adds configurable safeguards to Laravel database queries, helping detect and block dangerous or unintended SQL before it reaches production. Enforce safety rules, catch destructive statements, and protect critical data with minimal setup.

View on GitHub
Deep Wiki
Context7

Getting Started

  • Require the package via Composer and run it against an existing test suite first, not against production traffic. The tool is designed to observe the queries your application actually issues, so the test suite is the fastest source of realistic input.
  • If you are upgrading from 1.0.0, read UPGRADING.md before anything else. The fingerprint change for multi-row VALUES means existing baseline entries will no longer match, and regenerating is mandatory:
    QUERY_GUARD_GENERATE_BASELINE=1 <your test command>
    
    Then commit the new baseline and only afterwards re-enable fail-on.
  • Look first at the summary block, not the individual findings. As of 1.0.1 the summary explicitly states what the tool could not judge (plans that read no table, DBAL 3 connections without savepoints, and so on). That section tells you whether a clean run is genuinely clean.
  • First use case: run the full suite with tier 2 enabled and no fail-on, capture report-json, and read the counts by rule. Because 1.0.1 changes finding counts in both directions, a first run with fail-on="error" and tier 2 on will almost certainly fail for reasons unrelated to your code — triage it manually before wiring it into CI.
  • Where to look when something is confusing: the message text (it now names the real table plus alias, e.g. no suitable index on "settings_entry" (t0)), then the baseline file, then the report-json payload. Those three cover nearly all "why is this reported / why is this not reported" questions.

Implementation Patterns

  • Baseline-driven CI. Commit a baseline per branch or per mainline, run the suite in CI with fail-on="error", and let the baseline absorb pre-existing findings. Regenerate deliberately — never as part of the CI job itself — because regeneration hides new findings introduced by the same PR that regenerates.
  • Tier 2 as a periodic job. Tier 2 is the expensive, plan-reading pass and it just gained new coverage: tables inside subqueries are now checked on their own. Run it nightly or on a label rather than on every push if plan collection is slow, and expect a one-time burst of new findings on the first run after upgrading.
  • Doctrine projects: set savepoints explicitly. If you are on DBAL 3, put this in your test database config:
    doctrine:
      dbal:
        use_savepoints: true
    
    Without it, query-in-loop cannot distinguish consecutive flushes under a test transaction and will silently miss a flush() inside a loop. DBAL 4 is unaffected; the notice in the summary names the offending connection so you can find it without guessing.
  • ParaTest runs: aggregate report-json yourself. Each worker is started with --no-output, so no per-worker summary is printed and stdout only shows whatever the parent process emits. Point every worker at a distinct JSON path and merge them in a follow-up step; otherwise your CI log looks clean while findings sit in the artifacts.
  • Filter by table, not by alias, when triaging. Findings for JOIN (SELECT ...) x and Doctrine's dctrn_result paginator are gone, and the tables inside those subqueries are now reported against their real names. Grep-friendly table names make it easy to route findings to the owning team.
  • Treat no-possible-index as a join-shape check. Single-table reads with no WHERE (a findAll() over a lookup table) are no longer reported, which is correct — an index cannot narrow a full scan. In joins, a table without a condition is still checked, so the rule keeps its value on the queries where an index actually matters.
  • Use the "unjudged plan" count as a health metric. Plans that read no table — unique-key lookups finding no row, Impossible WHERE, SELECT version() — now get their own count instead of being lumped into "failed". A stable non-zero number here is normal; a growing one means your plan collection is being polluted by driver-level queries, not that your schema regressed.

Gotchas and Tips

  • Upgrading a baseline is not a no-op. VALUES (?, ?), (?, ?) and VALUES (?, ?) are now the same fingerprint, so the finding returns as new and the old baseline entry is reported as matching nothing. That "matching nothing" line is your checklist for entries to delete — do not silently regenerate and assume the diff is noise.
  • Finding counts move in both directions in 1.0.1. query-in-loop and no-possible-index report much less; tier 2 with subquery checking reports more. If you gate on fail-on="error" with tier 2 on, run the check once by hand after upgrading and diff the JSON against the previous run before trusting a red or green build.
  • query-in-loop counts a Doctrine flush() as one unit of work. A flush of five entities used to look like five queries from one place. A flush() inside a loop is still reported, and the message states how many units of work the queries came from — use that number to decide whether it is a real N+1 or just a batch flush.
  • Savepoint statements from the test harness are excluded from the trace. Under a rolled-back test transaction they used to wrap every flush; on at least one suite an entire finding consisted of them. If you previously suppressed findings that were purely savepoint noise, check whether those suppressions are now dead weight.
  • DBAL 3 without savepoints is a silent blind spot, not a warning you can ignore. The new summary notice is the only signal that query-in-loop is under-reporting on that connection. Fix the config or upgrade to DBAL 4; adding an ignore rule is the wrong response.
  • Derived tables can never have an index, so their disappearance from no-possible-index is intentional. If you had baseline entries or ignore rules targeting dctrn_result or a subquery alias, they will now match nothing — clean them up so future genuine findings are not hidden behind stale rules.
  • Messages changed shape. no suitable index on "settings_entry" (t0) names the table first and the alias in parentheses. Any log-scraping, dashboards, or alert rules keyed on the old alias-first format need updating.
  • The "table called unknown" bug is fixed, but if you baselined or ignored findings against a literal "unknown" table on MySQL, those entries are now orphans. Remove them rather than leaving them to silently mask a real table later.
  • A failing plan now says why, and no-table plans are counted separately. If you have alerting on "plan failed", retune it — the metric that used to include SELECT version() and empty unique lookups is now much narrower, so an unchanged threshold will under-alert.
  • Extension point worth knowing: the summary is the contract between the tool and your CI policy. Anything the tool cannot judge is stated there; if you build a custom gate around report-json, surface the unjudged counts alongside the finding counts so a reduction in coverage never reads as a clean build.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource