fix: ensure parent repo exists before inserting child records During backfill, issues/pulls/labels/etc. can arrive before their parent repo. Instead of dropping FK constraints, upsert a stub repo row (ON CONFLICT DO NOTHING) before each child insert. The stub is overwritten when the real repo record arrives. Also fix "schema-aware" → "schematic" terminology.
Author: Aaron Steven White
Commit
cd8431f6fb18e672d21c291be195d5951d8b9c05Parent: af6fdf00d7
Structural diff unavailable
These commits were pushed via plain git push, so no pre-parsed
schemas are available. Install git-remote-cospan and re-push via panproto:// to
see scope-level changes, breaking change detection, and semantic diffs.
brew install panproto/tap/git-remote-cospan6 files changed +63 -9
@@ -83,7 +83,7 @@
8383 8484 <!-- Description --> 8585 <p class="mt-6 max-w-lg text-[15px] leading-relaxed text-caption"> 86- Structural diffs, schema-aware merges, and algebraic validation powered by panproto. Built on AT Protocol. 86+ Structural diffs, schematic merges, and algebraic validation powered by panproto. Built on AT Protocol. 8787 </p> 8888 8989 </div>
@@ -74,7 +74,7 @@
7474 href="{basePath}/import" 7575 class="rounded-md border border-accent bg-accent/10 px-3 py-1.5 text-xs font-medium text-accent transition-colors hover:bg-accent/20" 7676 > 77- Import to Cospan 77+ Fork to Cospan 7878 </a> 7979 {/if} 8080 <ForkButton repoDid={data.repo.did} repoName={data.repo.name} />
@@ -55,14 +55,14 @@
5555 </script> 5656 5757 <svelte:head> 58- <title>Import from Tangled · Cospan</title> 58+ <title>Fork from Tangled · Cospan</title> 5959 </svelte:head> 6060 6161 <section> 6262 <div class="mb-5 flex items-end justify-between"> 6363 <div> 64- <h1 class="mb-1 text-lg font-semibold text-ink">Import from Tangled</h1> 65- <p class="text-[13px] text-caption">Bring repositories into Cospan for schema-aware version control.</p> 64+ <h1 class="mb-1 text-lg font-semibold text-ink">Fork from Tangled</h1> 65+ <p class="text-[13px] text-caption">Fork Tangled repositories into Cospan for schematic version control.</p> 6666 </div> 6767 {#if !auth.authenticated} 6868 <span class="text-[12px] text-ghost">Sign in to import your repos</span>
@@ -111,6 +111,29 @@ pub async fn list_recent(
111111 } 112112 } 113113 114+/// Insert a stub repo row if one doesn't already exist for (did, name). 115+/// Used during backfill when child records (issues, pulls, etc.) arrive 116+/// before their parent repo. The stub will be overwritten with full data 117+/// when the repo record itself is processed. 118+pub async fn ensure_exists( 119+ pool: &PgPool, 120+ did: &str, 121+ name: &str, 122+ source: &str, 123+) -> Result<(), sqlx::Error> { 124+ sqlx::query( 125+ "INSERT INTO repos (did, rkey, name, source, created_at) \ 126+ VALUES ($1, '', $2, $3, NOW()) \ 127+ ON CONFLICT (did, name) DO NOTHING", 128+ ) 129+ .bind(did) 130+ .bind(name) 131+ .bind(source) 132+ .execute(pool) 133+ .await?; 134+ Ok(()) 135+} 136+ 114137 pub async fn search( 115138 pool: &PgPool, 116139 query: &str,
@@ -116,6 +116,8 @@ async fn dispatch_special_upsert(
116116 serde_json::from_value(transform_record(state, collection, rec))?; 117117 row.rkey = rkey.to_string(); 118118 row.indexed_at = Utc::now(); 119+ let source = if collection.starts_with("sh.tangled.") { "tangled" } else { "cospan" }; 120+ db::repo::ensure_exists(&state.db, &row.repo_did, &row.repo_name, source).await?; 119121 db::ref_update::upsert(&state.db, &row).await?; 120122 121123 let _ = state.event_tx.send(IndexEvent::RefUpdate {
@@ -135,6 +137,8 @@ async fn dispatch_special_upsert(
135137 row.did = did.to_string(); 136138 row.rkey = rkey.to_string(); 137139 row.indexed_at = Utc::now(); 140+ let source = if collection.starts_with("sh.tangled.") { "tangled" } else { "cospan" }; 141+ db::repo::ensure_exists(&state.db, &row.repo_did, &row.repo_name, source).await?; 138142 db::issue::upsert(&state.db, &row).await?; 139143 140144 let _ = state.event_tx.send(IndexEvent::IssueCreated {
@@ -232,6 +236,8 @@ async fn dispatch_special_upsert(
232236 row.did = did.to_string(); 233237 row.rkey = rkey.to_string(); 234238 row.indexed_at = Utc::now(); 239+ let source = if collection.starts_with("sh.tangled.") { "tangled" } else { "cospan" }; 240+ db::repo::ensure_exists(&state.db, &row.repo_did, &row.repo_name, source).await?; 235241 db::pull::upsert(&state.db, &row).await?; 236242 237243 let _ = state.event_tx.send(IndexEvent::PullCreated {
@@ -343,6 +349,8 @@ async fn dispatch_special_upsert(
343349 row.did = did.to_string(); 344350 row.rkey = rkey.to_string(); 345351 row.indexed_at = Utc::now(); 352+ let source = if collection.starts_with("sh.tangled.") { "tangled" } else { "cospan" }; 353+ db::repo::ensure_exists(&state.db, &row.repo_did, &row.repo_name, source).await?; 346354 db::pipeline::upsert(&state.db, &row).await?; 347355 } 348356
@@ -385,6 +393,7 @@ async fn dispatch_special_upsert(
385393 row.did = did.to_string(); 386394 row.rkey = rkey.to_string(); 387395 row.indexed_at = Utc::now(); 396+ db::repo::ensure_exists(&state.db, &row.repo_did, &row.repo_name, "tangled").await?; 388397 db::label::upsert(&state.db, &row).await?; 389398 } 390399