Some checks failed
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m55s
CI / backend-tests (pull_request) Failing after 17m20s
Задача формулировала «импорт каждый день рапортует done с total_seen=0». Проверка на проде показала другое: импорт исправен — ежедневно вычитывает все 96 974 строки FDW-источника и честно их пропускает (rows_fetched = rows_skipped = 96974), а `total_seen` — поле админ-витрины, не счётчик импорта. Источник gendesign.rosreestr_deals стоял на Q1 2026 (загружен 30.04), хотя Q2 2026 опубликован Росреестром 10.07 и poll заметил его 14.08 (available=1). Оба сторожа — poll и deals_freshness_monitor — сработали и семь событий ушли в GlitchTip, где 0 правил / 0 адресатов / 0 отправок. Корень, которого в задаче не было: rosreestr_deals партиционирована по period_start_date, партиции созданы списком в 01_schema «2024 Q3 — 2026 Q1», и ничто новые не создаёт. Загрузка Q2 21.08 упала: ERROR: no partition of relation "rosreestr_deals" found for row DETAIL: (period_start_date) = (2026-04-01) То есть даже оператор, запустив загрузчик по подсказке poll, получил бы отказ. Это и объясняет, почему poll сделан «только сообщить». Что сделано: • миграция 193 — партиции Q2, Q3, Q4 2026 с запасом, идемпотентно, с lock_timeout; индексы наследуются от родителя (проверено: 4 на 2026q2); • JOBS загрузчика — 2026Q2–Q4 (квартал без CSV честно SKIP); • ловушка set -e в загрузчике: resolve_csv сигналит «файла нет» кодом 1, и первый же квартал без CSV молча ронял ВЕСЬ прогон до строки SKIP — на проде с одним Q2-файлом скрипт завершался rc=0, не напечатав ни строки. `|| true` на вызове; после правки боевой прогон на VPS: 12 кварталов, 2026Q2 «уже загружен (741874 строк)», остальные SKIP, rc=0; • тест-сторож горизонта: партиция обязана существовать на последний публикуемый квартал (+20 дней лага после конца квартала; Q2 2026 вышел 10.07) и на следующий — чтобы предупреждение приходило за квартал до отказа, а не в день публикации. Читает pg_inherits живого Postgres. Красная сторона воспроизводима на проде, где миграция уже применена: DETACH партиции в откатываемой транзакции → головная краснеет по значению («нет партиции на квартал 2026-04-01»), откат возвращает партицию (проверено: 12 партиций после теста). Без БД — skip с причиной, в allowlist; календарный тест идёт везде. Сам Q2 загружен на прод по штатному пути: 741 874 строки в rosreestr_deals (ЕКБ-фильтр 13 654), import-rosreestr.sh → tradein.deals +11 649 сделок, max(deal_date) 2026-01-01 → 2026-04-01. deals_freshness_monitor на следующем тике: alert 0, latest_quarter 2. pytest backend/tests/sql — 55 passed (через туннель к проду). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
165 lines
8.2 KiB
Bash
165 lines
8.2 KiB
Bash
#!/usr/bin/env bash
|
||
# Loads quarters of dataset_СДЕЛКИ into rosreestr_deals via staging (see JOBS below).
|
||
# Q3 2024 uses ';' separator, all later quarters use '~'.
|
||
#
|
||
# Usage:
|
||
# bash 02_load_all_quarters.sh # local test container (gd_test_pg)
|
||
# PG_HOST=localhost PG_PORT=15432 PGPASSWORD=... bash 02_load_all_quarters.sh # remote (via SSH tunnel)
|
||
set -euo pipefail
|
||
|
||
if [[ -n "${PG_HOST:-}" ]]; then
|
||
# Remote / TCP mode (e.g. prod via SSH tunnel)
|
||
PG_PORT="${PG_PORT:-5432}"
|
||
PG_USER="${PG_USER:-gendesign}"
|
||
PG_DB="${PG_DB:-gendesign}"
|
||
PSQL="docker run --rm -i -e PGPASSWORD postgres:16-alpine psql -h host.docker.internal -p $PG_PORT -U $PG_USER -d $PG_DB -v ON_ERROR_STOP=1"
|
||
PSQL_PIPE="docker run --rm -i -e PGPASSWORD postgres:16-alpine psql -h host.docker.internal -p $PG_PORT -U $PG_USER -d $PG_DB -v ON_ERROR_STOP=1"
|
||
else
|
||
# Local docker exec into the test container
|
||
PG_CONTAINER="${PG_CONTAINER:-gd_test_pg}"
|
||
PSQL="docker exec -i $PG_CONTAINER psql -U gendesign -d gendesign -v ON_ERROR_STOP=1"
|
||
PSQL_PIPE="$PSQL"
|
||
fi
|
||
|
||
cd "$(dirname "$0")/../raw"
|
||
|
||
# Имя CSV-файла внутри zip может варьироваться (rosreestr менял схему в Q4 2024).
|
||
# Скрипт автоматически пробует обе именные конвенции.
|
||
# Если файла нет — квартал пропускается с warning (не fail). Это позволяет
|
||
# догружать 2023 / 2024Q1-Q2 / новые кварталы постепенно по мере выкачки.
|
||
declare -a JOBS=(
|
||
# Формат CSV изменился в Q4 2024 (delim ; → ~, имя r_all → r-r_01-92).
|
||
# 2024Q1+Q2 уже опубликованы по новой схеме (r-r_01-92_y_YYYY_q_N), но
|
||
# delimiter мог не успеть перейти — попробуй сначала ~, при ошибках смени на ;.
|
||
# 2023 в виде квартальных CSV НЕ ПУБЛИКУЕТСЯ (архив до 2023г. содержит только
|
||
# JSON-снимки от ноября-декабря 2021 в /data-sets/Архив до 2023г. включительно/).
|
||
"2024Q1:2024-01-01:dataset_СДЕЛКИ_r-r_01-92_y_2024_q_1.csv:;"
|
||
"2024Q2:2024-04-01:dataset_СДЕЛКИ_r-r_01-92_y_2024_q_2.csv:;"
|
||
"2024Q3:2024-07-01:dataset_СДЕЛКИ_r_all_q_3.csv:;"
|
||
"2024Q4:2024-10-01:dataset_СДЕЛКИ_r-r_01-92_y_2024_q_4.csv:~"
|
||
"2025Q1:2025-01-01:dataset_СДЕЛКИ_r-r_01-92_y_2025_q_1.csv:~"
|
||
"2025Q2:2025-04-01:dataset_СДЕЛКИ_r-r_01-92_y_2025_q_2.csv:~"
|
||
"2025Q3:2025-07-01:dataset_СДЕЛКИ_r-r_01-92_y_2025_q_3.csv:~"
|
||
"2025Q4:2025-10-01:dataset_СДЕЛКИ_r-r_01-92_y_2025_q_4.csv:~"
|
||
"2026Q1:2026-01-01:dataset_СДЕЛКИ_r-r_01-92_y_2026_q_1.csv:~"
|
||
# #2998: партиции под Q2–Q4 2026 — миграция 193. Загрузчик скипает квартал, для
|
||
# которого нет CSV в data/raw/, поэтому строки на ещё не опубликованные кварталы
|
||
# безопасны: они ждут файла, а не падают.
|
||
"2026Q2:2026-04-01:dataset_СДЕЛКИ_r-r_01-92_y_2026_q_2.csv:~"
|
||
"2026Q3:2026-07-01:dataset_СДЕЛКИ_r-r_01-92_y_2026_q_3.csv:~"
|
||
"2026Q4:2026-10-01:dataset_СДЕЛКИ_r-r_01-92_y_2026_q_4.csv:~"
|
||
)
|
||
|
||
# Возможные имена zip-ов в data/raw/ (можно класть как есть, скрипт распакует).
|
||
# Имя в data/raw/ — sdelki_YYYYqN.csv.zip ИЛИ оригинальное dataset_СДЕЛКИ_*.csv.zip.
|
||
|
||
resolve_csv() {
|
||
# Принимает comma-separated список кандидатов имён csv. Возвращает первый
|
||
# существующий (распакованный) файл. Если ничего нет — пытается распаковать
|
||
# из zip с известными именами. Возвращает пустую строку если файл не найден.
|
||
local CANDIDATES="$1"
|
||
local SRC_Q="$2"
|
||
IFS=',' read -ra NAMES <<< "$CANDIDATES"
|
||
for name in "${NAMES[@]}"; do
|
||
if [[ -f "$name" ]]; then
|
||
echo "$name"
|
||
return 0
|
||
fi
|
||
done
|
||
# Try unzip from sdelki_YYYYqN.csv.zip (lowercase quarter).
|
||
local LOWER=$(echo "$SRC_Q" | tr '[:upper:]' '[:lower:]')
|
||
local ZIP_LOCAL="sdelki_${LOWER}.csv.zip"
|
||
if [[ -f "$ZIP_LOCAL" ]]; then
|
||
unzip -o -q "$ZIP_LOCAL" >&2 || true
|
||
for name in "${NAMES[@]}"; do
|
||
if [[ -f "$name" ]]; then echo "$name"; return 0; fi
|
||
done
|
||
fi
|
||
# Try original rosreestr-named zips.
|
||
for name in "${NAMES[@]}"; do
|
||
if [[ -f "${name}.zip" ]]; then
|
||
unzip -o -q "${name}.zip" >&2 || true
|
||
[[ -f "$name" ]] && { echo "$name"; return 0; }
|
||
fi
|
||
done
|
||
echo ""
|
||
return 1
|
||
}
|
||
|
||
for job in "${JOBS[@]}"; do
|
||
IFS=':' read -r SRC_Q PERIOD FILE_CANDIDATES SEP <<< "$job"
|
||
# `|| true` обязателен (#2998): resolve_csv сигналит «файла нет» кодом 1, а скрипт
|
||
# идёт под `set -e` — без этого первый же квартал без CSV в data/raw/ молча ронял
|
||
# ВЕСЬ прогон до строки SKIP, и до реально лежащего файла (например, одного Q2 на
|
||
# VPS) загрузчик не доходил никогда. Проверено на проде 21.08: с одним Q2-файлом в
|
||
# каталоге скрипт завершался с rc=0, не напечатав ни одной строки.
|
||
FILE=$(resolve_csv "$FILE_CANDIDATES" "$SRC_Q" || true)
|
||
if [[ -z "$FILE" ]]; then
|
||
echo "=== $SRC_Q SKIP — нет CSV в data/raw/ (искал: $FILE_CANDIDATES; sdelki_${SRC_Q,,}.csv.zip)"
|
||
continue
|
||
fi
|
||
# Idempotency: пропускаем если уже загружен с тем же source_quarter.
|
||
EXISTING=$($PSQL -t -A -c "SELECT COUNT(*) FROM rosreestr_deals WHERE source_quarter = '$SRC_Q'" || echo 0)
|
||
if [[ "${EXISTING:-0}" -gt 0 ]]; then
|
||
echo "=== $SRC_Q SKIP — уже загружен ($EXISTING строк). Удалить: DELETE FROM rosreestr_deals WHERE source_quarter='$SRC_Q'"
|
||
continue
|
||
fi
|
||
echo "=== $SRC_Q ($FILE, sep='$SEP') ==="
|
||
$PSQL -c "TRUNCATE rosreestr_deals_staging;"
|
||
start=$(date +%s)
|
||
cat "$FILE" | $PSQL -c "\\copy rosreestr_deals_staging FROM stdin (FORMAT csv, HEADER true, DELIMITER '$SEP', QUOTE '\"', NULL '')"
|
||
$PSQL <<SQL
|
||
INSERT INTO rosreestr_deals (
|
||
source_quarter, period_start_date,
|
||
region_code, okato, district, city, quarter_cad_number, street,
|
||
realestate_type_code, wall_material_code, purpose_code,
|
||
year_build, floor, area,
|
||
doc_type, deal_price, currency, deal_count
|
||
)
|
||
SELECT
|
||
'$SRC_Q', period_start_date,
|
||
region_code, NULLIF(okato, '-'), NULLIF(district, ''), NULLIF(city, ''),
|
||
quarter_cad_number, NULLIF(street, ''),
|
||
NULLIF(realestate_type_code, ''),
|
||
NULLIF(wall_material_code, ''),
|
||
NULLIF(purpose_code, ''),
|
||
-- year_build can contain garbage like '09.07.1988' — keep only plausible 4-digit years
|
||
CASE WHEN year_build ~ '^[12][0-9]{3}$' THEN year_build::INT ELSE NULL END,
|
||
NULLIF(floor, ''),
|
||
-- discard absurd area/price (data-quality outliers from source)
|
||
CASE WHEN area > 0 AND area < 1e8 THEN area ELSE NULL END,
|
||
NULLIF(doc_type, ''),
|
||
CASE WHEN deal_price > 0 AND deal_price < 1e15 THEN deal_price ELSE NULL END,
|
||
COALESCE(NULLIF(currency, ''), 'рубль'),
|
||
COALESCE(deal_count, 1)
|
||
FROM rosreestr_deals_staging
|
||
WHERE region_code IS NOT NULL
|
||
AND quarter_cad_number IS NOT NULL
|
||
AND quarter_cad_number <> ''
|
||
AND doc_type IS NOT NULL
|
||
AND doc_type <> ''
|
||
AND period_start_date IS NOT NULL;
|
||
SQL
|
||
end=$(date +%s)
|
||
echo " loaded in $((end-start))s"
|
||
done
|
||
|
||
$PSQL <<'SQL'
|
||
SELECT
|
||
source_quarter,
|
||
COUNT(*) AS rows_loaded,
|
||
SUM(deal_count) AS deals,
|
||
SUM(deal_count) FILTER (WHERE region_code = 66) AS sverdl_deals,
|
||
pg_size_pretty(pg_relation_size('rosreestr_deals_'||lower(source_quarter))) AS partition_size
|
||
FROM rosreestr_deals
|
||
GROUP BY source_quarter
|
||
ORDER BY source_quarter;
|
||
|
||
SELECT
|
||
'TOTAL' AS quarter,
|
||
COUNT(*) AS rows,
|
||
SUM(deal_count) AS deals,
|
||
SUM(deal_count) FILTER (WHERE region_code = 66) AS sverdl_deals,
|
||
pg_size_pretty(pg_total_relation_size('rosreestr_deals')) AS total_size
|
||
FROM rosreestr_deals;
|
||
SQL
|