1010from razorback .benchmarks .spider2_dbt import eval_spec as _eval_spec_mod
1111from razorback .benchmarks .spider2_dbt import verify as _verify_mod
1212from razorback .benchmarks .spider2_dbt .preflight import (
13+ _read_dbt_source_tables ,
14+ _read_duckdb_tables ,
1315 preflight_script_text ,
1416 resolve_spider2_db_name ,
1517)
4042# structural divergence from ade-bench's `project/`.
4143_DBT_PROJECT_DIRNAME = "dbt_project"
4244
43- _BUILD_CONTEXT_MARKER = (
44- "# Razorback: land spider2-dbt project + source DuckDB at /app before agent runtime."
45- )
45+ _BUILD_CONTEXT_MARKER = "# Razorback: land spider2-dbt project + source DuckDB at /app before agent runtime."
4646_DBT_DEPS_LAYER_MARKER = (
4747 "# Razorback: install declared dbt packages before agent runtime."
4848)
@@ -88,6 +88,9 @@ def materialize_spider2_harbor_task_view(
8888 exclude_globs = SPIDER2_DBT_DENY_GLOBS ,
8989 view_mode = view_mode ,
9090 )
91+ _repair_missing_spider2_source_tables (
92+ view , source_task_dir = Path (source_task_dir ), task_slug = task_slug
93+ )
9194 # RIDER (Codex finding 2): stage dbt_project/ (incl. the source .duckdb)
9295 # into the build context and COPY it to /app BEFORE the preflight RUN, so
9396 # the preflight `--workspace /app` can never fail on a missing project.
@@ -114,6 +117,92 @@ def _copy_into_view(src: Path, dst: Path) -> None:
114117 shutil .copy2 (src , dst )
115118
116119
120+ def _repair_missing_spider2_source_tables (
121+ view_dir : Path , * , source_task_dir : Path , task_slug : str
122+ ) -> None :
123+ """Hydrate missing referenced source tables from Spider2's gold DuckDB.
124+
125+ Some upstream Spider2 DBT examples ship a source DuckDB that lacks raw
126+ source tables declared and referenced by project models, while the per-task
127+ gold DuckDB still contains those raw relations alongside expected outputs.
128+ Copy only the referenced dbt source relations into the agent-facing source
129+ DB; never copy arbitrary gold tables or scorer condition tables.
130+ """
131+ project_dir = _dbt_project_dir (view_dir )
132+ if project_dir is None :
133+ return
134+
135+ db_name = resolve_spider2_db_name (project_dir , task_slug = task_slug )
136+ source_db = project_dir / f"{ db_name } .duckdb"
137+ if not source_db .is_file ():
138+ return
139+
140+ required_tables = _read_dbt_source_tables (project_dir )
141+ if not required_tables :
142+ return
143+
144+ observed_tables = _read_duckdb_tables (source_db )
145+ missing_tables = required_tables - observed_tables
146+ if not missing_tables :
147+ return
148+
149+ gold_db = _source_gold_db (source_task_dir )
150+ if gold_db is None :
151+ return
152+
153+ gold_tables = _read_duckdb_tables (gold_db )
154+ repairable = sorted (missing_tables & gold_tables )
155+ if not repairable :
156+ return
157+
158+ import duckdb
159+
160+ if source_db .is_symlink ():
161+ source_db_target = source_db .resolve (strict = True )
162+ source_db .unlink ()
163+ shutil .copy2 (source_db_target , source_db )
164+
165+ conn = duckdb .connect (str (source_db ))
166+ try :
167+ conn .execute (
168+ f"ATTACH { _sql_string (str (gold_db ))} AS razorback_gold (READ_ONLY)"
169+ )
170+ for schema , table in repairable :
171+ conn .execute (f"CREATE SCHEMA IF NOT EXISTS { _quote_ident (schema )} " )
172+ conn .execute (
173+ "CREATE TABLE "
174+ f"{ _quote_ident (schema )} .{ _quote_ident (table )} AS "
175+ "SELECT * FROM "
176+ f"razorback_gold.{ _quote_ident (schema )} .{ _quote_ident (table )} "
177+ )
178+ finally :
179+ conn .close ()
180+
181+
182+ def _source_gold_db (source_task_dir : Path ) -> Path | None :
183+ source_gold = Path (source_task_dir ) / "tests" / "gold"
184+ source_spec = source_gold / "spider2_eval.jsonl"
185+ if not source_spec .is_file ():
186+ return None
187+
188+ try :
189+ spec = _eval_spec_mod .load_eval_spec (source_spec )
190+ except Exception :
191+ return None
192+
193+ gold_basename = spec .gold or "gold.duckdb"
194+ source_gold_db = source_gold / gold_basename
195+ return source_gold_db if source_gold_db .is_file () else None
196+
197+
198+ def _quote_ident (value : str ) -> str :
199+ return '"' + value .replace ('"' , '""' ) + '"'
200+
201+
202+ def _sql_string (value : str ) -> str :
203+ return "'" + value .replace ("'" , "''" ) + "'"
204+
205+
117206def _ensure_verifier_assets (
118207 view_dir : Path , * , source_task_dir : Path , task_slug : str
119208) -> None :
@@ -193,9 +282,7 @@ def _ensure_verifier_assets(
193282 gold_db = shlex .quote (f"/tests/{ gold_basename } " ),
194283 )
195284 )
196- test_sh .chmod (
197- test_sh .stat ().st_mode | stat .S_IXUSR | stat .S_IXGRP | stat .S_IXOTH
198- )
285+ test_sh .chmod (test_sh .stat ().st_mode | stat .S_IXUSR | stat .S_IXGRP | stat .S_IXOTH )
199286
200287 # The deny-glob reflection strips gold FILES but leaves the now-empty
201288 # `gold/` directory behind. Prune empty `gold/`-named dirs so no `gold/`
@@ -233,12 +320,9 @@ def _dbt_project_dir(view_dir: Path) -> Path | None:
233320
234321
235322def _has_dbt_packages_manifest (view_dir : Path ) -> bool :
236- return (
237- (view_dir / _DBT_PROJECT_DIRNAME / "packages.yml" ).is_file ()
238- or (
239- view_dir / "environment" / _DBT_PROJECT_DIRNAME / "packages.yml"
240- ).is_file ()
241- )
323+ return (view_dir / _DBT_PROJECT_DIRNAME / "packages.yml" ).is_file () or (
324+ view_dir / "environment" / _DBT_PROJECT_DIRNAME / "packages.yml"
325+ ).is_file ()
242326
243327
244328def _ensure_spider2_build_context_layer (view_dir : Path ) -> None :
@@ -313,9 +397,7 @@ def _ensure_dbt_deps_image_layer(view_dir: Path) -> None:
313397 dockerfile .write_text (_insert_before_final_cmd (text , block ))
314398
315399
316- def _ensure_workspace_preflight_image_layer (
317- view_dir : Path , * , task_slug : str
318- ) -> None :
400+ def _ensure_workspace_preflight_image_layer (view_dir : Path , * , task_slug : str ) -> None :
319401 """Validate the source DuckDB at build time, before the agent runs.
320402
321403 Gated on `_has_dbt_project`: spider2-dbt has no task families, so the
0 commit comments