Wizard: warn in-UI when default dest table already exists.

Previously the existing-dest check fired on submit and surfaced as a raw
JSON 400. Now step 3 introspects the default dest up front and renders a
yellow banner listing existing columns; submit-time mismatches render
wizard_error.html (409) with missing vs. existing side-by-side and a back
link that re-plumbs the form qvals.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Paul Trowbridge 2026-04-25 13:27:23 -04:00
parent bb0b493d18
commit f18ea55a12
4 changed files with 105 additions and 7 deletions

View File

@ -13,6 +13,7 @@ wizard, editors, and SSE-driven live run watch come next.
from __future__ import annotations
from pathlib import Path
from urllib.parse import urlencode
from fastapi import APIRouter, FastAPI, HTTPException, Query, Request
from fastapi.responses import HTMLResponse, RedirectResponse
@ -340,6 +341,23 @@ def wizard_step3(request: Request,
default_dest_conn_id = conn.get("default_dest_connection_id")
default_dest_schema = conn.get("default_dest_schema") or ""
# Proactive warning — if the default dest table already exists, surface
# its columns now so the user can align picks before submit.
dest_warn: dict | None = None
if not fetch_error and default_dest_conn_id and default_dest_schema:
dest_conn_row = repo.get_connection(default_dest_conn_id)
if dest_conn_row is not None:
try:
existing = _existing_dest_columns(
dest_conn_row, default_dest_schema, default_module_name)
except jrunner.JrunnerError:
existing = None
if existing is not None:
dest_warn = {
"qualified": f"{default_dest_schema}.{default_module_name}",
"columns": sorted(existing),
}
return _templates.TemplateResponse(
request,
"wizard_step3.html",
@ -349,7 +367,8 @@ def wizard_step3(request: Request,
table_description=table_description,
fetch_error=fetch_error, default_module_name=default_module_name,
default_dest_conn_id=default_dest_conn_id,
default_dest_schema=default_dest_schema),
default_dest_schema=default_dest_schema,
dest_warn=dest_warn),
)
@ -448,12 +467,23 @@ async def wizard_create(request: Request):
missing = [c["dest_name"] for c in chosen
if c["dest_name"].lower() not in existing_cols]
if missing:
raise HTTPException(
400,
f"dest table {qualified_dest} already exists but is missing "
f"columns: {', '.join(missing)}. Drop the table, choose a "
f"different dest_table, or align your column picks to match "
f"the existing schema.")
back_qs = urlencode(
[("source_connection_id", source_connection_id),
("table", table),
("table_schema", qvals.get("schema") or qvals.get("library") or ""),
*qvals.items()])
return _templates.TemplateResponse(
request,
"wizard_error.html",
_ctx(
title="Dest table column mismatch",
qualified_dest=qualified_dest,
missing=missing,
existing=sorted(existing_cols),
back_qs=back_qs,
),
status_code=409,
)
try:
jrunner.run_dest_sql(

View File

@ -277,3 +277,4 @@ table.picker tbody tr:hover td { background: #1c2128; }
}
.flash.ok { border-color: #2f6b35; background: #16261a; color: #b6dcb8; }
.flash.err { border-color: #6b2f2f; background: #261616; color: #dcb6b6; }
.flash.warn { border-color: #6b5a2f; background: #261f16; color: #e1c98a; }

View File

@ -0,0 +1,57 @@
{% extends "base.html" %}
{% set section = "modules" %}
{% block title %}New module — error{% endblock %}
{% block content %}
<div class="panel">
<header>
Wizard error
<span class="subtitle">{{ title }}</span>
<span style="margin-left:auto">
<a href="/wizard/columns?{{ back_qs }}">&larr; back to step 3</a>
</span>
</header>
<div class="body">
<div class="flash err">
Dest table <code>{{ qualified_dest }}</code> already exists, but your
picks don't match its schema. Drop the table, choose a different
dest table, or align your column picks (dest names) with the
existing columns below.
</div>
<div class="two-col">
<div class="panel">
<header>
Missing from existing table
<span class="subtitle">{{ missing|length }}</span>
</header>
<div class="body tight">
<table class="grid">
<tbody>
{% for m in missing %}
<tr><td class="mono">{{ m }}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="panel">
<header>
Columns in existing dest
<span class="subtitle">{{ existing|length }}</span>
</header>
<div class="body tight">
<table class="grid">
<tbody>
{% for c in existing %}
<tr><td class="mono">{{ c }}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -18,6 +18,16 @@
</div>
</div>
{% if dest_warn %}
<div class="flash warn">
Dest table <code>{{ dest_warn.qualified }}</code> already exists on the
default destination. If you proceed, pipekit will <strong>not</strong> drop
or recreate it — your picks below (dest names) must match the existing
columns, or the create will fail. Existing columns:
<span class="mono">{{ dest_warn.columns|join(', ') }}</span>
</div>
{% endif %}
{% if not fetch_error %}
<form method="post" action="/wizard/create">
<input type="hidden" name="source_connection_id" value="{{ connection.id }}">