fix(tradein-estimator): restore CAST in IS NOT NULL predicates (revert audit #9, prod fix) #518
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#518
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/tradein-estimator-restore-cast"
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?
🚨 Production fix
PR #509 broke
/api/v1/trade-in/estimatewhenevertarget_house_typeis None (ortarget_yearis None — same risk).Stack trace (prod 2026-05-24 ~12:25)
$4istarget_house_type. psycopg3 prepared-statement plan step needs the bind type to evaluateIS NOT NULLitself — theCAST(... AS text)in the THEN branch is not enough because it never gets reached during planning.Root cause
PR #509 removed
CAST(:target_year AS integer)andCAST(:target_house_type AS text)from the 4IS NOT NULLpredicates in_fetch_analogs, citing audit finding #9 (perf). The audit's concern was a per-row CAST cost on ≤300 rows — negligible. The inline comment at lines 814–816 ofestimator.pyalready documented why those CASTs were load-bearing:PR #509 misread the audit; this PR reverts that single piece. THEN-branch CASTs untouched (always present). Other PR #509 changes (IMV atomic link, batched yandex history) stay in place — unrelated.
Diff
Exactly 4 lines edited (×2 in SELECT relevance_score, ×2 in row_number OVER ORDER BY):
WHEN :target_year IS NOT NULL→WHEN CAST(:target_year AS integer) IS NOT NULLWHEN :target_house_type IS NOT NULL→WHEN CAST(:target_house_type AS text) IS NOT NULLVerification
:target_year IS NOT NULL/:target_house_type IS NOT NULLleft (grep).Test plan
/estimatewithhouse_type=nullfrom frontend; expect 200 OK with estimate result.Deep Code Review — verdict
Summary
backend/app/services/estimator.py)Correctness 🎯 (the load-bearing point)
The stack trace
psycopg.errors.AmbiguousParameter: could not determine data type of parameter $4 / LINE 24: WHEN $4 IS NOT NULLis exactly the failure mode that bare:name IS NOT NULLproduces in psycopg v3 prepared-statement plans.$Nto a Postgres OID. A bare$N IS NOT NULLpredicate provides no type context — when the Python value isNone, the driver cannot infer it, plan fails before any row is evaluated.CAST(NULL AS integer) IS NOT NULL→FALSE, same asNULL IS NOT NULL→FALSE(PostgresIS NOT NULLon a typed NULL is well-defined). No boolean-eval regression.Performance ⚡
No concern. The CAST is on a constant bind parameter (not a column), planner constant-folds it once; per-row cost is zero. The audit's perf concern was misapplied — it would only matter if CAST were applied to
year_built/house_typecolumns.Project conventions 📋
Matches
.claude/rules/backend.md:The revert restores compliance.
Verification
:target_year IS NOT NULL, 0 bare:target_house_type IS NOT NULL, 2CAST(:target_year AS integer) IS NOT NULL, 2CAST(:target_house_type AS text) IS NOT NULL✅_fetch_analogsSQL touched; IMV / yandex history changes from PR #509 untouched ✅int | None/str | None— types match CAST targets ✅Cross-file impact
None — change is confined to the SQL string literal inside
_fetch_analogs. Same call sites, same param names, same param values. Behavior identical when both params are non-NULL (regression-safe).Recommended follow-up (non-blocking)
Add a regression test in
tradein-mvp/backend/tests/test_estimator.py(or equivalent): call_fetch_analogs(..., year_built=None, house_type=None)against a real DB and assert it does not raiseAmbiguousParameter. Cheapest insurance against PR #509-style re-occurrence.Complexity / blast radius
Approved. Merging.