From 91adb1e9e880abcca7f425efac686e4fdc1708d3 Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Wed, 30 Sep 2026 17:55:19 +0200 Subject: [PATCH] feat: no add-downstream + on assets nothing in the workspace writes Co-Authored-By: Claude Opus 5.5 (1M context) --- .../assets/AssetGraph/AssetGraphCanvas.svelte | 1 + .../assets/AssetGraph/AssetNode.svelte | 7 +++-- .../lib/components/assets/AssetGraph/types.ts | 3 ++ .../(logged)/pipeline/[folder]/+page.svelte | 30 +++++++++++++++++-- 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/frontend/src/lib/components/assets/AssetGraph/AssetGraphCanvas.svelte b/frontend/src/lib/components/assets/AssetGraph/AssetGraphCanvas.svelte index 1c9482ca81..569592a31d 100644 --- a/frontend/src/lib/components/assets/AssetGraph/AssetGraphCanvas.svelte +++ b/frontend/src/lib/components/assets/AssetGraph/AssetGraphCanvas.svelte @@ -623,6 +623,7 @@ error: a.error, dbt: a.dbt, onAddScript: onAddScriptForAsset, + neverWritten: a.never_written, pathPrefix, defaultPathSuffix, producers: producersByAsset.get(`${a.kind}:${a.path}`) ?? [], diff --git a/frontend/src/lib/components/assets/AssetGraph/AssetNode.svelte b/frontend/src/lib/components/assets/AssetGraph/AssetNode.svelte index 571b25ed28..7e32b6a647 100644 --- a/frontend/src/lib/components/assets/AssetGraph/AssetNode.svelte +++ b/frontend/src/lib/components/assets/AssetGraph/AssetNode.svelte @@ -156,6 +156,8 @@ foldedFixes?: NodeFix[] /** The assets-only node for the scripts that build no asset. */ noAsset?: boolean + /** Nothing in the workspace writes it. */ + neverWritten?: boolean /** Card width in the assets-only view, sized to its text. */ width?: number /** Assets-only view: deletes the producing script and trigger no other @@ -219,8 +221,9 @@ } } - // A red asset needs fixing where its error comes from, not a downstream step. - let showAdd = $derived(data.onAddScript != undefined && !data.error) + // A red asset needs fixing where its error comes from, not a downstream step; + // and a step downstream of what nothing writes would never run on its writes. + let showAdd = $derived(data.onAddScript != undefined && !data.error && !data.neverWritten) let menuItems: Item[] = $derived( data.onDeleteUpstream ? [ diff --git a/frontend/src/lib/components/assets/AssetGraph/types.ts b/frontend/src/lib/components/assets/AssetGraph/types.ts index c7c9f831a9..cf4dd414cb 100644 --- a/frontend/src/lib/components/assets/AssetGraph/types.ts +++ b/frontend/src/lib/components/assets/AssetGraph/types.ts @@ -8,6 +8,9 @@ export interface AssetGraphAssetNode { path: string // Filled in client-side: the node is misconfigured (see `pipelineNodeErrors`). error?: string + // Filled in client-side: nothing in the workspace writes it, so a step + // downstream of it would never run on its writes. + never_written?: boolean // Fork workspaces only: 'fork' when this ducklake asset was materialized in // the fork itself, 'deferred' when reads fall back to the parent workspace's // current table via a defer view. Absent outside forks / for other kinds / diff --git a/frontend/src/routes/(root)/(logged)/pipeline/[folder]/+page.svelte b/frontend/src/routes/(root)/(logged)/pipeline/[folder]/+page.svelte index e2773cd516..41093995c1 100644 --- a/frontend/src/routes/(root)/(logged)/pipeline/[folder]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/pipeline/[folder]/+page.svelte @@ -1705,8 +1705,24 @@ // Assets an asset trigger waits on (explicit `on`, or an auto-triggering read) // that nothing in the pipeline writes, checked against the whole workspace: an // asset trigger fires on a write from anywhere. + // Every other asset nothing in the pipeline writes is looked up too, so an + // asset nothing writes anywhere can go without the "add downstream step" +. + let writtenInPipeline = $derived( + new Set( + shownGraph.edges + .filter((e) => e.access_type === 'w' || e.access_type === 'rw') + .map((e) => `${e.asset_kind}:${e.asset_path}`) + ) + ) let unwrittenInPipeline = $derived( - [...pipelineNodeErrors(shownGraph, explicitOnByPath).unwrittenTriggerAssets] + [ + ...new Set([ + ...pipelineNodeErrors(shownGraph, explicitOnByPath).unwrittenTriggerAssets, + ...shownGraph.assets + .filter((a) => !a.dbt && !writtenInPipeline.has(`${a.kind}:${a.path}`)) + .map((a) => `${a.kind}:${a.path}`) + ]) + ] .sort() .join('\n') ) @@ -1768,8 +1784,16 @@ assets: g.assets .filter((a) => !phantom.has(`${a.kind}:${a.path}`)) .map((a) => { - const error = errors.assets.get(`${a.kind}:${a.path}`) - return error ? { ...a, error } : a + const key = `${a.kind}:${a.path}` + const error = errors.assets.get(key) + // Only once the workspace lookup has answered: before, it is unknown. + const never_written = + !a.dbt && + !writtenInPipeline.has(key) && + !!writtenElsewhere.current && + !writtenElsewhere.loading && + !writtenElsewhere.current.has(key) + return error || never_written ? { ...a, error, never_written } : a }), runnables: g.runnables.map((r) => { const draft = showDrafts ? pe.drafts.get(r.path)?.script : undefined