feat(sf-b1): GET /parcels/by-bbox — map entry endpoint + parcel_user_status table #336
No reviewers
Labels
No labels
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
Fable 5 ревью
feedback/max
generative
GG-форсайт
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
вторичка
ИРД
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#336
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/sf-b1-bbox-entry"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
SF Wave 1 final / Group A / sub-task B1. NEW bbox-search для карты.
Files
backend/app/api/v1/parcels.py—GET /by-bbox(placed BEFORE/{cad_num}/...wildcard)backend/app/schemas/parcel.py—ParcelMapMarker,ParcelBboxResponsebackend/tests/api/v1/test_parcel_by_bbox.py— 4 unit tests (mock DB)data/sql/119_parcel_user_status.sql— migrationEndpoint
GET /api/v1/parcels/by-bbox?min_lat&min_lon&max_lat&max_lon&limit=200&user_id?=...Returns parcels внутри bbox + overlay status из
parcel_user_status(если user_id передан).PostGIS:
ST_Intersects(geom, ST_MakeEnvelope(...)). psycopg v3 safeCAST(:x AS float).Migration
119_parcel_user_status.sqlTest plan
GET /by-bbox?min_lat=56.80&min_lon=60.60&max_lat=56.90&max_lon=60.70→ 200 + parcels[]?user_id=...overlay's statusPart of plan vault
code/patterns/SiteFinder_Backend_Migration_Plan_May17.md§B1. Last Wave 1 backend task.Deep Code Review — verdict: APPROVE
Summary
57676d5299Critical concerns — all CLEAR
1. Route ordering (the #1 risk for this PR). Verified in
backend/app/api/v1/parcels.py:@router.get("/by-bbox")) right after_INLINE_FETCH_POLL_INTERVAL_Sand before@router.post("/search")(was 1008, now ~1098)./{parcel_id},/{cad_num}/fetch-status,/{cad_num}/isochrones) are declared after the new endpoint → FastAPI will match/by-bboxto the bbox handler, not ascad_num="by-bbox".2. PostGIS argument order + SRID.
ST_MakeEnvelope(xmin, ymin, xmax, ymax, srid)expects lon-first. Code:xmin=lon, ymin=lat, xmax=lon, ymax=lat → correct. Envelope SRID 4326 matches
cad_parcels.geom GEOMETRY(Polygon, 4326)(data/sql/92_cad_bulk_layers.sql:76). GIST indexcad_parcels_geom_gistexists (data/sql/93_cad_parcels_geom_multipolygon.sql:49–50) →ST_Intersectswill use it.3. psycopg v3 / SQL safety. All bind-params use
CAST(:x AS type)— no:x::type(cast trap clean). No f-string SQL, no string concat, nopsycopg2/requests/print(). Parametrization correct.Migration
data/sql/119_parcel_user_status.sqlCREATE TABLE IF NOT EXISTS,CREATE INDEX IF NOT EXISTS— idempotent.(user_id, cad_num)coversWHERE user_id = ? AND cad_num = ?(the LEFT JOIN predicate) via leftmost-prefix → JOIN will be index-driven.(cad_num)index is appropriate for the LEFT JOIN fromcad_parcelsside (driving table side filters by bbox → join probes by cad_num).(user_id, status)index is fine for future "list my favorites" queries.statusenum,NOT NULLcolumns,updated_atdefault — all clean.Findings (non-blocking)
MEDIUM — Test mocking strategy. Tests use
patch("app.api.v1.parcels.get_db", return_value=iter([mock_db])). FastAPI resolvesDepends(get_db)via the DI container, not by re-importing the symbol — the proper pattern isapp.dependency_overrides[get_db] = lambda: mock_db. As written, the mock is likely bypassed and either: (a) hits real DB if connection string set, or (b) fails on Session init. Test #2 (invalid-bbox 400) does work because the early-return happens before DB use, but tests #1, #3, #4 may not actually exercise the SQL shape they claim. Recommend fix in follow-up PR — not a blocker since SQL is reviewed independently and the endpoint logic is straightforward.LOW — No bbox area cap.
bbox_area_km2is computed and returned but not validated. A request spanning all of Russia (~17M km²) would still execute, relying only on GIST +LIMIT 1000to bound result size. With GIST index this is bounded in practice, but a sanity rejection (e.g.if bbox_area_km2 > 10_000: raise 400) would be defensive. SF Wave 1 internal map — not a real DoS surface yet. Defer.LOW —
area_m2 DESC NULLS LASTordering inside large bbox. With huge bbox + LIMIT 200, Postgres will scan via GIST then sort byarea_m2(no covering index). For dense regions this means top-K sort over ~5–50K candidates — acceptable but worth EXPLAIN ANALYZE once real data load is in.Positive observations
/{cad_num}/...wildcards).lat_midcorrection inbbox_area_km2math (cos(radians(lat_mid))) — proper spherical approximation.Query(ge=-90, le=90)/Query(ge=-180, le=180)— FastAPI-level bounds validation for free.status=Nonewhenuser_idnot provided — protects against accidental cross-user leak even if SQL returns a row.limit ≤ 1000, default 200 — sane default.last_analysis_dateplaceholder clearly documented with reference to B2 (#307).Cross-file impact
parcels.pyimport block (ParcelBboxResponse,ParcelMapMarker).mathalready imported (line 4 ofparcels.py) — no missing import.Verdict rationale (3 sentences)
/by-bboxis registered before all wildcard GETs, so FastAPI will not match it ascad_num="by-bbox".ST_MakeEnvelope(xmin, ymin, xmax, ymax), SRID 4326 matches the geom column, and the existingcad_parcels_geom_gistindex will serveST_Intersects.Merging via squash.