# I4 — SQL-kilde på MAF-siden (plan) TDD (Iron Law: ingen produksjonskode uten rød test først). Ett `ingest.py` endres; ny konnektor + dispatch + typet §5-rendering. Ren stdlib-tillegg (`os`, `sqlite3`, `contextlib.closing`, `urllib.parse.quote`) → MAF-fri-guarden urørt. ## Steg ### S1 — Røde tester (skriv FØR impl) - `tests/test_ingest_sql.py` (konnektor + `materialize`-dispatch): - env-var usatt → `IngestError`; env peker på manglende fil → `IngestError`. - typet rendering via tmp sqlite-db: INTEGER→decimal, REAL→`repr`, TEXT→verbatim, NULL→`""`. - `float` integral (`4200.0`) → `"4200.0"` (bevart, ikke `"4200"`). - BLOB → `IngestError` (never silent coercion); `bool`-guard (konstruér via `SELECT`? sqlite gir int — dekk `bytes` som «other»; bool-guarden er defensiv, dekkes med en direkte `_sql_value_to_text(True)`-kall-assert). - `max_rows` overskredet → `IngestError`. - read-only (A2): skrive-query mot ro-connection → `IngestError`. - happy path: `sql`-manifest → bundle med korrekt provenance + body. - `tests/test_ingest_sql_loadbearing.py` (SQL-spesifikke sømmer, detach-RED): - `test_sql_generated_bundle_is_navigable_via_unchanged_okf` (speiler seam 3: `navigate_bundle` + `bundle_context` mot SQL-generert bundle; RØD hvis index-linking detaches). - `test_sql_null_renders_as_empty_not_none` (§5-load-bearing: RØD hvis None→`str()`). - `test_sql_real_preserves_float_form` (§5: `4200.0`→`"4200.0"`; RØD hvis int-coerced). - `test_sql_blob_fails_never_silent_coercion` (§5: BLOB→`IngestError`; RØD hvis silent str). - `test_sql_manifest_mapping_to_verdict_is_rejected` (§3-reservasjon fyrer på `sql`-source). - Oppdater 2 eksisterende: - `test_ingest_materialize.py::test_sql_source_has_no_connector_in_i2` → repekes til `http` (`test_http_source_has_no_connector` — den gjenstående I6-avvisningen; sql eksekverer nå). - `test_ingest_manifest.py::test_sql_and_http_variants_validate` — kommentar «sql → I4» oppdateres til at `file`+`sql` eksekverer, `http`→I6. - **Kjør → RØD** (read_sql finnes ikke; materialize avviser sql). ### S2 — Implementasjon (`ingest.py`) - Import: `os`, `sqlite3`, `from contextlib import closing`, `from urllib.parse import quote`. - `_sql_value_to_text(value) -> str` (rekkefølge: None → bool-guard → int → float → str → fail). - `read_sql(connection_ref, query, *, max_rows) -> tuple[list[str], list[list[str]]]`: env-resolusjon → fil-eksistens → ro-connection (`closing`) → `execute` → header fra `cursor.description` → streaming max_rows-tak → `_sql_value_to_text` per celle → wrap `sqlite3.Error/Warning` som `IngestError`. - `materialize`: erstatt `if not isinstance(..FileSource): raise` med dispatch: `http`→refuse (I6); `file`→root-resolusjon+`read_csv`; `sql`→`read_sql`. §8-logg per uttrekk. - Docstring-oppdatering: modul-docstring (SQL nå implementert, §5-tallregler pinnet); `render_table` parentes (SQL-verdier pre-konvertert til §5-tekst i `read_sql`). - **Kjør → GRØNN** (unit + loadbearing). ### S3 — Golden-fixture - `examples/ingest-golden-sql/`: - `fixture/portefolje.sqlite` — committet db (gen-script i denne prosjektmappa; schema i brief). Tabeller: `costs`(id INTEGER, item TEXT, amount REAL, note TEXT-nullable) med NULL + `4200.0`; `meta`(k TEXT, v TEXT) med pipe/backslash for escape-bevis. - `manifest.json` — `source.type:sql`, `connection_ref:"PORTEFOLJE_SQL_DSN"`, to uttrekk med ORDER BY. - `ingested-at.txt` — fast timestamp. - `expected-bundle/` — generert av `materialize`, HÅNDVERIFISERT mot §5 celle-for-celle, frosset. - `tests/test_ingest_golden_sql.py` — speiler `test_ingest_golden.py`; setter env-var til fixture-db (monkeypatch), asserterer bit-likhet + idempotent re-run. - **Kjør → GRØNN**. ### S4 — Detach-bevis (hver ny load-bearing, revertert) - null→str, float→int-cast, BLOB→str, index-link-detach, verdict-validator-fjern → bekreft RØD. ### S5 — Full verifisering + commit - `pytest -q` grønn · ruff+format+mypy grønne · `git diff --stat` (okf/shared/run urørt) · MAF-fri-guard grønn. - Overskriv STATE.md (I4 levert, NESTE = I5 gated søsken). Commits (Conventional, `(I4)`-scope): test(røde) → feat(impl) → test(golden+fixture) → docs(brief/plan) → evt. style(ruff). Push Forgejo. ## Verifisering (plan som helhet) Program-sluttkriteriets SQL-halvdel: golden bit-deterministisk fra delt spec, load-bearing røde ved detach, suite grønn uten nøkkel/nett, ingen artefakt påstår mer enn koden gjør (sqlite-fixture, ikke live-DB). Ingen §-endring i spec.