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 __future__ import annotations
from pathlib import Path from pathlib import Path
from urllib.parse import urlencode
from fastapi import APIRouter, FastAPI, HTTPException, Query, Request from fastapi import APIRouter, FastAPI, HTTPException, Query, Request
from fastapi.responses import HTMLResponse, RedirectResponse 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_conn_id = conn.get("default_dest_connection_id")
default_dest_schema = conn.get("default_dest_schema") or "" 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( return _templates.TemplateResponse(
request, request,
"wizard_step3.html", "wizard_step3.html",
@ -349,7 +367,8 @@ def wizard_step3(request: Request,
table_description=table_description, table_description=table_description,
fetch_error=fetch_error, default_module_name=default_module_name, fetch_error=fetch_error, default_module_name=default_module_name,
default_dest_conn_id=default_dest_conn_id, 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 missing = [c["dest_name"] for c in chosen
if c["dest_name"].lower() not in existing_cols] if c["dest_name"].lower() not in existing_cols]
if missing: if missing:
raise HTTPException( back_qs = urlencode(
400, [("source_connection_id", source_connection_id),
f"dest table {qualified_dest} already exists but is missing " ("table", table),
f"columns: {', '.join(missing)}. Drop the table, choose a " ("table_schema", qvals.get("schema") or qvals.get("library") or ""),
f"different dest_table, or align your column picks to match " *qvals.items()])
f"the existing schema.") 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: try:
jrunner.run_dest_sql( 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.ok { border-color: #2f6b35; background: #16261a; color: #b6dcb8; }
.flash.err { border-color: #6b2f2f; background: #261616; color: #dcb6b6; } .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>
</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 %} {% if not fetch_error %}
<form method="post" action="/wizard/create"> <form method="post" action="/wizard/create">
<input type="hidden" name="source_connection_id" value="{{ connection.id }}"> <input type="hidden" name="source_connection_id" value="{{ connection.id }}">