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 cd8431f6fb18e672d21c291be195d5951d8b9c05
Parent: 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-cospan
6 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 
cospan · schematic version control on atproto built on AT Protocol