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).
72 lines
4.6 KiB
Markdown
72 lines
4.6 KiB
Markdown
# 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.
|