mirror of
https://github.com/wasrusgen/zov-tech.git
synced 2026-06-03 19:04:49 +00:00
feat: canonicalize Measurements schema on startup + full column-order repair
_ensure_measurements_sheet() now:
1. Creates sheet with canonical headers if missing
2. Adds any missing columns
3. If column ORDER doesn't match _measurement_columns() — migrates all
data rows in-place: reads by column name, rewrites in canonical order
@app.on_event("startup") calls _ensure_measurements_sheet() via
asyncio.to_thread so column order is always corrected on deploy,
not just on first client_create.
This guarantees append_named_row() always finds columns in expected
positions, eliminating the silent data-in-wrong-column bug.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8318b25999
commit
f64a64e834
@ -47,6 +47,22 @@ app.add_middleware(
|
||||
)
|
||||
|
||||
|
||||
# =================================================================
|
||||
# Startup: канонизация схем таблиц
|
||||
# =================================================================
|
||||
|
||||
@app.on_event("startup")
|
||||
async def _on_startup() -> None:
|
||||
"""При запуске бэкенда канонизируем схему Measurements один раз.
|
||||
Это исправляет рассинхронизацию порядка колонок без ручного вмешательства."""
|
||||
import asyncio
|
||||
try:
|
||||
await asyncio.to_thread(_ensure_measurements_sheet)
|
||||
log.info("Startup: Measurements schema OK")
|
||||
except Exception as e:
|
||||
log.warning("Startup: Measurements schema check failed (non-fatal): %s", e)
|
||||
|
||||
|
||||
# =================================================================
|
||||
# Health & ping
|
||||
# =================================================================
|
||||
@ -906,19 +922,71 @@ def _measurement_columns() -> list[str]:
|
||||
|
||||
|
||||
def _ensure_measurements_sheet() -> None:
|
||||
"""Один раз догоняет схему Measurements — добавляет недостающие колонки."""
|
||||
"""Канонизирует схему Measurements:
|
||||
1. Создаёт лист если отсутствует.
|
||||
2. Добавляет недостающие колонки.
|
||||
3. Если порядок колонок не совпадает с _measurement_columns() —
|
||||
перестраивает лист: читает все данные, переставляет колонки по канону,
|
||||
перезаписывает лист целиком. Данные не теряются.
|
||||
"""
|
||||
want = _measurement_columns()
|
||||
|
||||
# --- Создать лист если не существует ---
|
||||
try:
|
||||
ws = sheets.sheet("Measurements")
|
||||
existing = ws.row_values(1)
|
||||
except Exception:
|
||||
sheets.ensure_sheet("Measurements", _measurement_columns())
|
||||
sheets.ensure_sheet("Measurements", want)
|
||||
log.info("Measurements: создан с каноническим заголовком")
|
||||
return
|
||||
want = _measurement_columns()
|
||||
|
||||
if not existing:
|
||||
ws.update("A1", [want])
|
||||
log.info("Measurements: заголовок установлен (лист был пуст)")
|
||||
return
|
||||
|
||||
# --- Добавить недостающие колонки (без нарушения порядка) ---
|
||||
missing = [c for c in want if c not in existing]
|
||||
if missing:
|
||||
new_headers = existing + missing
|
||||
ws.update("A1", [new_headers])
|
||||
log.info("Measurements: дополнили колонки: %s", missing)
|
||||
# Дописываем только в конец; данные встают в те позиции,
|
||||
# которые append_named_row() потом найдёт по имени
|
||||
ws.update("A1", [existing + missing])
|
||||
existing = existing + missing
|
||||
log.info("Measurements: добавлены колонки %s", missing)
|
||||
|
||||
# --- Канонизация порядка если он не совпадает ---
|
||||
# Берём только колонки из канона (extra-колонки вне канона сохраняем справа)
|
||||
canon_set = set(want)
|
||||
extra = [c for c in existing if c not in canon_set]
|
||||
canonical_order = want + extra # канон + внекановые справа
|
||||
|
||||
if existing == canonical_order:
|
||||
return # уже в правильном порядке — ничего не делаем
|
||||
|
||||
log.info("Measurements: обнаружен неканонический порядок колонок — запускаем миграцию")
|
||||
try:
|
||||
all_rows = ws.get_all_values()
|
||||
if len(all_rows) < 2:
|
||||
# Данных нет — просто переписать заголовок
|
||||
ws.update("A1", [canonical_order])
|
||||
log.info("Measurements: заголовок канонизирован (данных не было)")
|
||||
return
|
||||
|
||||
old_headers = all_rows[0]
|
||||
data_rows = all_rows[1:]
|
||||
|
||||
# Перестраиваем каждую строку: читаем по имени, пишем в канонический порядок
|
||||
new_rows = []
|
||||
for r in data_rows:
|
||||
old_dict = dict(zip(old_headers, r + [""] * max(0, len(old_headers) - len(r))))
|
||||
new_rows.append([old_dict.get(col, "") for col in canonical_order])
|
||||
|
||||
# Перезаписываем лист целиком
|
||||
ws.clear()
|
||||
ws.update("A1", [canonical_order] + new_rows, value_input_option="USER_ENTERED")
|
||||
log.info("Measurements: миграция завершена, %d строк пересортировано", len(new_rows))
|
||||
except Exception as e:
|
||||
log.error("Measurements: ошибка канонизации (данные не тронуты): %s", e)
|
||||
|
||||
|
||||
def _row_for_measurement(measurement_id: str, ts: str, **fields) -> dict[str, str]:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user