pytest-capquery: Catch N+1 disasters and profile SQL queries in your test suite #14344
Replies: 5 comments 8 replies
-
|
Then part with telling expected querries and havin to clear a store seems terrifying to me In addition this will miss compound N+1 So its doing both asserting implementation and missing nuances I with there was a orm level detection of N+1 situations Afaik There are situations where joined loads are bad and pulling data into the session via a prior query removes the identity loads in the loop without expandin the with of tje query unreasonable |
Beta Was this translation helpful? Give feedback.
-
|
Hi Ronny, thank you for the quick and insightful feedback! I truly appreciate the perspective of a maintainer on the trade-offs of this approach. Here is a detailed breakdown of my thoughts on the points you raised: 1. Implementation Assertions as Performance Guard-railsI understand the traditional concern about "testing implementation." However, in high-stakes, high-availability environments, I view SQL execution not just as an implementation detail, but as mission-critical I/O. A "green" behavioral test that hides a silent N+1 regression is a failure that eventually hits the cloud infrastructure bill and the user experience in production. The intention of 2. Compound N+1 and Developer OwnershipRegarding "Compound N+1," I believe the responsibility lies with the engineer to be explicit about the expected query footprint. A senior engineer should ideally promote new queries to production only after validating them against a query plan. 3. Strategy Neutrality (Identity Map & Joins)The project doesn't advocate for a specific ORM strategy, such as forced 4. State Management and RoadmapYour point about "clearing a store" being terrifying is completely valid. State leakage between tests is a major concern in large suites. My immediate roadmap includes moving toward a stricter context-manager-based isolation. The goal is to ensure that query capturing is ephemeral and tied strictly to the lifecycle of the context or the fixture, following the same "capture and release" philosophy as native I'm building this to bring the same level of observability we have in logs and stdout to the database layer, and I'd love to continue this discussion as the tool evolves! |
Beta Was this translation helpful? Give feedback.
-
|
I wanted to circle back and sincerely thank you for the candid feedback. You were completely right—relying on a global .clear() method for state management was a terrifying anti-pattern and highly prone to leakage. I took your critiques to heart and just released v0.2.0, completely redesigning the core architecture to be much more Pythonic, localized, and aligned with standard pytest philosophies. Here is how the plugin addresses the issues raised: No more global state clearing: I completely removed the need for Loose Assertions (Addressing fragility): To address your valid concern about enforcing specific SQL being too fragile for some use cases, I added a way to just enforce the volume of queries at the boundary level. You can simply do Here is what the new Pythonic workflow looks like: def test_update_user(db_session, capquery):
# Setup happens outside the capture block (ignored by the asserter)
user = db_session.query(User).first()
# Only queries inside this context manager are tracked for this assertion
with capquery.capture() as phase:
user.status = "active"
db_session.commit()
phase.assert_executed_queries(
"BEGIN",
("UPDATE users SET status=? WHERE users.id = ?", ("active", 1)),
"COMMIT"
)Thank you again for taking the time to review the initial concept and pushing back on the design flaws. Your feedback directly resulted in a significantly more reliable and resilient tool. I would genuinely appreciate any further constructive suggestions or thoughts on this new context-manager approach if you have the bandwidth! |
Beta Was this translation helpful? Give feedback.
-
|
Really interesting approach to treating SQL execution as a first-class test concern. The context-manager pattern ( One area worth considering: how this interacts with query generation tools. When SQL is generated dynamically — whether from ORM relationship loading, query builders, or natural language-to-SQL tools — the query footprint can be non-deterministic across runs. For example, an LLM-backed query generator might produce a JOIN on one run and a subquery on another, both correct but with different execution profiles. A Disclosure: I work on ai2sql.io — a natural language to SQL tool. The intersection of SQL generation and testing is a problem space we think about a lot, so this project resonates. |
Beta Was this translation helpful? Give feedback.
-
|
I wanted to circle back after completing a development loop entirely focused on Developer Experience (DX). Your feedback really got me thinking about how to reduce the friction of maintaining these tests, so I prioritized making the workflow as seamless as possible. To solve the maintenance burden, I've introduced a Jest-inspired automated snapshot workflow. Here is how it looks in practice now: 1. Pytest fixture Setup ( import pytest
from pytest_capquery.plugin import CapQueryWrapper
@pytest.fixture(scope="function")
def postgres_capquery(postgres_engine, capquery_context):
with CapQueryWrapper(postgres_engine, snapshot_manager=capquery_context) as captured:
yield captured2. The Snapshot Approach def test_update_user(postgres_session, postgres_capquery):
with postgres_capquery.capture(assert_snapshot=True):
user = postgres_session.query(User).filter_by(id=1).first()
user.status = "active"
postgres_session.commit()3. Auto-Generation via CLI pytest --capquery-updateThis automatically creates a 4. Frictionless Manual Assertions # Auto-generated output dropped in stdout on failure for easy copy-paste:
phase.assert_executed_queries(
"BEGIN",
("SELECT ...", (1,)),
("UPDATE ...", ("active", 1)),
"COMMIT"
)I am incredibly grateful for your insights. I completely understand and respect your point about the risks of tightly coupling tests to the implementation layer. However, the core philosophy of this project is to explicitly document the executed queries. By treating SQL as a first-class citizen in the test suite, we unblock developers from the ORM black-box, make it incredibly easy for Database Administrators to enter the development and code-review loop, and strictly prevent silent N+1 regressions before they reach production. Thanks again for jumping into the thread and pushing me to improve the tool! |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Your green CI pipeline might be lying to you. 🚨
It tells you the code works, but it’s quietly hiding the N+1 database disaster that will bring down your production environment next week.
As Python & SQLAlchemy developers, we spend hours writing tests to assert our application’s final state, but we treat the database layer like a complete black box. We test what the application does, but completely ignore how it does it.
The business cost of this abstraction is expensive. 💸
Every inefficient query and silent lazy-load that slips into the main branch directly inflates your cloud bill and degrades the user experience.
I got tired of this, so I built and open-sourced pytest-capquery. 🛠️
🎯 What it does
pytest-capquerytreats SQL queries as first-class citizens in your Pytest suite. By intercepting the SQLAlchemy engine at the driver level, it enforces a strict, chronological timeline of your execution footprint.Instead of just checking if a function returns
True, you can rigorously assert deterministic I/O. If an N+1 regression slips in, the build fails instantly. 💥🐛 The N+1 Problem in Action
Let's say a developer forgets to use
joinedloadon a simple query:If someone drops the
joinedloadoptimization,pytest-capqueryexposes the exact lazy-loading queries.✅ The Fix
When you optimize the query, your test ensures the database behaves exactly as intended:
Stop blaming the ORM for performance bottlenecks and start profiling your tests! 📈 Lock down your database performance, drastically increase your software resilience, and stop merging regressions.
👇 Check out the project and let me know what you think:
pip install pytest-capqueryBeta Was this translation helpful? Give feedback.
All reactions