Default severity: warning · Applicability: unsafe · Category: raw SQL
Detects SET / SET LOCAL / SET SESSION of a planner-toggling Postgres GUC inside application code — enable_* (hashjoin, mergejoin, nestloop, seqscan, indexscan, …), plan_cache_mode, and jit*. The check is pure text shape, so it sees the GUC name even when the value is a bind parameter (%s or %(name)s): cursor.execute('SET enable_seqscan = %s', ['off']) is flagged exactly like a literal.
SET enable_hashjoin = off does not make a query faster. It removes the planner’s ability to choose otherwise — and the planner is choosing per query, with row estimates the call site cannot see. The failure mode is asymmetric:
A plain SET is also connection-scoped. With connection pooling, the override leaks into whatever runs next on that same connection. SET LOCAL scopes it to the transaction and is the only form with an excuse — but even that is a judgement call a reviewer should see, so it is flagged too.
cursor.execute('SET enable_seqscan = %s; SET enable_bitmapscan = %s', ['off', 'off'])
cursor.execute("SET LOCAL enable_hashjoin = off")
connection.cursor().execute("SET SESSION enable_bitmapscan TO 'off'")
cursor.execute("SET plan_cache_mode = force_generic_plan")
cursor.execute("SET jit = off")
Each GUC in a multi-statement string is reported separately, and both the = and TO forms match.
SET search_path, SET timezone, SET statement_timeout, SET work_mem and the rest of the session-settings surface — those have legitimate per-request uses, and flagging them would drown the rule. Only the family that exists to overrule the planner is in scope.
Comment text is ignored as well: Python # comments, SQL -- comments and SQL /* ... */ blocks (including ones spanning several lines) do not produce findings, so commented-out SQL stays quiet.
def open_tickets(connection):
# every later query on this pooled connection inherits the override
connection.cursor().execute("SET enable_hashjoin = off")
return Ticket.objects.raw(TICKET_REPORT_SQL)
def open_tickets():
return Ticket.objects.raw(TICKET_REPORT_SQL)
Fix the query or the index instead — or, if an override is truly required, scope it with SET LOCAL inside a transaction and keep it auditable:
with transaction.atomic(), connection.cursor() as cur:
cur.execute("SET LOCAL enable_seqscan = off")
...
# orm-lens-disable-next-line DOL041
Or per-workspace in .vscode/settings.json: {"djangoOrmLens.rules": {"DOL041": "off"}}.