-
Notifications
You must be signed in to change notification settings - Fork 35
⚡ Bolt: Consolidated Aggregate Queries for Statistics #525
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -52,4 +52,8 @@ | |||||||||||||
|
|
||||||||||||||
| ## 2026-02-11 - Multi-Metric Aggregate Queries | ||||||||||||||
| **Learning:** Executing multiple separate `count()` queries to gather system statistics results in multiple database round-trips and redundant table scans. | ||||||||||||||
| **Action:** Use a single SQLAlchemy query with `func.count()` and `func.sum(case(...))` to calculate all metrics in one go. This reduces network overhead and allows the database to perform calculations in a single pass. | ||||||||||||||
| **Action:** Use a single SQLAlchemy query with `func.count()`, `func.sum(case(...))`, and `func.avg()` to calculate all metrics in one go. This reduces network overhead and allows the database to perform calculations in a single pass. | ||||||||||||||
|
|
||||||||||||||
| ## 2026-02-28 - Label Mismatch in Aggregate Queries | ||||||||||||||
| **Learning:** When using `func.label()` in SQLAlchemy aggregate queries, any mismatch between the SQL label and the Python attribute accessed in the response dictionary will cause an `AttributeError`. | ||||||||||||||
|
||||||||||||||
| **Learning:** When using `func.label()` in SQLAlchemy aggregate queries, any mismatch between the SQL label and the Python attribute accessed in the response dictionary will cause an `AttributeError`. | |
| **Learning:** When labeling expressions with `.label(...)` in SQLAlchemy aggregate queries, any mismatch between the SQL label and the Python attribute accessed in the response dictionary will cause an `AttributeError`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
In SQLAlchemy, is func.label()a valid API for naming expressions, or should.label()be called on the SQL expression itself (for examplefunc.count(...).label("total"))?
💡 Result:
func.label() is not the intended API for aliasing/naming an expression in SQLAlchemy; func is for generating SQL function calls (e.g., COUNT(), COALESCE()). To name an expression, call .label() on the SQL expression (or use the standalone sqlalchemy.label() helper), e.g.:
from sqlalchemy import func, select
stmt = select(func.count(MyTable.id).label("total"))SQLAlchemy’s docs describe labeling as a ColumnElement.label() method (and also provide the standalone label(name, element) function), which returns a Label (typically rendered with AS). [1] [2]
func.label() is the wrong API in this note.
SQLAlchemy labels are applied via .label() on the expression itself (e.g., func.count(...).label("total")). Documenting func.label() points readers at a non-existent API, since func is for generating SQL function calls, not for aliasing.
Suggested fix
-**Learning:** When using `func.label()` in SQLAlchemy aggregate queries, any mismatch between the SQL label and the Python attribute accessed in the response dictionary will cause an `AttributeError`.
+**Learning:** When using `.label()` on SQLAlchemy expressions in aggregate queries (for example, `func.count(...).label("total")`), any mismatch between the SQL label and the Python attribute accessed in the response dictionary will cause an `AttributeError`.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ## 2026-02-28 - Label Mismatch in Aggregate Queries | |
| **Learning:** When using `func.label()` in SQLAlchemy aggregate queries, any mismatch between the SQL label and the Python attribute accessed in the response dictionary will cause an `AttributeError`. | |
| **Action:** Ensure SQL labels in aggregate queries exactly match the keys expected by the API response schema or dictionary mapping. | |
| ## 2026-02-28 - Label Mismatch in Aggregate Queries | |
| **Learning:** When using `.label()` on SQLAlchemy expressions in aggregate queries (for example, `func.count(...).label("total")`), any mismatch between the SQL label and the Python attribute accessed in the response dictionary will cause an `AttributeError`. | |
| **Action:** Ensure SQL labels in aggregate queries exactly match the keys expected by the API response schema or dictionary mapping. |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.jules/bolt.md around lines 57 - 59, The note incorrectly references
func.label()—update the documentation to explain that labels are applied by
calling .label() on the SQL expression (e.g., call .label("total") on
func.count(...) or any column/expression) rather than on the func object;
replace any mentions of func.label() with the correct pattern such as
func.count(...).label("name") and clarify that the SQLAlchemy expression's
.label("key") must match the API/dictionary key expected.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,9 +1,8 @@ | ||||||||||||||||||||||||||||||||||
| from fastapi import APIRouter, Depends, HTTPException, status | ||||||||||||||||||||||||||||||||||
| from fastapi import APIRouter, Depends | ||||||||||||||||||||||||||||||||||
| from sqlalchemy.orm import Session | ||||||||||||||||||||||||||||||||||
| from sqlalchemy import func, case | ||||||||||||||||||||||||||||||||||
| from typing import List | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| from sqlalchemy import func, case | ||||||||||||||||||||||||||||||||||
| from backend.database import get_db | ||||||||||||||||||||||||||||||||||
| from backend.models import User, UserRole | ||||||||||||||||||||||||||||||||||
| from backend.schemas import UserResponse | ||||||||||||||||||||||||||||||||||
|
|
@@ -17,25 +16,23 @@ | |||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| @router.get("/users", response_model=List[UserResponse]) | ||||||||||||||||||||||||||||||||||
| def get_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): | ||||||||||||||||||||||||||||||||||
| users = db.query(User).offset(skip).limit(limit).all() | ||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||
| Get all users with pagination. | ||||||||||||||||||||||||||||||||||
| Optimized: Uses column projection to reduce database overhead. | ||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||
| users = db.query( | ||||||||||||||||||||||||||||||||||
| User.id, | ||||||||||||||||||||||||||||||||||
| User.email, | ||||||||||||||||||||||||||||||||||
| User.full_name, | ||||||||||||||||||||||||||||||||||
| User.role, | ||||||||||||||||||||||||||||||||||
| User.is_active, | ||||||||||||||||||||||||||||||||||
| User.created_at | ||||||||||||||||||||||||||||||||||
| ).offset(skip).limit(limit).all() | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+23
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a deterministic sort before paginating. Offset/limit over an unordered query is unstable, so Suggested fix- ).offset(skip).limit(limit).all()
+ ).order_by(User.id).offset(skip).limit(limit).all()📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| return users | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| @router.get("/stats") | ||||||||||||||||||||||||||||||||||
| def get_system_stats(db: Session = Depends(get_db)): | ||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||
| Get system statistics. | ||||||||||||||||||||||||||||||||||
| Optimized: Uses a single database query with aggregations to avoid multiple aggregate round-trips. | ||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||
| stats = db.query( | ||||||||||||||||||||||||||||||||||
| func.count(User.id).label("total_users"), | ||||||||||||||||||||||||||||||||||
| func.sum(case((User.role == UserRole.ADMIN, 1), else_=0)).label("admin_count"), | ||||||||||||||||||||||||||||||||||
| func.sum(case((User.is_active.is_(True), 1), else_=0)).label("active_users") | ||||||||||||||||||||||||||||||||||
| ).first() | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||
| "total_users": stats.total_users or 0, | ||||||||||||||||||||||||||||||||||
| "admin_count": stats.admin_count or 0, | ||||||||||||||||||||||||||||||||||
| "active_users": stats.active_users or 0, | ||||||||||||||||||||||||||||||||||
| Get system-wide user statistics. | ||||||||||||||||||||||||||||||||||
| Optimized: Uses a single aggregate query to calculate multiple metrics simultaneously, | ||||||||||||||||||||||||||||||||||
| reducing database round-trips and scan overhead. | ||||||||||||||||||||||||||||||||||
|
|
@@ -45,7 +42,7 @@ def get_system_stats(db: Session = Depends(get_db)): | |||||||||||||||||||||||||||||||||
| func.sum(case((User.role == UserRole.ADMIN, 1), else_=0)).label("admins"), | ||||||||||||||||||||||||||||||||||
| func.sum(case((User.is_active.is_(True), 1), else_=0)).label("active") | ||||||||||||||||||||||||||||||||||
| ).first() | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||
| "total_users": stats.total or 0, | ||||||||||||||||||||||||||||||||||
| "admin_count": int(stats.admins or 0), | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -403,43 +403,28 @@ def get_issue_visit_history( | |||||||||
| @router.get("/api/field-officer/visit-stats", response_model=VisitStatsResponse) | ||||||||||
| def get_visit_statistics(db: Session = Depends(get_db)): | ||||||||||
| """ | ||||||||||
| Get aggregate statistics for all field officer visits using optimized SQL queries | ||||||||||
|
|
||||||||||
| Returns metrics like total visits, verification status, geo-fence compliance, etc. | ||||||||||
| Get aggregate statistics for all field officer visits using optimized SQL queries. | ||||||||||
| Optimized: Consolidates 6 individual queries into a single aggregate query to reduce | ||||||||||
| database round-trips and scan overhead. | ||||||||||
| """ | ||||||||||
| try: | ||||||||||
| # Use SQL aggregates instead of loading all visits into memory | ||||||||||
| total_visits = db.query(func.count(FieldOfficerVisit.id)).scalar() or 0 | ||||||||||
|
|
||||||||||
| verified_visits = db.query(func.count(FieldOfficerVisit.id)).filter( | ||||||||||
| FieldOfficerVisit.verified_at.isnot(None) | ||||||||||
| ).scalar() or 0 | ||||||||||
|
|
||||||||||
| within_geofence_count = db.query(func.count(FieldOfficerVisit.id)).filter( | ||||||||||
| FieldOfficerVisit.within_geofence == True | ||||||||||
| ).scalar() or 0 | ||||||||||
|
|
||||||||||
| outside_geofence_count = db.query(func.count(FieldOfficerVisit.id)).filter( | ||||||||||
| FieldOfficerVisit.within_geofence == False | ||||||||||
| ).scalar() or 0 | ||||||||||
|
|
||||||||||
| unique_officers = db.query(func.count(func.distinct(FieldOfficerVisit.officer_email))).scalar() or 0 | ||||||||||
|
|
||||||||||
| average_distance = db.query(func.avg(FieldOfficerVisit.distance_from_site)).filter( | ||||||||||
| FieldOfficerVisit.distance_from_site.isnot(None) | ||||||||||
| ).scalar() | ||||||||||
|
|
||||||||||
| # Round to 2 decimals if not None | ||||||||||
| if average_distance is not None: | ||||||||||
| average_distance = round(float(average_distance), 2) | ||||||||||
| # Performance Boost: Single aggregate query for all metrics | ||||||||||
| stats = db.query( | ||||||||||
| func.count(FieldOfficerVisit.id).label("total"), | ||||||||||
| func.sum(case((FieldOfficerVisit.verified_at.isnot(None), 1), else_=0)).label("verified"), | ||||||||||
| func.sum(case((FieldOfficerVisit.within_geofence == True, 1), else_=0)).label("within"), | ||||||||||
| func.sum(case((FieldOfficerVisit.within_geofence == False, 1), else_=0)).label("outside"), | ||||||||||
|
Comment on lines
+415
to
+416
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # Check if the file exists and examine the context around lines 415-416
if [ -f "backend/routers/field_officer.py" ]; then
echo "=== File found, examining lines 410-420 ==="
sed -n '410,420p' backend/routers/field_officer.py
echo ""
echo "=== Full context of the function (searching for function definition) ==="
grep -n "def.*visit" backend/routers/field_officer.py | head -5
else
echo "File not found"
fiRepository: RohanExploit/VishwaGuru Length of output: 1007 🏁 Script executed: # Check for other boolean comparisons in the same file
echo "=== Searching for similar boolean comparisons in the file ==="
rg "== (True|False)" backend/routers/field_officer.py -nRepository: RohanExploit/VishwaGuru Length of output: 414 🌐 Web query:
💡 Result: Ruff E712 ( What triggers E712if cond == True: # E712
...
if cond == False: # E712
...[2] Preferred fixes
if cond:
...
if not cond:
...[1][2] When an explicit boolean check is actually intendedIf you specifically need to distinguish if cond is True:
...
if cond is False:
...This is also what the rule’s message recommends as an alternative. [2] Sources: Ruff rule list (E712 description) [1], pycodestyle E712 documentation [2]. 🌐 Web query:
💡 Result: In SQLAlchemy, NULL predicates (most common)
stmt = select(User).where(User.deleted_at.is_(None)) # deleted_at IS NULL
stmt = select(User).where(User.deleted_at.is_not(None)) # deleted_at IS NOT NULLNote: Boolean predicates
Typical patterns: from sqlalchemy import true, false
stmt = select(User).where(User.is_active.is_(true())) # IS TRUE
stmt = select(User).where(User.is_active.is_(false())) # IS FALSEYou can also do
|
||||||||||
| func.sum(case((FieldOfficerVisit.within_geofence == True, 1), else_=0)).label("within"), | |
| func.sum(case((FieldOfficerVisit.within_geofence == False, 1), else_=0)).label("outside"), | |
| func.sum(case((FieldOfficerVisit.within_geofence.is_(True), 1), else_=0)).label("within"), | |
| func.sum(case((FieldOfficerVisit.within_geofence.is_(False), 1), else_=0)).label("outside"), |
🧰 Tools
🪛 Ruff (0.15.4)
[error] 415-415: Avoid equality comparisons to True; use FieldOfficerVisit.within_geofence: for truth checks
Replace with FieldOfficerVisit.within_geofence
(E712)
[error] 416-416: Avoid equality comparisons to False; use not FieldOfficerVisit.within_geofence: for false checks
Replace with not FieldOfficerVisit.within_geofence
(E712)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/routers/field_officer.py` around lines 415 - 416, Replace the boolean
comparisons that use == True / == False in the aggregate expressions with
SQLAlchemy boolean predicates: change FieldOfficerVisit.within_geofence == True
to FieldOfficerVisit.within_geofence.is_(True) and
FieldOfficerVisit.within_geofence == False to
FieldOfficerVisit.within_geofence.is_(False) so the
func.sum(case(...)).label("within") and func.sum(case(...)).label("outside") use
.is_(True)/.is_(False) for explicit SQL and to avoid Ruff E712.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| import pytest | ||
| from fastapi.testclient import TestClient | ||
| from backend.main import app | ||
| from backend.database import get_db, Base, engine | ||
| from backend.models import FieldOfficerVisit, User, UserRole | ||
| from sqlalchemy.orm import Session | ||
| from datetime import datetime, timezone | ||
|
|
||
| @pytest.fixture | ||
| def db_session(): | ||
| Base.metadata.create_all(bind=engine) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: The fixture mutates the shared application database engine instead of an isolated test database, so running this test can drop real tables. Prompt for AI agents |
||
| session = Session(bind=engine) | ||
| yield session | ||
| session.close() | ||
| Base.metadata.drop_all(bind=engine) | ||
|
Comment on lines
+4
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't run schema setup/teardown against the application engine. This fixture imports #!/bin/bash
set -euo pipefail
# Expected: this should show an isolated test-only engine/URL, not the shared application engine.
fd '^database\.py$' backend --exec sed -n '1,220p' {}
echo
rg -n "create_engine|sessionmaker|DATABASE_URL|sqlite|postgres" backend tests🤖 Prompt for AI Agents |
||
|
|
||
| @pytest.fixture | ||
| def client(db_session): | ||
| app.dependency_overrides[get_db] = lambda: db_session | ||
| # Mock admin user for stats access | ||
| from backend.dependencies import get_current_admin_user | ||
| app.dependency_overrides[get_current_admin_user] = lambda: User(email="admin@example.com", role=UserRole.ADMIN) | ||
|
|
||
| with TestClient(app) as c: | ||
| yield c | ||
| app.dependency_overrides = {} | ||
|
Comment on lines
+18
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Expected: find a clear/delete/reset hook on the cache, or confirm that tests need to swap in a fresh cache instance.
fd '^cache\.py$' backend --exec sed -n '1,220p' {}
echo
rg -n 'recent_issues_cache|get\("stats"\)|set\([^)]*"stats"' backend testsRepository: RohanExploit/VishwaGuru Length of output: 7438 🏁 Script executed: # Check if test_bolt_stats.py exists and view its contents
fd 'test_bolt_stats.py' tests --exec wc -l {} \;
echo "---"
# View the test file
fd 'test_bolt_stats.py' tests --exec cat -n {} \;Repository: RohanExploit/VishwaGuru Length of output: 5224 🏁 Script executed: # Search for other test files that call /api/stats
rg '"/api/stats"' tests --type pyRepository: RohanExploit/VishwaGuru Length of output: 131 Clear the global stats cache in the fixture. The 🤖 Prompt for AI Agents |
||
|
|
||
| def test_get_visit_statistics(client, db_session): | ||
| # Add some visits | ||
| v1 = FieldOfficerVisit( | ||
| issue_id=1, | ||
| officer_email="off1@example.com", | ||
| officer_name="Officer 1", | ||
|
Comment on lines
+30
to
+33
|
||
| check_in_latitude=19.0, | ||
| check_in_longitude=72.0, | ||
| check_in_time=datetime.now(timezone.utc), | ||
| verified_at=datetime.now(timezone.utc), | ||
| within_geofence=True, | ||
| distance_from_site=10.5, | ||
| status="verified" | ||
| ) | ||
| v2 = FieldOfficerVisit( | ||
| issue_id=2, | ||
| officer_email="off1@example.com", | ||
| officer_name="Officer 1", | ||
| check_in_latitude=19.1, | ||
| check_in_longitude=72.1, | ||
| check_in_time=datetime.now(timezone.utc), | ||
| within_geofence=False, | ||
| distance_from_site=150.0, | ||
| status="checked_in" | ||
| ) | ||
| v3 = FieldOfficerVisit( | ||
| issue_id=3, | ||
| officer_email="off2@example.com", | ||
| officer_name="Officer 2", | ||
| check_in_latitude=19.2, | ||
| check_in_longitude=72.2, | ||
| check_in_time=datetime.now(timezone.utc), | ||
| within_geofence=True, | ||
| distance_from_site=20.0, | ||
| status="checked_in" | ||
| ) | ||
| db_session.add_all([v1, v2, v3]) | ||
| db_session.commit() | ||
|
Comment on lines
+28
to
+65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seed
Suggested fix def test_get_visit_statistics(client, db_session):
- # Add some visits
+ from backend.models import Issue
+
+ i1 = Issue(description="Issue 1", category="Road", status="open")
+ i2 = Issue(description="Issue 2", category="Road", status="open")
+ i3 = Issue(description="Issue 3", category="Road", status="open")
+ db_session.add_all([i1, i2, i3])
+ db_session.flush()
+
v1 = FieldOfficerVisit(
- issue_id=1,
+ issue_id=i1.id,
officer_email="off1@example.com",
officer_name="Officer 1",
check_in_latitude=19.0,
@@
v2 = FieldOfficerVisit(
- issue_id=2,
+ issue_id=i2.id,
officer_email="off1@example.com",
officer_name="Officer 1",
check_in_latitude=19.1,
@@
v3 = FieldOfficerVisit(
- issue_id=3,
+ issue_id=i3.id,
officer_email="off2@example.com",
officer_name="Officer 2",
check_in_latitude=19.2,🤖 Prompt for AI Agents |
||
|
|
||
| response = client.get("/api/field-officer/visit-stats") | ||
| assert response.status_code == 200 | ||
| data = response.json() | ||
|
|
||
| assert data["total_visits"] == 3 | ||
| assert data["verified_visits"] == 1 | ||
| assert data["within_geofence_count"] == 2 | ||
| assert data["outside_geofence_count"] == 1 | ||
| assert data["unique_officers"] == 2 | ||
| # (10.5 + 150.0 + 20.0) / 3 = 180.5 / 3 = 60.1666... -> 60.17 | ||
| assert data["average_distance_from_site"] == 60.17 | ||
|
|
||
| def test_get_system_stats(client, db_session): | ||
| # UserRole.ADMIN is already imported | ||
| u1 = User(email="admin1@example.com", hashed_password="pw", role=UserRole.ADMIN, is_active=True) | ||
| u2 = User(email="user1@example.com", hashed_password="pw", role=UserRole.USER, is_active=True) | ||
| u3 = User(email="user2@example.com", hashed_password="pw", role=UserRole.USER, is_active=False) | ||
|
|
||
| db_session.add_all([u1, u2, u3]) | ||
| db_session.commit() | ||
|
|
||
| response = client.get("/admin/stats") | ||
| assert response.status_code == 200 | ||
| data = response.json() | ||
|
|
||
| assert data["total_users"] == 3 | ||
| assert data["admin_count"] == 1 | ||
| assert data["active_users"] == 2 | ||
|
|
||
| def test_get_utility_stats(client, db_session): | ||
| from backend.models import Issue | ||
| i1 = Issue(description="Issue 1", category="Road", status="resolved") | ||
| i2 = Issue(description="Issue 2", category="Water", status="open") | ||
| i3 = Issue(description="Issue 3", category="Road", status="verified") | ||
|
|
||
| db_session.add_all([i1, i2, i3]) | ||
| db_session.commit() | ||
|
|
||
| response = client.get("/api/stats") | ||
| assert response.status_code == 200 | ||
| data = response.json() | ||
|
|
||
| assert data["total_issues"] == 3 | ||
| assert data["resolved_issues"] == 2 | ||
| assert data["pending_issues"] == 1 | ||
| assert data["issues_by_category"]["Road"] == 2 | ||
| assert data["issues_by_category"]["Water"] == 1 | ||
|
|
||
| def test_get_users_admin(client, db_session): | ||
| u1 = User(email="u1@example.com", hashed_password="pw", role=UserRole.USER, is_active=True) | ||
| db_session.add(u1) | ||
| db_session.commit() | ||
|
|
||
| response = client.get("/admin/users") | ||
| assert response.status_code == 200 | ||
| data = response.json() | ||
| assert len(data) >= 1 | ||
| assert any(u["email"] == "u1@example.com" for u in data) | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: This note references
func.label(), but SQLAlchemy labeling is done with expression.label(...)/ColumnElement.label(). Update the wording to avoid documenting an incorrect API.Prompt for AI agents