mirror of
https://github.com/wasrusgen/zov-tech.git
synced 2026-06-03 20:24: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
|
# Health & ping
|
||||||
# =================================================================
|
# =================================================================
|
||||||
@ -906,19 +922,71 @@ def _measurement_columns() -> list[str]:
|
|||||||
|
|
||||||
|
|
||||||
def _ensure_measurements_sheet() -> None:
|
def _ensure_measurements_sheet() -> None:
|
||||||
"""Один раз догоняет схему Measurements — добавляет недостающие колонки."""
|
"""Канонизирует схему Measurements:
|
||||||
|
1. Создаёт лист если отсутствует.
|
||||||
|
2. Добавляет недостающие колонки.
|
||||||
|
3. Если порядок колонок не совпадает с _measurement_columns() —
|
||||||
|
перестраивает лист: читает все данные, переставляет колонки по канону,
|
||||||
|
перезаписывает лист целиком. Данные не теряются.
|
||||||
|
"""
|
||||||
|
want = _measurement_columns()
|
||||||
|
|
||||||
|
# --- Создать лист если не существует ---
|
||||||
try:
|
try:
|
||||||
ws = sheets.sheet("Measurements")
|
ws = sheets.sheet("Measurements")
|
||||||
existing = ws.row_values(1)
|
existing = ws.row_values(1)
|
||||||
except Exception:
|
except Exception:
|
||||||
sheets.ensure_sheet("Measurements", _measurement_columns())
|
sheets.ensure_sheet("Measurements", want)
|
||||||
|
log.info("Measurements: создан с каноническим заголовком")
|
||||||
return
|
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]
|
missing = [c for c in want if c not in existing]
|
||||||
if missing:
|
if missing:
|
||||||
new_headers = existing + missing
|
# Дописываем только в конец; данные встают в те позиции,
|
||||||
ws.update("A1", [new_headers])
|
# которые append_named_row() потом найдёт по имени
|
||||||
log.info("Measurements: дополнили колонки: %s", missing)
|
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]:
|
def _row_for_measurement(measurement_id: str, ts: str, **fields) -> dict[str, str]:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user