diff --git a/docs/rfc/0004-dataframe-and-blob.md b/docs/rfc/0004-dataframe-and-blob.md new file mode 100644 index 0000000..13af605 --- /dev/null +++ b/docs/rfc/0004-dataframe-and-blob.md @@ -0,0 +1,139 @@ +# RFC 0004 — `DataFrame` und `Blob`: Content-Layer vs. Multi-Format-Modell + +| Feld | Wert | +|-------------|--------------------------------------------------------------| +| Status | Proposed | +| Datum | 2026-06-29 | +| Autor | lepy | +| Komponente | `sdata/sclass/dataframe.py`, `sdata/sclass/blob.py` | +| Betrifft | ob/wie `DataFrame` auf `Blob` aufbaut (Folge von RFC 0003) | +| Validierung | Analyse/Designvorschlag — noch nicht implementiert | + +## 1. Zusammenfassung + +RFC 0003 hat `Blob` zur Content-/Integritäts-/Provenienz-Grundlage gemacht; +`FileReference` und `Image` bauen bereits darauf auf. Offen blieb **`DataFrame`**. + +Dieses RFC analysiert, ob `DataFrame` von `Blob` **erben** sollte. Befund: **nein** — +das `Blob`-Modell („**ein** fixer Content-Blob") passt schlecht auf das +`DataFrame`-Modell („eine **lebende** pandas-Tabelle, on-demand in **viele** Formate +kodierbar"). Empfohlen wird stattdessen **Komposition** (`DataFrame.as_blob(fmt)`) +plus optional ein **gemeinsamer Integritäts-/Provenienz-Mixin** — `DataFrame` gewinnt +den `Blob`-Mehrwert (Prüfsumme/`verify`/`size`, Provenienz-Metadaten, Content-Zugriff +in einem gewählten Format) **ohne** Basisklassenwechsel und **ohne** Bruch des +etablierten `to_dict`-Layouts. + +## 2. Gegenüberstellung der Modelle + +| Aspekt | `Blob` | `DataFrame` | +| ----------------- | ---------------------------------------- | ---------------------------------------------------- | +| Inhalt | **ein** fixer Blob (`bytes`/`uri`) | **lebende** pandas-`df` (mutierbar) | +| Speicherort | `self.data['content']` (serialisiert) | `self._df` (in-memory, nicht in `data`) | +| Formate | genau eines (der Blob selbst) | **viele**: Parquet/Arrow/Feather/CSV/dict/JSON-LD/HDF5/Data Package | +| `to_dict`-Layout | `data.content = {type,value,filetype}` | `data.parquet_bytes` (base64) + `data.column_metadata` | +| Identität/Hash | Hash **des** Blobs | Hash **wovon?** (Parquet? CSV? df?) — formatabhängig | +| Zusatz-Metadaten | `checksum`/`mime_type`/`license`/… | `column_metadata` (pro Spalte) + Dataset-Metadaten | + +Kernspannung: `Blob` setzt einen **einzigen, festen** Byte-Inhalt voraus; die +`DataFrame` hat **keinen** kanonischen Byte-Inhalt — erst die Wahl des Formats +erzeugt Bytes, und die Tabelle bleibt danach veränderbar. + +## 3. Was `DataFrame` von `Blob` gewinnen würde + +`sha256`/`md5`/`sha1`, `verify()`/`update_checksum()`, `size`, `exists()`, +`open()`/`write(uri)`, sowie die Provenienz-Felder (`checksum`, `mime_type`, +`license`, `source_uri`, `created`/`modified`, `publisher`) — alles nützlich, aber +**nicht** an eine Vererbung gebunden. + +## 4. Optionen + +### Option A — Volle Vererbung `DataFrame(Blob)` + +Die df-Standardserialisierung (Parquet) als `Blob`-Content ablegen. + +* **Pro:** „is-a Blob"; einheitliche Identität/Integrität. +* **Contra (gravierend):** + * **`to_dict`-Bruch:** `Blob` erwartet `data.content = {type,value,filetype}`; + `DataFrame.to_dict` nutzt `data.parquet_bytes`/`data.column_metadata`. Eine + Vereinheitlichung bricht **bestehende `.sjson`/dict-Artefakte** und alle + Round-Trips (Tests, evtl. gespeicherte Daten). + * **Lebende vs. fixe Daten:** der `Blob`-Content müsste bei **jeder** + df-Mutation neu erzeugt/invalidiert werden (Sync-Problem, Cache-Inkohärenz). + * **„ein Format" vs. „viele":** Parquet als Content bevorzugt ein Format künstlich; + CSV/Arrow/HDF5/Data Package sind gleichwertig. + * **Hash-Semantik:** `checksum` eines `DataFrame` ist mehrdeutig (Parquet ist nicht + deterministisch byte-gleich; CSV/Arrow ergeben andere Hashes). +* **Bewertung:** hohes Risiko, geringer Zusatznutzen → **nicht empfohlen.** + +### Option B — Gemeinsamer Integritäts-/Provenienz-Mixin + +Den Hash-/`verify`-/Provenienz-Teil aus `Blob` in einen wiederverwendbaren Mixin +ziehen (über einen `content_bytes`-Hook), den **`Blob` und `DataFrame`** nutzen. +`DataFrame.content_bytes` = Standardserialisierung (z. B. Parquet-Bytes). + +* **Pro:** `DataFrame` erhält `sha256`/`verify`/`size`/Provenienz **additiv**, ohne + Basiswechsel und ohne `to_dict`-Bruch. +* **Contra:** kein „is-a Blob"; leichte Refaktorierung von `Blob` nötig. + +### Option C — Komposition: `DataFrame.as_blob(fmt="parquet") -> Blob` + +Die df bei Bedarf als `Blob` über die gewählte Serialisierung **ausgeben** +(`bytes`-Content). Damit lässt sich eine Tabelle als binäres Asset behandeln +(speichern/übertragen/signieren/hashen) — in **jedem** Format. + +* **Pro:** sauber, additiv, **null** Bruchrisiko; respektiert das Multi-Format-Modell + (Format ist ein Parameter, nicht die Basisklasse); nutzt die schon vorhandenen + `to_parquet`/`to_arrow`/`to_csv`/… . +* **Contra:** kein „is-a Blob" (aber semantisch korrekt: eine Tabelle **ist** kein + einzelner Blob). + +### Option D — Status quo (getrennt) + +`DataFrame` bleibt `Base`. Klarstellung: `Blob` ist die Grundlage für +**single-content/binäre** Daten (Dateien/Bilder/Blobs); `DataFrame` ist ein +eigenständiges **tabellarisches** Modell mit eigener Multi-Format-Serialisierung. + +## 5. Empfehlung + +**Komposition (Option C) als primärer Weg, optional ergänzt um den Mixin (Option B); +keine Vererbung (Option A).** + +1. **`DataFrame.as_blob(fmt="parquet", content_type="bytes") -> Blob`** (klein, + additiv): erzeugt aus `to_parquet()`/`to_csv()`/`to_arrow-IPC`/… einen `Blob` mit + passendem `filetype`/`mime_type`. Damit erbt die Tabelle indirekt **alle** + Blob-Fähigkeiten (Hash/verify/size/write/open/Sidecar) in einem **gewählten** + Format. +2. *(Optional, später)* Integritäts-/Provenienz-**Mixin** aus `Blob` extrahieren und + in `DataFrame` einhängen, falls `df.sha256`/`df.verify()` **direkt** (ohne Umweg + über `as_blob`) gewünscht sind. Eigener PR, rein additiv. + +**Begriffliche Klärung der „Grundlage aller Datenformate" (RFC 0003):** `Blob` +liefert den **Content-/Integritäts-/Provenienz-Layer**. Container mit **einem** +Inhalt (`FileReference`, `Image`) **erben** davon; Container mit **lebenden, +mehrformatigen** Daten (`DataFrame`) **komponieren** damit (`as_blob`). „Grundlage" +heißt also *gemeinsamer Layer*, nicht *zwingende Basisklasse*. + +## 6. Kompatibilität / Migration + +* Option C ist strikt additiv: keine Änderung an `to_dict`/`from_dict`, keine + Basisklasse, keine bestehenden Artefakte betroffen. +* `as_blob` baut nur auf vorhandenen Serialisierern auf; `pyproject.toml`-`omit` + unverändert; optionale Backends (pyarrow) wie gehabt geguardet. + +## 7. Testplan (für die spätere Umsetzung von Option C) + +* `as_blob()` Default (Parquet): `blob.filetype`/`mime_type` korrekt; `blob.content_bytes` + == `df.to_parquet()`; `Blob`-`sha256`/`size`/`verify` funktionieren. +* `as_blob(fmt="csv")` / `fmt="arrow"`: jeweils Inhalt == entsprechende Serialisierung. +* Round-Trip-Skizze: `DataFrame.from_parquet_bytes(df.as_blob().content_bytes)` ≡ df. +* Unbekanntes `fmt` → `ValueError`. + +## 8. Risiken / offene Punkte + +* **Determinismus der Prüfsumme:** Parquet ist nicht garantiert byte-stabil + (Kompression/Metadaten-Reihenfolge). Für reproduzierbare Hashes ggf. CSV (mit + fixierten Optionen) als „canonical form" anbieten — zu spezifizieren. +* **`as_blob`-Default-Format:** Parquet (kompakt/typisiert) vs. CSV (deterministisch) + — Default festlegen. +* Falls später doch eine „is-a"-Beziehung gewünscht ist, müsste ein **getrenntes** + RFC den `to_dict`-Layout-Übergang inkl. Migration behandeln. diff --git a/mkdocs.yml b/mkdocs.yml index 793fd95..6cac146 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -69,4 +69,5 @@ nav: - "0001 — json1sqlitestore index naming": rfc/0001-json1sqlitestore-index-naming.md - "0002 — HDF5 DataFrame serialization": rfc/0002-hdf5-dataframe-serialization.md - "0003 — Blob as data foundation": rfc/0003-blob-as-data-foundation.md + - "0004 — DataFrame and Blob": rfc/0004-dataframe-and-blob.md - Releasing: releasing.md