portfolio-optimiser/.claude/projects/2026-07-04-i4-ingest-sql-maf/plan.md
Kjell Tore Guttormsen 1b7612b22d docs(i4): brief + plan + golden fixture builder (I4)
Session brief (scope, non-goals, pinned SQL type/number decisions, verification) + TDD plan + build_fixture.py (regenerates the sqlite golden; the committed db bytes need not reproduce — the golden compares the derived bundle).
2026-07-04 06:56:08 +02:00

4.6 KiB

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; sqlread_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.jsonsource.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.