feat(analytics): mv_layout_velocity MV + refresh helper (#113 PR B) #194
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#194
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/113-mv-layout-velocity"
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
PR B (из 4) для Issue #113 Layout Analysis — materialized view + refresh helper. Builds on PR #193 (PR A schemas).
Что в PR
data/sql/94_mv_layout_velocity.sql(NEW, 72 LOC) — MV агрегирующийobjective_corpus_room_month×objective_complex_mappingза last 24 months, grouped by(domrf_obj_id, room_bucket). Cyrillicстудиянормализован к ASCIIstudio(consistency с layout_signature.py PR A).backend/app/services/site_finder/layout_velocity_refresh.py(NEW, 42 LOC) — Python helperrefresh_layout_velocity(db, concurrently=True)для будущего periodic refresh (НЕ scheduled здесь — follow-up issue).Data verification (live на проде через MCP)
objective_corpus_room_monthobjective_complex_mappingSchema surprises (caught by database-expert)
objective_complex_mapping.objective_complex_name(НЕproject_name)report_monthisdate(not timestamp) —::datecast type-safeWITH DATAdefault на CREATE MATERIALIZED VIEW → initial populate implicit, отдельный REFRESH в .sql не нуженUNIQUE INDEX
mv_layout_velocity_pk ON (obj_id, room_bucket)— required дляREFRESH CONCURRENTLY(used by helper). Re-apply migration delete'нет MV → first refresh после re-apply должен бытьconcurrently=False(helper docs это explain).Что НЕ в PR
feat/113-best-layouts-endpoint—POST /api/v1/parcels/{cad_num}/best-layouts(читает из этого MV)feat/113-pdf-tz-and-ui— WeasyPrint PDF ТЗ + frontendBestLayoutsBlock.tsxTest plan
mcp__postgres-gendesign__explain_query)68_schema_objective.sql:311—objective_complex_namePK)68_schema_objective.sql:122confirms.claude/rules/sql.md+.claude/rules/deploy.md)SELECT COUNT(*) FROM mv_layout_velocity≥ 400 на продеCloses part of #113 (full close after PR C/D).
Deep Code Review — verdict
Status: 🟡 MEDIUM (1 🟠 high + 6 🟡 medium, no blockers)
Reviewer: deep-code-reviewer (opus) · Head SHA:
144970a· Lines: +114 / -0🟠 High
data/sql/94_mv_layout_velocity.sql:30,38—DROP MATERIALIZED VIEW IF EXISTS … CASCADE+CREATEбезIF NOT EXISTSнарушает idempotency contract из.claude/rules/sql.md. Re-apply (DR / dev-локал / lost_schema_migrationsentry) снесёт MV + всё что от неё зависит (включая будущий/best-layouts). Pattern изdata/sql/03_materialized_views.sql:5,46,70тот же — accepted в codebase, но именно эта практика создаёт риск.concurrently=False"DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_matviews WHERE matviewname='mv_layout_velocity') THEN CREATE ...; END IF; END $$;— true idempotent🟡 Medium
94_mv_layout_velocity.sql:33-37,57—WITH DATA(default) заставляет initial populate выполниться внутри transaction. Pattern из03_materialized_views.sql:38,64,89используетWITH NO DATA+ отдельныйREFRESHпослеCREATE UNIQUE INDEX(instant index creation на пустой MV, меньше lock duration). Fix: добавитьWITH NO DATA+REFRESH MATERIALIZED VIEW mv_layout_velocity;передCOMMIT;.94_mv_layout_velocity.sql:41-43— JOIN losing rows whereobjective_group != 'Екатеринбург'(тихо). UNIQUE наobjective_complex_mapping(objective_complex_name, objective_group)— сейчас все 114 mapped в ЕКБ, OK. Но при подтяжке Пермь/Челябинск (vault) одинproject_nameможет дать дубли через разныеobjective_group→ Cartesian → следующийREFRESH CONCURRENTLYупадёт сduplicate key. Fix: либоWHERE cm.objective_group = 'Екатеринбург', либо PK на(obj_id, room_bucket, objective_group).94_mv_layout_velocity.sql:34—NOW() - INTERVAL '24 months'без явного scope filter → MV размер "молча" растёт с coverage. ДобавитьCOMMENT ON MATERIALIZED VIEW mv_layout_velocity IS 'last 24 months from refresh time'(mirror52_schema_prinzip_crm.sql:105).backend/app/services/site_finder/layout_velocity_refresh.py:40— отсутствуетdb.commit()послеREFRESH. SQLAlchemy Session под autobegin не commit'ится автоматом;SELECT COUNT(*)на line 40 в той же uncommitted tx может видеть stale данные через pooled другой connection. Fix: явныйdb.commit()илиwith db.begin():.layout_velocity_refresh.py:38— f-string в SQL (text(f"REFRESH ... {mode} ...")). Не SQL-injection (mode controlled), но нарушает букву.claude/rules/backend.md. Fix:if concurrently: db.execute(text("REFRESH MATERIALIZED VIEW CONCURRENTLY mv_layout_velocity"))/else: db.execute(text("REFRESH MATERIALIZED VIEW mv_layout_velocity")).layout_velocity_refresh.py:42—REFRESH CONCURRENTLYпадает если MV не populated. Docstring это объясняет, но explicittry/except OperationalErrorс fallback на non-concurrent безопаснее.🟢 Low / nits
94_mv_layout_velocity.sql:5—U+2009thin space "19 738 rows" — обычный пробел для greplayout_velocity_refresh.py:1-10— docstring без указания где caller (Celery beat? admin endpoint?)backend/tests/test_layout_velocity_mv.py(untracked вgit status) — убедись что не попадёт случайно в следующий PRCross-file impact
'studio'(PR Alayout_signature.py:10), но existingvelocity.py:229,analytics_queries.py:1582,VelocityBlock.tsx:6,test_velocity.py:271— всё ещё Cyrillic'студия'. PR C UI для/best-layoutsMUST использовать'studio', не re-useBUCKET_ORDERизVelocityBlock. Intentional divergence (не bug), но нуженdecisions/2026-05-16-layout-mv-studio-normalisation.mdв vault.Performance findings
EXPLAIN ANALYZE (live на проде через MCP):
Positive observations
mv_layout_velocity_pkкорректен для CONCURRENTLYBEGIN; COMMIT;обёртка, sequential94_*.sqlномер, type casts корректныBlast radius
DROP MVчерез MCP)Next steps
data/sql/**+backend/services/**) — human merge required даже после fixesDeep Code Review — re-review verdict (head
46c2a84)Summary
144970a(1 🟠 + 6 🟡)data/sql/94_mv_layout_velocity.sql, P1:backend/app/services/site_finder/layout_velocity_refresh.py)data/sql/**+backend/app/services/**Fix verification vs previous review
46c2a8494_mv_layout_velocity.sql:23-29— explicitWARN:comment block: re-apply risk +_schema_migrationsmitigation + reminder про concurrently=False для first refresh после re-apply94_mv_layout_velocity.sql:71—WITH NO DATA, indexes created на пустой MV (instant),REFRESH MATERIALIZED VIEWпосле индексов94_mv_layout_velocity.sql:67—AND cm.objective_group = 'Екатеринбург'+ комментарий "защита от cross-region Cartesian при future multi-city"94_mv_layout_velocity.sql:86-89— sources + refresh механизмlayout_velocity_refresh.py:42, 51—db.commit()после обеих REFRESH веток +db.rollback()перед fallbacklayout_velocity_refresh.py:39-42— explicitif concurrently:/else:branches, две статичныхtext()literallayout_velocity_refresh.py:43-52—except OperationalError+ substring match"cannot refresh materialized view"+ warning log + fallback на non-concurrentAll 7 prior items addressed. No new issues introduced.
Live EXPLAIN (re-verified на проде)
Plan with new
WHERE cm.objective_group = 'Екатеринбург':objective_groupfilter — no-op today (1 group exists, 114 EKB rows, 0 nulls) — pure future-proofing.Positive observations
WITH NO DATA+ post-indexREFRESHpattern correctly minimises lock duration during initial CREATE.db.rollback()before retry — no transaction state leak.OperationalErrorsubstring match on"cannot refresh materialized view"— slightly fragile (locale-dependent), but Postgres emits this in English regardless oflc_messagesfor SQLSTATE 55000. Acceptable.'Екатеринбург'hardcoded literal — defensible scope-limited choice for MVP (EKB-only), matches existing data.Action
✅ APPROVE verdict, но auto-merge BLOCKED per scope policy (touches
data/sql/**+backend/app/services/**).@lekss361 — approved, needs human merge per scope policy. После merge: deploy.yml auto-apply migration → qa-tester smoke:
SELECT COUNT(*) FROM mv_layout_velocity≥ 400.Risk / blast radius
_schema_migrations(с safety comment как fallback documentation).DROP MATERIALIZED VIEW IF EXISTS mv_layout_velocity CASCADE;(no data loss, source tables untouched).