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.
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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.SELECT version() and empty unique lookups is now much narrower, so an unchanged threshold will under-alert.report-json, surface the unjudged counts alongside the finding counts so a reduction in coverage never reads as a clean build.How can I help you explore Laravel packages today?