Configuration reference#
| Option | Values | Default | Meaning |
|---|---|---|---|
queryguard_mode |
fail, warn, off |
fail |
fail fails a test with N+1. warn reports it as a QueryGuardWarning and lets the test pass. off turns detection off; tests without a budget are not measured. |
queryguard_relationship_threshold |
integer ≥ 2 | 2 |
Number of different parent objects for which one relationship must be lazy-loaded to count as N+1. |
queryguard_repeat_threshold |
integer ≥ 2 | 5 |
Number of times one normalized SELECT must run to count as N+1. Schema reflection (PRAGMA, sqlite_master, pg_catalog, information_schema) is not counted, see how detection works. |
queryguard_small_repeat_threshold |
0 or integer ≥ 2, below the repeat threshold |
0 (off) |
Number of runs of one statement from one source line, each with different parameters, that count as N+1 although they are below queryguard_repeat_threshold. Pagination and writes are not counted. |
queryguard_repeat_scope |
test, session |
test |
What the repeat, relationship, column-load and write-loop rules count together. test adds up everything one test runs. session counts each SQLAlchemy Session (or, for a connection no Session opened, each connection checkout) on its own, which is what a test that makes several independent requests needs. See Tests with several sessions or requests. |
queryguard_write_loop_threshold |
0 or integer ≥ 2 |
0 (off) |
Number of single-row INSERT, UPDATE or DELETE statements of one shape from one source line that count as a write loop (kind write-loop). |
queryguard_unused_eager_load |
off, warn, fail |
off |
Experimental (exempt from the deprecation policy). Reports relationships a test eager-loads and never reads (kind unused-eager-load); warn only warns, fail fails the test in fail mode. See docs/unused-eager-load.md. |
queryguard_fail_on |
kinds or fnmatch patterns, one per line |
empty (all kinds) | The finding kinds that fail a test in fail mode; the other kinds are reported as QueryGuardWarning and the test passes. For example relationship-load alone. Budgets always fail. |
queryguard_column_load_threshold |
integer ≥ 2 | 5 |
Number of different instances of one class that must load deferred or expired columns on their own to count as N+1. |
queryguard_max_events |
integer ≥ 0 | 10000 |
Statements (and relationship loads) stored per test for reports. Beyond it they are only counted, and the report says so. 0 stores everything. |
queryguard_ignore |
patterns, one per line | empty | Findings to leave out of reports: KIND LABEL [file=GLOB] [function=GLOB], see Opting out. |
queryguard_baseline |
path | empty | Baseline file, relative to the rootdir: findings listed in it do not fail or warn, see Baseline. |
queryguard_index_check |
true, false |
false |
Checks each SELECT for a missing index (PostgreSQL 16 or newer, and SELECT, UPDATE and DELETE on SQLite, MySQL and MariaDB). |
queryguard_index_advisor |
true, false |
true |
Suggests an index for each missing-index finding, see Index advisor. Used with the index check. |
queryguard_hypopg |
auto, on, off |
auto |
Verifies the suggested indexes with the hypopg extension: auto when it is installed, on also creates it inside the check's savepoint, off never uses it. |
queryguard_index_ignore_tables |
table names or fnmatch patterns, one per line |
empty | Tables the index check ignores. A pattern matches table or schema.table, quoted or not, for example countries, "UserAccount", lookup_* or audit.*; the spellings are the same as in queryguard_ignore and allow_nplusone, see Ignoring findings. |
Any invalid value stops pytest with a usage error.
The options with a fixed set of values (queryguard_mode, queryguard_repeat_scope, queryguard_unused_eager_load, queryguard_hypopg) and --queryguard-mode ignore case and surrounding spaces: WARN, Warn and warn mean the same.
In pyproject.toml:
[tool.pytest.ini_options]
queryguard_mode = "warn"
queryguard_relationship_threshold = 3
queryguard_repeat_threshold = 10
queryguard_index_check = true
queryguard_index_ignore_tables = ["countries", "lookup_*"]
With pytest 9 you can use the native TOML table instead:
[tool.pytest]
queryguard_mode = "warn"
queryguard_relationship_threshold = 3
queryguard_repeat_threshold = 10
queryguard_index_check = true
queryguard_index_ignore_tables = ["countries", "lookup_*"]
pytest.ini and setup.cfg work as usual.
The warning classes, pytest_queryguard.QueryGuardWarning for N+1 and
missing indexes in warn mode,
pytest_queryguard.QueryGuardInternalWarning for an internal error and
pytest_queryguard.QueryGuardDeprecationWarning for an option or name that a
later release may remove (a DeprecationWarning, not a QueryGuardWarning),
work with pytest's usual warning filters. For example, to turn the findings
of warn mode back into failures for one job:
[tool.pytest.ini_options]
queryguard_mode = "warn"
filterwarnings = ["error::pytest_queryguard.QueryGuardWarning"]
A blanket filterwarnings = error has the same effect: in warn mode it makes
every finding fail its test, like fail mode. The failure shows the finding
without the plugin's own source.
An "error" filter never applies to QueryGuardInternalWarning: a bug in the
plugin must not fail a test, so with -W error or filterwarnings = error
that warning is still shown in the warnings summary, not raised. An "ignore"
filter hides it as usual; the "queryguard: measurement stopped" section of the
summary is printed either way.
Command-line options:
| Option | Meaning |
|---|---|
--queryguard-mode=fail\|warn\|off |
Overrides queryguard_mode for this run. |
--queryguard-report |
Prints the 10 tests with the most queries at the end of the session. |
--queryguard-baseline=PATH |
Baseline file for this run; overrides queryguard_baseline. |
--queryguard-sarif=PATH |
Writes the findings of this run to PATH as SARIF 2.1.0. |
--queryguard-update-baseline |
Writes the findings of this run to the baseline file instead of reporting them. |
--queryguard-index-check |
Turns the missing-index check on for this run. |
--no-queryguard-index-check |
Turns the missing-index check off for this run, whatever queryguard_index_check says. |
Markers#
| Marker | Meaning |
|---|---|
max_queries(limit) |
Fails the test if it runs more than limit SQL statements. See Budgets and timing. |
max_db_time(seconds) |
Fails the test if its statements take more than that many seconds in total. |
allow_nplusone(*labels, kind=..., file=..., function=...) |
Leaves out findings: all of them without arguments, or only those that match the fnmatch patterns. See Ignoring findings. |
allow_missing_index(*tables) |
Skips the missing-index check for the test, or only for the given tables. allow_nplusone without arguments does not affect the index check; with patterns it also matches missing-index findings, as in allow_nplusone("public.audit_log", kind="missing-index"). |