gendesign/data/sql/77_nspd_geo_jobs.sql
lekss361 3919a40c49 refactor(nspd-geo): unify on rosreestr2coord v5, drop legacy column
Объединяю несколько связанных изменений вокруг NSPD geo bulk-fetcher:

Adapt to rosreestr2coord v5 API:
- nspd_lite.fetch_via_rosreestr2coord: drop `delay` kwarg from Area()
  (removed upstream in v5); keep it in our function signature for
  backward-compat, comment why.
- nspd_geo worker: add explicit time.sleep(rate_ms/1000) after lib-branch
  fetch — in v4 the library throttled internally via delay, in v5
  rate limiting is the caller's job. Без этого получали Area.__init__()
  unexpected kwarg `delay` на каждом target.

Drop use_rosreestr2coord switch:
- Removed urllib-vs-lib choice everywhere. We always use community
  rosreestr2coord library — авторы регулярно обновляют WAF-tricks,
  наш urllib-fetcher (fetch_geoportal) уже неактуален.
- admin_scrape.py: Pydantic schema, INSERT, SELECT, API response
  cleaned of `use_rosreestr2coord`.
- nspd_geo.enqueue_geo_job: param dropped, INSERT shrunk.
- worker process loop: dropped `if use_lib:` branch + import of
  fetch_geoportal.
- frontend/geo/page.tsx: removed checkbox + GeoJob.use_rosreestr2coord
  field + POST body field.

DB column drop:
- data/sql/78_drop_use_rosreestr2coord.sql (NEW):
  DROP COLUMN nspd_geo_jobs.use_rosreestr2coord + CREATE OR REPLACE
  VIEW v_scrape_runs_unified (которая depended on the column).
- data/sql/77_nspd_geo_jobs.sql: cleaned historical DDL for fresh setups.
- Migration applied to prod (in-conversation via postgres MCP).

Frontend polish:
- Thematic ID changed from free-form number input to labeled select
  (1=parcel / 2=quarter / 4=admin / 5=building / 7=zone / 15=complex).
- Auto-sync thematic_id from Job kind on change (override possible).
- ScrapeLogsPanel: extended union type with "nspd_geo" + fixed
  /admin/scrape/geo to pass scraperType="nspd_geo" (was "nspd",
  filtering empty legacy nspd_scrape_log table; real logs live in
  nspd_geo_log via v_scrape_log_unified).

Verified: ruff ✓, tsc --noEmit ✓, migration ran (BEGIN..COMMIT clean).

Deploy order safe: prod column уже удалена → новый backend код, который
не INSERT'ит use_rosreestr2coord, совпадёт со схемой после deploy.
2026-05-11 14:13:33 +03:00

