fix(apply): preserve project name on a name-less re-apply
`upsert_project_tx` defaulted a missing `[project].name` to the slug and then `ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name` wrote that fabricated value unconditionally — so a re-apply from a clone whose manifest omits `name` silently clobbered the stored display name back to the slug (visible in `pic projects ls` / `pic groups ls`). Bind the raw optional name once and guard both sides on it: `VALUES ($1, COALESCE($2, $1), $3)` (first apply falls back to the slug for the NOT NULL column) and `DO UPDATE SET name = COALESCE($2, projects.name)` (a re-apply updates the name only when the manifest actually declares one). An omitted optional field now preserves persisted data instead of mutating it. Pinned by a new `reapply_without_name_preserves_project_name` journey. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3430,22 +3430,26 @@ fn validate_project_slug(slug: &str) -> Result<(), ApplyError> {
|
||||
}
|
||||
|
||||
/// Register (or fetch) a project by slug inside the apply tx, returning its id.
|
||||
/// First apply with a new slug inserts; a re-apply updates the display name
|
||||
/// (the manifest is authoritative) but preserves the original `created_by`.
|
||||
/// First apply with a new slug inserts, falling back to the slug when the
|
||||
/// manifest omits `name` (the column is NOT NULL). A re-apply updates the
|
||||
/// display name ONLY when the manifest actually declares one — a name-less
|
||||
/// `[project]` block (e.g. a clone scaffolded without a name) preserves the
|
||||
/// previously-set name and `created_by` rather than clobbering the name back
|
||||
/// to the slug. `$2` (the optional declared name) is bound once and referenced
|
||||
/// in both the INSERT fallback and the ON CONFLICT guard.
|
||||
async fn upsert_project_tx(
|
||||
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
decl: &ProjectDecl,
|
||||
actor: AdminUserId,
|
||||
) -> Result<ProjectId, ApplyError> {
|
||||
validate_project_slug(&decl.slug)?;
|
||||
let name = decl.name.clone().unwrap_or_else(|| decl.slug.clone());
|
||||
let (id,): (uuid::Uuid,) = sqlx::query_as(
|
||||
"INSERT INTO projects (slug, name, created_by) VALUES ($1, $2, $3) \
|
||||
ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name \
|
||||
"INSERT INTO projects (slug, name, created_by) VALUES ($1, COALESCE($2, $1), $3) \
|
||||
ON CONFLICT (slug) DO UPDATE SET name = COALESCE($2, projects.name) \
|
||||
RETURNING id",
|
||||
)
|
||||
.bind(&decl.slug)
|
||||
.bind(&name)
|
||||
.bind(decl.name.clone())
|
||||
.bind(actor.into_inner())
|
||||
.fetch_one(&mut **tx)
|
||||
.await
|
||||
|
||||
Reference in New Issue
Block a user