fix(translate): исправление INSERT + выгрузка CSV для неуспешных запусков

**Исправления ошибок:**
- superset_executor: убран несуществующий fallback endpoint /api/v1/sql_lab/execute/
  (правильный URL /api/v1/sqllab/execute/ подтверждён браузерным запросом Superset)
  Ошибка 500 (permission denied) больше не маскируется 404
- orchestrator_run_completion: run.status=FAILED при провале insert
  (раньше всегда ставился COMPLETED). timeout тоже treated as failure
- test_orchestrator: assertion обновлён COMPLETED→FAILED

**Новая функция — выгрузка CSV:**
- GET /api/translate/runs/{run_id}/failed.csv — возвращает CSV с успешно
  переведёнными, но не вставленными записями (status=SUCCESS)
- Колонки: record_id, source_*, target_sql, source_data (JSON), языки, error_message
- Метаданные запуска (run_status, insert_status, run_error) в конце файла
- Кнопки скачивания в 3 местах фронта:
  - История запусков (при status=FAILED)
  - TranslationRunProgress (insert_failed)
  - TranslationRunResult (failed/partial/insert_failed)
- downloadFailedCsv() через fetchApiBlob + Blob URL
This commit is contained in:
2026-06-01 22:38:52 +03:00
parent 40e364d9f6
commit 28b28338c5
9 changed files with 243 additions and 43 deletions

View File

@@ -420,7 +420,7 @@ class TestTranslationOrchestrator:
# endregion test_execute_run_no_job
# region test_execute_run_insert_failure [TYPE Function]
# @BRIEF SQL generation/insert returns failure, run still completes but with error logged.
# @BRIEF SQL generation/insert returns failure run marked FAILED with error logged.
def test_execute_run_insert_failure(self, mock_job: MagicMock) -> None:
db = MagicMock()
config_manager = MagicMock()
@@ -464,7 +464,7 @@ class TestTranslationOrchestrator:
patch.object(engine, 'event_log'):
result = orch.execute_run(run)
assert result.status == "COMPLETED"
assert result.status == "FAILED"
assert result.insert_status == "failed"
assert "timeout" in (result.error_message or "")

View File

@@ -106,9 +106,17 @@ def complete_success(
run.insert_status = insert_result.get("status")
run.superset_execution_id = str(insert_result.get("query_id") or "")
run.superset_execution_log = insert_result
if run.status != "FAILED":
# Treat "failed" and "timeout" as terminal failures.
# "skipped" (no records) and "success" are non-failure outcomes.
if insert_result.get("status") in ("failed", "timeout"):
run.status = "FAILED"
if insert_result.get("error_message"):
run.error_message = insert_result["error_message"]
elif not run.error_message:
run.error_message = f"SQL insert phase {insert_result.get('status')}"
elif run.status != "FAILED":
run.status = "COMPLETED"
if insert_result.get("error_message"):
if insert_result.get("error_message") and not run.error_message:
run.error_message = insert_result["error_message"]
run.completed_at = datetime.now(UTC)
db.flush()

View File

@@ -173,36 +173,30 @@ class SupersetSqlLabExecutor:
"client_id": f"trl-{uuid.uuid4().hex[:7]}", # max 11 chars for Superset varchar(11)
}
# Try multiple SQL Lab endpoints — some Superset versions use /sqllab/execute/,
# others use /sql_lab/execute/ (with underscore)
candidate_endpoints = ["/api/v1/sqllab/execute/", "/api/v1/sql_lab/execute/"]
response = None
last_error = None
for endpoint in candidate_endpoints:
try:
response = client.network.request(
method="POST",
endpoint=endpoint,
data=json.dumps(payload),
headers={"Content-Type": "application/json"},
)
if isinstance(response, dict) and response:
logger.reason("SQL Lab endpoint succeeded", extra={
"endpoint": endpoint,
})
break
except Exception as ep_err:
last_error = ep_err
logger.explore("SQL Lab endpoint failed, trying next", extra={
"error": str(ep_err),
"endpoint": endpoint,
})
response = None
if response is None:
error_msg = f"All SQL Lab endpoints failed. Last error: {last_error}"
logger.explore("SQL Lab execute failed", extra={"error": error_msg})
raise ValueError(f"Superset SQL Lab execute failed: {error_msg}")
# Use the canonical Superset SQL Lab execute endpoint.
# NOTE: Some older docs reference /sql_lab/execute/ (with underscore),
# but this endpoint does NOT exist in current Superset versions.
# The browser UI uses /api/v1/sqllab/execute/ — confirmed by
# fetch() calls in Superset SQL Lab.
endpoint = "/api/v1/sqllab/execute/"
try:
response = client.network.request(
method="POST",
endpoint=endpoint,
data=json.dumps(payload),
headers={"Content-Type": "application/json"},
)
logger.reason("SQL Lab endpoint succeeded", extra={
"endpoint": endpoint,
})
except Exception as ep_err:
logger.explore("SQL Lab execute failed", extra={
"error": str(ep_err),
"endpoint": endpoint,
})
raise ValueError(
f"Superset SQL Lab execute failed: {ep_err}"
) from ep_err
# Parse response — try multiple known Superset response formats
result = response if isinstance(response, dict) else {}