# I4 — SQL-kilde på MAF-siden (brief) > **Økt-type:** kode-økt, offline, IKKE gatet (lokalt/offline; driftsmodellens klarsignal > dekker den). Andre kildetype i ingest-programmet, mot I1s polymorfe manifest-skjema UTEN > spec-endring. Speiler I2 (`file`/CSV) for `sql`. > > **Kilder:** [ingest-spec](../../../shared/ingest-spec.md) (normativ, FROSSET) · [målbilde > ingest](../../../docs/plan/2026-07-03-maalbilde-ingest-lag.md) · [sesjonsplan §I4](../../../docs/plan/2026-07-03-sesjonsplan-ingest.md). ## Mål (én lukket leveranse) sqlite-konnektor på MAF-siden mot det polymorfe manifest-skjemaet: `manifest (source.type: sql)` → read-only sqlite-uttrekk → materialisert OKF-bundle (samme §5–§7 provenance + §6 index som `file`), hele veien load-bearing- og golden-testet. `materialize()` går fra å avvise alt ≠ `FileSource` til å dispatche `FileSource → read_csv`, `SqlSource → read_sql` (`http` forblir I6-avvist). ## Suksesskriterier (hver = verifiserbar) 1. `read_sql(connection_ref, query, *, max_rows)` eksekverer et read-only SELECT mot en sqlite-fil resolvet fra env-varen `connection_ref` navner, med §5-typet celle-rendering. 2. `materialize()` materialiserer et `sql`-manifest til en bundle bit-identisk med en golden (`examples/ingest-golden-sql/`), idempotent ved re-run. 3. Nye load-bearing-tester (SQL-navigerbarhet + §5-type-sømmer) er RØDE ved detach (bevist). 4. Full suite grønn uten nøkkel/nettverk; `okf.py`/`shared/`/`run.py` urørt; ruff+format+mypy grønne; null modellkall. ## Non-goals (bygg IKKE) - Ingen spec-/commons-endring (spec FROSSET; oppdages drift → STOPP, egen GATET runde). - Ingen `http`-konnektor (I6), ingen `python -m`-CLI, ingen live-DB, ingen ORDER-BY-håndhevelse i kode (manifest-forfatters SHOULD, §4), ingen per-rad-filsplitting, ingen inkrementell re-ingest, ingen index-typing-RFC (åpen, utsatt). - Ingen søsken-repo-endring (I5, GATET). ## Låste beslutninger — SQL §5-type/tall-regler (delegert HIT av STATE; pinnes i kode+golden) STATE: «CSV-celle-verbatim-regelen utsatte tall-/type-håndtering eksplisitt til sql/I4 → SQL-kolonners type/tall-regler avgjøres HER.» Konsistente med §5, ikke en spec-endring: | Python-type (sqlite default) | §5-tekst | Begrunnelse | |---|---|---| | `None` (SQL NULL) | `""` (tom) | §5 «SQL NULL as the empty string» | | `int` (INTEGER) | `str(v)` plain decimal | §5 «integers in plain decimal» | | `float` (REAL) | `repr(v)` korteste round-trip | §5 «non-integral numbers … shortest round-trip decimal form»; `repr` er Pythons korteste round-trip (ekstrem magnitude → E-notasjon, akseptert som korteste form) | | `str` (TEXT) | verbatim (escapes i `render_table`) | §5 «text verbatim» | | `bool` | `IngestError` | sqlite emitter den ikke; guard hindrer `str(True)`→`"True"` silent-coercion | | `bytes` (BLOB) / annet | `IngestError` | §5 «any other value type MUST fail (never silent coercion)» | **Konnektor-regler:** - `connection_ref` = env-var-NAVN → verdi = **sti til sqlite-fil** (I4-scope: sqlite-fixture). Env-var usatt → `IngestError` fail-fast; fil mangler → `IngestError` (speiler `read_csv`). - Read-only: åpnes `file:{quote(path)}?mode=ro` med `uri=True` → et skrive-forsøk feiler på DB-nivå (§4 «SHOULD enforce read-only», robust — ikke skjør streng-parsing). - Single-statement: `Connection.execute` avviser flere statements naturlig (§4 «one statement»). - `max_rows`: streaming-tak, `IngestError` ved overskridelse (§8, aldri silent truncation — speiler `read_csv`). - sqlite3-feil (syntaks, skrive-forsøk, korrupt db) → wrappes som `IngestError` (fail-fast, ærlig — som `read_csv`s `FileNotFoundError`-wrapping). ## Antakelser (med test) - **A1: sqlite3 default-typing gir int/float/str/bytes/None** (ingen registrerte konvertere) → testet i `test_ingest_sql.py` (typet fixture-db, én rad per type). - **A2: `mode=ro` håndhever read-only** → test: skrive-query mot ro-connection raiser. - **A3: golden-db-bytene påvirker ikke bundle-determinismen** (kun DB-INNHOLD via query; env-sti lekker aldri inn i frontmatter) → golden bit-deterministisk uansett checkout-sti. ## Verifisering (kommandoer) - `uv run pytest -q` — 211+ passerer, ingen ny skip, ingen nett/nøkkel. - Detach-bevis (manuelt, revertert): bryt hver ny load-bearing-søm → bekreft RØD → revert. - `uv run ruff check . && uv run ruff format --check . && uv run mypy src` — grønne. - `git diff --stat` — `src/portfolio_optimiser/okf.py`, `shared/`, `src/portfolio_optimiser/run.py` IKKE i diffen. - MAF-fri: `test_ingest_module_is_maf_free_and_context_layer_pure` grønn (kun stdlib lagt til).