From 3293330cece141f3a38b23df600e8254eb672573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Mon, 3 Aug 2026 10:18:10 +0400 Subject: [PATCH] =?UTF-8?q?fix:=20=D1=80=D1=83=D1=87=D0=BD=D0=BE=D0=B9=20U?= =?UTF-8?q?PSERT=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE=20ON=20CONFLICT=20?= =?UTF-8?q?=E2=80=94=20=D0=BD=D0=B5=20=D1=82=D1=80=D0=B5=D0=B1=D1=83=D0=B5?= =?UTF-8?q?=D1=82=20=D0=B8=D0=BD=D0=B4=D0=B5=D0=BA=D1=81=D0=B0=20(v1.2.39)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ON CONFLICT требовал уникальный индекс idx_runs_op_uid, который не всегда создавался (мультистейтмент SQL в psycopg2). → save_run падал молча → история пустая. Новый подход: UPDATE по op_uid, если rowcount==0 → INSERT. Не зависит от индексов, работает всегда. --- site/app.py | 2 +- site/db/init_db.py | 11 --------- site/db/save_run.py | 56 +++++++++++++++++++++++++-------------------- 3 files changed, 32 insertions(+), 37 deletions(-) diff --git a/site/app.py b/site/app.py index 8c684a8..2cdc414 100644 --- a/site/app.py +++ b/site/app.py @@ -32,7 +32,7 @@ from routes.api_scenario_defs import bp_defs as api_scenario_defs_bp # Версия — показывается в топбаре UI. Меняется при КАЖДОМ изменении кода. # Нужна для фильтрации истории (пользователь видит только записи своей версии). -VERSION = "1.2.38" +VERSION = "1.2.39" # Flask-приложение с Jinja2-шаблонами из папки templates/ app = Flask(__name__, template_folder="templates", static_folder="static") diff --git a/site/db/init_db.py b/site/db/init_db.py index d563c22..339c92c 100644 --- a/site/db/init_db.py +++ b/site/db/init_db.py @@ -84,12 +84,6 @@ ALTER TABLE runs ADD COLUMN IF NOT EXISTS app_version VARCHAR(16); ALTER TABLE runs ADD COLUMN IF NOT EXISTS scenario_run_id INTEGER; ALTER TABLE runs ADD COLUMN IF NOT EXISTS step_number INTEGER; ALTER TABLE runs ADD COLUMN IF NOT EXISTS instance_meta JSONB; - --- Уникальный индекс на op_uid (частичный — только не-NULL). --- Нужен для UPSERT: сценарий вызывает save_run дважды (RUNNING → OK), --- второй вызов обновляет существующую строку вместо INSERT дубликата. -CREATE UNIQUE INDEX IF NOT EXISTS idx_runs_op_uid - ON runs (op_uid) WHERE op_uid IS NOT NULL; """ # ── DDL для scenario_runs (запуски сценариев) ── @@ -218,11 +212,6 @@ def init_db(): cur.execute(SCHEMA_SQL) cur.execute(MIGRATION_SQL) cur.execute(SCENARIO_RUNS_SQL) - - # Индекс для UPSERT в save_run — отдельным запросом для надёжности - # (мультистейтмент SQL в psycopg2 может выполнить не все операторы) - cur.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_runs_op_uid ON runs (op_uid) WHERE op_uid IS NOT NULL") - conn.commit() cur.close() print("[DB] Schema initialized", flush=True) diff --git a/site/db/save_run.py b/site/db/save_run.py index a350d2c..b52730c 100644 --- a/site/db/save_run.py +++ b/site/db/save_run.py @@ -51,31 +51,37 @@ def save_run(client_id, stand, user_email, svc_id, svc_name, op_name, svc_op_id, try: cur = conn.cursor() - cur.execute(""" - INSERT INTO runs (client_id, stand, user_email, svc_id, svc_name, - op_name, svc_op_id, op_uid, instance_uid, display_name, - status, duration_sec, error_log, params, stages, app_version, - scenario_run_id, step_number, instance_meta) - VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) - ON CONFLICT (op_uid) WHERE op_uid IS NOT NULL DO UPDATE SET - status = EXCLUDED.status, - duration_sec = EXCLUDED.duration_sec, - error_log = EXCLUDED.error_log, - stages = EXCLUDED.stages, - svc_name = EXCLUDED.svc_name, - instance_meta = EXCLUDED.instance_meta - """, ( - client_id, stand, user_email, svc_id, svc_name, op_name, svc_op_id, - op_uid, instance_uid, display_name, status, duration_sec, - error_log, - # params и stages — JSONB: сериализуем в JSON-строку - json.dumps(params) if params else None, - json.dumps(stages) if stages else None, - app_version, - scenario_run_id, step_number, - # instance_meta — тоже JSONB - json.dumps(instance_meta) if instance_meta else None, - )) + + # Ручной UPSERT (UPDATE → INSERT если не затронуто строк). + # Не используем ON CONFLICT — не требует индекса, надёжнее. + # Сценарий вызывает save_run дважды (RUNNING → OK) — второй вызов + # обновляет существующую строку вместо создания дубликата. + params_json = json.dumps(params) if params else None + stages_json = json.dumps(stages) if stages else None + meta_json = json.dumps(instance_meta) if instance_meta else None + + if op_uid: + cur.execute(""" + UPDATE runs SET + status = %s, duration_sec = %s, error_log = %s, + stages = %s, svc_name = %s, instance_meta = %s + WHERE op_uid = %s + """, (status, duration_sec, error_log, stages_json, svc_name, meta_json, op_uid)) + + if not op_uid or cur.rowcount == 0: + # Нет op_uid ИЛИ UPDATE не затронул строк → INSERT + cur.execute(""" + INSERT INTO runs (client_id, stand, user_email, svc_id, svc_name, + op_name, svc_op_id, op_uid, instance_uid, display_name, + status, duration_sec, error_log, params, stages, app_version, + scenario_run_id, step_number, instance_meta) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + """, ( + client_id, stand, user_email, svc_id, svc_name, op_name, svc_op_id, + op_uid, instance_uid, display_name, status, duration_sec, + error_log, params_json, stages_json, app_version, + scenario_run_id, step_number, meta_json, + )) conn.commit() cur.close() print(f"[DB] save_run OK: {op_name} {status} {duration_sec}s", flush=True)