149 lines
7.7 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- Resume-friendly NSPD geo backfill jobs.
--
-- Зачем:
-- Bulk-загрузка кадастровых данных по 100k+ cad-номеров занимает часы.
-- При redeploy worker'а Celery теряет in-memory state. Решение —
-- хранить очередь и текущий прогресс в БД, после редеплоя
-- worker_ready hook подхватывает незавершённые jobs.
--
-- Архитектура:
-- nspd_geo_jobs — общий журнал заданий (один job = одна bulk-операция)
-- nspd_geo_targets — список cad-номеров для обработки (1 строка = 1 cad)
-- со статусом (pending/done/failed) — это и есть resume-state.
--
-- При redeploy:
-- worker_ready signal → SELECT FROM nspd_geo_jobs WHERE status='running'
-- → re-enqueue Celery task с тем же job_id
-- → task SELECT FROM nspd_geo_targets WHERE job_id=:id AND status='pending'
-- → продолжает с той точки где остановился (idempotent UPSERT в cad_quarters_geom)
--
-- Применять через mcp postgres execute_sql или psql direct.
CREATE TABLE IF NOT EXISTS nspd_geo_jobs (
job_id BIGSERIAL PRIMARY KEY,
name TEXT, -- человекочитаемое имя ('УрФО expansion 2026-Q2')
job_kind TEXT NOT NULL, -- 'quarters' | 'parcels' | 'buildings' | 'mixed'
-- Источник целей
source_kind TEXT NOT NULL, -- 'rosreestr_pending' | 'manual_list' | 'region_bbox'
source_params jsonb, -- что-то вроде {"region_codes":[66,74,72,59], "thematic_id":2}
-- Параметры fetch (всегда через rosreestr2coord lib v5+;
-- legacy колонка `use_rosreestr2coord` удалена миграцией 78_).
rate_ms INTEGER NOT NULL DEFAULT 600,
-- Состояние
status TEXT NOT NULL DEFAULT 'queued', -- queued | running | done | failed | paused
started_at timestamptz,
finished_at timestamptz,
heartbeat_at timestamptz,
-- Счётчики
targets_total INTEGER NOT NULL DEFAULT 0,
targets_done INTEGER NOT NULL DEFAULT 0,
targets_failed INTEGER NOT NULL DEFAULT 0,
targets_skipped INTEGER NOT NULL DEFAULT 0, -- уже было в БД (idempotent skip)
requests_count INTEGER NOT NULL DEFAULT 0,
waf_blocked_count INTEGER NOT NULL DEFAULT 0,
-- Audit
error TEXT,
triggered_by TEXT NOT NULL DEFAULT 'manual',-- manual | beat | resume
created_at timestamptz NOT NULL DEFAULT NOW(),
updated_at timestamptz NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS nspd_geo_jobs_status_idx
ON nspd_geo_jobs(status, created_at DESC);
CREATE INDEX IF NOT EXISTS nspd_geo_jobs_running_idx
ON nspd_geo_jobs(heartbeat_at DESC) WHERE status = 'running';
COMMENT ON TABLE nspd_geo_jobs IS
'Журнал bulk geo-jobs для NSPD-fetcher. Resume-friendly: при redeploy '
'worker_ready hook подхватывает status=running с устаревшим heartbeat.';
-- ── Targets (cad-номера к обработке) ─────────────────────────────────────────
CREATE TABLE IF NOT EXISTS nspd_geo_targets (
target_id BIGSERIAL PRIMARY KEY,
job_id BIGINT NOT NULL REFERENCES nspd_geo_jobs(job_id) ON DELETE CASCADE,
cad_num TEXT NOT NULL, -- '66:41:0204016' для квартала, '...:10' для участка
thematic_id SMALLINT NOT NULL, -- 1=parcel | 2=quarter | 5=building
status TEXT NOT NULL DEFAULT 'pending', -- pending | done | failed | skipped
attempts SMALLINT NOT NULL DEFAULT 0,
-- Результат
result_features INTEGER, -- сколько features вернулось
saved_to_table TEXT, -- 'cad_quarters_geom' | 'cad_buildings' | ...
error_msg TEXT,
fetched_at timestamptz,
-- Avoid duplicate work внутри одного job
UNIQUE (job_id, cad_num, thematic_id)
);
CREATE INDEX IF NOT EXISTS nspd_geo_targets_pending_idx
ON nspd_geo_targets(job_id, status) WHERE status = 'pending';
CREATE INDEX IF NOT EXISTS nspd_geo_targets_failed_idx
ON nspd_geo_targets(job_id) WHERE status = 'failed';
COMMENT ON TABLE nspd_geo_targets IS
'Список cad-номеров для обработки в рамках job. Resume-state: '
'task просто SELECT WHERE status=pending и продолжает.';
-- ── nspd_geo_log: логи (видны через v_scrape_log_unified) ───────────────────
CREATE TABLE IF NOT EXISTS nspd_geo_log (
log_id BIGSERIAL PRIMARY KEY,
job_id BIGINT REFERENCES nspd_geo_jobs(job_id) ON DELETE CASCADE,
ts timestamptz NOT NULL DEFAULT NOW(),
level TEXT NOT NULL DEFAULT 'info',
stage TEXT,
cad_num TEXT,
message TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS nspd_geo_log_job_idx
ON nspd_geo_log(job_id, log_id DESC);
CREATE INDEX IF NOT EXISTS nspd_geo_log_ts_idx
ON nspd_geo_log(ts DESC);
-- ── Расширение unified-views на nspd_geo (UNION ALL) ────────────────────────
CREATE OR REPLACE VIEW v_scrape_runs_unified AS
SELECT 'kn' AS scraper_type, run_id, started_at, finished_at, heartbeat_at,
status, error, NULL::text AS triggered_by,
region_codes::text AS scope, requests_count,
objects_count AS items_ok, NULL::integer AS items_failed,
flats_count AS sub_items_ok,
progress_obj_index AS progress_idx, total_obj_count AS progress_total,
jsonb_build_object('developers', developer_ids, 'snapshot_date', snapshot_date,
'resumed_from', resumed_from_run_id, 'params', params) AS extra
FROM kn_scrape_runs
UNION ALL
SELECT 'nspd', run_id, started_at, finished_at, heartbeat_at, status, error,
triggered_by, region_code::text, requests_count,
quarters_ok, quarters_failed, buildings_ok,
NULL, pending_count,
jsonb_build_object('waf_429_count', waf_429_count)
FROM nspd_scrape_runs
UNION ALL
SELECT 'objective', run_id, started_at, finished_at, heartbeat_at, status, error,
triggered_by, group_name, requests_count,
reports_ok, reports_failed, rows_lots,
NULL, NULL,
jsonb_build_object('rows_corpus_room', rows_corpus_room, 'rows_history', rows_history)
FROM objective_scrape_runs
UNION ALL
SELECT 'nspd_geo', job_id, started_at, finished_at, heartbeat_at, status, error,
triggered_by, COALESCE(name, job_kind),
requests_count, targets_done, targets_failed, targets_skipped,
targets_done, targets_total,
jsonb_build_object('job_kind', job_kind, 'source_kind', source_kind,
'rate_ms', rate_ms,
'waf_blocked_count', waf_blocked_count,
'source_params', source_params)
FROM nspd_geo_jobs;
CREATE OR REPLACE VIEW v_scrape_log_unified AS
SELECT 'kn' AS scraper_type, log_id, run_id, ts, level, stage,
obj_id::text AS entity_id, message
FROM kn_scrape_log
UNION ALL
SELECT 'nspd', log_id, run_id, ts, level, stage, cad_number, message
FROM nspd_scrape_log
UNION ALL
SELECT 'nspd_geo', log_id, job_id AS run_id, ts, level, stage, cad_num, message
FROM nspd_geo_log;